Bug 817836 - Support for console.profile and console.profileEnd; r=dcamp

This commit is contained in:
Anton Kovalyov 2013-05-14 15:25:28 -07:00
parent d7a6ac8ae0
commit 27a9375ceb
15 changed files with 725 additions and 116 deletions

View File

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

View File

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

View File

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

View File

@ -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,75 @@ 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) => {
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 +285,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 +306,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 +314,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 +331,7 @@ ProfilerController.prototype = {
return void cb(resp.error);
}
sharedData.startTime = (new Date()).getTime();
profile.timeStarted = getCurrentTime();
profile.timeStarted = 0;
cb();
});
});
@ -223,7 +366,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.

View File

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

View File

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

View File

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

View File

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

View File

@ -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,<p>JavaScript Profiler test</p>";
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");
}

View File

@ -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,<p>JavaScript Profiler test</p>";
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));
}

View File

@ -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,<p>JavaScript Profiler test</p>";
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");
}

View File

@ -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,<p>JavaScript Profiler test</p>";
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()"));
}

View File

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

View File

@ -0,0 +1,21 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html>
<head>
<meta charset='utf-8'/>
<title>console.profile from content</title>
</head>
<body>
<script>
console.profile();
var a = new Array(500);
while (a.length) {
a.shift();
}
console.profileEnd();
</script>
</body>
</html>

View File

@ -5,6 +5,11 @@
"use strict";
var connCount = 0;
var startTime = 0;
function getCurrentTime() {
return (new Date()).getTime() - startTime;
}
/**
* Creates a ProfilerActor. ProfilerActor provides remote access to the
@ -44,6 +49,7 @@ ProfilerActor.prototype = {
this._profiler.StartProfiler(aRequest.entries, aRequest.interval,
aRequest.features, aRequest.features.length);
this._started = true;
startTime = (new Date()).getTime();
return { "msg": "profiler started" }
},
onStopProfiler: function(aRequest) {
@ -57,11 +63,12 @@ ProfilerActor.prototype = {
},
onGetProfile: function(aRequest) {
var profile = this._profiler.getProfileData();
return { "profile": profile }
return { "profile": profile, "currentTime": getCurrentTime() }
},
onIsActive: function(aRequest) {
var isActive = this._profiler.IsActive();
return { "isActive": isActive }
var currentTime = isActive ? getCurrentTime() : null;
return { "isActive": isActive, "currentTime": currentTime }
},
onGetResponsivenessTimes: function(aRequest) {
var times = this._profiler.GetResponsivenessTimes({});
@ -128,11 +135,62 @@ ProfilerActor.prototype = {
aSubject = (aSubject && aSubject.wrappedJSObject) || aSubject;
aData = (aData && aData.wrappedJSObject) || aData;
this.conn.send({ from: this.actorID,
type: "eventNotification",
event: aTopic,
subject: JSON.parse(JSON.stringify(aSubject, cycleBreaker)),
data: JSON.parse(JSON.stringify(aData, cycleBreaker)) });
let subj = JSON.parse(JSON.stringify(aSubject, cycleBreaker));
let data = JSON.parse(JSON.stringify(aData, cycleBreaker));
let send = (extra) => {
data = data || {};
if (extra)
data.extra = extra;
this.conn.send({
from: this.actorID,
type: "eventNotification",
event: aTopic,
subject: subj,
data: data
});
}
if (aTopic !== "console-api-profiler")
return void send();
// If the event was generated from console.profile or
// console.profileEnd we need to start the profiler
// right away and only then notify our client. Otherwise,
// we'll lose precious samples.
let name = subj.arguments[0];
if (subj.action === "profile") {
let resp = this.onIsActive();
if (resp.isActive) {
return void send({
name: name,
currentTime: resp.currentTime,
action: "profile"
});
}
this.onStartProfiler({
entries: 1000000,
interval: 1,
features: ["js"]
});
return void send({ currentTime: 0, action: "profile", name: name });
}
if (subj.action === "profileEnd") {
let resp = this.onGetProfile();
resp.action = "profileEnd";
resp.name = name;
send(resp);
}
return undefined; // Otherwise xpcshell tests fail.
}, "ProfilerActor.prototype.observe"),
};