Merge latest green birch changeset and mozilla-central

This commit is contained in:
Ed Morley 2013-05-29 15:23:27 +01:00
commit 744a030aa7
35 changed files with 1099 additions and 252 deletions

View File

@ -577,7 +577,8 @@
<menuitem id="menu_chromeDebugger"
observes="devtoolsMenuBroadcaster_ChromeDebugger"/>
<menuitem id="menu_browserConsole"
observes="devtoolsMenuBroadcaster_BrowserConsole"/>
observes="devtoolsMenuBroadcaster_BrowserConsole"
accesskey="&browserConsoleCmd.accesskey;"/>
<menuitem id="menu_responsiveUI"
observes="devtoolsMenuBroadcaster_ResponsiveUI"
accesskey="&responsiveDesignTool.accesskey;"/>

View File

@ -96,7 +96,7 @@
<command id="Tools:DevToolbar" oncommand="DeveloperToolbar.toggle();" disabled="true" hidden="true"/>
<command id="Tools:DevToolbarFocus" oncommand="DeveloperToolbar.focusToggle();" disabled="true"/>
<command id="Tools:ChromeDebugger" oncommand="DebuggerUI.toggleChromeDebugger();" disabled="true" hidden="true"/>
<command id="Tools:BrowserConsole" oncommand="HUDConsoleUI.toggleBrowserConsole();" disabled="true" hidden="true"/>
<command id="Tools:BrowserConsole" oncommand="HUDConsoleUI.toggleBrowserConsole();"/>
<command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();" disabled="true" hidden="true"/>
<command id="Tools:ResponsiveUI" oncommand="ResponsiveUI.toggle();" disabled="true" hidden="true"/>
<command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/>
@ -187,6 +187,7 @@
command="Tools:ChromeDebugger"/>
<broadcaster id="devtoolsMenuBroadcaster_BrowserConsole"
label="&browserConsoleCmd.label;"
key="key_browserConsole"
command="Tools:BrowserConsole"/>
<broadcaster id="devtoolsMenuBroadcaster_Scratchpad"
label="&scratchpad.label;"
@ -203,7 +204,6 @@
command="View:PageSource"/>
<broadcaster id="devtoolsMenuBroadcaster_ErrorConsole"
label="&errorConsoleCmd.label;"
key="key_errorConsole"
command="Tools:ErrorConsole"/>
<broadcaster id="devtoolsMenuBroadcaster_GetMoreTools"
label="&getMoreDevtoolsCmd.label;"
@ -261,7 +261,7 @@
<key id="key_openDownloads" key="&downloads.commandkey;" command="Tools:Downloads" modifiers="accel"/>
#endif
<key id="key_openAddons" key="&addons.commandkey;" command="Tools:Addons" modifiers="accel,shift"/>
<key id="key_errorConsole" key="&errorConsoleCmd.commandkey;" command="Tools:ErrorConsole" modifiers="accel,shift"/>
<key id="key_browserConsole" key="&browserConsoleCmd.commandkey;" command="Tools:BrowserConsole" modifiers="accel,shift"/>
<key id="key_devToolbar" keycode="&devToolbar.keycode;" modifiers="shift"
keytext="&devToolbar.keytext;" command="Tools:DevToolbarFocus"/>
<key id="key_responsiveUI" key="&responsiveDesignTool.commandkey;" command="Tools:ResponsiveUI"

View File

