michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu, Cr} = require("chrome"); michael@0: const events = require("sdk/event/core"); michael@0: const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const protocol = require("devtools/server/protocol"); michael@0: const {ContentObserver} = require("devtools/content-observer"); michael@0: michael@0: const {on, once, off, emit} = events; michael@0: const {method, Arg, Option, RetVal} = protocol; michael@0: michael@0: exports.register = function(handle) { michael@0: handle.addTabActor(CallWatcherActor, "callWatcherActor"); michael@0: }; michael@0: michael@0: exports.unregister = function(handle) { michael@0: handle.removeTabActor(CallWatcherActor); michael@0: }; michael@0: michael@0: /** michael@0: * Type describing a single function call in a stack trace. michael@0: */ michael@0: protocol.types.addDictType("call-stack-item", { michael@0: name: "string", michael@0: file: "string", michael@0: line: "number" michael@0: }); michael@0: michael@0: /** michael@0: * Type describing an overview of a function call. michael@0: */ michael@0: protocol.types.addDictType("call-details", { michael@0: type: "number", michael@0: name: "string", michael@0: stack: "array:call-stack-item" michael@0: }); michael@0: michael@0: /** michael@0: * This actor contains information about a function call, like the function michael@0: * type, name, stack, arguments, returned value etc. michael@0: */ michael@0: let FunctionCallActor = protocol.ActorClass({ michael@0: typeName: "function-call", michael@0: michael@0: /** michael@0: * Creates the function call actor. michael@0: * michael@0: * @param DebuggerServerConnection conn michael@0: * The server connection. michael@0: * @param DOMWindow window michael@0: * The content window. michael@0: * @param string global michael@0: * The name of the global object owning this function, like michael@0: * "CanvasRenderingContext2D" or "WebGLRenderingContext". michael@0: * @param object caller michael@0: * The object owning the function when it was called. michael@0: * For example, in `foo.bar()`, the caller is `foo`. michael@0: * @param number type michael@0: * Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER. michael@0: * @param string name michael@0: * The called function's name. michael@0: * @param array stack michael@0: * The called function's stack, as a list of { name, file, line } objects. michael@0: * @param array args michael@0: * The called function's arguments. michael@0: * @param any result michael@0: * The value returned by the function call. michael@0: */ michael@0: initialize: function(conn, [window, global, caller, type, name, stack, args, result]) { michael@0: protocol.Actor.prototype.initialize.call(this, conn); michael@0: michael@0: this.details = { michael@0: window: window, michael@0: caller: caller, michael@0: type: type, michael@0: name: name, michael@0: stack: stack, michael@0: args: args, michael@0: result: result michael@0: }; michael@0: michael@0: this.meta = { michael@0: global: -1, michael@0: previews: { caller: "", args: "" } michael@0: }; michael@0: michael@0: if (global == "WebGLRenderingContext") { michael@0: this.meta.global = CallWatcherFront.CANVAS_WEBGL_CONTEXT; michael@0: } else if (global == "CanvasRenderingContext2D") { michael@0: this.meta.global = CallWatcherFront.CANVAS_2D_CONTEXT; michael@0: } else if (global == "window") { michael@0: this.meta.global = CallWatcherFront.UNKNOWN_SCOPE; michael@0: } else { michael@0: this.meta.global = CallWatcherFront.GLOBAL_SCOPE; michael@0: } michael@0: michael@0: this.meta.previews.caller = this._generateCallerPreview(); michael@0: this.meta.previews.args = this._generateArgsPreview(); michael@0: }, michael@0: michael@0: /** michael@0: * Customize the marshalling of this actor to provide some generic information michael@0: * directly on the Front instance. michael@0: */ michael@0: form: function() { michael@0: return { michael@0: actor: this.actorID, michael@0: type: this.details.type, michael@0: name: this.details.name, michael@0: file: this.details.stack[0].file, michael@0: line: this.details.stack[0].line, michael@0: callerPreview: this.meta.previews.caller, michael@0: argsPreview: this.meta.previews.args michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Gets more information about this function call, which is not necessarily michael@0: * available on the Front instance. michael@0: */ michael@0: getDetails: method(function() { michael@0: let { type, name, stack } = this.details; michael@0: michael@0: // Since not all calls on the stack have corresponding owner files (e.g. michael@0: // callbacks of a requestAnimationFrame etc.), there's no benefit in michael@0: // returning them, as the user can't jump to the Debugger from them. michael@0: for (let i = stack.length - 1;;) { michael@0: if (stack[i].file) { michael@0: break; michael@0: } michael@0: stack.pop(); michael@0: i--; michael@0: } michael@0: michael@0: // XXX: Use grips for objects and serialize them properly, in order michael@0: // to add the function's caller, arguments and return value. Bug 978957. michael@0: return { michael@0: type: type, michael@0: name: name, michael@0: stack: stack michael@0: }; michael@0: }, { michael@0: response: { info: RetVal("call-details") } michael@0: }), michael@0: michael@0: /** michael@0: * Serializes the caller's name so that it can be easily be transferred michael@0: * as a string, but still be useful when displayed in a potential UI. michael@0: * michael@0: * @return string michael@0: * The caller's name as a string. michael@0: */ michael@0: _generateCallerPreview: function() { michael@0: let global = this.meta.global; michael@0: if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { michael@0: return "gl"; michael@0: } michael@0: if (global == CallWatcherFront.CANVAS_2D_CONTEXT) { michael@0: return "ctx"; michael@0: } michael@0: return ""; michael@0: }, michael@0: michael@0: /** michael@0: * Serializes the arguments so that they can be easily be transferred michael@0: * as a string, but still be useful when displayed in a potential UI. michael@0: * michael@0: * @return string michael@0: * The arguments as a string. michael@0: */ michael@0: _generateArgsPreview: function() { michael@0: let { caller, args } = this.details; michael@0: let { global } = this.meta; michael@0: michael@0: // XXX: All of this sucks. Make this smarter, so that the frontend michael@0: // can inspect each argument, be it object or primitive. Bug 978960. michael@0: let serializeArgs = () => args.map(arg => { michael@0: if (typeof arg == "undefined") { michael@0: return "undefined"; michael@0: } michael@0: if (typeof arg == "function") { michael@0: return "Function"; michael@0: } michael@0: if (typeof arg == "object") { michael@0: return "Object"; michael@0: } michael@0: if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { michael@0: // XXX: This doesn't handle combined bitmasks. Bug 978964. michael@0: return getEnumsLookupTable("webgl", caller)[arg] || arg; michael@0: } michael@0: if (global == CallWatcherFront.CANVAS_2D_CONTEXT) { michael@0: return getEnumsLookupTable("2d", caller)[arg] || arg; michael@0: } michael@0: return arg; michael@0: }); michael@0: michael@0: return serializeArgs().join(", "); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * The corresponding Front object for the FunctionCallActor. michael@0: */ michael@0: let FunctionCallFront = protocol.FrontClass(FunctionCallActor, { michael@0: initialize: function(client, form) { michael@0: protocol.Front.prototype.initialize.call(this, client, form); michael@0: }, michael@0: michael@0: /** michael@0: * Adds some generic information directly to this instance, michael@0: * to avoid extra roundtrips. michael@0: */ michael@0: form: function(form) { michael@0: this.actorID = form.actor; michael@0: this.type = form.type; michael@0: this.name = form.name; michael@0: this.file = form.file; michael@0: this.line = form.line; michael@0: this.callerPreview = form.callerPreview; michael@0: this.argsPreview = form.argsPreview; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * This actor observes function calls on certain objects or globals. michael@0: */ michael@0: let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({ michael@0: typeName: "call-watcher", michael@0: initialize: function(conn, tabActor) { michael@0: protocol.Actor.prototype.initialize.call(this, conn); michael@0: this.tabActor = tabActor; michael@0: this._onGlobalCreated = this._onGlobalCreated.bind(this); michael@0: this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this); michael@0: this._onContentFunctionCall = this._onContentFunctionCall.bind(this); michael@0: }, michael@0: destroy: function(conn) { michael@0: protocol.Actor.prototype.destroy.call(this, conn); michael@0: this.finalize(); michael@0: }, michael@0: michael@0: /** michael@0: * Starts waiting for the current tab actor's document global to be michael@0: * created, in order to instrument the specified objects and become michael@0: * aware of everything the content does with them. michael@0: */ michael@0: setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload }) { michael@0: if (this._initialized) { michael@0: return; michael@0: } michael@0: this._initialized = true; michael@0: michael@0: this._functionCalls = []; michael@0: this._tracedGlobals = tracedGlobals || []; michael@0: this._tracedFunctions = tracedFunctions || []; michael@0: this._contentObserver = new ContentObserver(this.tabActor); michael@0: michael@0: on(this._contentObserver, "global-created", this._onGlobalCreated); michael@0: on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed); michael@0: michael@0: if (startRecording) { michael@0: this.resumeRecording(); michael@0: } michael@0: if (performReload) { michael@0: this.tabActor.window.location.reload(); michael@0: } michael@0: }, { michael@0: request: { michael@0: tracedGlobals: Option(0, "nullable:array:string"), michael@0: tracedFunctions: Option(0, "nullable:array:string"), michael@0: startRecording: Option(0, "boolean"), michael@0: performReload: Option(0, "boolean") michael@0: }, michael@0: oneway: true michael@0: }), michael@0: michael@0: /** michael@0: * Stops listening for document global changes and puts this actor michael@0: * to hibernation. This method is called automatically just before the michael@0: * actor is destroyed. michael@0: */ michael@0: finalize: method(function() { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: this._initialized = false; michael@0: michael@0: this._contentObserver.stopListening(); michael@0: off(this._contentObserver, "global-created", this._onGlobalCreated); michael@0: off(this._contentObserver, "global-destroyed", this._onGlobalDestroyed); michael@0: michael@0: this._tracedGlobals = null; michael@0: this._tracedFunctions = null; michael@0: this._contentObserver = null; michael@0: }, { michael@0: oneway: true michael@0: }), michael@0: michael@0: /** michael@0: * Returns whether the instrumented function calls are currently recorded. michael@0: */ michael@0: isRecording: method(function() { michael@0: return this._recording; michael@0: }, { michael@0: response: RetVal("boolean") michael@0: }), michael@0: michael@0: /** michael@0: * Starts recording function calls. michael@0: */ michael@0: resumeRecording: method(function() { michael@0: this._recording = true; michael@0: }), michael@0: michael@0: /** michael@0: * Stops recording function calls. michael@0: */ michael@0: pauseRecording: method(function() { michael@0: this._recording = false; michael@0: return this._functionCalls; michael@0: }, { michael@0: response: { calls: RetVal("array:function-call") } michael@0: }), michael@0: michael@0: /** michael@0: * Erases all the recorded function calls. michael@0: * Calling `resumeRecording` or `pauseRecording` does not erase history. michael@0: */ michael@0: eraseRecording: method(function() { michael@0: this._functionCalls = []; michael@0: }), michael@0: michael@0: /** michael@0: * Lightweight listener invoked whenever an instrumented function is called michael@0: * while recording. We're doing this to avoid the event emitter overhead, michael@0: * since this is expected to be a very hot function. michael@0: */ michael@0: onCall: function() {}, michael@0: michael@0: /** michael@0: * Invoked whenever the current tab actor's document global is created. michael@0: */ michael@0: _onGlobalCreated: function(window) { michael@0: let self = this; michael@0: michael@0: this._tracedWindowId = ContentObserver.GetInnerWindowID(window); michael@0: let unwrappedWindow = XPCNativeWrapper.unwrap(window); michael@0: let callback = this._onContentFunctionCall; michael@0: michael@0: for (let global of this._tracedGlobals) { michael@0: let prototype = unwrappedWindow[global].prototype; michael@0: let properties = Object.keys(prototype); michael@0: properties.forEach(name => overrideSymbol(global, prototype, name, callback)); michael@0: } michael@0: michael@0: for (let name of this._tracedFunctions) { michael@0: overrideSymbol("window", unwrappedWindow, name, callback); michael@0: } michael@0: michael@0: /** michael@0: * Instruments a method, getter or setter on the specified target object to michael@0: * invoke a callback whenever it is called. michael@0: */ michael@0: function overrideSymbol(global, target, name, callback) { michael@0: let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name); michael@0: michael@0: if (propertyDescriptor.get || propertyDescriptor.set) { michael@0: overrideAccessor(global, target, name, propertyDescriptor, callback); michael@0: return; michael@0: } michael@0: if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") { michael@0: overrideFunction(global, target, name, propertyDescriptor, callback); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Instruments a function on the specified target object. michael@0: */ michael@0: function overrideFunction(global, target, name, descriptor, callback) { michael@0: let originalFunc = target[name]; michael@0: michael@0: Object.defineProperty(target, name, { michael@0: value: function(...args) { michael@0: let result = originalFunc.apply(this, args); michael@0: michael@0: if (self._recording) { michael@0: let stack = getStack(name); michael@0: let type = CallWatcherFront.METHOD_FUNCTION; michael@0: callback(unwrappedWindow, global, this, type, name, stack, args, result); michael@0: } michael@0: return result; michael@0: }, michael@0: configurable: descriptor.configurable, michael@0: enumerable: descriptor.enumerable, michael@0: writable: true michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Instruments a getter or setter on the specified target object. michael@0: */ michael@0: function overrideAccessor(global, target, name, descriptor, callback) { michael@0: let originalGetter = target.__lookupGetter__(name); michael@0: let originalSetter = target.__lookupSetter__(name); michael@0: michael@0: Object.defineProperty(target, name, { michael@0: get: function(...args) { michael@0: if (!originalGetter) return undefined; michael@0: let result = originalGetter.apply(this, args); michael@0: michael@0: if (self._recording) { michael@0: let stack = getStack(name); michael@0: let type = CallWatcherFront.GETTER_FUNCTION; michael@0: callback(unwrappedWindow, global, this, type, name, stack, args, result); michael@0: } michael@0: return result; michael@0: }, michael@0: set: function(...args) { michael@0: if (!originalSetter) return; michael@0: originalSetter.apply(this, args); michael@0: michael@0: if (self._recording) { michael@0: let stack = getStack(name); michael@0: let type = CallWatcherFront.SETTER_FUNCTION; michael@0: callback(unwrappedWindow, global, this, type, name, stack, args, undefined); michael@0: } michael@0: }, michael@0: configurable: descriptor.configurable, michael@0: enumerable: descriptor.enumerable michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Stores the relevant information about calls on the stack when michael@0: * a function is called. michael@0: */ michael@0: function getStack(caller) { michael@0: try { michael@0: // Using Components.stack wouldn't be a better idea, since it's michael@0: // much slower because it attempts to retrieve the C++ stack as well. michael@0: throw new Error(); michael@0: } catch (e) { michael@0: var stack = e.stack; michael@0: } michael@0: michael@0: // Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be michael@0: // much prettier, but this is a very hot function, so let's sqeeze michael@0: // every drop of performance out of it. michael@0: let calls = []; michael@0: let callIndex = 0; michael@0: let currNewLinePivot = stack.indexOf("\n") + 1; michael@0: let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot); michael@0: michael@0: while (nextNewLinePivot > 0) { michael@0: let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot); michael@0: let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1); michael@0: let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1); michael@0: michael@0: if (!calls[callIndex]) { michael@0: calls[callIndex] = { name: "", file: "", line: 0 }; michael@0: } michael@0: if (!calls[callIndex + 1]) { michael@0: calls[callIndex + 1] = { name: "", file: "", line: 0 }; michael@0: } michael@0: michael@0: if (callIndex > 0) { michael@0: let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex); michael@0: let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex); michael@0: let name = stack.substring(currNewLinePivot, nameDelimiterIndex); michael@0: calls[callIndex].name = name; michael@0: calls[callIndex - 1].file = file; michael@0: calls[callIndex - 1].line = line; michael@0: } else { michael@0: // Since the topmost stack frame is actually our overwritten function, michael@0: // it will not have the expected name. michael@0: calls[0].name = caller; michael@0: } michael@0: michael@0: currNewLinePivot = nextNewLinePivot + 1; michael@0: nextNewLinePivot = stack.indexOf("\n", currNewLinePivot); michael@0: callIndex++; michael@0: } michael@0: michael@0: return calls; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Invoked whenever the current tab actor's inner window is destroyed. michael@0: */ michael@0: _onGlobalDestroyed: function(id) { michael@0: if (this._tracedWindowId == id) { michael@0: this.pauseRecording(); michael@0: this.eraseRecording(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Invoked whenever an instrumented function is called. michael@0: */ michael@0: _onContentFunctionCall: function(...details) { michael@0: let functionCall = new FunctionCallActor(this.conn, details); michael@0: this._functionCalls.push(functionCall); michael@0: this.onCall(functionCall); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * The corresponding Front object for the CallWatcherActor. michael@0: */ michael@0: let CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, { michael@0: initialize: function(client, { callWatcherActor }) { michael@0: protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor }); michael@0: client.addActorPool(this); michael@0: this.manage(this); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Constants. michael@0: */ michael@0: CallWatcherFront.METHOD_FUNCTION = 0; michael@0: CallWatcherFront.GETTER_FUNCTION = 1; michael@0: CallWatcherFront.SETTER_FUNCTION = 2; michael@0: michael@0: CallWatcherFront.GLOBAL_SCOPE = 0; michael@0: CallWatcherFront.UNKNOWN_SCOPE = 1; michael@0: CallWatcherFront.CANVAS_WEBGL_CONTEXT = 2; michael@0: CallWatcherFront.CANVAS_2D_CONTEXT = 3; michael@0: michael@0: /** michael@0: * A lookup table for cross-referencing flags or properties with their name michael@0: * assuming they look LIKE_THIS most of the time. michael@0: * michael@0: * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed michael@0: * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT". michael@0: */ michael@0: var gEnumRegex = /^[A-Z_]+$/; michael@0: var gEnumsLookupTable = {}; michael@0: michael@0: function getEnumsLookupTable(type, object) { michael@0: let cachedEnum = gEnumsLookupTable[type]; michael@0: if (cachedEnum) { michael@0: return cachedEnum; michael@0: } michael@0: michael@0: let table = gEnumsLookupTable[type] = {}; michael@0: michael@0: for (let key in object) { michael@0: if (key.match(gEnumRegex)) { michael@0: table[object[key]] = key; michael@0: } michael@0: } michael@0: michael@0: return table; michael@0: }