Bug 828051: Add GCLI commands for the JavaScript Profiler. r=dcamp

This commit is contained in:
Anton Kovalyov 2013-03-28 15:13:24 -07:00
parent 5aee482ab9
commit 43765a7141
9 changed files with 542 additions and 32 deletions

View File

@ -14,3 +14,4 @@ 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");

View File

@ -142,7 +142,7 @@ ProfilerConnection.prototype = {
*/
function ProfilerController(target) {
this.profiler = new ProfilerConnection(target.client);
this.pool = {};
this.profiles = new Map();
// Chrome debugging targets have already obtained a reference to the
// profiler actor.
@ -210,12 +210,14 @@ ProfilerController.prototype = {
* argument: an error object (may be null).
*/
start: function PC_start(name, cb) {
if (this.pool[name]) {
if (this.profiles.has(name)) {
return;
}
let profile = this.pool[name] = makeProfile(name);
let profiler = this.profiler;
let profile = makeProfile(name);
this.profiles.set(name, profile);
// If profile is already running, no need to do anything.
if (this.isProfileRecording(profile)) {
@ -251,15 +253,15 @@ ProfilerController.prototype = {
*/
stop: function PC_stop(name, cb) {
let profiler = this.profiler;
let profile = this.pool[name];
let profile = this.profiles.get(name);
if (!profile || !this.isProfileRecording(profile)) {
return;
}
let isRecording = function () {
for (let name in this.pool) {
if (this.isProfileRecording(this.pool[name])) {
for (let [ name, profile ] of this.profiles) {
if (this.isProfileRecording(profile)) {
return true;
}
}

View File

@ -44,15 +44,19 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services",
* @param ProfilerPanel panel
* A reference to the container panel.
*/
function ProfileUI(uid, 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.panel = panel;
this.uid = uid;
this.name = name;
this.iframe = doc.createElement("iframe");
this.iframe.setAttribute("flex", "1");
@ -70,9 +74,6 @@ function ProfileUI(uid, panel) {
return;
}
let label = doc.querySelector("li#profile-" + this.uid + " > h1");
let name = label.textContent.replace(/\s\*$/, "");
switch (event.data.status) {
case "loaded":
if (this.panel._runningUid !== null) {
@ -87,26 +88,10 @@ function ProfileUI(uid, panel) {
this.emit("ready");
break;
case "start":
// 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.
this.panel.startProfiling(name, function onStart() {
label.textContent = name + " *";
this.panel.broadcast(this.uid, {task: "onStarted"});
this.emit("started");
}.bind(this));
this.start();
break;
case "stop":
// Stop profiling and, once stopped, notify the underlying page so
// that it could update the UI and remove a star from the profile
// name.
this.panel.stopProfiling(name, function onStop() {
label.textContent = name;
this.panel.broadcast(this.uid, {task: "onStopped"});
this.emit("stopped");
}.bind(this));
this.stop();
break;
case "disabled":
this.emit("disabled");
@ -169,6 +154,56 @@ ProfileUI.prototype = {
poll();
},
/**
* Update profile's label in the sidebar.
*
* @param string text
* New text for the label.
*/
updateLabel: function PUI_udpateLabel(text) {
let doc = this.panel.document;
let label = doc.querySelector("li#profile-" + this.uid + "> h1");
label.textContent = text;
},
/**
* 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.
*/
start: function PUI_start() {
if (this.isStarted || this.isFinished) {
return;
}
this.panel.startProfiling(this.name, function onStart() {
this.isStarted = true;
this.updateLabel(this.name + " *");
this.panel.broadcast(this.uid, {task: "onStarted"});
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.
*/
stop: function PUI_stop() {
if (!this.isStarted || this.isFinished) {
return;
}
this.panel.stopProfiling(this.name, function onStop() {
this.isStarted = false;
this.isFinished = true;
this.updateLabel(this.name);
this.panel.broadcast(this.uid, {task: "onStopped"});
this.emit("stopped");
}.bind(this));
},
/**
* Destroys the ProfileUI instance.
*/
@ -276,7 +311,9 @@ ProfilerPanel.prototype = {
this.controller.connect(function onConnect() {
let create = this.document.getElementById("profiler-create");
create.addEventListener("click", this.createProfile.bind(this), false);
create.addEventListener("click", function (ev) {
this.createProfile()
}.bind(this), false);
create.removeAttribute("disabled");
let profile = this.createProfile();
@ -303,13 +340,21 @@ ProfilerPanel.prototype = {
* the newly created profile, they have do to switch
* explicitly.
*
* @param string name
* (optional) name of the new profile
*
* @return ProfilerPanel
*/
createProfile: function PP_addProfile() {
createProfile: function PP_createProfile(name) {
if (name && this.getProfileByName(name)) {
return this.getProfileByName(name);
}
let uid = ++this._uid;
let list = this.document.getElementById("profiles-list");
let item = this.document.createElement("li");
let wrap = this.document.createElement("h1");
name = name || L10N.getFormatStr("profiler.profileName", [uid]);
item.setAttribute("id", "profile-" + uid);
item.setAttribute("data-uid", uid);
@ -318,12 +363,12 @@ ProfilerPanel.prototype = {
}.bind(this), false);
wrap.className = "profile-name";
wrap.textContent = L10N.getFormatStr("profiler.profileName", [uid]);
wrap.textContent = name;
item.appendChild(wrap);
list.appendChild(item);
let profile = new ProfileUI(uid, this);
let profile = new ProfileUI(uid, name, this);
this.profiles.set(uid, profile);
this.emit("profileCreated", uid);
@ -424,6 +469,40 @@ ProfilerPanel.prototype = {
}.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;
},
/**
* Broadcast messages to all Cleopatra instances.
*

View File

@ -0,0 +1,211 @@
/* 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/. */
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
this.EXPORTED_SYMBOLS = [];
Cu.import("resource:///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");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
var Promise = require('util/promise');
/*
* 'profiler' command. Doesn't do anything.
*/
gcli.addCommand({
name: "profiler",
description: gcli.lookup("profilerDesc"),
manual: gcli.lookup("profilerManual")
});
/*
* 'profiler open' command
*/
gcli.addCommand({
name: "profiler open",
description: gcli.lookup("profilerOpenDesc"),
params: [],
exec: function (args, context) {
return gDevTools.showToolbox(context.environment.target, "jsprofiler")
.then(function () null);
}
});
/*
* 'profiler close' command
*/
gcli.addCommand({
name: "profiler close",
description: gcli.lookup("profilerCloseDesc"),
params: [],
exec: function (args, context) {
return gDevTools.closeToolbox(context.environment.target)
.then(function () null);
}
});
/*
* 'profiler start' command
*/
gcli.addCommand({
name: "profiler start",
description: gcli.lookup("profilerStartDesc"),
returnType: "string",
params: [
{
name: "name",
type: "string"
}
],
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 (profile.isFinished) {
throw gcli.lookup("profilerAlradyFinished");
}
panel.switchToProfile(profile, function () profile.start());
return gcli.lookup("profilerStarting");
}
return gDevTools.showToolbox(context.environment.target, "jsprofiler")
.then(start);
}
});
/*
* 'profiler stop' command
*/
gcli.addCommand({
name: "profiler stop",
description: gcli.lookup("profilerStopDesc"),
returnType: "string",
params: [
{
name: "name",
type: "string"
}
],
exec: function (args, context) {
function stop() {
let panel = getPanel(context, "jsprofiler");
let profile = panel.getProfileByName(args.name);
if (!profile) {
throw gcli.lookup("profilerNotFound");
}
if (profile.isFinished) {
throw gcli.lookup("profilerAlreadyFinished");
}
if (!profile.isStarted) {
throw gcli.lookup("profilerNotStarted");
}
panel.switchToProfile(profile, function () profile.stop());
return gcli.lookup("profilerStopping");
}
return gDevTools.showToolbox(context.environment.target, "jsprofiler")
.then(stop);
}
});
/*
* 'profiler list' command
*/
gcli.addCommand({
name: "profiler list",
description: gcli.lookup("profilerListDesc"),
returnType: "dom",
params: [],
exec: function (args, context) {
let panel = getPanel(context, "jsprofiler");
if (!panel) {
throw gcli.lookup("profilerNotReady");
}
let doc = panel.document;
let div = createXHTMLElement(doc, "div");
let ol = createXHTMLElement(doc, "ol");
for ([ uid, profile] of panel.profiles) {
let li = createXHTMLElement(doc, "li");
li.textContent = profile.name;
if (profile.isStarted) {
li.textContent += " *";
}
ol.appendChild(li);
}
div.appendChild(ol);
return div;
}
});
/*
* 'profiler show' command
*/
gcli.addCommand({
name: "profiler show",
description: gcli.lookup("profilerShowDesc"),
params: [
{
name: "name",
type: "string"
}
],
exec: function (args, context) {
let panel = getPanel(context, "jsprofiler");
if (!panel) {
throw gcli.lookup("profilerNotReady");
}
let profile = panel.getProfileByName(args.name);
if (!profile) {
throw gcli.lookup("profilerNotFound");
}
panel.switchToProfile(profile);
}
});
function getPanel(context, id) {
if (context == null) {
return undefined;
}
let toolbox = gDevTools.getToolbox(context.environment.target);
return toolbox == null ? undefined : toolbox.getPanel(id);
}
function createXHTMLElement(document, tagname) {
return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
}

View File

@ -14,6 +14,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_profiler_profiles.js \
browser_profiler_remote.js \
browser_profiler_bug_834878_source_buttons.js \
browser_profiler_cmd.js \
head.js \
$(NULL)

View File

@ -0,0 +1,125 @@
/* 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 gTarget, gPanel, gOptions;
function cmd(typed, expected="") {
helpers.audit(gOptions, [{
setup: typed,
exec: { output: expected }
}]);
}
function test() {
waitForExplicitFinish();
helpers.addTabWithToolbar(URL, function (options) {
gOptions = options;
gTarget = options.target;
return gDevTools.showToolbox(options.target, "jsprofiler")
.then(setupGlobals)
.then(testProfilerStart)
.then(testProfilerList)
.then(testProfilerStop)
.then(testProfilerClose)
}).then(finishUp);
}
function setupGlobals() {
let deferred = Promise.defer();
gPanel = gDevTools.getToolbox(gTarget).getPanel("jsprofiler");
deferred.resolve();
return deferred.promise;
}
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");
deferred.resolve();
});
cmd("profiler start", "Starting...");
return deferred.promise;
}
function testProfilerList() {
let deferred = Promise.defer();
cmd("profiler list", /^.*Profile\s1.*Profile\s2\s\*.*$/);
deferred.resolve();
return deferred.promise;
}
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 profliling");
cmd('profiler stop "invalid"', "Profile not found")
deferred.resolve();
});
cmd('profiler stop "Profile 2"', "Stopping...");
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");
deferred.resolve();
});
cmd('profile show "Profile 1"');
return deferred.promise;
}
function testProfilerClose() {
let deferred = Promise.defer();
helpers.audit(gOptions, [{
setup: "profiler close",
completed: false,
exec: { output: "" }
}]);
let toolbox = gDevTools.getToolbox(gOptions.target);
if (!toolbox) {
ok(true, "Profiler was closed.");
deferred.resolve();
} else {
toolbox.on("destroyed", function () {
ok(true, "Profiler was closed.");
deferred.resolve();
});
}
return deferred.promise;
}
function finishUp() {
gTarget = null;
gPanel = null;
gOptions = null;
finish();
}

View File

@ -37,6 +37,26 @@ function onProfileCreated(name, uid) {
}
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 label = gPanel.document.querySelector("li#profile-" + uid + "> h1");
is(label.textContent, "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() {

View File

@ -14,7 +14,12 @@ let gDevTools = temp.gDevTools;
Cu.import("resource://gre/modules/devtools/dbg-server.jsm", temp);
let DebuggerServer = temp.DebuggerServer;
// Import the GCLI test helper
let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
registerCleanupFunction(function () {
helpers = null;
Services.prefs.clearUserPref(PROFILER_ENABLED);
Services.prefs.clearUserPref(REMOTE_ENABLED);
DebuggerServer.destroy();

View File

@ -1060,3 +1060,69 @@ paintflashingManual=Draw repainted areas in different colors
# LOCALIZATION NOTE (paintflashingTooltip) A string displayed as the
# tooltip of button in devtools toolbox which toggles paint flashing.
paintflashingTooltip=Highlight painted area
# LOCALIZATION NOTE (profilerDesc) A very short string used to describe the
# function of the profiler command.
profilerDesc=Manage profiler
# LOCALIZATION NOTE (profilerManual) A longer description describing the
# set of commands that control the profiler.
profilerManual=Commands to start or stop a JavaScript profiler
# LOCALIZATION NOTE (profilerOpen) A very short string used to describe the function
# of the profiler open command.
profilerOpenDesc=Open the profiler
# LOCALIZATION NOTE (profilerClose) A very short string used to describe the function
# of the profiler close command.
profilerCloseDesc=Close the profiler
# LOCALIZATION NOTE (profilerStart) A very short string used to describe the function
# of the profiler start command.
profilerStartDesc=Start profiling
# LOCALIZATION NOTE (profilerStop) A very short string used to describe the function
# of the profiler stop command.
profilerStopDesc=Stop profiling
# LOCALIZATION NOTE (profilerList) A very short string used to describe the function
# of the profiler list command.
profilerListDesc=List all profiles
# LOCALIZATION NOTE (profilerShow) A very short string used to describe the function
# of the profiler show command.
profilerShowDesc=Show individual 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
# LOCALIZATION NOTE (profilerNotFound) A message that is displayed whenever
# an operation cannot be completed because the profile in question could not be
# found.
profilerNotFound=Profile not found
# LOCALIZATION NOTE (profilerNotStarted) A message that is displayed whenever
# 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.
profilerNotStarted=This profile has not been started yet. Use 'profile start' to start profliling
# LOCALIZATION NOTE (profilerStarting) A very short string that indicates that
# we're starting the profiler.
profilerStarting=Starting...
# LOCALIZATION NOTE (profilerStopping) A very short string that indicates that
# we're stopping the profiler.
profilerStopping=Stopping...
# LOCALIZATION NOTE (profilerNotReady) A message that is displayed whenever
# an operation cannot be completed because the profiler has not been opened yet.
profilerNotReady=For this command to work you need to open the profiler first