Bug 828038 - Change profile recording UI and behavior; r=dcamp

This commit is contained in:
Anton Kovalyov 2013-06-27 17:31:52 -07:00
Родитель ec75a4559a
Коммит 1706e0cec0
39 изменённых файлов: 1122 добавлений и 1232 удалений

Просмотреть файл

@ -2,10 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
this.EXPORTED_SYMBOLS = [];
this.EXPORTED_SYMBOLS = [ ];
const Cu = Components.utils; const Cu = Components.utils;
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
Cu.import("resource:///modules/devtools/BuiltinCommands.jsm"); Cu.import("resource:///modules/devtools/BuiltinCommands.jsm");
Cu.import("resource:///modules/devtools/CmdDebugger.jsm"); Cu.import("resource:///modules/devtools/CmdDebugger.jsm");
@ -14,4 +14,5 @@ Cu.import("resource:///modules/devtools/CmdInspect.jsm");
Cu.import("resource:///modules/devtools/CmdResize.jsm"); Cu.import("resource:///modules/devtools/CmdResize.jsm");
Cu.import("resource:///modules/devtools/CmdTilt.jsm"); Cu.import("resource:///modules/devtools/CmdTilt.jsm");
Cu.import("resource:///modules/devtools/CmdScratchpad.jsm"); Cu.import("resource:///modules/devtools/CmdScratchpad.jsm");
Cu.import("resource:///modules/devtools/cmd-profiler.jsm");
require("devtools/profiler/commands.js");

Просмотреть файл

@ -13,7 +13,8 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js"); Cu.import("resource:///modules/devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource://gre/modules/devtools/Loader.jsm"); Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource:///modules/devtools/ProfilerController.jsm");
var ProfilerController = devtools.require("devtools/profiler/controller");
const FORBIDDEN_IDS = new Set(["toolbox", ""]); const FORBIDDEN_IDS = new Set(["toolbox", ""]);
const MAX_ORDINAL = 99; const MAX_ORDINAL = 99;

Просмотреть файл

@ -25,7 +25,7 @@ loader.lazyGetter(this, "InspectorPanel", function() require("devtools/inspector
loader.lazyImporter(this, "WebConsolePanel", "resource:///modules/WebConsolePanel.jsm"); loader.lazyImporter(this, "WebConsolePanel", "resource:///modules/WebConsolePanel.jsm");
loader.lazyImporter(this, "DebuggerPanel", "resource:///modules/devtools/DebuggerPanel.jsm"); loader.lazyImporter(this, "DebuggerPanel", "resource:///modules/devtools/DebuggerPanel.jsm");
loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm"); loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm");
loader.lazyImporter(this, "ProfilerPanel", "resource:///modules/devtools/ProfilerPanel.jsm"); loader.lazyGetter(this, "ProfilerPanel", function() require("devtools/profiler/panel"));
loader.lazyImporter(this, "NetMonitorPanel", "resource:///modules/devtools/NetMonitorPanel.jsm"); loader.lazyImporter(this, "NetMonitorPanel", "resource:///modules/devtools/NetMonitorPanel.jsm");
// Strings // Strings

Просмотреть файл

@ -12,4 +12,4 @@ include $(DEPTH)/config/autoconf.mk
include $(topsrcdir)/config/rules.mk include $(topsrcdir)/config/rules.mk
libs:: libs::
$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/profiler

Просмотреть файл

@ -1,43 +0,0 @@
/* 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";
const Cu = Components.utils;
const ProfilerProps = "chrome://browser/locale/devtools/profiler.properties";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
this.EXPORTED_SYMBOLS = ["L10N"];
/**
* Localization helper methods.
*/
let L10N = {
/**
* Returns a simple localized string.
*
* @param string name
* @return string
*/
getStr: function L10N_getStr(name) {
return this.stringBundle.GetStringFromName(name);
},
/**
* Returns formatted localized string.
*
* @param string name
* @param array params
* @return string
*/
getFormatStr: function L10N_getFormatStr(name, params) {
return this.stringBundle.formatStringFromName(name, params, params.length);
}
};
XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function () {
return Services.strings.createBundle(ProfilerProps);
});

Просмотреть файл

