diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm index 9138d6462508..90bf78760701 100644 --- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -13,6 +13,7 @@ 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"); const FORBIDDEN_IDS = new Set(["toolbox", ""]); const MAX_ORDINAL = 99; @@ -621,6 +622,25 @@ let gDevToolsBrowser = { } }, + /** + * Connects to the SPS profiler when the developer tools are open. + */ + _connectToProfiler: function DT_connectToProfiler() { + for (let win of gDevToolsBrowser._trackedBrowserWindows) { + if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) { + let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab); + if (gDevTools._toolboxes.has(target)) { + target.makeRemote().then(() => { + let profiler = new ProfilerController(target); + profiler.connect(); + }).then(null, Cu.reportError); + + return; + } + } + } + }, + /** * Remove the menuitem for a tool to all open browser windows. * @@ -694,6 +714,7 @@ let gDevToolsBrowser = { * All browser windows have been closed, tidy up remaining objects. */ destroy: function() { + gDevTools.off("toolbox-ready", gDevToolsBrowser._connectToProfiler); Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application"); }, } @@ -712,6 +733,7 @@ gDevTools.on("tool-unregistered", function(ev, toolId) { }); gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox); +gDevTools.on("toolbox-ready", gDevToolsBrowser._connectToProfiler); gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox); Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false); diff --git a/browser/devtools/framework/target.js b/browser/devtools/framework/target.js index 1a6b7a8e95f5..d76ec7f98022 100644 --- a/browser/devtools/framework/target.js +++ b/browser/devtools/framework/target.js @@ -207,6 +207,10 @@ TabTarget.prototype = { return this._form; }, + get root() { + return this._root; + }, + get client() { return this._client; }, @@ -287,6 +291,7 @@ TabTarget.prototype = { if (this.isLocalTab) { this._client.connect((aType, aTraits) => { this._client.listTabs(aResponse => { + this._root = aResponse; this._form = aResponse.tabs[aResponse.selected]; attachTab(); }); diff --git a/browser/devtools/framework/toolbox.js b/browser/devtools/framework/toolbox.js index 67b969f28004..be09c8dad369 100644 --- a/browser/devtools/framework/toolbox.js +++ b/browser/devtools/framework/toolbox.js @@ -409,6 +409,49 @@ Toolbox.prototype = { this._addKeysToWindow(); }, + /** + * Load a tool with a given id. + * + * @param {string} id + * The id of the tool to load. + */ + loadTool: function TBOX_loadTool(id) { + let deferred = Promise.defer(); + let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); + + if (iframe) { + this.once(id + "-ready", () => { dump("\n\n\nREADY MOTHAFUCKAS\n\n\n"); deferred.resolve() }); + return deferred.promise; + } + + let definition = gDevTools.getToolDefinitionMap().get(id); + iframe = this.doc.createElement("iframe"); + iframe.className = "toolbox-panel-iframe"; + iframe.id = "toolbox-panel-iframe-" + id; + iframe.setAttribute("flex", 1); + iframe.setAttribute("forceOwnRefreshDriver", ""); + iframe.tooltip = "aHTMLTooltip"; + + let vbox = this.doc.getElementById("toolbox-panel-" + id); + vbox.appendChild(iframe); + + let onLoad = () => { + iframe.removeEventListener("DOMContentLoaded", onLoad, true); + + let built = definition.build(iframe.contentWindow, this); + Promise.resolve(built).then((panel) => { + this._toolPanels.set(id, panel); + this.emit(id + "-ready", panel); + gDevTools.emit(id + "-ready", this, panel); + deferred.resolve(panel); + }); + }; + + iframe.addEventListener("DOMContentLoaded", onLoad, true); + iframe.setAttribute("src", definition.url); + return deferred.promise; + }, + /** * Switch to the tool with the given id * @@ -464,8 +507,6 @@ Toolbox.prototype = { let deck = this.doc.getElementById("toolbox-deck"); deck.selectedIndex = index; - let definition = gDevTools.getToolDefinitionMap().get(id); - this._currentToolId = id; let resolveSelected = panel => { @@ -476,32 +517,11 @@ Toolbox.prototype = { let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); if (!iframe) { - iframe = this.doc.createElement("iframe"); - iframe.className = "toolbox-panel-iframe"; - iframe.id = "toolbox-panel-iframe-" + id; - iframe.setAttribute("flex", 1); - iframe.setAttribute("forceOwnRefreshDriver", ""); - iframe.tooltip = "aHTMLTooltip"; - - let vbox = this.doc.getElementById("toolbox-panel-" + id); - vbox.appendChild(iframe); - - let boundLoad = function() { - iframe.removeEventListener("DOMContentLoaded", boundLoad, true); - - let built = definition.build(iframe.contentWindow, this); - Promise.resolve(built).then(function(panel) { - this._toolPanels.set(id, panel); - - this.emit(id + "-ready", panel); - gDevTools.emit(id + "-ready", this, panel); - - resolveSelected(panel); - }.bind(this)); - }.bind(this); - - iframe.addEventListener("DOMContentLoaded", boundLoad, true); - iframe.setAttribute("src", definition.url); + this.loadTool(id).then((panel) => { + this.emit("select", id); + this.emit(id + "-selected", panel); + deferred.resolve(panel); + }); } else { let panel = this._toolPanels.get(id); // only emit 'select' event if the iframe has been loaded diff --git a/browser/devtools/profiler/ProfilerController.jsm b/browser/devtools/profiler/ProfilerController.jsm index 89dfe24ea91f..261a914eed95 100644 --- a/browser/devtools/profiler/ProfilerController.jsm +++ b/browser/devtools/profiler/ProfilerController.jsm @@ -10,13 +10,16 @@ const Cu = Components.utils; 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"); let EXPORTED_SYMBOLS = ["ProfilerController"]; -XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function () { - Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); - return DebuggerServer; -}); +XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", + "resource:///modules/devtools/gDevTools.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", + "resource://gre/modules/devtools/dbg-server.jsm"); /** * Data structure that contains information that has @@ -24,18 +27,24 @@ XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function () { * instances. */ const sharedData = { - startTime: 0, data: new WeakMap(), + controllers: new WeakMap(), }; /** * Makes a structure representing an individual profile. */ -function makeProfile(name) { +function makeProfile(name, def={}) { + if (def.timeStarted == null) + def.timeStarted = null; + + if (def.timeEnded == null) + def.timeEnded = null; + return { name: name, - timeStarted: null, - timeEnded: null + timeStarted: def.timeStarted, + timeEnded: def.timeEnded }; } @@ -50,10 +59,6 @@ function getProfiles(target) { return sharedData.data.get(target); } -function getCurrentTime() { - return (new Date()).getTime() - sharedData.startTime; -} - /** * Object to control the JavaScript Profiler over the remote * debugging protocol. @@ -62,9 +67,14 @@ function getCurrentTime() { * A target object as defined in Target.jsm */ function ProfilerController(target) { + if (sharedData.controllers.has(target)) { + return sharedData.controllers.get(target); + } + this.target = target; this.client = target.client; this.isConnected = false; + this.consoleProfiles = []; addTarget(target); @@ -74,6 +84,8 @@ function ProfilerController(target) { this.isConnected = true; this.actor = target.form.profilerActor; } + + sharedData.controllers.set(target, this); }; ProfilerController.prototype = { @@ -97,6 +109,76 @@ ProfilerController.prototype = { return profile.timeStarted !== null && profile.timeEnded === null; }, + /** + * A listener that fires whenever console.profile or console.profileEnd + * is called. + * + * @param string type + * 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) { + 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 + })); + this.consoleProfiles.push(profile.name); + }; + + let profileEnd = () => { + if (!name && !this.consoleProfiles.length) + return; + + if (!name) + name = this.consoleProfiles.pop(); + else + this.consoleProfiles.filter((n) => n !== name); + + if (!this.profiles.has(name)) + return; + + let profile = this.profiles.get(name); + if (!this.isProfileRecording(profile)) + 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; + }); + + return { samples: samples }; + }); + + let ui = panel.getProfileByName(name); + ui.data = profileData; + ui.parse(profileData, () => panel.emit("parsed")); + ui.stop((name, cb) => cb()); + }; + + if (type === "profile") + profileStart(); + + if (type === "profileEnd") + profileEnd(); + }, + /** * Connects to the client unless we're already connected. * @@ -105,16 +187,76 @@ ProfilerController.prototype = { * the controller is already connected, this function * will be called immediately (synchronously). */ - connect: function (cb) { + connect: function (cb=function(){}) { if (this.isConnected) { return void cb(); } + // Check if we already have a grip to the listTabs response object + // and, if we do, use it to get to the profilerActor. Otherwise, + // call listTabs. The problem is that if we call listTabs twice + // webconsole tests fail (see bug 872826). + + let register = () => { + let data = { events: ["console-api-profiler"] }; + + // Check if Gecko Profiler Addon [1] is installed and, if it is, + // don't register our own console event listeners. Gecko Profiler + // Addon takes care of console.profile and console.profileEnd methods + // and we don't want to break it. + // + // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/ + + AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => { + if (addon && !addon.userDisabled && !addon.softDisabled) + return void cb(); + + this.request("registerEventNotifications", data, (resp) => { + this.client.addListener("eventNotification", (type, resp) => { + let toolbox = gDevTools.getToolbox(this.target); + 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) => { + dump("\n\n\nCHOO HOOO MOTHERFUCKERS (" + resp.subject.action + ")\n\n\n"); + this.onConsoleEvent(resp.subject.action, resp.data, panel); + }); + + toolbox.loadTool("jsprofiler"); + }); + }); + + cb(); + }); + }; + + if (this.target.root) { + this.actor = this.target.root.profilerActor; + this.isConnected = true; + return void register(); + } + this.client.listTabs((resp) => { this.actor = resp.profilerActor; this.isConnected = true; - cb(); - }) + register(); + }); }, /** @@ -144,7 +286,9 @@ ProfilerController.prototype = { * value indicating if the profiler is active or not. */ isActive: function (cb) { - this.request("isActive", {}, (resp) => cb(resp.error, resp.isActive)); + this.request("isActive", {}, (resp) => { + cb(resp.error, resp.isActive, resp.currentTime); + }); }, /** @@ -163,6 +307,7 @@ ProfilerController.prototype = { } let profile = makeProfile(name); + this.consoleProfiles.push(name); this.profiles.set(name, profile); // If profile is already running, no need to do anything. @@ -170,9 +315,9 @@ ProfilerController.prototype = { return void cb(); } - this.isActive((err, isActive) => { + this.isActive((err, isActive, currentTime) => { if (isActive) { - profile.timeStarted = getCurrentTime(); + profile.timeStarted = currentTime; return void cb(); } @@ -187,8 +332,7 @@ ProfilerController.prototype = { return void cb(resp.error); } - sharedData.startTime = (new Date()).getTime(); - profile.timeStarted = getCurrentTime(); + profile.timeStarted = 0; cb(); }); }); @@ -223,7 +367,7 @@ ProfilerController.prototype = { } let data = resp.profile; - profile.timeEnded = getCurrentTime(); + profile.timeEnded = resp.currentTime; // Filter out all samples that fall out of current // profile's range. diff --git a/browser/devtools/profiler/ProfilerPanel.jsm b/browser/devtools/profiler/ProfilerPanel.jsm index 3c8478e02d4a..97099cfdfa2b 100644 --- a/browser/devtools/profiler/ProfilerPanel.jsm +++ b/browser/devtools/profiler/ProfilerPanel.jsm @@ -11,6 +11,7 @@ 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://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/devtools/Console.jsm"); this.EXPORTED_SYMBOLS = ["ProfilerPanel"]; @@ -54,6 +55,7 @@ function ProfileUI(uid, name, panel) { this.isStarted = false; this.isFinished = false; + this.messages = []; this.panel = panel; this.uid = uid; this.name = name; @@ -76,14 +78,6 @@ function ProfileUI(uid, name, panel) { switch (event.data.status) { case "loaded": - if (this.panel._runningUid !== null) { - this.iframe.contentWindow.postMessage(JSON.stringify({ - uid: this._runningUid, - isCurrent: this._runningUid === uid, - task: "onStarted" - }), "*"); - } - this.isReady = true; this.emit("ready"); break; @@ -106,6 +100,22 @@ function ProfileUI(uid, name, panel) { } 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"); }, @@ -126,32 +136,27 @@ ProfileUI.prototype = { */ parse: function PUI_parse(data, onParsed) { if (!this.isReady) { - return; + return void this.on("ready", this.parse.bind(this, data, onParsed)); } - let win = this.iframe.contentWindow; + this.message({ task: "receiveProfileData", rawProfile: data }).then(() => { + let poll = () => { + let wait = this.panel.window.setTimeout.bind(null, poll, 100); + let trail = this.contentWindow.gBreadcrumbTrail; - win.postMessage(JSON.stringify({ - task: "receiveProfileData", - rawProfile: data - }), "*"); + if (!trail) { + return wait(); + } - let poll = function pollBreadcrumbs() { - let wait = this.panel.window.setTimeout.bind(null, poll, 100); - let trail = win.gBreadcrumbTrail; + if (!trail._breadcrumbs || !trail._breadcrumbs.length) { + return wait(); + } - if (!trail) { - return wait(); - } + onParsed(); + }; - if (!trail._breadcrumbs || !trail._breadcrumbs.length) { - return wait(); - } - - onParsed(); - }.bind(this); - - poll(); + poll(); + }); }, /** @@ -171,37 +176,90 @@ ProfileUI.prototype = { * 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() { + start: function PUI_start(startFn) { if (this.isStarted || this.isFinished) { return; } - this.panel.startProfiling(this.name, function onStart() { + startFn = startFn || this.panel.startProfiling.bind(this.panel); + startFn(this.name, () => { this.isStarted = true; this.updateLabel(this.name + " *"); - this.panel.broadcast(this.uid, {task: "onStarted"}); + this.panel.broadcast(this.uid, {task: "onStarted"}); // Do we really need this? this.emit("started"); - }.bind(this)); + }); }, /** * 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() { + stop: function PUI_stop(stopFn) { if (!this.isStarted || this.isFinished) { return; } - this.panel.stopProfiling(this.name, function onStop() { + stopFn = stopFn || this.panel.stopProfiling.bind(this.panel); + stopFn(this.name, () => { this.isStarted = false; this.isFinished = true; this.updateLabel(this.name); this.panel.broadcast(this.uid, {task: "onStopped"}); this.emit("stopped"); - }.bind(this)); + }); + }, + + /** + * 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(); + } }, /** @@ -212,6 +270,7 @@ ProfileUI.prototype = { this.panel = null; this.uid = null; this.iframe = null; + this.messages = null; } }; @@ -249,6 +308,7 @@ function ProfilerPanel(frame, toolbox) { this.profiles = new Map(); this._uid = 0; + this._msgQueue = {}; EventEmitter.decorate(this); } @@ -265,6 +325,7 @@ ProfilerPanel.prototype = { _activeUid: null, _runningUid: null, _browserWin: null, + _msgQueue: null, get activeProfile() { return this.profiles.get(this._activeUid); @@ -297,6 +358,7 @@ ProfilerPanel.prototype = { */ open: function PP_open() { let promise; + // Local profiling needs to make the target remote. if (!this.target.isRemote) { promise = this.target.makeRemote(); @@ -350,7 +412,21 @@ ProfilerPanel.prototype = { return this.getProfileByName(name); } - let uid = ++this._uid; + 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 list = this.document.getElementById("profiles-list"); let item = this.document.createElement("li"); let wrap = this.document.createElement("h1"); @@ -403,15 +479,17 @@ ProfilerPanel.prototype = { this.activeProfile = profile; if (profile.isReady) { + profile.flushMessages(); this.emit("profileSwitched", profile.uid); onLoad(); return; } - profile.once("ready", function () { + profile.once("ready", () => { + profile.flushMessages(); this.emit("profileSwitched", profile.uid); onLoad(); - }.bind(this)); + }); }, /** @@ -422,15 +500,14 @@ ProfilerPanel.prototype = { * that profiling had been successfuly started. */ startProfiling: function PP_startProfiling(name, onStart) { - this.controller.start(name, function (err) { + this.controller.start(name, (err) => { if (err) { - Cu.reportError("ProfilerController.start: " + err.message); - return; + return void Cu.reportError("ProfilerController.start: " + err.message); } onStart(); this.emit("started"); - }.bind(this)); + }); }, /** @@ -503,6 +580,28 @@ ProfilerPanel.prototype = { 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. * @@ -524,18 +623,13 @@ ProfilerPanel.prototype = { this._runningUid = null; } - let uid = this._uid; - while (uid >= 0) { - if (this.profiles.has(uid)) { - let iframe = this.profiles.get(uid).iframe; - iframe.contentWindow.postMessage(JSON.stringify({ - uid: target, - isCurrent: target === uid, - task: data.task - }), "*"); - } - uid -= 1; - } + this.eachProfile((profile) => { + profile.message({ + uid: target, + isCurrent: target === profile.uid, + task: data.task + }); + }); }, /** diff --git a/browser/devtools/profiler/cleopatra/js/ui.js b/browser/devtools/profiler/cleopatra/js/ui.js index b33a0534be5d..d9a1966e6183 100755 --- a/browser/devtools/profiler/cleopatra/js/ui.js +++ b/browser/devtools/profiler/cleopatra/js/ui.js @@ -515,6 +515,7 @@ HistogramView.prototype = { cancelAnimationFrame(self._pendingAnimationFrame); self._pendingAnimationFrame = null; self._render(highlightedCallstack); + self._busyCover.classList.remove("busy"); }); }, _render: function HistogramView__render(highlightedCallstack) { diff --git a/browser/devtools/profiler/test/Makefile.in b/browser/devtools/profiler/test/Makefile.in index 1a81656d4ba8..5a2a09ba0767 100644 --- a/browser/devtools/profiler/test/Makefile.in +++ b/browser/devtools/profiler/test/Makefile.in @@ -19,12 +19,17 @@ MOCHITEST_BROWSER_TESTS = \ 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 \ head.js \ $(NULL) MOCHITEST_BROWSER_PAGES = \ mock_profiler_bug_834878_page.html \ mock_profiler_bug_834878_script.js \ + mock_console_api.html \ $(NULL) MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES 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 index 3fa530e2734f..0c32eb433140 100644 --- a/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js +++ b/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js @@ -33,11 +33,6 @@ function getCleoControls(doc) { ]; } -function sendFromProfile(uid, msg) { - let [win, doc] = getProfileInternals(uid); - win.parent.postMessage({ uid: uid, status: msg }, "*"); -} - function startProfiling() { gPanel.profiles.get(gPanel.activeProfile.uid).once("started", function () { setTimeout(function () { diff --git a/browser/devtools/profiler/test/browser_profiler_console_api.js b/browser/devtools/profiler/test/browser_profiler_console_api.js new file mode 100644 index 000000000000..f98cf13e7618 --- /dev/null +++ b/browser/devtools/profiler/test/browser_profiler_console_api.js @@ -0,0 +1,66 @@ +/* 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, (tab, browser, panel) => { + gTab = tab; + gPanel = panel; + + openConsole(tab, testConsoleProfile); + }); +} + +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); + + 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"); + let getTitle = (uid) => + panel.document.querySelector("li#profile-" + uid + " > h1").textContent; + + is(getTitle(1), "Profile 1", "Profile 1 doesn't have a star next to it."); + is(getTitle(2), "Profile 2 *", "Profile 2 doesn't have a star next to it."); + is(getTitle(3), "Profile 3", "Profile 3 doesn't have a star next to it."); + + // Make sure we can still stop profiles via the UI. + + gPanel.profiles.get(2).once("stopped", () => { + is(getTitle(2), "Profile 2", "Profile 2 doesn't have a star next to it."); + tearDown(gTab, () => gTab = gPanel = null); + }); + + sendFromProfile(2, "stop"); +} \ 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 new file mode 100644 index 000000000000..869e6475cbbf --- /dev/null +++ b/browser/devtools/profiler/test/browser_profiler_console_api_content.js @@ -0,0 +1,53 @@ +/* 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
"; +const BASE = "http://example.com/browser/browser/devtools/profiler/test/"; +const PAGE = BASE + "mock_console_api.html"; + +let gTab, gPanel, gToolbox; + +function test() { + waitForExplicitFinish(); + + setUp(URL, (tab, browser, panel) => { + gTab = tab; + gPanel = panel; + + openProfiler(tab, (toolbox) => { + gToolbox = toolbox; + loadUrl(PAGE, tab, () => { + gPanel.on("profileCreated", runTests); + }); + }); + }); +} + +function runTests() { + let getTitle = (uid) => + gPanel.document.querySelector("li#profile-" + uid + " > h1").textContent; + + is(getTitle(1), "Profile 1", "Profile 1 doesn't have a star next to it."); + is(getTitle(2), "Profile 2", "Profile 2 doesn't have a star next to it."); + + gPanel.once("parsed", () => { + function assertSampleAndFinish() { + let [win,doc] = getProfileInternals(); + let sample = doc.getElementsByClassName("samplePercentage"); + + if (sample.length <= 0) + return void setTimeout(assertSampleAndFinish, 100); + + ok(sample.length > 0, "We have Cleopatra UI displayed"); + tearDown(gTab, () => { + gTab = null; + gPanel = null; + gToolbox = null; + }); + } + + assertSampleAndFinish(); + }); + + gPanel.switchToProfile(gPanel.profiles.get(2), () => dump("\n\n\nSWITCHED\n\n\n")); +} \ No newline at end of file diff --git a/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js b/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js new file mode 100644 index 000000000000..17a66fe8fb47 --- /dev/null +++ b/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js @@ -0,0 +1,38 @@ +/* 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, (tab, browser, panel) => { + gTab = tab; + gPanel = panel; + + openProfiler(tab, runTests); + }); +} + +function runTests(toolbox) { + let panel = toolbox.getPanel("jsprofiler"); + let getTitle = (uid) => + panel.document.querySelector("li#profile-" + uid + " > h1").textContent; + + panel.profiles.get(1).once("started", () => { + is(getTitle(1), "Profile 1 *", "Profile 1 has a start next to it."); + + openConsole(gTab, (hud) => { + panel.profiles.get(1).once("stopped", () => { + is(getTitle(1), "Profile 1", "Profile 1 doesn't have a star next to it."); + tearDown(gTab, () => gTab = gPanel = null); + }); + + hud.jsterm.execute("console.profileEnd()"); + }); + }); + + sendFromProfile(1, "start"); +} \ 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 new file mode 100644 index 000000000000..f0232fa4f527 --- /dev/null +++ b/browser/devtools/profiler/test/browser_profiler_console_api_named.js @@ -0,0 +1,66 @@ +/* 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, (tab, browser, panel) => { + gTab = tab; + gPanel = panel; + + openConsole(tab, testConsoleProfile); + }); +} + +function testConsoleProfile(hud) { + hud.jsterm.clearOutput(true); + + // Here we start two named profiles and then end one of them. + + let profilesStarted = 0; + + function profileEnd(_, uid) { + let profile = gPanel.profiles.get(uid); + + 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.on("profileCreated", profileEnd); + hud.jsterm.execute("console.profile('Second')"); + hud.jsterm.execute("console.profile('Third')"); +} + +function checkProfiles(toolbox) { + let panel = toolbox.getPanel("jsprofiler"); + let getTitle = (uid) => + panel.document.querySelector("li#profile-" + uid + " > h1").textContent; + + is(getTitle(1), "Profile 1", "Profile 1 doesn't have a star next to it."); + is(getTitle(2), "Second", "Second doesn't have a star next to it."); + is(getTitle(3), "Third *", "Third does have a star next to it."); + + // Make sure we can still stop profiles via the queue pop. + + gPanel.profiles.get(3).once("stopped", () => { + openProfiler(gTab, () => { + is(getTitle(3), "Third", "Third doesn't have a star next to it."); + tearDown(gTab, () => gTab = gPanel = null); + }); + }); + + openConsole(gTab, (hud) => hud.jsterm.execute("console.profileEnd()")); +} \ No newline at end of file diff --git a/browser/devtools/profiler/test/head.js b/browser/devtools/profiler/test/head.js index 651ddb2c75c0..81894c2b732a 100644 --- a/browser/devtools/profiler/test/head.js +++ b/browser/devtools/profiler/test/head.js @@ -14,6 +14,9 @@ let TargetFactory = temp.devtools.TargetFactory; Cu.import("resource://gre/modules/devtools/dbg-server.jsm", temp); let DebuggerServer = temp.DebuggerServer; +Cu.import("resource:///modules/HUDService.jsm", temp); +let HUDService = temp.HUDService; + // Import the GCLI test helper let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this); @@ -33,11 +36,19 @@ function getProfileInternals(uid) { return [win, doc]; } +function sendFromProfile(uid, msg) { + let [win, doc] = getProfileInternals(uid); + win.parent.postMessage({ uid: uid, status: msg }, "*"); +} + function loadTab(url, callback) { let tab = gBrowser.addTab(); gBrowser.selectedTab = tab; - content.location.assign(url); + loadUrl(url, tab, callback); +} +function loadUrl(url, tab, callback) { + content.location.assign(url); let browser = gBrowser.getBrowserForTab(tab); if (browser.contentDocument.readyState === "complete") { callback(tab, browser); @@ -57,6 +68,17 @@ function openProfiler(tab, callback) { gDevTools.showToolbox(target, "jsprofiler").then(callback); } +function openConsole(tab, cb=function(){}) { + // This function was borrowed from webconsole/test/head.js + let target = TargetFactory.forTab(tab); + + gDevTools.showToolbox(target, "webconsole").then(function (toolbox) { + let hud = toolbox.getCurrentPanel().hud; + hud.jsterm._lazyVariablesView = false; + cb(hud); + }); +} + function closeProfiler(tab, callback) { let target = TargetFactory.forTab(tab); let toolbox = gDevTools.getToolbox(target); diff --git a/browser/devtools/profiler/test/mock_console_api.html b/browser/devtools/profiler/test/mock_console_api.html new file mode 100644 index 000000000000..2a626c9aac88 --- /dev/null +++ b/browser/devtools/profiler/test/mock_console_api.html @@ -0,0 +1,21 @@ + + + + + + +