mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge fx-team to mozilla-central a=merge
This commit is contained in:
commit
d7cf50ea6d
@ -1653,6 +1653,7 @@ pref("shumway.disabled", true);
|
||||
pref("image.mem.max_decoded_image_kb", 256000);
|
||||
|
||||
pref("loop.enabled", true);
|
||||
pref("loop.screenshare.enabled", false);
|
||||
pref("loop.server", "https://loop.services.mozilla.com/v0");
|
||||
pref("loop.seenToS", "unseen");
|
||||
pref("loop.showPartnerLogo", true);
|
||||
|
@ -22,6 +22,37 @@ XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
|
||||
|
||||
var gTab;
|
||||
|
||||
// Taken from dom/media/tests/mochitest/head.js
|
||||
function isMacOSX10_6orOlder() {
|
||||
var is106orOlder = false;
|
||||
|
||||
if (navigator.platform.indexOf("Mac") == 0) {
|
||||
var version = Cc["@mozilla.org/system-info;1"]
|
||||
.getService(Ci.nsIPropertyBag2)
|
||||
.getProperty("version");
|
||||
// the next line is correct: Mac OS 10.6 corresponds to Darwin version 10.x !
|
||||
// Mac OS 10.7 is Darwin version 11.x. the |version| string we've got here
|
||||
// is the Darwin version.
|
||||
is106orOlder = (parseFloat(version) < 11.0);
|
||||
}
|
||||
return is106orOlder;
|
||||
}
|
||||
|
||||
// Screensharing is disabled on older platforms (WinXP and Mac 10.6).
|
||||
function isOldPlatform() {
|
||||
const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
|
||||
if (isMacOSX10_6orOlder() || isWinXP) {
|
||||
info(true, "Screensharing disabled for OSX10.6 and WinXP");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Linux prompts aren't working for screensharing.
|
||||
function isLinux() {
|
||||
return navigator.platform.indexOf("Linux") != -1;
|
||||
}
|
||||
|
||||
var gObservedTopics = {};
|
||||
function observer(aSubject, aTopic, aData) {
|
||||
if (!(aTopic in gObservedTopics))
|
||||
@ -97,16 +128,41 @@ function expectNoObserverCalled() {
|
||||
gObservedTopics = {};
|
||||
}
|
||||
|
||||
function promiseMessage(aMessage, aAction) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
content.addEventListener("message", function messageListener(event) {
|
||||
content.removeEventListener("message", messageListener);
|
||||
is(event.data, aMessage, "received " + aMessage);
|
||||
if (event.data == aMessage)
|
||||
deferred.resolve();
|
||||
else
|
||||
deferred.reject();
|
||||
});
|
||||
|
||||
if (aAction)
|
||||
aAction();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function getMediaCaptureState() {
|
||||
let hasVideo = {};
|
||||
let hasAudio = {};
|
||||
MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio);
|
||||
let hasScreenShare = {};
|
||||
let hasWindowShare = {};
|
||||
MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio,
|
||||
hasScreenShare, hasWindowShare);
|
||||
if (hasVideo.value && hasAudio.value)
|
||||
return "CameraAndMicrophone";
|
||||
if (hasVideo.value)
|
||||
return "Camera";
|
||||
if (hasAudio.value)
|
||||
return "Microphone";
|
||||
if (hasScreenShare)
|
||||
return "Screen";
|
||||
if (hasWindowShare)
|
||||
return "Window";
|
||||
return "none";
|
||||
}
|
||||
|
||||
@ -173,6 +229,7 @@ registerCleanupFunction(function() {
|
||||
Services.prefs.setCharPref(PREF_LOOP_CSP, originalLoopCsp);
|
||||
});
|
||||
|
||||
const permissionError = "error: PermissionDeniedError: The user did not grant permission for the operation.";
|
||||
|
||||
let gTests = [
|
||||
|
||||
@ -207,6 +264,46 @@ let gTests = [
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "getUserMedia about:loopconversation should prompt for window sharing",
|
||||
run: function checkShareScreenLoop() {
|
||||
if (isOldPlatform() || isLinux()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Services.prefs.setCharPref(PREF_LOOP_CSP, "default-src 'unsafe-inline'");
|
||||
|
||||
let classID = Cc["@mozilla.org/uuid-generator;1"]
|
||||
.getService(Ci.nsIUUIDGenerator).generateUUID();
|
||||
registrar.registerFactory(classID, "",
|
||||
"@mozilla.org/network/protocol/about;1?what=loopconversation",
|
||||
factory);
|
||||
|
||||
yield loadPage("about:loopconversation");
|
||||
|
||||
yield promiseObserverCalled("getUserMedia:request", () => {
|
||||
info("requesting screen");
|
||||
content.wrappedJSObject.requestDevice(false, true, "window");
|
||||
});
|
||||
// Wait for the devices to actually be captured and running before
|
||||
// proceeding.
|
||||
yield promisePopupNotification("webRTC-shareDevices");
|
||||
|
||||
isnot(getMediaCaptureState(), "Window",
|
||||
"expected camera and microphone not to be shared");
|
||||
|
||||
yield promiseMessage(permissionError, () => {
|
||||
PopupNotifications.panel.firstChild.button.click();
|
||||
});
|
||||
|
||||
expectObserverCalled("getUserMedia:response:deny");
|
||||
expectObserverCalled("recording-window-ended");
|
||||
|
||||
registrar.unregisterFactory(classID, factory);
|
||||
Services.prefs.setCharPref(PREF_LOOP_CSP, originalLoopCsp);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "getUserMedia about:evil should prompt",
|
||||
run: function checkAudioVideoNonLoop() {
|
||||
@ -236,6 +333,8 @@ function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true);
|
||||
// Ensure this is always true
|
||||
Services.prefs.setBoolPref("media.getusermedia.screensharing.enabled", true);
|
||||
|
||||
gTab = gBrowser.addTab();
|
||||
gBrowser.selectedTab = gTab;
|
||||
|
@ -24,11 +24,17 @@ function message(m) {
|
||||
|
||||
var gStream;
|
||||
|
||||
function requestDevice(aAudio, aVideo) {
|
||||
function requestDevice(aAudio, aVideo, aShare) {
|
||||
var opts = {video: aVideo, audio: aAudio};
|
||||
if (useFakeStreams) {
|
||||
if (aShare) {
|
||||
opts.video = {
|
||||
mozMediaSource: aShare,
|
||||
mediaSource: aShare
|
||||
}
|
||||
} else if (useFakeStreams) {
|
||||
opts.fake = true;
|
||||
}
|
||||
|
||||
window.navigator.mozGetUserMedia(opts, function(stream) {
|
||||
gStream = stream;
|
||||
message("ok");
|
||||
|
@ -28,7 +28,6 @@
|
||||
<script type="text/javascript" src="loop/shared/js/utils.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/actions.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/validate.js"></script>
|
||||
@ -41,6 +40,7 @@
|
||||
<script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/feedbackStore.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/feedbackViews.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
|
||||
|
@ -42,7 +42,8 @@ loop.conversation = (function(mozL10n) {
|
||||
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
|
||||
.isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
|
||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
@ -78,6 +79,7 @@ loop.conversation = (function(mozL10n) {
|
||||
case "room": {
|
||||
return (React.createElement(DesktopRoomConversationView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
mozLoop: this.props.mozLoop,
|
||||
roomStore: this.props.roomStore}
|
||||
));
|
||||
}
|
||||
@ -189,7 +191,8 @@ loop.conversation = (function(mozL10n) {
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
dispatcher: dispatcher,
|
||||
sdk: window.OT}
|
||||
sdk: window.OT,
|
||||
mozLoop: navigator.mozLoop}
|
||||
), document.querySelector('#main'));
|
||||
|
||||
dispatcher.dispatch(new sharedActions.GetWindowData({
|
||||
|
@ -42,7 +42,8 @@ loop.conversation = (function(mozL10n) {
|
||||
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
|
||||
.isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
|
||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
@ -78,6 +79,7 @@ loop.conversation = (function(mozL10n) {
|
||||
case "room": {
|
||||
return (<DesktopRoomConversationView
|
||||
dispatcher={this.props.dispatcher}
|
||||
mozLoop={this.props.mozLoop}
|
||||
roomStore={this.props.roomStore}
|
||||
/>);
|
||||
}
|
||||
@ -190,6 +192,7 @@ loop.conversation = (function(mozL10n) {
|
||||
conversation={conversation}
|
||||
dispatcher={dispatcher}
|
||||
sdk={window.OT}
|
||||
mozLoop={navigator.mozLoop}
|
||||
/>, document.querySelector('#main'));
|
||||
|
||||
dispatcher.dispatch(new sharedActions.GetWindowData({
|
||||
|
@ -898,7 +898,7 @@ loop.conversationViews = (function(mozL10n) {
|
||||
React.createElement("div", {className: "conversation"},
|
||||
React.createElement("div", {className: "media nested"},
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: "video_inner remote"})
|
||||
React.createElement("div", {className: "video_inner remote remote-stream"})
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses})
|
||||
),
|
||||
|
@ -898,7 +898,7 @@ loop.conversationViews = (function(mozL10n) {
|
||||
<div className="conversation">
|
||||
<div className="media nested">
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className="video_inner remote"></div>
|
||||
<div className="video_inner remote remote-stream"></div>
|
||||
</div>
|
||||
<div className={localStreamClasses}></div>
|
||||
</div>
|
||||
|
@ -14,6 +14,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
var sharedViews = loop.shared.views;
|
||||
|
||||
/**
|
||||
@ -169,7 +170,8 @@ loop.roomViews = (function(mozL10n) {
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
_renderInvitationOverlay: function() {
|
||||
@ -193,6 +195,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
publishVideo: !this.state.videoMuted
|
||||
}),
|
||||
getLocalElementFunc: this._getElement.bind(this, ".local"),
|
||||
getScreenShareElementFunc: this._getElement.bind(this, ".screen"),
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote")
|
||||
}));
|
||||
}
|
||||
@ -238,6 +241,11 @@ loop.roomViews = (function(mozL10n) {
|
||||
"room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
var screenShareData = {
|
||||
state: this.state.screenSharingState,
|
||||
visible: this.props.mozLoop.getLoopPref("screenshare.enabled")
|
||||
};
|
||||
|
||||
switch(this.state.roomState) {
|
||||
case ROOM_STATES.FAILED:
|
||||
case ROOM_STATES.FULL: {
|
||||
@ -266,15 +274,18 @@ loop.roomViews = (function(mozL10n) {
|
||||
React.createElement("div", {className: "conversation room-conversation"},
|
||||
React.createElement("div", {className: "media nested"},
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: "video_inner remote"})
|
||||
React.createElement("div", {className: "video_inner remote remote-stream"})
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses})
|
||||
React.createElement("div", {className: localStreamClasses}),
|
||||
React.createElement("div", {className: "screen hide"})
|
||||
),
|
||||
React.createElement(sharedViews.ConversationToolbar, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
video: {enabled: !this.state.videoMuted, visible: true},
|
||||
audio: {enabled: !this.state.audioMuted, visible: true},
|
||||
publishStream: this.publishStream,
|
||||
hangup: this.leaveRoom})
|
||||
hangup: this.leaveRoom,
|
||||
screenShare: screenShareData})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -14,6 +14,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
var sharedViews = loop.shared.views;
|
||||
|
||||
/**
|
||||
@ -169,7 +170,8 @@ loop.roomViews = (function(mozL10n) {
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
_renderInvitationOverlay: function() {
|
||||
@ -193,6 +195,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
publishVideo: !this.state.videoMuted
|
||||
}),
|
||||
getLocalElementFunc: this._getElement.bind(this, ".local"),
|
||||
getScreenShareElementFunc: this._getElement.bind(this, ".screen"),
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote")
|
||||
}));
|
||||
}
|
||||
@ -238,6 +241,11 @@ loop.roomViews = (function(mozL10n) {
|
||||
"room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
var screenShareData = {
|
||||
state: this.state.screenSharingState,
|
||||
visible: this.props.mozLoop.getLoopPref("screenshare.enabled")
|
||||
};
|
||||
|
||||
switch(this.state.roomState) {
|
||||
case ROOM_STATES.FAILED:
|
||||
case ROOM_STATES.FULL: {
|
||||
@ -266,15 +274,18 @@ loop.roomViews = (function(mozL10n) {
|
||||
<div className="conversation room-conversation">
|
||||
<div className="media nested">
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className="video_inner remote"></div>
|
||||
<div className="video_inner remote remote-stream"></div>
|
||||
</div>
|
||||
<div className={localStreamClasses}></div>
|
||||
<div className="screen hide"></div>
|
||||
</div>
|
||||
<sharedViews.ConversationToolbar
|
||||
dispatcher={this.props.dispatcher}
|
||||
video={{enabled: !this.state.videoMuted, visible: true}}
|
||||
audio={{enabled: !this.state.audioMuted, visible: true}}
|
||||
publishStream={this.publishStream}
|
||||
hangup={this.leaveRoom} />
|
||||
hangup={this.leaveRoom}
|
||||
screenShare={screenShareData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,6 +43,11 @@
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.btn-screen-share-entry {
|
||||
float: right !important;
|
||||
border-left: 1px solid #5a5a5a;
|
||||
}
|
||||
|
||||
.conversation-toolbar-btn-box {
|
||||
border-right: 1px solid #5a5a5a;
|
||||
}
|
||||
@ -147,11 +152,11 @@
|
||||
}
|
||||
|
||||
/* Common media control buttons behavior */
|
||||
.conversation-toolbar .media-control {
|
||||
.conversation-toolbar .transparent-button {
|
||||
background-color: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
.conversation-toolbar .media-control:hover {
|
||||
.conversation-toolbar .transparent-button:hover {
|
||||
background-color: rgba(255,255,255,.35);
|
||||
opacity: 1;
|
||||
}
|
||||
@ -192,6 +197,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Screen share button */
|
||||
.btn-screen-share {
|
||||
/* XXX Replace this with the real button: bug 1126286 */
|
||||
background-image: url(../img/video-inverse-14x14.png);
|
||||
}
|
||||
|
||||
.btn-screen-share.active {
|
||||
background-color: #6CB23E;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-screen-share.disabled {
|
||||
/* XXX Add css here for disabled state: bug 1126286 */
|
||||
}
|
||||
|
||||
.fx-embedded .remote_wrapper {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
@ -211,7 +231,7 @@
|
||||
|
||||
/* Side by side video elements */
|
||||
|
||||
.conversation .media.side-by-side .remote {
|
||||
.conversation .media.side-by-side .remote-stream {
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
@ -489,7 +509,7 @@
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.conversation .media.nested .remote {
|
||||
.conversation .media.nested .remote-stream {
|
||||
display: inline-block;
|
||||
position: absolute; /* workaround for lack of object-fit; see bug 1020445 */
|
||||
width: 100%;
|
||||
|
@ -160,6 +160,9 @@ loop.shared.actions = (function() {
|
||||
publisherConfig: Object,
|
||||
// The local stream element
|
||||
getLocalElementFunc: Function,
|
||||
// The screen share element; optional until all conversation
|
||||
// types support it.
|
||||
// getScreenShareElementFunc: Function,
|
||||
// The remote stream element
|
||||
getRemoteElementFunc: Function
|
||||
}),
|
||||
@ -195,6 +198,33 @@ loop.shared.actions = (function() {
|
||||
enabled: Boolean
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to start a screen share.
|
||||
*/
|
||||
StartScreenShare: Action.define("startScreenShare", {
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to end a screen share.
|
||||
*/
|
||||
EndScreenShare: Action.define("endScreenShare", {
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to notifiy that screen sharing is active or not.
|
||||
*/
|
||||
ScreenSharingState: Action.define("screenSharingState", {
|
||||
// One of loop.shared.utils.SCREEN_SHARE_STATES.
|
||||
state: String
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to notify that a shared screen is being received (or not).
|
||||
*/
|
||||
ReceivingScreenShare: Action.define("receivingScreenShare", {
|
||||
receiving: Boolean
|
||||
}),
|
||||
|
||||
/**
|
||||
* Creates a new room.
|
||||
* XXX: should move to some roomActions module - refs bug 1079284
|
||||
|
@ -12,12 +12,14 @@ loop.store.ActiveRoomStore = (function() {
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
|
||||
// Error numbers taken from
|
||||
// https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json
|
||||
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
|
||||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
|
||||
/**
|
||||
* Active room store.
|
||||
*
|
||||
@ -70,7 +72,9 @@ loop.store.ActiveRoomStore = (function() {
|
||||
// anyone is not considered as 'used'
|
||||
used: false,
|
||||
localVideoDimensions: {},
|
||||
remoteVideoDimensions: {}
|
||||
remoteVideoDimensions: {},
|
||||
screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
|
||||
receivingScreenShare: false
|
||||
};
|
||||
},
|
||||
|
||||
@ -117,6 +121,8 @@ loop.store.ActiveRoomStore = (function() {
|
||||
"connectedToSdkServers",
|
||||
"connectionFailure",
|
||||
"setMute",
|
||||
"screenSharingState",
|
||||
"receivingScreenShare",
|
||||
"remotePeerDisconnected",
|
||||
"remotePeerConnected",
|
||||
"windowUnload",
|
||||
@ -369,6 +375,20 @@ loop.store.ActiveRoomStore = (function() {
|
||||
this.setStoreState(muteState);
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to note the current screensharing state.
|
||||
*/
|
||||
screenSharingState: function(actionData) {
|
||||
this.setStoreState({screenSharingState: actionData.state});
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to note the current state of receiving screenshare data.
|
||||
*/
|
||||
receivingScreenShare: function(actionData) {
|
||||
this.setStoreState({receivingScreenShare: actionData.receiving});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles recording when a remote peer has connected to the servers.
|
||||
*/
|
||||
|
@ -259,7 +259,8 @@ loop.shared.mixins = (function() {
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the dimensions of the remote video stream.
|
||||
* Retrieve the dimensions of the active remote video stream. This assumes
|
||||
* that if screens are being shared, the remote camera stream is hidden.
|
||||
* Example output:
|
||||
* {
|
||||
* width: 680,
|
||||
@ -270,6 +271,8 @@ loop.shared.mixins = (function() {
|
||||
* offsetY: 0
|
||||
* }
|
||||
*
|
||||
* Note: This expects a class on the element that has the name "remote" or the
|
||||
* same name as the possible video types (currently only "screen").
|
||||
* Note: Once we support multiple remote video streams, this function will
|
||||
* need to be updated.
|
||||
* @return {Object} contains the remote stream dimension properties of its
|
||||
@ -320,7 +323,7 @@ loop.shared.mixins = (function() {
|
||||
|
||||
// Calculate the size of each individual letter- or pillarbox for convenience.
|
||||
remoteVideoDimensions.offsetX = remoteVideoDimensions.width -
|
||||
remoteVideoDimensions.streamWidth
|
||||
remoteVideoDimensions.streamWidth;
|
||||
if (remoteVideoDimensions.offsetX > 0) {
|
||||
remoteVideoDimensions.offsetX /= 2;
|
||||
}
|
||||
@ -351,18 +354,22 @@ loop.shared.mixins = (function() {
|
||||
this._bufferedUpdateVideo = null;
|
||||
var localStreamParent = this._getElement(".local .OT_publisher");
|
||||
var remoteStreamParent = this._getElement(".remote .OT_subscriber");
|
||||
var screenShareStreamParent = this._getElement('.screen .OT_subscriber');
|
||||
if (localStreamParent) {
|
||||
localStreamParent.style.width = "100%";
|
||||
}
|
||||
if (remoteStreamParent) {
|
||||
remoteStreamParent.style.height = "100%";
|
||||
}
|
||||
if (screenShareStreamParent) {
|
||||
screenShareStreamParent.style.height = "100%";
|
||||
}
|
||||
|
||||
// Update the position and dimensions of the containers of local video
|
||||
// streams, if necessary. The consumer of this mixin should implement the
|
||||
// actual updating mechanism.
|
||||
Object.keys(this._videoDimensionsCache.local).forEach(function(videoType) {
|
||||
var ratio = this._videoDimensionsCache.local[videoType].aspectRatio
|
||||
var ratio = this._videoDimensionsCache.local[videoType].aspectRatio;
|
||||
if (videoType == "camera" && this.updateLocalCameraPosition) {
|
||||
this.updateLocalCameraPosition(ratio);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ loop.OTSdkDriver = (function() {
|
||||
var sharedActions = loop.shared.actions;
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
var STREAM_PROPERTIES = loop.shared.utils.STREAM_PROPERTIES;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
|
||||
/**
|
||||
* This is a wrapper for the OT sdk. It is used to translate the SDK events into
|
||||
@ -30,11 +31,21 @@ loop.OTSdkDriver = (function() {
|
||||
|
||||
this.dispatcher.register(this, [
|
||||
"setupStreamElements",
|
||||
"setMute"
|
||||
"setMute",
|
||||
"startScreenShare",
|
||||
"endScreenShare"
|
||||
]);
|
||||
};
|
||||
|
||||
OTSdkDriver.prototype = {
|
||||
/**
|
||||
* Clones the publisher config into a new object, as the sdk modifies the
|
||||
* properties object.
|
||||
*/
|
||||
_getCopyPublisherConfig: function() {
|
||||
return _.extend({}, this.publisherConfig);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the setupStreamElements action. Saves the required data and
|
||||
* kicks off the initialising of the publisher.
|
||||
@ -44,6 +55,7 @@ loop.OTSdkDriver = (function() {
|
||||
*/
|
||||
setupStreamElements: function(actionData) {
|
||||
this.getLocalElement = actionData.getLocalElementFunc;
|
||||
this.getScreenShareElementFunc = actionData.getScreenShareElementFunc;
|
||||
this.getRemoteElement = actionData.getRemoteElementFunc;
|
||||
this.publisherConfig = actionData.publisherConfig;
|
||||
|
||||
@ -51,7 +63,7 @@ loop.OTSdkDriver = (function() {
|
||||
// the initial connect of the session. This saves time when setting up
|
||||
// the media.
|
||||
this.publisher = this.sdk.initPublisher(this.getLocalElement(),
|
||||
this.publisherConfig);
|
||||
this._getCopyPublisherConfig());
|
||||
this.publisher.on("streamCreated", this._onLocalStreamCreated.bind(this));
|
||||
this.publisher.on("accessAllowed", this._onPublishComplete.bind(this));
|
||||
this.publisher.on("accessDenied", this._onPublishDenied.bind(this));
|
||||
@ -74,6 +86,41 @@ loop.OTSdkDriver = (function() {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates a screen sharing publisher.
|
||||
*/
|
||||
startScreenShare: function() {
|
||||
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.PENDING
|
||||
}));
|
||||
|
||||
var config = this._getCopyPublisherConfig();
|
||||
// This is temporary until we get a sharing type selector
|
||||
config.videoSource = "window";
|
||||
|
||||
this.screenshare = this.sdk.initPublisher(this.getScreenShareElementFunc(),
|
||||
config);
|
||||
this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
|
||||
this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ends an active screenshare session.
|
||||
*/
|
||||
endScreenShare: function() {
|
||||
if (!this.screenshare) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.session.unpublish(this.screenshare);
|
||||
this.screenshare.off("accessAllowed accessDenied");
|
||||
this.screenshare.destroy();
|
||||
delete this.screenshare;
|
||||
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.INACTIVE
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Connects a session for the SDK, listening to the required events.
|
||||
*
|
||||
@ -89,6 +136,7 @@ loop.OTSdkDriver = (function() {
|
||||
|
||||
this.session.on("connectionCreated", this._onConnectionCreated.bind(this));
|
||||
this.session.on("streamCreated", this._onRemoteStreamCreated.bind(this));
|
||||
this.session.on("streamDestroyed", this._onRemoteStreamDestroyed.bind(this));
|
||||
this.session.on("connectionDestroyed",
|
||||
this._onConnectionDestroyed.bind(this));
|
||||
this.session.on("sessionDisconnected",
|
||||
@ -104,6 +152,8 @@ loop.OTSdkDriver = (function() {
|
||||
* Disconnects the sdk session.
|
||||
*/
|
||||
disconnectSession: function() {
|
||||
this.endScreenShare();
|
||||
|
||||
if (this.session) {
|
||||
this.session.off("streamCreated streamDestroyed connectionDestroyed " +
|
||||
"sessionDisconnected streamPropertyChanged");
|
||||
@ -231,6 +281,30 @@ loop.OTSdkDriver = (function() {
|
||||
this.dispatcher.dispatch(new sharedActions.RemotePeerConnected());
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles when a remote screen share is created, subscribing to
|
||||
* the stream, and notifying the stores that a share is being
|
||||
* received.
|
||||
*
|
||||
* @param {Stream} stream The SDK Stream:
|
||||
* https://tokbox.com/opentok/libraries/client/js/reference/Stream.html
|
||||
*/
|
||||
_handleRemoteScreenShareCreated: function(stream) {
|
||||
if (!this.getScreenShareElementFunc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Let the stores know first so they can update the display.
|
||||
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
|
||||
receiving: true
|
||||
}));
|
||||
|
||||
var remoteElement = this.getScreenShareElementFunc();
|
||||
|
||||
this.session.subscribe(stream,
|
||||
remoteElement, this._getCopyPublisherConfig());
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the event when the remote stream is created.
|
||||
*
|
||||
@ -246,8 +320,15 @@ loop.OTSdkDriver = (function() {
|
||||
}));
|
||||
}
|
||||
|
||||
if (event.stream.videoType === "screen") {
|
||||
this._handleRemoteScreenShareCreated(event.stream);
|
||||
return;
|
||||
}
|
||||
|
||||
var remoteElement = this.getRemoteElement();
|
||||
|
||||
this.session.subscribe(event.stream,
|
||||
this.getRemoteElement(), this.publisherConfig);
|
||||
remoteElement, this._getCopyPublisherConfig());
|
||||
|
||||
this._subscribedRemoteStream = true;
|
||||
if (this._checkAllStreamsConnected()) {
|
||||
@ -258,7 +339,7 @@ loop.OTSdkDriver = (function() {
|
||||
/**
|
||||
* Handles the event when the local stream is created.
|
||||
*
|
||||
* @param {StreamEvent} event The event details:
|
||||
* @param {StreamEvent} event The event details:
|
||||
* https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
|
||||
*/
|
||||
_onLocalStreamCreated: function(event) {
|
||||
@ -271,6 +352,25 @@ loop.OTSdkDriver = (function() {
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Handles the event when the remote stream is destroyed.
|
||||
*
|
||||
* @param {StreamEvent} event The event details:
|
||||
* https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
|
||||
*/
|
||||
_onRemoteStreamDestroyed: function(event) {
|
||||
if (event.stream.videoType !== "screen") {
|
||||
return;
|
||||
}
|
||||
|
||||
// All we need to do is notify the store we're no longer receiving,
|
||||
// the sdk should do the rest.
|
||||
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
|
||||
receiving: false
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called from the sdk when the media access dialog is opened.
|
||||
* Prevents the default action, to prevent the SDK's "allow access"
|
||||
@ -347,6 +447,25 @@ loop.OTSdkDriver = (function() {
|
||||
_checkAllStreamsConnected: function() {
|
||||
return this._publishedLocalStream &&
|
||||
this._subscribedRemoteStream;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a screenshare is complete, publishes it to the session.
|
||||
*/
|
||||
_onScreenShareGranted: function() {
|
||||
this.session.publish(this.screenshare);
|
||||
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.ACTIVE
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a screenshare is denied. Notifies the other stores.
|
||||
*/
|
||||
_onScreenShareDenied: function() {
|
||||
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.INACTIVE
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -48,6 +48,13 @@ loop.shared.utils = (function(mozL10n) {
|
||||
HAS_VIDEO: "hasVideo"
|
||||
};
|
||||
|
||||
var SCREEN_SHARE_STATES = {
|
||||
INACTIVE: "ss-inactive",
|
||||
// Pending is when the user is being prompted, aka gUM in progress.
|
||||
PENDING: "ss-pending",
|
||||
ACTIVE: "ss-active"
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a given date into an l10n-friendly string.
|
||||
*
|
||||
@ -145,6 +152,7 @@ loop.shared.utils = (function(mozL10n) {
|
||||
REST_ERRNOS: REST_ERRNOS,
|
||||
WEBSOCKET_REASONS: WEBSOCKET_REASONS,
|
||||
STREAM_PROPERTIES: STREAM_PROPERTIES,
|
||||
SCREEN_SHARE_STATES: SCREEN_SHARE_STATES,
|
||||
Helper: Helper,
|
||||
composeCallUrlEmail: composeCallUrlEmail,
|
||||
formatDate: formatDate,
|
||||
|
@ -8,11 +8,13 @@
|
||||
/* global loop:true, React */
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.views = (function(_, OT, l10n) {
|
||||
loop.shared.views = (function(_, l10n) {
|
||||
"use strict";
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedModels = loop.shared.models;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
|
||||
/**
|
||||
* Media control button.
|
||||
@ -46,6 +48,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
var classesObj = {
|
||||
"btn": true,
|
||||
"media-control": true,
|
||||
"transparent-button": true,
|
||||
"local-media": this.props.scope === "local",
|
||||
"muted": !this.props.enabled,
|
||||
"hide": !this.props.visible
|
||||
@ -72,6 +75,60 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Screen sharing control button.
|
||||
*
|
||||
* Required props:
|
||||
* - {loop.Dispatcher} dispatcher The dispatcher instance
|
||||
* - {Boolean} visible Set to true to display the button
|
||||
* - {String} state One of the screen sharing states, see
|
||||
* loop.shared.utils.SCREEN_SHARE_STATES
|
||||
*/
|
||||
var ScreenShareControlButton = React.createClass({displayName: "ScreenShareControlButton",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
visible: React.PropTypes.bool.isRequired,
|
||||
state: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
handleClick: function() {
|
||||
if (this.props.state === SCREEN_SHARE_STATES.ACTIVE) {
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.EndScreenShare({}));
|
||||
} else {
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.StartScreenShare({}));
|
||||
}
|
||||
},
|
||||
|
||||
_getTitle: function() {
|
||||
var prefix = this.props.state === SCREEN_SHARE_STATES.ACTIVE ?
|
||||
"active" : "inactive";
|
||||
|
||||
return l10n.get(prefix + "_screenshare_button_title");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var screenShareClasses = React.addons.classSet({
|
||||
"btn": true,
|
||||
"btn-screen-share": true,
|
||||
"transparent-button": true,
|
||||
"active": this.props.state === SCREEN_SHARE_STATES.ACTIVE,
|
||||
"disabled": this.props.state === SCREEN_SHARE_STATES.PENDING
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("button", {className: screenShareClasses,
|
||||
onClick: this.handleClick,
|
||||
title: this._getTitle()})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation controls.
|
||||
*/
|
||||
@ -80,13 +137,16 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
return {
|
||||
video: {enabled: true, visible: true},
|
||||
audio: {enabled: true, visible: true},
|
||||
screenShare: {state: SCREEN_SHARE_STATES.INACTIVE, visible: false},
|
||||
enableHangup: true
|
||||
};
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
video: React.PropTypes.object.isRequired,
|
||||
audio: React.PropTypes.object.isRequired,
|
||||
screenShare: React.PropTypes.object,
|
||||
hangup: React.PropTypes.func.isRequired,
|
||||
publishStream: React.PropTypes.func.isRequired,
|
||||
hangupButtonLabel: React.PropTypes.string,
|
||||
@ -110,7 +170,6 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
return (
|
||||
React.createElement("ul", {className: "conversation-toolbar"},
|
||||
React.createElement("li", {className: "conversation-toolbar-btn-box btn-hangup-entry"},
|
||||
@ -131,6 +190,11 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
enabled: this.props.audio.enabled,
|
||||
visible: this.props.audio.visible,
|
||||
scope: "local", type: "audio"})
|
||||
),
|
||||
React.createElement("li", {className: "conversation-toolbar-btn-box btn-screen-share-entry"},
|
||||
React.createElement(ScreenShareControlButton, {dispatcher: this.props.dispatcher,
|
||||
visible: this.props.screenShare.visible,
|
||||
state: this.props.screenShare.state})
|
||||
)
|
||||
)
|
||||
);
|
||||
@ -294,7 +358,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
React.createElement("div", {className: "conversation"},
|
||||
React.createElement("div", {className: "media nested"},
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: "video_inner remote"})
|
||||
React.createElement("div", {className: "video_inner remote remote-stream"})
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses})
|
||||
),
|
||||
@ -461,6 +525,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
ConversationView: ConversationView,
|
||||
ConversationToolbar: ConversationToolbar,
|
||||
MediaControlButton: MediaControlButton,
|
||||
ScreenShareControlButton: ScreenShareControlButton,
|
||||
NotificationListView: NotificationListView
|
||||
};
|
||||
})(_, window.OT, navigator.mozL10n || document.mozL10n);
|
||||
})(_, navigator.mozL10n || document.mozL10n);
|
||||
|
@ -8,11 +8,13 @@
|
||||
/* global loop:true, React */
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.views = (function(_, OT, l10n) {
|
||||
loop.shared.views = (function(_, l10n) {
|
||||
"use strict";
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedModels = loop.shared.models;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
|
||||
/**
|
||||
* Media control button.
|
||||
@ -46,6 +48,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
var classesObj = {
|
||||
"btn": true,
|
||||
"media-control": true,
|
||||
"transparent-button": true,
|
||||
"local-media": this.props.scope === "local",
|
||||
"muted": !this.props.enabled,
|
||||
"hide": !this.props.visible
|
||||
@ -72,6 +75,60 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Screen sharing control button.
|
||||
*
|
||||
* Required props:
|
||||
* - {loop.Dispatcher} dispatcher The dispatcher instance
|
||||
* - {Boolean} visible Set to true to display the button
|
||||
* - {String} state One of the screen sharing states, see
|
||||
* loop.shared.utils.SCREEN_SHARE_STATES
|
||||
*/
|
||||
var ScreenShareControlButton = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
visible: React.PropTypes.bool.isRequired,
|
||||
state: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
handleClick: function() {
|
||||
if (this.props.state === SCREEN_SHARE_STATES.ACTIVE) {
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.EndScreenShare({}));
|
||||
} else {
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.StartScreenShare({}));
|
||||
}
|
||||
},
|
||||
|
||||
_getTitle: function() {
|
||||
var prefix = this.props.state === SCREEN_SHARE_STATES.ACTIVE ?
|
||||
"active" : "inactive";
|
||||
|
||||
return l10n.get(prefix + "_screenshare_button_title");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var screenShareClasses = React.addons.classSet({
|
||||
"btn": true,
|
||||
"btn-screen-share": true,
|
||||
"transparent-button": true,
|
||||
"active": this.props.state === SCREEN_SHARE_STATES.ACTIVE,
|
||||
"disabled": this.props.state === SCREEN_SHARE_STATES.PENDING
|
||||
});
|
||||
|
||||
return (
|
||||
<button className={screenShareClasses}
|
||||
onClick={this.handleClick}
|
||||
title={this._getTitle()}></button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation controls.
|
||||
*/
|
||||
@ -80,13 +137,16 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
return {
|
||||
video: {enabled: true, visible: true},
|
||||
audio: {enabled: true, visible: true},
|
||||
screenShare: {state: SCREEN_SHARE_STATES.INACTIVE, visible: false},
|
||||
enableHangup: true
|
||||
};
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
video: React.PropTypes.object.isRequired,
|
||||
audio: React.PropTypes.object.isRequired,
|
||||
screenShare: React.PropTypes.object,
|
||||
hangup: React.PropTypes.func.isRequired,
|
||||
publishStream: React.PropTypes.func.isRequired,
|
||||
hangupButtonLabel: React.PropTypes.string,
|
||||
@ -110,7 +170,6 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
return (
|
||||
<ul className="conversation-toolbar">
|
||||
<li className="conversation-toolbar-btn-box btn-hangup-entry">
|
||||
@ -132,6 +191,11 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
visible={this.props.audio.visible}
|
||||
scope="local" type="audio" />
|
||||
</li>
|
||||
<li className="conversation-toolbar-btn-box btn-screen-share-entry">
|
||||
<ScreenShareControlButton dispatcher={this.props.dispatcher}
|
||||
visible={this.props.screenShare.visible}
|
||||
state={this.props.screenShare.state} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@ -294,7 +358,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
<div className="conversation">
|
||||
<div className="media nested">
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className="video_inner remote"></div>
|
||||
<div className="video_inner remote remote-stream"></div>
|
||||
</div>
|
||||
<div className={localStreamClasses}></div>
|
||||
</div>
|
||||
@ -461,6 +525,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
ConversationView: ConversationView,
|
||||
ConversationToolbar: ConversationToolbar,
|
||||
MediaControlButton: MediaControlButton,
|
||||
ScreenShareControlButton: ScreenShareControlButton,
|
||||
NotificationListView: NotificationListView
|
||||
};
|
||||
})(_, window.OT, navigator.mozL10n || document.mozL10n);
|
||||
})(_, navigator.mozL10n || document.mozL10n);
|
||||
|
@ -101,7 +101,6 @@
|
||||
<script type="text/javascript" src="shared/js/utils.js"></script>
|
||||
<script type="text/javascript" src="shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="shared/js/mixins.js"></script>
|
||||
<script type="text/javascript" src="shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
|
||||
<script type="text/javascript" src="shared/js/actions.js"></script>
|
||||
<script type="text/javascript" src="shared/js/validate.js"></script>
|
||||
@ -113,6 +112,7 @@
|
||||
<script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
|
||||
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
|
||||
<script type="text/javascript" src="shared/js/feedbackStore.js"></script>
|
||||
<script type="text/javascript" src="shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="shared/js/feedbackViews.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneClient.js"></script>
|
||||
|
@ -251,7 +251,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
|
||||
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
|
||||
getLocalElementFunc: this._getElement.bind(this, ".local"),
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote")
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote"),
|
||||
getScreenShareElementFunc: this._getElement.bind(this, ".screen")
|
||||
}));
|
||||
}
|
||||
|
||||
@ -339,6 +340,19 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
"local-stream-audio": this.state.videoMuted
|
||||
});
|
||||
|
||||
var remoteStreamClasses = React.addons.classSet({
|
||||
"video_inner": true,
|
||||
"remote": true,
|
||||
"remote-stream": true,
|
||||
hide: this.state.receivingScreenShare
|
||||
});
|
||||
|
||||
var screenShareStreamClasses = React.addons.classSet({
|
||||
"screen": true,
|
||||
"remote-stream": true,
|
||||
hide: !this.state.receivingScreenShare
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "room-conversation-wrapper"},
|
||||
React.createElement("div", {className: "beta-logo"}),
|
||||
@ -357,11 +371,13 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
mozL10n.get("self_view_hidden_message")
|
||||
),
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: "video_inner remote"})
|
||||
React.createElement("div", {className: remoteStreamClasses}),
|
||||
React.createElement("div", {className: screenShareStreamClasses})
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses})
|
||||
),
|
||||
React.createElement(sharedViews.ConversationToolbar, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
video: {enabled: !this.state.videoMuted,
|
||||
visible: this._roomIsActive()},
|
||||
audio: {enabled: !this.state.audioMuted,
|
||||
|
@ -251,7 +251,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
|
||||
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
|
||||
getLocalElementFunc: this._getElement.bind(this, ".local"),
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote")
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote"),
|
||||
getScreenShareElementFunc: this._getElement.bind(this, ".screen")
|
||||
}));
|
||||
}
|
||||
|
||||
@ -339,6 +340,19 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
"local-stream-audio": this.state.videoMuted
|
||||
});
|
||||
|
||||
var remoteStreamClasses = React.addons.classSet({
|
||||
"video_inner": true,
|
||||
"remote": true,
|
||||
"remote-stream": true,
|
||||
hide: this.state.receivingScreenShare
|
||||
});
|
||||
|
||||
var screenShareStreamClasses = React.addons.classSet({
|
||||
"screen": true,
|
||||
"remote-stream": true,
|
||||
hide: !this.state.receivingScreenShare
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="room-conversation-wrapper">
|
||||
<div className="beta-logo" />
|
||||
@ -357,11 +371,13 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
{mozL10n.get("self_view_hidden_message")}
|
||||
</span>
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className="video_inner remote"></div>
|
||||
<div className={remoteStreamClasses}></div>
|
||||
<div className={screenShareStreamClasses}></div>
|
||||
</div>
|
||||
<div className={localStreamClasses}></div>
|
||||
</div>
|
||||
<sharedViews.ConversationToolbar
|
||||
dispatcher={this.props.dispatcher}
|
||||
video={{enabled: !this.state.videoMuted,
|
||||
visible: this._roomIsActive()}}
|
||||
audio={{enabled: !this.state.audioMuted,
|
||||
|
@ -17,6 +17,9 @@ mute_local_audio_button_title=Mute your audio
|
||||
unmute_local_audio_button_title=Unmute your audio
|
||||
mute_local_video_button_title=Mute your video
|
||||
unmute_local_video_button_title=Unmute your video
|
||||
active_screenshare_button_title=Stop sharing
|
||||
inactive_screenshare_button_title=Share your screen
|
||||
|
||||
outgoing_call_title=Start conversation?
|
||||
call_with_contact_title=Conversation with {{incomingCallIdentity}}
|
||||
welcome=Welcome to the {{clientShortname}} web client.
|
||||
|
@ -141,7 +141,8 @@ describe("loop.conversation", function() {
|
||||
sdk: {},
|
||||
conversationStore: conversationStore,
|
||||
conversationAppStore: conversationAppStore,
|
||||
dispatcher: dispatcher
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: navigator.mozLoop
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,6 @@
|
||||
<script src="../../content/shared/js/feedbackApiClient.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/mixins.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/websocket.js"></script>
|
||||
<script src="../../content/shared/js/actions.js"></script>
|
||||
<script src="../../content/shared/js/validate.js"></script>
|
||||
@ -56,6 +55,7 @@
|
||||
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
|
||||
<script src="../../content/shared/js/activeRoomStore.js"></script>
|
||||
<script src="../../content/shared/js/feedbackStore.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/feedbackViews.js"></script>
|
||||
<script src="../../content/js/client.js"></script>
|
||||
<script src="../../content/js/conversationAppStore.js"></script>
|
||||
|
@ -6,20 +6,25 @@ describe("loop.roomViews", function () {
|
||||
"use strict";
|
||||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
|
||||
var sandbox, dispatcher, roomStore, activeRoomStore, fakeWindow;
|
||||
var fakeMozLoop;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
dispatcher = new loop.Dispatcher();
|
||||
|
||||
fakeMozLoop = {
|
||||
getAudioBlob: sinon.stub(),
|
||||
getLoopPref: sinon.stub()
|
||||
};
|
||||
|
||||
fakeWindow = {
|
||||
document: {},
|
||||
navigator: {
|
||||
mozLoop: {
|
||||
getAudioBlob: sinon.stub()
|
||||
}
|
||||
mozLoop: fakeMozLoop
|
||||
},
|
||||
addEventListener: function() {},
|
||||
removeEventListener: function() {}
|
||||
@ -62,16 +67,10 @@ describe("loop.roomViews", function () {
|
||||
roomStore: roomStore
|
||||
}));
|
||||
|
||||
expect(testView.state).eql({
|
||||
roomState: ROOM_STATES.INIT,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
failureReason: undefined,
|
||||
used: false,
|
||||
foo: "bar",
|
||||
localVideoDimensions: {},
|
||||
remoteVideoDimensions: {}
|
||||
});
|
||||
var expectedState = _.extend({foo: "bar"},
|
||||
activeRoomStore.getInitialStoreState());
|
||||
|
||||
expect(testView.state).eql(expectedState);
|
||||
});
|
||||
|
||||
it("should listen to store changes", function() {
|
||||
@ -216,7 +215,8 @@ describe("loop.roomViews", function () {
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.roomViews.DesktopRoomConversationView, {
|
||||
dispatcher: dispatcher,
|
||||
roomStore: roomStore
|
||||
roomStore: roomStore,
|
||||
mozLoop: fakeMozLoop
|
||||
}));
|
||||
}
|
||||
|
||||
@ -276,6 +276,20 @@ describe("loop.roomViews", function () {
|
||||
expect(muteBtn.classList.contains("muted")).eql(true);
|
||||
});
|
||||
|
||||
it("should dispatch a `StartScreenShare` action when sharing is not active " +
|
||||
"and the screen share button is pressed", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
view.setState({screenSharingState: SCREEN_SHARE_STATES.INACTIVE});
|
||||
|
||||
var muteBtn = view.getDOMNode().querySelector('.btn-mute-video');
|
||||
|
||||
React.addons.TestUtils.Simulate.click(muteBtn);
|
||||
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "setMute"));
|
||||
});
|
||||
|
||||
describe("#componentWillUpdate", function() {
|
||||
function expectActionDispatched(view) {
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
|
@ -14,6 +14,7 @@ FIREFOX_PREFERENCES = {
|
||||
"media.volume_scale": "0",
|
||||
"loop.gettingStarted.seen": True,
|
||||
"loop.seenToS": "seen",
|
||||
"loop.screenshare.enabled": True,
|
||||
|
||||
# this dialog is fragile, and likely to introduce intermittent failures
|
||||
"media.navigator.permission.disabled": True
|
||||
|
@ -4,7 +4,6 @@ import urlparse
|
||||
from errors import NoSuchElementException, StaleElementException
|
||||
# noinspection PyUnresolvedReferences
|
||||
from wait import Wait
|
||||
from time import sleep
|
||||
|
||||
import os
|
||||
import sys
|
||||
@ -126,23 +125,32 @@ class Test1BrowserCall(MarionetteTestCase):
|
||||
join_button.click()
|
||||
|
||||
# Assumes the standlone or the conversation window is selected first.
|
||||
def check_remote_video(self):
|
||||
video_wrapper = self.wait_for_element_displayed(
|
||||
By.CSS_SELECTOR,
|
||||
".media .OT_subscriber .OT_widget-container", 20)
|
||||
video = self.wait_for_subelement_displayed(
|
||||
video_wrapper, By.TAG_NAME, "video")
|
||||
def check_video(self, selector):
|
||||
video_wrapper = self.wait_for_element_displayed(By.CSS_SELECTOR,
|
||||
selector, 20)
|
||||
video = self.wait_for_subelement_displayed(video_wrapper,
|
||||
By.TAG_NAME, "video")
|
||||
|
||||
self.wait_for_element_attribute_to_be_false(video, "paused")
|
||||
self.assertEqual(video.get_attribute("ended"), "false")
|
||||
|
||||
def standalone_check_remote_video(self):
|
||||
self.switch_to_standalone()
|
||||
self.check_remote_video()
|
||||
self.check_video(".remote .OT_subscriber .OT_widget-container")
|
||||
|
||||
def local_check_remote_video(self):
|
||||
self.switch_to_chatbox()
|
||||
self.check_remote_video()
|
||||
self.check_video(".remote .OT_subscriber .OT_widget-container")
|
||||
|
||||
def local_enable_screenshare(self):
|
||||
self.switch_to_chatbox()
|
||||
button = self.marionette.find_element(By.CLASS_NAME, "btn-screen-share")
|
||||
|
||||
button.click()
|
||||
|
||||
def standalone_check_remote_screenshare(self):
|
||||
self.switch_to_standalone()
|
||||
self.check_video(".media .screen .OT_subscriber .OT_widget-container")
|
||||
|
||||
def local_leave_room_and_verify_feedback(self):
|
||||
button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
|
||||
@ -170,6 +178,11 @@ class Test1BrowserCall(MarionetteTestCase):
|
||||
self.standalone_check_remote_video()
|
||||
self.local_check_remote_video()
|
||||
|
||||
# XXX To enable this, we either need to navigate the permissions prompt
|
||||
# or have a route where we don't need the permissions prompt.
|
||||
# self.local_enable_screenshare()
|
||||
# self.standalone_check_remote_screenshare()
|
||||
|
||||
# hangup the call
|
||||
self.local_leave_room_and_verify_feedback()
|
||||
|
||||
|
@ -9,6 +9,7 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver;
|
||||
var fakeMultiplexGum;
|
||||
|
||||
@ -646,6 +647,26 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#screenSharingState", function() {
|
||||
it("should save the state", function() {
|
||||
store.screenSharingState(new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.ACTIVE
|
||||
}));
|
||||
|
||||
expect(store.getStoreState().screenSharingState).eql(SCREEN_SHARE_STATES.ACTIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#receivingScreenShare", function() {
|
||||
it("should save the state", function() {
|
||||
store.receivingScreenShare(new sharedActions.ReceivingScreenShare({
|
||||
receiving: true
|
||||
}));
|
||||
|
||||
expect(store.getStoreState().receivingScreenShare).eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#remotePeerConnected", function() {
|
||||
it("should set the state to `HAS_PARTICIPANTS`", function() {
|
||||
store.remotePeerConnected();
|
||||
|
@ -42,7 +42,6 @@
|
||||
<script src="../../content/shared/js/utils.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/mixins.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/websocket.js"></script>
|
||||
<script src="../../content/shared/js/feedbackApiClient.js"></script>
|
||||
<script src="../../content/shared/js/validate.js"></script>
|
||||
@ -56,6 +55,7 @@
|
||||
<script src="../../content/shared/js/roomStore.js"></script>
|
||||
<script src="../../content/shared/js/conversationStore.js"></script>
|
||||
<script src="../../content/shared/js/feedbackStore.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/feedbackViews.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
|
@ -236,13 +236,15 @@ describe("loop.shared.mixins", function() {
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
var localElement, remoteElement;
|
||||
var localElement, remoteElement, screenShareElement;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(view, "getDOMNode").returns({
|
||||
querySelector: function(classSelector) {
|
||||
if (classSelector.contains("local")) {
|
||||
return localElement;
|
||||
return localElement;
|
||||
} else if (classSelector.contains("screen")) {
|
||||
return screenShareElement;
|
||||
}
|
||||
return remoteElement;
|
||||
}
|
||||
@ -275,6 +277,19 @@ describe("loop.shared.mixins", function() {
|
||||
|
||||
expect(remoteElement.style.height).eql("100%");
|
||||
});
|
||||
|
||||
it("should update the height on the screen share stream element", function() {
|
||||
screenShareElement = {
|
||||
offsetWidth: 100,
|
||||
offsetHeight: 100,
|
||||
style: { height: "0%" }
|
||||
};
|
||||
|
||||
rootObject.events.resize();
|
||||
sandbox.clock.tick(10);
|
||||
|
||||
expect(screenShareElement.style.height).eql("100%");
|
||||
});
|
||||
});
|
||||
|
||||
describe("orientationchange", function() {
|
||||
@ -303,6 +318,19 @@ describe("loop.shared.mixins", function() {
|
||||
|
||||
expect(remoteElement.style.height).eql("100%");
|
||||
});
|
||||
|
||||
it("should update the height on the screen share stream element", function() {
|
||||
screenShareElement = {
|
||||
offsetWidth: 100,
|
||||
offsetHeight: 100,
|
||||
style: { height: "0%" }
|
||||
};
|
||||
|
||||
rootObject.events.orientationchange();
|
||||
sandbox.clock.tick(10);
|
||||
|
||||
expect(screenShareElement.style.height).eql("100%");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
@ -9,15 +9,18 @@ describe("loop.OTSdkDriver", function () {
|
||||
var sharedActions = loop.shared.actions;
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
var STREAM_PROPERTIES = loop.shared.utils.STREAM_PROPERTIES;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
var sandbox;
|
||||
var dispatcher, driver, publisher, sdk, session, sessionData;
|
||||
var fakeLocalElement, fakeRemoteElement, publisherConfig, fakeEvent;
|
||||
var fakeLocalElement, fakeRemoteElement, fakeScreenElement;
|
||||
var publisherConfig, fakeEvent;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
fakeLocalElement = {fake: 1};
|
||||
fakeRemoteElement = {fake: 2};
|
||||
fakeScreenElement = {fake: 3};
|
||||
fakeEvent = {
|
||||
preventDefault: sinon.stub()
|
||||
};
|
||||
@ -35,6 +38,7 @@ describe("loop.OTSdkDriver", function () {
|
||||
connect: sinon.stub(),
|
||||
disconnect: sinon.stub(),
|
||||
publish: sinon.stub(),
|
||||
unpublish: sinon.stub(),
|
||||
subscribe: sinon.stub(),
|
||||
forceDisconnect: sinon.stub()
|
||||
}, Backbone.Events);
|
||||
@ -119,6 +123,74 @@ describe("loop.OTSdkDriver", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#startScreenShare", function() {
|
||||
var fakeElement;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
fakeElement = {
|
||||
className: "fakeVideo"
|
||||
};
|
||||
|
||||
driver.getScreenShareElementFunc = function() {
|
||||
return fakeElement;
|
||||
};
|
||||
});
|
||||
|
||||
it("should dispatch a `ScreenSharingState` action", function() {
|
||||
driver.startScreenShare(new sharedActions.StartScreenShare());
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.PENDING
|
||||
}));
|
||||
});
|
||||
|
||||
it("should initialize a publisher", function() {
|
||||
driver.startScreenShare(new sharedActions.StartScreenShare());
|
||||
|
||||
sinon.assert.calledOnce(sdk.initPublisher);
|
||||
sinon.assert.calledWithMatch(sdk.initPublisher,
|
||||
fakeElement, {videoSource: "window"});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#endScreenShare", function() {
|
||||
beforeEach(function() {
|
||||
driver.getScreenShareElementFunc = function() {};
|
||||
|
||||
driver.startScreenShare(new sharedActions.StartScreenShare());
|
||||
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
driver.session = session;
|
||||
});
|
||||
|
||||
it("should unpublish the share", function() {
|
||||
driver.endScreenShare(new sharedActions.EndScreenShare());
|
||||
|
||||
sinon.assert.calledOnce(session.unpublish);
|
||||
});
|
||||
|
||||
it("should destroy the share", function() {
|
||||
driver.endScreenShare(new sharedActions.EndScreenShare());
|
||||
|
||||
sinon.assert.calledOnce(publisher.destroy);
|
||||
});
|
||||
|
||||
it("should dispatch a `ScreenSharingState` action", function() {
|
||||
driver.endScreenShare(new sharedActions.EndScreenShare());
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.INACTIVE
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#connectSession", function() {
|
||||
it("should initialise a new session", function() {
|
||||
driver.connectSession(sessionData);
|
||||
@ -214,12 +286,13 @@ describe("loop.OTSdkDriver", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
describe("Events (general media)", function() {
|
||||
beforeEach(function() {
|
||||
driver.connectSession(sessionData);
|
||||
|
||||
dispatcher.dispatch(new sharedActions.SetupStreamElements({
|
||||
getLocalElementFunc: function() {return fakeLocalElement;},
|
||||
getScreenShareElementFunc: function() {return fakeScreenElement;},
|
||||
getRemoteElementFunc: function() {return fakeRemoteElement;},
|
||||
publisherConfig: publisherConfig
|
||||
}));
|
||||
@ -283,16 +356,50 @@ describe("loop.OTSdkDriver", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("streamCreated", function() {
|
||||
describe("streamCreated (publisher/local)", function() {
|
||||
it("should dispatch a VideoDimensionsChanged action", function() {
|
||||
var fakeStream = {
|
||||
hasVideo: true,
|
||||
videoType: "camera",
|
||||
videoDimensions: {width: 1, height: 2}
|
||||
};
|
||||
|
||||
publisher.trigger("streamCreated", {stream: fakeStream});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.VideoDimensionsChanged({
|
||||
isLocal: true,
|
||||
videoType: "camera",
|
||||
dimensions: {width: 1, height: 2}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("streamCreated (session/remote)", function() {
|
||||
var fakeStream;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeStream = {
|
||||
fakeStream: 3
|
||||
hasVideo: true,
|
||||
videoType: "camera",
|
||||
videoDimensions: {width: 1, height: 2}
|
||||
};
|
||||
});
|
||||
|
||||
it("should subscribe to the stream", function() {
|
||||
it("should dispatch a VideoDimensionsChanged action", function() {
|
||||
session.trigger("streamCreated", {stream: fakeStream});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.VideoDimensionsChanged({
|
||||
isLocal: false,
|
||||
videoType: "camera",
|
||||
dimensions: {width: 1, height: 2}
|
||||
}));
|
||||
});
|
||||
|
||||
it("should subscribe to a camera stream", function() {
|
||||
session.trigger("streamCreated", {stream: fakeStream});
|
||||
|
||||
sinon.assert.calledOnce(session.subscribe);
|
||||
@ -300,15 +407,85 @@ describe("loop.OTSdkDriver", function () {
|
||||
fakeStream, fakeRemoteElement, publisherConfig);
|
||||
});
|
||||
|
||||
it("should subscribe to a screen sharing stream", function() {
|
||||
fakeStream.videoType = "screen";
|
||||
|
||||
session.trigger("streamCreated", {stream: fakeStream});
|
||||
|
||||
sinon.assert.calledOnce(session.subscribe);
|
||||
sinon.assert.calledWithExactly(session.subscribe,
|
||||
fakeStream, fakeScreenElement, publisherConfig);
|
||||
});
|
||||
|
||||
it("should dispach a mediaConnected action if both streams are up", function() {
|
||||
driver._publishedLocalStream = true;
|
||||
|
||||
session.trigger("streamCreated", {stream: fakeStream});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Called twice due to the VideoDimensionsChanged above.
|
||||
sinon.assert.calledTwice(dispatcher.dispatch);
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "mediaConnected"));
|
||||
});
|
||||
|
||||
it("should not dispatch a mediaConnected action for screen sharing streams",
|
||||
function() {
|
||||
driver._publishedLocalStream = true;
|
||||
fakeStream.videoType = "screen";
|
||||
|
||||
session.trigger("streamCreated", {stream: fakeStream});
|
||||
|
||||
sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "mediaConnected"));
|
||||
});
|
||||
|
||||
it("should not dispatch a ReceivingScreenShare action for camera streams",
|
||||
function() {
|
||||
session.trigger("streamCreated", {stream: fakeStream});
|
||||
|
||||
sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
|
||||
new sharedActions.ReceivingScreenShare({receiving: true}));
|
||||
});
|
||||
|
||||
it("should dispatch a ReceivingScreenShare action for screen sharing streams",
|
||||
function() {
|
||||
fakeStream.videoType = "screen";
|
||||
|
||||
session.trigger("streamCreated", {stream: fakeStream});
|
||||
|
||||
// Called twice due to the VideoDimensionsChanged above.
|
||||
sinon.assert.calledTwice(dispatcher.dispatch);
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
new sharedActions.ReceivingScreenShare({receiving: true}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("streamDestroyed", function() {
|
||||
var fakeStream;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeStream = {
|
||||
videoType: "screen"
|
||||
};
|
||||
});
|
||||
|
||||
it("should dispatch a ReceivingScreenShare action", function() {
|
||||
session.trigger("streamDestroyed", {stream: fakeStream});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.ReceivingScreenShare({
|
||||
receiving: false
|
||||
}));
|
||||
});
|
||||
|
||||
it("should not dispatch an action if the videoType is camera", function() {
|
||||
fakeStream.videoType = "camera";
|
||||
|
||||
session.trigger("streamDestroyed", {stream: fakeStream});
|
||||
|
||||
sinon.assert.notCalled(dispatcher.dispatch);
|
||||
});
|
||||
});
|
||||
|
||||
describe("streamPropertyChanged", function() {
|
||||
@ -345,8 +522,8 @@ describe("loop.OTSdkDriver", function () {
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "videoDimensionsChanged"))
|
||||
})
|
||||
sinon.match.hasOwn("name", "videoDimensionsChanged"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("connectionCreated", function() {
|
||||
@ -427,4 +604,46 @@ describe("loop.OTSdkDriver", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events (screenshare)", function() {
|
||||
beforeEach(function() {
|
||||
driver.connectSession(sessionData);
|
||||
|
||||
driver.getScreenShareElementFunc = function() {};
|
||||
|
||||
driver.startScreenShare(new sharedActions.StartScreenShare());
|
||||
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
describe("accessAllowed", function() {
|
||||
it("should publish the stream", function() {
|
||||
publisher.trigger("accessAllowed", fakeEvent);
|
||||
|
||||
sinon.assert.calledOnce(session.publish);
|
||||
});
|
||||
|
||||
it("should dispatch a `ScreenSharingState` action", function() {
|
||||
publisher.trigger("accessAllowed", fakeEvent);
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.ACTIVE
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("accessDenied", function() {
|
||||
it("should dispatch a `ScreenShareState` action", function() {
|
||||
publisher.trigger("accessDenied", fakeEvent);
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.ScreenSharingState({
|
||||
state: SCREEN_SHARE_STATES.INACTIVE
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,10 +12,11 @@ var TestUtils = React.addons.TestUtils;
|
||||
describe("loop.shared.views", function() {
|
||||
"use strict";
|
||||
|
||||
var sharedModels = loop.shared.models,
|
||||
sharedViews = loop.shared.views,
|
||||
getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass,
|
||||
sandbox, fakeAudioXHR;
|
||||
var sharedModels = loop.shared.models;
|
||||
var sharedViews = loop.shared.views;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
var getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass;
|
||||
var sandbox, fakeAudioXHR, dispatcher;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@ -23,6 +24,10 @@ describe("loop.shared.views", function() {
|
||||
sandbox.stub(l10n, "get", function(x) {
|
||||
return "translated:" + x;
|
||||
});
|
||||
|
||||
dispatcher = new loop.Dispatcher();
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
fakeAudioXHR = {
|
||||
open: sinon.spy(),
|
||||
send: function() {},
|
||||
@ -92,6 +97,76 @@ describe("loop.shared.views", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ScreenShareControlButton", function() {
|
||||
it("should render a visible share button", function() {
|
||||
var comp = TestUtils.renderIntoDocument(
|
||||
React.createElement(sharedViews.ScreenShareControlButton, {
|
||||
dispatcher: dispatcher,
|
||||
visible: true,
|
||||
state: SCREEN_SHARE_STATES.INACTIVE
|
||||
}));
|
||||
|
||||
expect(comp.getDOMNode().classList.contains("active")).eql(false);
|
||||
expect(comp.getDOMNode().classList.contains("disabled")).eql(false);
|
||||
});
|
||||
|
||||
it("should render a disabled share button when share is pending", function() {
|
||||
var comp = TestUtils.renderIntoDocument(
|
||||
React.createElement(sharedViews.ScreenShareControlButton, {
|
||||
dispatcher: dispatcher,
|
||||
visible: true,
|
||||
state: SCREEN_SHARE_STATES.PENDING
|
||||
}));
|
||||
|
||||
expect(comp.getDOMNode().classList.contains("active")).eql(false);
|
||||
expect(comp.getDOMNode().classList.contains("disabled")).eql(true);
|
||||
});
|
||||
|
||||
it("should render an active share button", function() {
|
||||
var comp = TestUtils.renderIntoDocument(
|
||||
React.createElement(sharedViews.ScreenShareControlButton, {
|
||||
dispatcher: dispatcher,
|
||||
visible: true,
|
||||
state: SCREEN_SHARE_STATES.ACTIVE
|
||||
}));
|
||||
|
||||
expect(comp.getDOMNode().classList.contains("active")).eql(true);
|
||||
expect(comp.getDOMNode().classList.contains("disabled")).eql(false);
|
||||
});
|
||||
|
||||
it("should dispatch a StartScreenShare action on click when the state is not active",
|
||||
function() {
|
||||
var comp = TestUtils.renderIntoDocument(
|
||||
React.createElement(sharedViews.ScreenShareControlButton, {
|
||||
dispatcher: dispatcher,
|
||||
visible: true,
|
||||
state: SCREEN_SHARE_STATES.INACTIVE
|
||||
}));
|
||||
|
||||
TestUtils.Simulate.click(comp.getDOMNode());
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.StartScreenShare({}));
|
||||
});
|
||||
|
||||
it("should dispatch a EndScreenShare action on click when the state is active",
|
||||
function() {
|
||||
var comp = TestUtils.renderIntoDocument(
|
||||
React.createElement(sharedViews.ScreenShareControlButton, {
|
||||
dispatcher: dispatcher,
|
||||
visible: true,
|
||||
state: SCREEN_SHARE_STATES.ACTIVE
|
||||
}));
|
||||
|
||||
TestUtils.Simulate.click(comp.getDOMNode());
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.EndScreenShare({}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConversationToolbar", function() {
|
||||
var hangup, publishStream;
|
||||
|
||||
|
@ -41,7 +41,6 @@
|
||||
<script src="../../content/shared/js/utils.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/mixins.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/websocket.js"></script>
|
||||
<script src="../../content/shared/js/feedbackApiClient.js"></script>
|
||||
<script src="../../content/shared/js/actions.js"></script>
|
||||
@ -52,6 +51,7 @@
|
||||
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
|
||||
<script src="../../content/shared/js/activeRoomStore.js"></script>
|
||||
<script src="../../content/shared/js/feedbackStore.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/feedbackViews.js"></script>
|
||||
<script src="../../content/shared/js/otSdkDriver.js"></script>
|
||||
<script src="../../standalone/content/js/multiplexGum.js"></script>
|
||||
|
@ -54,6 +54,7 @@ navigator.mozLoop = {
|
||||
switch(pref) {
|
||||
// Ensure we skip FTE completely.
|
||||
case "gettingStarted.seen":
|
||||
case "screenshare.enabled":
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
@ -37,7 +37,6 @@
|
||||
<script src="../content/shared/js/utils.js"></script>
|
||||
<script src="../content/shared/js/models.js"></script>
|
||||
<script src="../content/shared/js/mixins.js"></script>
|
||||
<script src="../content/shared/js/views.js"></script>
|
||||
<script src="../content/shared/js/websocket.js"></script>
|
||||
<script src="../content/shared/js/validate.js"></script>
|
||||
<script src="../content/shared/js/dispatcher.js"></script>
|
||||
@ -48,6 +47,7 @@
|
||||
<script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
|
||||
<script src="../content/shared/js/activeRoomStore.js"></script>
|
||||
<script src="../content/shared/js/feedbackStore.js"></script>
|
||||
<script src="../content/shared/js/views.js"></script>
|
||||
<script src="../content/shared/js/feedbackViews.js"></script>
|
||||
<script src="../content/js/roomViews.js"></script>
|
||||
<script src="../content/js/conversationViews.js"></script>
|
||||
|
@ -567,6 +567,7 @@
|
||||
React.createElement(DesktopRoomConversationView, {
|
||||
roomStore: roomStore,
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: navigator.mozLoop,
|
||||
roomState: ROOM_STATES.INIT})
|
||||
)
|
||||
),
|
||||
@ -577,6 +578,7 @@
|
||||
React.createElement(DesktopRoomConversationView, {
|
||||
roomStore: roomStore,
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: navigator.mozLoop,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS})
|
||||
)
|
||||
)
|
||||
|
@ -567,6 +567,7 @@
|
||||
<DesktopRoomConversationView
|
||||
roomStore={roomStore}
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={navigator.mozLoop}
|
||||
roomState={ROOM_STATES.INIT} />
|
||||
</div>
|
||||
</Example>
|
||||
@ -577,6 +578,7 @@
|
||||
<DesktopRoomConversationView
|
||||
roomStore={roomStore}
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={navigator.mozLoop}
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
|
||||
</div>
|
||||
</Example>
|
||||
|
@ -99,9 +99,11 @@ browser.jar:
|
||||
content/browser/devtools/performance/views/toolbar.js (performance/views/toolbar.js)
|
||||
content/browser/devtools/performance/views/details.js (performance/views/details.js)
|
||||
content/browser/devtools/performance/views/details-subview.js (performance/views/details-abstract-subview.js)
|
||||
content/browser/devtools/performance/views/details-call-tree.js (performance/views/details-call-tree.js)
|
||||
content/browser/devtools/performance/views/details-waterfall.js (performance/views/details-waterfall.js)
|
||||
content/browser/devtools/performance/views/details-flamegraph.js (performance/views/details-flamegraph.js)
|
||||
content/browser/devtools/performance/views/details-js-call-tree.js (performance/views/details-js-call-tree.js)
|
||||
content/browser/devtools/performance/views/details-js-flamegraph.js (performance/views/details-js-flamegraph.js)
|
||||
content/browser/devtools/performance/views/details-memory-call-tree.js (performance/views/details-memory-call-tree.js)
|
||||
content/browser/devtools/performance/views/details-memory-flamegraph.js (performance/views/details-memory-flamegraph.js)
|
||||
content/browser/devtools/performance/views/recordings.js (performance/views/recordings.js)
|
||||
#endif
|
||||
content/browser/devtools/responsivedesign/resize-commands.js (responsivedesign/resize-commands.js)
|
||||
|
@ -13,12 +13,23 @@ loader.lazyRequireGetter(this, "EventEmitter",
|
||||
"devtools/toolkit/event-emitter");
|
||||
loader.lazyRequireGetter(this, "TimelineFront",
|
||||
"devtools/server/actors/timeline", true);
|
||||
loader.lazyRequireGetter(this, "MemoryFront",
|
||||
"devtools/server/actors/memory", true);
|
||||
|
||||
loader.lazyRequireGetter(this, "DevToolsUtils",
|
||||
"devtools/toolkit/DevToolsUtils");
|
||||
|
||||
loader.lazyImporter(this, "gDevTools",
|
||||
"resource:///modules/devtools/gDevTools.jsm");
|
||||
|
||||
loader.lazyImporter(this, "setTimeout",
|
||||
"resource://gre/modules/Timer.jsm");
|
||||
loader.lazyImporter(this, "clearTimeout",
|
||||
"resource://gre/modules/Timer.jsm");
|
||||
|
||||
// How often do we pull allocation sites from the memory actor.
|
||||
const DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT = 200; // ms
|
||||
|
||||
/**
|
||||
* A cache of all PerformanceActorsConnection instances.
|
||||
* The keys are Target objects.
|
||||
@ -44,6 +55,25 @@ SharedPerformanceActors.forTarget = function(target) {
|
||||
return instance;
|
||||
};
|
||||
|
||||
/**
|
||||
* A dummy front decorated with the provided methods.
|
||||
*
|
||||
* @param array blueprint
|
||||
* A list of [funcName, retVal] describing the class.
|
||||
*/
|
||||
function MockedFront(blueprint) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
for (let [funcName, retVal] of blueprint) {
|
||||
this[funcName] = (x => x).bind(this, retVal);
|
||||
}
|
||||
}
|
||||
|
||||
MockedFront.prototype = {
|
||||
initialize: function() {},
|
||||
destroy: function() {}
|
||||
};
|
||||
|
||||
/**
|
||||
* A connection to underlying actors (profiler, memory, framerate, etc.)
|
||||
* shared by all tools in a target.
|
||||
@ -67,7 +97,7 @@ function PerformanceActorsConnection(target) {
|
||||
PerformanceActorsConnection.prototype = {
|
||||
/**
|
||||
* Initializes a connection to the profiler and other miscellaneous actors.
|
||||
* If already open, nothing happens.
|
||||
* If in the process of opening, or already open, nothing happens.
|
||||
*
|
||||
* @return object
|
||||
* A promise that is resolved once the connection is established.
|
||||
@ -80,11 +110,13 @@ PerformanceActorsConnection.prototype = {
|
||||
// Local debugging needs to make the target remote.
|
||||
yield this._target.makeRemote();
|
||||
|
||||
// Sets `this._profiler`
|
||||
// Sets `this._profiler`, `this._timeline` and `this._memory`.
|
||||
// Only initialize the timeline and memory fronts if the respective actors
|
||||
// are available. Older Gecko versions don't have existing implementations,
|
||||
// in which case all the methods we need can be easily mocked.
|
||||
yield this._connectProfilerActor();
|
||||
|
||||
// Sets or shims `this._timeline`
|
||||
yield this._connectTimelineActor();
|
||||
yield this._connectMemoryActor();
|
||||
|
||||
this._connected = true;
|
||||
|
||||
@ -94,10 +126,10 @@ PerformanceActorsConnection.prototype = {
|
||||
/**
|
||||
* Destroys this connection.
|
||||
*/
|
||||
destroy: function () {
|
||||
this._disconnectActors();
|
||||
destroy: Task.async(function*() {
|
||||
yield this._disconnectActors();
|
||||
this._connected = false;
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Initializes a connection to the profiler actor.
|
||||
@ -126,46 +158,51 @@ PerformanceActorsConnection.prototype = {
|
||||
|
||||
/**
|
||||
* Initializes a connection to a timeline actor.
|
||||
* TODO: use framework level feature detection from bug 1069673
|
||||
*/
|
||||
_connectTimelineActor: function() {
|
||||
// Only initialize the timeline front if the respective actor is available.
|
||||
// Older Gecko versions don't have an existing implementation, in which case
|
||||
// all the methods we need can be easily mocked.
|
||||
//
|
||||
// If the timeline actor exists, all underlying actors (memory, framerate) exist,
|
||||
// with the expected methods and behaviour. If using the Performance tool,
|
||||
// and timeline actor does not exist (FxOS devices < Gecko 35),
|
||||
// then just use the mocked actor and do not display timeline data.
|
||||
//
|
||||
// TODO use framework level feature detection from bug 1069673
|
||||
if (this._target.form && this._target.form.timelineActor) {
|
||||
this._timeline = new TimelineFront(this._target.client, this._target.form);
|
||||
} else {
|
||||
this._timeline = {
|
||||
start: () => 0,
|
||||
stop: () => 0,
|
||||
isRecording: () => false,
|
||||
on: () => null,
|
||||
off: () => null,
|
||||
once: () => promise.reject(),
|
||||
destroy: () => null
|
||||
};
|
||||
this._timeline = new MockedFront([
|
||||
["start", 0],
|
||||
["stop", 0]
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes a connection to a memory actor.
|
||||
* TODO: use framework level feature detection from bug 1069673
|
||||
*/
|
||||
_connectMemoryActor: function() {
|
||||
if (this._target.form && this._target.form.memoryActor) {
|
||||
this._memory = new MemoryFront(this._target.client, this._target.form);
|
||||
} else {
|
||||
this._memory = new MockedFront([
|
||||
["attach"],
|
||||
["detach"],
|
||||
["startRecordingAllocations", 0],
|
||||
["stopRecordingAllocations", 0],
|
||||
["getAllocations"]
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes the connections to non-profiler actors.
|
||||
*/
|
||||
_disconnectActors: function () {
|
||||
this._timeline.destroy();
|
||||
},
|
||||
_disconnectActors: Task.async(function* () {
|
||||
yield this._timeline.destroy();
|
||||
yield this._memory.destroy();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Sends the request over the remote debugging protocol to the
|
||||
* specified actor.
|
||||
*
|
||||
* @param string actor
|
||||
* The designated actor. Currently supported: "profiler", "timeline".
|
||||
* Currently supported: "profiler", "timeline", "memory".
|
||||
* @param string method
|
||||
* Method to call on the backend.
|
||||
* @param any args [optional]
|
||||
@ -188,6 +225,11 @@ PerformanceActorsConnection.prototype = {
|
||||
if (actor == "timeline") {
|
||||
return this._timeline[method].apply(this._timeline, args);
|
||||
}
|
||||
|
||||
// Handle requests to the memory actor.
|
||||
if (actor == "memory") {
|
||||
return this._memory[method].apply(this._memory, args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -208,69 +250,142 @@ function PerformanceFront(connection) {
|
||||
connection._timeline.on("frames", (delta, frames) => this.emit("frames", delta, frames));
|
||||
connection._timeline.on("memory", (delta, measurement) => this.emit("memory", delta, measurement));
|
||||
connection._timeline.on("ticks", (delta, timestamps) => this.emit("ticks", delta, timestamps));
|
||||
|
||||
this._pullAllocationSites = this._pullAllocationSites.bind(this);
|
||||
this._sitesPullTimeout = 0;
|
||||
}
|
||||
|
||||
PerformanceFront.prototype = {
|
||||
/**
|
||||
* Manually begins a recording session.
|
||||
*
|
||||
* @param object timelineOptions
|
||||
* An options object to pass to the timeline front. Supported
|
||||
* properties are `withTicks` and `withMemory`.
|
||||
* @param object options
|
||||
* An options object to pass to the actors. Supported properties are
|
||||
* `withTicks`, `withMemory` and `withAllocations`.
|
||||
* @return object
|
||||
* A promise that is resolved once recording has started.
|
||||
*/
|
||||
startRecording: Task.async(function*(timelineOptions = {}) {
|
||||
let profilerStatus = yield this._request("profiler", "isActive");
|
||||
let profilerStartTime;
|
||||
startRecording: Task.async(function*(options = {}) {
|
||||
// All actors are started asynchronously over the remote debugging protocol.
|
||||
// Get the corresponding start times from each one of them.
|
||||
let profilerStartTime = yield this._startProfiler();
|
||||
let timelineStartTime = yield this._startTimeline(options);
|
||||
let memoryStartTime = yield this._startMemory(options);
|
||||
|
||||
// Start the profiler only if it wasn't already active. The built-in
|
||||
// nsIPerformance module will be kept recording, because it's the same instance
|
||||
// for all targets and interacts with the whole platform, so we don't want
|
||||
// to affect other clients by stopping (or restarting) it.
|
||||
if (!profilerStatus.isActive) {
|
||||
// Extend the profiler options so that protocol.js doesn't modify the original.
|
||||
let profilerOptions = extend({}, this._customProfilerOptions);
|
||||
yield this._request("profiler", "startProfiler", profilerOptions);
|
||||
profilerStartTime = 0;
|
||||
this.emit("profiler-activated");
|
||||
} else {
|
||||
profilerStartTime = profilerStatus.currentTime;
|
||||
this.emit("profiler-already-active");
|
||||
}
|
||||
|
||||
// The timeline actor is target-dependent, so just make sure it's recording.
|
||||
// It won't, however, be available in older Geckos (FF < 35).
|
||||
let timelineStartTime = yield this._request("timeline", "start", timelineOptions);
|
||||
|
||||
// Return the start times from the two actors. They will be used to
|
||||
// synchronize the profiler and timeline data.
|
||||
return {
|
||||
profilerStartTime,
|
||||
timelineStartTime
|
||||
timelineStartTime,
|
||||
memoryStartTime
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* Manually ends the current recording session.
|
||||
*
|
||||
* @param object options
|
||||
* @see PerformanceFront.prototype.startRecording
|
||||
* @return object
|
||||
* A promise that is resolved once recording has stopped,
|
||||
* with the profiler and timeline data.
|
||||
* with the profiler and memory data, along with all the end times.
|
||||
*/
|
||||
stopRecording: Task.async(function*() {
|
||||
let timelineEndTime = yield this._request("timeline", "stop");
|
||||
stopRecording: Task.async(function*(options = {}) {
|
||||
let memoryEndTime = yield this._stopMemory(options);
|
||||
let timelineEndTime = yield this._stopTimeline(options);
|
||||
let profilerData = yield this._request("profiler", "getProfile");
|
||||
|
||||
// Return the end times from the two actors. They will be used to
|
||||
// synchronize the profiler and timeline data.
|
||||
return {
|
||||
// Data available only at the end of a recording.
|
||||
profile: profilerData.profile,
|
||||
|
||||
// End times for all the actors.
|
||||
profilerEndTime: profilerData.currentTime,
|
||||
timelineEndTime: timelineEndTime
|
||||
timelineEndTime: timelineEndTime,
|
||||
memoryEndTime: memoryEndTime
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* Starts the profiler actor, if necessary.
|
||||
*/
|
||||
_startProfiler: Task.async(function *() {
|
||||
// Start the profiler only if it wasn't already active. The built-in
|
||||
// nsIPerformance module will be kept recording, because it's the same instance
|
||||
// for all targets and interacts with the whole platform, so we don't want
|
||||
// to affect other clients by stopping (or restarting) it.
|
||||
let profilerStatus = yield this._request("profiler", "isActive");
|
||||
if (profilerStatus.isActive) {
|
||||
this.emit("profiler-already-active");
|
||||
return profilerStatus.currentTime;
|
||||
}
|
||||
|
||||
// Extend the profiler options so that protocol.js doesn't modify the original.
|
||||
let profilerOptions = extend({}, this._customProfilerOptions);
|
||||
yield this._request("profiler", "startProfiler", profilerOptions);
|
||||
|
||||
this.emit("profiler-activated");
|
||||
return 0;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Starts the timeline actor.
|
||||
*/
|
||||
_startTimeline: Task.async(function *(options) {
|
||||
// The timeline actor is target-dependent, so just make sure it's recording.
|
||||
// It won't, however, be available in older Geckos (FF < 35).
|
||||
return (yield this._request("timeline", "start", options));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stops the timeline actor.
|
||||
*/
|
||||
_stopTimeline: Task.async(function *(options) {
|
||||
return (yield this._request("timeline", "stop"));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Starts the timeline actor, if necessary.
|
||||
*/
|
||||
_startMemory: Task.async(function *(options) {
|
||||
if (!options.withAllocations) {
|
||||
return 0;
|
||||
}
|
||||
yield this._request("memory", "attach");
|
||||
let memoryStartTime = yield this._request("memory", "startRecordingAllocations");
|
||||
yield this._pullAllocationSites();
|
||||
return memoryStartTime;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stops the timeline actor, if necessary.
|
||||
*/
|
||||
_stopMemory: Task.async(function *(options) {
|
||||
if (!options.withAllocations) {
|
||||
return 0;
|
||||
}
|
||||
clearTimeout(this._sitesPullTimeout);
|
||||
let memoryEndTime = yield this._request("memory", "stopRecordingAllocations");
|
||||
yield this._request("memory", "detach");
|
||||
return memoryEndTime;
|
||||
}),
|
||||
|
||||
/**
|
||||
* At regular intervals, pull allocations from the memory actor, and forward
|
||||
* them to consumers.
|
||||
*/
|
||||
_pullAllocationSites: Task.async(function *() {
|
||||
let memoryData = yield this._request("memory", "getAllocations");
|
||||
|
||||
this.emit("allocations", {
|
||||
sites: memoryData.allocations,
|
||||
timestamps: memoryData.allocationsTimestamps,
|
||||
frames: memoryData.frames,
|
||||
counts: memoryData.counts
|
||||
});
|
||||
|
||||
let delay = DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT;
|
||||
this._sitesPullTimeout = setTimeout(this._pullAllocationSites, delay);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Overrides the options sent to the built-in profiler module when activating,
|
||||
* such as the maximum entries count, the sampling interval etc.
|
||||
@ -285,7 +400,8 @@ PerformanceFront.prototype = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A collection of small wrappers promisifying functions invoking callbacks.
|
||||
* Returns a promise resolved with a listing of all the tabs in the
|
||||
* provided thread client.
|
||||
*/
|
||||
function listTabs(client) {
|
||||
let deferred = promise.defer();
|
||||
|
@ -131,7 +131,7 @@ function convertLegacyData (legacyData) {
|
||||
let { profilerData, ticksData, recordingDuration } = legacyData;
|
||||
|
||||
// The `profilerData` and `ticksData` stay, but the previously unrecorded
|
||||
// fields just are empty arrays.
|
||||
// fields just are empty arrays or objects.
|
||||
let data = {
|
||||
label: profilerData.profilerLabel,
|
||||
duration: recordingDuration,
|
||||
@ -139,6 +139,7 @@ function convertLegacyData (legacyData) {
|
||||
frames: [],
|
||||
memory: [],
|
||||
ticks: ticksData,
|
||||
allocations: { sites: [], timestamps: [], frames: [], counts: [] },
|
||||
profile: profilerData.profile
|
||||
};
|
||||
|
||||
|
@ -28,14 +28,16 @@ RecordingModel.prototype = {
|
||||
_recording: false,
|
||||
_profilerStartTime: 0,
|
||||
_timelineStartTime: 0,
|
||||
_memoryStartTime: 0,
|
||||
|
||||
// Serializable fields, necessary and sufficient for import and export.
|
||||
_label: "",
|
||||
_duration: 0,
|
||||
_markers: null,
|
||||
_frames: null,
|
||||
_ticks: null,
|
||||
_memory: null,
|
||||
_ticks: null,
|
||||
_allocations: null,
|
||||
_profile: null,
|
||||
|
||||
/**
|
||||
@ -54,6 +56,7 @@ RecordingModel.prototype = {
|
||||
this._frames = recordingData.frames;
|
||||
this._memory = recordingData.memory;
|
||||
this._ticks = recordingData.ticks;
|
||||
this._allocations = recordingData.allocations;
|
||||
this._profile = recordingData.profile;
|
||||
}),
|
||||
|
||||
@ -72,8 +75,7 @@ RecordingModel.prototype = {
|
||||
* Starts recording with the PerformanceFront.
|
||||
*
|
||||
* @param object options
|
||||
* An options object to pass to the timeline front. Supported
|
||||
* properties are `withTicks` and `withMemory`.
|
||||
* @see PerformanceFront.prototype.startRecording
|
||||
*/
|
||||
startRecording: Task.async(function *(options = {}) {
|
||||
// Times must come from the actor in order to be self-consistent.
|
||||
@ -85,19 +87,24 @@ RecordingModel.prototype = {
|
||||
let info = yield this._front.startRecording(options);
|
||||
this._profilerStartTime = info.profilerStartTime;
|
||||
this._timelineStartTime = info.timelineStartTime;
|
||||
this._memoryStartTime = info.memoryStartTime;
|
||||
this._recording = true;
|
||||
|
||||
this._markers = [];
|
||||
this._frames = [];
|
||||
this._memory = [];
|
||||
this._ticks = [];
|
||||
this._allocations = { sites: [], timestamps: [], frames: [], counts: [] };
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stops recording with the PerformanceFront.
|
||||
*
|
||||
* @param object options
|
||||
* @see RecordingModel.prototype.startRecording
|
||||
*/
|
||||
stopRecording: Task.async(function *() {
|
||||
let info = yield this._front.stopRecording();
|
||||
stopRecording: Task.async(function *(options) {
|
||||
let info = yield this._front.stopRecording(options);
|
||||
this._profile = info.profile;
|
||||
this._duration = info.profilerEndTime - this._profilerStartTime;
|
||||
this._recording = false;
|
||||
@ -168,6 +175,14 @@ RecordingModel.prototype = {
|
||||
return this._ticks;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the memory allocations data in this recording.
|
||||
* @return array
|
||||
*/
|
||||
getAllocations: function() {
|
||||
return this._allocations;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the profiler data in this recording.
|
||||
* @return array
|
||||
@ -186,8 +201,9 @@ RecordingModel.prototype = {
|
||||
let frames = this.getFrames();
|
||||
let memory = this.getMemory();
|
||||
let ticks = this.getTicks();
|
||||
let allocations = this.getAllocations();
|
||||
let profile = this.getProfile();
|
||||
return { label, duration, markers, frames, memory, ticks, profile };
|
||||
return { label, duration, markers, frames, memory, ticks, allocations, profile };
|
||||
},
|
||||
|
||||
/**
|
||||
@ -209,35 +225,50 @@ RecordingModel.prototype = {
|
||||
}
|
||||
|
||||
switch (eventName) {
|
||||
// Accumulate markers into an array. Furthermore, timestamps do not
|
||||
// have a zero epoch, so offset all of them by the timeline's start time.
|
||||
case "markers":
|
||||
// Accumulate timeline markers into an array. Furthermore, the timestamps
|
||||
// do not have a zero epoch, so offset all of them by the start time.
|
||||
case "markers": {
|
||||
let [markers] = data;
|
||||
RecordingUtils.offsetMarkerTimes(markers, this._timelineStartTime);
|
||||
Array.prototype.push.apply(this._markers, markers);
|
||||
break;
|
||||
|
||||
}
|
||||
// Accumulate stack frames into an array.
|
||||
case "frames":
|
||||
case "frames": {
|
||||
let [, frames] = data;
|
||||
Array.prototype.push.apply(this._frames, frames);
|
||||
break;
|
||||
|
||||
// Accumulate memory measurements into an array. Furthermore, the
|
||||
// timestamp does not have a zero epoch, so offset it.
|
||||
case "memory":
|
||||
}
|
||||
// Accumulate memory measurements into an array. Furthermore, the timestamp
|
||||
// does not have a zero epoch, so offset it by the actor's start time.
|
||||
case "memory": {
|
||||
let [currentTime, measurement] = data;
|
||||
this._memory.push({
|
||||
delta: currentTime - this._timelineStartTime,
|
||||
value: measurement.total / 1024 / 1024
|
||||
});
|
||||
break;
|
||||
|
||||
}
|
||||
// Save the accumulated refresh driver ticks.
|
||||
case "ticks":
|
||||
case "ticks": {
|
||||
let [, timestamps] = data;
|
||||
this._ticks = timestamps;
|
||||
break;
|
||||
}
|
||||
// Accumulate allocation sites into an array. Furthermore, the timestamps
|
||||
// do not have a zero epoch, and are microseconds instead of milliseconds,
|
||||
// so offset all of them by the start time, also converting from µs to ms.
|
||||
case "allocations": {
|
||||
let [{ sites, timestamps, frames, counts }] = data;
|
||||
let timeOffset = this._memoryStartTime * 1000;
|
||||
let timeScale = 1000;
|
||||
RecordingUtils.offsetAndScaleTimestamps(timestamps, timeOffset, timeScale);
|
||||
Array.prototype.push.apply(this._allocations.sites, sites);
|
||||
Array.prototype.push.apply(this._allocations.timestamps, timestamps);
|
||||
Array.prototype.push.apply(this._allocations.frames, frames);
|
||||
Array.prototype.push.apply(this._allocations.counts, counts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -58,6 +58,29 @@ exports.RecordingUtils.offsetMarkerTimes = function(markers, timeOffset) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offsets and scales all the timestamps in the provided array by the
|
||||
* specified time and scale factor.
|
||||
*
|
||||
* @param array array
|
||||
* A list of timestamps received from the backend.
|
||||
* @param number timeOffset
|
||||
* The amount of time to offset by (in milliseconds).
|
||||
* @param number timeScale
|
||||
* The factor to scale by, after offsetting.
|
||||
*/
|
||||
exports.RecordingUtils.offsetAndScaleTimestamps = function(timestamps, timeOffset, timeScale) {
|
||||
for (let i = 0, len = timestamps.length; i < len; i++) {
|
||||
timestamps[i] -= timeOffset;
|
||||
timestamps[i] /= timeScale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache used in `RecordingUtils.getSamplesFromAllocations`.
|
||||
*/
|
||||
let gSamplesFromAllocationCache = new WeakMap();
|
||||
|
||||
/**
|
||||
* Converts allocation data from the memory actor to something that follows
|
||||
* the same structure as the samples data received from the profiler.
|
||||
@ -70,6 +93,11 @@ exports.RecordingUtils.offsetMarkerTimes = function(markers, timeOffset) {
|
||||
* The samples data.
|
||||
*/
|
||||
exports.RecordingUtils.getSamplesFromAllocations = function(allocations) {
|
||||
let cached = gSamplesFromAllocationCache.get(allocations);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let { sites, timestamps, frames, counts } = allocations;
|
||||
let samples = [];
|
||||
|
||||
@ -99,5 +127,6 @@ exports.RecordingUtils.getSamplesFromAllocations = function(allocations) {
|
||||
sample.frames.reverse();
|
||||
}
|
||||
|
||||
gSamplesFromAllocationCache.set(allocations, samples);
|
||||
return samples;
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ PerformancePanel.prototype = {
|
||||
}
|
||||
|
||||
// Destroy the connection to ensure packet handlers are removed from client.
|
||||
this._connection.destroy();
|
||||
yield this._connection.destroy();
|
||||
|
||||
yield this.panelWin.shutdownPerformance();
|
||||
this.emit("destroyed");
|
||||
|
@ -21,6 +21,8 @@ devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
|
||||
"devtools/timeline/global", true);
|
||||
devtools.lazyRequireGetter(this, "L10N",
|
||||
"devtools/profiler/global", true);
|
||||
devtools.lazyRequireGetter(this, "RecordingUtils",
|
||||
"devtools/performance/recording-utils", true);
|
||||
devtools.lazyRequireGetter(this, "RecordingModel",
|
||||
"devtools/performance/recording-model", true);
|
||||
devtools.lazyRequireGetter(this, "MarkersOverview",
|
||||
@ -98,14 +100,20 @@ const EVENTS = {
|
||||
// Emitted by the DetailsView when a subview is selected
|
||||
DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",
|
||||
|
||||
// Emitted by the CallTreeView when a call tree has been rendered
|
||||
CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered",
|
||||
|
||||
// Emitted by the WaterfallView when it has been rendered
|
||||
WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
|
||||
|
||||
// Emitted by the FlameGraphView when it has been rendered
|
||||
FLAMEGRAPH_RENDERED: "Performance:UI:FlameGraphRendered",
|
||||
// Emitted by the JsCallTreeView when a call tree has been rendered
|
||||
JS_CALL_TREE_RENDERED: "Performance:UI:JsCallTreeRendered",
|
||||
|
||||
// Emitted by the JsFlameGraphView when it has been rendered
|
||||
JS_FLAMEGRAPH_RENDERED: "Performance:UI:JsFlameGraphRendered",
|
||||
|
||||
// Emitted by the MemoryCallTreeView when a call tree has been rendered
|
||||
MEMORY_CALL_TREE_RENDERED: "Performance:UI:MemoryCallTreeRendered",
|
||||
|
||||
// Emitted by the MemoryFlameGraphView when it has been rendered
|
||||
MEMORY_FLAMEGRAPH_RENDERED: "Performance:UI:MemoryFlameGraphRendered",
|
||||
|
||||
// When a source is shown in the JavaScript Debugger at a specific location.
|
||||
SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
|
||||
@ -184,10 +192,11 @@ let PerformanceController = {
|
||||
RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
|
||||
RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
|
||||
|
||||
gFront.on("ticks", this._onTimelineData); // framerate
|
||||
gFront.on("markers", this._onTimelineData); // timeline markers
|
||||
gFront.on("frames", this._onTimelineData); // stack frames
|
||||
gFront.on("memory", this._onTimelineData); // memory measurements
|
||||
gFront.on("ticks", this._onTimelineData); // framerate
|
||||
gFront.on("allocations", this._onTimelineData); // memory allocations
|
||||
}),
|
||||
|
||||
/**
|
||||
@ -201,10 +210,11 @@ let PerformanceController = {
|
||||
RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
|
||||
RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
|
||||
|
||||
gFront.off("ticks", this._onTimelineData);
|
||||
gFront.off("markers", this._onTimelineData);
|
||||
gFront.off("frames", this._onTimelineData);
|
||||
gFront.off("memory", this._onTimelineData);
|
||||
gFront.off("ticks", this._onTimelineData);
|
||||
gFront.off("allocations", this._onTimelineData);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -223,7 +233,11 @@ let PerformanceController = {
|
||||
let recording = this._createRecording();
|
||||
|
||||
this.emit(EVENTS.RECORDING_WILL_START, recording);
|
||||
yield recording.startRecording({ withTicks: true, withMemory: true });
|
||||
yield recording.startRecording({
|
||||
withTicks: true,
|
||||
withMemory: true,
|
||||
withAllocations: true
|
||||
});
|
||||
this.emit(EVENTS.RECORDING_STARTED, recording);
|
||||
|
||||
this.setCurrentRecording(recording);
|
||||
@ -237,7 +251,9 @@ let PerformanceController = {
|
||||
let recording = this._getLatestRecording();
|
||||
|
||||
this.emit(EVENTS.RECORDING_WILL_STOP, recording);
|
||||
yield recording.stopRecording();
|
||||
yield recording.stopRecording({
|
||||
withAllocations: true
|
||||
});
|
||||
this.emit(EVENTS.RECORDING_STOPPED, recording);
|
||||
}),
|
||||
|
||||
@ -318,7 +334,7 @@ let PerformanceController = {
|
||||
* Fired whenever the PerformanceFront emits markers, memory or ticks.
|
||||
*/
|
||||
_onTimelineData: function (...data) {
|
||||
this._recordings.forEach(profile => profile.addTimelineData.apply(profile, data));
|
||||
this._recordings.forEach(e => e.addTimelineData.apply(e, data));
|
||||
this.emit(EVENTS.TIMELINE_DATA, ...data);
|
||||
},
|
||||
|
||||
|
@ -20,9 +20,11 @@
|
||||
<script type="application/javascript" src="performance/views/overview.js"/>
|
||||
<script type="application/javascript" src="performance/views/toolbar.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-subview.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-call-tree.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-waterfall.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-flamegraph.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-js-call-tree.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-js-flamegraph.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-memory-call-tree.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-memory-flamegraph.js"/>
|
||||
<script type="application/javascript" src="performance/views/details.js"/>
|
||||
<script type="application/javascript" src="performance/views/recordings.js"/>
|
||||
|
||||
@ -60,14 +62,25 @@
|
||||
<toolbar id="performance-toolbar" class="devtools-toolbar">
|
||||
<hbox id="performance-toolbar-controls-detail-views" class="devtools-toolbarbutton-group">
|
||||
<toolbarbutton id="select-waterfall-view"
|
||||
class="devtools-toolbarbutton"
|
||||
class="devtools-toolbarbutton devtools-button"
|
||||
label="&profilerUI.toolbar.waterfall;"
|
||||
data-view="waterfall" />
|
||||
<toolbarbutton id="select-calltree-view"
|
||||
class="devtools-toolbarbutton"
|
||||
data-view="calltree" />
|
||||
<toolbarbutton id="select-flamegraph-view"
|
||||
class="devtools-toolbarbutton"
|
||||
data-view="flamegraph" />
|
||||
<toolbarbutton id="select-js-calltree-view"
|
||||
class="devtools-toolbarbutton devtools-button"
|
||||
label="&profilerUI.toolbar.js-calltree;"
|
||||
data-view="js-calltree" />
|
||||
<toolbarbutton id="select-js-flamegraph-view"
|
||||
class="devtools-toolbarbutton devtools-button"
|
||||
label="&profilerUI.toolbar.js-flamegraph;"
|
||||
data-view="js-flamegraph" />
|
||||
<toolbarbutton id="select-memory-calltree-view"
|
||||
class="devtools-toolbarbutton devtools-button"
|
||||
label="&profilerUI.toolbar.memory-calltree;"
|
||||
data-view="memory-calltree" />
|
||||
<toolbarbutton id="select-memory-flamegraph-view"
|
||||
class="devtools-toolbarbutton devtools-button"
|
||||
label="&profilerUI.toolbar.memory-flamegraph;"
|
||||
data-view="memory-flamegraph" />
|
||||
</hbox>
|
||||
<spacer flex="1"></spacer>
|
||||
<hbox id="performance-toolbar-control-options" class="devtools-toolbarbutton-group">
|
||||
@ -94,12 +107,12 @@
|
||||
height="150"/>
|
||||
</hbox>
|
||||
|
||||
<vbox id="calltree-view" flex="1">
|
||||
<vbox id="js-calltree-view" flex="1">
|
||||
<hbox class="call-tree-headers-container">
|
||||
<label class="plain call-tree-header"
|
||||
type="duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalDuration;"/>
|
||||
value="&profilerUI.table.totalDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="percentage"
|
||||
crop="end"
|
||||
@ -107,7 +120,7 @@
|
||||
<label class="plain call-tree-header"
|
||||
type="self-duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfDuration;"/>
|
||||
value="&profilerUI.table.selfDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-percentage"
|
||||
crop="end"
|
||||
@ -123,9 +136,53 @@
|
||||
</hbox>
|
||||
<vbox class="call-tree-cells-container" flex="1"/>
|
||||
</vbox>
|
||||
<hbox id="flamegraph-view" flex="1">
|
||||
|
||||
<hbox id="js-flamegraph-view" flex="1">
|
||||
</hbox>
|
||||
|
||||
<vbox id="memory-calltree-view" flex="1">
|
||||
<hbox class="call-tree-headers-container">
|
||||
<label class="plain call-tree-header"
|
||||
type="duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="percentage"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalPercentage;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="allocations"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalAlloc;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-percentage"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfPercentage;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-allocations"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfAlloc;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="samples"
|
||||
crop="end"
|
||||
value="&profilerUI.table.samples;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="function"
|
||||
crop="end"
|
||||
value="&profilerUI.table.function;"/>
|
||||
</hbox>
|
||||
<vbox class="call-tree-cells-container" flex="1"/>
|
||||
</vbox>
|
||||
|
||||
<hbox id="memory-flamegraph-view" flex="1">
|
||||
<!-- TODO: bug 1077461 -->
|
||||
</hbox>
|
||||
</deck>
|
||||
|
||||
</vbox>
|
||||
</hbox>
|
||||
</window>
|
||||
|
@ -14,6 +14,8 @@ support-files =
|
||||
[browser_perf-data-samples.js]
|
||||
[browser_perf-details-calltree-render.js]
|
||||
[browser_perf-details-flamegraph-render.js]
|
||||
[browser_perf-details-memory-calltree-render.js]
|
||||
[browser_perf-details-memory-flamegraph-render.js]
|
||||
[browser_perf-details-waterfall-render.js]
|
||||
[browser_perf-details-01.js]
|
||||
[browser_perf-details-02.js]
|
||||
@ -28,7 +30,8 @@ support-files =
|
||||
[browser_perf-front.js]
|
||||
[browser_perf-jump-to-debugger-01.js]
|
||||
[browser_perf-jump-to-debugger-02.js]
|
||||
[browser_perf-options-invert-call-tree.js]
|
||||
[browser_perf-options-invert-call-tree-01.js]
|
||||
[browser_perf-options-invert-call-tree-02.js]
|
||||
[browser_perf-overview-render-01.js]
|
||||
[browser_perf-overview-render-02.js]
|
||||
[browser_perf-overview-render-03.js]
|
||||
|
@ -13,10 +13,17 @@ function spawnTest () {
|
||||
|
||||
// Select calltree view
|
||||
let viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
|
||||
command($("toolbarbutton[data-view='calltree']"));
|
||||
command($("toolbarbutton[data-view='js-calltree']"));
|
||||
let [_, viewName] = yield viewChanged;
|
||||
is(viewName, "calltree", "DETAILS_VIEW_SELECTED fired with view name");
|
||||
checkViews(DetailsView, doc, "calltree");
|
||||
is(viewName, "js-calltree", "DETAILS_VIEW_SELECTED fired with view name");
|
||||
checkViews(DetailsView, doc, "js-calltree");
|
||||
|
||||
// Select flamegraph view
|
||||
viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
|
||||
command($("toolbarbutton[data-view='js-flamegraph']"));
|
||||
[_, viewName] = yield viewChanged;
|
||||
is(viewName, "js-flamegraph", "DETAILS_VIEW_SELECTED fired with view name");
|
||||
checkViews(DetailsView, doc, "js-flamegraph");
|
||||
|
||||
// Select waterfall view
|
||||
viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
|
||||
@ -25,13 +32,6 @@ function spawnTest () {
|
||||
is(viewName, "waterfall", "DETAILS_VIEW_SELECTED fired with view name");
|
||||
checkViews(DetailsView, doc, "waterfall");
|
||||
|
||||
// Select flamegraph view
|
||||
viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
|
||||
command($("toolbarbutton[data-view='flamegraph']"));
|
||||
[_, viewName] = yield viewChanged;
|
||||
is(viewName, "flamegraph", "DETAILS_VIEW_SELECTED fired with view name");
|
||||
checkViews(DetailsView, doc, "flamegraph");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
@ -7,25 +7,25 @@
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, DetailsView } = panel.panelWin;
|
||||
let { WaterfallView, CallTreeView, FlameGraphView } = panel.panelWin;
|
||||
let { WaterfallView, JsCallTreeView, JsFlameGraphView } = panel.panelWin;
|
||||
|
||||
ok(DetailsView.isViewSelected(WaterfallView),
|
||||
"The waterfall view is selected by default in the details view.");
|
||||
|
||||
let selected = DetailsView.whenViewSelected(CallTreeView);
|
||||
let selected = DetailsView.whenViewSelected(JsCallTreeView);
|
||||
let notified = DetailsView.once(EVENTS.DETAILS_VIEW_SELECTED);
|
||||
DetailsView.selectView("calltree");
|
||||
DetailsView.selectView("js-calltree");
|
||||
yield Promise.all([selected, notified]);
|
||||
|
||||
ok(DetailsView.isViewSelected(CallTreeView),
|
||||
ok(DetailsView.isViewSelected(JsCallTreeView),
|
||||
"The waterfall view is now selected in the details view.");
|
||||
|
||||
selected = DetailsView.whenViewSelected(FlameGraphView);
|
||||
selected = DetailsView.whenViewSelected(JsFlameGraphView);
|
||||
notified = DetailsView.once(EVENTS.DETAILS_VIEW_SELECTED);
|
||||
DetailsView.selectView("flamegraph");
|
||||
DetailsView.selectView("js-flamegraph");
|
||||
yield Promise.all([selected, notified]);
|
||||
|
||||
ok(DetailsView.isViewSelected(FlameGraphView),
|
||||
ok(DetailsView.isViewSelected(JsFlameGraphView),
|
||||
"The flamegraph view is now selected in the details view.");
|
||||
|
||||
selected = DetailsView.whenViewSelected(WaterfallView);
|
||||
|
@ -6,28 +6,28 @@
|
||||
*/
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, DetailsView, CallTreeView } = panel.panelWin;
|
||||
let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
|
||||
|
||||
DetailsView.selectView("calltree");
|
||||
ok(DetailsView.isViewSelected(CallTreeView), "The call tree is now selected.");
|
||||
DetailsView.selectView("js-calltree");
|
||||
ok(DetailsView.isViewSelected(JsCallTreeView), "The call tree is now selected.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
let rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
|
||||
let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "CallTreeView rendered after recording is stopped.");
|
||||
ok(true, "JsCallTreeView rendered after recording is stopped.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
|
||||
rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "CallTreeView rendered again after recording completed a second time.");
|
||||
ok(true, "JsCallTreeView rendered again after recording completed a second time.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
|
@ -6,28 +6,28 @@
|
||||
*/
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, DetailsView, FlameGraphView } = panel.panelWin;
|
||||
let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
|
||||
|
||||
DetailsView.selectView("flamegraph");
|
||||
ok(DetailsView.isViewSelected(FlameGraphView), "The flamegraph is now selected.");
|
||||
DetailsView.selectView("js-flamegraph");
|
||||
ok(DetailsView.isViewSelected(JsFlameGraphView), "The flamegraph is now selected.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
let rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
|
||||
let rendered = once(JsFlameGraphView, EVENTS.JS_FLAMEGRAPH_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "FlameGraphView rendered after recording is stopped.");
|
||||
ok(true, "JsFlameGraphView rendered after recording is stopped.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
|
||||
rendered = once(JsFlameGraphView, EVENTS.JS_FLAMEGRAPH_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "FlameGraphView rendered again after recording completed a second time.");
|
||||
ok(true, "JsFlameGraphView rendered again after recording completed a second time.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
|
@ -0,0 +1,34 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests that the memory call tree view renders content after recording.
|
||||
*/
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
|
||||
|
||||
DetailsView.selectView("memory-calltree");
|
||||
ok(DetailsView.isViewSelected(MemoryCallTreeView), "The call tree is now selected.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
let rendered = once(MemoryCallTreeView, EVENTS.MEMORY_CALL_TREE_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "MemoryCallTreeView rendered after recording is stopped.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
rendered = once(MemoryCallTreeView, EVENTS.MEMORY_CALL_TREE_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "MemoryCallTreeView rendered again after recording completed a second time.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests that the memory flamegraph view renders content after recording.
|
||||
*/
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
|
||||
|
||||
DetailsView.selectView("memory-flamegraph");
|
||||
ok(DetailsView.isViewSelected(MemoryFlameGraphView), "The flamegraph is now selected.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
let rendered = once(MemoryFlameGraphView, EVENTS.MEMORY_FLAMEGRAPH_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "MemoryFlameGraphView rendered after recording is stopped.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
rendered = once(MemoryFlameGraphView, EVENTS.MEMORY_FLAMEGRAPH_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "MemoryFlameGraphView rendered again after recording completed a second time.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
@ -10,26 +10,44 @@ let WAIT_TIME = 1000;
|
||||
function spawnTest () {
|
||||
let { target, front } = yield initBackend(SIMPLE_URL);
|
||||
|
||||
let { profilerStartTime, timelineStartTime } = yield front.startRecording();
|
||||
let {
|
||||
profilerStartTime,
|
||||
timelineStartTime,
|
||||
memoryStartTime
|
||||
} = yield front.startRecording({
|
||||
withAllocations: true
|
||||
});
|
||||
|
||||
ok(typeof profilerStartTime === "number",
|
||||
"The front.startRecording() emits a profiler start time.");
|
||||
ok(typeof timelineStartTime === "number",
|
||||
"The front.startRecording() emits a timeline start time.");
|
||||
ok(typeof memoryStartTime === "number",
|
||||
"The front.startRecording() emits a memory start time.");
|
||||
|
||||
yield busyWait(WAIT_TIME);
|
||||
|
||||
let { profilerEndTime, timelineEndTime } = yield front.stopRecording();
|
||||
let {
|
||||
profilerEndTime,
|
||||
timelineEndTime,
|
||||
memoryEndTime
|
||||
} = yield front.stopRecording({
|
||||
withAllocations: true
|
||||
});
|
||||
|
||||
ok(typeof profilerEndTime === "number",
|
||||
"The front.stopRecording() emits a profiler end time.");
|
||||
ok(typeof timelineEndTime === "number",
|
||||
"The front.stopRecording() emits a timeline end time.");
|
||||
ok(typeof memoryEndTime === "number",
|
||||
"The front.stopRecording() emits a memory end time.");
|
||||
|
||||
ok(profilerEndTime > profilerStartTime,
|
||||
"The profilerEndTime is after profilerStartTime.");
|
||||
ok(timelineEndTime > timelineStartTime,
|
||||
"The timelineEndTime is after timelineStartTime.");
|
||||
ok(memoryEndTime > memoryStartTime,
|
||||
"The memoryEndTime is after memoryStartTime.");
|
||||
|
||||
yield removeTab(target.tab);
|
||||
finish();
|
||||
|
@ -0,0 +1,40 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const INVERT_PREF = "devtools.performance.ui.invert-call-tree";
|
||||
|
||||
/**
|
||||
* Tests that the js call tree view is re-rendered after the
|
||||
* "invert-call-tree" pref is changed.
|
||||
*/
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
|
||||
|
||||
Services.prefs.setBoolPref(INVERT_PREF, true);
|
||||
|
||||
DetailsView.selectView("js-calltree");
|
||||
ok(DetailsView.isViewSelected(JsCallTreeView), "The call tree is now selected.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
|
||||
Services.prefs.setBoolPref(INVERT_PREF, false);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "JsCallTreeView rerendered when toggling invert-call-tree.");
|
||||
|
||||
rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
|
||||
Services.prefs.setBoolPref(INVERT_PREF, true);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "JsCallTreeView rerendered when toggling back invert-call-tree.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const INVERT_PREF = "devtools.performance.ui.invert-call-tree";
|
||||
|
||||
/**
|
||||
* Tests that the memory call tree view is re-rendered after the
|
||||
* "invert-call-tree" pref is changed.
|
||||
*/
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
|
||||
|
||||
Services.prefs.setBoolPref(INVERT_PREF, true);
|
||||
|
||||
DetailsView.selectView("memory-calltree");
|
||||
ok(DetailsView.isViewSelected(MemoryCallTreeView), "The call tree is now selected.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
let rendered = once(MemoryCallTreeView, EVENTS.MEMORY_CALL_TREE_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
rendered = once(MemoryCallTreeView, EVENTS.MEMORY_CALL_TREE_RENDERED);
|
||||
Services.prefs.setBoolPref(INVERT_PREF, false);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "MemoryCallTreeView rerendered when toggling invert-call-tree.");
|
||||
|
||||
rendered = once(MemoryCallTreeView, EVENTS.MEMORY_CALL_TREE_RENDERED);
|
||||
Services.prefs.setBoolPref(INVERT_PREF, true);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "MemoryCallTreeView rerendered when toggling back invert-call-tree.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const INVERT_PREF = "devtools.performance.ui.invert-call-tree";
|
||||
|
||||
/**
|
||||
* Tests that the call tree view renders after recording.
|
||||
*/
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, DetailsView, CallTreeView } = panel.panelWin;
|
||||
|
||||
Services.prefs.setBoolPref(INVERT_PREF, true);
|
||||
|
||||
DetailsView.selectView("calltree");
|
||||
ok(DetailsView.isViewSelected(CallTreeView), "The call tree is now selected.");
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
|
||||
let rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
|
||||
Services.prefs.setBoolPref(INVERT_PREF, false);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "CallTreeView rerendered when toggling invert-call-tree.");
|
||||
|
||||
rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
|
||||
Services.prefs.setBoolPref(INVERT_PREF, true);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "CallTreeView rerendered when toggling back invert-call-tree.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
@ -53,9 +53,9 @@ function spawnTest () {
|
||||
OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, fail);
|
||||
|
||||
let secondInterval = OverviewView.getTimeInterval();
|
||||
is(secondInterval.startTime, 30,
|
||||
is(Math.round(secondInterval.startTime), 30,
|
||||
"The interval's start time was properly set again.");
|
||||
is(secondInterval.endTime, 40,
|
||||
is(Math.round(secondInterval.endTime), 40,
|
||||
"The interval's end time was properly set again.");
|
||||
|
||||
yield teardown(panel);
|
||||
|
@ -7,14 +7,14 @@
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, PerformanceController, OverviewView, DetailsView } = panel.panelWin;
|
||||
let { WaterfallView, CallTreeView, FlameGraphView } = panel.panelWin;
|
||||
let { WaterfallView, JsCallTreeView, JsFlameGraphView } = panel.panelWin;
|
||||
|
||||
let updatedWaterfall = 0;
|
||||
let updatedCallTree = 0;
|
||||
let updatedFlameGraph = 0;
|
||||
WaterfallView.on(EVENTS.WATERFALL_RENDERED, () => updatedWaterfall++);
|
||||
CallTreeView.on(EVENTS.CALL_TREE_RENDERED, () => updatedCallTree++);
|
||||
FlameGraphView.on(EVENTS.FLAMEGRAPH_RENDERED, () => updatedFlameGraph++);
|
||||
JsCallTreeView.on(EVENTS.JS_CALL_TREE_RENDERED, () => updatedCallTree++);
|
||||
JsFlameGraphView.on(EVENTS.JS_FLAMEGRAPH_RENDERED, () => updatedFlameGraph++);
|
||||
|
||||
yield startRecording(panel);
|
||||
yield busyWait(100);
|
||||
@ -26,23 +26,23 @@ function spawnTest () {
|
||||
yield rendered;
|
||||
ok(true, "Waterfall rerenders when a range in the overview graph is selected.");
|
||||
|
||||
rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
|
||||
DetailsView.selectView("calltree");
|
||||
rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
|
||||
DetailsView.selectView("js-calltree");
|
||||
yield rendered;
|
||||
ok(true, "Call tree rerenders after its corresponding pane is shown.");
|
||||
|
||||
rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
|
||||
DetailsView.selectView("flamegraph");
|
||||
rendered = once(JsFlameGraphView, EVENTS.JS_FLAMEGRAPH_RENDERED);
|
||||
DetailsView.selectView("js-flamegraph");
|
||||
yield rendered;
|
||||
ok(true, "Flamegraph rerenders after its corresponding pane is shown.");
|
||||
|
||||
rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
|
||||
rendered = once(JsFlameGraphView, EVENTS.JS_FLAMEGRAPH_RENDERED);
|
||||
OverviewView.emit(EVENTS.OVERVIEW_RANGE_CLEARED);
|
||||
yield rendered;
|
||||
ok(true, "Flamegraph rerenders when a range in the overview graph is removed.");
|
||||
|
||||
rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
|
||||
DetailsView.selectView("calltree");
|
||||
rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
|
||||
DetailsView.selectView("js-calltree");
|
||||
yield rendered;
|
||||
ok(true, "Call tree rerenders after its corresponding pane is shown.");
|
||||
|
||||
@ -52,8 +52,8 @@ function spawnTest () {
|
||||
ok(true, "Waterfall rerenders after its corresponding pane is shown.");
|
||||
|
||||
is(updatedWaterfall, 3, "WaterfallView rerendered 3 times.");
|
||||
is(updatedCallTree, 2, "CallTreeView rerendered 2 times.");
|
||||
is(updatedFlameGraph, 2, "FlameGraphView rerendered 2 times.");
|
||||
is(updatedCallTree, 2, "JsCallTreeView rerendered 2 times.");
|
||||
is(updatedFlameGraph, 2, "JsFlameGraphView rerendered 2 times.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
|
@ -56,8 +56,10 @@ let test = Task.async(function*() {
|
||||
"The impored data is identical to the original data (4).");
|
||||
is(importedData.ticks.toSource(), originalData.ticks.toSource(),
|
||||
"The impored data is identical to the original data (5).");
|
||||
is(importedData.profile.toSource(), originalData.profile.toSource(),
|
||||
is(importedData.allocations.toSource(), originalData.allocations.toSource(),
|
||||
"The impored data is identical to the original data (6).");
|
||||
is(importedData.profile.toSource(), originalData.profile.toSource(),
|
||||
"The impored data is identical to the original data (7).");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
|
@ -54,16 +54,18 @@ let test = Task.async(function*() {
|
||||
"The imported legacy data was successfully converted for the current tool (1).");
|
||||
is(importedData.duration, data.duration,
|
||||
"The imported legacy data was successfully converted for the current tool (2).");
|
||||
is(importedData.markers.toSource(), [].toSource(),
|
||||
is(importedData.markers.toSource(), data.markers.toSource(),
|
||||
"The imported legacy data was successfully converted for the current tool (3).");
|
||||
is(importedData.frames.toSource(), [].toSource(),
|
||||
is(importedData.frames.toSource(), data.frames.toSource(),
|
||||
"The imported legacy data was successfully converted for the current tool (4).");
|
||||
is(importedData.memory.toSource(), [].toSource(),
|
||||
is(importedData.memory.toSource(), data.memory.toSource(),
|
||||
"The imported legacy data was successfully converted for the current tool (5).");
|
||||
is(importedData.ticks.toSource(), data.ticks.toSource(),
|
||||
"The imported legacy data was successfully converted for the current tool (6).");
|
||||
is(importedData.profile.toSource(), data.profile.toSource(),
|
||||
is(importedData.allocations.toSource(), data.allocations.toSource(),
|
||||
"The imported legacy data was successfully converted for the current tool (7).");
|
||||
is(importedData.profile.toSource(), data.profile.toSource(),
|
||||
"The imported legacy data was successfully converted for the current tool (8).");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
|
@ -298,9 +298,9 @@ function waitForWidgetsRendered(panel) {
|
||||
let {
|
||||
EVENTS,
|
||||
OverviewView,
|
||||
CallTreeView,
|
||||
WaterfallView,
|
||||
FlameGraphView
|
||||
JsCallTreeView,
|
||||
JsFlameGraphView
|
||||
} = panel.panelWin;
|
||||
|
||||
return Promise.all([
|
||||
@ -309,8 +309,8 @@ function waitForWidgetsRendered(panel) {
|
||||
once(OverviewView, EVENTS.FRAMERATE_GRAPH_RENDERED),
|
||||
once(OverviewView, EVENTS.OVERVIEW_RENDERED),
|
||||
once(WaterfallView, EVENTS.WATERFALL_RENDERED),
|
||||
once(CallTreeView, EVENTS.CALL_TREE_RENDERED),
|
||||
once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED)
|
||||
once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED),
|
||||
once(JsFlameGraphView, EVENTS.JS_FLAMEGRAPH_RENDERED)
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
/**
|
||||
* CallTree view containing profiler call tree, controlled by DetailsView.
|
||||
*/
|
||||
let CallTreeView = Heritage.extend(DetailsSubview, {
|
||||
let JsCallTreeView = Heritage.extend(DetailsSubview, {
|
||||
rangeChangeDebounceTime: 50, // ms
|
||||
|
||||
/**
|
||||
@ -43,7 +43,7 @@ let CallTreeView = Heritage.extend(DetailsSubview, {
|
||||
let profile = recording.getProfile();
|
||||
let threadNode = this._prepareCallTree(profile, interval, options);
|
||||
this._populateCallTree(threadNode, options);
|
||||
this.emit(EVENTS.CALL_TREE_RENDERED);
|
||||
this.emit(EVENTS.JS_CALL_TREE_RENDERED);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -94,10 +94,13 @@ let CallTreeView = Heritage.extend(DetailsSubview, {
|
||||
root.on("link", this._onLink);
|
||||
|
||||
// Clear out other call trees.
|
||||
let container = $(".call-tree-cells-container");
|
||||
let container = $("#js-calltree-view > .call-tree-cells-container");
|
||||
container.innerHTML = "";
|
||||
root.attachTo(container);
|
||||
|
||||
// Profiler data does not contain memory allocations information.
|
||||
root.toggleAllocations(false);
|
||||
|
||||
// When platform data isn't shown, hide the cateogry labels, since they're
|
||||
// only available for C++ frames.
|
||||
let contentOnly = !Prefs.showPlatformData;
|
@ -7,14 +7,14 @@
|
||||
* FlameGraph view containing a pyramid-like visualization of a profile,
|
||||
* controlled by DetailsView.
|
||||
*/
|
||||
let FlameGraphView = Heritage.extend(DetailsSubview, {
|
||||
let JsFlameGraphView = Heritage.extend(DetailsSubview, {
|
||||
/**
|
||||
* Sets up the view with event binding.
|
||||
*/
|
||||
initialize: Task.async(function* () {
|
||||
DetailsSubview.initialize.call(this);
|
||||
|
||||
this.graph = new FlameGraph($("#flamegraph-view"));
|
||||
this.graph = new FlameGraph($("#js-flamegraph-view"));
|
||||
this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
|
||||
yield this.graph.ready();
|
||||
|
||||
@ -61,7 +61,7 @@ let FlameGraphView = Heritage.extend(DetailsSubview, {
|
||||
}
|
||||
});
|
||||
|
||||
this.emit(EVENTS.FLAMEGRAPH_RENDERED);
|
||||
this.emit(EVENTS.JS_FLAMEGRAPH_RENDERED);
|
||||
},
|
||||
|
||||
/**
|
115
browser/devtools/performance/views/details-memory-call-tree.js
Normal file
115
browser/devtools/performance/views/details-memory-call-tree.js
Normal file
@ -0,0 +1,115 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* CallTree view containing memory allocation sites, controlled by DetailsView.
|
||||
*/
|
||||
let MemoryCallTreeView = Heritage.extend(DetailsSubview, {
|
||||
rangeChangeDebounceTime: 100, // ms
|
||||
|
||||
/**
|
||||
* Sets up the view with event binding.
|
||||
*/
|
||||
initialize: function () {
|
||||
DetailsSubview.initialize.call(this);
|
||||
|
||||
this._onPrefChanged = this._onPrefChanged.bind(this);
|
||||
this._onLink = this._onLink.bind(this);
|
||||
|
||||
PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unbinds events.
|
||||
*/
|
||||
destroy: function () {
|
||||
DetailsSubview.destroy.call(this);
|
||||
|
||||
PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method for handling all the set up for rendering a new call tree.
|
||||
*
|
||||
* @param object interval [optional]
|
||||
* The { startTime, endTime }, in milliseconds.
|
||||
* @param object options [optional]
|
||||
* Additional options for new the call tree.
|
||||
*/
|
||||
render: function (interval={}, options={}) {
|
||||
let recording = PerformanceController.getCurrentRecording();
|
||||
let allocations = recording.getAllocations();
|
||||
let threadNode = this._prepareCallTree(allocations, interval, options);
|
||||
this._populateCallTree(threadNode, options);
|
||||
this.emit(EVENTS.MEMORY_CALL_TREE_RENDERED);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fired on the "link" event for the call tree in this container.
|
||||
*/
|
||||
_onLink: function (_, treeItem) {
|
||||
let { url, line } = treeItem.frame.getInfo();
|
||||
viewSourceInDebugger(url, line).then(
|
||||
() => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER),
|
||||
() => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the recording is stopped and prepares data to
|
||||
* populate the call tree.
|
||||
*/
|
||||
_prepareCallTree: function (allocations, { startTime, endTime }, options) {
|
||||
let samples = RecordingUtils.getSamplesFromAllocations(allocations);
|
||||
let contentOnly = !Prefs.showPlatformData;
|
||||
let invertTree = PerformanceController.getPref("invert-call-tree");
|
||||
|
||||
let threadNode = new ThreadNode(samples,
|
||||
{ startTime, endTime, contentOnly, invertTree });
|
||||
|
||||
// If we have an empty profile (no samples), then don't invert the tree, as
|
||||
// it would hide the root node and a completely blank call tree space can be
|
||||
// mis-interpreted as an error.
|
||||
options.inverted = invertTree && threadNode.samples > 0;
|
||||
|
||||
return threadNode;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the call tree.
|
||||
*/
|
||||
_populateCallTree: function (frameNode, options={}) {
|
||||
let root = new CallView({
|
||||
frame: frameNode,
|
||||
inverted: options.inverted,
|
||||
// Root nodes are hidden in inverted call trees.
|
||||
hidden: options.inverted,
|
||||
// Memory call trees should be sorted by allocations.
|
||||
sortingPredicate: (a, b) => a.frame.allocations < b.frame.allocations ? 1 : -1,
|
||||
// Call trees should only auto-expand when not inverted. Passing undefined
|
||||
// will default to the CALL_TREE_AUTO_EXPAND depth.
|
||||
autoExpandDepth: options.inverted ? 0 : undefined,
|
||||
});
|
||||
|
||||
// Bind events.
|
||||
root.on("link", this._onLink);
|
||||
|
||||
// Clear out other call trees.
|
||||
let container = $("#memory-calltree-view > .call-tree-cells-container");
|
||||
container.innerHTML = "";
|
||||
root.attachTo(container);
|
||||
|
||||
// Memory allocation samples don't contain cateogry labels.
|
||||
root.toggleCategories(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a preference under "devtools.performance.ui." is changed.
|
||||
*/
|
||||
_onPrefChanged: function (_, prefName, value) {
|
||||
if (prefName === "invert-call-tree") {
|
||||
this.render(OverviewView.getTimeInterval());
|
||||
}
|
||||
}
|
||||
});
|
@ -0,0 +1,73 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* FlameGraph view containing a pyramid-like visualization of memory allocation
|
||||
* sites, controlled by DetailsView.
|
||||
*/
|
||||
let MemoryFlameGraphView = Heritage.extend(DetailsSubview, {
|
||||
/**
|
||||
* Sets up the view with event binding.
|
||||
*/
|
||||
initialize: Task.async(function* () {
|
||||
DetailsSubview.initialize.call(this);
|
||||
|
||||
this.graph = new FlameGraph($("#memory-flamegraph-view"));
|
||||
this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
|
||||
yield this.graph.ready();
|
||||
|
||||
this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
|
||||
|
||||
this.graph.on("selecting", this._onRangeChangeInGraph);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Unbinds events.
|
||||
*/
|
||||
destroy: function () {
|
||||
DetailsSubview.destroy.call(this);
|
||||
|
||||
this.graph.off("selecting", this._onRangeChangeInGraph);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method for handling all the set up for rendering a new flamegraph.
|
||||
*
|
||||
* @param object interval [optional]
|
||||
* The { startTime, endTime }, in milliseconds.
|
||||
*/
|
||||
render: function (interval={}) {
|
||||
let recording = PerformanceController.getCurrentRecording();
|
||||
let duration = recording.getDuration();
|
||||
let allocations = recording.getAllocations();
|
||||
|
||||
let samples = RecordingUtils.getSamplesFromAllocations(allocations);
|
||||
let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
|
||||
flattenRecursion: Prefs.flattenTreeRecursion,
|
||||
showIdleBlocks: Prefs.showIdleBlocks && L10N.getStr("table.idle")
|
||||
});
|
||||
|
||||
this.graph.setData({ data,
|
||||
bounds: {
|
||||
startTime: 0,
|
||||
endTime: duration
|
||||
},
|
||||
visible: {
|
||||
startTime: interval.startTime || 0,
|
||||
endTime: interval.endTime || duration
|
||||
}
|
||||
});
|
||||
|
||||
this.emit(EVENTS.MEMORY_FLAMEGRAPH_RENDERED);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fired when a range is selected or cleared in the FlameGraph.
|
||||
*/
|
||||
_onRangeChangeInGraph: function () {
|
||||
let interval = this.graph.getViewRange();
|
||||
OverviewView.setTimeInterval(interval, { stopPropagation: true });
|
||||
}
|
||||
});
|
@ -14,9 +14,11 @@ let DetailsView = {
|
||||
* Name to node+object mapping of subviews.
|
||||
*/
|
||||
components: {
|
||||
waterfall: { id: "waterfall-view", view: WaterfallView },
|
||||
calltree: { id: "calltree-view", view: CallTreeView },
|
||||
flamegraph: { id: "flamegraph-view", view: FlameGraphView }
|
||||
"waterfall": { id: "waterfall-view", view: WaterfallView },
|
||||
"js-calltree": { id: "js-calltree-view", view: JsCallTreeView },
|
||||
"js-flamegraph": { id: "js-flamegraph-view", view: JsFlameGraphView },
|
||||
"memory-calltree": { id: "memory-calltree-view", view: MemoryCallTreeView },
|
||||
"memory-flamegraph": { id: "memory-flamegraph-view", view: MemoryFlameGraphView }
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -112,7 +112,7 @@
|
||||
<label class="plain call-tree-header"
|
||||
type="duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalDuration;"/>
|
||||
value="&profilerUI.table.totalDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="percentage"
|
||||
crop="end"
|
||||
@ -120,7 +120,7 @@
|
||||
<label class="plain call-tree-header"
|
||||
type="self-duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfDuration;"/>
|
||||
value="&profilerUI.table.selfDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-percentage"
|
||||
crop="end"
|
||||
|
@ -9,7 +9,7 @@ function test() {
|
||||
let { FrameNode } = devtools.require("devtools/profiler/tree-model");
|
||||
|
||||
let frame1 = new FrameNode({
|
||||
location: "hello/<.world (http://foo/bar.js:123)",
|
||||
location: "hello/<.world (http://foo/bar.js:123:987)",
|
||||
line: 456
|
||||
});
|
||||
|
||||
@ -25,13 +25,15 @@ function test() {
|
||||
"The first frame node has the correct url.");
|
||||
is(frame1.getInfo().line, 123,
|
||||
"The first frame node has the correct line.");
|
||||
is(frame1.getInfo().column, 987,
|
||||
"The first frame node has the correct column.");
|
||||
is(frame1.getInfo().categoryData.toSource(), "({})",
|
||||
"The first frame node has the correct category data.");
|
||||
is(frame1.getInfo().isContent, true,
|
||||
"The first frame node has the correct content flag.");
|
||||
|
||||
let frame2 = new FrameNode({
|
||||
location: "hello/<.world (http://foo/bar.js#baz:123)",
|
||||
location: "hello/<.world (http://foo/bar.js#baz:123:987)",
|
||||
line: 456
|
||||
});
|
||||
|
||||
@ -47,13 +49,15 @@ function test() {
|
||||
"The second frame node has the correct url.");
|
||||
is(frame2.getInfo().line, 123,
|
||||
"The second frame node has the correct line.");
|
||||
is(frame2.getInfo().column, 987,
|
||||
"The second frame node has the correct column.");
|
||||
is(frame2.getInfo().categoryData.toSource(), "({})",
|
||||
"The second frame node has the correct category data.");
|
||||
is(frame2.getInfo().isContent, true,
|
||||
"The second frame node has the correct content flag.");
|
||||
|
||||
let frame3 = new FrameNode({
|
||||
location: "hello/<.world (http://foo/#bar:123)",
|
||||
location: "hello/<.world (http://foo/#bar:123:987)",
|
||||
line: 456
|
||||
});
|
||||
|
||||
@ -69,13 +73,15 @@ function test() {
|
||||
"The third frame node has the correct url.");
|
||||
is(frame3.getInfo().line, 123,
|
||||
"The third frame node has the correct line.");
|
||||
is(frame3.getInfo().column, 987,
|
||||
"The third frame node has the correct column.");
|
||||
is(frame3.getInfo().categoryData.toSource(), "({})",
|
||||
"The third frame node has the correct category data.");
|
||||
is(frame3.getInfo().isContent, true,
|
||||
"The third frame node has the correct content flag.");
|
||||
|
||||
let frame4 = new FrameNode({
|
||||
location: "hello/<.world (http://foo/:123)",
|
||||
location: "hello/<.world (http://foo/:123:987)",
|
||||
line: 456
|
||||
});
|
||||
|
||||
@ -91,13 +97,15 @@ function test() {
|
||||
"The fourth frame node has the correct url.");
|
||||
is(frame4.getInfo().line, 123,
|
||||
"The fourth frame node has the correct line.");
|
||||
is(frame4.getInfo().column, 987,
|
||||
"The fourth frame node has the correct column.");
|
||||
is(frame4.getInfo().categoryData.toSource(), "({})",
|
||||
"The fourth frame node has the correct category data.");
|
||||
is(frame4.getInfo().isContent, true,
|
||||
"The fourth frame node has the correct content flag.");
|
||||
|
||||
let frame5 = new FrameNode({
|
||||
location: "hello/<.world (resource://foo.js -> http://bar/baz.js:123)",
|
||||
location: "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
|
||||
line: 456
|
||||
});
|
||||
|
||||
@ -113,6 +121,8 @@ function test() {
|
||||
"The fifth frame node has the correct url.");
|
||||
is(frame5.getInfo().line, 123,
|
||||
"The fifth frame node has the correct line.");
|
||||
is(frame5.getInfo().column, 987,
|
||||
"The fifth frame node has the correct column.");
|
||||
is(frame5.getInfo().categoryData.toSource(), "({})",
|
||||
"The fifth frame node has the correct category data.");
|
||||
is(frame5.getInfo().isContent, false,
|
||||
@ -121,6 +131,7 @@ function test() {
|
||||
let frame6 = new FrameNode({
|
||||
location: "Foo::Bar::Baz",
|
||||
line: 456,
|
||||
column: 123,
|
||||
category: 8
|
||||
});
|
||||
|
||||
@ -136,6 +147,8 @@ function test() {
|
||||
"The sixth frame node has the correct url.");
|
||||
is(frame6.getInfo().line, 456,
|
||||
"The sixth frame node has the correct line.");
|
||||
is(frame6.getInfo().column, 123,
|
||||
"The sixth frame node has the correct column.");
|
||||
is(frame6.getInfo().categoryData.abbrev, "other",
|
||||
"The sixth frame node has the correct category data.");
|
||||
is(frame6.getInfo().isContent, false,
|
||||
@ -157,6 +170,8 @@ function test() {
|
||||
"The seventh frame node has the correct url.");
|
||||
is(frame7.getInfo().line, null,
|
||||
"The seventh frame node has the correct line.");
|
||||
is(frame7.getInfo().column, null,
|
||||
"The seventh frame node has the correct column.");
|
||||
is(frame7.getInfo().categoryData.abbrev, "js",
|
||||
"The seventh frame node has the correct category data.");
|
||||
is(frame7.getInfo().isContent, false,
|
||||
|
@ -22,14 +22,14 @@ function test() {
|
||||
is(container.childNodes[0].className, "call-tree-item",
|
||||
"The root node in the tree has the correct class name.");
|
||||
|
||||
is(container.childNodes[0].childNodes.length, 6,
|
||||
is(container.childNodes[0].childNodes.length, 8,
|
||||
"The root node in the tree has the correct number of children.");
|
||||
is(container.childNodes[0].querySelectorAll(".call-tree-cell").length, 6,
|
||||
is(container.childNodes[0].querySelectorAll(".call-tree-cell").length, 8,
|
||||
"The root node in the tree has only 'call-tree-cell' children.");
|
||||
|
||||
is(container.childNodes[0].childNodes[0].getAttribute("type"), "duration",
|
||||
"The root node in the tree has a duration cell.");
|
||||
is(container.childNodes[0].childNodes[0].getAttribute("value"), "15",
|
||||
is(container.childNodes[0].childNodes[0].getAttribute("value"), "15 ms",
|
||||
"The root node in the tree has the correct duration cell value.");
|
||||
|
||||
is(container.childNodes[0].childNodes[1].getAttribute("type"), "percentage",
|
||||
@ -37,24 +37,34 @@ function test() {
|
||||
is(container.childNodes[0].childNodes[1].getAttribute("value"), "100%",
|
||||
"The root node in the tree has the correct percentage cell value.");
|
||||
|
||||
is(container.childNodes[0].childNodes[2].getAttribute("type"), "self-duration",
|
||||
is(container.childNodes[0].childNodes[2].getAttribute("type"), "allocations",
|
||||
"The root node in the tree has a self-duration cell.");
|
||||
is(container.childNodes[0].childNodes[2].getAttribute("value"), "0",
|
||||
"The root node in the tree has the correct self-duration cell value.");
|
||||
|
||||
is(container.childNodes[0].childNodes[3].getAttribute("type"), "self-percentage",
|
||||
is(container.childNodes[0].childNodes[3].getAttribute("type"), "self-duration",
|
||||
"The root node in the tree has a self-duration cell.");
|
||||
is(container.childNodes[0].childNodes[3].getAttribute("value"), "0 ms",
|
||||
"The root node in the tree has the correct self-duration cell value.");
|
||||
|
||||
is(container.childNodes[0].childNodes[4].getAttribute("type"), "self-percentage",
|
||||
"The root node in the tree has a self-percentage cell.");
|
||||
is(container.childNodes[0].childNodes[3].getAttribute("value"), "0%",
|
||||
is(container.childNodes[0].childNodes[4].getAttribute("value"), "0%",
|
||||
"The root node in the tree has the correct self-percentage cell value.");
|
||||
|
||||
is(container.childNodes[0].childNodes[4].getAttribute("type"), "samples",
|
||||
is(container.childNodes[0].childNodes[5].getAttribute("type"), "self-allocations",
|
||||
"The root node in the tree has a self-percentage cell.");
|
||||
is(container.childNodes[0].childNodes[5].getAttribute("value"), "0",
|
||||
"The root node in the tree has the correct self-percentage cell value.");
|
||||
|
||||
is(container.childNodes[0].childNodes[6].getAttribute("type"), "samples",
|
||||
"The root node in the tree has an samples cell.");
|
||||
is(container.childNodes[0].childNodes[4].getAttribute("value"), "4",
|
||||
is(container.childNodes[0].childNodes[6].getAttribute("value"), "4",
|
||||
"The root node in the tree has the correct samples cell value.");
|
||||
|
||||
is(container.childNodes[0].childNodes[5].getAttribute("type"), "function",
|
||||
is(container.childNodes[0].childNodes[7].getAttribute("type"), "function",
|
||||
"The root node in the tree has a function cell.");
|
||||
is(container.childNodes[0].childNodes[5].style.MozMarginStart, "0px",
|
||||
is(container.childNodes[0].childNodes[7].style.MozMarginStart, "0px",
|
||||
"The root node in the tree has the correct indentation.");
|
||||
|
||||
finish();
|
||||
|
@ -27,7 +27,7 @@ function test() {
|
||||
is(container.childNodes[0].className, "call-tree-item",
|
||||
"The root node in the tree has the correct class name.");
|
||||
|
||||
is($$dur(0).getAttribute("value"), "15",
|
||||
is($$dur(0).getAttribute("value"), "15 ms",
|
||||
"The root's duration cell displays the correct value.");
|
||||
is($$perc(0).getAttribute("value"), "100%",
|
||||
"The root's percentage cell displays the correct value.");
|
||||
@ -53,7 +53,7 @@ function test() {
|
||||
is(container.childNodes[1].className, "call-tree-item",
|
||||
"The .A node in the tree has the correct class name.");
|
||||
|
||||
is($$dur(1).getAttribute("value"), "15",
|
||||
is($$dur(1).getAttribute("value"), "15 ms",
|
||||
"The .A node's duration cell displays the correct value.");
|
||||
is($$perc(1).getAttribute("value"), "100%",
|
||||
"The .A node's percentage cell displays the correct value.");
|
||||
@ -82,7 +82,7 @@ function test() {
|
||||
is(container.childNodes[3].className, "call-tree-item",
|
||||
"The .E node in the tree has the correct class name.");
|
||||
|
||||
is($$dur(2).getAttribute("value"), "8",
|
||||
is($$dur(2).getAttribute("value"), "8 ms",
|
||||
"The .A.B node's duration cell displays the correct value.");
|
||||
is($$perc(2).getAttribute("value"), "75%",
|
||||
"The .A.B node's percentage cell displays the correct value.");
|
||||
@ -101,7 +101,7 @@ function test() {
|
||||
is($$fun(".call-tree-category")[2].getAttribute("value"), "Styles",
|
||||
"The .A.B node's function cell displays the correct category.");
|
||||
|
||||
is($$dur(3).getAttribute("value"), "7",
|
||||
is($$dur(3).getAttribute("value"), "7 ms",
|
||||
"The .A.E node's duration cell displays the correct value.");
|
||||
is($$perc(3).getAttribute("value"), "25%",
|
||||
"The .A.E node's percentage cell displays the correct value.");
|
||||
|
@ -55,19 +55,19 @@ function test() {
|
||||
is($$name(6).getAttribute("value"), "F",
|
||||
"The .A.E.F node's function cell displays the correct name.");
|
||||
|
||||
is($$duration(0).getAttribute("value"), "15",
|
||||
is($$duration(0).getAttribute("value"), "15 ms",
|
||||
"The root node's function cell displays the correct duration.");
|
||||
is($$duration(1).getAttribute("value"), "15",
|
||||
is($$duration(1).getAttribute("value"), "15 ms",
|
||||
"The .A node's function cell displays the correct duration.");
|
||||
is($$duration(2).getAttribute("value"), "8",
|
||||
is($$duration(2).getAttribute("value"), "8 ms",
|
||||
"The .A.B node's function cell displays the correct duration.");
|
||||
is($$duration(3).getAttribute("value"), "3",
|
||||
is($$duration(3).getAttribute("value"), "3 ms",
|
||||
"The .A.B.D node's function cell displays the correct duration.");
|
||||
is($$duration(4).getAttribute("value"), "5",
|
||||
is($$duration(4).getAttribute("value"), "5 ms",
|
||||
"The .A.B.C node's function cell displays the correct duration.");
|
||||
is($$duration(5).getAttribute("value"), "7",
|
||||
is($$duration(5).getAttribute("value"), "7 ms",
|
||||
"The .A.E node's function cell displays the correct duration.");
|
||||
is($$duration(6).getAttribute("value"), "7",
|
||||
is($$duration(6).getAttribute("value"), "7 ms",
|
||||
"The .A.E.F node's function cell displays the correct duration.");
|
||||
|
||||
finish();
|
||||
|
@ -42,24 +42,28 @@ function test() {
|
||||
ok(!A.target.querySelector(".call-tree-category").hidden,
|
||||
"The .A.B.D node's category label cell should not be hidden.");
|
||||
|
||||
is(D.target.childNodes.length, 6,
|
||||
is(D.target.childNodes.length, 8,
|
||||
"The number of columns displayed for tree items is correct.");
|
||||
is(D.target.childNodes[0].getAttribute("type"), "duration",
|
||||
"The first column displayed for tree items is correct.");
|
||||
is(D.target.childNodes[1].getAttribute("type"), "percentage",
|
||||
"The third column displayed for tree items is correct.");
|
||||
is(D.target.childNodes[2].getAttribute("type"), "self-duration",
|
||||
is(D.target.childNodes[2].getAttribute("type"), "allocations",
|
||||
"The second column displayed for tree items is correct.");
|
||||
is(D.target.childNodes[3].getAttribute("type"), "self-percentage",
|
||||
is(D.target.childNodes[3].getAttribute("type"), "self-duration",
|
||||
"The second column displayed for tree items is correct.");
|
||||
is(D.target.childNodes[4].getAttribute("type"), "self-percentage",
|
||||
"The fourth column displayed for tree items is correct.");
|
||||
is(D.target.childNodes[4].getAttribute("type"), "samples",
|
||||
is(D.target.childNodes[5].getAttribute("type"), "self-allocations",
|
||||
"The fourth column displayed for tree items is correct.");
|
||||
is(D.target.childNodes[6].getAttribute("type"), "samples",
|
||||
"The fifth column displayed for tree items is correct.");
|
||||
is(D.target.childNodes[5].getAttribute("type"), "function",
|
||||
is(D.target.childNodes[7].getAttribute("type"), "function",
|
||||
"The sixth column displayed for tree items is correct.");
|
||||
|
||||
let functionCell = D.target.childNodes[5];
|
||||
let functionCell = D.target.childNodes[7];
|
||||
|
||||
is(functionCell.childNodes.length, 8,
|
||||
is(functionCell.childNodes.length, 9,
|
||||
"The number of columns displayed for function cells is correct.");
|
||||
is(functionCell.childNodes[0].className, "arrow theme-twisty",
|
||||
"The first node displayed for function cells is correct.");
|
||||
@ -69,13 +73,15 @@ function test() {
|
||||
"The third node displayed for function cells is correct.");
|
||||
is(functionCell.childNodes[3].className, "plain call-tree-line",
|
||||
"The fourth node displayed for function cells is correct.");
|
||||
is(functionCell.childNodes[4].className, "plain call-tree-host",
|
||||
is(functionCell.childNodes[4].className, "plain call-tree-column",
|
||||
"The fifth node displayed for function cells is correct.");
|
||||
is(functionCell.childNodes[5].className, "plain call-tree-zoom",
|
||||
is(functionCell.childNodes[5].className, "plain call-tree-host",
|
||||
"The fifth node displayed for function cells is correct.");
|
||||
is(functionCell.childNodes[6].className, "plain call-tree-zoom",
|
||||
"The sixth node displayed for function cells is correct.");
|
||||
is(functionCell.childNodes[6].tagName, "spacer",
|
||||
is(functionCell.childNodes[7].tagName, "spacer",
|
||||
"The seventh node displayed for function cells is correct.");
|
||||
is(functionCell.childNodes[7].className, "plain call-tree-category",
|
||||
is(functionCell.childNodes[8].className, "plain call-tree-category",
|
||||
"The eight node displayed for function cells is correct.");
|
||||
|
||||
finish();
|
||||
|
@ -509,6 +509,7 @@ let ProfileView = {
|
||||
|
||||
let contentOnly = !Prefs.showPlatformData;
|
||||
callTreeRoot.toggleCategories(!contentOnly);
|
||||
callTreeRoot.toggleAllocations(false);
|
||||
|
||||
this._callTreeRootByPanel.set(panel, callTreeRoot);
|
||||
},
|
||||
|
@ -136,13 +136,19 @@ ThreadNode.prototype = {
|
||||
* so it may very well (not?) include the function name, url, etc.
|
||||
* @param number line
|
||||
* The line number inside the source containing this function call.
|
||||
* @param number column
|
||||
* The column number inside the source containing this function call.
|
||||
* @param number category
|
||||
* The category type of this function call ("js", "graphics" etc.).
|
||||
* @param number allocations
|
||||
* The number of memory allocations performed in this frame.
|
||||
*/
|
||||
function FrameNode({ location, line, category }) {
|
||||
function FrameNode({ location, line, column, category, allocations }) {
|
||||
this.location = location;
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
this.category = category;
|
||||
this.allocations = allocations || 0;
|
||||
this.sampleTimes = [];
|
||||
this.samples = 0;
|
||||
this.duration = 0;
|
||||
@ -200,24 +206,28 @@ FrameNode.prototype = {
|
||||
// default to an "unknown" category otherwise.
|
||||
let categoryData = CATEGORY_MAPPINGS[this.category] || {};
|
||||
|
||||
// Parse the `location` for the function name, source url and line.
|
||||
let firstParen = this.location.indexOf("(");
|
||||
let lastColon = this.location.lastIndexOf(":");
|
||||
let resource = this.location.substring(firstParen + 1, lastColon);
|
||||
let line = this.location.substring(lastColon + 1).replace(")", "");
|
||||
// Parse the `location` for the function name, source url, line, column etc.
|
||||
let lineAndColumn = this.location.match(/((:\d+)*)\)?$/)[1];
|
||||
let [, line, column] = lineAndColumn.split(":");
|
||||
line = line || this.line;
|
||||
column = column || this.column;
|
||||
|
||||
let firstParenIndex = this.location.indexOf("(");
|
||||
let lineAndColumnIndex = this.location.indexOf(lineAndColumn);
|
||||
let resource = this.location.substring(firstParenIndex + 1, lineAndColumnIndex);
|
||||
|
||||
let url = resource.split(" -> ").pop();
|
||||
let uri = nsIURL(url);
|
||||
let functionName, fileName, hostName;
|
||||
|
||||
// If the URI digged out from the `location` is valid, this is a JS frame.
|
||||
if (uri) {
|
||||
functionName = this.location.substring(0, firstParen - 1);
|
||||
functionName = this.location.substring(0, firstParenIndex - 1);
|
||||
fileName = (uri.fileName + (uri.ref ? "#" + uri.ref : "")) || "/";
|
||||
hostName = uri.host;
|
||||
} else {
|
||||
functionName = this.location;
|
||||
url = null;
|
||||
line = null;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -226,7 +236,8 @@ FrameNode.prototype = {
|
||||
fileName: fileName,
|
||||
hostName: hostName,
|
||||
url: url,
|
||||
line: line || this.line,
|
||||
line: line,
|
||||
column: column,
|
||||
categoryData: categoryData,
|
||||
isContent: !!isContent(this)
|
||||
};
|
||||
|
@ -13,10 +13,13 @@ loader.lazyImporter(this, "Heritage",
|
||||
loader.lazyImporter(this, "AbstractTreeItem",
|
||||
"resource:///modules/devtools/AbstractTreeItem.jsm");
|
||||
|
||||
const MILLISECOND_UNITS = L10N.getStr("table.ms");
|
||||
const PERCENTAGE_UNITS = L10N.getStr("table.percentage");
|
||||
const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
|
||||
const ZOOM_BUTTON_TOOLTIP = L10N.getStr("table.zoom.tooltiptext");
|
||||
const CALL_TREE_INDENTATION = 16; // px
|
||||
const CALL_TREE_AUTO_EXPAND = 3; // depth
|
||||
const CALL_TREE_INDENTATION = 16; // px
|
||||
const DEFAULT_SORTING_PREDICATE = (a, b) => a.frame.samples < b.frame.samples ? 1 : -1;
|
||||
|
||||
const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
|
||||
const sum = vals => vals.reduce((a, b) => a + b, 0);
|
||||
@ -37,10 +40,6 @@ exports.CallView = CallView;
|
||||
* Every instance of a `CallView` represents a row in the call tree. The same
|
||||
* parent node is used for all rows.
|
||||
*
|
||||
* @param number autoExpandDepth [optional]
|
||||
* The depth to which the tree should automatically expand. Defualts to
|
||||
* the caller's autoExpandDepth if a caller exists, otherwise defaults to
|
||||
* CALL_TREE_AUTO_EXPAND.
|
||||
* @param CallView caller
|
||||
* The CallView considered the "caller" frame. This instance will be
|
||||
* represent the "callee". Should be null for root nodes.
|
||||
@ -54,9 +53,17 @@ exports.CallView = CallView;
|
||||
* @param boolean inverted [optional]
|
||||
* Whether the call tree has been inverted (bottom up, rather than
|
||||
* top-down). Defaults to false.
|
||||
* @param function sortingPredicate [optional]
|
||||
* The predicate used to sort the tree items when created. Defaults to
|
||||
* the caller's sortingPredicate if a caller exists, otherwise defaults
|
||||
* to DEFAULT_SORTING_PREDICATE. The two passed arguments are FrameNodes.
|
||||
* @param number autoExpandDepth [optional]
|
||||
* The depth to which the tree should automatically expand. Defualts to
|
||||
* the caller's `autoExpandDepth` if a caller exists, otherwise defaults
|
||||
* to CALL_TREE_AUTO_EXPAND.
|
||||
*/
|
||||
function CallView({ autoExpandDepth, caller, frame, level, hidden, inverted }) {
|
||||
// Assume no indentation if the this tree item's level is not specified.
|
||||
function CallView({ caller, frame, level, hidden, inverted, sortingPredicate, autoExpandDepth }) {
|
||||
// Assume no indentation if this tree item's level is not specified.
|
||||
level = level || 0;
|
||||
|
||||
// Don't increase indentation if this tree item is hidden.
|
||||
@ -66,6 +73,11 @@ function CallView({ autoExpandDepth, caller, frame, level, hidden, inverted }) {
|
||||
|
||||
AbstractTreeItem.call(this, { parent: caller, level });
|
||||
|
||||
this.sortingPredicate = sortingPredicate != null
|
||||
? sortingPredicate
|
||||
: caller ? caller.sortingPredicate
|
||||
: DEFAULT_SORTING_PREDICATE
|
||||
|
||||
this.autoExpandDepth = autoExpandDepth != null
|
||||
? autoExpandDepth
|
||||
: caller ? caller.autoExpandDepth
|
||||
@ -95,18 +107,23 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
|
||||
let selfPercentage;
|
||||
let selfDuration;
|
||||
let totalAllocations;
|
||||
|
||||
if (!this._getChildCalls().length) {
|
||||
selfPercentage = framePercentage;
|
||||
selfDuration = this.frame.duration;
|
||||
totalAllocations = this.frame.allocations;
|
||||
} else {
|
||||
let childrenPercentage = sum(
|
||||
[this._getPercentage(c.samples) for (c of this._getChildCalls())]);
|
||||
let childrenDuration = sum(
|
||||
[c.duration for (c of this._getChildCalls())]);
|
||||
let childrenAllocations = sum(
|
||||
[c.allocations for (c of this._getChildCalls())]);
|
||||
|
||||
selfPercentage = clamp(framePercentage - childrenPercentage, 0, 100);
|
||||
selfDuration = this.frame.duration - childrenDuration;
|
||||
totalAllocations = this.frame.allocations + childrenAllocations;
|
||||
|
||||
if (this.inverted) {
|
||||
selfPercentage = framePercentage - selfPercentage;
|
||||
@ -118,6 +135,8 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
let selfDurationCell = this._createTimeCell(selfDuration, true);
|
||||
let percentageCell = this._createExecutionCell(framePercentage);
|
||||
let selfPercentageCell = this._createExecutionCell(selfPercentage, true);
|
||||
let allocationsCell = this._createAllocationsCell(totalAllocations);
|
||||
let selfAllocationsCell = this._createAllocationsCell(this.frame.allocations, true);
|
||||
let samplesCell = this._createSamplesCell(this.frame.samples);
|
||||
let functionCell = this._createFunctionCell(arrowNode, frameInfo, this.level);
|
||||
|
||||
@ -138,8 +157,10 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
|
||||
targetNode.appendChild(durationCell);
|
||||
targetNode.appendChild(percentageCell);
|
||||
targetNode.appendChild(allocationsCell);
|
||||
targetNode.appendChild(selfDurationCell);
|
||||
targetNode.appendChild(selfPercentageCell);
|
||||
targetNode.appendChild(selfAllocationsCell);
|
||||
targetNode.appendChild(samplesCell);
|
||||
targetNode.appendChild(functionCell);
|
||||
|
||||
@ -177,8 +198,9 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
}));
|
||||
}
|
||||
|
||||
// Sort the "callees" asc. by samples, before inserting them in the tree.
|
||||
children.sort((a, b) => a.frame.samples < b.frame.samples ? 1 : -1);
|
||||
// Sort the "callees" asc. by samples, before inserting them in the tree,
|
||||
// if no other sorting predicate was specified on this on the root item.
|
||||
children.sort(this.sortingPredicate);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -190,7 +212,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
cell.className = "plain call-tree-cell";
|
||||
cell.setAttribute("type", isSelf ? "self-duration" : "duration");
|
||||
cell.setAttribute("crop", "end");
|
||||
cell.setAttribute("value", L10N.numberWithDecimals(duration, 2));
|
||||
cell.setAttribute("value", L10N.numberWithDecimals(duration, 2) + " " + MILLISECOND_UNITS);
|
||||
return cell;
|
||||
},
|
||||
_createExecutionCell: function(percentage, isSelf = false) {
|
||||
@ -198,7 +220,15 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
cell.className = "plain call-tree-cell";
|
||||
cell.setAttribute("type", isSelf ? "self-percentage" : "percentage");
|
||||
cell.setAttribute("crop", "end");
|
||||
cell.setAttribute("value", L10N.numberWithDecimals(percentage, 2) + "%");
|
||||
cell.setAttribute("value", L10N.numberWithDecimals(percentage, 2) + PERCENTAGE_UNITS);
|
||||
return cell;
|
||||
},
|
||||
_createAllocationsCell: function(count, isSelf = false) {
|
||||
let cell = this.document.createElement("label");
|
||||
cell.className = "plain call-tree-cell";
|
||||
cell.setAttribute("type", isSelf ? "self-allocations" : "allocations");
|
||||
cell.setAttribute("crop", "end");
|
||||
cell.setAttribute("value", count || 0);
|
||||
return cell;
|
||||
},
|
||||
_createSamplesCell: function(count) {
|
||||
@ -237,6 +267,11 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
lineNode.setAttribute("value", frameInfo.line ? ":" + frameInfo.line : "");
|
||||
cell.appendChild(lineNode);
|
||||
|
||||
let columnNode = this.document.createElement("label");
|
||||
columnNode.className = "plain call-tree-column";
|
||||
columnNode.setAttribute("value", frameInfo.column ? ":" + frameInfo.column : "");
|
||||
cell.appendChild(columnNode);
|
||||
|
||||
let hostNode = this.document.createElement("label");
|
||||
hostNode.className = "plain call-tree-host";
|
||||
hostNode.setAttribute("value", frameInfo.hostName || "");
|
||||
@ -266,6 +301,18 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
return cell;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the allocations information hidden or visible.
|
||||
* @param boolean visible
|
||||
*/
|
||||
toggleAllocations: function(visible) {
|
||||
if (!visible) {
|
||||
this.container.setAttribute("allocations-hidden", "");
|
||||
} else {
|
||||
this.container.removeAttribute("allocations-hidden");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the category information hidden or visible.
|
||||
* @param boolean visible
|
||||
|
@ -49,9 +49,9 @@ function testGraph(graph) {
|
||||
|
||||
scroll(graph, 10000, HORIZONTAL_AXIS, 1);
|
||||
|
||||
is(graph.getViewRange().startTime, 140,
|
||||
is(Math.round(graph.getViewRange().startTime), 150,
|
||||
"The selection start boundary is correct on HiDPI (2).");
|
||||
is(graph.getViewRange().endTime, 150,
|
||||
is(Math.round(graph.getViewRange().endTime), 150,
|
||||
"The selection end boundary is correct on HiDPI (2).");
|
||||
|
||||
is(graph.getOuterBounds().startTime, 0,
|
||||
|
@ -24,7 +24,7 @@ const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
|
||||
|
||||
const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
|
||||
const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
|
||||
const GRAPH_MIN_SELECTION_WIDTH = 20; // ms
|
||||
const GRAPH_MIN_SELECTION_WIDTH = 0.001; // ms
|
||||
|
||||
const TIMELINE_TICKS_MULTIPLE = 5; // ms
|
||||
const TIMELINE_TICKS_SPACING_MIN = 75; // px
|
||||
|
@ -41,13 +41,23 @@
|
||||
- on a button that remvoes all the recordings. -->
|
||||
<!ENTITY profilerUI.clearButton "Clear">
|
||||
|
||||
<!-- LOCALIZATION NOTE (profilerUI.toolbar.*): These strings are displayed
|
||||
- in the toolbar on buttons that select which view is currently shown. -->
|
||||
<!ENTITY profilerUI.toolbar.waterfall "Timeline">
|
||||
<!ENTITY profilerUI.toolbar.js-calltree "JavaScript">
|
||||
<!ENTITY profilerUI.toolbar.memory-calltree "Memory">
|
||||
<!ENTITY profilerUI.toolbar.js-flamegraph "JS Flame Chart">
|
||||
<!ENTITY profilerUI.toolbar.memory-flamegraph "Memory Flame Chart">
|
||||
|
||||
<!-- LOCALIZATION NOTE (profilerUI.table.*): These strings are displayed
|
||||
- in the call tree headers for a recording. -->
|
||||
<!ENTITY profilerUI.table.totalDuration "Total Time (ms)">
|
||||
<!ENTITY profilerUI.table.selfDuration "Self Time (ms)">
|
||||
<!ENTITY profilerUI.table.totalDuration2 "Total Time">
|
||||
<!ENTITY profilerUI.table.selfDuration2 "Self Time">
|
||||
<!ENTITY profilerUI.table.totalPercentage "Total Cost">
|
||||
<!ENTITY profilerUI.table.selfPercentage "Self Cost">
|
||||
<!ENTITY profilerUI.table.samples "Samples">
|
||||
<!ENTITY profilerUI.table.totalAlloc "Total Allocations">
|
||||
<!ENTITY profilerUI.table.selfAlloc "Self Allocations">
|
||||
<!ENTITY profilerUI.table.function "Function">
|
||||
|
||||
<!-- LOCALIZATION NOTE (profilerUI.newtab.tooltiptext): The tooltiptext shown
|
||||
|
@ -83,6 +83,14 @@ category.graphics=Graphics
|
||||
category.storage=Storage
|
||||
category.events=Input & Events
|
||||
|
||||
# LOCALIZATION NOTE (graphs.ms):
|
||||
# This string is displayed in the call tree after units of time in milliseconds.
|
||||
table.ms=ms
|
||||
|
||||
# LOCALIZATION NOTE (graphs.ms):
|
||||
# This string is displayed in the call tree after units representing percentages.
|
||||
table.percentage=%
|
||||
|
||||
# LOCALIZATION NOTE (table.root):
|
||||
# This string is displayed in the call tree for the root node.
|
||||
table.root=(root)
|
||||
|
@ -183,6 +183,8 @@ mute_local_audio_button_title=Mute your audio
|
||||
unmute_local_audio_button_title=Unmute your audio
|
||||
mute_local_video_button_title=Mute your video
|
||||
unmute_local_video_button_title=Unmute your video
|
||||
active_screenshare_button_title=Stop sharing
|
||||
inactive_screenshare_button_title=Share your screen
|
||||
|
||||
## LOCALIZATION NOTE (call_with_contact_title): The title displayed
|
||||
## when calling a contact. Don't translate the part between {{..}} because
|
||||
|
@ -28,9 +28,9 @@
|
||||
</g>
|
||||
<g id="details-call-tree">
|
||||
<rect x="0px" y="3px" width="16px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="3px" y="6px" width="7px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="6px" y="9px" width="6px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="9px" y="12px" width="5px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="0px" y="6px" width="8px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="0px" y="9px" width="11px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="0px" y="12px" width="6px" height="2px" rx="1" ry="1"/>
|
||||
</g>
|
||||
<g id="details-flamegraph">
|
||||
<rect x="0px" y="3px" width="16px" height="2px" rx="1" ry="1"/>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@ -25,7 +25,14 @@
|
||||
-moz-border-end-color: var(--theme-splitter-color);
|
||||
}
|
||||
|
||||
/* Overview Panel */
|
||||
#performance-toolbar-controls-detail-views > toolbarbutton {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#performance-toolbar-controls-detail-views .toolbarbutton-text {
|
||||
-moz-padding-start: 4px;
|
||||
-moz-padding-end: 8px;
|
||||
}
|
||||
|
||||
#record-button {
|
||||
list-style-image: url(profiler-stopwatch.svg);
|
||||
@ -45,11 +52,13 @@
|
||||
list-style-image: url(performance-icons.svg#details-waterfall);
|
||||
}
|
||||
|
||||
#select-calltree-view {
|
||||
#select-js-calltree-view,
|
||||
#select-memory-calltree-view {
|
||||
list-style-image: url(performance-icons.svg#details-call-tree);
|
||||
}
|
||||
|
||||
#select-flamegraph-view {
|
||||
#select-js-flamegraph-view,
|
||||
#select-memory-flamegraph-view {
|
||||
list-style-image: url(performance-icons.svg#details-flamegraph);
|
||||
}
|
||||
|
||||
@ -61,27 +70,42 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.call-tree-cells-container[allocations-hidden] .call-tree-cell[type="allocations"],
|
||||
.call-tree-cells-container[allocations-hidden] .call-tree-cell[type="self-allocations"],
|
||||
.call-tree-cells-container[categories-hidden] .call-tree-category {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.call-tree-header {
|
||||
font-size: 90%;
|
||||
padding-top: 2px !important;
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.call-tree-header[type="duration"],
|
||||
.call-tree-cell[type="duration"],
|
||||
.call-tree-header[type="self-duration"],
|
||||
.call-tree-cell[type="self-duration"] {
|
||||
width: 9em;
|
||||
width: 6vw;
|
||||
}
|
||||
|
||||
.call-tree-header[type="percentage"],
|
||||
.call-tree-cell[type="percentage"],
|
||||
.call-tree-header[type="self-percentage"],
|
||||
.call-tree-cell[type="self-percentage"] {
|
||||
width: 6em;
|
||||
width: 5vw;
|
||||
}
|
||||
|
||||
.call-tree-header[type="samples"],
|
||||
.call-tree-cell[type="samples"] {
|
||||
width: 5em;
|
||||
width: 4vw;
|
||||
}
|
||||
|
||||
.call-tree-header[type="allocations"],
|
||||
.call-tree-cell[type="allocations"],
|
||||
.call-tree-header[type="self-allocations"],
|
||||
.call-tree-cell[type="self-allocations"] {
|
||||
width: 7vw;
|
||||
}
|
||||
|
||||
.call-tree-header[type="function"],
|
||||
@ -142,7 +166,8 @@
|
||||
|
||||
.call-tree-item:not([origin="content"]) .call-tree-name,
|
||||
.call-tree-item:not([origin="content"]) .call-tree-url,
|
||||
.call-tree-item:not([origin="content"]) .call-tree-line {
|
||||
.call-tree-item:not([origin="content"]) .call-tree-line,
|
||||
.call-tree-item:not([origin="content"]) .call-tree-column {
|
||||
/* Style chrome and non-JS nodes differently. */
|
||||
opacity: 0.6;
|
||||
}
|
||||
@ -164,14 +189,21 @@
|
||||
color: var(--theme-highlight-orange);
|
||||
}
|
||||
|
||||
.call-tree-column {
|
||||
color: var(--theme-highlight-orange);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.call-tree-host {
|
||||
-moz-margin-start: 8px !important;
|
||||
font-size: 90%;
|
||||
color: var(--theme-content-color2);
|
||||
}
|
||||
|
||||
.call-tree-name[value=""],
|
||||
.call-tree-url[value=""],
|
||||
.call-tree-line[value=""],
|
||||
.call-tree-column[value=""],
|
||||
.call-tree-host[value=""] {
|
||||
display: none;
|
||||
}
|
||||
|
@ -220,6 +220,8 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.call-tree-cells-container[allocations-hidden] .call-tree-cell[type="allocations"],
|
||||
.call-tree-cells-container[allocations-hidden] .call-tree-cell[type="self-allocations"],
|
||||
.call-tree-cells-container[categories-hidden] .call-tree-category {
|
||||
display: none;
|
||||
}
|
||||
@ -305,7 +307,8 @@
|
||||
|
||||
.call-tree-item:not([origin="content"]) .call-tree-name,
|
||||
.call-tree-item:not([origin="content"]) .call-tree-url,
|
||||
.call-tree-item:not([origin="content"]) .call-tree-line {
|
||||
.call-tree-item:not([origin="content"]) .call-tree-line,
|
||||
.call-tree-item:not([origin="content"]) .call-tree-column {
|
||||
/* Style chrome and non-JS nodes differently. */
|
||||
opacity: 0.6;
|
||||
}
|
||||
@ -323,19 +326,25 @@
|
||||
color: var(--theme-highlight-blue);
|
||||
}
|
||||
|
||||
|
||||
.call-tree-line {
|
||||
color: var(--theme-highlight-orange);
|
||||
}
|
||||
|
||||
.call-tree-column {
|
||||
color: var(--theme-highlight-orange);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.call-tree-host {
|
||||
-moz-margin-start: 8px !important;
|
||||
font-size: 90%;
|
||||
color: var(--theme-content-color2);
|
||||
}
|
||||
|
||||
.call-tree-name[value=""],
|
||||
.call-tree-url[value=""],
|
||||
.call-tree-line[value=""],
|
||||
.call-tree-column[value=""],
|
||||
.call-tree-host[value=""] {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1683,6 +1683,18 @@ MediaManager::GetUserMedia(
|
||||
|
||||
nsIURI* docURI = aWindow->GetDocumentURI();
|
||||
|
||||
bool isLoop = false;
|
||||
nsCOMPtr<nsIURI> loopURI;
|
||||
nsresult rv = NS_NewURI(getter_AddRefs(loopURI), "about:loopconversation");
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
rv = docURI->EqualsExceptRef(loopURI, &isLoop);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (isLoop) {
|
||||
privileged = true;
|
||||
}
|
||||
|
||||
|
||||
if (c.mVideo.IsMediaTrackConstraints()) {
|
||||
auto& tc = c.mVideo.GetAsMediaTrackConstraints();
|
||||
MediaSourceEnum src = StringToEnum(dom::MediaSourceEnumValues::strings,
|
||||
@ -1725,6 +1737,17 @@ MediaManager::GetUserMedia(
|
||||
default:
|
||||
return task->Denied(NS_LITERAL_STRING("NotFoundError"));
|
||||
}
|
||||
|
||||
// For all but tab sharing, Loop needs to prompt as we are using the
|
||||
// permission menu for selection of the device currently. For tab sharing,
|
||||
// Loop has implicit permissions within Firefox, as it is built-in,
|
||||
// and will manage the active tab and provide appropriate UI.
|
||||
if (isLoop &&
|
||||
(src == dom::MediaSourceEnum::Window ||
|
||||
src == dom::MediaSourceEnum::Application ||
|
||||
src == dom::MediaSourceEnum::Screen)) {
|
||||
privileged = false;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MOZ_B2G_CAMERA
|
||||
@ -1733,17 +1756,6 @@ MediaManager::GetUserMedia(
|
||||
}
|
||||
#endif
|
||||
|
||||
bool isLoop = false;
|
||||
nsCOMPtr<nsIURI> loopURI;
|
||||
nsresult rv = NS_NewURI(getter_AddRefs(loopURI), "about:loopconversation");
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
rv = docURI->EqualsExceptRef(loopURI, &isLoop);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (isLoop) {
|
||||
privileged = true;
|
||||
}
|
||||
|
||||
// XXX No full support for picture in Desktop yet (needs proper UI)
|
||||
if (privileged ||
|
||||
(c.mFake && !Preferences::GetBool("media.navigator.permission.fake"))) {
|
||||
|
@ -304,11 +304,12 @@ public class TopSitesPanel extends HomeFragment {
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
// Discard any additional item clicks on the list
|
||||
// as the panel is getting destroyed (see bug 930160).
|
||||
// Discard any additional item clicks on the list as the
|
||||
// panel is getting destroyed (see bugs 930160 & 1096958).
|
||||
mList.setOnItemClickListener(null);
|
||||
mList = null;
|
||||
mGrid.setOnItemClickListener(null);
|
||||
|
||||
mList = null;
|
||||
mGrid = null;
|
||||
mListAdapter = null;
|
||||
mGridAdapter = null;
|
||||
|
@ -469,7 +469,7 @@ public class testDistribution extends ContentProviderTest {
|
||||
JSONObject response = clickTrackingTile(StringHelper.DISTRIBUTION1_LABEL);
|
||||
mAsserter.is(response.getInt("click"), 0, "JSON click index matched");
|
||||
mAsserter.is(response.getString("locale"), localeCode, "JSON locale code matched");
|
||||
mAsserter.is(response.getString("tiles"), "[{\"id\":123},{\"id\":456},{},{},{},{}]", "JSON tiles data matched");
|
||||
mAsserter.is(response.getString("tiles"), "[{\"id\":123},{\"id\":456},{\"id\":632},{\"id\":629},{\"id\":630},{\"id\":631}]", "JSON tiles data matched");
|
||||
|
||||
inputAndLoadUrl(StringHelper.ABOUT_HOME_URL);
|
||||
|
||||
@ -479,7 +479,7 @@ public class testDistribution extends ContentProviderTest {
|
||||
// Click the second tracking tile and verify the posted data.
|
||||
response = clickTrackingTile(StringHelper.DISTRIBUTION2_LABEL);
|
||||
mAsserter.is(response.getInt("click"), 1, "JSON click index matched");
|
||||
mAsserter.is(response.getString("tiles"), "[{\"id\":123},{\"id\":456,\"pin\":true},{},{},{},{}]", "JSON tiles data matched");
|
||||
mAsserter.is(response.getString("tiles"), "[{\"id\":123},{\"id\":456,\"pin\":true},{\"id\":632},{\"id\":629},{\"id\":630},{\"id\":631}]", "JSON tiles data matched");
|
||||
|
||||
inputAndLoadUrl(StringHelper.ABOUT_HOME_URL);
|
||||
|
||||
|
@ -54,15 +54,19 @@ browser.suggestedsites.list.3=fxsupport
|
||||
browser.suggestedsites.mozilla.title=The Mozilla Project
|
||||
browser.suggestedsites.mozilla.url=https://www.mozilla.org/en-US/
|
||||
browser.suggestedsites.mozilla.bgcolor=#ce4e41
|
||||
browser.suggestedsites.mozilla.trackingid=632
|
||||
|
||||
browser.suggestedsites.fxmarketplace.title=Firefox Marketplace
|
||||
browser.suggestedsites.fxmarketplace.url=https://marketplace.firefox.com/
|
||||
browser.suggestedsites.fxmarketplace.bgcolor=#0096dd
|
||||
browser.suggestedsites.fxmarketplace.trackingid=629
|
||||
|
||||
browser.suggestedsites.fxaddons.title=Add-ons: Customize Firefox
|
||||
browser.suggestedsites.fxaddons.url=https://addons.mozilla.org/en-US/android/
|
||||
browser.suggestedsites.fxaddons.bgcolor=#62be06
|
||||
browser.suggestedsites.fxaddons.trackingid=630
|
||||
|
||||
browser.suggestedsites.fxsupport.title=Firefox Help and Support
|
||||
browser.suggestedsites.fxsupport.url=https://support.mozilla.org/en-US/products/mobile
|
||||
browser.suggestedsites.fxsupport.bgcolor=#f37c00
|
||||
browser.suggestedsites.fxsupport.trackingid=631
|
||||
|
@ -165,11 +165,15 @@ let MemoryActor = protocol.ActorClass({
|
||||
? options.probability
|
||||
: 1.0;
|
||||
this.dbg.memory.trackingAllocationSites = true;
|
||||
|
||||
return Date.now();
|
||||
}), {
|
||||
request: {
|
||||
options: Arg(0, "nullable:AllocationsRecordingOptions")
|
||||
},
|
||||
response: {}
|
||||
response: {
|
||||
value: RetVal(0, "number")
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
@ -178,9 +182,13 @@ let MemoryActor = protocol.ActorClass({
|
||||
stopRecordingAllocations: method(expectState("attached", function() {
|
||||
this.dbg.memory.trackingAllocationSites = false;
|
||||
this._clearFrames();
|
||||
|
||||
return Date.now();
|
||||
}), {
|
||||
request: {},
|
||||
response: {}
|
||||
response: {
|
||||
value: RetVal(0, "number")
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -79,6 +79,6 @@ xul|description {
|
||||
html|a:-moz-focusring,
|
||||
xul|*.text-link:-moz-focusring,
|
||||
xul|*.inline-link:-moz-focusring {
|
||||
outline-width: 0;
|
||||
box-shadow: @focusRingShadow@;
|
||||
color: #ff9500;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -68,6 +68,20 @@ notification[type="critical"] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Invert the close icon for @type=info since both are normally dark. It's unclear
|
||||
why !important is necessary here so remove it if it's no longer needed.
|
||||
*/
|
||||
notification[type="info"]:not([value="translation"]) .close-icon:not(:hover) {
|
||||
-moz-image-region: rect(0, 64px, 16px, 48px) !important;
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
notification[type="info"]:not([value="translation"]) .close-icon:not(:hover) {
|
||||
-moz-image-region: rect(0, 128px, 32px, 96px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.messageCloseButton:-moz-focusring > .toolbarbutton-icon {
|
||||
border-radius: 10000px;
|
||||
box-shadow: 0 0 2px 1px -moz-mac-focusring,
|
||||
|
Loading…
Reference in New Issue
Block a user