@ -1,716 +0,0 @@
/* 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";
const Cu = Components.utils;
Cu.import("resource:///modules/devtools/gDevTools.jsm");
Cu.import("resource:///modules/devtools/ProfilerController.jsm");
Cu.import("resource:///modules/devtools/ProfilerHelpers.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/Console.jsm");
this.EXPORTED_SYMBOLS = ["ProfilerPanel"];
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/commonjs/sdk/core/promise.js");
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
"resource://gre/modules/devtools/dbg-server.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
const PROFILE_IDLE = 0;
const PROFILE_RUNNING = 1;
const PROFILE_COMPLETED = 2;
/**
* An instance of a profile UI. Profile UI consists of
* an iframe with Cleopatra loaded in it and some
* surrounding meta-data (such as uids).
*
* Its main function is to talk to the Cleopatra instance
* inside of the iframe.
*
* ProfileUI is also an event emitter. It emits the following events:
* - ready, when Cleopatra is done loading (you can also check the isReady
* property to see if a particular instance has been loaded yet.
* - disabled, when Cleopatra gets disabled. Happens when another ProfileUI
* instance starts the profiler.
* - enabled, when Cleopatra gets enabled. Happens when another ProfileUI
* instance stops the profiler.
*
* @param number uid
* Unique ID for this profile.
* @param ProfilerPanel panel
* A reference to the container panel.
*/
function ProfileUI(uid, name, panel) {
let doc = panel.document;
let win = panel.window;
EventEmitter.decorate(this);
this.isReady = false;
this.isStarted = false;
this.isFinished = false;
this.messages = [];
this.panel = panel;
this.uid = uid;
this.name = name;
this.iframe = doc.createElement("iframe");
this.iframe.setAttribute("flex", "1");
this.iframe.setAttribute("id", "profiler-cleo-" + uid);
this.iframe.setAttribute("src", "cleopatra.html?" + uid);
this.iframe.setAttribute("hidden", "true");
// Append our iframe and subscribe to postMessage events.
// They'll tell us when the underlying page is done loading
// or when user clicks on start/stop buttons.
doc.getElementById("profiler-report").appendChild(this.iframe);
win.addEventListener("message", function (event) {
if (parseInt(event.data.uid, 10) !== parseInt(this.uid, 10)) {
return;
}
switch (event.data.status) {
case "loaded":
this.isReady = true;
this.emit("ready");
break;
case "start":
this.start();
break;
case "stop":
this.stop();
break;
case "disabled":
this.emit("disabled");
break;
case "enabled":
this.emit("enabled");
break;
case "displaysource":
this.panel.displaySource(event.data.data);
}
}.bind(this));
}
ProfileUI.prototype = {
/**
* Returns a contentWindow of the iframe pointing to Cleopatra
* if it exists and can be accessed. Otherwise returns null.
*/
get contentWindow() {
if (!this.iframe) {
return null;
}
try {
return this.iframe.contentWindow;
} catch (err) {
return null;
}
},
show: function PUI_show() {
this.iframe.removeAttribute("hidden");
},
hide: function PUI_hide() {
this.iframe.setAttribute("hidden", true);
},
/**
* Send raw profiling data to Cleopatra for parsing.
*
* @param object data
* Raw profiling data from the SPS Profiler.
* @param function onParsed
* A callback to be called when Cleopatra finishes
* parsing and displaying results.
*
*/
parse: function PUI_parse(data, onParsed) {
if (!this.isReady) {
return void this.on("ready", this.parse.bind(this, data, onParsed));
}
this.message({ task: "receiveProfileData", rawProfile: data }).then(() => {
let poll = () => {
let wait = this.panel.window.setTimeout.bind(null, poll, 100);
let trail = this.contentWindow.gBreadcrumbTrail;
if (!trail) {
return wait();
}
if (!trail._breadcrumbs || !trail._breadcrumbs.length) {
return wait();
}
onParsed();
};
poll();
});
},
/**
* Start profiling and, once started, notify the underlying page
* so that it could update the UI. Also, once started, we add a
* star to the profile name to indicate which profile is currently
* running.
*
* @param function startFn
* A function to use instead of the default
* this.panel.startProfiling. Useful when you
* need mark panel as started after the profiler
* has been started elsewhere. It must take two
* params and call the second one.
*/
start: function PUI_start(startFn) {
if (this.isStarted || this.isFinished) {
return;
}
startFn = startFn || this.panel.startProfiling.bind(this.panel);
startFn(this.name, () => {
this.isStarted = true;
this.panel.sidebar.setProfileState(this, PROFILE_RUNNING);
this.panel.broadcast(this.uid, {task: "onStarted"}); // Do we really need this?
this.emit("started");
});
},
/**
* Stop profiling and, once stopped, notify the underlying page so
* that it could update the UI and remove a star from the profile
* name.
*
* @param function stopFn
* A function to use instead of the default
* this.panel.stopProfiling. Useful when you
* need mark panel as stopped after the profiler
* has been stopped elsewhere. It must take two
* params and call the second one.
*/
stop: function PUI_stop(stopFn) {
if (!this.isStarted || this.isFinished) {
return;
}
stopFn = stopFn || this.panel.stopProfiling.bind(this.panel);
stopFn(this.name, () => {
this.isStarted = false;
this.isFinished = true;
this.panel.sidebar.setProfileState(this, PROFILE_COMPLETED);
this.panel.broadcast(this.uid, {task: "onStopped"});
this.emit("stopped");
});
},
/**
* Send a message to Cleopatra instance. If a message cannot be
* sent, this method queues it for later.
*
* @param object data JSON data to send (must be serializable)
* @return promise
*/
message: function PIU_message(data) {
let deferred = Promise.defer();
let win = this.contentWindow;
data = JSON.stringify(data);
if (win) {
win.postMessage(data, "*");
deferred.resolve();
} else {
this.messages.push({ data: data, onSuccess: () => deferred.resolve() });
}
return deferred.promise;
},
/**
* Send all queued messages (see this.message for more info)
*/
flushMessages: function PIU_flushMessages() {
if (!this.contentWindow) {
return;
}
let msg;
while (msg = this.messages.shift()) {
this.contentWindow.postMessage(msg.data, "*");
msg.onSuccess();
}
},
/**
* Destroys the ProfileUI instance.
*/
destroy: function PUI_destroy() {
this.isReady = null
this.panel = null;
this.uid = null;
this.iframe = null;
this.messages = null;
}
};
function SidebarView(el) {
EventEmitter.decorate(this);
this.widget = new SideMenuWidget(el);
}
SidebarView.prototype = Heritage.extend(WidgetMethods, {
getItemByProfile: function (profile) {
return this.getItemForPredicate(item => item.attachment.uid === profile.uid);
},
setProfileState: function (profile, state) {
let item = this.getItemByProfile(profile);
let label = item.target.querySelector(".profiler-sidebar-item > span");
switch (state) {
case PROFILE_IDLE:
label.textContent = L10N.getStr("profiler.stateIdle");
break;
case PROFILE_RUNNING:
label.textContent = L10N.getStr("profiler.stateRunning");
break;
case PROFILE_COMPLETED:
label.textContent = L10N.getStr("profiler.stateCompleted");
break;
default: // Wrong state, do nothing.
return;
}
item.attachment.state = state;
this.emit("stateChanged", item);
}
});
/**
* Profiler panel. It is responsible for creating and managing
* different profile instances (see ProfileUI).
*
* ProfilerPanel is an event emitter. It can emit the following
* events:
*
* - ready: after the panel is done loading everything,
* including the default profile instance.
* - started: after the panel successfuly starts our SPS
* profiler.
* - stopped: after the panel successfuly stops our SPS
* profiler and is ready to hand over profiling
* data
* - parsed: after Cleopatra finishes parsing profiling
* data.
* - destroyed: after the panel cleans up after itself and
* is ready to be destroyed.
*
* The following events are used mainly by tests to prevent
* accidential oranges:
*
* - profileCreated: after a new profile is created.
* - profileSwitched: after user switches to a different
* profile.
*/
function ProfilerPanel(frame, toolbox) {
this.isReady = false;
this.window = frame.window;
this.document = frame.document;
this.target = toolbox.target;
this.profiles = new Map();
this._uid = 0;
this._msgQueue = {};
EventEmitter.decorate(this);
}
ProfilerPanel.prototype = {
isReady: null,
window: null,
document: null,
target: null,
controller: null,
profiles: null,
sidebar: null,
_uid: null,
_activeUid: null,
_runningUid: null,
_browserWin: null,
_msgQueue: null,
get activeProfile() {
return this.profiles.get(this._activeUid);
},
set activeProfile(profile) {
if (this._activeUid === profile.uid)
return;
if (this.activeProfile)
this.activeProfile.hide();
this._activeUid = profile.uid;
profile.show();
},
get browserWindow() {
if (this._browserWin) {
return this._browserWin;
}
let win = this.window.top;
let type = win.document.documentElement.getAttribute("windowtype");
if (type !== "navigator:browser") {
win = Services.wm.getMostRecentWindow("navigator:browser");
}
return this._browserWin = win;
},
/**
* Open a debug connection and, on success, switch to the newly created
* profile.
*
* @return Promise
*/
open: function PP_open() {
// Local profiling needs to make the target remote.
let target = this.target;
let promise = !target.isRemote ? target.makeRemote() : Promise.resolve(target);
return promise
.then((target) => {
let deferred = Promise.defer();
this.controller = new ProfilerController(this.target);
this.sidebar = new SidebarView(this.document.querySelector("#profiles-list"));
this.sidebar.widget.addEventListener("select", (ev) => {
if (!ev.detail)
return;
let profile = this.profiles.get(ev.detail.attachment.uid);
this.activeProfile = profile;
if (profile.isReady) {
profile.flushMessages();
return void this.emit("profileSwitched", profile.uid);
}
profile.once("ready", () => {
profile.flushMessages();
this.emit("profileSwitched", profile.uid);
});
});
this.controller.connect(() => {
let create = this.document.getElementById("profiler-create");
create.addEventListener("click", () => this.createProfile(), false);
create.removeAttribute("disabled");
let profile = this.createProfile();
let onSwitch = (_, uid) => {
if (profile.uid !== uid)
return;
this.off("profileSwitched", onSwitch);
this.isReady = true;
this.emit("ready");
deferred.resolve(this);
};
this.on("profileSwitched", onSwitch);
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
});
return deferred.promise;
})
.then(null, (reason) =>
Cu.reportError("ProfilePanel open failed: " + reason.message));
},
/**
* Creates a new profile instance (see ProfileUI) and
* adds an appropriate item to the sidebar. Note that
* this method doesn't automatically switch user to
* the newly created profile, they have do to switch
* explicitly.
*
* @param string name
* (optional) name of the new profile
*
* @return ProfilerPanel
*/
createProfile: function PP_createProfile(name) {
if (name && this.getProfileByName(name)) {
return this.getProfileByName(name);
}
let uid = ++this._uid;
// If profile is anonymous, increase its UID until we get
// to the unused name. This way if someone manually creates
// a profile named say 'Profile 2' we won't create a dup
// with the same name. We will just skip over uid 2.
if (!name) {
name = L10N.getFormatStr("profiler.profileName", [uid]);
while (this.getProfileByName(name)) {
uid = ++this._uid;
name = L10N.getFormatStr("profiler.profileName", [uid]);
}
}
let box = this.document.createElement("vbox");
box.className = "profiler-sidebar-item";
box.id = "profile-" + uid;
let h3 = this.document.createElement("h3");
h3.textContent = name;
let span = this.document.createElement("span");
span.textContent = L10N.getStr("profiler.stateIdle");
box.appendChild(h3);
box.appendChild(span);
this.sidebar.push([box], { attachment: { uid: uid, name: name, state: PROFILE_IDLE } });
let profile = new ProfileUI(uid, name, this);
this.profiles.set(uid, profile);
this.emit("profileCreated", uid);
return profile;
},
/**
* Start collecting profile data.
*
* @param function onStart
* A function to call once we get the message
* that profiling had been successfuly started.
*/
startProfiling: function PP_startProfiling(name, onStart) {
this.controller.start(name, (err) => {
if (err) {
return void Cu.reportError("ProfilerController.start: " + err.message);
}
onStart();
this.emit("started");
});
},
/**
* Stop collecting profile data and send it to Cleopatra
* for parsing.
*
* @param function onStop
* A function to call once we get the message
* that profiling had been successfuly stopped.
*/
stopProfiling: function PP_stopProfiling(name, onStop) {
this.controller.isActive(function (err, isActive) {
if (err) {
Cu.reportError("ProfilerController.isActive: " + err.message);
return;
}
if (!isActive) {
return;
}
this.controller.stop(name, function (err, data) {
if (err) {
Cu.reportError("ProfilerController.stop: " + err.message);
return;
}
this.activeProfile.data = data;
this.activeProfile.parse(data, function onParsed() {
this.emit("parsed");
}.bind(this));
onStop();
this.emit("stopped", data);
}.bind(this));
}.bind(this));
},
/**
* Lookup an individual profile by its name.
*
* @param string name name of the profile
* @return profile object or null
*/
getProfileByName: function PP_getProfileByName(name) {
if (!this.profiles) {
return null;
}
for (let [ uid, profile ] of this.profiles) {
if (profile.name === name) {
return profile;
}
}
return null;
},
/**
* Lookup an individual profile by its UID.
*
* @param number uid UID of the profile
* @return profile object or null
*/
getProfileByUID: function PP_getProfileByUID(uid) {
if (!this.profiles) {
return null;
}
return this.profiles.get(uid) || null;
},
/**
* Iterates over each available profile and calls
* a callback with it as a parameter.
*
* @param function cb a callback to call
*/
eachProfile: function PP_eachProfile(cb) {
let uid = this._uid;
if (!this.profiles) {
return;
}
while (uid >= 0) {
if (this.profiles.has(uid)) {
cb(this.profiles.get(uid));
}
uid -= 1;
}
},
/**
* Broadcast messages to all Cleopatra instances.
*
* @param number target
* UID of the recepient profile. All profiles will receive the message
* but the profile specified by 'target' will have a special property,
* isCurrent, set to true.
* @param object data
* An object with a property 'task' that will be sent over to Cleopatra.
*/
broadcast: function PP_broadcast(target, data) {
if (!this.profiles) {
return;
}
if (data.task === "onStarted") {
this._runningUid = target;
} else {
this._runningUid = null;
}
this.eachProfile((profile) => {
profile.message({
uid: target,
isCurrent: target === profile.uid,
task: data.task
});
});
},
/**
* Open file specified in data in either a debugger or view-source.
*
* @param object data
* An object describing the file. It must have three properties:
* - uri
* - line
* - isChrome (chrome files are opened via view-source)
*/
displaySource: function PP_displaySource(data, onOpen=function() {}) {
let win = this.window;
let panelWin, timeout;
function onSourceShown(event) {
if (event.detail.url !== data.uri) {
return;
}
panelWin.removeEventListener("Debugger:SourceShown", onSourceShown, false);
panelWin.editor.setCaretPosition(data.line - 1);
onOpen();
}
if (data.isChrome) {
return void this.browserWindow.gViewSourceUtils.
viewSource(data.uri, null, this.document, data.line);
}
gDevTools.showToolbox(this.target, "jsdebugger").then(function (toolbox) {
let dbg = toolbox.getCurrentPanel();
panelWin = dbg.panelWin;
let view = dbg.panelWin.DebuggerView;
if (view.Sources.selectedValue === data.uri) {
view.editor.setCaretPosition(data.line - 1);
onOpen();
return;
}
panelWin.addEventListener("Debugger:SourceShown", onSourceShown, false);
panelWin.DebuggerView.Sources.preferredSource = data.uri;
}.bind(this));
},
/**
* Cleanup.
*/
destroy: function PP_destroy() {
if (this.profiles) {
let uid = this._uid;
while (uid >= 0) {
if (this.profiles.has(uid)) {
this.profiles.get(uid).destroy();
this.profiles.delete(uid);
}
uid -= 1;
}
}
if (this.controller) {
this.controller.destroy();
}
this.isReady = null;
this.window = null;
this.document = null;
this.target = null;
this.controller = null;
this.profiles = null;
this._uid = null;
this._activeUid = null;
this.emit("destroyed");
}
};

