Merge mozilla-central and fx-team

This commit is contained in:
Ed Morley 2013-06-28 14:37:15 +01:00
commit a8a59fd05f
48 changed files with 1390 additions and 1239 deletions

View File

@ -263,6 +263,9 @@ let SessionStoreInternal = {
// set default load state
_loadState: STATE_STOPPED,
// initial state to restore after startup
_initialState: null,
// During the initial restore and setBrowserState calls tracks the number of
// windows yet to be restored
_restoreCount: -1,
@ -753,7 +756,7 @@ let SessionStoreInternal = {
// actually wanted to restore so that we can do it later in case
// the user opens another, non-private window.
this._deferredInitialState = this._initialState;
delete this._initialState;
this._initialState = null;
// Nothing to restore now, notify observers things are complete.
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
@ -764,11 +767,12 @@ let SessionStoreInternal = {
this._restoreCount = this._initialState.windows ? this._initialState.windows.length : 0;
this.restoreWindow(aWindow, this._initialState,
this._isCmdLineEmpty(aWindow, this._initialState));
delete this._initialState;
// _loadState changed from "stopped" to "running"
// force a save operation so that crashes happening during startup are correctly counted
this.saveState(true);
this._initialState.session.state = STATE_RUNNING_STR;
this._saveStateObject(this._initialState);
this._initialState = null;
}
}
else {

View File

@ -2,10 +2,10 @@
* 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/. */
this.EXPORTED_SYMBOLS = [ ];
this.EXPORTED_SYMBOLS = [];
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/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/CmdTilt.jsm");
Cu.import("resource:///modules/devtools/CmdScratchpad.jsm");
Cu.import("resource:///modules/devtools/cmd-profiler.jsm");
require("devtools/profiler/commands.js");

View File

@ -93,6 +93,9 @@ DebuggerPanel.prototype = {
highlightWhenPaused: function() {
this._toolbox.highlightTool("jsdebugger");
// Also raise the toolbox window if it is undocked or select the
// corresponding tab when toolbox is docked.
this._toolbox.raise();
},
unhighlightWhenResumed: function() {

View File

@ -95,6 +95,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_dbg_bug737803_editor_actual_location.js \
browser_dbg_bug786070_hide_nonenums.js \
browser_dbg_bug868163_highight_on_pause.js \
browser_dbg_bug883220_raise_on_pause.js \
browser_dbg_displayName.js \
browser_dbg_pause-exceptions.js \
browser_dbg_multiple-windows.js \

View File

@ -0,0 +1,134 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// Tests that debugger's tab is highlighted when it is paused and not the
// currently selected tool.
var gTab = null;
var gTab2 = null;
var gDebugger = null;
var gToolbox = null;
var gToolboxTab = null;
var gFocusedWindow = null;
Promise._reportErrors = true;
function test() {
debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
gTab = aTab;
gDebugger = aPane.panelWin;
gToolbox = aPane._toolbox;
gToolboxTab = gToolbox.doc.getElementById("toolbox-tab-jsdebugger");
gBrowser.selectedTab = gTab2 = gBrowser.addTab();
executeSoon(function() {
is(gBrowser.selectedTab, gTab2, "Debugger's tab is not the selected tab.");
gFocusedWindow = window;
testPause();
});
});
}
function focusMainWindow() {
// Make sure toolbox is not focused.
window.addEventListener("focus", onFocus, true);
// execute soon to avoid any race conditions between toolbox and main window
// getting focused.
executeSoon(() => {
window.focus();
});
}
function onFocus() {
window.removeEventListener("focus", onFocus, true);
info("main window focused.")
gFocusedWindow = window;
testPause();
}
function testPause() {
is(gDebugger.DebuggerController.activeThread.paused, false,
"Should be running after debug_tab_pane.");
is(gFocusedWindow, window, "Main window is the top level window before pause");
if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
gToolbox._host._window.onfocus = () => {
gFocusedWindow = gToolbox._host._window;
};
}
gDebugger.DebuggerController.activeThread.addOneTimeListener("paused", function() {
Services.tm.currentThread.dispatch({ run: function() {
if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
is(gFocusedWindow, gToolbox._host._window,
"Toolbox window is the top level window on pause.");
}
else {
is(gBrowser.selectedTab, gTab, "Debugger's tab got selected.");
}
gToolbox.selectTool("webconsole").then(() => {
ok(gToolboxTab.classList.contains("highlighted"),
"The highlighted class is present");
ok(!gToolboxTab.hasAttribute("selected") ||
gToolboxTab.getAttribute("selected") != "true",
"The tab is not selected");
}).then(() => gToolbox.selectTool("jsdebugger")).then(() => {
ok(gToolboxTab.classList.contains("highlighted"),
"The highlighted class is present");
ok(gToolboxTab.hasAttribute("selected") &&
gToolboxTab.getAttribute("selected") == "true",
"and the tab is selected, so the orange glow will not be present.");
}).then(testResume);
}}, 0);
});
EventUtils.sendMouseEvent({ type: "mousedown" },
gDebugger.document.getElementById("resume"),
gDebugger);
}
function testResume() {
gDebugger.DebuggerController.activeThread.addOneTimeListener("resumed", function() {
Services.tm.currentThread.dispatch({ run: function() {
gToolbox.selectTool("webconsole").then(() => {
ok(!gToolboxTab.classList.contains("highlighted"),
"The highlighted class is not present now after the resume");
ok(!gToolboxTab.hasAttribute("selected") ||
gToolboxTab.getAttribute("selected") != "true",
"The tab is not selected");
}).then(maybeEndTest);
}}, 0);
});
EventUtils.sendMouseEvent({ type: "mousedown" },
gDebugger.document.getElementById("resume"),
gDebugger);
}
function maybeEndTest() {
if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
gToolbox.switchHost(devtools.Toolbox.HostType.BOTTOM)
.then(closeDebuggerAndFinish);
}
else {
info("switching to toolbox window.")
gToolbox.switchHost(devtools.Toolbox.HostType.WINDOW).then(focusMainWindow).then(null, console.error);
}
}
registerCleanupFunction(function() {
Services.prefs.setCharPref("devtools.toolbox.host", devtools.Toolbox.HostType.BOTTOM);
removeTab(gTab);
removeTab(gTab2);
gTab = null;
gTab2 = null;
gDebugger = null;
gToolbox = null;
gToolboxTab = null;
gFocusedWindow = null;
});

