/* 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, Cr} = require("chrome"); const events = require("sdk/event/core"); const protocol = require("devtools/server/protocol"); const {serializeStack, parseStack} = require("toolkit/loader"); const {on, once, off, emit} = events; const {method, Arg, Option, RetVal} = protocol; /** * Type describing a single function call in a stack trace. */ protocol.types.addDictType("call-stack-item", { name: "string", file: "string", line: "number" }); /** * Type describing an overview of a function call. */ protocol.types.addDictType("call-details", { type: "number", name: "string", stack: "array:call-stack-item" }); /** * This actor contains information about a function call, like the function * type, name, stack, arguments, returned value etc. */ var FunctionCallActor = protocol.ActorClass({ typeName: "function-call", /** * Creates the function call actor. * * @param DebuggerServerConnection conn * The server connection. * @param DOMWindow window * The content window. * @param string global * The name of the global object owning this function, like * "CanvasRenderingContext2D" or "WebGLRenderingContext". * @param object caller * The object owning the function when it was called. * For example, in `foo.bar()`, the caller is `foo`. * @param number type * Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER. * @param string name * The called function's name. * @param array stack * The called function's stack, as a list of { name, file, line } objects. * @param number timestamp * The timestamp of draw-related functions * @param array args * The called function's arguments. * @param any result * The value returned by the function call. * @param boolean holdWeak * Determines whether or not FunctionCallActor stores a weak reference * to the underlying objects. */ initialize: function(conn, [window, global, caller, type, name, stack, timestamp, args, result], holdWeak) { protocol.Actor.prototype.initialize.call(this, conn); this.details = { type: type, name: name, stack: stack, timestamp: timestamp }; // Store a weak reference to all objects so we don't // prevent natural GC if `holdWeak` was passed into // setup as truthy. Used in the Web Audio Editor. if (holdWeak) { let weakRefs = { window: Cu.getWeakReference(window), caller: Cu.getWeakReference(caller), result: Cu.getWeakReference(result), args: Cu.getWeakReference(args) }; Object.defineProperties(this.details, { window: { get: () => weakRefs.window.get() }, caller: { get: () => weakRefs.caller.get() }, result: { get: () => weakRefs.result.get() }, args: { get: () => weakRefs.args.get() }, timestamp: { get: () => weakRefs.timestamp.get() }, }); } // Otherwise, hold strong references to the objects. else { this.details.window = window; this.details.caller = caller; this.details.result = result; this.details.args = args; this.details.timestamp = timestamp; } this.meta = { global: -1, previews: { caller: "", args: "" } }; if (global == "WebGLRenderingContext") { this.meta.global = CallWatcherFront.CANVAS_WEBGL_CONTEXT; } else if (global == "CanvasRenderingContext2D") { this.meta.global = CallWatcherFront.CANVAS_2D_CONTEXT; } else if (global == "window") { this.meta.global = CallWatcherFront.UNKNOWN_SCOPE; } else { this.meta.global = CallWatcherFront.GLOBAL_SCOPE; } this.meta.previews.caller = this._generateCallerPreview(); this.meta.previews.args = this._generateArgsPreview(); }, /** * Customize the marshalling of this actor to provide some generic information * directly on the Front instance. */ form: function() { return { actor: this.actorID, type: this.details.type, name: this.details.name, file: this.details.stack[0].file, line: this.details.stack[0].line, timestamp: this.details.timestamp, callerPreview: this.meta.previews.caller, argsPreview: this.meta.previews.args }; }, /** * Gets more information about this function call, which is not necessarily * available on the Front instance. */ getDetails: method(function() { let { type, name, stack, timestamp } = this.details; // Since not all calls on the stack have corresponding owner files (e.g. // callbacks of a requestAnimationFrame etc.), there's no benefit in // returning them, as the user can't jump to the Debugger from them. for (let i = stack.length - 1;;) { if (stack[i].file) { break; } stack.pop(); i--; } // XXX: Use grips for objects and serialize them properly, in order // to add the function's caller, arguments and return value. Bug 978957. return { type: type, name: name, stack: stack, timestamp: timestamp }; }, { response: { info: RetVal("call-details") } }), /** * Serializes the caller's name so that it can be easily be transferred * as a string, but still be useful when displayed in a potential UI. * * @return string * The caller's name as a string. */ _generateCallerPreview: function() { let global = this.meta.global; if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { return "gl"; } if (global == CallWatcherFront.CANVAS_2D_CONTEXT) { return "ctx"; } return ""; }, /** * Serializes the arguments so that they can be easily be transferred * as a string, but still be useful when displayed in a potential UI. * * @return string * The arguments as a string. */ _generateArgsPreview: function() { let { caller, args, name } = this.details; let { global } = this.meta; // Get method signature to determine if there are any enums // used in this method. let enumArgs = (CallWatcherFront.ENUM_METHODS[global] || {})[name]; if (typeof enumArgs === "function") { enumArgs = enumArgs(args); } // XXX: All of this sucks. Make this smarter, so that the frontend // can inspect each argument, be it object or primitive. Bug 978960. let serializeArgs = () => args.map((arg, i) => { if (arg === undefined) { return "undefined"; } if (arg === null) { return "null"; } if (typeof arg == "function") { return "Function"; } if (typeof arg == "object") { return "Object"; } // If this argument matches the method's signature // and is an enum, change it to its constant name. if (enumArgs && enumArgs.indexOf(i) !== -1) { return getBitToEnumValue(global, caller, arg); } return arg; }); return serializeArgs().join(", "); } }); /** * The corresponding Front object for the FunctionCallActor. */ var FunctionCallFront = protocol.FrontClass(FunctionCallActor, { initialize: function(client, form) { protocol.Front.prototype.initialize.call(this, client, form); }, /** * Adds some generic information directly to this instance, * to avoid extra roundtrips. */ form: function(form) { this.actorID = form.actor; this.type = form.type; this.name = form.name; this.file = form.file; this.line = form.line; this.timestamp = form.timestamp; this.callerPreview = form.callerPreview; this.argsPreview = form.argsPreview; } }); /** * This actor observes function calls on certain objects or globals. */ var CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({ typeName: "call-watcher", initialize: function(conn, tabActor) { protocol.Actor.prototype.initialize.call(this, conn); this.tabActor = tabActor; this._onGlobalCreated = this._onGlobalCreated.bind(this); this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this); this._onContentFunctionCall = this._onContentFunctionCall.bind(this); }, destroy: function(conn) { protocol.Actor.prototype.destroy.call(this, conn); this.finalize(); }, /** * Starts waiting for the current tab actor's document global to be * created, in order to instrument the specified objects and become * aware of everything the content does with them. */ setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak, storeCalls }) { if (this._initialized) { return; } this._initialized = true; this._functionCalls = []; this._tracedGlobals = tracedGlobals || []; this._tracedFunctions = tracedFunctions || []; this._holdWeak = !!holdWeak; this._storeCalls = !!storeCalls; on(this.tabActor, "window-ready", this._onGlobalCreated); on(this.tabActor, "window-destroyed", this._onGlobalDestroyed); if (startRecording) { this.resumeRecording(); } if (performReload) { this.tabActor.window.location.reload(); } }, { request: { tracedGlobals: Option(0, "nullable:array:string"), tracedFunctions: Option(0, "nullable:array:string"), startRecording: Option(0, "boolean"), performReload: Option(0, "boolean"), holdWeak: Option(0, "boolean"), storeCalls: Option(0, "boolean") }, oneway: true }), /** * Stops listening for document global changes and puts this actor * to hibernation. This method is called automatically just before the * actor is destroyed. */ finalize: method(function() { if (!this._initialized) { return; } this._initialized = false; this._finalized = true; off(this.tabActor, "window-ready", this._onGlobalCreated); off(this.tabActor, "window-destroyed", this._onGlobalDestroyed); this._tracedGlobals = null; this._tracedFunctions = null; }, { oneway: true }), /** * Returns whether the instrumented function calls are currently recorded. */ isRecording: method(function() { return this._recording; }, { response: RetVal("boolean") }), /** * Initialize frame start timestamp for measuring */ initFrameStartTimestamp: method(function() { this._frameStartTimestamp = this.tabActor.window.performance.now(); }), /** * Starts recording function calls. */ resumeRecording: method(function() { this._recording = true; }), /** * Stops recording function calls. */ pauseRecording: method(function() { this._recording = false; return this._functionCalls; }, { response: { calls: RetVal("array:function-call") } }), /** * Erases all the recorded function calls. * Calling `resumeRecording` or `pauseRecording` does not erase history. */ eraseRecording: method(function() { this._functionCalls = []; }), /** * Lightweight listener invoked whenever an instrumented function is called * while recording. We're doing this to avoid the event emitter overhead, * since this is expected to be a very hot function. */ onCall: function() {}, /** * Invoked whenever the current tab actor's document global is created. */ _onGlobalCreated: function({window, id, isTopLevel}) { let self = this; // TODO: bug 981748, support more than just the top-level documents. if (!isTopLevel) { return; } this._tracedWindowId = id; let unwrappedWindow = XPCNativeWrapper.unwrap(window); let callback = this._onContentFunctionCall; for (let global of this._tracedGlobals) { let prototype = unwrappedWindow[global].prototype; let properties = Object.keys(prototype); properties.forEach(name => overrideSymbol(global, prototype, name, callback)); } for (let name of this._tracedFunctions) { overrideSymbol("window", unwrappedWindow, name, callback); } /** * Instruments a method, getter or setter on the specified target object to * invoke a callback whenever it is called. */ function overrideSymbol(global, target, name, callback) { let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name); if (propertyDescriptor.get || propertyDescriptor.set) { overrideAccessor(global, target, name, propertyDescriptor, callback); return; } if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") { overrideFunction(global, target, name, propertyDescriptor, callback); return; } } /** * Instruments a function on the specified target object. */ function overrideFunction(global, target, name, descriptor, callback) { // Invoking .apply on an unxrayed content function doesn't work, because // the arguments array is inaccessible to it. Get Xrays back. let originalFunc = Cu.unwaiveXrays(target[name]); Cu.exportFunction(function(...args) { let result; try { result = Cu.waiveXrays(originalFunc.apply(this, args)); } catch (e) { throw createContentError(e, unwrappedWindow); } if (self._recording) { let timestamp = self.tabActor.window.performance.now() - self._frameStartTimestamp; let stack = getStack(name); let type = CallWatcherFront.METHOD_FUNCTION; callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, result); } return result; }, target, { defineAs: name }); Object.defineProperty(target, name, { configurable: descriptor.configurable, enumerable: descriptor.enumerable, writable: true }); } /** * Instruments a getter or setter on the specified target object. */ function overrideAccessor(global, target, name, descriptor, callback) { // Invoking .apply on an unxrayed content function doesn't work, because // the arguments array is inaccessible to it. Get Xrays back. let originalGetter = Cu.unwaiveXrays(target.__lookupGetter__(name)); let originalSetter = Cu.unwaiveXrays(target.__lookupSetter__(name)); Object.defineProperty(target, name, { get: function(...args) { if (!originalGetter) return undefined; let result = Cu.waiveXrays(originalGetter.apply(this, args)); if (self._recording) { let timestamp = self.tabActor.window.performance.now() - self._frameStartTimestamp; let stack = getStack(name); let type = CallWatcherFront.GETTER_FUNCTION; callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, result); } return result; }, set: function(...args) { if (!originalSetter) return; originalSetter.apply(this, args); if (self._recording) { let timestamp = self.tabActor.window.performance.now() - self._frameStartTimestamp; let stack = getStack(name); let type = CallWatcherFront.SETTER_FUNCTION; callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, undefined); } }, configurable: descriptor.configurable, enumerable: descriptor.enumerable }); } /** * Stores the relevant information about calls on the stack when * a function is called. */ function getStack(caller) { try { // Using Components.stack wouldn't be a better idea, since it's // much slower because it attempts to retrieve the C++ stack as well. throw new Error(); } catch (e) { var stack = e.stack; } // Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be // much prettier, but this is a very hot function, so let's sqeeze // every drop of performance out of it. let calls = []; let callIndex = 0; let currNewLinePivot = stack.indexOf("\n") + 1; let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot); while (nextNewLinePivot > 0) { let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot); let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1); let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1); if (!calls[callIndex]) { calls[callIndex] = { name: "", file: "", line: 0 }; } if (!calls[callIndex + 1]) { calls[callIndex + 1] = { name: "", file: "", line: 0 }; } if (callIndex > 0) { let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex); let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex); let name = stack.substring(currNewLinePivot, nameDelimiterIndex); calls[callIndex].name = name; calls[callIndex - 1].file = file; calls[callIndex - 1].line = line; } else { // Since the topmost stack frame is actually our overwritten function, // it will not have the expected name. calls[0].name = caller; } currNewLinePivot = nextNewLinePivot + 1; nextNewLinePivot = stack.indexOf("\n", currNewLinePivot); callIndex++; } return calls; } }, /** * Invoked whenever the current tab actor's inner window is destroyed. */ _onGlobalDestroyed: function({window, id, isTopLevel}) { if (this._tracedWindowId == id) { this.pauseRecording(); this.eraseRecording(); } }, /** * Invoked whenever an instrumented function is called. */ _onContentFunctionCall: function(...details) { // If the consuming tool has finalized call-watcher, ignore the // still-instrumented calls. if (this._finalized) { return; } let functionCall = new FunctionCallActor(this.conn, details, this._holdWeak); if (this._storeCalls) { this._functionCalls.push(functionCall); } this.onCall(functionCall); } }); /** * The corresponding Front object for the CallWatcherActor. */ var CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, { initialize: function(client, { callWatcherActor }) { protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor }); this.manage(this); } }); /** * Constants. */ CallWatcherFront.METHOD_FUNCTION = 0; CallWatcherFront.GETTER_FUNCTION = 1; CallWatcherFront.SETTER_FUNCTION = 2; CallWatcherFront.GLOBAL_SCOPE = 0; CallWatcherFront.UNKNOWN_SCOPE = 1; CallWatcherFront.CANVAS_WEBGL_CONTEXT = 2; CallWatcherFront.CANVAS_2D_CONTEXT = 3; CallWatcherFront.ENUM_METHODS = {}; CallWatcherFront.ENUM_METHODS[CallWatcherFront.CANVAS_2D_CONTEXT] = { asyncDrawXULElement: [6], drawWindow: [6] }; CallWatcherFront.ENUM_METHODS[CallWatcherFront.CANVAS_WEBGL_CONTEXT] = { activeTexture: [0], bindBuffer: [0], bindFramebuffer: [0], bindRenderbuffer: [0], bindTexture: [0], blendEquation: [0], blendEquationSeparate: [0, 1], blendFunc: [0, 1], blendFuncSeparate: [0, 1, 2, 3], bufferData: [0, 1, 2], bufferSubData: [0, 1], checkFramebufferStatus: [0], clear: [0], compressedTexImage2D: [0, 2], compressedTexSubImage2D: [0, 6], copyTexImage2D: [0, 2], copyTexSubImage2D: [0], createShader: [0], cullFace: [0], depthFunc: [0], disable: [0], drawArrays: [0], drawElements: [0, 2], enable: [0], framebufferRenderbuffer: [0, 1, 2], framebufferTexture2D: [0, 1, 2], frontFace: [0], generateMipmap: [0], getBufferParameter: [0, 1], getParameter: [0], getFramebufferAttachmentParameter: [0, 1, 2], getProgramParameter: [1], getRenderbufferParameter: [0, 1], getShaderParameter: [1], getShaderPrecisionFormat: [0, 1], getTexParameter: [0, 1], getVertexAttrib: [1], getVertexAttribOffset: [1], hint: [0, 1], isEnabled: [0], pixelStorei: [0], readPixels: [4, 5], renderbufferStorage: [0, 1], stencilFunc: [0], stencilFuncSeparate: [0, 1], stencilMaskSeparate: [0], stencilOp: [0, 1, 2], stencilOpSeparate: [0, 1, 2, 3], texImage2D: (args) => args.length > 6 ? [0, 2, 6, 7] : [0, 2, 3, 4], texParameterf: [0, 1], texParameteri: [0, 1, 2], texSubImage2D: (args) => args.length === 9 ? [0, 6, 7] : [0, 4, 5], vertexAttribPointer: [2] }; /** * A lookup table for cross-referencing flags or properties with their name * assuming they look LIKE_THIS most of the time. * * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT". */ var gEnumRegex = /^[A-Z][A-Z0-9_]+$/; var gEnumsLookupTable = {}; // These values are returned from errors, or empty values, // and need to be ignored when checking arguments due to the bitwise math. var INVALID_ENUMS = [ "INVALID_ENUM", "NO_ERROR", "INVALID_VALUE", "OUT_OF_MEMORY", "NONE" ]; function getBitToEnumValue(type, object, arg) { let table = gEnumsLookupTable[type]; // If mapping not yet created, do it on the first run. if (!table) { table = gEnumsLookupTable[type] = {}; for (let key in object) { if (key.match(gEnumRegex)) { // Maps `16384` to `"COLOR_BUFFER_BIT"`, etc. table[object[key]] = key; } } } // If a single bit value, just return it. if (table[arg]) { return table[arg]; } // Otherwise, attempt to reduce it to the original bit flags: // `16640` -> "COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT" let flags = []; for (let flag in table) { if (INVALID_ENUMS.indexOf(table[flag]) !== -1) { continue; } // Cast to integer as all values are stored as strings // in `table` flag = flag | 0; if (flag && (arg & flag) === flag) { flags.push(table[flag]); } } // Cache the combined bitmask value return table[arg] = flags.join(" | ") || arg; } /** * Creates a new error from an error that originated from content but was called * from a wrapped overridden method. This is so we can make our own error * that does not look like it originated from the call watcher. * * We use toolkit/loader's parseStack and serializeStack rather than the * parsing done in the local `getStack` function, because it does not expose * column number, would have to change the protocol models `call-stack-items` and `call-details` * which hurts backwards compatibility, and the local `getStack` is an optimized, hot function. */ function createContentError (e, win) { let { message, name, stack } = e; let parsedStack = parseStack(stack); let { fileName, lineNumber, columnNumber } = parsedStack[parsedStack.length - 1]; let error; let isDOMException = e instanceof Ci.nsIDOMDOMException; let constructor = isDOMException ? win.DOMException : (win[e.name] || win.Error); if (isDOMException) { error = new constructor(message, name); Object.defineProperties(error, { code: { value: e.code }, columnNumber: { value: 0 }, // columnNumber is always 0 for DOMExceptions? filename: { value: fileName }, // note the lowercase `filename` lineNumber: { value: lineNumber }, result: { value: e.result }, stack: { value: serializeStack(parsedStack) } }); } else { // Constructing an error here retains all the stack information, // and we can add message, fileName and lineNumber via constructor, though // need to manually add columnNumber. error = new constructor(message, fileName, lineNumber); Object.defineProperty(error, "columnNumber", { value: columnNumber }); } return error; }