Просмотреть файл

@ -0,0 +1,162 @@
/* 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";
let { defer } = require("sdk/core/promise");
let EventEmitter = require("devtools/shared/event-emitter");
const { PROFILE_IDLE, PROFILE_COMPLETED, PROFILE_RUNNING } = require("devtools/profiler/consts");
/**
* An implementation of a profile visualization that uses Cleopatra.
* It consists of an iframe with Cleopatra loaded in it and some
* surrounding meta-data (such as UIDs).
*
* Cleopatra is also an event emitter. It emits the following events:
* - ready, when Cleopatra is done loading (you can also check the isReady
* property to see if a particular instance has been loaded yet.
*
* @param number uid
* Unique ID for this profile.
* @param ProfilerPanel panel
* A reference to the container panel.
*/
function Cleopatra(uid, name, panel) {
let doc = panel.document;
let win = panel.window;
EventEmitter.decorate(this);
this.isReady = false;
this.isStarted = false;
this.isFinished = false;
this.panel = panel;
this.uid = uid;
this.name = name;
this.iframe = doc.createElement("iframe");
this.iframe.setAttribute("flex", "1");
this.iframe.setAttribute("id", "profiler-cleo-" + uid);
this.iframe.setAttribute("src", "cleopatra.html?" + uid);
this.iframe.setAttribute("hidden", "true");
// Append our iframe and subscribe to postMessage events.
// They'll tell us when the underlying page is done loading
// or when user clicks on start/stop buttons.
doc.getElementById("profiler-report").appendChild(this.iframe);
win.addEventListener("message", function (event) {
if (parseInt(event.data.uid, 10) !== parseInt(this.uid, 10)) {
return;
}
switch (event.data.status) {
case "loaded":
this.isReady = true;
this.emit("ready");
break;
case "displaysource":
this.panel.displaySource(event.data.data);
}
}.bind(this));
}
Cleopatra.prototype = {
/**
* Returns a contentWindow of the iframe pointing to Cleopatra
* if it exists and can be accessed. Otherwise returns null.
*/
get contentWindow() {
if (!this.iframe) {
return null;
}
try {
return this.iframe.contentWindow;
} catch (err) {
return null;
}
},
show: function () {
this.iframe.removeAttribute("hidden");
},
hide: function () {
this.iframe.setAttribute("hidden", true);
},
/**
* Send raw profiling data to Cleopatra for parsing.
*
* @param object data
* Raw profiling data from the SPS Profiler.
* @param function onParsed
* A callback to be called when Cleopatra finishes
* parsing and displaying results.
*
*/
parse: function (data, onParsed) {
if (!this.isReady) {
return void this.on("ready", this.parse.bind(this, data, onParsed));
}
this.message({ task: "receiveProfileData", rawProfile: data }).then(() => {
let poll = () => {
let wait = this.panel.window.setTimeout.bind(null, poll, 100);
let trail = this.contentWindow.gBreadcrumbTrail;
if (!trail) {
return wait();
}
if (!trail._breadcrumbs || !trail._breadcrumbs.length) {
return wait();
}
onParsed();
};
poll();
});
},
/**
* Send a message to Cleopatra instance. If a message cannot be
* sent, this method queues it for later.
*
* @param object data JSON data to send (must be serializable)
* @return promise
*/
message: function (data) {
let deferred = defer();
data = JSON.stringify(data);
let send = () => {
if (!this.contentWindow)
setTimeout(send, 50);
this.contentWindow.postMessage(data, "*");
deferred.resolve();
};
send();
return deferred.promise;
},
/**
* Destroys the ProfileUI instance.
*/
destroy: function () {
this.isReady = null;
this.panel = null;
this.uid = null;
this.iframe = null;
this.messages = null;
}
};
module.exports = Cleopatra;

Просмотреть файл

@ -2,19 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#mainarea > .controlPane { #mainarea {
font-size: 120%;
padding-top: 75px;
text-align: center;
}
#stopWrapper {
display: none;
}
#profilerMessage {
color: #999;
display: none;
} }
/* De-emphasize chrome functions */ /* De-emphasize chrome functions */

Просмотреть файл