View File

@ -13,7 +13,8 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
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 MAX_ORDINAL = 99;

View File

@ -28,6 +28,7 @@ MOCHITEST_BROWSER_FILES = \
browser_toolbox_options_disablejs.html \
browser_toolbox_options_disablejs_iframe.html \
browser_toolbox_highlight.js \
browser_toolbox_raise.js \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,87 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/Services.jsm");
let temp = {}
Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
let DevTools = temp.DevTools;
Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
let devtools = temp.devtools;
let Toolbox = devtools.Toolbox;
let toolbox, target, tab1, tab2;
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = tab1 = gBrowser.addTab();
tab2 = gBrowser.addTab();
target = TargetFactory.forTab(gBrowser.selectedTab);
gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
gDevTools.showToolbox(target)
.then(testBottomHost, console.error)
.then(null, console.error);
}, true);
content.location = "data:text/html,test for opening toolbox in different hosts";
}
function testBottomHost(aToolbox) {
toolbox = aToolbox;
// switch to another tab and test toolbox.raise()
gBrowser.selectedTab = tab2;
executeSoon(function() {
is(gBrowser.selectedTab, tab2, "Correct tab is selected before calling raise");
toolbox.raise();
executeSoon(function() {
is(gBrowser.selectedTab, tab1, "Correct tab was selected after calling raise");
toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost).then(null, console.error);
});
});
}
function testWindowHost() {
// Make sure toolbox is not focused.
window.addEventListener("focus", onFocus, true);
// Need to wait for focus as otherwise window.focus() is overridden by
// toolbox window getting focused first on Linux and Mac.
let onToolboxFocus = () => {
toolbox._host._window.removeEventListener("focus", onToolboxFocus, true);
info("focusing main window.");
window.focus()
};
// Need to wait for toolbox window to get focus.
toolbox._host._window.addEventListener("focus", onToolboxFocus, true);
}
function onFocus() {
info("Main window is focused before calling toolbox.raise()")
window.removeEventListener("focus", onFocus, true);
// Check if toolbox window got focus.
toolbox._host._window.onfocus = () => {
ok(true, "Toolbox window is the focused window after calling toolbox.raise()");
cleanup();
};
// Now raise toolbox.
toolbox.raise();
}
function cleanup() {
Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
toolbox.destroy().then(function() {
DevTools = Toolbox = toolbox = target = null;
gBrowser.removeCurrentTab();
gBrowser.removeCurrentTab();
finish();
});
}

