merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2015-02-03 14:36:21 +01:00
commit d7cf50ea6d
90 changed files with 2042 additions and 393 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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");

View File

@ -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>

View File

@ -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({

View File

@ -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({

View File

@ -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})
),

View File

@ -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>

View File

@ -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})
)
)
)

View File

@ -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>

View File

@ -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%;

View File

@ -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

View File

@ -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.
*/

View File

@ -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);
}

View File

@ -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
}));
}
};

View File

@ -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,

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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.

View File

@ -141,7 +141,8 @@ describe("loop.conversation", function() {
sdk: {},
conversationStore: conversationStore,
conversationAppStore: conversationAppStore,
dispatcher: dispatcher
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
}));
}

View File

@ -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>

View File

@ -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);

View File

@ -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

View File

@ -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()

View File

@ -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();

View File

@ -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 -->

View File

@ -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%");
});
});

View File

@ -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
}));
});
});
});
});

View File

@ -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;

View File

@ -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>

View File

@ -54,6 +54,7 @@ navigator.mozLoop = {
switch(pref) {
// Ensure we skip FTE completely.
case "gettingStarted.seen":
case "screenshare.enabled":
return true;
}
},

View File

@ -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>

View File

@ -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})
)
)

View File

@ -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>

View File

@ -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)

View File

@ -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();

View File

@ -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
};

View File

@ -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;
}
}
}
};

View File

@ -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;
}

View File

@ -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");

View File

@ -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);
},

View File

@ -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>

View File

@ -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]

View File

@ -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();
}

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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)
]);
}

View File

@ -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;

View File

@ -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);
},
/**

View 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());
}
}
});

View File

@ -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 });
}
});

View File

@ -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 }
},
/**

View File

@ -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"

View File

@ -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,

View File

@ -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();

View File

@ -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.");

View File

@ -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();

View File

@ -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();

View File

@ -509,6 +509,7 @@ let ProfileView = {
let contentOnly = !Prefs.showPlatformData;
callTreeRoot.toggleCategories(!contentOnly);
callTreeRoot.toggleAllocations(false);
this._callTreeRootByPanel.set(panel, callTreeRoot);
},

View File

@ -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)
};

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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"))) {

View File

@ -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;

View File

@ -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);

View File

@ -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

View File

@ -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")
}
}),
/**

View File

@ -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;
}

View File

@ -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,