@ -11,10 +11,6 @@ var gInstanceUID;
* @param string status * @param string status
* Status to send to the parent page: * Status to send to the parent page:
* - loaded, when page is loaded. * - loaded, when page is loaded.
* - start, when user wants to start profiling.
* - stop, when user wants to stop profiling.
* - disabled, when the profiler was disabled
* - enabled, when the profiler was enabled
* - displaysource, when user wants to display source * - displaysource, when user wants to display source
* @param object data (optional) * @param object data (optional)
* Additional data to send to the parent page. * Additional data to send to the parent page.
@ -109,22 +105,10 @@ function initUI() {
notifyParent("stop"); notifyParent("stop");
}, false); }, false);
var controlPane = document.createElement("div"); var message = document.createElement("div");
var startProfiling = gStrings.getFormatStr("profiler.startProfiling", message.className = "message";
["<span class='btn'></span>"]); message.innerHTML = "To start profiling click the button above.";
var stopProfiling = gStrings.getFormatStr("profiler.stopProfiling", gMainArea.appendChild(message);
["<span class='btn'></span>"]);
controlPane.className = "controlPane";
controlPane.innerHTML =
"<p id='startWrapper'>" + startProfiling + "</p>" +
"<p id='stopWrapper'>" + stopProfiling + "</p>" +
"<p id='profilerMessage'></p>";
controlPane.querySelector("#startWrapper > span.btn").appendChild(startButton);
controlPane.querySelector("#stopWrapper > span.btn").appendChild(stopButton);
gMainArea.appendChild(controlPane);
} }
/** /**

Просмотреть файл

@ -1,5 +1,9 @@
const Cu = Components.utils; const Cu = Components.utils;
Cu.import("resource:///modules/devtools/ProfilerHelpers.jsm"); const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const { L10N_BUNDLE } = require("devtools/profiler/consts");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
var L10N = new ViewHelpers.L10N(L10N_BUNDLE);
/** /**
* Shortcuts for the L10N helper functions. Used in Cleopatra. * Shortcuts for the L10N helper functions. Used in Cleopatra.

Просмотреть файл

@ -2,20 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { classes: Cc, interfaces: Ci, utils: Cu } = Components; const { Cu } = require("chrome");
this.EXPORTED_SYMBOLS = []; module.exports = [];
Cu.import("resource://gre/modules/devtools/gcli.jsm"); Cu.import("resource://gre/modules/devtools/gcli.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/Require.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", loader.lazyGetter(this, "gDevTools",
"resource:///modules/devtools/gDevTools.jsm"); () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
XPCOMUtils.defineLazyModuleGetter(this, "console", var Promise = require("sdk/core/promise");
"resource://gre/modules/devtools/Console.jsm");
var Promise = require('util/promise');
/* /*
* 'profiler' command. Doesn't do anything. * 'profiler' command. Doesn't do anything.
@ -64,39 +59,17 @@ gcli.addCommand({
name: "profiler start", name: "profiler start",
description: gcli.lookup("profilerStartDesc"), description: gcli.lookup("profilerStartDesc"),
returnType: "string", returnType: "string",
params: [],
params: [
{
name: "name",
type: "string",
manual: gcli.lookup("profilerStartManual")
}
],
exec: function (args, context) { exec: function (args, context) {
function start() { function start() {
let name = args.name;
let panel = getPanel(context, "jsprofiler"); let panel = getPanel(context, "jsprofiler");
let profile = panel.getProfileByName(name) || panel.createProfile(name);
if (profile.isStarted) { if (panel.recordingProfile)
throw gcli.lookup("profilerAlreadyStarted"); throw gcli.lookup("profilerAlreadyStarted2");
}
if (profile.isFinished) { panel.toggleRecording();
throw gcli.lookup("profilerAlreadyFinished"); return gcli.lookup("profilerStarted");
}
let item = panel.sidebar.getItemByProfile(profile);
if (panel.sidebar.selectedItem === item) {
profile.start();
} else {
panel.on("profileSwitched", () => profile.start());
panel.sidebar.selectedItem = item;
}
return gcli.lookup("profilerStarting2");
} }
return gDevTools.showToolbox(context.environment.target, "jsprofiler") return gDevTools.showToolbox(context.environment.target, "jsprofiler")
@ -111,42 +84,16 @@ gcli.addCommand({
name: "profiler stop", name: "profiler stop",
description: gcli.lookup("profilerStopDesc"), description: gcli.lookup("profilerStopDesc"),
returnType: "string", returnType: "string",
params: [],
params: [
{
name: "name",
type: "string",
manual: gcli.lookup("profilerStopManual")
}
],
exec: function (args, context) { exec: function (args, context) {
function stop() { function stop() {
let panel = getPanel(context, "jsprofiler"); let panel = getPanel(context, "jsprofiler");
let profile = panel.getProfileByName(args.name);
if (!profile) { if (!panel.recordingProfile)
throw gcli.lookup("profilerNotFound"); throw gcli.lookup("profilerNotStarted3");
}
if (profile.isFinished) { panel.toggleRecording();
throw gcli.lookup("profilerAlreadyFinished");
}
if (!profile.isStarted) {
throw gcli.lookup("profilerNotStarted2");
}
let item = panel.sidebar.getItemByProfile(profile);
if (panel.sidebar.selectedItem === item) {
profile.stop();
} else {
panel.on("profileSwitched", () => profile.stop());
panel.sidebar.selectedItem = item;
}
return gcli.lookup("profilerStopping2");
} }
return gDevTools.showToolbox(context.environment.target, "jsprofiler") return gDevTools.showToolbox(context.environment.target, "jsprofiler")

Просмотреть файл

@ -0,0 +1,13 @@
/* 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";
module.exports = {
L10N_BUNDLE: "chrome://browser/locale/devtools/profiler.properties",
PROFILER_ENABLED: "devtools.profiler.enabled",
PROFILE_IDLE: 0,
PROFILE_RUNNING: 1,
PROFILE_COMPLETED: 2
};

Просмотреть файл

@ -4,22 +4,36 @@
"use strict"; "use strict";
const Cc = Components.classes; var isJSM = typeof require !== "function";
const Ci = Components.interfaces;
const Cu = Components.utils; // This code is needed because, for whatever reason, mochitest can't
// find any requirejs module so we have to load it old school way. :(
if (isJSM) {
var Cu = this["Components"].utils;
let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils;
this["loader"] = { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils) };
this["require"] = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
} else {
var { Cu } = require("chrome");
}
const { L10N_BUNDLE } = require("devtools/profiler/consts");
var EventEmitter = require("devtools/shared/event-emitter");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/Console.jsm"); Cu.import("resource://gre/modules/devtools/Console.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
let EXPORTED_SYMBOLS = ["ProfilerController"]; loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", loader.lazyGetter(this, "gDevTools",
"resource:///modules/devtools/gDevTools.jsm"); () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", loader.lazyGetter(this, "DebuggerServer",
"resource://gre/modules/devtools/dbg-server.jsm"); () => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer);
/** /**
* Data structure that contains information that has * Data structure that contains information that has
@ -44,7 +58,8 @@ function makeProfile(name, def={}) {
return { return {
name: name, name: name,
timeStarted: def.timeStarted, timeStarted: def.timeStarted,
timeEnded: def.timeEnded timeEnded: def.timeEnded,
fromConsole: def.fromConsole || false
}; };
} }
@ -75,6 +90,7 @@ function ProfilerController(target) {
this.client = target.client; this.client = target.client;
this.isConnected = false; this.isConnected = false;
this.consoleProfiles = []; this.consoleProfiles = [];
this.reservedNames = {};
addTarget(target); addTarget(target);
@ -86,9 +102,16 @@ function ProfilerController(target) {
} }
sharedData.controllers.set(target, this); sharedData.controllers.set(target, this);
EventEmitter.decorate(this);
}; };
ProfilerController.prototype = { ProfilerController.prototype = {
target: null,
client: null,
isConnected: null,
consoleProfiles: null,
reservedNames: null,
/** /**
* Return a map of profile results for the current target. * Return a map of profile results for the current target.
* *
@ -109,6 +132,19 @@ ProfilerController.prototype = {
return profile.timeStarted !== null && profile.timeEnded === null; return profile.timeStarted !== null && profile.timeEnded === null;
}, },
getProfileName: function PC_getProfileName() {
let num = 1;
let name = L10N.getFormatStr("profiler.profileName", [num]);
while (this.reservedNames[name]) {
num += 1;
name = L10N.getFormatStr("profiler.profileName", [num]);
}
this.reservedNames[name] = true;
return name;
},
/** /**
* A listener that fires whenever console.profile or console.profileEnd * A listener that fires whenever console.profile or console.profileEnd
* is called. * is called.
@ -117,26 +153,23 @@ ProfilerController.prototype = {
* Type of a call. Either 'profile' or 'profileEnd'. * Type of a call. Either 'profile' or 'profileEnd'.
* @param object data * @param object data
* Event data. * Event data.
* @param object panel
* A reference to the ProfilerPanel in the current tab.
*/ */
onConsoleEvent: function (type, data, panel) { onConsoleEvent: function (type, data) {
let name = data.extra.name; let name = data.extra.name;
let profileStart = () => { let profileStart = () => {
if (name && this.profiles.has(name)) if (name && this.profiles.has(name))
return; return;
// Add profile to the UI (createProfile will return
// an automatically generated name if 'name' is falsey).
let profile = panel.createProfile(name);
profile.start((name, cb) => cb());
// Add profile structure to shared data. // Add profile structure to shared data.
this.profiles.set(profile.name, makeProfile(profile.name, { let profile = makeProfile(name || this.getProfileName(), {
timeStarted: data.extra.currentTime timeStarted: data.extra.currentTime,
})); fromConsole: true
});
this.profiles.set(profile.name, profile);
this.consoleProfiles.push(profile.name); this.consoleProfiles.push(profile.name);
this.emit("profileStart", profile);
}; };
let profileEnd = () => { let profileEnd = () => {
@ -156,8 +189,6 @@ ProfilerController.prototype = {
return; return;
let profileData = data.extra.profile; let profileData = data.extra.profile;
profile.timeEnded = data.extra.currentTime;
profileData.threads = profileData.threads.map((thread) => { profileData.threads = profileData.threads.map((thread) => {
let samples = thread.samples.filter((sample) => { let samples = thread.samples.filter((sample) => {
return sample.time >= profile.timeStarted; return sample.time >= profile.timeStarted;
@ -166,10 +197,10 @@ ProfilerController.prototype = {
return { samples: samples }; return { samples: samples };
}); });
let ui = panel.getProfileByName(name); profile.timeEnded = data.extra.currentTime;
ui.data = profileData; profile.data = profileData;
ui.parse(profileData, () => panel.emit("parsed"));
ui.stop((name, cb) => cb()); this.emit("profileEnd", profile);
}; };
if (type === "profile") if (type === "profile")
@ -217,27 +248,7 @@ ProfilerController.prototype = {
if (toolbox == null) if (toolbox == null)
return; return;
let panel = toolbox.getPanel("jsprofiler"); this.onConsoleEvent(resp.subject.action, resp.data);
if (panel)
return void this.onConsoleEvent(resp.subject.action, resp.data, panel);
// Can't use a promise here because of a race condition when the promise
// is resolved only after -ready event is fired when creating a new panel
// and during the -ready event when waiting for a panel to be created:
//
// console.profile(); // creates a new panel, waits for the promise
// console.profileEnd(); // panel is not created yet but loading
//
// -> jsprofiler-ready event is fired which triggers a promise for profileEnd
// -> a promise for profile is triggered.
//
// And it should be the other way around. Hence the event.
toolbox.once("jsprofiler-ready", (_, panel) => {
this.onConsoleEvent(resp.subject.action, resp.data, panel);
});
toolbox.loadTool("jsprofiler");
}); });
}); });
@ -392,3 +403,9 @@ ProfilerController.prototype = {
this.actor = null; this.actor = null;
} }
}; };
if (isJSM) {
var EXPORTED_SYMBOLS = ["ProfilerController"];
} else {
module.exports = ProfilerController;
}

Просмотреть файл

