diff --git a/browser/devtools/webconsole/HUDService-content.js b/browser/devtools/webconsole/HUDService-content.js
index 1648a388bf3..cbc046f6d11 100644
--- a/browser/devtools/webconsole/HUDService-content.js
+++ b/browser/devtools/webconsole/HUDService-content.js
@@ -499,9 +499,8 @@ let Manager = {
Manager = ConsoleAPIObserver = JSTerm = ConsoleListener = NetworkMonitor =
NetworkResponseListener = ConsoleProgressListener = null;
- Cc = Ci = Cu = XPCOMUtils = Services = gConsoleStorage =
- WebConsoleUtils = l10n = JSPropertyProvider = NetworkHelper =
- NetUtil = activityDistributor = null;
+ XPCOMUtils = gConsoleStorage = WebConsoleUtils = l10n = JSPropertyProvider =
+ NetworkHelper = NetUtil = activityDistributor = null;
},
};
@@ -1503,7 +1502,7 @@ NetworkResponseListener.prototype = {
*/
_findOpenResponse: function NRL__findOpenResponse()
{
- if (this._foundOpenResponse) {
+ if (!_alive || this._foundOpenResponse) {
return;
}
@@ -1611,7 +1610,9 @@ NetworkResponseListener.prototype = {
this.receivedData = "";
- NetworkMonitor.sendActivity(this.httpActivity);
+ if (_alive) {
+ NetworkMonitor.sendActivity(this.httpActivity);
+ }
this.httpActivity.channel = null;
this.httpActivity = null;
@@ -1745,7 +1746,7 @@ let NetworkMonitor = {
// NetworkResponseListener is responsible with updating the httpActivity
// object with the data from the new object in openResponses.
- if (aTopic != "http-on-examine-response" ||
+ if (!_alive || aTopic != "http-on-examine-response" ||
!(aSubject instanceof Ci.nsIHttpChannel)) {
return;
}
diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm
index eefdf987f56..2a7b97bca86 100644
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -173,6 +173,14 @@ const GROUP_INDENT = 12;
// The pref prefix for webconsole filters
const PREFS_PREFIX = "devtools.webconsole.filter.";
+// The number of messages to display in a single display update. If we display
+// too many messages at once we slow the Firefox UI too much.
+const MESSAGES_IN_INTERVAL = 30;
+
+// The delay between display updates - tells how often we should push new
+// messages to screen.
+const OUTPUT_INTERVAL = 90; // milliseconds
+
///////////////////////////////////////////////////////////////////////////
//// Helper for creating the network panel.
@@ -212,58 +220,21 @@ function createElement(aDocument, aTag, aAttributes)
* @param integer aCategory
* The category of message nodes to limit.
* @return number
- * The current user-selected log limit.
+ * The number of removed nodes.
*/
function pruneConsoleOutputIfNecessary(aHUDId, aCategory)
{
- // Get the log limit, either from the pref or from the constant.
- let logLimit;
- try {
- let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory];
- logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
- } catch (e) {
- logLimit = DEFAULT_LOG_LIMIT;
- }
-
let hudRef = HUDService.getHudReferenceById(aHUDId);
let outputNode = hudRef.outputNode;
+ let logLimit = hudRef.logLimitForCategory(aCategory);
- let scrollBox = outputNode.scrollBoxObject.element;
- let oldScrollHeight = scrollBox.scrollHeight;
- let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode);
-
- // Prune the nodes.
- let messageNodes = outputNode.querySelectorAll(".webconsole-msg-" +
+ let messageNodes = outputNode.getElementsByClassName("webconsole-msg-" +
CATEGORY_CLASS_FRAGMENTS[aCategory]);
- let removeNodes = messageNodes.length - logLimit;
- for (let i = 0; i < removeNodes; i++) {
- let node = messageNodes[i];
- if (node._evalCacheId && !node._panelOpen) {
- hudRef.jsterm.clearObjectCache(node._evalCacheId);
- }
+ let n = Math.max(0, messageNodes.length - logLimit);
+ let toRemove = Array.prototype.slice.call(messageNodes, 0, n);
+ toRemove.forEach(hudRef.removeOutputMessage, hudRef);
- if (node.classList.contains("webconsole-msg-cssparser")) {
- let desc = messageNodes[i].childNodes[2].textContent;
- let location = "";
- if (node.childNodes[4]) {
- location = node.childNodes[4].getAttribute("title");
- }
- delete hudRef.cssNodes[desc + location];
- }
- else if (node.classList.contains("webconsole-msg-inspector")) {
- hudRef.pruneConsoleDirNode(node);
- continue;
- }
-
- node.parentNode.removeChild(node);
- }
-
- if (!scrolledToBottom && removeNodes > 0 &&
- oldScrollHeight != scrollBox.scrollHeight) {
- scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight;
- }
-
- return logLimit;
+ return n;
}
///////////////////////////////////////////////////////////////////////////
@@ -470,11 +441,10 @@ HUD_SERVICE.prototype =
{
// Go through the nodes and adjust the placement of "webconsole-new-group"
// classes.
-
let nodes = aOutputNode.querySelectorAll(".hud-msg-node" +
":not(.hud-filtered-by-string):not(.hud-filtered-by-type)");
let lastTimestamp;
- for (let i = 0; i < nodes.length; i++) {
+ for (let i = 0, n = nodes.length; i < n; i++) {
let thisTimestamp = nodes[i].timestamp;
if (lastTimestamp != null &&
thisTimestamp >= lastTimestamp + NEW_GROUP_DELAY) {
@@ -565,9 +535,9 @@ HUD_SERVICE.prototype =
{
let outputNode = this.getHudReferenceById(aHUDId).outputNode;
- let nodes = outputNode.querySelectorAll(".hud-msg-node");
+ let nodes = outputNode.getElementsByClassName("hud-msg-node");
- for (let i = 0; i < nodes.length; ++i) {
+ for (let i = 0, n = nodes.length; i < n; ++i) {
let node = nodes[i];
// hide nodes that match the strings
@@ -1054,6 +1024,9 @@ function HeadsUpDisplay(aTab)
// create a panel dynamically and attach to the parentNode
this.createHUD();
+ this._outputQueue = [];
+ this._pruneCategoriesQueue = {};
+
// create the JSTerm input element
this.jsterm = new JSTerm(this);
this.jsterm.inputNode.focus();
@@ -1065,6 +1038,40 @@ function HeadsUpDisplay(aTab)
}
HeadsUpDisplay.prototype = {
+ /**
+ * Last time when we displayed any message in the output. Timestamp in
+ * milliseconds since the Unix epoch.
+ *
+ * @private
+ * @type number
+ */
+ _lastOutputFlush: 0,
+
+ /**
+ * The number of messages displayed in the last interval. The interval is
+ * given by OUTPUT_INTERVAL.
+ *
+ * @private
+ * @type number
+ */
+ _messagesDisplayedInInterval: 0,
+
+ /**
+ * Message nodes are stored here in a queue for later display.
+ *
+ * @private
+ * @type array
+ */
+ _outputQueue: null,
+
+ /**
+ * Keep track of the categories we need to prune from time to time.
+ *
+ * @private
+ * @type array
+ */
+ _pruneCategoriesQueue: null,
+
/**
* Message names that the HUD listens for. These messages come from the remote
* Web Console content script.
@@ -1394,10 +1401,6 @@ HeadsUpDisplay.prototype = {
return;
}
- // Turn off scrolling for the moment.
- ConsoleUtils.scroll = false;
- this.outputNode.hidden = true;
-
aRemoteMessages.forEach(function(aMessage) {
switch (aMessage._type) {
case "PageError":
@@ -1408,17 +1411,6 @@ HeadsUpDisplay.prototype = {
break;
}
}, this);
-
- this.outputNode.hidden = false;
- ConsoleUtils.scroll = true;
-
- // Scroll to bottom.
- let numChildren = this.outputNode.childNodes.length;
- if (numChildren && this.outputNode.clientHeight) {
- // We also check the clientHeight to force a reflow, otherwise
- // ensureIndexIsVisible() does not work after outputNode.hidden = false.
- this.outputNode.ensureIndexIsVisible(numChildren - 1);
- }
},
/**
@@ -2485,12 +2477,273 @@ HeadsUpDisplay.prototype = {
}, false);
},
+ /**
+ * Output a message node. This filters a node appropriately, then sends it to
+ * the output, regrouping and pruning output as necessary.
+ *
+ * Note: this call is async - the given message node may not be displayed when
+ * you call this method.
+ *
+ * @param nsIDOMNode aNode
+ * The message node to send to the output.
+ * @param nsIDOMNode [aNodeAfter]
+ * Insert the node after the given aNodeAfter (optional).
+ */
+ outputMessageNode: function HUD_outputMessageNode(aNode, aNodeAfter)
+ {
+ this._outputQueue.push([aNode, aNodeAfter]);
+ this._flushMessageQueue();
+ },
+
+ /**
+ * Try to flush the output message queue. This takes the messages in the
+ * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL.
+ * Further output is queued to happen later - see OUTPUT_INTERVAL.
+ *
+ * @private
+ */
+ _flushMessageQueue: function HUD__flushMessageQueue()
+ {
+ if ((Date.now() - this._lastOutputFlush) >= OUTPUT_INTERVAL) {
+ this._messagesDisplayedInInterval = 0;
+ }
+
+ // Determine how many messages we can display now.
+ let toDisplay = Math.min(this._outputQueue.length,
+ MESSAGES_IN_INTERVAL -
+ this._messagesDisplayedInInterval);
+
+ if (!toDisplay) {
+ if (!this._outputTimeout && this._outputQueue.length > 0) {
+ this._outputTimeout =
+ this.chromeWindow.setTimeout(function() {
+ delete this._outputTimeout;
+ this._flushMessageQueue();
+ }.bind(this), OUTPUT_INTERVAL);
+ }
+ return;
+ }
+
+ // Try to prune the message queue.
+ let shouldPrune = false;
+ if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) {
+ toDisplay = Math.min(this._outputQueue.length, toDisplay);
+ shouldPrune = true;
+ }
+
+ let batch = this._outputQueue.splice(0, toDisplay);
+ if (!batch.length) {
+ return;
+ }
+
+ let outputNode = this.outputNode;
+ let lastVisibleNode = null;
+ let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode);
+ let scrollBox = outputNode.scrollBoxObject.element;
+
+ let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
+
+ // Output the current batch of messages.
+ for (let item of batch) {
+ if (this._outputMessageFromQueue(hudIdSupportsString, item)) {
+ lastVisibleNode = item[0];
+ }
+ }
+
+ // Keep track of how many messages we displayed, so we do not display too
+ // many at once.
+ this._messagesDisplayedInInterval += batch.length;
+
+ let oldScrollHeight = 0;
+
+ // Prune messages if needed. We do not do this for every flush call to
+ // improve performance.
+ let removedNodes = 0;
+ if (shouldPrune || !(this._outputQueue.length % 20)) {
+ oldScrollHeight = scrollBox.scrollHeight;
+
+ let categories = Object.keys(this._pruneCategoriesQueue);
+ categories.forEach(function _pruneOutput(aCategory) {
+ removedNodes += pruneConsoleOutputIfNecessary(this.hudId, aCategory);
+ }, this);
+ this._pruneCategoriesQueue = {};
+ }
+
+ // Regroup messages at the end of the queue.
+ if (!this._outputQueue.length) {
+ HUDService.regroupOutput(outputNode);
+ }
+
+ let isInputOutput = lastVisibleNode &&
+ (lastVisibleNode.classList.contains("webconsole-msg-input") ||
+ lastVisibleNode.classList.contains("webconsole-msg-output"));
+
+ // Scroll to the new node if it is not filtered, and if the output node is
+ // scrolled at the bottom or if the new node is a jsterm input/output
+ // message.
+ if (lastVisibleNode && (scrolledToBottom || isInputOutput)) {
+ ConsoleUtils.scrollToVisible(lastVisibleNode);
+ }
+ else if (!scrolledToBottom && removedNodes > 0 &&
+ oldScrollHeight != scrollBox.scrollHeight) {
+ // If there were pruned messages and if scroll is not at the bottom, then
+ // we need to adjust the scroll location.
+ scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight;
+ }
+
+ // If the queue is not empty, schedule another flush.
+ if (!this._outputTimeout && this._outputQueue.length > 0) {
+ this._outputTimeout =
+ this.chromeWindow.setTimeout(function() {
+ delete this._outputTimeout;
+ this._flushMessageQueue();
+ }.bind(this), OUTPUT_INTERVAL);
+ }
+
+ this._lastOutputFlush = Date.now();
+ },
+
+ /**
+ * Output a message from the queue.
+ *
+ * @private
+ * @param nsISupportsString aHudIdSupportsString
+ * The HUD ID as an nsISupportsString.
+ * @param array aItem
+ * An item from the output queue - this item represents a message.
+ * @return boolean
+ * True if the message is visible, false otherwise.
+ */
+ _outputMessageFromQueue:
+ function HUD__outputMessageFromQueue(aHudIdSupportsString, aItem)
+ {
+ let [node, afterNode] = aItem;
+
+ let isFiltered = ConsoleUtils.filterMessageNode(node, this.hudId);
+
+ let isRepeated = false;
+ if (node.classList.contains("webconsole-msg-cssparser")) {
+ isRepeated = ConsoleUtils.filterRepeatedCSS(node, this.outputNode,
+ this.hudId);
+ }
+
+ if (!isRepeated &&
+ (node.classList.contains("webconsole-msg-console") ||
+ node.classList.contains("webconsole-msg-exception") ||
+ node.classList.contains("webconsole-msg-error"))) {
+ isRepeated = ConsoleUtils.filterRepeatedConsole(node, this.outputNode);
+ }
+
+ if (!isRepeated) {
+ this.outputNode.insertBefore(node,
+ afterNode ? afterNode.nextSibling : null);
+ this._pruneCategoriesQueue[node.category] = true;
+ }
+
+ let nodeID = node.getAttribute("id");
+ Services.obs.notifyObservers(aHudIdSupportsString,
+ "web-console-message-created", nodeID);
+
+ return !isRepeated && !isFiltered;
+ },
+
+ /**
+ * Prune the queue of messages to display. This avoids displaying messages
+ * that will be removed at the end of the queue anyway.
+ * @private
+ */
+ _pruneOutputQueue: function HUD__pruneOutputQueue()
+ {
+ let nodes = {};
+
+ // Group the messages per category.
+ this._outputQueue.forEach(function(aItem, aIndex) {
+ let [node] = aItem;
+ let category = node.category;
+ if (!(category in nodes)) {
+ nodes[category] = [];
+ }
+ nodes[category].push(aIndex);
+ }, this);
+
+ let pruned = 0;
+
+ // Loop through the categories we found and prune if needed.
+ for (let category in nodes) {
+ let limit = this.logLimitForCategory(category);
+ let indexes = nodes[category];
+ if (indexes.length > limit) {
+ let n = Math.max(0, indexes.length - limit);
+ pruned += n;
+ for (let i = n - 1; i >= 0; i--) {
+ let node = this._outputQueue[indexes[i]][0];
+ this._outputQueue.splice(indexes[i], 1);
+ }
+ }
+ }
+
+ return pruned;
+ },
+
+ /**
+ * Retrieve the limit of messages for a specific category.
+ *
+ * @param number aCategory
+ * The category of messages you want to retrieve the limit for. See the
+ * CATEGORY_* constants.
+ * @return number
+ * The number of messages allowed for the specific category.
+ */
+ logLimitForCategory: function HUD_logLimitForCategory(aCategory)
+ {
+ let logLimit = DEFAULT_LOG_LIMIT;
+
+ try {
+ let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory];
+ logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
+ logLimit = Math.max(logLimit, 1);
+ }
+ catch (e) { }
+
+ return logLimit;
+ },
+
+ /**
+ * Remove a given message from the output.
+ *
+ * @param nsIDOMNode aNode
+ * The message node you want to remove.
+ */
+ removeOutputMessage: function HUD_removeOutputMessage(aNode)
+ {
+ if (aNode._evalCacheId && !aNode._panelOpen) {
+ this.jsterm.clearObjectCache(aNode._evalCacheId);
+ }
+
+ if (aNode.classList.contains("webconsole-msg-cssparser")) {
+ let desc = aNode.childNodes[2].textContent;
+ let location = "";
+ if (aNode.childNodes[4]) {
+ location = aNode.childNodes[4].getAttribute("title");
+ }
+ delete this.cssNodes[desc + location];
+ }
+ else if (aNode.classList.contains("webconsole-msg-inspector")) {
+ this.pruneConsoleDirNode(aNode);
+ return;
+ }
+
+ aNode.parentNode.removeChild(aNode);
+ },
+
/**
* Destroy the HUD object. Call this method to avoid memory leaks when the Web
* Console is closed.
*/
destroy: function HUD_destroy()
{
+ this._outputQueue = [];
+
this.sendMessageToContent("WebConsole:Destroy", {});
this._messageListeners.forEach(function(aName) {
@@ -2533,6 +2786,8 @@ HeadsUpDisplay.prototype = {
delete this.messageManager;
delete this.browser;
delete this.chromeDocument;
+ delete this.chromeWindow;
+ delete this.outputNode;
this.positionMenuitems.above.removeEventListener("command",
this._positionConsoleAbove, false);
@@ -2592,7 +2847,7 @@ function JSTerm(aHud)
this.hudId = this.hud.hudId;
- this.lastCompletion = {};
+ this.lastCompletion = { value: null };
this.history = [];
this.historyIndex = 0;
this.historyPlaceHolder = 0; // this.history.length;
@@ -2890,17 +3145,7 @@ JSTerm.prototype = {
let outputNode = hud.outputNode;
let node;
while ((node = outputNode.firstChild)) {
- if (node._evalCacheId && !node._panelOpen) {
- this.clearObjectCache(node._evalCacheId);
- }
-
- if (node.classList &&
- node.classList.contains("webconsole-msg-inspector")) {
- hud.pruneConsoleDirNode(node);
- }
- else {
- outputNode.removeChild(node);
- }
+ hud.removeOutputMessage(node);
}
hud.HUDBox.lastTimestamp = 0;
@@ -3244,7 +3489,11 @@ JSTerm.prototype = {
input: this.inputNode.value,
};
- this.lastCompletion = {requestId: message.id, completionType: aType};
+ this.lastCompletion = {
+ requestId: message.id,
+ completionType: aType,
+ value: null,
+ };
let callback = this._receiveAutocompleteProperties.bind(this, aCallback);
this.hud.sendMessageToContent("JSTerm:Autocomplete", message, callback);
},
@@ -3336,7 +3585,7 @@ JSTerm.prototype = {
clearCompletion: function JSTF_clearCompletion()
{
this.autocompletePopup.clearItems();
- this.lastCompletion = {};
+ this.lastCompletion = { value: null };
this.updateCompleteNode("");
if (this.autocompletePopup.isOpen) {
this.autocompletePopup.hidePopup();
@@ -3389,7 +3638,10 @@ JSTerm.prototype = {
*/
clearObjectCache: function JST_clearObjectCache(aCacheId)
{
- this.hud.sendMessageToContent("JSTerm:ClearObjectCache", {cacheId: aCacheId});
+ if (this.hud) {
+ this.hud.sendMessageToContent("JSTerm:ClearObjectCache",
+ { cacheId: aCacheId });
+ }
},
/**
@@ -3887,13 +4139,18 @@ ConsoleUtils = {
* The newly-created message node.
* @param string aHUDId
* The ID of the HUD which this node is to be inserted into.
+ * @return boolean
+ * True if the message was filtered or false otherwise.
*/
filterMessageNode: function ConsoleUtils_filterMessageNode(aNode, aHUDId) {
+ let isFiltered = false;
+
// Filter by the message type.
let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity];
if (prefKey && !HUDService.getFilterState(aHUDId, prefKey)) {
// The node is filtered by type.
aNode.classList.add("hud-filtered-by-type");
+ isFiltered = true;
}
// Filter on the search string.
@@ -3903,7 +4160,10 @@ ConsoleUtils = {
// if string matches the filter text
if (!HUDService.stringMatchesFilters(text, search)) {
aNode.classList.add("hud-filtered-by-string");
+ isFiltered = true;
}
+
+ return isFiltered;
},
/**
@@ -4010,50 +4270,8 @@ ConsoleUtils = {
*/
outputMessageNode:
function ConsoleUtils_outputMessageNode(aNode, aHUDId, aNodeAfter) {
- ConsoleUtils.filterMessageNode(aNode, aHUDId);
- let outputNode = HUDService.hudReferences[aHUDId].outputNode;
-
- let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode);
-
- let isRepeated = false;
- if (aNode.classList.contains("webconsole-msg-cssparser")) {
- isRepeated = this.filterRepeatedCSS(aNode, outputNode, aHUDId);
- }
-
- if (!isRepeated &&
- (aNode.classList.contains("webconsole-msg-console") ||
- aNode.classList.contains("webconsole-msg-exception") ||
- aNode.classList.contains("webconsole-msg-error"))) {
- isRepeated = this.filterRepeatedConsole(aNode, outputNode);
- }
-
- if (!isRepeated) {
- outputNode.insertBefore(aNode, aNodeAfter ? aNodeAfter.nextSibling : null);
- }
-
- HUDService.regroupOutput(outputNode);
-
- if (pruneConsoleOutputIfNecessary(aHUDId, aNode.category) == 0) {
- // We can't very well scroll to make the message node visible if the log
- // limit is zero and the node was destroyed in the first place.
- return;
- }
-
- let isInputOutput = aNode.classList.contains("webconsole-msg-input") ||
- aNode.classList.contains("webconsole-msg-output");
- let isFiltered = aNode.classList.contains("hud-filtered-by-string") ||
- aNode.classList.contains("hud-filtered-by-type");
-
- // Scroll to the new node if it is not filtered, and if the output node is
- // scrolled at the bottom or if the new node is a jsterm input/output
- // message.
- if (!isFiltered && !isRepeated && (scrolledToBottom || isInputOutput)) {
- ConsoleUtils.scrollToVisible(aNode);
- }
-
- let id = WebConsoleUtils.supportsString(aHUDId);
- let nodeID = aNode.getAttribute("id");
- Services.obs.notifyObservers(id, "web-console-message-created", nodeID);
+ let hud = HUDService.getHudReferenceById(aHUDId);
+ hud.outputMessageNode(aNode, aNodeAfter);
},
/**
@@ -4083,6 +4301,10 @@ ConsoleUtils = {
HeadsUpDisplayUICommands = {
refreshCommand: function UIC_refreshCommand() {
var window = HUDService.currentContext();
+ if (!window) {
+ return;
+ }
+
let command = window.document.getElementById("Tools:WebConsole");
if (this.getOpenHUD() != null) {
command.setAttribute("checked", true);
diff --git a/browser/devtools/webconsole/WebConsoleUtils.jsm b/browser/devtools/webconsole/WebConsoleUtils.jsm
index d2bd321b792..d124ef2b854 100644
--- a/browser/devtools/webconsole/WebConsoleUtils.jsm
+++ b/browser/devtools/webconsole/WebConsoleUtils.jsm
@@ -888,6 +888,9 @@ function JSPropertyProvider(aScope, aInputValue)
matchProp = properties.pop().trimLeft();
for (let i = 0; i < properties.length; i++) {
let prop = properties[i].trim();
+ if (!prop) {
+ return null;
+ }
// If obj is undefined or null, then there is no chance to run completion
// on it. Exit here.
diff --git a/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js b/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js
index 328d3300639..c35cbd7d53a 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js
@@ -14,25 +14,32 @@ function test() {
}
function onLoad(aEvent) {
- browser.removeEventListener(aEvent.type, arguments.callee, true);
- openConsole();
-
- browser.addEventListener("load", testBasicNetLogging, true);
- content.location = TEST_NETWORK_URI;
-}
-
-function testBasicNetLogging(aEvent) {
- browser.removeEventListener(aEvent.type, arguments.callee, true);
-
- outputNode = HUDService.getHudByWindow(content).outputNode;
-
- executeSoon(function() {
- findLogEntry("test-network.html");
- findLogEntry("testscript.js");
- findLogEntry("test-image.png");
- findLogEntry("network console");
-
- finishTest();
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, function() {
+ browser.addEventListener("load", testBasicNetLogging, true);
+ content.location = TEST_NETWORK_URI;
+ });
+}
+
+function testBasicNetLogging(aEvent) {
+ browser.removeEventListener(aEvent.type, testBasicNetLogging, true);
+
+ outputNode = HUDService.getHudByWindow(content).outputNode;
+
+ waitForSuccess({
+ name: "network console message",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("running network console") > -1;
+ },
+ successFn: function()
+ {
+ findLogEntry("test-network.html");
+ findLogEntry("testscript.js");
+ findLogEntry("test-image.png");
+ finishTest();
+ },
+ failureFn: finishTest,
});
}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js
index 9d94721b55b..33fdc6e9e55 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js
@@ -18,32 +18,61 @@ function test() {
function testGroups(HUD) {
let jsterm = HUD.jsterm;
let outputNode = HUD.outputNode;
+ jsterm.clearOutput();
// We test for one group by testing for zero "new" groups. The
// "webconsole-new-group" class creates a divider. Thus one group is
// indicated by zero new groups, two groups are indicated by one new group,
// and so on.
+ let waitForSecondMessage = {
+ name: "second console message",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".webconsole-msg-output").length == 2;
+ },
+ successFn: function()
+ {
+ let timestamp1 = Date.now();
+ if (timestamp1 - timestamp0 < 5000) {
+ is(outputNode.querySelectorAll(".webconsole-new-group").length, 0,
+ "no group dividers exist after the second console message");
+ }
+
+ for (let i = 0; i < outputNode.itemCount; i++) {
+ outputNode.getItemAtIndex(i).timestamp = 0; // a "far past" value
+ }
+
+ jsterm.execute("2");
+ waitForSuccess(waitForThirdMessage);
+ },
+ failureFn: finishTest,
+ };
+
+ let waitForThirdMessage = {
+ name: "one group divider exists after the third console message",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".webconsole-new-group").length == 1;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ };
+
let timestamp0 = Date.now();
jsterm.execute("0");
- is(outputNode.querySelectorAll(".webconsole-new-group").length, 0,
- "no group dividers exist after the first console message");
- jsterm.execute("1");
- let timestamp1 = Date.now();
- if (timestamp1 - timestamp0 < 5000) {
- is(outputNode.querySelectorAll(".webconsole-new-group").length, 0,
- "no group dividers exist after the second console message");
- }
-
- for (let i = 0; i < outputNode.itemCount; i++) {
- outputNode.getItemAtIndex(i).timestamp = 0; // a "far past" value
- }
-
- jsterm.execute("2");
- is(outputNode.querySelectorAll(".webconsole-new-group").length, 1,
- "one group divider exists after the third console message");
-
- finishTest();
+ waitForSuccess({
+ name: "no group dividers exist after the first console message",
+ validatorFn: function()
+ {
+ return outputNode.querySelectorAll(".webconsole-new-group").length == 0;
+ },
+ successFn: function()
+ {
+ jsterm.execute("1");
+ waitForSuccess(waitForSecondMessage);
+ },
+ failureFn: finishTest,
+ });
}
-
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js
index b2ff1432b31..1f142ce741d 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js
@@ -96,21 +96,6 @@ function testGen() {
is(countMessageNodes(), 30, "there are 30 message nodes in the output " +
"when the log limit is set to 30");
- prefBranch.setIntPref("console", 0);
- console.log("baz");
-
- waitForSuccess({
- name: "clear output",
- validatorFn: function()
- {
- return countMessageNodes() == 0;
- },
- successFn: testNext,
- failureFn: finishTest,
- });
-
- yield;
-
prefBranch.clearUserPref("console");
hud = testDriver = prefBranch = console = outputNode = null;
finishTest();
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
index 55bddd93df4..504bdaf5589 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
@@ -23,18 +23,36 @@ function consoleOpened(aHud) {
hud.filterBox.value = "test message";
HUDService.updateFilterText(hud.filterBox);
- browser.addEventListener("load", tabReload, true);
+ let waitForNetwork = {
+ name: "network message",
+ validatorFn: function()
+ {
+ return hud.outputNode.querySelector(".webconsole-msg-network");
+ },
+ successFn: testScroll,
+ failureFn: finishTest,
+ };
- executeSoon(function() {
- content.location.reload();
+ waitForSuccess({
+ name: "console messages displayed",
+ validatorFn: function()
+ {
+ return hud.outputNode.textContent.indexOf("test message 199") > -1;
+ },
+ successFn: function()
+ {
+ browser.addEventListener("load", function onReload() {
+ browser.removeEventListener("load", onReload, true);
+ waitForSuccess(waitForNetwork);
+ }, true);
+ content.location.reload();
+ },
+ failureFn: finishTest,
});
}
-function tabReload(aEvent) {
- browser.removeEventListener(aEvent.type, tabReload, true);
-
+function testScroll() {
let msgNode = hud.outputNode.querySelector(".webconsole-msg-network");
- ok(msgNode, "found network message");
ok(msgNode.classList.contains("hud-filtered-by-type"),
"network message is filtered by type");
ok(msgNode.classList.contains("hud-filtered-by-string"),
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
index a76295b5092..978c1588031 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
@@ -22,24 +22,28 @@ let TestObserver = {
};
function tabLoad(aEvent) {
- browser.removeEventListener(aEvent.type, arguments.callee, true);
+ browser.removeEventListener(aEvent.type, tabLoad, true);
- openConsole();
-
- let hudId = HUDService.getHudIdByWindow(content);
- hud = HUDService.hudReferences[hudId];
-
- Services.obs.addObserver(TestObserver, "console-api-log-event", false);
- content.location.reload();
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ Services.obs.addObserver(TestObserver, "console-api-log-event", false);
+ content.location.reload();
+ });
}
function performTest() {
- isnot(hud.outputNode.textContent.indexOf("foobarBug613013"), -1,
- "console.log() message found");
-
Services.obs.removeObserver(TestObserver, "console-api-log-event");
TestObserver = null;
- finishTest();
+
+ waitForSuccess({
+ name: "console.log() message",
+ validatorFn: function()
+ {
+ return hud.outputNode.textContent.indexOf("foobarBug613013") > -1;
+ },
+ successFn: finishTest,
+ failureFn: finishTest,
+ });
}
function test() {
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
index dd209380665..279fc2967a0 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
@@ -17,6 +17,8 @@ function consoleOpened(hud) {
content.console.log("test message " + i);
}
+ let oldScrollTop = -1;
+
waitForSuccess({
name: "console.log messages displayed",
validatorFn: function()
@@ -25,11 +27,24 @@ function consoleOpened(hud) {
},
successFn: function()
{
- let oldScrollTop = boxObject.scrollTop;
+ oldScrollTop = boxObject.scrollTop;
ok(oldScrollTop > 0, "scroll location is not at the top");
hud.jsterm.execute("'hello world'");
+ waitForSuccess(waitForExecute);
+ },
+ failureFn: finishTest,
+ });
+
+ let waitForExecute = {
+ name: "jsterm output displayed",
+ validatorFn: function()
+ {
+ return outputNode.querySelector(".webconsole-msg-output");
+ },
+ successFn: function()
+ {
isnot(boxObject.scrollTop, oldScrollTop, "scroll location updated");
oldScrollTop = boxObject.scrollTop;
@@ -40,7 +55,7 @@ function consoleOpened(hud) {
finishTest();
},
failureFn: finishTest,
- });
+ };
}
function test() {
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js b/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js
index 017b3f90151..cf3d2e3010e 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js
@@ -56,14 +56,15 @@ function testWebDevLimits2() {
}
waitForSuccess({
- name: "11 console.log messages displayed",
+ name: "10 console.log messages displayed and one pruned",
validatorFn: function()
{
- return outputNode.textContent.indexOf("test message 10") > -1;
+ let message0 = outputNode.textContent.indexOf("test message 0");
+ let message10 = outputNode.textContent.indexOf("test message 10");
+ return message0 == -1 && message10 > -1;
},
successFn: function()
{
- testLogEntry(outputNode, "test message 0", "first message is pruned", false, true);
findLogEntry("test message 1");
// Check if the sentinel entry is still there.
findLogEntry("bar is not defined");
@@ -161,14 +162,28 @@ function loadImage() {
gCounter++;
return;
}
- is(gCounter, 11, "loaded 11 files");
- testLogEntry(outputNode, "test-image.png?_fubar=0", "first message is pruned", false, true);
- findLogEntry("test-image.png?_fubar=1");
- // Check if the sentinel entry is still there.
- findLogEntry("testing Net limits");
- Services.prefs.setIntPref("devtools.hud.loglimit.network", gOldPref);
- testCssLimits();
+ is(gCounter, 11, "loaded 11 files");
+
+ waitForSuccess({
+ name: "loaded 11 files, one message pruned",
+ validatorFn: function()
+ {
+ let message0 = outputNode.querySelector('*[value*="test-image.png?_fubar=0"]');
+ let message10 = outputNode.querySelector('*[value*="test-image.png?_fubar=10"]');
+ return !message0 && message10;
+ },
+ successFn: function()
+ {
+ findLogEntry("test-image.png?_fubar=1");
+ // Check if the sentinel entry is still there.
+ findLogEntry("testing Net limits");
+
+ Services.prefs.setIntPref("devtools.hud.loglimit.network", gOldPref);
+ testCssLimits();
+ },
+ failureFn: testCssLimits,
+ });
}
function testCssLimits() {
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js b/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
index 432e55cbecc..89841ce8454 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
@@ -16,29 +16,38 @@ function test() {
}
function onLoad(aEvent) {
- browser.removeEventListener(aEvent.type, arguments.callee, true);
- openConsole();
- hudId = HUDService.getHudIdByWindow(content);
-
- browser.addEventListener("load", testConsoleFileLocation, true);
- content.location = TEST_URI;
-}
-
-function testConsoleFileLocation(aEvent) {
- browser.removeEventListener(aEvent.type, arguments.callee, true);
-
- outputNode = HUDService.hudReferences[hudId].outputNode;
-
- executeSoon(function() {
- findLogEntry("test-file-location.js");
- findLogEntry("message for level");
- findLogEntry("test-file-location.js:5");
- findLogEntry("test-file-location.js:6");
- findLogEntry("test-file-location.js:7");
- findLogEntry("test-file-location.js:8");
- findLogEntry("test-file-location.js:9");
-
- finishTest();
+ browser.removeEventListener(aEvent.type, onLoad, true);
+ openConsole(null, function(aHud) {
+ hud = aHud;
+ browser.addEventListener("load", testConsoleFileLocation, true);
+ content.location = TEST_URI;
+ });
+}
+
+function testConsoleFileLocation(aEvent) {
+ browser.removeEventListener(aEvent.type, testConsoleFileLocation, true);
+
+ outputNode = hud.outputNode;
+
+ waitForSuccess({
+ name: "console API messages",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("message for level debug") > -1;
+ },
+ successFn: function()
+ {
+ findLogEntry("test-file-location.js");
+ findLogEntry("message for level");
+ findLogEntry("test-file-location.js:5");
+ findLogEntry("test-file-location.js:6");
+ findLogEntry("test-file-location.js:7");
+ findLogEntry("test-file-location.js:8");
+ findLogEntry("test-file-location.js:9");
+
+ finishTest();
+ },
+ failureFn: finishTest,
});
}
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
index 7dd6e766340..fac1f38960b 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
@@ -18,18 +18,25 @@ function test() {
function consoleOpened(hud) {
outputNode = hud.outputNode;
- executeSoon(function() {
- findLogEntry("aTimer: timer started");
- findLogEntry("ms");
-
- // The next test makes sure that timers with the same name but in separate
- // tabs, do not contain the same value.
- addTab("data:text/html;charset=utf-8,");
- browser.addEventListener("load", function onLoad() {
- browser.removeEventListener("load", onLoad, true);
- openConsole(null, testTimerIndependenceInTabs);
- }, true);
+ waitForSuccess({
+ name: "aTimer started",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("aTimer: timer started") > -1;
+ },
+ successFn: function()
+ {
+ findLogEntry("ms");
+ // The next test makes sure that timers with the same name but in separate
+ // tabs, do not contain the same value.
+ addTab("data:text/html;charset=utf-8,");
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ openConsole(null, testTimerIndependenceInTabs);
+ }, true);
+ },
+ failureFn: finishTest,
});
}
@@ -56,22 +63,30 @@ function testTimerIndependenceInSameTab() {
let hud = HUDService.hudReferences[hudId];
outputNode = hud.outputNode;
- executeSoon(function() {
- findLogEntry("bTimer: timer started");
- hud.jsterm.clearOutput();
+ waitForSuccess({
+ name: "bTimer started",
+ validatorFn: function()
+ {
+ return outputNode.textContent.indexOf("bTimer: timer started") > -1;
+ },
+ successFn: function() {
+ hud.jsterm.clearOutput();
- // Now the following console.timeEnd() call shouldn't display anything,
- // if the timers in different pages are not related.
- browser.addEventListener("load", function onLoad() {
- browser.removeEventListener("load", onLoad, true);
- executeSoon(testTimerIndependenceInSameTabAgain);
- }, true);
- content.location = "data:text/html;charset=utf-8,";
+ // Now the following console.timeEnd() call shouldn't display anything,
+ // if the timers in different pages are not related.
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ executeSoon(testTimerIndependenceInSameTabAgain);
+ }, true);
+ content.location = "data:text/html;charset=utf-8," +
+ "";
+ },
+ failureFn: finishTest,
});
}
-function testTimerIndependenceInSameTabAgain(hud) {
+function testTimerIndependenceInSameTabAgain() {
let hudId = HUDService.getHudIdByWindow(content);
let hud = HUDService.hudReferences[hudId];
outputNode = hud.outputNode;
diff --git a/dom/base/ConsoleAPI.js b/dom/base/ConsoleAPI.js
index 5579d660dbe..867229d339e 100644
--- a/dom/base/ConsoleAPI.js
+++ b/dom/base/ConsoleAPI.js
@@ -7,13 +7,28 @@
let Cu = Components.utils;
let Ci = Components.interfaces;
let Cc = Components.classes;
+
// The maximum allowed number of concurrent timers per page.
const MAX_PAGE_TIMERS = 10000;
+// The regular expression used to parse %s/%d and other placeholders for
+// variables in strings that need to be interpolated.
+const ARGUMENT_PATTERN = /%\d*\.?\d*([osdif])\b/g;
+
+// The maximum stacktrace depth when populating the stacktrace array used for
+// console.trace().
+const DEFAULT_MAX_STACKTRACE_DEPTH = 200;
+
+// The console API methods are async and their action is executed later. This
+// delay tells how much later.
+const CALL_DELAY = 30; // milliseconds
+
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm");
+let nsITimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
function ConsoleAPI() {}
ConsoleAPI.prototype = {
@@ -21,11 +36,17 @@ ConsoleAPI.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]),
+ _timerInitialized: false,
+ _queuedCalls: null,
+ _timerCallback: null,
+ _destroyedWindows: null,
+
// nsIDOMGlobalPropertyInitializer
init: function CA_init(aWindow) {
Services.obs.addObserver(this, "xpcom-shutdown", false);
Services.obs.addObserver(this, "inner-window-destroyed", false);
+
let outerID;
let innerID;
try {
@@ -39,45 +60,50 @@ ConsoleAPI.prototype = {
Cu.reportError(ex);
}
+ let meta = {
+ outerID: outerID,
+ innerID: innerID,
+ };
+
let self = this;
let chromeObject = {
// window.console API
log: function CA_log() {
- self.notifyObservers(outerID, innerID, "log", self.processArguments(arguments));
+ self.queueCall("log", arguments, meta);
},
info: function CA_info() {
- self.notifyObservers(outerID, innerID, "info", self.processArguments(arguments));
+ self.queueCall("info", arguments, meta);
},
warn: function CA_warn() {
- self.notifyObservers(outerID, innerID, "warn", self.processArguments(arguments));
+ self.queueCall("warn", arguments, meta);
},
error: function CA_error() {
- self.notifyObservers(outerID, innerID, "error", self.processArguments(arguments));
+ self.queueCall("error", arguments, meta);
},
debug: function CA_debug() {
- self.notifyObservers(outerID, innerID, "log", self.processArguments(arguments));
+ self.queueCall("debug", arguments, meta);
},
trace: function CA_trace() {
- self.notifyObservers(outerID, innerID, "trace", self.getStackTrace());
+ self.queueCall("trace", arguments, meta);
},
// Displays an interactive listing of all the properties of an object.
dir: function CA_dir() {
- self.notifyObservers(outerID, innerID, "dir", arguments);
+ self.queueCall("dir", arguments, meta);
},
group: function CA_group() {
- self.notifyObservers(outerID, innerID, "group", self.beginGroup(arguments));
+ self.queueCall("group", arguments, meta);
},
groupCollapsed: function CA_groupCollapsed() {
- self.notifyObservers(outerID, innerID, "groupCollapsed", self.beginGroup(arguments));
+ self.queueCall("groupCollapsed", arguments, meta);
},
groupEnd: function CA_groupEnd() {
- self.notifyObservers(outerID, innerID, "groupEnd", arguments);
+ self.queueCall("groupEnd", arguments, meta);
},
time: function CA_time() {
- self.notifyObservers(outerID, innerID, "time", self.startTimer(innerID, arguments[0]));
+ self.queueCall("time", arguments, meta);
},
timeEnd: function CA_timeEnd() {
- self.notifyObservers(outerID, innerID, "timeEnd", self.stopTimer(innerID, arguments[0]));
+ self.queueCall("timeEnd", arguments, meta);
},
__exposedProps__: {
log: "r",
@@ -123,6 +149,12 @@ ConsoleAPI.prototype = {
Object.defineProperties(contentObj, properties);
Cu.makeObjectPropsNormal(contentObj);
+ this._queuedCalls = [];
+ this._destroyedWindows = [];
+ this._timerCallback = {
+ notify: this._timerCallbackNotify.bind(this),
+ };
+
return contentObj;
},
@@ -131,51 +163,144 @@ ConsoleAPI.prototype = {
if (aTopic == "xpcom-shutdown") {
Services.obs.removeObserver(this, "xpcom-shutdown");
Services.obs.removeObserver(this, "inner-window-destroyed");
+ this._destroyedWindows = [];
+ this._queuedCalls = [];
}
else if (aTopic == "inner-window-destroyed") {
let innerWindowID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
delete this.timerRegistry[innerWindowID + ""];
+ this._destroyedWindows.push(innerWindowID);
}
},
+ /**
+ * Queue a call to a console method. See the CALL_DELAY constant.
+ *
+ * @param string aMethod
+ * The console method the code has invoked.
+ * @param object aArguments
+ * The arguments passed to the console method.
+ * @param object aMeta
+ * The associated call meta information. This needs to hold the inner
+ * and outer window IDs from where the console method was called.
+ */
+ queueCall: function CA_queueCall(aMethod, aArguments, aMeta)
+ {
+ let metaForCall = {
+ outerID: aMeta.outerID,
+ innerID: aMeta.innerID,
+ timeStamp: Date.now(),
+ stack: this.getStackTrace(aMethod != "trace" ? 1 : null),
+ };
+
+ this._queuedCalls.push([aMethod, aArguments, metaForCall]);
+
+ if (!this._timerInitialized) {
+ nsITimer.initWithCallback(this._timerCallback, CALL_DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ this._timerInitialized = true;
+ }
+ },
+
+ /**
+ * Timer callback used to process each of the queued calls.
+ * @private
+ */
+ _timerCallbackNotify: function CA__timerCallbackNotify()
+ {
+ this._timerInitialized = false;
+ this._queuedCalls.splice(0).forEach(this._processQueuedCall, this);
+ this._destroyedWindows = [];
+ },
+
+ /**
+ * Process a queued call to a console method.
+ *
+ * @private
+ * @param array aCall
+ * Array that holds information about the queued call.
+ */
+ _processQueuedCall: function CA__processQueuedItem(aCall)
+ {
+ let [method, args, meta] = aCall;
+
+ let notifyMeta = {
+ outerID: meta.outerID,
+ innerID: meta.innerID,
+ timeStamp: meta.timeStamp,
+ frame: meta.stack[0],
+ };
+
+ let notifyArguments = null;
+
+ switch (method) {
+ case "log":
+ case "info":
+ case "warn":
+ case "error":
+ case "debug":
+ notifyArguments = this.processArguments(args);
+ break;
+ case "trace":
+ notifyArguments = meta.stack;
+ break;
+ case "group":
+ case "groupCollapsed":
+ notifyArguments = this.beginGroup(args);
+ break;
+ case "groupEnd":
+ case "dir":
+ notifyArguments = args;
+ break;
+ case "time":
+ notifyArguments = this.startTimer(meta.innerID, args[0], meta.timeStamp);
+ break;
+ case "timeEnd":
+ notifyArguments = this.stopTimer(meta.innerID, args[0], meta.timeStamp);
+ break;
+ default:
+ // unknown console API method!
+ return;
+ }
+
+ this.notifyObservers(method, notifyArguments, notifyMeta);
+ },
+
/**
* Notify all observers of any console API call.
*
- * @param number aOuterWindowID
- * The outer window ID from where the message came from.
- * @param number aInnerWindowID
- * The inner window ID from where the message came from.
* @param string aLevel
* The message level.
* @param mixed aArguments
* The arguments given to the console API call.
- **/
- notifyObservers:
- function CA_notifyObservers(aOuterWindowID, aInnerWindowID, aLevel, aArguments) {
- if (!aOuterWindowID) {
- return;
- }
-
- let stack = this.getStackTrace();
- // Skip the first frame since it contains an internal call.
- let frame = stack[1];
+ * @param object aMeta
+ * Object that holds metadata about the console API call:
+ * - outerID - the outer ID of the window where the message came from.
+ * - innerID - the inner ID of the window where the message came from.
+ * - frame - the youngest content frame in the call stack.
+ * - timeStamp - when the console API call occurred.
+ */
+ notifyObservers: function CA_notifyObservers(aLevel, aArguments, aMeta) {
let consoleEvent = {
- ID: aOuterWindowID,
- innerID: aInnerWindowID,
+ ID: aMeta.outerID,
+ innerID: aMeta.innerID,
level: aLevel,
- filename: frame.filename,
- lineNumber: frame.lineNumber,
- functionName: frame.functionName,
+ filename: aMeta.frame.filename,
+ lineNumber: aMeta.frame.lineNumber,
+ functionName: aMeta.frame.functionName,
arguments: aArguments,
- timeStamp: Date.now(),
+ timeStamp: aMeta.timeStamp,
};
consoleEvent.wrappedJSObject = consoleEvent;
- ConsoleAPIStorage.recordEvent(aInnerWindowID, consoleEvent);
+ // Store messages for which the inner window was not destroyed.
+ if (this._destroyedWindows.indexOf(aMeta.innerID) == -1) {
+ ConsoleAPIStorage.recordEvent(aMeta.innerID, consoleEvent);
+ }
- Services.obs.notifyObservers(consoleEvent,
- "console-api-log-event", aOuterWindowID);
+ Services.obs.notifyObservers(consoleEvent, "console-api-log-event",
+ aMeta.outerID);
},
/**
@@ -189,18 +314,14 @@ ConsoleAPI.prototype = {
* The arguments given to the console API call.
**/
processArguments: function CA_processArguments(aArguments) {
- if (aArguments.length < 2) {
+ if (aArguments.length < 2 || typeof aArguments[0] != "string") {
return aArguments;
}
let args = Array.prototype.slice.call(aArguments);
let format = args.shift();
- if (typeof format != "string") {
- return aArguments;
- }
// Format specification regular expression.
- let pattern = /%(\d*).?(\d*)[a-zA-Z]/g;
- let processed = format.replace(pattern, function CA_PA_substitute(spec) {
- switch (spec[spec.length-1]) {
+ let processed = format.replace(ARGUMENT_PATTERN, function CA_PA_substitute(match, submatch) {
+ switch (submatch) {
case "o":
case "s":
return String(args.shift());
@@ -210,7 +331,7 @@ ConsoleAPI.prototype = {
case "f":
return parseFloat(args.shift());
default:
- return spec;
+ return submatch;
};
});
args.unshift(processed);
@@ -220,13 +341,19 @@ ConsoleAPI.prototype = {
/**
* Build the stacktrace array for the console.trace() call.
*
+ * @param number [aMaxDepth=DEFAULT_MAX_STACKTRACE_DEPTH]
+ * Optional maximum stacktrace depth.
* @return array
* Each element is a stack frame that holds the following properties:
* filename, lineNumber, functionName and language.
- **/
- getStackTrace: function CA_getStackTrace() {
+ */
+ getStackTrace: function CA_getStackTrace(aMaxDepth) {
+ if (!aMaxDepth) {
+ aMaxDepth = DEFAULT_MAX_STACKTRACE_DEPTH;
+ }
+
let stack = [];
- let frame = Components.stack.caller;
+ let frame = Components.stack.caller.caller;
while (frame = frame.caller) {
if (frame.language == Ci.nsIProgrammingLanguage.JAVASCRIPT ||
frame.language == Ci.nsIProgrammingLanguage.JAVASCRIPT2) {
@@ -236,6 +363,9 @@ ConsoleAPI.prototype = {
functionName: frame.name,
language: frame.language,
});
+ if (stack.length == aMaxDepth) {
+ break;
+ }
}
}
@@ -265,13 +395,15 @@ ConsoleAPI.prototype = {
* The inner ID of the window.
* @param string aName
* The name of the timer.
+ * @param number [aTimestamp=Date.now()]
+ * Optional timestamp that tells when the timer was originally started.
* @return object
* The name property holds the timer name and the started property
* holds the time the timer was started. In case of error, it returns
* an object with the single property "error" that contains the key
* for retrieving the localized error message.
**/
- startTimer: function CA_startTimer(aWindowId, aName) {
+ startTimer: function CA_startTimer(aWindowId, aName, aTimestamp) {
if (!aName) {
return;
}
@@ -285,7 +417,7 @@ ConsoleAPI.prototype = {
}
let key = aWindowId + "-" + aName.toString();
if (!pageTimers[key]) {
- pageTimers[key] = Date.now();
+ pageTimers[key] = aTimestamp || Date.now();
}
return { name: aName, started: pageTimers[key] };
},
@@ -297,11 +429,13 @@ ConsoleAPI.prototype = {
* The inner ID of the window.
* @param string aName
* The name of the timer.
+ * @param number [aTimestamp=Date.now()]
+ * Optional timestamp that tells when the timer was originally stopped.
* @return object
* The name property holds the timer name and the duration property
* holds the number of milliseconds since the timer was started.
**/
- stopTimer: function CA_stopTimer(aWindowId, aName) {
+ stopTimer: function CA_stopTimer(aWindowId, aName, aTimestamp) {
if (!aName) {
return;
}
@@ -314,7 +448,7 @@ ConsoleAPI.prototype = {
if (!pageTimers[key]) {
return;
}
- let duration = Date.now() - pageTimers[key];
+ let duration = (aTimestamp || Date.now()) - pageTimers[key];
delete pageTimers[key];
return { name: aName, duration: duration };
}
diff --git a/dom/tests/browser/browser_ConsoleAPITests.js b/dom/tests/browser/browser_ConsoleAPITests.js
index e24b708d146..bfd32aa5c3b 100644
--- a/dom/tests/browser/browser_ConsoleAPITests.js
+++ b/dom/tests/browser/browser_ConsoleAPITests.js
@@ -5,7 +5,7 @@
const TEST_URI = "http://example.com/browser/dom/tests/browser/test-console-api.html";
-var gWindow, gLevel, gArgs;
+var gWindow, gLevel, gArgs, gTestDriver;
function test() {
waitForExplicitFinish();
@@ -15,6 +15,7 @@ function test() {
var browser = gBrowser.selectedBrowser;
registerCleanupFunction(function () {
+ gWindow = gLevel = gArgs = gTestDriver = null;
gBrowser.removeTab(tab);
});
@@ -25,7 +26,8 @@ function test() {
executeSoon(function test_executeSoon() {
gWindow = browser.contentWindow;
consoleAPISanityTest();
- observeConsoleTest();
+ gTestDriver = observeConsoleTest();
+ gTestDriver.next();
});
}, false);
@@ -42,9 +44,6 @@ function testConsoleData(aMessageObject) {
if (gLevel == "trace") {
is(aMessageObject.arguments.toSource(), gArgs.toSource(),
"stack trace is correct");
-
- // Now test the location information in console.log()
- startLocationTest();
}
else {
gArgs.forEach(function (a, i) {
@@ -52,10 +51,7 @@ function testConsoleData(aMessageObject) {
});
}
- if (aMessageObject.level == "error") {
- // Now test console.trace()
- startTraceTest();
- }
+ gTestDriver.next();
}
function testLocationData(aMessageObject) {
@@ -163,9 +159,11 @@ function observeConsoleTest() {
let win = XPCNativeWrapper.unwrap(gWindow);
expect("log", "arg");
win.console.log("arg");
+ yield;
expect("info", "arg", "extra arg");
win.console.info("arg", "extra arg");
+ yield;
// We don't currently support width and precision qualifiers, but we don't
// choke on them either.
@@ -174,29 +172,49 @@ function observeConsoleTest() {
1,
"PI",
3.14159);
+ yield;
+
expect("log", "%d, %s, %l");
win.console.log("%d, %s, %l");
+ yield;
+
expect("log", "%a %b %c");
win.console.log("%a %b %c");
+ yield;
+
expect("log", "%a %b %c", "a", "b");
win.console.log("%a %b %c", "a", "b");
+ yield;
+
expect("log", "2, a, %l", 3);
win.console.log("%d, %s, %l", 2, "a", 3);
+ yield;
// Bug #692550 handle null and undefined.
expect("log", "null, undefined");
win.console.log("%s, %s", null, undefined);
+ yield;
// Bug #696288 handle object as first argument.
let obj = { a: 1 };
expect("log", obj, "a");
win.console.log(obj, "a");
+ yield;
expect("dir", win.toString());
win.console.dir(win);
+ yield;
expect("error", "arg");
win.console.error("arg");
+
+ yield;
+
+ startTraceTest();
+ yield;
+
+ startLocationTest();
+ yield;
}
function consoleAPISanityTest() {
diff --git a/dom/tests/browser/browser_ConsoleStoragePBTest.js b/dom/tests/browser/browser_ConsoleStoragePBTest.js
index 2a44a09d501..4da2b8940dc 100644
--- a/dom/tests/browser/browser_ConsoleStoragePBTest.js
+++ b/dom/tests/browser/browser_ConsoleStoragePBTest.js
@@ -14,24 +14,44 @@ function test() {
var CSS = {};
Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", CSS);
- function checkStorageOccurs(shouldOccur) {
+ let innerID, beforeEvents, storageShouldOccur;
+
+ var ConsoleObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function CO_observe(aSubject, aTopic, aData)
+ {
+ if (aTopic != "console-api-log-event") {
+ return;
+ }
+
+ let afterEvents = CSS.ConsoleAPIStorage.getEvents(innerID);
+
+ is(beforeEvents.length == afterEvents.length - 1,
+ storageShouldOccur,
+ "storage should" + (storageShouldOccur ? "" : " not") + " occur");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(ConsoleObserver, "console-api-log-event");
+ pb.privateBrowsingEnabled = storageShouldOccur;
+ });
+ }
+ };
+
+ function checkStorageOccurs() {
+ Services.obs.addObserver(ConsoleObserver, "console-api-log-event", false);
+
let win = XPCNativeWrapper.unwrap(browser.contentWindow);
- let innerID = getInnerWindowId(win);
+ innerID = getInnerWindowId(win);
- let beforeEvents = CSS.ConsoleAPIStorage.getEvents(innerID);
- win.console.log("foo bar baz (private: " + !shouldOccur + ")");
-
- let afterEvents = CSS.ConsoleAPIStorage.getEvents(innerID);
-
- is(beforeEvents.length == afterEvents.length - 1,
- shouldOccur, "storage should" + (shouldOccur ? "" : "n't") + " occur");
+ beforeEvents = CSS.ConsoleAPIStorage.getEvents(innerID);
+ win.console.log("foo bar baz (private: " + !storageShouldOccur + ")");
}
function pbObserver(aSubject, aTopic, aData) {
if (aData == "enter") {
- checkStorageOccurs(false);
-
- executeSoon(function () { pb.privateBrowsingEnabled = false; });
+ storageShouldOccur = false;
+ checkStorageOccurs();
} else if (aData == "exit") {
executeSoon(finish);
}
@@ -61,9 +81,8 @@ function test() {
browser.removeEventListener("DOMContentLoaded", onLoad, false);
- checkStorageOccurs(true);
-
- pb.privateBrowsingEnabled = true;
+ storageShouldOccur = true;
+ checkStorageOccurs();
}, false);
}