summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/call-watcher.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/call-watcher.js')
-rw-r--r--devtools/server/actors/call-watcher.js634
1 files changed, 634 insertions, 0 deletions
diff --git a/devtools/server/actors/call-watcher.js b/devtools/server/actors/call-watcher.js
new file mode 100644
index 000000000..5729f9508
--- /dev/null
+++ b/devtools/server/actors/call-watcher.js
@@ -0,0 +1,634 @@
+/* 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/shared/protocol");
+const {serializeStack, parseStack} = require("toolkit/loader");
+
+const {on, once, off, emit} = events;
+const {method, Arg, Option, RetVal} = protocol;
+
+const { functionCallSpec, callWatcherSpec } = require("devtools/shared/specs/call-watcher");
+const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
+
+/**
+ * This actor contains information about a function call, like the function
+ * type, name, stack, arguments, returned value etc.
+ */
+var FunctionCallActor = protocol.ActorClassWithSpec(functionCallSpec, {
+ /**
+ * 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 performance.now() timestamp when the function was called.
+ * @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 = {
+ global: global,
+ 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.
+ if (holdWeak) {
+ let weakRefs = {
+ window: Cu.getWeakReference(window),
+ caller: Cu.getWeakReference(caller),
+ args: Cu.getWeakReference(args),
+ result: Cu.getWeakReference(result),
+ };
+
+ Object.defineProperties(this.details, {
+ window: { get: () => weakRefs.window.get() },
+ caller: { get: () => weakRefs.caller.get() },
+ args: { get: () => weakRefs.args.get() },
+ result: { get: () => weakRefs.result.get() },
+ });
+ }
+ // Otherwise, hold strong references to the objects.
+ else {
+ this.details.window = window;
+ this.details.caller = caller;
+ this.details.args = args;
+ this.details.result = result;
+ }
+
+ // The caller, args and results are string names for now. It would
+ // certainly be nicer if they were Object actors. Make this smarter, so
+ // that the frontend can inspect each argument, be it object or primitive.
+ // Bug 978960.
+ this.details.previews = {
+ caller: this._generateStringPreview(caller),
+ args: this._generateArgsPreview(args),
+ result: this._generateStringPreview(result)
+ };
+ },
+
+ /**
+ * 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.details.previews.caller,
+ argsPreview: this.details.previews.args,
+ resultPreview: this.details.previews.result
+ };
+ },
+
+ /**
+ * Gets more information about this function call, which is not necessarily
+ * available on the Front instance.
+ */
+ getDetails: 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
+ };
+ },
+
+ /**
+ * Serializes the arguments so that they can be easily be transferred
+ * as a string, but still be useful when displayed in a potential UI.
+ *
+ * @param array args
+ * The source arguments.
+ * @return string
+ * The arguments as a string.
+ */
+ _generateArgsPreview: function (args) {
+ let { global, name, caller } = this.details;
+
+ // Get method signature to determine if there are any enums
+ // used in this method.
+ let methodSignatureEnums;
+
+ let knownGlobal = CallWatcherFront.KNOWN_METHODS[global];
+ if (knownGlobal) {
+ let knownMethod = knownGlobal[name];
+ if (knownMethod) {
+ let isOverloaded = typeof knownMethod.enums === "function";
+ if (isOverloaded) {
+ methodSignatureEnums = methodSignatureEnums(args);
+ } else {
+ methodSignatureEnums = knownMethod.enums;
+ }
+ }
+ }
+
+ let serializeArgs = () => args.map((arg, i) => {
+ // XXX: Bug 978960.
+ 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 (methodSignatureEnums && methodSignatureEnums.has(i)) {
+ return getBitToEnumValue(global, caller, arg);
+ }
+ return arg + "";
+ });
+
+ return serializeArgs().join(", ");
+ },
+
+ /**
+ * Serializes the data so that it can be easily be transferred
+ * as a string, but still be useful when displayed in a potential UI.
+ *
+ * @param object data
+ * The source data.
+ * @return string
+ * The arguments as a string.
+ */
+ _generateStringPreview: function (data) {
+ // XXX: Bug 978960.
+ if (data === undefined) {
+ return "undefined";
+ }
+ if (data === null) {
+ return "null";
+ }
+ if (typeof data == "function") {
+ return "Function";
+ }
+ if (typeof data == "object") {
+ return "Object";
+ }
+ return data + "";
+ }
+});
+
+/**
+ * This actor observes function calls on certain objects or globals.
+ */
+var CallWatcherActor = exports.CallWatcherActor = protocol.ActorClassWithSpec(callWatcherSpec, {
+ 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);
+ on(this.tabActor, "window-ready", this._onGlobalCreated);
+ on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
+ },
+ destroy: function (conn) {
+ protocol.Actor.prototype.destroy.call(this, conn);
+ off(this.tabActor, "window-ready", this._onGlobalCreated);
+ off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
+ this.finalize();
+ },
+
+ /**
+ * 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: null,
+
+ /**
+ * 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: function ({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak, storeCalls }) {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ this._timestampEpoch = 0;
+
+ this._functionCalls = [];
+ this._tracedGlobals = tracedGlobals || [];
+ this._tracedFunctions = tracedFunctions || [];
+ this._holdWeak = !!holdWeak;
+ this._storeCalls = !!storeCalls;
+
+ if (startRecording) {
+ this.resumeRecording();
+ }
+ if (performReload) {
+ this.tabActor.window.location.reload();
+ }
+ },
+
+ /**
+ * Stops listening for document global changes and puts this actor
+ * to hibernation. This method is called automatically just before the
+ * actor is destroyed.
+ */
+ finalize: function () {
+ if (!this._initialized) {
+ return;
+ }
+ this._initialized = false;
+ this._finalized = true;
+
+ this._tracedGlobals = null;
+ this._tracedFunctions = null;
+ },
+
+ /**
+ * Returns whether the instrumented function calls are currently recorded.
+ */
+ isRecording: function () {
+ return this._recording;
+ },
+
+ /**
+ * Initialize the timestamp epoch used to offset function call timestamps.
+ */
+ initTimestampEpoch: function () {
+ this._timestampEpoch = this.tabActor.window.performance.now();
+ },
+
+ /**
+ * Starts recording function calls.
+ */
+ resumeRecording: function () {
+ this._recording = true;
+ },
+
+ /**
+ * Stops recording function calls.
+ */
+ pauseRecording: function () {
+ this._recording = false;
+ return this._functionCalls;
+ },
+
+ /**
+ * Erases all the recorded function calls.
+ * Calling `resumeRecording` or `pauseRecording` does not erase history.
+ */
+ eraseRecording: function () {
+ this._functionCalls = [];
+ },
+
+ /**
+ * Invoked whenever the current tab actor's document global is created.
+ */
+ _onGlobalCreated: function ({window, id, isTopLevel}) {
+ if (!this._initialized) {
+ return;
+ }
+
+ // TODO: bug 981748, support more than just the top-level documents.
+ if (!isTopLevel) {
+ return;
+ }
+
+ let self = this;
+ 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 type = CallWatcherFront.METHOD_FUNCTION;
+ let stack = getStack(name);
+ let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch;
+ 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 type = CallWatcherFront.GETTER_FUNCTION;
+ let stack = getStack(name);
+ let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch;
+ 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 type = CallWatcherFront.SETTER_FUNCTION;
+ let stack = getStack(name);
+ let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch;
+ 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();
+ this._timestampEpoch = 0;
+ }
+ },
+
+ /**
+ * 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);
+ }
+
+ if (this.onCall) {
+ this.onCall(functionCall);
+ } else {
+ emit(this, "call", functionCall);
+ }
+ }
+});
+
+/**
+ * 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;
+}