@ -0,0 +1,474 @@
/* 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";
const { Cu } = require("chrome");
const { PROFILE_IDLE, PROFILE_RUNNING, PROFILE_COMPLETED } = require("devtools/profiler/consts");
var EventEmitter = require("devtools/shared/event-emitter");
var Promise = require("sdk/core/promise");
var Cleopatra = require("devtools/profiler/cleopatra");
var Sidebar = require("devtools/profiler/sidebar");
var ProfilerController = require("devtools/profiler/controller");
Cu.import("resource:///modules/devtools/gDevTools.jsm");
Cu.import("resource://gre/modules/devtools/Console.jsm");
Cu.import("resource://gre/modules/Services.jsm")
/**
* Profiler panel. It is responsible for creating and managing
* different profile instances (see cleopatra.js).
*
* ProfilerPanel is an event emitter. It can emit the following
* events:
*
* - ready: after the panel is done loading everything,
* including the default profile instance.
* - started: after the panel successfuly starts our SPS
* profiler.
* - stopped: after the panel successfuly stops our SPS
* profiler and is ready to hand over profiling
* data
* - parsed: after Cleopatra finishes parsing profiling
* data.
* - destroyed: after the panel cleans up after itself and
* is ready to be destroyed.
*
* The following events are used mainly by tests to prevent
* accidential oranges:
*
* - profileCreated: after a new profile is created.
* - profileSwitched: after user switches to a different
* profile.
*/
function ProfilerPanel(frame, toolbox) {
this.isReady = false;
this.window = frame.window;
this.document = frame.document;
this.target = toolbox.target;
this.profiles = new Map();
this._uid = 0;
this._msgQueue = {};
EventEmitter.decorate(this);
}
ProfilerPanel.prototype = {
isReady: null,
window: null,
document: null,
target: null,
controller: null,
profiles: null,
sidebar: null,
_uid: null,
_activeUid: null,
_runningUid: null,
_browserWin: null,
_msgQueue: null,
get controls() {
let doc = this.document;
return {
get record() doc.querySelector("#profiler-start")
};
},
get activeProfile() {
return this.profiles.get(this._activeUid);
},
set activeProfile(profile) {
if (this._activeUid === profile.uid)
return;
if (this.activeProfile)
this.activeProfile.hide();
this._activeUid = profile.uid;
profile.show();
},
set recordingProfile(profile) {
let btn = this.controls.record;
this._runningUid = profile ? profile.uid : null;
if (this._runningUid)
btn.setAttribute("checked", true)
else
btn.removeAttribute("checked");
},
get recordingProfile() {
return this.profiles.get(this._runningUid);
},
get browserWindow() {
if (this._browserWin) {
return this._browserWin;
}
let win = this.window.top;
let type = win.document.documentElement.getAttribute("windowtype");
if (type !== "navigator:browser") {
win = Services.wm.getMostRecentWindow("navigator:browser");
}
return this._browserWin = win;
},
/**
* Open a debug connection and, on success, switch to the newly created
* profile.
*
* @return Promise
*/
open: function PP_open() {
// Local profiling needs to make the target remote.
let target = this.target;
let promise = !target.isRemote ? target.makeRemote() : Promise.resolve(target);
return promise
.then((target) => {
let deferred = Promise.defer();
this.controller = new ProfilerController(this.target);
this.sidebar = new Sidebar(this.document.querySelector("#profiles-list"));
this.sidebar.widget.addEventListener("select", (ev) => {
if (!ev.detail)
return;
let profile = this.profiles.get(ev.detail.attachment.uid);
this.activeProfile = profile;
if (profile.isReady) {
return void this.emit("profileSwitched", profile.uid);
}
profile.once("ready", () => {
this.emit("profileSwitched", profile.uid);
});
});
this.controller.connect(() => {
let btn = this.controls.record;
btn.addEventListener("click", () => this.toggleRecording(), false);
btn.removeAttribute("disabled");
// Import queued profiles.
for (let [name, data] of this.controller.profiles) {
let profile = this.createProfile(name);
profile.isStarted = false;
profile.isFinished = true;
profile.data = data.data;
profile.parse(data.data, () => this.emit("parsed"));
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
if (!this.sidebar.selectedItem) {
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
}
}
this.isReady = true;
this.emit("ready");
deferred.resolve(this);
});
this.controller.on("profileEnd", (_, data) => {
let profile = this.createProfile(data.name);
profile.isStarted = false;
profile.isFinished = true;
profile.data = data.data;
profile.parse(data.data, () => this.emit("parsed"));
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
if (!this.sidebar.selectedItem)
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
if (this.recordingProfile && !data.fromConsole)
this.recordingProfile = null;
this.emit("stopped");
});
return deferred.promise;
})
.then(null, (reason) =>
Cu.reportError("ProfilePanel open failed: " + reason.message));
},
/**
* Creates a new profile instance (see cleopatra.js) and
* adds an appropriate item to the sidebar. Note that
* this method doesn't automatically switch user to
* the newly created profile, they have do to switch
* explicitly.
*
* @param string name
* (optional) name of the new profile
*
* @return ProfilerPanel
*/
createProfile: function (name) {
if (name && this.getProfileByName(name)) {
return this.getProfileByName(name);
}
let uid = ++this._uid;
let name = name || this.controller.getProfileName();
let profile = new Cleopatra(uid, name, this);
this.profiles.set(uid, profile);
this.sidebar.addProfile(profile);
this.emit("profileCreated", uid);
return profile;
},
/**
* Starts or stops profile recording.
*/
toggleRecording: function () {
let profile = this.recordingProfile;
if (!profile) {
profile = this.createProfile();
this.startProfiling(profile.name, () => {
profile.isStarted = true;
this.sidebar.setProfileState(profile, PROFILE_RUNNING);
this.recordingProfile = profile;
this.emit("started");
});
return;
}
this.stopProfiling(profile.name, (data) => {
profile.isStarted = false;
profile.isFinished = true;
profile.data = data;
profile.parse(data, () => this.emit("parsed"));
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
this.activeProfile = profile;
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
this.recordingProfile = null;
this.emit("stopped");
});
},
/**
* Start collecting profile data.
*
* @param function onStart
* A function to call once we get the message
* that profiling had been successfuly started.
*/
startProfiling: function (name, onStart) {
this.controller.start(name, (err) => {
if (err) {
return void Cu.reportError("ProfilerController.start: " + err.message);
}
onStart();
this.emit("started");
});
},
/**
* Stop collecting profile data.
*
* @param function onStop
* A function to call once we get the message
* that profiling had been successfuly stopped.
*/
stopProfiling: function (name, onStop) {
this.controller.isActive((err, isActive) => {
if (err) {
Cu.reportError("ProfilerController.isActive: " + err.message);
return;
}
if (!isActive) {
return;
}
this.controller.stop(name, (err, data) => {
if (err) {
Cu.reportError("ProfilerController.stop: " + err.message);
return;
}
onStop(data);
this.emit("stopped", data);
});
});
},
/**
* Lookup an individual profile by its name.
*
* @param string name name of the profile
* @return profile object or null
*/
getProfileByName: function PP_getProfileByName(name) {
if (!this.profiles) {
return null;
}
for (let [ uid, profile ] of this.profiles) {
if (profile.name === name) {
return profile;
}
}
return null;
},
/**
* Lookup an individual profile by its UID.
*
* @param number uid UID of the profile
* @return profile object or null
*/
getProfileByUID: function PP_getProfileByUID(uid) {
if (!this.profiles) {
return null;
}
return this.profiles.get(uid) || null;
},
/**
* Iterates over each available profile and calls
* a callback with it as a parameter.
*
* @param function cb a callback to call
*/
eachProfile: function PP_eachProfile(cb) {
let uid = this._uid;
if (!this.profiles) {
return;
}
while (uid >= 0) {
if (this.profiles.has(uid)) {
cb(this.profiles.get(uid));
}
uid -= 1;
}
},
/**
* Broadcast messages to all Cleopatra instances.
*
* @param number target
* UID of the recepient profile. All profiles will receive the message
* but the profile specified by 'target' will have a special property,
* isCurrent, set to true.
* @param object data
* An object with a property 'task' that will be sent over to Cleopatra.
*/
broadcast: function PP_broadcast(target, data) {
if (!this.profiles) {
return;
}
this.eachProfile((profile) => {
profile.message({
uid: target,
isCurrent: target === profile.uid,
task: data.task
});
});
},
/**
* Open file specified in data in either a debugger or view-source.
*
* @param object data
* An object describing the file. It must have three properties:
* - uri
* - line
* - isChrome (chrome files are opened via view-source)
*/
displaySource: function PP_displaySource(data, onOpen=function() {}) {
let win = this.window;
let panelWin, timeout;
function onSourceShown(event) {
if (event.detail.url !== data.uri) {
return;
}
panelWin.removeEventListener("Debugger:SourceShown", onSourceShown, false);
panelWin.editor.setCaretPosition(data.line - 1);
onOpen();
}
if (data.isChrome) {
return void this.browserWindow.gViewSourceUtils.
viewSource(data.uri, null, this.document, data.line);
}
gDevTools.showToolbox(this.target, "jsdebugger").then(function (toolbox) {
let dbg = toolbox.getCurrentPanel();
panelWin = dbg.panelWin;
let view = dbg.panelWin.DebuggerView;
if (view.Sources.selectedValue === data.uri) {
view.editor.setCaretPosition(data.line - 1);
onOpen();
return;
}
panelWin.addEventListener("Debugger:SourceShown", onSourceShown, false);
panelWin.DebuggerView.Sources.preferredSource = data.uri;
}.bind(this));
},
/**
* Cleanup.
*/
destroy: function PP_destroy() {
if (this.profiles) {
let uid = this._uid;
while (uid >= 0) {
if (this.profiles.has(uid)) {
this.profiles.get(uid).destroy();
this.profiles.delete(uid);
}
uid -= 1;
}
}
if (this.controller) {
this.controller.destroy();
}
this.isReady = null;
this.window = null;
this.document = null;
this.target = null;
this.controller = null;
this.profiles = null;
this._uid = null;
this._activeUid = null;
this.emit("destroyed");
}
};
module.exports = ProfilerPanel;

Просмотреть файл

@ -19,10 +19,9 @@
<box flex="1" id="profiler-chrome" class="devtools-responsive-container"> <box flex="1" id="profiler-chrome" class="devtools-responsive-container">
<vbox class="profiler-sidebar"> <vbox class="profiler-sidebar">
<toolbar class="devtools-toolbar"> <toolbar class="devtools-toolbar">
<toolbarbutton id="profiler-create" <hbox id="profiler-controls">
class="devtools-toolbarbutton" <toolbarbutton id="profiler-start" class="devtools-toolbarbutton"/>
label="&profilerNew.label;" </hbox>
disabled="true"/>
</toolbar> </toolbar>
<vbox id="profiles-list" flex="1"> <vbox id="profiles-list" flex="1">

Просмотреть файл