@ -1193,17 +1193,8 @@ var gBrowserInit = {
cmd.removeAttribute("hidden");
}
// Enable the Browser Console?
if (chromeEnabled) {
let cmd = document.getElementById("Tools:BrowserConsole");
cmd.removeAttribute("disabled");
cmd.removeAttribute("hidden");
}
// Enable Error Console?
// Temporarily enabled. See bug 798925.
let consoleEnabled = true || gPrefService.getBoolPref("devtools.errorconsole.enabled") ||
chromeEnabled;
let consoleEnabled = gPrefService.getBoolPref("devtools.errorconsole.enabled");
if (consoleEnabled) {
let cmd = document.getElementById("Tools:ErrorConsole");
cmd.removeAttribute("disabled");

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

@ -25,7 +25,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "gcli",
XPCOMUtils.defineLazyModuleGetter(this, "CmdCommands",
"resource:///modules/devtools/BuiltinCommands.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener",
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleServiceListener",
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
@ -444,8 +444,8 @@ DeveloperToolbar.prototype._initErrorsCount = function DT__initErrorsCount(aTab)
}
let window = aTab.linkedBrowser.contentWindow;
let listener = new PageErrorListener(window, {
onPageError: this._onPageError.bind(this, tabId),
let listener = new ConsoleServiceListener(window, {
onConsoleServiceMessage: this._onPageError.bind(this, tabId),
});
listener.init();

View File

@ -131,6 +131,7 @@ MOCHITEST_BROWSER_FILES = \
browser_console_native_getters.js \
browser_bug_871156_ctrlw_close_tab.js \
browser_console_private_browsing.js \
browser_console_nsiconsolemessage.js \
head.js \
$(NULL)

View File

@ -9,6 +9,15 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/te
function test()
{
let oldFunction = HUDConsoleUI.toggleBrowserConsole;
let functionExecuted = false;
HUDConsoleUI.toggleBrowserConsole = () => functionExecuted = true;
EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, content);
ok(functionExecuted,
"toggleBrowserConsole() was executed by the Ctrl-Shift-J key shortcut");
HUDConsoleUI.toggleBrowserConsole = oldFunction;
HUDConsoleUI.toggleBrowserConsole().then(consoleOpened);
}

View File

@ -0,0 +1,79 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// Check that nsIConsoleMessages are displayed in the Browser Console.
// See bug 859756.
const TEST_URI = "data:text/html;charset=utf8,<title>bug859756</title>\n" +
"<p>hello world\n<p>nsIConsoleMessages ftw!";
function test()
{
addTab(TEST_URI);
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
// Test for cached nsIConsoleMessages.
Services.console.logStringMessage("test1 for bug859756");
info("open web console");
openConsole(null, consoleOpened);
}, true);
}
function consoleOpened(hud)
{
ok(hud, "web console opened");
Services.console.logStringMessage("do-not-show-me");
content.console.log("foobarz");
waitForMessages({
webconsole: hud,
messages: [
{
text: "foobarz",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
},
],
}).then(() => {
let text = hud.outputNode.textContent;
is(text.indexOf("do-not-show-me"), -1,
"nsIConsoleMessages are not displayed");
is(text.indexOf("test1 for bug859756"), -1,
"nsIConsoleMessages are not displayed (confirmed)");
closeConsole(null, onWebConsoleClose);
});
}
function onWebConsoleClose()
{
info("web console closed");
HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
}
function onBrowserConsoleOpen(hud)
{
ok(hud, "browser console opened");
Services.console.logStringMessage("test2 for bug859756");
waitForMessages({
webconsole: hud,
messages: [
{
text: "test1 for bug859756",
category: CATEGORY_JS,
},
{
text: "test2 for bug859756",
category: CATEGORY_JS,
},
{
text: "do-not-show-me",
category: CATEGORY_JS,
},
],
}).then(finishTest);
}

View File