View File

@ -25,7 +25,7 @@ loader.lazyGetter(this, "InspectorPanel", function() require("devtools/inspector
loader.lazyImporter(this, "WebConsolePanel", "resource:///modules/WebConsolePanel.jsm");
loader.lazyImporter(this, "DebuggerPanel", "resource:///modules/devtools/DebuggerPanel.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");
// Strings

View File

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

View File

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

View File

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

View File

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

View File

@ -2,19 +2,7 @@
* 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/. */
#mainarea > .controlPane {
font-size: 120%;
padding-top: 75px;
text-align: center;
}
#stopWrapper {
display: none;
}
#profilerMessage {
color: #999;
display: none;
#mainarea {
}
/* De-emphasize chrome functions */

View File

@ -11,10 +11,6 @@ var gInstanceUID;
* @param string status
* Status to send to the parent page:
* - 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
* @param object data (optional)
* Additional data to send to the parent page.
@ -109,22 +105,10 @@ function initUI() {
notifyParent("stop");
}, false);
var controlPane = document.createElement("div");
var startProfiling = gStrings.getFormatStr("profiler.startProfiling",
["<span class='btn'></span>"]);
var stopProfiling = gStrings.getFormatStr("profiler.stopProfiling",
["<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);
var message = document.createElement("div");
message.className = "message";
message.innerHTML = "To start profiling click the button above.";
gMainArea.appendChild(message);
}
/**

View File

@ -1,5 +1,9 @@
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.

View File

@ -2,20 +2,15 @@
* 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/. */
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
this.EXPORTED_SYMBOLS = [];
const { Cu } = require("chrome");
module.exports = [];
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",
"resource:///modules/devtools/gDevTools.jsm");
loader.lazyGetter(this, "gDevTools",
() => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
var Promise = require('util/promise');
var Promise = require("sdk/core/promise");
/*
* 'profiler' command. Doesn't do anything.
@ -64,39 +59,17 @@ gcli.addCommand({
name: "profiler start",
description: gcli.lookup("profilerStartDesc"),
returnType: "string",
params: [
{
name: "name",
type: "string",
manual: gcli.lookup("profilerStartManual")
}
],
params: [],
exec: function (args, context) {
function start() {
let name = args.name;
let panel = getPanel(context, "jsprofiler");
let profile = panel.getProfileByName(name) || panel.createProfile(name);
if (profile.isStarted) {
throw gcli.lookup("profilerAlreadyStarted");
}
if (panel.recordingProfile)
throw gcli.lookup("profilerAlreadyStarted2");
if (profile.isFinished) {
throw gcli.lookup("profilerAlreadyFinished");
}
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");
panel.toggleRecording();
return gcli.lookup("profilerStarted");
}
return gDevTools.showToolbox(context.environment.target, "jsprofiler")
@ -111,42 +84,16 @@ gcli.addCommand({
name: "profiler stop",
description: gcli.lookup("profilerStopDesc"),
returnType: "string",
params: [
{
name: "name",
type: "string",
manual: gcli.lookup("profilerStopManual")
}
],
params: [],
exec: function (args, context) {
function stop() {
let panel = getPanel(context, "jsprofiler");
let profile = panel.getProfileByName(args.name);
if (!profile) {
throw gcli.lookup("profilerNotFound");
}
if (!panel.recordingProfile)
throw gcli.lookup("profilerNotStarted3");
if (profile.isFinished) {
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");
panel.toggleRecording();
}
return gDevTools.showToolbox(context.environment.target, "jsprofiler")

View File

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

View File

@ -4,22 +4,36 @@
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
var isJSM = typeof require !== "function";
// 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/Console.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",
"resource:///modules/devtools/gDevTools.jsm");
loader.lazyGetter(this, "gDevTools",
() => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
"resource://gre/modules/devtools/dbg-server.jsm");
loader.lazyGetter(this, "DebuggerServer",
() => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer);
/**
* Data structure that contains information that has
@ -44,7 +58,8 @@ function makeProfile(name, def={}) {
return {
name: name,
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.isConnected = false;
this.consoleProfiles = [];
this.reservedNames = {};
addTarget(target);
@ -86,9 +102,16 @@ function ProfilerController(target) {
}
sharedData.controllers.set(target, this);
EventEmitter.decorate(this);
};
ProfilerController.prototype = {
target: null,
client: null,
isConnected: null,
consoleProfiles: null,
reservedNames: null,
/**
* Return a map of profile results for the current target.
*
@ -109,6 +132,19 @@ ProfilerController.prototype = {
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
* is called.
@ -117,26 +153,23 @@ ProfilerController.prototype = {
* Type of a call. Either 'profile' or 'profileEnd'.
* @param object 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 profileStart = () => {
if (name && this.profiles.has(name))
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.
this.profiles.set(profile.name, makeProfile(profile.name, {
timeStarted: data.extra.currentTime
}));
let profile = makeProfile(name || this.getProfileName(), {
timeStarted: data.extra.currentTime,
fromConsole: true
});
this.profiles.set(profile.name, profile);
this.consoleProfiles.push(profile.name);
this.emit("profileStart", profile);
};
let profileEnd = () => {
@ -156,8 +189,6 @@ ProfilerController.prototype = {
return;
let profileData = data.extra.profile;
profile.timeEnded = data.extra.currentTime;
profileData.threads = profileData.threads.map((thread) => {
let samples = thread.samples.filter((sample) => {
return sample.time >= profile.timeStarted;
@ -166,10 +197,10 @@ ProfilerController.prototype = {
return { samples: samples };
});
let ui = panel.getProfileByName(name);
ui.data = profileData;
ui.parse(profileData, () => panel.emit("parsed"));
ui.stop((name, cb) => cb());
profile.timeEnded = data.extra.currentTime;
profile.data = profileData;
this.emit("profileEnd", profile);
};
if (type === "profile")
@ -217,27 +248,7 @@ ProfilerController.prototype = {
if (toolbox == null)
return;
let panel = toolbox.getPanel("jsprofiler");
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");
this.onConsoleEvent(resp.subject.action, resp.data);
});
});
@ -392,3 +403,9 @@ ProfilerController.prototype = {
this.actor = null;
}
};
if (isJSM) {
var EXPORTED_SYMBOLS = ["ProfilerController"];
} else {
module.exports = ProfilerController;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,30 +9,26 @@ function test() {
waitForExplicitFinish();
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() {
let target = TargetFactory.forTab(tab);
let dbg = gDevTools.getToolbox(target).getPanel("jsdebugger");
let view = dbg.panelWin.DebuggerView;
panel.displaySource(data, function onOpen() {
let target = TargetFactory.forTab(tab);
let dbg = gDevTools.getToolbox(target).getPanel("jsdebugger");
let view = dbg.panelWin.DebuggerView;
is(view.Sources.selectedValue, data.uri, "URI is different");
is(view.editor.getCaretPosition().line, data.line - 1,
"Line is different");
is(view.Sources.selectedValue, data.uri, "URI is different");
is(view.editor.getCaretPosition().line, data.line - 1,
"Line is different");
// Test the case where script is already loaded.
view.editor.setCaretPosition(1);
gDevTools.showToolbox(target, "jsprofiler").then(function () {
panel.displaySource(data, function onOpenAgain() {
is(view.editor.getCaretPosition().line, data.line - 1,
"Line is different");
tearDown(tab);
});
// Test the case where script is already loaded.
view.editor.setCaretPosition(1);
gDevTools.showToolbox(target, "jsprofiler").then(function () {
panel.displaySource(data, function onOpenAgain() {
is(view.editor.getCaretPosition().line, data.line - 1,
"Line is different");
tearDown(tab);
});
});
});
panel.createProfile();
});
}

View File

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

View File

@ -19,46 +19,17 @@ function test() {
function testConsoleProfile(hud) {
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;
function profileEnd(_, uid) {
let profile = gPanel.profiles.get(uid);
gPanel.once("parsed", () => {
let profile = gPanel.activeProfile;
profile.once("started", () => {
if (++profilesStarted < 2)
return;
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);
is(profile.name, "Profile 1", "Profile name is OK");
is(gPanel.sidebar.selectedItem, gPanel.sidebar.getItemByProfile(profile), "Sidebar is OK");
is(gPanel.sidebar.selectedItem.attachment.state, PROFILE_COMPLETED);
tearDown(gTab, () => gTab = gPanel = null);
});
sendFromProfile(2, "stop");
hud.jsterm.execute("console.profile()");
hud.jsterm.execute("console.profileEnd()");
}

View File

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

View File

@ -18,12 +18,13 @@ function test() {
function runTests(toolbox) {
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);
openConsole(gTab, (hud) => {
panel.profiles.get(1).once("stopped", () => {
panel.once("stopped", () => {
is(getSidebarItem(1, panel).attachment.state, PROFILE_COMPLETED);
tearDown(gTab, () => gTab = gPanel = null);
});
@ -32,5 +33,5 @@ function runTests(toolbox) {
});
});
sendFromProfile(1, "start");
record.click();
}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ Cu.import("resource://gre/modules/devtools/dbg-client.jsm", temp);
let DebuggerClient = temp.DebuggerClient;
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;
function test() {

View File

@ -3,7 +3,7 @@
const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
let gTab, gPanel, gAttempts = 0;
let gTab, gPanel;
function test() {
waitForExplicitFinish();
@ -12,55 +12,104 @@ function test() {
gTab = tab;
gPanel = panel;
panel.once("started", onStart);
panel.once("parsed", onParsed);
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);
function done() {
tearDown(gTab, () => { gPanel = null; gTab = null; });
}
ok(sample.length > 0, "We have some items displayed");
is(sample[0].innerHTML, "100.0%", "First percentage is 100%");
tearDown(gTab, function onTearDown() {
gPanel = null;
gTab = null;
});
}
assertSample();
startRecording()
.then(stopRecording)
.then(startRecordingAgain)
.then(stopRecording)
.then(switchBackToTheFirstOne)
.then(done);
});
}
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;
}

View File

@ -163,4 +163,4 @@ var ShutdownObserver = {
{
Services.obs.removeObserver(this, "quit-application-granted");
}
};
};

View File

@ -1399,6 +1399,7 @@ var Scratchpad = {
telemetry.toolClosed("scratchpad");
window.close();
}
if (aCallback) {
aCallback();
}
@ -1467,6 +1468,9 @@ var Scratchpad = {
}
},
/**
* Opens the MDN documentation page for Scratchpad.
*/
openDocumentationPage: function SP_openDocumentationPage()
{
let url = this.strings.GetStringFromName("help.openDocumentationPage");
@ -1476,7 +1480,6 @@ var Scratchpad = {
},
};
/**
* Encapsulates management of the sidebar containing the VariablesView for
* object inspection.

View File

@ -162,7 +162,6 @@
command="sp-cmd-close"/>
</menupopup>
</menu>
<menu id="sp-edit-menu" label="&editMenu.label;"
accesskey="&editMenu.accesskey;">
<menupopup id="sp-menu_editpopup"
@ -182,7 +181,6 @@
<menuitem id="se-menu-gotoLine"/>
</menupopup>
</menu>
<menu id="sp-execute-menu" label="&executeMenu.label;"
accesskey="&executeMenu.accesskey;">
<menupopup id="sp-menu_executepopup">
@ -252,6 +250,36 @@
</menu>
</menubar>
<toolbar id="sp-toolbar"
class="devtools-toolbar">
<toolbarbutton id="sp-toolbar-open"
class="devtools-toolbarbutton"
label="&openFileCmd.label;"
command="sp-cmd-openFile"/>
<toolbarbutton id="sp-toolbar-save"
class="devtools-toolbarbutton"
label="&saveFileCmd.label;"
command="sp-cmd-save"/>
<toolbarbutton id="sp-toolbar-saveAs"
class="devtools-toolbarbutton"
label="&saveFileAsCmd.label;"
command="sp-cmd-saveas"/>
<toolbarspacer/>
<toolbarbutton id="sp-toolbar-run"
class="devtools-toolbarbutton"
label="&run.label;"
command="sp-cmd-run"/>
<toolbarbutton id="sp-toolbar-inspect"
class="devtools-toolbarbutton"
label="&inspect.label;"
command="sp-cmd-inspect"/>
<toolbarbutton id="sp-toolbar-display"
class="devtools-toolbarbutton"
label="&display.label;"
command="sp-cmd-display"/>
</toolbar>
<popupset id="scratchpad-popups">
<menupopup id="scratchpad-text-popup"
onpopupshowing="goUpdateSourceEditorMenuItems()">

View File

@ -1238,13 +1238,7 @@ profilerShowManual=Name of a profile.
# LOCALIZATION NOTE (profilerAlreadyStarted) A message that is displayed whenever
# an operation cannot be completed because the profile in question has already
# been started.
profilerAlreadyStarted=This 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
profilerAlreadyStarted2=Profile has already been started
# LOCALIZATION NOTE (profilerNotFound) A message that is displayed whenever
# 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
# started yet. It also contains a hint to use the 'profile start' command to
# 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
# we're starting the profiler.
profilerStarting2=Starting…
# LOCALIZATION NOTE (profilerStopping) A very short string that indicates that
# we're stopping the profiler.
profilerStopping2=Stopping…
# LOCALIZATION NOTE (profilerStarted) A very short string that indicates that
# we have started recording.
profilerStarted=Recording...
# LOCALIZATION NOTE (profilerNotReady) A message that is displayed whenever
# an operation cannot be completed because the profiler has not been opened yet.

View File

@ -13,3 +13,11 @@
<!-- LOCALIZATION NOTE (profilerNew.label): This is the label for the
- button that creates a new profile. -->
<!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">

View File

@ -97,3 +97,9 @@ profiler.stateRunning=Running
# This string is used to show that the profile in question is in COMPLETED
# state meaning that it has been started and stopped already.
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -201,6 +201,7 @@ browser.jar:
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/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/tool-options.png (devtools/tool-options.png)
skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -287,6 +287,7 @@ browser.jar:
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/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/tool-options.png (devtools/tool-options.png)
skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png)

View File

@ -4,6 +4,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%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 {
min-width: 196px;
}
@ -30,14 +42,33 @@
color: rgb(128, 195, 228);
}
.devtools-toolbar {
height: 26px;
padding: 3px;
#profiler-controls > toolbarbutton {
margin: 0;
box-shadow: none;
border-radius: 0;
border-width: 0;
-moz-border-end-width: 1px;
outline-offset: -3px;
}
.devtools-toolbar .devtools-toolbarbutton {
min-width: 48px;
min-height: 0;
font-size: 11px;
padding: 0px 8px;
#profiler-controls > toolbarbutton:last-of-type {
-moz-border-end-width: 0;
}
#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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -227,6 +227,7 @@ browser.jar:
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/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/tool-options.png (devtools/tool-options.png)
skin/classic/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png)
@ -481,6 +482,7 @@ browser.jar:
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/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/tool-options.png (devtools/tool-options.png)
skin/classic/aero/browser/devtools/tool-webconsole.png (devtools/tool-webconsole.png)

View File

@ -14,6 +14,37 @@ function getCurrentTime() {
/**
* Creates a ProfilerActor. ProfilerActor provides remote access to the
* 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)
{