@ -0,0 +1,86 @@
/* 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";
let { Cu } = require("chrome");
let EventEmitter = require("devtools/shared/event-emitter");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const {
PROFILE_IDLE,
PROFILE_COMPLETED,
PROFILE_RUNNING,
L10N_BUNDLE
} = require("devtools/profiler/consts");
loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
function Sidebar(el) {
EventEmitter.decorate(this);
this.document = el.ownerDocument;
this.widget = new SideMenuWidget(el);
this.widget.notice = L10N.getStr("profiler.sidebarNotice");
}
Sidebar.prototype = Heritage.extend(WidgetMethods, {
addProfile: function (profile) {
let doc = this.document;
let box = doc.createElement("vbox");
let h3 = doc.createElement("h3");
let span = doc.createElement("span");
box.id = "profile-" + profile.uid;
box.className = "profiler-sidebar-item";
h3.textContent = profile.name;
span.textContent = L10N.getStr("profiler.stateIdle");
box.appendChild(h3);
box.appendChild(span);
this.push([box], {
attachment: {
uid: profile.uid,
name: profile.name,
state: PROFILE_IDLE
}
});
},
getElementByProfile: function (profile) {
return this.document.querySelector("#profile-" + profile.uid);
},
getItemByProfile: function (profile) {
return this.getItemForPredicate(item => item.attachment.uid === profile.uid);
},
setProfileState: function (profile, state) {
let item = this.getItemByProfile(profile);
let label = item.target.querySelector(".profiler-sidebar-item > span");
switch (state) {
case PROFILE_IDLE:
label.textContent = L10N.getStr("profiler.stateIdle");
break;
case PROFILE_RUNNING:
label.textContent = L10N.getStr("profiler.stateRunning");
break;
case PROFILE_COMPLETED:
label.textContent = L10N.getStr("profiler.stateCompleted");
break;
default: // Wrong state, do nothing.
return;
}
item.attachment.state = state;
this.emit("stateChanged", item);
}
});
module.exports = Sidebar;

Просмотреть файл

@ -11,18 +11,17 @@ relativesrcdir = @relativesrcdir@
include $(DEPTH)/config/autoconf.mk include $(DEPTH)/config/autoconf.mk
MOCHITEST_BROWSER_TESTS = \ MOCHITEST_BROWSER_TESTS = \
browser_profiler_profiles.js \
browser_profiler_remote.js \ browser_profiler_remote.js \
browser_profiler_bug_834878_source_buttons.js \ browser_profiler_bug_834878_source_buttons.js \
browser_profiler_cmd.js \ browser_profiler_cmd.js \
browser_profiler_run.js \ browser_profiler_run.js \
browser_profiler_controller.js \ browser_profiler_controller.js \
browser_profiler_bug_830664_multiple_profiles.js \
browser_profiler_bug_855244_multiple_tabs.js \ browser_profiler_bug_855244_multiple_tabs.js \
browser_profiler_console_api.js \ browser_profiler_console_api.js \
browser_profiler_console_api_named.js \ browser_profiler_console_api_named.js \
browser_profiler_console_api_mixed.js \ browser_profiler_console_api_mixed.js \
browser_profiler_console_api_content.js \ browser_profiler_console_api_content.js \
browser_profiler_escape.js \
head.js \ head.js \
$(NULL) $(NULL)

Просмотреть файл

@ -1,63 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
let gTab, gPanel, gUid;
function test() {
waitForExplicitFinish();
setUp(URL, function onSetUp(tab, browser, panel) {
gTab = tab;
gPanel = panel;
gPanel.once("profileCreated", function (_, uid) {
gUid = uid;
let profile = gPanel.profiles.get(uid);
if (profile.isReady) {
startProfiling();
} else {
profile.once("ready", startProfiling);
}
});
gPanel.createProfile();
});
}
function startProfiling() {
gPanel.profiles.get(gPanel.activeProfile.uid).once("started", function () {
setTimeout(function () {
sendFromProfile(2, "start");
gPanel.profiles.get(2).once("started", function () setTimeout(stopProfiling, 50));
}, 50);
});
sendFromProfile(gPanel.activeProfile.uid, "start");
}
function stopProfiling() {
is(getSidebarItem(1).attachment.state, PROFILE_RUNNING);
is(getSidebarItem(2).attachment.state, PROFILE_RUNNING);
gPanel.profiles.get(gPanel.activeProfile.uid).once("stopped", function () {
is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
sendFromProfile(2, "stop");
gPanel.profiles.get(2).once("stopped", confirmAndFinish);
});
sendFromProfile(gPanel.activeProfile.uid, "stop");
}
function confirmAndFinish(ev, data) {
is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
is(getSidebarItem(2).attachment.state, PROFILE_COMPLETED);
tearDown(gTab, function onTearDown() {
gPanel = null;
gTab = null;
gUid = null;
});
}

Просмотреть файл

@ -9,30 +9,26 @@ function test() {
waitForExplicitFinish(); waitForExplicitFinish();
setUp(URL, function onSetUp(tab, browser, panel) { setUp(URL, function onSetUp(tab, browser, panel) {
panel.once("profileCreated", function () { let data = { uri: SCRIPT, line: 5, isChrome: false };
let data = { uri: SCRIPT, line: 5, isChrome: false };
panel.displaySource(data, function onOpen() { panel.displaySource(data, function onOpen() {
let target = TargetFactory.forTab(tab); let target = TargetFactory.forTab(tab);
let dbg = gDevTools.getToolbox(target).getPanel("jsdebugger"); let dbg = gDevTools.getToolbox(target).getPanel("jsdebugger");
let view = dbg.panelWin.DebuggerView; let view = dbg.panelWin.DebuggerView;
is(view.Sources.selectedValue, data.uri, "URI is different"); is(view.Sources.selectedValue, data.uri, "URI is different");
is(view.editor.getCaretPosition().line, data.line - 1, is(view.editor.getCaretPosition().line, data.line - 1,
"Line is different"); "Line is different");
// Test the case where script is already loaded. // Test the case where script is already loaded.
view.editor.setCaretPosition(1); view.editor.setCaretPosition(1);
gDevTools.showToolbox(target, "jsprofiler").then(function () { gDevTools.showToolbox(target, "jsprofiler").then(function () {
panel.displaySource(data, function onOpenAgain() { panel.displaySource(data, function onOpenAgain() {
is(view.editor.getCaretPosition().line, data.line - 1, is(view.editor.getCaretPosition().line, data.line - 1,
"Line is different"); "Line is different");
tearDown(tab); tearDown(tab);
});
}); });
}); });
}); });
panel.createProfile();
}); });
} }

Просмотреть файл

@ -41,54 +41,41 @@ function testProfilerStart() {
let deferred = Promise.defer(); let deferred = Promise.defer();
gPanel.once("started", function () { gPanel.once("started", function () {
is(gPanel.profiles.size, 2, "There are two profiles"); is(gPanel.profiles.size, 1, "There is a new profile");
ok(!gPanel.getProfileByName("Profile 1").isStarted, "Profile 1 wasn't started"); is(gPanel.getProfileByName("Profile 1"), gPanel.recordingProfile, "Recording profile is OK");
ok(gPanel.getProfileByName("Profile 2").isStarted, "Profile 2 was started"); ok(!gPanel.activeProfile, "There's no active profile yet");
cmd('profiler start "Profile 2"', "This profile has already been started"); cmd("profiler start", gcli.lookup("profilerAlreadyStarted2"));
deferred.resolve(); deferred.resolve();
}); });
cmd("profiler start", gcli.lookup("profilerStarting2")); cmd("profiler start", gcli.lookup("profilerStarted"));
return deferred.promise; return deferred.promise;
} }
function testProfilerList() { function testProfilerList() {
let deferred = Promise.defer(); cmd("profiler list", /^.*Profile\s1\s\*.*$/);
cmd("profiler list", /^.*Profile\s1.*Profile\s2\s\*.*$/);
deferred.resolve();
return deferred.promise;
} }
function testProfilerStop() { function testProfilerStop() {
let deferred = Promise.defer(); let deferred = Promise.defer();
gPanel.once("stopped", function () { gPanel.once("stopped", function () {
ok(!gPanel.getProfileByName("Profile 2").isStarted, "Profile 2 was stopped"); is(gPanel.activeProfile, gPanel.getProfileByName("Profile 1"), "Active profile is OK");
ok(gPanel.getProfileByName("Profile 2").isFinished, "Profile 2 was stopped"); ok(!gPanel.recordingProfile, "There's no recording profile");
cmd('profiler stop "Profile 2"', "This profile has already been completed. " + cmd("profiler stop", gcli.lookup("profilerNotStarted3"));
"Use 'profile show' command to see its results");
cmd('profiler stop "Profile 1"', "This profile has not been started yet. " +
"Use 'profile start' to start profiling");
cmd('profiler stop "invalid"', "Profile not found")
deferred.resolve(); deferred.resolve();
}); });
cmd('profiler stop "Profile 2"', gcli.lookup("profilerStopping2")); cmd("profiler stop");
return deferred.promise; return deferred.promise;
} }
function testProfilerShow() { function testProfilerShow() {
let deferred = Promise.defer(); let deferred = Promise.defer();
is(gPanel.getProfileByName("Profile 2").uid, gPanel.activeProfile.uid,
"Profile 2 is active");
gPanel.once("profileSwitched", function () { gPanel.once("profileSwitched", function () {
is(gPanel.getProfileByName("Profile 1").uid, gPanel.activeProfile.uid, is(gPanel.getProfileByName("Profile 1"), gPanel.activeProfile, "Profile 1 is active");
"Profile 1 is active"); cmd('profile show "invalid"', gcli.lookup("profilerNotFound"));
cmd('profile show "invalid"', "Profile not found");
deferred.resolve(); deferred.resolve();
}); });

Просмотреть файл

@ -19,46 +19,17 @@ function test() {
function testConsoleProfile(hud) { function testConsoleProfile(hud) {
hud.jsterm.clearOutput(true); hud.jsterm.clearOutput(true);
// Here we start two named profiles and then end one of them.
// profileEnd, when name is not provided, simply pops the latest
// profile.
let profilesStarted = 0; let profilesStarted = 0;
function profileEnd(_, uid) { gPanel.once("parsed", () => {
let profile = gPanel.profiles.get(uid); let profile = gPanel.activeProfile;
profile.once("started", () => { is(profile.name, "Profile 1", "Profile name is OK");
if (++profilesStarted < 2) is(gPanel.sidebar.selectedItem, gPanel.sidebar.getItemByProfile(profile), "Sidebar is OK");
return; is(gPanel.sidebar.selectedItem.attachment.state, PROFILE_COMPLETED);
gPanel.off("profileCreated", profileEnd);
gPanel.profiles.get(3).once("stopped", () => {
openProfiler(gTab, checkProfiles);
});
hud.jsterm.execute("console.profileEnd()");
});
}
gPanel.on("profileCreated", profileEnd);
hud.jsterm.execute("console.profile()");
hud.jsterm.execute("console.profile()");
}
function checkProfiles(toolbox) {
let panel = toolbox.getPanel("jsprofiler");
is(getSidebarItem(1, panel).attachment.state, PROFILE_IDLE);
is(getSidebarItem(2, panel).attachment.state, PROFILE_RUNNING);
is(getSidebarItem(3, panel).attachment.state, PROFILE_COMPLETED);
// Make sure we can still stop profiles via the UI.
gPanel.profiles.get(2).once("stopped", () => {
is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED);
tearDown(gTab, () => gTab = gPanel = null); tearDown(gTab, () => gTab = gPanel = null);
}); });
sendFromProfile(2, "stop"); hud.jsterm.execute("console.profile()");
hud.jsterm.execute("console.profileEnd()");
} }

Просмотреть файл

@ -29,8 +29,7 @@ function test() {
} }
function runTests() { function runTests() {
is(getSidebarItem(1).attachment.state, PROFILE_IDLE); is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
is(getSidebarItem(2).attachment.state, PROFILE_COMPLETED);
gPanel.once("parsed", () => { gPanel.once("parsed", () => {
function assertSampleAndFinish() { function assertSampleAndFinish() {

Просмотреть файл

@ -18,12 +18,13 @@ function test() {
function runTests(toolbox) { function runTests(toolbox) {
let panel = toolbox.getPanel("jsprofiler"); let panel = toolbox.getPanel("jsprofiler");
let record = gPanel.controls.record;
panel.profiles.get(1).once("started", () => { panel.once("started", () => {
is(getSidebarItem(1, panel).attachment.state, PROFILE_RUNNING); is(getSidebarItem(1, panel).attachment.state, PROFILE_RUNNING);
openConsole(gTab, (hud) => { openConsole(gTab, (hud) => {
panel.profiles.get(1).once("stopped", () => { panel.once("stopped", () => {
is(getSidebarItem(1, panel).attachment.state, PROFILE_COMPLETED); is(getSidebarItem(1, panel).attachment.state, PROFILE_COMPLETED);
tearDown(gTab, () => gTab = gPanel = null); tearDown(gTab, () => gTab = gPanel = null);
}); });
@ -32,5 +33,5 @@ function runTests(toolbox) {
}); });
}); });
sendFromProfile(1, "start"); record.click();
} }

Просмотреть файл

@ -23,23 +23,16 @@ function testConsoleProfile(hud) {
let profilesStarted = 0; let profilesStarted = 0;
function profileEnd(_, uid) { function endProfile() {
let profile = gPanel.profiles.get(uid); if (++profilesStarted < 2)
return;
profile.once("started", () => { gPanel.controller.off("profileStart", endProfile);
if (++profilesStarted < 2) gPanel.controller.once("profileEnd", () => openProfiler(gTab, checkProfiles));
return; hud.jsterm.execute("console.profileEnd('Second')");
gPanel.off("profileCreated", profileEnd);
gPanel.profiles.get(2).once("stopped", () => {
openProfiler(gTab, checkProfiles);
});
hud.jsterm.execute("console.profileEnd('Second')");
});
} }
gPanel.on("profileCreated", profileEnd); gPanel.controller.on("profileStart", endProfile);
hud.jsterm.execute("console.profile('Second')"); hud.jsterm.execute("console.profile('Second')");
hud.jsterm.execute("console.profile('Third')"); hud.jsterm.execute("console.profile('Third')");
} }
@ -47,17 +40,14 @@ function testConsoleProfile(hud) {
function checkProfiles(toolbox) { function checkProfiles(toolbox) {
let panel = toolbox.getPanel("jsprofiler"); let panel = toolbox.getPanel("jsprofiler");
is(getSidebarItem(1, panel).attachment.state, PROFILE_IDLE); is(getSidebarItem(1, panel).attachment.name, "Second", "Name in sidebar is OK");
is(getSidebarItem(2, panel).attachment.name, "Second"); is(getSidebarItem(1, panel).attachment.state, PROFILE_COMPLETED, "State in sidebar is OK");
is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED);
is(getSidebarItem(3, panel).attachment.name, "Third");
is(getSidebarItem(3, panel).attachment.state, PROFILE_RUNNING);
// Make sure we can still stop profiles via the queue pop. // Make sure we can still stop profiles via the queue pop.
gPanel.profiles.get(3).once("stopped", () => { gPanel.controller.once("profileEnd", () => {
openProfiler(gTab, () => { openProfiler(gTab, () => {
is(getSidebarItem(3, panel).attachment.state, PROFILE_COMPLETED); is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED, "State in sidebar is OK");
tearDown(gTab, () => gTab = gPanel = null); tearDown(gTab, () => gTab = gPanel = null);
}); });
}); });

Просмотреть файл

@ -0,0 +1,43 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
let gTab, gPanel;
function test() {
waitForExplicitFinish();
setUp(URL, function (tab, browser, panel) {
gTab = tab;
gPanel = panel;
let record = gPanel.controls.record;
gPanel.once("started", () => {
gPanel.once("stopped", () => {
let [ win, doc ] = getProfileInternals(gPanel.activeProfile.uid);
let expl = "<script>function f() {}</script></textarea><img/src='about:logo'>";
let expl2 = "<script>function f() {}</script></pre><img/src='about:logo'>";
is(win.escapeHTML(expl),
"&lt;script&gt;function f() {}&lt;/script&gt;&lt;/textarea&gt;&lt;img/src='about:logo'&gt;");
is(win.escapeHTML(expl2),
"&lt;script&gt;function f() {}&lt;/script&gt;&lt;/pre&gt;&lt;img/src='about:logo'&gt;");
tearDown(gTab, () => {
gTab = null;
gPanel = null;
});
});
setTimeout(() => {
record.click();
}, 50);
});
record.click();
});
}

Просмотреть файл

@ -1,69 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
let gTab, gPanel;
function test() {
waitForExplicitFinish();
setUp(URL, function onSetUp(tab, browser, panel) {
gTab = tab;
gPanel = panel;
panel.once("profileCreated", onProfileCreated);
panel.once("profileSwitched", onProfileSwitched);
testNewProfile();
});
}
function testNewProfile() {
is(gPanel.profiles.size, 1, "There is only one profile");
let btn = gPanel.document.getElementById("profiler-create");
ok(!btn.getAttribute("disabled"), "Create Profile button is not disabled");
btn.click();
}
function onProfileCreated(name, uid) {
is(gPanel.profiles.size, 2, "There are two profiles now");
ok(gPanel.activeProfile.uid !== uid, "New profile is not yet active");
let btn = gPanel.document.getElementById("profile-" + uid);
ok(btn, "Profile item has been added to the sidebar");
btn.click();
}
function onProfileSwitched(name, uid) {
gPanel.once("profileCreated", onNamedProfileCreated);
gPanel.once("profileSwitched", onNamedProfileSwitched);
ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
gPanel.createProfile("Custom Profile");
}
function onNamedProfileCreated(name, uid) {
is(gPanel.profiles.size, 3, "There are three profiles now");
is(gPanel.getProfileByUID(uid).name, "Custom Profile", "Name is correct");
let profile = gPanel.profiles.get(uid);
let data = gPanel.sidebar.getItemByProfile(profile).attachment;
is(data.uid, uid, "UID is correct");
is(data.name, "Custom Profile", "Name is correct on the label");
let btn = gPanel.document.getElementById("profile-" + uid);
ok(btn, "Profile item has been added to the sidebar");
btn.click();
}
function onNamedProfileSwitched(name, uid) {
ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
tearDown(gTab, function onTearDown() {
gPanel = null;
gTab = null;
});
}

Просмотреть файл

@ -12,7 +12,7 @@ Cu.import("resource://gre/modules/devtools/dbg-client.jsm", temp);
let DebuggerClient = temp.DebuggerClient; let DebuggerClient = temp.DebuggerClient;
let debuggerSocketConnect = temp.debuggerSocketConnect; let debuggerSocketConnect = temp.debuggerSocketConnect;
Cu.import("resource:///modules/devtools/ProfilerController.jsm", temp); Cu.import("resource:///modules/devtools/profiler/controller.js", temp);
let ProfilerController = temp.ProfilerController; let ProfilerController = temp.ProfilerController;
function test() { function test() {

Просмотреть файл

@ -3,7 +3,7 @@
const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>"; const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
let gTab, gPanel, gAttempts = 0; let gTab, gPanel;
function test() { function test() {
waitForExplicitFinish(); waitForExplicitFinish();
@ -12,55 +12,104 @@ function test() {
gTab = tab; gTab = tab;
gPanel = panel; gPanel = panel;
panel.once("started", onStart); function done() {
panel.once("parsed", onParsed); tearDown(gTab, () => { gPanel = null; gTab = null; });
testUI();
});
}
function testUI() {
ok(gPanel, "Profiler panel exists");
ok(gPanel.activeProfile, "Active profile exists");
let [win, doc] = getProfileInternals();
let startButton = doc.querySelector(".controlPane #startWrapper button");
let stopButton = doc.querySelector(".controlPane #stopWrapper button");
ok(startButton, "Start button exists");
ok(stopButton, "Stop button exists");
startButton.click();
}
function onStart() {
gPanel.controller.isActive(function (err, isActive) {
ok(isActive, "Profiler is running");
let [win, doc] = getProfileInternals();
let stopButton = doc.querySelector(".controlPane #stopWrapper button");
setTimeout(function () stopButton.click(), 100);
});
}
function onParsed() {
function assertSample() {
let [win,doc] = getProfileInternals();
let sample = doc.getElementsByClassName("samplePercentage");
if (sample.length <= 0) {
return void setTimeout(assertSample, 100);
} }
ok(sample.length > 0, "We have some items displayed"); startRecording()
is(sample[0].innerHTML, "100.0%", "First percentage is 100%"); .then(stopRecording)
.then(startRecordingAgain)
tearDown(gTab, function onTearDown() { .then(stopRecording)
gPanel = null; .then(switchBackToTheFirstOne)
gTab = null; .then(done);
}); });
}
assertSample();
} }
function startRecording() {
let deferred = Promise.defer();
ok(gPanel, "Profiler panel exists");
ok(!gPanel.activeProfile, "Active profile doesn't exist");
ok(!gPanel.recordingProfile, "Recording profile doesn't exist");
let record = gPanel.controls.record;
ok(record, "Record button exists.");
ok(!record.getAttribute("checked"), "Record button is unchecked");
gPanel.once("started", () => {
let item = gPanel.sidebar.getItemByProfile(gPanel.recordingProfile);
is(item.attachment.name, "Profile 1");
is(item.attachment.state, PROFILE_RUNNING);
gPanel.controller.isActive(function (err, isActive) {
ok(isActive, "Profiler is running");
deferred.resolve();
});
});
record.click();
return deferred.promise;
}
function stopRecording() {
let deferred = Promise.defer();
gPanel.once("parsed", () => {
let item = gPanel.sidebar.getItemByProfile(gPanel.activeProfile);
is(item.attachment.state, PROFILE_COMPLETED);
function assertSample() {
let [ win, doc ] = getProfileInternals();
let sample = doc.getElementsByClassName("samplePercentage");
if (sample.length <= 0) {
return void setTimeout(assertSample, 100);
}
ok(sample.length > 0, "We have some items displayed");
is(sample[0].innerHTML, "100.0%", "First percentage is 100%");
deferred.resolve();
}
assertSample();
});
setTimeout(function () gPanel.controls.record.click(), 100);
return deferred.promise;
}
function startRecordingAgain() {
let deferred = Promise.defer();
let record = gPanel.controls.record;
ok(!record.getAttribute("checked"), "Record button is unchecked");
gPanel.once("started", () => {
ok(gPanel.activeProfile !== gPanel.recordingProfile);
let item = gPanel.sidebar.getItemByProfile(gPanel.recordingProfile);
is(item.attachment.name, "Profile 2");
is(item.attachment.state, PROFILE_RUNNING);
deferred.resolve();
});
record.click();
return deferred.promise;
}
function switchBackToTheFirstOne() {
let deferred = Promise.defer();
let button = gPanel.sidebar.getElementByProfile({ uid: 1 });
let item = gPanel.sidebar.getItemByProfile({ uid: 1 });
gPanel.once("profileSwitched", () => {
is(gPanel.activeProfile.uid, 1, "activeProfile is correct");
is(gPanel.sidebar.selectedItem, item, "selectedItem is correct");
deferred.resolve();
});
button.click();
return deferred.promise;
}

Просмотреть файл

@ -1238,13 +1238,7 @@ profilerShowManual=Name of a profile.
# LOCALIZATION NOTE (profilerAlreadyStarted) A message that is displayed whenever # LOCALIZATION NOTE (profilerAlreadyStarted) A message that is displayed whenever
# an operation cannot be completed because the profile in question has already # an operation cannot be completed because the profile in question has already
# been started. # been started.
profilerAlreadyStarted=This profile has already been started profilerAlreadyStarted2=Profile has already been started
# LOCALIZATION NOTE (profilerAlreadyFinished) A message that is displayed whenever
# an operation cannot be completed because the profile in question has already
# been finished. It also contains a hint to use the 'profile show' command to see
# the profiling results.
profilerAlreadyFinished=This profile has already been completed. Use 'profile show' command to see its results
# LOCALIZATION NOTE (profilerNotFound) A message that is displayed whenever # LOCALIZATION NOTE (profilerNotFound) A message that is displayed whenever
# an operation cannot be completed because the profile in question could not be # an operation cannot be completed because the profile in question could not be
@ -1255,15 +1249,11 @@ profilerNotFound=Profile not found
# an operation cannot be completed because the profile in question has not been # an operation cannot be completed because the profile in question has not been
# started yet. It also contains a hint to use the 'profile start' command to # started yet. It also contains a hint to use the 'profile start' command to
# start the profiler. # start the profiler.
profilerNotStarted2=This profile has not been started yet. Use 'profile start' to start profiling profilerNotStarted3=Profiler has not been started yet. Use 'profile start' to start profiling
# LOCALIZATION NOTE (profilerStarting) A very short string that indicates that # LOCALIZATION NOTE (profilerStarted) A very short string that indicates that
# we're starting the profiler. # we have started recording.
profilerStarting2=Starting… profilerStarted=Recording...
# LOCALIZATION NOTE (profilerStopping) A very short string that indicates that
# we're stopping the profiler.
profilerStopping2=Stopping…
# LOCALIZATION NOTE (profilerNotReady) A message that is displayed whenever # LOCALIZATION NOTE (profilerNotReady) A message that is displayed whenever
# an operation cannot be completed because the profiler has not been opened yet. # an operation cannot be completed because the profiler has not been opened yet.

Просмотреть файл

@ -13,3 +13,11 @@
<!-- LOCALIZATION NOTE (profilerNew.label): This is the label for the <!-- LOCALIZATION NOTE (profilerNew.label): This is the label for the
- button that creates a new profile. --> - button that creates a new profile. -->
<!ENTITY profilerNew.label "New"> <!ENTITY profilerNew.label "New">
<!-- LOCALIZATION NOTE (profilerStart.label): This is the label for the
- button that starts the profiler. -->
<!ENTITY profilerStart.label "Start">
<!-- LOCALIZATION NOTE (profilerStop.label): This is the label for the
- button that stops the profiler. -->
<!ENTITY profilerStop.label "Stop">

Просмотреть файл

@ -97,3 +97,9 @@ profiler.stateRunning=Running
# This string is used to show that the profile in question is in COMPLETED # This string is used to show that the profile in question is in COMPLETED
# state meaning that it has been started and stopped already. # state meaning that it has been started and stopped already.
profiler.stateCompleted=Completed profiler.stateCompleted=Completed
# LOCALIZATION NOTE (profiler.sidebarNotice)
# This string is displayed in the profiler sidebar when there are no
# existing profiles to show (usually happens when the user opens the
# profiler for the first time).
profiler.sidebarNotice=There are no profiles yet.

Просмотреть файл

Просмотреть файл

@ -200,6 +200,7 @@ browser.jar:
skin/classic/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css) skin/classic/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css)
skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css) skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
skin/classic/browser/devtools/inspector.css (devtools/inspector.css) skin/classic/browser/devtools/inspector.css (devtools/inspector.css)
skin/classic/browser/devtools/profiler-stopwatch.png (devtools/profiler-stopwatch.png)
skin/classic/browser/devtools/toolbox.css (devtools/toolbox.css) skin/classic/browser/devtools/toolbox.css (devtools/toolbox.css)
skin/classic/browser/devtools/tool-options.png (devtools/tool-options.png) skin/classic/browser/devtools/tool-options.png (devtools/tool-options.png)
skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png) skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png)

Просмотреть файл

Просмотреть файл

@ -286,6 +286,7 @@ browser.jar:
skin/classic/browser/devtools/dock-bottom.png (devtools/dock-bottom.png) skin/classic/browser/devtools/dock-bottom.png (devtools/dock-bottom.png)
skin/classic/browser/devtools/dock-side.png (devtools/dock-side.png) skin/classic/browser/devtools/dock-side.png (devtools/dock-side.png)
* skin/classic/browser/devtools/inspector.css (devtools/inspector.css) * skin/classic/browser/devtools/inspector.css (devtools/inspector.css)
skin/classic/browser/devtools/profiler-stopwatch.png (devtools/profiler-stopwatch.png)
skin/classic/browser/devtools/toolbox.css (devtools/toolbox.css) skin/classic/browser/devtools/toolbox.css (devtools/toolbox.css)
skin/classic/browser/devtools/tool-options.png (devtools/tool-options.png) skin/classic/browser/devtools/tool-options.png (devtools/tool-options.png)
skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png) skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png)

Просмотреть файл

@ -4,6 +4,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%endif %endif
.profiler-sidebar-empty-notice {
max-width: 176px;
padding: 10px;
background-color: rgb(61, 69, 76);
color: white;
font-weight: bold;
}
.devtools-toolbar {
min-height: 33px;
}
.profiler-sidebar { .profiler-sidebar {
min-width: 196px; min-width: 196px;
} }
@ -30,14 +42,33 @@
color: rgb(128, 195, 228); color: rgb(128, 195, 228);
} }
.devtools-toolbar { #profiler-controls > toolbarbutton {
height: 26px; margin: 0;
padding: 3px; box-shadow: none;
border-radius: 0;
border-width: 0;
-moz-border-end-width: 1px;
outline-offset: -3px;
} }
.devtools-toolbar .devtools-toolbarbutton { #profiler-controls > toolbarbutton:last-of-type {
min-width: 48px; -moz-border-end-width: 0;
min-height: 0; }
font-size: 11px;
padding: 0px 8px; #profiler-controls {
box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset,
0 0 0 1px hsla(210,16%,76%,.15) inset,
0 1px 0 hsla(210,16%,76%,.15);
border: 1px solid hsla(210,8%,5%,.45);
border-radius: 3px;
margin: 0 3px;
}
#profiler-start {
list-style-image: url("chrome://browser/skin/devtools/profiler-stopwatch.png");
-moz-image-region: rect(0px,16px,16px,0px);
}
#profiler-start[checked] {
-moz-image-region: rect(0px,32px,16px,16px);
} }

Просмотреть файл

Просмотреть файл

@ -226,6 +226,7 @@ browser.jar:
skin/classic/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css) skin/classic/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css)
skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css) skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
skin/classic/browser/devtools/inspector.css (devtools/inspector.css) skin/classic/browser/devtools/inspector.css (devtools/inspector.css)
skin/classic/browser/devtools/profiler-stopwatch.png (devtools/profiler-stopwatch.png)
skin/classic/browser/devtools/toolbox.css (devtools/toolbox.css) skin/classic/browser/devtools/toolbox.css (devtools/toolbox.css)
skin/classic/browser/devtools/tool-options.png (devtools/tool-options.png) skin/classic/browser/devtools/tool-options.png (devtools/tool-options.png)
skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png) skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png)
@ -479,6 +480,7 @@ browser.jar:
skin/classic/aero/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css) skin/classic/aero/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css)
skin/classic/aero/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css) skin/classic/aero/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
skin/classic/aero/browser/devtools/inspector.css (devtools/inspector.css) skin/classic/aero/browser/devtools/inspector.css (devtools/inspector.css)
skin/classic/aero/browser/devtools/profiler-stopwatch.png (devtools/profiler-stopwatch.png)
skin/classic/aero/browser/devtools/toolbox.css (devtools/toolbox.css) skin/classic/aero/browser/devtools/toolbox.css (devtools/toolbox.css)
skin/classic/aero/browser/devtools/tool-options.png (devtools/tool-options.png) skin/classic/aero/browser/devtools/tool-options.png (devtools/tool-options.png)
skin/classic/aero/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png) skin/classic/aero/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png)

Просмотреть файл

@ -14,6 +14,37 @@ function getCurrentTime() {
/** /**
* Creates a ProfilerActor. ProfilerActor provides remote access to the * Creates a ProfilerActor. ProfilerActor provides remote access to the
* built-in profiler module. * built-in profiler module.
*
* ProfilerActor.onGetProfile returns a JavaScript object with data
* generated by our built-in profiler moduele. It has the following
* format:
*
* {
* libs: string,
* meta: {
* interval: number,
* platform: string,
* (...)
* },
* threads: [
* {
* samples: [
* {
* frames: [
* {
* line: number,
* location: string
* }
* ],
* name: string
* responsiveness: number (in ms)
* time: number (nspr time)
* }
* ]
* }
* ]
* }
*
*/ */
function ProfilerActor(aConnection) function ProfilerActor(aConnection)
{ {