toolkit/devtools/server/actors/call-watcher.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     4 "use strict";
     6 const {Cc, Ci, Cu, Cr} = require("chrome");
     7 const events = require("sdk/event/core");
     8 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
     9 const protocol = require("devtools/server/protocol");
    10 const {ContentObserver} = require("devtools/content-observer");
    12 const {on, once, off, emit} = events;
    13 const {method, Arg, Option, RetVal} = protocol;
    15 exports.register = function(handle) {
    16   handle.addTabActor(CallWatcherActor, "callWatcherActor");
    17 };
    19 exports.unregister = function(handle) {
    20   handle.removeTabActor(CallWatcherActor);
    21 };
    23 /**
    24  * Type describing a single function call in a stack trace.
    25  */
    26 protocol.types.addDictType("call-stack-item", {
    27   name: "string",
    28   file: "string",
    29   line: "number"
    30 });
    32 /**
    33  * Type describing an overview of a function call.
    34  */
    35 protocol.types.addDictType("call-details", {
    36   type: "number",
    37   name: "string",
    38   stack: "array:call-stack-item"
    39 });
    41 /**
    42  * This actor contains information about a function call, like the function
    43  * type, name, stack, arguments, returned value etc.
    44  */
    45 let FunctionCallActor = protocol.ActorClass({
    46   typeName: "function-call",
    48   /**
    49    * Creates the function call actor.
    50    *
    51    * @param DebuggerServerConnection conn
    52    *        The server connection.
    53    * @param DOMWindow window
    54    *        The content window.
    55    * @param string global
    56    *        The name of the global object owning this function, like
    57    *        "CanvasRenderingContext2D" or "WebGLRenderingContext".
    58    * @param object caller
    59    *        The object owning the function when it was called.
    60    *        For example, in `foo.bar()`, the caller is `foo`.
    61    * @param number type
    62    *        Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER.
    63    * @param string name
    64    *        The called function's name.
    65    * @param array stack
    66    *        The called function's stack, as a list of { name, file, line } objects.
    67    * @param array args
    68    *        The called function's arguments.
    69    * @param any result
    70    *        The value returned by the function call.
    71    */
    72   initialize: function(conn, [window, global, caller, type, name, stack, args, result]) {
    73     protocol.Actor.prototype.initialize.call(this, conn);
    75     this.details = {
    76       window: window,
    77       caller: caller,
    78       type: type,
    79       name: name,
    80       stack: stack,
    81       args: args,
    82       result: result
    83     };
    85     this.meta = {
    86       global: -1,
    87       previews: { caller: "", args: "" }
    88     };
    90     if (global == "WebGLRenderingContext") {
    91       this.meta.global = CallWatcherFront.CANVAS_WEBGL_CONTEXT;
    92     } else if (global == "CanvasRenderingContext2D") {
    93       this.meta.global = CallWatcherFront.CANVAS_2D_CONTEXT;
    94     } else if (global == "window") {
    95       this.meta.global = CallWatcherFront.UNKNOWN_SCOPE;
    96     } else {
    97       this.meta.global = CallWatcherFront.GLOBAL_SCOPE;
    98     }
   100     this.meta.previews.caller = this._generateCallerPreview();
   101     this.meta.previews.args = this._generateArgsPreview();
   102   },
   104   /**
   105    * Customize the marshalling of this actor to provide some generic information
   106    * directly on the Front instance.
   107    */
   108   form: function() {
   109     return {
   110       actor: this.actorID,
   111       type: this.details.type,
   112       name: this.details.name,
   113       file: this.details.stack[0].file,
   114       line: this.details.stack[0].line,
   115       callerPreview: this.meta.previews.caller,
   116       argsPreview: this.meta.previews.args
   117     };
   118   },
   120   /**
   121    * Gets more information about this function call, which is not necessarily
   122    * available on the Front instance.
   123    */
   124   getDetails: method(function() {
   125     let { type, name, stack } = this.details;
   127     // Since not all calls on the stack have corresponding owner files (e.g.
   128     // callbacks of a requestAnimationFrame etc.), there's no benefit in
   129     // returning them, as the user can't jump to the Debugger from them.
   130     for (let i = stack.length - 1;;) {
   131       if (stack[i].file) {
   132         break;
   133       }
   134       stack.pop();
   135       i--;
   136     }
   138     // XXX: Use grips for objects and serialize them properly, in order
   139     // to add the function's caller, arguments and return value. Bug 978957.
   140     return {
   141       type: type,
   142       name: name,
   143       stack: stack
   144     };
   145   }, {
   146     response: { info: RetVal("call-details") }
   147   }),
   149   /**
   150    * Serializes the caller's name so that it can be easily be transferred
   151    * as a string, but still be useful when displayed in a potential UI.
   152    *
   153    * @return string
   154    *         The caller's name as a string.
   155    */
   156   _generateCallerPreview: function() {
   157     let global = this.meta.global;
   158     if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
   159       return "gl";
   160     }
   161     if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
   162       return "ctx";
   163     }
   164     return "";
   165   },
   167   /**
   168    * Serializes the arguments so that they can be easily be transferred
   169    * as a string, but still be useful when displayed in a potential UI.
   170    *
   171    * @return string
   172    *         The arguments as a string.
   173    */
   174   _generateArgsPreview: function() {
   175     let { caller, args } = this.details;
   176     let { global } = this.meta;
   178     // XXX: All of this sucks. Make this smarter, so that the frontend
   179     // can inspect each argument, be it object or primitive. Bug 978960.
   180     let serializeArgs = () => args.map(arg => {
   181       if (typeof arg == "undefined") {
   182         return "undefined";
   183       }
   184       if (typeof arg == "function") {
   185         return "Function";
   186       }
   187       if (typeof arg == "object") {
   188         return "Object";
   189       }
   190       if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
   191         // XXX: This doesn't handle combined bitmasks. Bug 978964.
   192         return getEnumsLookupTable("webgl", caller)[arg] || arg;
   193       }
   194       if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
   195         return getEnumsLookupTable("2d", caller)[arg] || arg;
   196       }
   197       return arg;
   198     });
   200     return serializeArgs().join(", ");
   201   }
   202 });
   204 /**
   205  * The corresponding Front object for the FunctionCallActor.
   206  */
   207 let FunctionCallFront = protocol.FrontClass(FunctionCallActor, {
   208   initialize: function(client, form) {
   209     protocol.Front.prototype.initialize.call(this, client, form);
   210   },
   212   /**
   213    * Adds some generic information directly to this instance,
   214    * to avoid extra roundtrips.
   215    */
   216   form: function(form) {
   217     this.actorID = form.actor;
   218     this.type = form.type;
   219     this.name = form.name;
   220     this.file = form.file;
   221     this.line = form.line;
   222     this.callerPreview = form.callerPreview;
   223     this.argsPreview = form.argsPreview;
   224   }
   225 });
   227 /**
   228  * This actor observes function calls on certain objects or globals.
   229  */
   230 let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
   231   typeName: "call-watcher",
   232   initialize: function(conn, tabActor) {
   233     protocol.Actor.prototype.initialize.call(this, conn);
   234     this.tabActor = tabActor;
   235     this._onGlobalCreated = this._onGlobalCreated.bind(this);
   236     this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
   237     this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
   238   },
   239   destroy: function(conn) {
   240     protocol.Actor.prototype.destroy.call(this, conn);
   241     this.finalize();
   242   },
   244   /**
   245    * Starts waiting for the current tab actor's document global to be
   246    * created, in order to instrument the specified objects and become
   247    * aware of everything the content does with them.
   248    */
   249   setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload }) {
   250     if (this._initialized) {
   251       return;
   252     }
   253     this._initialized = true;
   255     this._functionCalls = [];
   256     this._tracedGlobals = tracedGlobals || [];
   257     this._tracedFunctions = tracedFunctions || [];
   258     this._contentObserver = new ContentObserver(this.tabActor);
   260     on(this._contentObserver, "global-created", this._onGlobalCreated);
   261     on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
   263     if (startRecording) {
   264       this.resumeRecording();
   265     }
   266     if (performReload) {
   267       this.tabActor.window.location.reload();
   268     }
   269   }, {
   270     request: {
   271       tracedGlobals: Option(0, "nullable:array:string"),
   272       tracedFunctions: Option(0, "nullable:array:string"),
   273       startRecording: Option(0, "boolean"),
   274       performReload: Option(0, "boolean")
   275     },
   276     oneway: true
   277   }),
   279   /**
   280    * Stops listening for document global changes and puts this actor
   281    * to hibernation. This method is called automatically just before the
   282    * actor is destroyed.
   283    */
   284   finalize: method(function() {
   285     if (!this._initialized) {
   286       return;
   287     }
   288     this._initialized = false;
   290     this._contentObserver.stopListening();
   291     off(this._contentObserver, "global-created", this._onGlobalCreated);
   292     off(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
   294     this._tracedGlobals = null;
   295     this._tracedFunctions = null;
   296     this._contentObserver = null;
   297   }, {
   298     oneway: true
   299   }),
   301   /**
   302    * Returns whether the instrumented function calls are currently recorded.
   303    */
   304   isRecording: method(function() {
   305     return this._recording;
   306   }, {
   307     response: RetVal("boolean")
   308   }),
   310   /**
   311    * Starts recording function calls.
   312    */
   313   resumeRecording: method(function() {
   314     this._recording = true;
   315   }),
   317   /**
   318    * Stops recording function calls.
   319    */
   320   pauseRecording: method(function() {
   321     this._recording = false;
   322     return this._functionCalls;
   323   }, {
   324     response: { calls: RetVal("array:function-call") }
   325   }),
   327   /**
   328    * Erases all the recorded function calls.
   329    * Calling `resumeRecording` or `pauseRecording` does not erase history.
   330    */
   331   eraseRecording: method(function() {
   332     this._functionCalls = [];
   333   }),
   335   /**
   336    * Lightweight listener invoked whenever an instrumented function is called
   337    * while recording. We're doing this to avoid the event emitter overhead,
   338    * since this is expected to be a very hot function.
   339    */
   340   onCall: function() {},
   342   /**
   343    * Invoked whenever the current tab actor's document global is created.
   344    */
   345   _onGlobalCreated: function(window) {
   346     let self = this;
   348     this._tracedWindowId = ContentObserver.GetInnerWindowID(window);
   349     let unwrappedWindow = XPCNativeWrapper.unwrap(window);
   350     let callback = this._onContentFunctionCall;
   352     for (let global of this._tracedGlobals) {
   353       let prototype = unwrappedWindow[global].prototype;
   354       let properties = Object.keys(prototype);
   355       properties.forEach(name => overrideSymbol(global, prototype, name, callback));
   356     }
   358     for (let name of this._tracedFunctions) {
   359       overrideSymbol("window", unwrappedWindow, name, callback);
   360     }
   362     /**
   363      * Instruments a method, getter or setter on the specified target object to
   364      * invoke a callback whenever it is called.
   365      */
   366     function overrideSymbol(global, target, name, callback) {
   367       let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name);
   369       if (propertyDescriptor.get || propertyDescriptor.set) {
   370         overrideAccessor(global, target, name, propertyDescriptor, callback);
   371         return;
   372       }
   373       if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") {
   374         overrideFunction(global, target, name, propertyDescriptor, callback);
   375         return;
   376       }
   377     }
   379     /**
   380      * Instruments a function on the specified target object.
   381      */
   382     function overrideFunction(global, target, name, descriptor, callback) {
   383       let originalFunc = target[name];
   385       Object.defineProperty(target, name, {
   386         value: function(...args) {
   387           let result = originalFunc.apply(this, args);
   389           if (self._recording) {
   390             let stack = getStack(name);
   391             let type = CallWatcherFront.METHOD_FUNCTION;
   392             callback(unwrappedWindow, global, this, type, name, stack, args, result);
   393           }
   394           return result;
   395         },
   396         configurable: descriptor.configurable,
   397         enumerable: descriptor.enumerable,
   398         writable: true
   399       });
   400     }
   402     /**
   403      * Instruments a getter or setter on the specified target object.
   404      */
   405     function overrideAccessor(global, target, name, descriptor, callback) {
   406       let originalGetter = target.__lookupGetter__(name);
   407       let originalSetter = target.__lookupSetter__(name);
   409       Object.defineProperty(target, name, {
   410         get: function(...args) {
   411           if (!originalGetter) return undefined;
   412           let result = originalGetter.apply(this, args);
   414           if (self._recording) {
   415             let stack = getStack(name);
   416             let type = CallWatcherFront.GETTER_FUNCTION;
   417             callback(unwrappedWindow, global, this, type, name, stack, args, result);
   418           }
   419           return result;
   420         },
   421         set: function(...args) {
   422           if (!originalSetter) return;
   423           originalSetter.apply(this, args);
   425           if (self._recording) {
   426             let stack = getStack(name);
   427             let type = CallWatcherFront.SETTER_FUNCTION;
   428             callback(unwrappedWindow, global, this, type, name, stack, args, undefined);
   429           }
   430         },
   431         configurable: descriptor.configurable,
   432         enumerable: descriptor.enumerable
   433       });
   434     }
   436     /**
   437      * Stores the relevant information about calls on the stack when
   438      * a function is called.
   439      */
   440     function getStack(caller) {
   441       try {
   442         // Using Components.stack wouldn't be a better idea, since it's
   443         // much slower because it attempts to retrieve the C++ stack as well.
   444         throw new Error();
   445       } catch (e) {
   446         var stack = e.stack;
   447       }
   449       // Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be
   450       // much prettier, but this is a very hot function, so let's sqeeze
   451       // every drop of performance out of it.
   452       let calls = [];
   453       let callIndex = 0;
   454       let currNewLinePivot = stack.indexOf("\n") + 1;
   455       let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
   457       while (nextNewLinePivot > 0) {
   458         let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot);
   459         let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1);
   460         let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1);
   462         if (!calls[callIndex]) {
   463           calls[callIndex] = { name: "", file: "", line: 0 };
   464         }
   465         if (!calls[callIndex + 1]) {
   466           calls[callIndex + 1] = { name: "", file: "", line: 0 };
   467         }
   469         if (callIndex > 0) {
   470           let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex);
   471           let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex);
   472           let name = stack.substring(currNewLinePivot, nameDelimiterIndex);
   473           calls[callIndex].name = name;
   474           calls[callIndex - 1].file = file;
   475           calls[callIndex - 1].line = line;
   476         } else {
   477           // Since the topmost stack frame is actually our overwritten function,
   478           // it will not have the expected name.
   479           calls[0].name = caller;
   480         }
   482         currNewLinePivot = nextNewLinePivot + 1;
   483         nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
   484         callIndex++;
   485       }
   487       return calls;
   488     }
   489   },
   491   /**
   492    * Invoked whenever the current tab actor's inner window is destroyed.
   493    */
   494   _onGlobalDestroyed: function(id) {
   495     if (this._tracedWindowId == id) {
   496       this.pauseRecording();
   497       this.eraseRecording();
   498     }
   499   },
   501   /**
   502    * Invoked whenever an instrumented function is called.
   503    */
   504   _onContentFunctionCall: function(...details) {
   505     let functionCall = new FunctionCallActor(this.conn, details);
   506     this._functionCalls.push(functionCall);
   507     this.onCall(functionCall);
   508   }
   509 });
   511 /**
   512  * The corresponding Front object for the CallWatcherActor.
   513  */
   514 let CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, {
   515   initialize: function(client, { callWatcherActor }) {
   516     protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor });
   517     client.addActorPool(this);
   518     this.manage(this);
   519   }
   520 });
   522 /**
   523  * Constants.
   524  */
   525 CallWatcherFront.METHOD_FUNCTION = 0;
   526 CallWatcherFront.GETTER_FUNCTION = 1;
   527 CallWatcherFront.SETTER_FUNCTION = 2;
   529 CallWatcherFront.GLOBAL_SCOPE = 0;
   530 CallWatcherFront.UNKNOWN_SCOPE = 1;
   531 CallWatcherFront.CANVAS_WEBGL_CONTEXT = 2;
   532 CallWatcherFront.CANVAS_2D_CONTEXT = 3;
   534 /**
   535  * A lookup table for cross-referencing flags or properties with their name
   536  * assuming they look LIKE_THIS most of the time.
   537  *
   538  * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed
   539  * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT".
   540  */
   541 var gEnumRegex = /^[A-Z_]+$/;
   542 var gEnumsLookupTable = {};
   544 function getEnumsLookupTable(type, object) {
   545   let cachedEnum = gEnumsLookupTable[type];
   546   if (cachedEnum) {
   547     return cachedEnum;
   548   }
   550   let table = gEnumsLookupTable[type] = {};
   552   for (let key in object) {
   553     if (key.match(gEnumRegex)) {
   554       table[object[key]] = key;
   555     }
   556   }
   558   return table;
   559 }

mercurial