diff --git a/browser/devtools/commandline/Commands.jsm b/browser/devtools/commandline/Commands.jsm index bf1759c2d5ce..b560b707b962 100644 --- a/browser/devtools/commandline/Commands.jsm +++ b/browser/devtools/commandline/Commands.jsm @@ -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"); diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm index 1de3dbaf37a1..ff1838fd8755 100644 --- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -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; diff --git a/browser/devtools/main.js b/browser/devtools/main.js index 5475a5e490c5..1ac06ed0f4e6 100644 --- a/browser/devtools/main.js +++ b/browser/devtools/main.js @@ -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 diff --git a/browser/devtools/profiler/Makefile.in b/browser/devtools/profiler/Makefile.in index 42f17d7fecd8..c439da0af99c 100644 --- a/browser/devtools/profiler/Makefile.in +++ b/browser/devtools/profiler/Makefile.in @@ -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 diff --git a/browser/devtools/profiler/ProfilerHelpers.jsm b/browser/devtools/profiler/ProfilerHelpers.jsm deleted file mode 100644 index cfd03ba18b67..000000000000 --- a/browser/devtools/profiler/ProfilerHelpers.jsm +++ /dev/null @@ -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); -}); \ No newline at end of file diff --git a/browser/devtools/profiler/ProfilerPanel.jsm b/browser/devtools/profiler/ProfilerPanel.jsm deleted file mode 100644 index 0ff79fbb7194..000000000000 --- a/browser/devtools/profiler/ProfilerPanel.jsm +++ /dev/null @@ -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"); - } -}; diff --git a/browser/devtools/profiler/cleopatra.js b/browser/devtools/profiler/cleopatra.js new file mode 100644 index 000000000000..a9890440478d --- /dev/null +++ b/browser/devtools/profiler/cleopatra.js @@ -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; \ No newline at end of file diff --git a/browser/devtools/profiler/cleopatra/css/devtools.css b/browser/devtools/profiler/cleopatra/css/devtools.css index 8eb0bc119997..0b4ebc6bebfa 100644 --- a/browser/devtools/profiler/cleopatra/css/devtools.css +++ b/browser/devtools/profiler/cleopatra/css/devtools.css @@ -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 */ diff --git a/browser/devtools/profiler/cleopatra/js/devtools.js b/browser/devtools/profiler/cleopatra/js/devtools.js index 7a80d517b245..042d67cc44a0 100644 --- a/browser/devtools/profiler/cleopatra/js/devtools.js +++ b/browser/devtools/profiler/cleopatra/js/devtools.js @@ -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", - [""]); - var stopProfiling = gStrings.getFormatStr("profiler.stopProfiling", - [""]); - - controlPane.className = "controlPane"; - controlPane.innerHTML = - "

" + startProfiling + "

" + - "

" + stopProfiling + "

" + - "

