123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- /* 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;
- }
|