Wed, 31 Dec 2014 06:09:35 +0100
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 }