"; - - 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); } /** diff --git a/browser/devtools/profiler/cleopatra/js/strings.js b/browser/devtools/profiler/cleopatra/js/strings.js index 42b368a3c1e6..7c63527fa27e 100644 --- a/browser/devtools/profiler/cleopatra/js/strings.js +++ b/browser/devtools/profiler/cleopatra/js/strings.js @@ -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. diff --git a/browser/devtools/profiler/cmd-profiler.jsm b/browser/devtools/profiler/commands.js similarity index 64% rename from browser/devtools/profiler/cmd-profiler.jsm rename to browser/devtools/profiler/commands.js index e146626c502b..66086adb72e4 100644 --- a/browser/devtools/profiler/cmd-profiler.jsm +++ b/browser/devtools/profiler/commands.js @@ -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") diff --git a/browser/devtools/profiler/consts.js b/browser/devtools/profiler/consts.js new file mode 100644 index 000000000000..1324b06cc9e4 --- /dev/null +++ b/browser/devtools/profiler/consts.js @@ -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 +}; \ No newline at end of file diff --git a/browser/devtools/profiler/ProfilerController.jsm b/browser/devtools/profiler/controller.js similarity index 80% rename from browser/devtools/profiler/ProfilerController.jsm rename to browser/devtools/profiler/controller.js index a89a90b6cd5b..8c23948e82c3 100644 --- a/browser/devtools/profiler/ProfilerController.jsm +++ b/browser/devtools/profiler/controller.js @@ -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; +} \ No newline at end of file diff --git a/browser/devtools/profiler/panel.js b/browser/devtools/profiler/panel.js new file mode 100644 index 000000000000..acccf47e1b1d --- /dev/null +++ b/browser/devtools/profiler/panel.js @@ -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; \ No newline at end of file diff --git a/browser/devtools/profiler/profiler.xul b/browser/devtools/profiler/profiler.xul index fbda6d55b5e1..5ee105cb039a 100644 --- a/browser/devtools/profiler/profiler.xul +++ b/browser/devtools/profiler/profiler.xul @@ -19,10 +19,9 @@ - + + + diff --git a/browser/devtools/profiler/sidebar.js b/browser/devtools/profiler/sidebar.js new file mode 100644 index 000000000000..f08fa510627c --- /dev/null +++ b/browser/devtools/profiler/sidebar.js @@ -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; \ No newline at end of file diff --git a/browser/devtools/profiler/test/Makefile.in b/browser/devtools/profiler/test/Makefile.in index 5a2a09ba0767..5181f8c0567e 100644 --- a/browser/devtools/profiler/test/Makefile.in +++ b/browser/devtools/profiler/test/Makefile.in @@ -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) diff --git a/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js b/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js deleted file mode 100644 index 1acafd5c8f58..000000000000 --- a/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js +++ /dev/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,

JavaScript Profiler test

"; - -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; - }); -} diff --git a/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js b/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js index 3fdc40e9577b..bd84b2751d1c 100644 --- a/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js +++ b/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js @@ -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(); }); } diff --git a/browser/devtools/profiler/test/browser_profiler_cmd.js b/browser/devtools/profiler/test/browser_profiler_cmd.js index 321b01b48284..c2b56993047f 100644 --- a/browser/devtools/profiler/test/browser_profiler_cmd.js +++ b/browser/devtools/profiler/test/browser_profiler_cmd.js @@ -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(); }); diff --git a/browser/devtools/profiler/test/browser_profiler_console_api.js b/browser/devtools/profiler/test/browser_profiler_console_api.js index 0b25c0142595..64e98110dbd3 100644 --- a/browser/devtools/profiler/test/browser_profiler_console_api.js +++ b/browser/devtools/profiler/test/browser_profiler_console_api.js @@ -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()"); } \ No newline at end of file diff --git a/browser/devtools/profiler/test/browser_profiler_console_api_content.js b/browser/devtools/profiler/test/browser_profiler_console_api_content.js index 67e0b1084f6b..3c664db341e3 100644 --- a/browser/devtools/profiler/test/browser_profiler_console_api_content.js +++ b/browser/devtools/profiler/test/browser_profiler_console_api_content.js @@ -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() { diff --git a/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js b/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js index 548050e88d28..397052e9bdbb 100644 --- a/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js +++ b/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js @@ -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(); } \ No newline at end of file diff --git a/browser/devtools/profiler/test/browser_profiler_console_api_named.js b/browser/devtools/profiler/test/browser_profiler_console_api_named.js index 460676aa7aa4..893c59f597ee 100644 --- a/browser/devtools/profiler/test/browser_profiler_console_api_named.js +++ b/browser/devtools/profiler/test/browser_profiler_console_api_named.js @@ -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); }); }); diff --git a/browser/devtools/profiler/test/browser_profiler_escape.js b/browser/devtools/profiler/test/browser_profiler_escape.js new file mode 100644 index 000000000000..1a0164dc4d8a --- /dev/null +++ b/browser/devtools/profiler/test/browser_profiler_escape.js @@ -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,