@ -961,6 +961,9 @@ WebConsoleFrame.prototype = {
[category, aMessage]);
break;
}
case "LogMessage":
this.handleLogMessage(aMessage);
break;
case "ConsoleAPI":
this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
[aMessage]);
@ -1198,6 +1201,21 @@ WebConsoleFrame.prototype = {
this.outputMessage(category, this.reportPageError, [category, aPageError]);
},
/**
* Handle log messages received from the server. This method outputs the given
* message.
*
* @param object aPacket
* The message packet received from the server.
*/
handleLogMessage: function WCF_handleLogMessage(aPacket)
{
this.outputMessage(CATEGORY_JS, () => {
return this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, aPacket.message,
null, null, null, null, aPacket.timeStamp);
});
},
/**
* Log network event.
*
@ -4530,6 +4548,7 @@ function WebConsoleConnectionProxy(aWebConsole, aTarget)
this.target = aTarget;
this._onPageError = this._onPageError.bind(this);
this._onLogMessage = this._onLogMessage.bind(this);
this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
this._onNetworkEvent = this._onNetworkEvent.bind(this);
this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
@ -4634,6 +4653,7 @@ WebConsoleConnectionProxy.prototype = {
let client = this.client = this.target.client;
client.addListener("logMessage", this._onLogMessage);
client.addListener("pageError", this._onPageError);
client.addListener("consoleAPICall", this._onConsoleAPICall);
client.addListener("networkEvent", this._onNetworkEvent);
@ -4755,6 +4775,23 @@ WebConsoleConnectionProxy.prototype = {
}
},
/**
* The "logMessage" message type handler. We redirect any message to the UI
* for displaying.
*
* @private
* @param string aType
* Message type.
* @param object aPacket
* The message received from the server.
*/
_onLogMessage: function WCCP__onLogMessage(aType, aPacket)
{
if (this.owner && aPacket.from == this._consoleActor) {
this.owner.handleLogMessage(aPacket);
}
},
/**
* The "consoleAPICall" message type handler. We redirect any message to
* the UI for displaying.
@ -4899,6 +4936,7 @@ WebConsoleConnectionProxy.prototype = {
return this._disconnecter.promise;
}
this.client.removeListener("logMessage", this._onLogMessage);
this.client.removeListener("pageError", this._onPageError);
this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
this.client.removeListener("networkEvent", this._onNetworkEvent);

View File

@ -214,11 +214,12 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!ENTITY errorConsoleCmd.label "Error Console">
<!ENTITY errorConsoleCmd.accesskey "C">
<!ENTITY errorConsoleCmd.commandkey "j">
<!ENTITY remoteWebConsoleCmd.label "Remote Web Console">
<!ENTITY browserConsoleCmd.label "Browser Console">
<!ENTITY browserConsoleCmd.commandkey "j">
<!ENTITY browserConsoleCmd.accesskey "B">
<!ENTITY inspectContextMenu.label "Inspect Element">
<!ENTITY inspectContextMenu.accesskey "Q">

View File

@ -11,7 +11,7 @@
#include "nsISupports.idl"
#include "nsIConsoleMessage.idl"
[scriptable, uuid(ec640482-be5f-49a0-a9cb-c87eacce9291)]
[scriptable, uuid(cac9d8e8-0d53-4fa8-9903-bb367e4fa1fe)]
interface nsIScriptError : nsIConsoleMessage
{
/** pseudo-flag for default case */
@ -49,15 +49,6 @@ interface nsIScriptError : nsIConsoleMessage
*/
readonly attribute string category;
/*
The time (in milliseconds from the Epoch) that the script error instance
was initialised, and thus the time when the error occurred.
Currently used to display date and time of the message in Error console.
The timestamp is initialized as JS_now/1000 so that it can be
compared to Date.now in Javascript.
*/
readonly attribute long long timeStamp;
/* Get the window id this was initialized with. Zero will be
returned if init() was used instead of initWithWindowID(). */
readonly attribute unsigned long long outerWindowID;
@ -87,13 +78,11 @@ interface nsIScriptError : nsIConsoleMessage
in uint32_t flags,
in string category,
in unsigned long long innerWindowID);
AUTF8String toString();
};
%{ C++
#define NS_SCRIPTERROR_CID \
{ 0xe38e53b9, 0x5bb0, 0x456a, { 0xb5, 0x53, 0x57, 0x93, 0x70, 0xcb, 0x15, 0x67 }}
{ 0x1950539a, 0x90f0, 0x4d22, { 0xb5, 0xaf, 0x71, 0x32, 0x9c, 0x68, 0xfa, 0x35 }}
#define NS_SCRIPTERROR_CONTRACTID "@mozilla.org/scripterror;1"
%}

View File

