diff options
Diffstat (limited to 'devtools/server/actors/utils/webconsole-utils.js')
-rw-r--r-- | devtools/server/actors/utils/webconsole-utils.js | 1063 |
1 files changed, 1063 insertions, 0 deletions
diff --git a/devtools/server/actors/utils/webconsole-utils.js b/devtools/server/actors/utils/webconsole-utils.js new file mode 100644 index 000000000..597f1ddb3 --- /dev/null +++ b/devtools/server/actors/utils/webconsole-utils.js @@ -0,0 +1,1063 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft= javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Cc, Ci, Cu, components} = require("chrome"); +const {isWindowIncluded} = require("devtools/shared/layout/utils"); +const Services = require("Services"); +const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); + +// TODO: Bug 842672 - browser/ imports modules from toolkit/. +// Note that these are only used in WebConsoleCommands, see $0 and pprint(). +loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager"); + +const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [ + "SharedWorker", + "ServiceWorker", + "Worker" +]; + +var WebConsoleUtils = { + + /** + * Given a message, return one of CONSOLE_WORKER_IDS if it matches + * one of those. + * + * @return string + */ + getWorkerType: function (message) { + let id = message ? message.innerID : null; + return CONSOLE_WORKER_IDS[CONSOLE_WORKER_IDS.indexOf(id)] || null; + }, + + /** + * Clone an object. + * + * @param object object + * The object you want cloned. + * @param boolean recursive + * Tells if you want to dig deeper into the object, to clone + * recursively. + * @param function [filter] + * Optional, filter function, called for every property. Three + * arguments are passed: key, value and object. Return true if the + * property should be added to the cloned object. Return false to skip + * the property. + * @return object + * The cloned object. + */ + cloneObject: function (object, recursive, filter) { + if (typeof object != "object") { + return object; + } + + let temp; + + if (Array.isArray(object)) { + temp = []; + Array.forEach(object, function (value, index) { + if (!filter || filter(index, value, object)) { + temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value); + } + }); + } else { + temp = {}; + for (let key in object) { + let value = object[key]; + if (object.hasOwnProperty(key) && + (!filter || filter(key, value, object))) { + temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value; + } + } + } + + return temp; + }, + + /** + * Gets the ID of the inner window of this DOM window. + * + * @param nsIDOMWindow window + * @return integer + * Inner ID for the given window. + */ + getInnerWindowId: function (window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; + }, + + /** + * Recursively gather a list of inner window ids given a + * top level window. + * + * @param nsIDOMWindow window + * @return Array + * list of inner window ids. + */ + getInnerWindowIDsForFrames: function (window) { + let innerWindowID = this.getInnerWindowId(window); + let ids = [innerWindowID]; + + if (window.frames) { + for (let i = 0; i < window.frames.length; i++) { + let frame = window.frames[i]; + ids = ids.concat(this.getInnerWindowIDsForFrames(frame)); + } + } + + return ids; + }, + + /** + * Get the property descriptor for the given object. + * + * @param object object + * The object that contains the property. + * @param string prop + * The property you want to get the descriptor for. + * @return object + * Property descriptor. + */ + getPropertyDescriptor: function (object, prop) { + let desc = null; + while (object) { + try { + if ((desc = Object.getOwnPropertyDescriptor(object, prop))) { + break; + } + } catch (ex) { + // Native getters throw here. See bug 520882. + // null throws TypeError. + if (ex.name != "NS_ERROR_XPC_BAD_CONVERT_JS" && + ex.name != "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO" && + ex.name != "TypeError") { + throw ex; + } + } + + try { + object = Object.getPrototypeOf(object); + } catch (ex) { + if (ex.name == "TypeError") { + return desc; + } + throw ex; + } + } + return desc; + }, + + /** + * Create a grip for the given value. If the value is an object, + * an object wrapper will be created. + * + * @param mixed value + * The value you want to create a grip for, before sending it to the + * client. + * @param function objectWrapper + * If the value is an object then the objectWrapper function is + * invoked to give us an object grip. See this.getObjectGrip(). + * @return mixed + * The value grip. + */ + createValueGrip: function (value, objectWrapper) { + switch (typeof value) { + case "boolean": + return value; + case "string": + return objectWrapper(value); + case "number": + if (value === Infinity) { + return { type: "Infinity" }; + } else if (value === -Infinity) { + return { type: "-Infinity" }; + } else if (Number.isNaN(value)) { + return { type: "NaN" }; + } else if (!value && 1 / value === -Infinity) { + return { type: "-0" }; + } + return value; + case "undefined": + return { type: "undefined" }; + case "object": + if (value === null) { + return { type: "null" }; + } + // Fall through. + case "function": + return objectWrapper(value); + default: + console.error("Failed to provide a grip for value of " + typeof value + + ": " + value); + return null; + } + }, +}; + +exports.Utils = WebConsoleUtils; + +// The page errors listener + +/** + * 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 [window] + * Optional - the window object for which we are created. This is used + * for filtering out messages that belong to other windows. + * @param object listener + * The listener object must have one method: + * - onConsoleServiceMessage(). This method is invoked with one argument, + * the nsIConsoleMessage, whenever a relevant message is received. + */ +function ConsoleServiceListener(window, listener) { + this.window = window; + this.listener = listener; +} +exports.ConsoleServiceListener = ConsoleServiceListener; + +ConsoleServiceListener.prototype = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]), + + /** + * The content window for which we listen to page errors. + * @type nsIDOMWindow + */ + window: null, + + /** + * The listener object which is notified of messages from the console service. + * @type object + */ + listener: null, + + /** + * Initialize the nsIConsoleService listener. + */ + init: function () { + Services.console.registerListener(this); + }, + + /** + * The nsIConsoleService observer. This method takes all the script error + * messages belonging to the current window and sends them to the remote Web + * Console instance. + * + * @param nsIConsoleMessage message + * The message object coming from the nsIConsoleService. + */ + observe: function (message) { + if (!this.listener) { + return; + } + + if (this.window) { + if (!(message instanceof Ci.nsIScriptError) || + !message.outerWindowID || + !this.isCategoryAllowed(message.category)) { + return; + } + + let errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID); + if (!errorWindow || !isWindowIncluded(this.window, errorWindow)) { + return; + } + } + + this.listener.onConsoleServiceMessage(message); + }, + + /** + * 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 category + * The message category you want to check. + * @return boolean + * True if the category is allowed to be logged, false otherwise. + */ + isCategoryAllowed: function (category) { + if (!category) { + return false; + } + + switch (category) { + case "XPConnect JavaScript": + case "component javascript": + case "chrome javascript": + case "chrome registration": + case "XBL": + case "XBL Prototype Handler": + case "XBL Content Sink": + case "xbl javascript": + return false; + } + + return true; + }, + + /** + * Get the cached page errors for the current inner window and its (i)frames. + * + * @param boolean [includePrivate=false] + * Tells if you want to also retrieve messages coming from private + * windows. Defaults to false. + * @return array + * The array of cached messages. Each element is an nsIScriptError or + * an nsIConsoleMessage + */ + getCachedMessages: function (includePrivate = false) { + let errors = Services.console.getMessageArray() || []; + + // if !this.window, we're in a browser console. Still need to filter + // private messages. + if (!this.window) { + return errors.filter((error) => { + if (error instanceof Ci.nsIScriptError) { + if (!includePrivate && error.isFromPrivateWindow) { + return false; + } + } + + return true; + }); + } + + let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window); + + return errors.filter((error) => { + if (error instanceof Ci.nsIScriptError) { + if (!includePrivate && error.isFromPrivateWindow) { + return false; + } + if (ids && + (ids.indexOf(error.innerWindowID) == -1 || + !this.isCategoryAllowed(error.category))) { + return false; + } + } else if (ids && ids[0]) { + // If this is not an nsIScriptError and we need to do window-based + // filtering we skip this message. + return false; + } + + return true; + }); + }, + + /** + * Remove the nsIConsoleService listener. + */ + destroy: function () { + Services.console.unregisterListener(this); + this.listener = this.window = null; + }, +}; + +// The window.console API observer + +/** + * The window.console API observer. This allows the window.console API messages + * to be sent to the remote Web Console instance. + * + * @constructor + * @param nsIDOMWindow window + * Optional - the window object for which we are created. This is used + * for filtering out messages that belong to other windows. + * @param object owner + * The owner object must have the following methods: + * - onConsoleAPICall(). This method is invoked with one argument, the + * Console API message that comes from the observer service, whenever + * a relevant console API call is received. + * @param object filteringOptions + * Optional - The filteringOptions that this listener should listen to: + * - addonId: filter console messages based on the addonId. + */ +function ConsoleAPIListener(window, owner, {addonId} = {}) { + this.window = window; + this.owner = owner; + this.addonId = addonId; +} +exports.ConsoleAPIListener = ConsoleAPIListener; + +ConsoleAPIListener.prototype = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + /** + * The content window for which we listen to window.console API calls. + * @type nsIDOMWindow + */ + window: null, + + /** + * The owner object which is notified of window.console API calls. It must + * have a onConsoleAPICall method which is invoked with one argument: the + * console API call object that comes from the observer service. + * + * @type object + * @see WebConsoleActor + */ + owner: null, + + /** + * The addonId that we listen for. If not null then only messages from this + * console will be returned. + */ + addonId: null, + + /** + * Initialize the window.console API observer. + */ + init: function () { + // Note that the observer is process-wide. We will filter the messages as + // needed, see CAL_observe(). + Services.obs.addObserver(this, "console-api-log-event", false); + }, + + /** + * The console API message observer. When messages are received from the + * observer service we forward them to the remote Web Console instance. + * + * @param object message + * The message object receives from the observer service. + * @param string topic + * The message topic received from the observer service. + */ + observe: function (message, topic) { + if (!this.owner) { + return; + } + + // Here, wrappedJSObject is not a security wrapper but a property defined + // by the XPCOM component which allows us to unwrap the XPCOM interface and + // access the underlying JSObject. + let apiMessage = message.wrappedJSObject; + + if (!this.isMessageRelevant(apiMessage)) { + return; + } + + this.owner.onConsoleAPICall(apiMessage); + }, + + /** + * Given a message, return true if this window should show it and false + * if it should be ignored. + * + * @param message + * The message from the Storage Service + * @return bool + * Do we care about this message? + */ + isMessageRelevant: function (message) { + let workerType = WebConsoleUtils.getWorkerType(message); + + if (this.window && workerType === "ServiceWorker") { + // For messages from Service Workers, message.ID is the + // scope, which can be used to determine whether it's controlling + // a window. + let scope = message.ID; + + if (!swm.shouldReportToWindow(this.window, scope)) { + return false; + } + } + + if (this.window && !workerType) { + let msgWindow = Services.wm.getCurrentInnerWindowWithId(message.innerID); + if (!msgWindow || !isWindowIncluded(this.window, msgWindow)) { + // Not the same window! + return false; + } + } + + if (this.addonId) { + // ConsoleAPI.jsm messages contains a consoleID, (and it is currently + // used in Addon SDK add-ons), the standard 'console' object + // (which is used in regular webpages and in WebExtensions pages) + // contains the originAttributes of the source document principal. + + // Filtering based on the originAttributes used by + // the Console API object. + if (message.originAttributes && + message.originAttributes.addonId == this.addonId) { + return true; + } + + // Filtering based on the old-style consoleID property used by + // the legacy Console JSM module. + if (message.consoleID && message.consoleID == `addon/${this.addonId}`) { + return true; + } + + return false; + } + + return true; + }, + + /** + * Get the cached messages for the current inner window and its (i)frames. + * + * @param boolean [includePrivate=false] + * Tells if you want to also retrieve messages coming from private + * windows. Defaults to false. + * @return array + * The array of cached messages. + */ + getCachedMessages: function (includePrivate = false) { + let messages = []; + let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] + .getService(Ci.nsIConsoleAPIStorage); + + // if !this.window, we're in a browser console. Retrieve all events + // for filtering based on privacy. + if (!this.window) { + messages = ConsoleAPIStorage.getEvents(); + } else { + let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window); + ids.forEach((id) => { + messages = messages.concat(ConsoleAPIStorage.getEvents(id)); + }); + } + + CONSOLE_WORKER_IDS.forEach((id) => { + messages = messages.concat(ConsoleAPIStorage.getEvents(id)); + }); + + messages = messages.filter(msg => { + return this.isMessageRelevant(msg); + }); + + if (includePrivate) { + return messages; + } + + return messages.filter((m) => !m.private); + }, + + /** + * Destroy the console API listener. + */ + destroy: function () { + Services.obs.removeObserver(this, "console-api-log-event"); + this.window = this.owner = null; + }, +}; + +/** + * WebConsole commands manager. + * + * Defines a set of functions /variables ("commands") that are available from + * the Web Console but not from the web page. + * + */ +var WebConsoleCommands = { + _registeredCommands: new Map(), + _originalCommands: new Map(), + + /** + * @private + * Reserved for built-in commands. To register a command from the code of an + * add-on, see WebConsoleCommands.register instead. + * + * @see WebConsoleCommands.register + */ + _registerOriginal: function (name, command) { + this.register(name, command); + this._originalCommands.set(name, this.getCommand(name)); + }, + + /** + * Register a new command. + * @param {string} name The command name (exemple: "$") + * @param {(function|object)} command The command to register. + * It can be a function so the command is a function (like "$()"), + * or it can also be a property descriptor to describe a getter / value (like + * "$0"). + * + * The command function or the command getter are passed a owner object as + * their first parameter (see the example below). + * + * Note that setters don't work currently and "enumerable" and "configurable" + * are forced to true. + * + * @example + * + * WebConsoleCommands.register("$", function JSTH_$(owner, selector) + * { + * return owner.window.document.querySelector(selector); + * }); + * + * WebConsoleCommands.register("$0", { + * get: function(owner) { + * return owner.makeDebuggeeValue(owner.selectedNode); + * } + * }); + */ + register: function (name, command) { + this._registeredCommands.set(name, command); + }, + + /** + * Unregister a command. + * + * If the command being unregister overrode a built-in command, + * the latter is restored. + * + * @param {string} name The name of the command + */ + unregister: function (name) { + this._registeredCommands.delete(name); + if (this._originalCommands.has(name)) { + this.register(name, this._originalCommands.get(name)); + } + }, + + /** + * Returns a command by its name. + * + * @param {string} name The name of the command. + * + * @return {(function|object)} The command. + */ + getCommand: function (name) { + return this._registeredCommands.get(name); + }, + + /** + * Returns true if a command is registered with the given name. + * + * @param {string} name The name of the command. + * + * @return {boolean} True if the command is registered. + */ + hasCommand: function (name) { + return this._registeredCommands.has(name); + }, +}; + +exports.WebConsoleCommands = WebConsoleCommands; + +/* + * Built-in commands. + * + * A list of helper functions used by Firebug can be found here: + * http://getfirebug.com/wiki/index.php/Command_Line_API + */ + +/** + * Find a node by ID. + * + * @param string id + * The ID of the element you want. + * @return nsIDOMNode or null + * The result of calling document.querySelector(selector). + */ +WebConsoleCommands._registerOriginal("$", function (owner, selector) { + return owner.window.document.querySelector(selector); +}); + +/** + * Find the nodes matching a CSS selector. + * + * @param string selector + * A string that is passed to window.document.querySelectorAll. + * @return nsIDOMNodeList + * Returns the result of document.querySelectorAll(selector). + */ +WebConsoleCommands._registerOriginal("$$", function (owner, selector) { + let nodes = owner.window.document.querySelectorAll(selector); + + // Calling owner.window.Array.from() doesn't work without accessing the + // wrappedJSObject, so just loop through the results instead. + let result = new owner.window.Array(); + for (let i = 0; i < nodes.length; i++) { + result.push(nodes[i]); + } + return result; +}); + +/** + * Returns the result of the last console input evaluation + * + * @return object|undefined + * Returns last console evaluation or undefined + */ +WebConsoleCommands._registerOriginal("$_", { + get: function (owner) { + return owner.consoleActor.getLastConsoleInputEvaluation(); + } +}); + +/** + * Runs an xPath query and returns all matched nodes. + * + * @param string xPath + * xPath search query to execute. + * @param [optional] nsIDOMNode context + * Context to run the xPath query on. Uses window.document if not set. + * @return array of nsIDOMNode + */ +WebConsoleCommands._registerOriginal("$x", function (owner, xPath, context) { + let nodes = new owner.window.Array(); + + // Not waiving Xrays, since we want the original Document.evaluate function, + // instead of anything that's been redefined. + let doc = owner.window.document; + context = context || doc; + + let results = doc.evaluate(xPath, context, null, + Ci.nsIDOMXPathResult.ANY_TYPE, null); + let node; + while ((node = results.iterateNext())) { + nodes.push(node); + } + + return nodes; +}); + +/** + * Returns the currently selected object in the highlighter. + * + * @return Object representing the current selection in the + * Inspector, or null if no selection exists. + */ +WebConsoleCommands._registerOriginal("$0", { + get: function (owner) { + return owner.makeDebuggeeValue(owner.selectedNode); + } +}); + +/** + * Clears the output of the WebConsole. + */ +WebConsoleCommands._registerOriginal("clear", function (owner) { + owner.helperResult = { + type: "clearOutput", + }; +}); + +/** + * Clears the input history of the WebConsole. + */ +WebConsoleCommands._registerOriginal("clearHistory", function (owner) { + owner.helperResult = { + type: "clearHistory", + }; +}); + +/** + * Returns the result of Object.keys(object). + * + * @param object object + * Object to return the property names from. + * @return array of strings + */ +WebConsoleCommands._registerOriginal("keys", function (owner, object) { + // Need to waive Xrays so we can iterate functions and accessor properties + return Cu.cloneInto(Object.keys(Cu.waiveXrays(object)), owner.window); +}); + +/** + * Returns the values of all properties on object. + * + * @param object object + * Object to display the values from. + * @return array of string + */ +WebConsoleCommands._registerOriginal("values", function (owner, object) { + let values = []; + // Need to waive Xrays so we can iterate functions and accessor properties + let waived = Cu.waiveXrays(object); + let names = Object.getOwnPropertyNames(waived); + + for (let name of names) { + values.push(waived[name]); + } + + return Cu.cloneInto(values, owner.window); +}); + +/** + * Opens a help window in MDN. + */ +WebConsoleCommands._registerOriginal("help", function (owner) { + owner.helperResult = { type: "help" }; +}); + +/** + * Change the JS evaluation scope. + * + * @param DOMElement|string|window window + * The window object to use for eval scope. This can be a string that + * is used to perform document.querySelector(), to find the iframe that + * you want to cd() to. A DOMElement can be given as well, the + * .contentWindow property is used. Lastly, you can directly pass + * a window object. If you call cd() with no arguments, the current + * eval scope is cleared back to its default (the top window). + */ +WebConsoleCommands._registerOriginal("cd", function (owner, window) { + if (!window) { + owner.consoleActor.evalWindow = null; + owner.helperResult = { type: "cd" }; + return; + } + + if (typeof window == "string") { + window = owner.window.document.querySelector(window); + } + if (window instanceof Ci.nsIDOMElement && window.contentWindow) { + window = window.contentWindow; + } + if (!(window instanceof Ci.nsIDOMWindow)) { + owner.helperResult = { + type: "error", + message: "cdFunctionInvalidArgument" + }; + return; + } + + owner.consoleActor.evalWindow = window; + owner.helperResult = { type: "cd" }; +}); + +/** + * Inspects the passed object. This is done by opening the PropertyPanel. + * + * @param object object + * Object to inspect. + */ +WebConsoleCommands._registerOriginal("inspect", function (owner, object) { + let dbgObj = owner.makeDebuggeeValue(object); + let grip = owner.createValueGrip(dbgObj); + owner.helperResult = { + type: "inspectObject", + input: owner.evalInput, + object: grip, + }; +}); + +/** + * Prints object to the output. + * + * @param object object + * Object to print to the output. + * @return string + */ +WebConsoleCommands._registerOriginal("pprint", function (owner, object) { + if (object === null || object === undefined || object === true || + object === false) { + owner.helperResult = { + type: "error", + message: "helperFuncUnsupportedTypeError", + }; + return null; + } + + owner.helperResult = { rawOutput: true }; + + if (typeof object == "function") { + return object + "\n"; + } + + let output = []; + + let obj = object; + for (let name in obj) { + let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {}; + if (desc.get || desc.set) { + // TODO: Bug 842672 - toolkit/ imports modules from browser/. + let getGrip = VariablesView.getGrip(desc.get); + let setGrip = VariablesView.getGrip(desc.set); + let getString = VariablesView.getString(getGrip); + let setString = VariablesView.getString(setGrip); + output.push(name + ":", " get: " + getString, " set: " + setString); + } else { + let valueGrip = VariablesView.getGrip(obj[name]); + let valueString = VariablesView.getString(valueGrip); + output.push(name + ": " + valueString); + } + } + + return " " + output.join("\n "); +}); + +/** + * Print the String representation of a value to the output, as-is. + * + * @param any value + * A value you want to output as a string. + * @return void + */ +WebConsoleCommands._registerOriginal("print", function (owner, value) { + owner.helperResult = { rawOutput: true }; + if (typeof value === "symbol") { + return Symbol.prototype.toString.call(value); + } + // Waiving Xrays here allows us to see a closer representation of the + // underlying object. This may execute arbitrary content code, but that + // code will run with content privileges, and the result will be rendered + // inert by coercing it to a String. + return String(Cu.waiveXrays(value)); +}); + +/** + * Copy the String representation of a value to the clipboard. + * + * @param any value + * A value you want to copy as a string. + * @return void + */ +WebConsoleCommands._registerOriginal("copy", function (owner, value) { + let payload; + try { + if (value instanceof Ci.nsIDOMElement) { + payload = value.outerHTML; + } else if (typeof value == "string") { + payload = value; + } else { + payload = JSON.stringify(value, null, " "); + } + } catch (ex) { + payload = "/* " + ex + " */"; + } + owner.helperResult = { + type: "copyValueToClipboard", + value: payload, + }; +}); + +/** + * (Internal only) Add the bindings to |owner.sandbox|. + * This is intended to be used by the WebConsole actor only. + * + * @param object owner + * The owning object. + */ +function addWebConsoleCommands(owner) { + if (!owner) { + throw new Error("The owner is required"); + } + for (let [name, command] of WebConsoleCommands._registeredCommands) { + if (typeof command === "function") { + owner.sandbox[name] = command.bind(undefined, owner); + } else if (typeof command === "object") { + let clone = Object.assign({}, command, { + // We force the enumerability and the configurability (so the + // WebConsoleActor can reconfigure the property). + enumerable: true, + configurable: true + }); + + if (typeof command.get === "function") { + clone.get = command.get.bind(undefined, owner); + } + if (typeof command.set === "function") { + clone.set = command.set.bind(undefined, owner); + } + + Object.defineProperty(owner.sandbox, name, clone); + } + } +} + +exports.addWebConsoleCommands = addWebConsoleCommands; + +/** + * A ReflowObserver that listens for reflow events from the page. + * Implements nsIReflowObserver. + * + * @constructor + * @param object window + * The window for which we need to track reflow. + * @param object owner + * The listener owner which needs to implement: + * - onReflowActivity(reflowInfo) + */ + +function ConsoleReflowListener(window, listener) { + this.docshell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + this.listener = listener; + this.docshell.addWeakReflowObserver(this); +} + +exports.ConsoleReflowListener = ConsoleReflowListener; + +ConsoleReflowListener.prototype = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver, + Ci.nsISupportsWeakReference]), + docshell: null, + listener: null, + + /** + * Forward reflow event to listener. + * + * @param DOMHighResTimeStamp start + * @param DOMHighResTimeStamp end + * @param boolean interruptible + */ + sendReflow: function (start, end, interruptible) { + let frame = components.stack.caller.caller; + + let filename = frame ? frame.filename : null; + + if (filename) { + // Because filename could be of the form "xxx.js -> xxx.js -> xxx.js", + // we only take the last part. + filename = filename.split(" ").pop(); + } + + this.listener.onReflowActivity({ + interruptible: interruptible, + start: start, + end: end, + sourceURL: filename, + sourceLine: frame ? frame.lineNumber : null, + functionName: frame ? frame.name : null + }); + }, + + /** + * On uninterruptible reflow + * + * @param DOMHighResTimeStamp start + * @param DOMHighResTimeStamp end + */ + reflow: function (start, end) { + this.sendReflow(start, end, false); + }, + + /** + * On interruptible reflow + * + * @param DOMHighResTimeStamp start + * @param DOMHighResTimeStamp end + */ + reflowInterruptible: function (start, end) { + this.sendReflow(start, end, true); + }, + + /** + * Unregister listener. + */ + destroy: function () { + this.docshell.removeWeakReflowObserver(this); + this.listener = this.docshell = null; + }, +}; |