|
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"; |
|
5 |
|
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"); |
|
11 |
|
12 const {on, once, off, emit} = events; |
|
13 const {method, Arg, Option, RetVal} = protocol; |
|
14 |
|
15 exports.register = function(handle) { |
|
16 handle.addTabActor(CallWatcherActor, "callWatcherActor"); |
|
17 }; |
|
18 |
|
19 exports.unregister = function(handle) { |
|
20 handle.removeTabActor(CallWatcherActor); |
|
21 }; |
|
22 |
|
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 }); |
|
31 |
|
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 }); |
|
40 |
|
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", |
|
47 |
|
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); |
|
74 |
|
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 }; |
|
84 |
|
85 this.meta = { |
|
86 global: -1, |
|
87 previews: { caller: "", args: "" } |
|
88 }; |
|
89 |
|
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 } |
|
99 |
|
100 this.meta.previews.caller = this._generateCallerPreview(); |
|
101 this.meta.previews.args = this._generateArgsPreview(); |
|
102 }, |
|
103 |
|
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 }, |
|
119 |
|
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; |
|
126 |
|
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 } |
|
137 |
|
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 }), |
|
148 |
|
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 }, |
|
166 |
|
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; |
|
177 |
|
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 }); |
|
199 |
|
200 return serializeArgs().join(", "); |
|
201 } |
|
202 }); |
|
203 |
|
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 }, |
|
211 |
|
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 }); |
|
226 |
|
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 }, |
|
243 |
|
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; |
|
254 |
|
255 this._functionCalls = []; |
|
256 this._tracedGlobals = tracedGlobals || []; |
|
257 this._tracedFunctions = tracedFunctions || []; |
|
258 this._contentObserver = new ContentObserver(this.tabActor); |
|
259 |
|
260 on(this._contentObserver, "global-created", this._onGlobalCreated); |
|
261 on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed); |
|
262 |
|
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 }), |
|
278 |
|
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; |
|
289 |
|
290 this._contentObserver.stopListening(); |
|
291 off(this._contentObserver, "global-created", this._onGlobalCreated); |
|
292 off(this._contentObserver, "global-destroyed", this._onGlobalDestroyed); |
|
293 |
|
294 this._tracedGlobals = null; |
|
295 this._tracedFunctions = null; |
|
296 this._contentObserver = null; |
|
297 }, { |
|
298 oneway: true |
|
299 }), |
|
300 |
|
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 }), |
|
309 |
|
310 /** |
|
311 * Starts recording function calls. |
|
312 */ |
|
313 resumeRecording: method(function() { |
|
314 this._recording = true; |
|
315 }), |
|
316 |
|
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 }), |
|
326 |
|
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 }), |
|
334 |
|
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() {}, |
|
341 |
|
342 /** |
|
343 * Invoked whenever the current tab actor's document global is created. |
|
344 */ |
|
345 _onGlobalCreated: function(window) { |
|
346 let self = this; |
|
347 |
|
348 this._tracedWindowId = ContentObserver.GetInnerWindowID(window); |
|
349 let unwrappedWindow = XPCNativeWrapper.unwrap(window); |
|
350 let callback = this._onContentFunctionCall; |
|
351 |
|
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 } |
|
357 |
|
358 for (let name of this._tracedFunctions) { |
|
359 overrideSymbol("window", unwrappedWindow, name, callback); |
|
360 } |
|
361 |
|
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); |
|
368 |
|
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 } |
|
378 |
|
379 /** |
|
380 * Instruments a function on the specified target object. |
|
381 */ |
|
382 function overrideFunction(global, target, name, descriptor, callback) { |
|
383 let originalFunc = target[name]; |
|
384 |
|
385 Object.defineProperty(target, name, { |
|
386 value: function(...args) { |
|
387 let result = originalFunc.apply(this, args); |
|
388 |
|
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 } |
|
401 |
|
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); |
|
408 |
|
409 Object.defineProperty(target, name, { |
|
410 get: function(...args) { |
|
411 if (!originalGetter) return undefined; |
|
412 let result = originalGetter.apply(this, args); |
|
413 |
|
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); |
|
424 |
|
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 } |
|
435 |
|
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 } |
|
448 |
|
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); |
|
456 |
|
457 while (nextNewLinePivot > 0) { |
|
458 let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot); |
|
459 let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1); |
|
460 let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1); |
|
461 |
|
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 } |
|
468 |
|
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 } |
|
481 |
|
482 currNewLinePivot = nextNewLinePivot + 1; |
|
483 nextNewLinePivot = stack.indexOf("\n", currNewLinePivot); |
|
484 callIndex++; |
|
485 } |
|
486 |
|
487 return calls; |
|
488 } |
|
489 }, |
|
490 |
|
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 }, |
|
500 |
|
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 }); |
|
510 |
|
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 }); |
|
521 |
|
522 /** |
|
523 * Constants. |
|
524 */ |
|
525 CallWatcherFront.METHOD_FUNCTION = 0; |
|
526 CallWatcherFront.GETTER_FUNCTION = 1; |
|
527 CallWatcherFront.SETTER_FUNCTION = 2; |
|
528 |
|
529 CallWatcherFront.GLOBAL_SCOPE = 0; |
|
530 CallWatcherFront.UNKNOWN_SCOPE = 1; |
|
531 CallWatcherFront.CANVAS_WEBGL_CONTEXT = 2; |
|
532 CallWatcherFront.CANVAS_2D_CONTEXT = 3; |
|
533 |
|
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 = {}; |
|
543 |
|
544 function getEnumsLookupTable(type, object) { |
|
545 let cachedEnum = gEnumsLookupTable[type]; |
|
546 if (cachedEnum) { |
|
547 return cachedEnum; |
|
548 } |
|
549 |
|
550 let table = gEnumsLookupTable[type] = {}; |
|
551 |
|
552 for (let key in object) { |
|
553 if (key.match(gEnumRegex)) { |
|
554 table[object[key]] = key; |
|
555 } |
|
556 } |
|
557 |
|
558 return table; |
|
559 } |