JavaScript Profiler test

"; + +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 = ""; + let expl2 = ""; + + is(win.escapeHTML(expl), + "<script>function f() {}</script></textarea><img/src='about:logo'>"); + + is(win.escapeHTML(expl2), + "<script>function f() {}</script></pre><img/src='about:logo'>"); + + tearDown(gTab, () => { + gTab = null; + gPanel = null; + }); + }); + + setTimeout(() => { + record.click(); + }, 50); + }); + + record.click(); + }); +} \ No newline at end of file diff --git a/browser/devtools/profiler/test/browser_profiler_profiles.js b/browser/devtools/profiler/test/browser_profiler_profiles.js deleted file mode 100644 index 10f7b4bcb265..000000000000 --- a/browser/devtools/profiler/test/browser_profiler_profiles.js +++ /dev/null @@ -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,

JavaScript Profiler test

"; - -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; - }); -} \ No newline at end of file diff --git a/browser/devtools/profiler/test/browser_profiler_remote.js b/browser/devtools/profiler/test/browser_profiler_remote.js index 450f9d2902e5..31d819baf17f 100644 --- a/browser/devtools/profiler/test/browser_profiler_remote.js +++ b/browser/devtools/profiler/test/browser_profiler_remote.js @@ -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() { diff --git a/browser/devtools/profiler/test/browser_profiler_run.js b/browser/devtools/profiler/test/browser_profiler_run.js index 80e162ffbdaf..14524cf5cdb8 100644 --- a/browser/devtools/profiler/test/browser_profiler_run.js +++ b/browser/devtools/profiler/test/browser_profiler_run.js @@ -3,7 +3,7 @@ const URL = "data:text/html;charset=utf8,

JavaScript Profiler test

"; -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; +} \ No newline at end of file diff --git a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties index a912170ef1b2..5b1a9ce47e09 100644 --- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties +++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties @@ -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. diff --git a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd index ce221eb5ec59..c78244ac16cd 100644 --- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd @@ -13,3 +13,11 @@ + + + + + + \ No newline at end of file diff --git a/browser/locales/en-US/chrome/browser/devtools/profiler.properties b/browser/locales/en-US/chrome/browser/devtools/profiler.properties index 01de77c655d8..b46bd9d0417b 100644 --- a/browser/locales/en-US/chrome/browser/devtools/profiler.properties +++ b/browser/locales/en-US/chrome/browser/devtools/profiler.properties @@ -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. \ No newline at end of file diff --git a/browser/themes/linux/devtools/profiler-stopwatch.png b/browser/themes/linux/devtools/profiler-stopwatch.png new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn index dc5a1af29b1d..f78afec50822 100644 --- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -200,6 +200,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) diff --git a/browser/themes/osx/devtools/profiler-stopwatch.png b/browser/themes/osx/devtools/profiler-stopwatch.png new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn index 2313b18c4dd0..ca691951d900 100644 --- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -286,6 +286,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) diff --git a/browser/themes/shared/devtools/profiler.inc.css b/browser/themes/shared/devtools/profiler.inc.css index a8849cfac286..d66cf1c7d6b3 100644 --- a/browser/themes/shared/devtools/profiler.inc.css +++ b/browser/themes/shared/devtools/profiler.inc.css @@ -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); } \ No newline at end of file diff --git a/browser/themes/windows/devtools/profiler-stopwatch.png b/browser/themes/windows/devtools/profiler-stopwatch.png new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn index 1439a323b12c..929dcf63ebbf 100644 --- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -226,6 +226,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) @@ -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-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) diff --git a/toolkit/devtools/server/actors/profiler.js b/toolkit/devtools/server/actors/profiler.js index bc87f4859968..f7b2be9bbb29 100644 --- a/toolkit/devtools/server/actors/profiler.js +++ b/toolkit/devtools/server/actors/profiler.js @@ -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) {