@ -175,6 +175,7 @@ const UnsolicitedNotifications = {
"eventNotification": "eventNotification",
"fileActivity": "fileActivity",
"lastPrivateContextExited": "lastPrivateContextExited",
"logMessage": "logMessage",
"networkEvent": "networkEvent",
"networkEventUpdate": "networkEventUpdate",
"newGlobal": "newGlobal",

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

View File

@ -18,7 +18,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services",
XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener",
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleServiceListener",
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIListener",
@ -182,10 +182,10 @@ WebConsoleActor.prototype =
_window: null,
/**
* The PageErrorListener instance.
* The ConsoleServiceListener instance.
* @type object
*/
pageErrorListener: null,
consoleServiceListener: null,
/**
* The ConsoleAPIListener instance.
@ -228,9 +228,9 @@ WebConsoleActor.prototype =
*/
disconnect: function WCA_disconnect()
{
if (this.pageErrorListener) {
this.pageErrorListener.destroy();
this.pageErrorListener = null;
if (this.consoleServiceListener) {
this.consoleServiceListener.destroy();
this.consoleServiceListener = null;
}
if (this.consoleAPIListener) {
this.consoleAPIListener.destroy();
@ -384,10 +384,10 @@ WebConsoleActor.prototype =
let listener = aRequest.listeners.shift();
switch (listener) {
case "PageError":
if (!this.pageErrorListener) {
this.pageErrorListener =
new PageErrorListener(window, this);
this.pageErrorListener.init();
if (!this.consoleServiceListener) {
this.consoleServiceListener =
new ConsoleServiceListener(window, this);
this.consoleServiceListener.init();
}
startedListeners.push(listener);
break;
@ -447,9 +447,9 @@ WebConsoleActor.prototype =
let listener = toDetach.shift();
switch (listener) {
case "PageError":
if (this.pageErrorListener) {
this.pageErrorListener.destroy();
this.pageErrorListener = null;
if (this.consoleServiceListener) {
this.consoleServiceListener.destroy();
this.consoleServiceListener = null;
}
stoppedListeners.push(listener);
break;
@ -505,28 +505,42 @@ WebConsoleActor.prototype =
while (types.length > 0) {
let type = types.shift();
switch (type) {
case "ConsoleAPI":
if (this.consoleAPIListener) {
let cache = this.consoleAPIListener
.getCachedMessages(!this._isGlobalActor);
cache.forEach((aMessage) => {
let message = this.prepareConsoleMessageForRemote(aMessage);
message._type = type;
messages.push(message);
});
case "ConsoleAPI": {
if (!this.consoleAPIListener) {
break;
}
let cache = this.consoleAPIListener
.getCachedMessages(!this._isGlobalActor);
cache.forEach((aMessage) => {
let message = this.prepareConsoleMessageForRemote(aMessage);
message._type = type;
messages.push(message);
});
break;
case "PageError":
if (this.pageErrorListener) {
let cache = this.pageErrorListener
.getCachedMessages(!this._isGlobalActor);
cache.forEach((aMessage) => {
let message = this.preparePageErrorForRemote(aMessage);
message._type = type;
messages.push(message);
});
}
case "PageError": {
if (!this.consoleServiceListener) {
break;
}
let cache = this.consoleServiceListener
.getCachedMessages(!this._isGlobalActor);
cache.forEach((aMessage) => {
let message = null;
if (aMessage instanceof Ci.nsIScriptError) {
message = this.preparePageErrorForRemote(aMessage);
message._type = type;
}
else {
message = {
_type: "LogMessage",
message: aMessage.message,
timeStamp: aMessage.timeStamp,
};
}
messages.push(message);
});
break;
}
}
}
@ -618,6 +632,10 @@ WebConsoleActor.prototype =
let windowId = !this._isGlobalActor ?
WebConsoleUtils.getInnerWindowId(this.window) : null;
ConsoleAPIStorage.clearEvents(windowId);
if (this._isGlobalActor) {
Services.console.logStringMessage(null); // for the Error Console
Services.console.reset();
}
return {};
},
@ -875,19 +893,30 @@ WebConsoleActor.prototype =
//////////////////
/**
* Handler for page errors received from the PageErrorListener. This method
* sends the nsIScriptError to the remote Web Console client.
* Handler for messages received from the ConsoleServiceListener. This method
* sends the nsIConsoleMessage to the remote Web Console client.
*
* @param nsIScriptError aPageError
* The page error we need to send to the client.
* @param nsIConsoleMessage aMessage
* The message we need to send to the client.
*/
onPageError: function WCA_onPageError(aPageError)
onConsoleServiceMessage: function WCA_onConsoleServiceMessage(aMessage)
{
let packet = {
from: this.actorID,
type: "pageError",
pageError: this.preparePageErrorForRemote(aPageError),
};
let packet;
if (aMessage instanceof Ci.nsIScriptError) {
packet = {
from: this.actorID,
type: "pageError",
pageError: this.preparePageErrorForRemote(aMessage),
};
}
else {
packet = {
from: this.actorID,
type: "logMessage",
message: aMessage.message,
timeStamp: aMessage.timeStamp,
};
}
this.conn.send(packet);
},

View File

@ -42,7 +42,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
"resource:///modules/devtools/VariablesView.jsm");
this.EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", "JSTermHelpers",
"PageErrorListener", "ConsoleAPIListener",
"ConsoleServiceListener", "ConsoleAPIListener",
"NetworkResponseListener", "NetworkMonitor",
"ConsoleProgressListener"];
@ -876,25 +876,25 @@ return JSPropertyProvider;
///////////////////////////////////////////////////////////////////////////////
/**
* The nsIConsoleService listener. This is used to send all the page errors
* (JavaScript, CSS and more) to the remote Web Console instance.
* The nsIConsoleService listener. This is used to send all of the console
* messages (JavaScript, CSS and more) to the remote Web Console instance.
*
* @constructor
* @param nsIDOMWindow [aWindow]
* Optional - the window object for which we are created. This is used
* for filtering out messages that belong to other windows.
* @param object aListener
* The listener object must have a method: onPageError. This method is
* invoked with one argument, the nsIScriptError, whenever a relevant
* page error is received.
* The listener object must have one method:
* - onConsoleServiceMessage(). This method is invoked with one argument, the
* nsIConsoleMessage, whenever a relevant message is received.
*/
this.PageErrorListener = function PageErrorListener(aWindow, aListener)
this.ConsoleServiceListener = function ConsoleServiceListener(aWindow, aListener)
{
this.window = aWindow;
this.listener = aListener;
}
PageErrorListener.prototype =
ConsoleServiceListener.prototype =
{
QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
@ -905,8 +905,7 @@ PageErrorListener.prototype =
window: null,
/**
* The listener object which is notified of page errors. It must have
* a onPageError method which is invoked with one argument: the nsIScriptError.
* The listener object which is notified of messages from the console service.
* @type object
*/
listener: null,
@ -914,7 +913,7 @@ PageErrorListener.prototype =
/**
* Initialize the nsIConsoleService listener.
*/
init: function PEL_init()
init: function CSL_init()
{
Services.console.registerListener(this);
},
@ -924,43 +923,48 @@ PageErrorListener.prototype =
* messages belonging to the current window and sends them to the remote Web
* Console instance.
*
* @param nsIScriptError aScriptError
* The script error object coming from the nsIConsoleService.
* @param nsIConsoleMessage aMessage
* The message object coming from the nsIConsoleService.
*/
observe: function PEL_observe(aScriptError)
observe: function CSL_observe(aMessage)
{
if (!this.listener ||
!(aScriptError instanceof Ci.nsIScriptError)) {
if (!this.listener) {
return;
}
if (this.window) {
if (!aScriptError.outerWindowID ||
!this.isCategoryAllowed(aScriptError.category)) {
if (!(aMessage instanceof Ci.nsIScriptError) ||
!aMessage.outerWindowID ||
!this.isCategoryAllowed(aMessage.category)) {
return;
}
let errorWindow =
Services.wm.getOuterWindowWithId(aScriptError.outerWindowID);
let errorWindow = Services.wm.getOuterWindowWithId(aMessage.outerWindowID);
if (!errorWindow || errorWindow.top != this.window) {
return;
}
}
this.listener.onPageError(aScriptError);
if (aMessage.message) {
this.listener.onConsoleServiceMessage(aMessage);
}
},
/**
* Check if the given script error category is allowed to be tracked or not.
* Check if the given message category is allowed to be tracked or not.
* We ignore chrome-originating errors as we only care about content.
*
* @param string aCategory
* The nsIScriptError category you want to check.
* The message category you want to check.
* @return boolean
* True if the category is allowed to be logged, false otherwise.
*/
isCategoryAllowed: function PEL_isCategoryAllowed(aCategory)
isCategoryAllowed: function CSL_isCategoryAllowed(aCategory)
{
if (!aCategory) {
return false;
}
switch (aCategory) {
case "XPConnect JavaScript":
case "component javascript":
@ -983,26 +987,32 @@ PageErrorListener.prototype =
* Tells if you want to also retrieve messages coming from private
* windows. Defaults to false.
* @return array
* The array of cached messages.
* The array of cached messages. Each element is an nsIScriptError or
* an nsIConsoleMessage
*/
getCachedMessages: function PEL_getCachedMessages(aIncludePrivate = false)
getCachedMessages: function CSL_getCachedMessages(aIncludePrivate = false)
{
let innerWindowId = this.window ?
let innerWindowID = this.window ?
WebConsoleUtils.getInnerWindowId(this.window) : null;
let errors = Services.console.getMessageArray() || [];
return errors.filter((aError) => {
if (!(aError instanceof Ci.nsIScriptError)) {
return false;
}
if (!aIncludePrivate && aError.isFromPrivateWindow) {
return false;
}
if (innerWindowId &&
(aError.innerWindowID != innerWindowId ||
!this.isCategoryAllowed(aError.category))) {
if (aError instanceof Ci.nsIScriptError) {
if (!aIncludePrivate && aError.isFromPrivateWindow) {
return false;
}
if (innerWindowID &&
(aError.innerWindowID != innerWindowID ||
!this.isCategoryAllowed(aError.category))) {
return false;
}
}
else if (innerWindowID) {
// If this is not an nsIScriptError and we need to do window-based
// filtering we skip this message.
return false;
}
return true;
});
},
@ -1010,7 +1020,7 @@ PageErrorListener.prototype =
/**
* Remove the nsIConsoleService listener.
*/
destroy: function PEL_destroy()
destroy: function CSL_destroy()
{
Services.console.unregisterListener(this);
this.listener = this.window = null;
@ -1112,12 +1122,12 @@ ConsoleAPIListener.prototype =
{
let innerWindowId = this.window ?
WebConsoleUtils.getInnerWindowId(this.window) : null;
return ConsoleAPIStorage.getEvents(innerWindowId).filter((aMessage) => {
if (!aIncludePrivate && aMessage.private) {
return false;
}
return true;
});
let events = ConsoleAPIStorage.getEvents(innerWindowId);
if (aIncludePrivate) {
return events;
}
return events.filter((m) => !m.private);
},
/**

View File

@ -24,6 +24,7 @@ MOCHITEST_CHROME_FILES = \
test_bug819670_getter_throws.html \
test_object_actor_native_getters.html \
test_object_actor_native_getters_lenient_this.html \
test_nsiconsolemessage.html \
network_requests_iframe.html \
data.json \
data.json^headers^ \

View File

@ -106,7 +106,10 @@ function checkObject(aObject, aExpected)
function checkValue(aName, aValue, aExpected)
{
if (aValue === undefined) {
if (aExpected === null) {
ok(!aValue, "'" + aName + "' is null");
}
else if (aValue === undefined) {
ok(false, "'" + aName + "' is undefined");
}
else if (typeof aExpected == "string" || typeof aExpected == "number" ||

View File

@ -95,7 +95,7 @@ function doConsoleCalls()
<script class="testbody" type="text/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
let consoleAPIListener, pageErrorListener;
let consoleAPIListener, consoleServiceListener;
let consoleAPICalls = 0;
let pageErrors = 0;
@ -108,7 +108,7 @@ let handlers = {
}
},
onPageError: function onPageError()
onConsoleServiceMessage: function onConsoleServiceMessage()
{
pageErrors++;
if (pageErrors == expectedPageErrors.length) {
@ -153,16 +153,16 @@ function onCachedConsoleAPI(aState, aResponse)
});
closeDebugger(aState, function() {
pageErrorListener = new PageErrorListener(null, handlers);
pageErrorListener.init();
consoleServiceListener = new ConsoleServiceListener(null, handlers);
consoleServiceListener.init();
doPageErrors();
});
}
function testPageErrors()
{
pageErrorListener.destroy();
pageErrorListener = null;
consoleServiceListener.destroy();
consoleServiceListener = null;
attachConsole(["PageError"], onAttach2);
}

View File

@ -0,0 +1,62 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for nsIConsoleMessages</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript;version=1.8" src="common.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Make sure that nsIConsoleMessages are logged. See bug 859756.</p>
<script class="testbody" type="text/javascript;version=1.8">
"use strict";
SimpleTest.waitForExplicitFinish();
let expectedMessages = [];
function startTest()
{
removeEventListener("load", startTest);
attachConsole(["PageError"], onAttach);
}
function onAttach(aState, aResponse)
{
onLogMessage = onLogMessage.bind(null, aState);
aState.dbgClient.addListener("logMessage", onLogMessage);
expectedMessages = [{
message: "hello world! bug859756",
timeStamp: /^\d+$/,
}];
Services.console.logStringMessage("hello world! bug859756");
info("waiting for messages");
}
let receivedMessages = [];
function onLogMessage(aState, aType, aPacket)
{
is(aPacket.from, aState.actor, "packet actor");
receivedMessages.push(aPacket);
if (receivedMessages.length != expectedMessages.length) {
return;
}
aState.dbgClient.removeListener("logMessage", onLogMessage);
checkObject(receivedMessages, expectedMessages);
closeDebugger(aState, () => SimpleTest.finish());
}
addEventListener("load", startTest);
</script>
</body>
</html>

View File

@ -9,29 +9,41 @@
#include "nsConsoleMessage.h"
#include "nsReadableUtils.h"
#include "jsapi.h"
NS_IMPL_THREADSAFE_ISUPPORTS1(nsConsoleMessage, nsIConsoleMessage)
nsConsoleMessage::nsConsoleMessage()
nsConsoleMessage::nsConsoleMessage()
: mMessage(),
mTimeStamp(0)
{
}
nsConsoleMessage::nsConsoleMessage(const PRUnichar *message)
nsConsoleMessage::nsConsoleMessage(const PRUnichar *message)
{
mMessage.Assign(message);
mTimeStamp = JS_Now() / 1000;
mMessage.Assign(message);
}
NS_IMETHODIMP
nsConsoleMessage::GetMessageMoz(PRUnichar **result) {
*result = ToNewUnicode(mMessage);
nsConsoleMessage::GetMessageMoz(PRUnichar **result)
{
*result = ToNewUnicode(mMessage);
return NS_OK;
return NS_OK;
}
// NS_IMETHODIMP
// nsConsoleMessage::Init(const PRUnichar *message) {
// nsAutoString newMessage(message);
// mMessage = ToNewUnicode(newMessage);
// return NS_OK;
// }
NS_IMETHODIMP
nsConsoleMessage::GetTimeStamp(int64_t *aTimeStamp)
{
*aTimeStamp = mTimeStamp;
return NS_OK;
}
NS_IMETHODIMP
nsConsoleMessage::ToString(nsACString& /*UTF8*/ aResult)
{
CopyUTF16toUTF8(mMessage, aResult);
return NS_OK;
}

View File

@ -22,6 +22,7 @@ public:
private:
~nsConsoleMessage() {}
int64_t mTimeStamp;
nsString mMessage;
};

View File

@ -10,13 +10,23 @@
* provide an object that can be qi'ed to provide more specific
* message information.
*/
[scriptable, uuid(41bd8784-1dd2-11b2-9553-8606958fffe1)]
[scriptable, uuid(c14c151b-5ea4-47ed-8e85-d392cdd3e154)]
interface nsIConsoleMessage : nsISupports
{
/**
* The time (in milliseconds from the Epoch) that the message instance
* was initialised.
* The timestamp is initialized as JS_now/1000 so that it can be
* compared to Date.now in Javascript.
*/
readonly attribute long long timeStamp;
[binaryname(MessageMoz)] readonly attribute wstring message;
AUTF8String toString();
};
%{ C++
#define NS_CONSOLEMESSAGE_CID \
{ 0x56c9d666, 0x1dd2, 0x11b2, { 0xb4, 0x3c, 0xa8, 0x4b, 0xf3, 0xb3, 0xec, 0xbb }}
{ 0x024efc9e, 0x54dc, 0x4844, { 0x80, 0x4b, 0x41, 0xd3, 0xf3, 0x69, 0x90, 0x73 }}
%}