call-watcher.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  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. const {Cc, Ci, Cu, Cr} = require("chrome");
  6. const events = require("sdk/event/core");
  7. const protocol = require("devtools/shared/protocol");
  8. const {serializeStack, parseStack} = require("toolkit/loader");
  9. const {on, once, off, emit} = events;
  10. const {method, Arg, Option, RetVal} = protocol;
  11. const { functionCallSpec, callWatcherSpec } = require("devtools/shared/specs/call-watcher");
  12. const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
  13. /**
  14. * This actor contains information about a function call, like the function
  15. * type, name, stack, arguments, returned value etc.
  16. */
  17. var FunctionCallActor = protocol.ActorClassWithSpec(functionCallSpec, {
  18. /**
  19. * Creates the function call actor.
  20. *
  21. * @param DebuggerServerConnection conn
  22. * The server connection.
  23. * @param DOMWindow window
  24. * The content window.
  25. * @param string global
  26. * The name of the global object owning this function, like
  27. * "CanvasRenderingContext2D" or "WebGLRenderingContext".
  28. * @param object caller
  29. * The object owning the function when it was called.
  30. * For example, in `foo.bar()`, the caller is `foo`.
  31. * @param number type
  32. * Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER.
  33. * @param string name
  34. * The called function's name.
  35. * @param array stack
  36. * The called function's stack, as a list of { name, file, line } objects.
  37. * @param number timestamp
  38. * The performance.now() timestamp when the function was called.
  39. * @param array args
  40. * The called function's arguments.
  41. * @param any result
  42. * The value returned by the function call.
  43. * @param boolean holdWeak
  44. * Determines whether or not FunctionCallActor stores a weak reference
  45. * to the underlying objects.
  46. */
  47. initialize: function (conn, [window, global, caller, type, name, stack, timestamp, args, result], holdWeak) {
  48. protocol.Actor.prototype.initialize.call(this, conn);
  49. this.details = {
  50. global: global,
  51. type: type,
  52. name: name,
  53. stack: stack,
  54. timestamp: timestamp
  55. };
  56. // Store a weak reference to all objects so we don't
  57. // prevent natural GC if `holdWeak` was passed into
  58. // setup as truthy.
  59. if (holdWeak) {
  60. let weakRefs = {
  61. window: Cu.getWeakReference(window),
  62. caller: Cu.getWeakReference(caller),
  63. args: Cu.getWeakReference(args),
  64. result: Cu.getWeakReference(result),
  65. };
  66. Object.defineProperties(this.details, {
  67. window: { get: () => weakRefs.window.get() },
  68. caller: { get: () => weakRefs.caller.get() },
  69. args: { get: () => weakRefs.args.get() },
  70. result: { get: () => weakRefs.result.get() },
  71. });
  72. }
  73. // Otherwise, hold strong references to the objects.
  74. else {
  75. this.details.window = window;
  76. this.details.caller = caller;
  77. this.details.args = args;
  78. this.details.result = result;
  79. }
  80. // The caller, args and results are string names for now. It would
  81. // certainly be nicer if they were Object actors. Make this smarter, so
  82. // that the frontend can inspect each argument, be it object or primitive.
  83. // Bug 978960.
  84. this.details.previews = {
  85. caller: this._generateStringPreview(caller),
  86. args: this._generateArgsPreview(args),
  87. result: this._generateStringPreview(result)
  88. };
  89. },
  90. /**
  91. * Customize the marshalling of this actor to provide some generic information
  92. * directly on the Front instance.
  93. */
  94. form: function () {
  95. return {
  96. actor: this.actorID,
  97. type: this.details.type,
  98. name: this.details.name,
  99. file: this.details.stack[0].file,
  100. line: this.details.stack[0].line,
  101. timestamp: this.details.timestamp,
  102. callerPreview: this.details.previews.caller,
  103. argsPreview: this.details.previews.args,
  104. resultPreview: this.details.previews.result
  105. };
  106. },
  107. /**
  108. * Gets more information about this function call, which is not necessarily
  109. * available on the Front instance.
  110. */
  111. getDetails: function () {
  112. let { type, name, stack, timestamp } = this.details;
  113. // Since not all calls on the stack have corresponding owner files (e.g.
  114. // callbacks of a requestAnimationFrame etc.), there's no benefit in
  115. // returning them, as the user can't jump to the Debugger from them.
  116. for (let i = stack.length - 1; ;) {
  117. if (stack[i].file) {
  118. break;
  119. }
  120. stack.pop();
  121. i--;
  122. }
  123. // XXX: Use grips for objects and serialize them properly, in order
  124. // to add the function's caller, arguments and return value. Bug 978957.
  125. return {
  126. type: type,
  127. name: name,
  128. stack: stack,
  129. timestamp: timestamp
  130. };
  131. },
  132. /**
  133. * Serializes the arguments so that they can be easily be transferred
  134. * as a string, but still be useful when displayed in a potential UI.
  135. *
  136. * @param array args
  137. * The source arguments.
  138. * @return string
  139. * The arguments as a string.
  140. */
  141. _generateArgsPreview: function (args) {
  142. let { global, name, caller } = this.details;
  143. // Get method signature to determine if there are any enums
  144. // used in this method.
  145. let methodSignatureEnums;
  146. let knownGlobal = CallWatcherFront.KNOWN_METHODS[global];
  147. if (knownGlobal) {
  148. let knownMethod = knownGlobal[name];
  149. if (knownMethod) {
  150. let isOverloaded = typeof knownMethod.enums === "function";
  151. if (isOverloaded) {
  152. methodSignatureEnums = methodSignatureEnums(args);
  153. } else {
  154. methodSignatureEnums = knownMethod.enums;
  155. }
  156. }
  157. }
  158. let serializeArgs = () => args.map((arg, i) => {
  159. // XXX: Bug 978960.
  160. if (arg === undefined) {
  161. return "undefined";
  162. }
  163. if (arg === null) {
  164. return "null";
  165. }
  166. if (typeof arg == "function") {
  167. return "Function";
  168. }
  169. if (typeof arg == "object") {
  170. return "Object";
  171. }
  172. // If this argument matches the method's signature
  173. // and is an enum, change it to its constant name.
  174. if (methodSignatureEnums && methodSignatureEnums.has(i)) {
  175. return getBitToEnumValue(global, caller, arg);
  176. }
  177. return arg + "";
  178. });
  179. return serializeArgs().join(", ");
  180. },
  181. /**
  182. * Serializes the data so that it can be easily be transferred
  183. * as a string, but still be useful when displayed in a potential UI.
  184. *
  185. * @param object data
  186. * The source data.
  187. * @return string
  188. * The arguments as a string.
  189. */
  190. _generateStringPreview: function (data) {
  191. // XXX: Bug 978960.
  192. if (data === undefined) {
  193. return "undefined";
  194. }
  195. if (data === null) {
  196. return "null";
  197. }
  198. if (typeof data == "function") {
  199. return "Function";
  200. }
  201. if (typeof data == "object") {
  202. return "Object";
  203. }
  204. return data + "";
  205. }
  206. });
  207. /**
  208. * This actor observes function calls on certain objects or globals.
  209. */
  210. var CallWatcherActor = exports.CallWatcherActor = protocol.ActorClassWithSpec(callWatcherSpec, {
  211. initialize: function (conn, tabActor) {
  212. protocol.Actor.prototype.initialize.call(this, conn);
  213. this.tabActor = tabActor;
  214. this._onGlobalCreated = this._onGlobalCreated.bind(this);
  215. this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
  216. this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
  217. on(this.tabActor, "window-ready", this._onGlobalCreated);
  218. on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
  219. },
  220. destroy: function (conn) {
  221. protocol.Actor.prototype.destroy.call(this, conn);
  222. off(this.tabActor, "window-ready", this._onGlobalCreated);
  223. off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
  224. this.finalize();
  225. },
  226. /**
  227. * Lightweight listener invoked whenever an instrumented function is called
  228. * while recording. We're doing this to avoid the event emitter overhead,
  229. * since this is expected to be a very hot function.
  230. */
  231. onCall: null,
  232. /**
  233. * Starts waiting for the current tab actor's document global to be
  234. * created, in order to instrument the specified objects and become
  235. * aware of everything the content does with them.
  236. */
  237. setup: function ({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak, storeCalls }) {
  238. if (this._initialized) {
  239. return;
  240. }
  241. this._initialized = true;
  242. this._timestampEpoch = 0;
  243. this._functionCalls = [];
  244. this._tracedGlobals = tracedGlobals || [];
  245. this._tracedFunctions = tracedFunctions || [];
  246. this._holdWeak = !!holdWeak;
  247. this._storeCalls = !!storeCalls;
  248. if (startRecording) {
  249. this.resumeRecording();
  250. }
  251. if (performReload) {
  252. this.tabActor.window.location.reload();
  253. }
  254. },
  255. /**
  256. * Stops listening for document global changes and puts this actor
  257. * to hibernation. This method is called automatically just before the
  258. * actor is destroyed.
  259. */
  260. finalize: function () {
  261. if (!this._initialized) {
  262. return;
  263. }
  264. this._initialized = false;
  265. this._finalized = true;
  266. this._tracedGlobals = null;
  267. this._tracedFunctions = null;
  268. },
  269. /**
  270. * Returns whether the instrumented function calls are currently recorded.
  271. */
  272. isRecording: function () {
  273. return this._recording;
  274. },
  275. /**
  276. * Initialize the timestamp epoch used to offset function call timestamps.
  277. */
  278. initTimestampEpoch: function () {
  279. this._timestampEpoch = this.tabActor.window.performance.now();
  280. },
  281. /**
  282. * Starts recording function calls.
  283. */
  284. resumeRecording: function () {
  285. this._recording = true;
  286. },
  287. /**
  288. * Stops recording function calls.
  289. */
  290. pauseRecording: function () {
  291. this._recording = false;
  292. return this._functionCalls;
  293. },
  294. /**
  295. * Erases all the recorded function calls.
  296. * Calling `resumeRecording` or `pauseRecording` does not erase history.
  297. */
  298. eraseRecording: function () {
  299. this._functionCalls = [];
  300. },
  301. /**
  302. * Invoked whenever the current tab actor's document global is created.
  303. */
  304. _onGlobalCreated: function ({window, id, isTopLevel}) {
  305. if (!this._initialized) {
  306. return;
  307. }
  308. // TODO: bug 981748, support more than just the top-level documents.
  309. if (!isTopLevel) {
  310. return;
  311. }
  312. let self = this;
  313. this._tracedWindowId = id;
  314. let unwrappedWindow = XPCNativeWrapper.unwrap(window);
  315. let callback = this._onContentFunctionCall;
  316. for (let global of this._tracedGlobals) {
  317. let prototype = unwrappedWindow[global].prototype;
  318. let properties = Object.keys(prototype);
  319. properties.forEach(name => overrideSymbol(global, prototype, name, callback));
  320. }
  321. for (let name of this._tracedFunctions) {
  322. overrideSymbol("window", unwrappedWindow, name, callback);
  323. }
  324. /**
  325. * Instruments a method, getter or setter on the specified target object to
  326. * invoke a callback whenever it is called.
  327. */
  328. function overrideSymbol(global, target, name, callback) {
  329. let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name);
  330. if (propertyDescriptor.get || propertyDescriptor.set) {
  331. overrideAccessor(global, target, name, propertyDescriptor, callback);
  332. return;
  333. }
  334. if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") {
  335. overrideFunction(global, target, name, propertyDescriptor, callback);
  336. return;
  337. }
  338. }
  339. /**
  340. * Instruments a function on the specified target object.
  341. */
  342. function overrideFunction(global, target, name, descriptor, callback) {
  343. // Invoking .apply on an unxrayed content function doesn't work, because
  344. // the arguments array is inaccessible to it. Get Xrays back.
  345. let originalFunc = Cu.unwaiveXrays(target[name]);
  346. Cu.exportFunction(function (...args) {
  347. let result;
  348. try {
  349. result = Cu.waiveXrays(originalFunc.apply(this, args));
  350. } catch (e) {
  351. throw createContentError(e, unwrappedWindow);
  352. }
  353. if (self._recording) {
  354. let type = CallWatcherFront.METHOD_FUNCTION;
  355. let stack = getStack(name);
  356. let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch;
  357. callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, result);
  358. }
  359. return result;
  360. }, target, { defineAs: name });
  361. Object.defineProperty(target, name, {
  362. configurable: descriptor.configurable,
  363. enumerable: descriptor.enumerable,
  364. writable: true
  365. });
  366. }
  367. /**
  368. * Instruments a getter or setter on the specified target object.
  369. */
  370. function overrideAccessor(global, target, name, descriptor, callback) {
  371. // Invoking .apply on an unxrayed content function doesn't work, because
  372. // the arguments array is inaccessible to it. Get Xrays back.
  373. let originalGetter = Cu.unwaiveXrays(target.__lookupGetter__(name));
  374. let originalSetter = Cu.unwaiveXrays(target.__lookupSetter__(name));
  375. Object.defineProperty(target, name, {
  376. get: function (...args) {
  377. if (!originalGetter) return undefined;
  378. let result = Cu.waiveXrays(originalGetter.apply(this, args));
  379. if (self._recording) {
  380. let type = CallWatcherFront.GETTER_FUNCTION;
  381. let stack = getStack(name);
  382. let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch;
  383. callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, result);
  384. }
  385. return result;
  386. },
  387. set: function (...args) {
  388. if (!originalSetter) return;
  389. originalSetter.apply(this, args);
  390. if (self._recording) {
  391. let type = CallWatcherFront.SETTER_FUNCTION;
  392. let stack = getStack(name);
  393. let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch;
  394. callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, undefined);
  395. }
  396. },
  397. configurable: descriptor.configurable,
  398. enumerable: descriptor.enumerable
  399. });
  400. }
  401. /**
  402. * Stores the relevant information about calls on the stack when
  403. * a function is called.
  404. */
  405. function getStack(caller) {
  406. try {
  407. // Using Components.stack wouldn't be a better idea, since it's
  408. // much slower because it attempts to retrieve the C++ stack as well.
  409. throw new Error();
  410. } catch (e) {
  411. var stack = e.stack;
  412. }
  413. // Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be
  414. // much prettier, but this is a very hot function, so let's sqeeze
  415. // every drop of performance out of it.
  416. let calls = [];
  417. let callIndex = 0;
  418. let currNewLinePivot = stack.indexOf("\n") + 1;
  419. let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
  420. while (nextNewLinePivot > 0) {
  421. let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot);
  422. let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1);
  423. let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1);
  424. if (!calls[callIndex]) {
  425. calls[callIndex] = { name: "", file: "", line: 0 };
  426. }
  427. if (!calls[callIndex + 1]) {
  428. calls[callIndex + 1] = { name: "", file: "", line: 0 };
  429. }
  430. if (callIndex > 0) {
  431. let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex);
  432. let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex);
  433. let name = stack.substring(currNewLinePivot, nameDelimiterIndex);
  434. calls[callIndex].name = name;
  435. calls[callIndex - 1].file = file;
  436. calls[callIndex - 1].line = line;
  437. } else {
  438. // Since the topmost stack frame is actually our overwritten function,
  439. // it will not have the expected name.
  440. calls[0].name = caller;
  441. }
  442. currNewLinePivot = nextNewLinePivot + 1;
  443. nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
  444. callIndex++;
  445. }
  446. return calls;
  447. }
  448. },
  449. /**
  450. * Invoked whenever the current tab actor's inner window is destroyed.
  451. */
  452. _onGlobalDestroyed: function ({window, id, isTopLevel}) {
  453. if (this._tracedWindowId == id) {
  454. this.pauseRecording();
  455. this.eraseRecording();
  456. this._timestampEpoch = 0;
  457. }
  458. },
  459. /**
  460. * Invoked whenever an instrumented function is called.
  461. */
  462. _onContentFunctionCall: function (...details) {
  463. // If the consuming tool has finalized call-watcher, ignore the
  464. // still-instrumented calls.
  465. if (this._finalized) {
  466. return;
  467. }
  468. let functionCall = new FunctionCallActor(this.conn, details, this._holdWeak);
  469. if (this._storeCalls) {
  470. this._functionCalls.push(functionCall);
  471. }
  472. if (this.onCall) {
  473. this.onCall(functionCall);
  474. } else {
  475. emit(this, "call", functionCall);
  476. }
  477. }
  478. });
  479. /**
  480. * A lookup table for cross-referencing flags or properties with their name
  481. * assuming they look LIKE_THIS most of the time.
  482. *
  483. * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed
  484. * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT".
  485. */
  486. var gEnumRegex = /^[A-Z][A-Z0-9_]+$/;
  487. var gEnumsLookupTable = {};
  488. // These values are returned from errors, or empty values,
  489. // and need to be ignored when checking arguments due to the bitwise math.
  490. var INVALID_ENUMS = [
  491. "INVALID_ENUM", "NO_ERROR", "INVALID_VALUE", "OUT_OF_MEMORY", "NONE"
  492. ];
  493. function getBitToEnumValue(type, object, arg) {
  494. let table = gEnumsLookupTable[type];
  495. // If mapping not yet created, do it on the first run.
  496. if (!table) {
  497. table = gEnumsLookupTable[type] = {};
  498. for (let key in object) {
  499. if (key.match(gEnumRegex)) {
  500. // Maps `16384` to `"COLOR_BUFFER_BIT"`, etc.
  501. table[object[key]] = key;
  502. }
  503. }
  504. }
  505. // If a single bit value, just return it.
  506. if (table[arg]) {
  507. return table[arg];
  508. }
  509. // Otherwise, attempt to reduce it to the original bit flags:
  510. // `16640` -> "COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT"
  511. let flags = [];
  512. for (let flag in table) {
  513. if (INVALID_ENUMS.indexOf(table[flag]) !== -1) {
  514. continue;
  515. }
  516. // Cast to integer as all values are stored as strings
  517. // in `table`
  518. flag = flag | 0;
  519. if (flag && (arg & flag) === flag) {
  520. flags.push(table[flag]);
  521. }
  522. }
  523. // Cache the combined bitmask value
  524. return table[arg] = flags.join(" | ") || arg;
  525. }
  526. /**
  527. * Creates a new error from an error that originated from content but was called
  528. * from a wrapped overridden method. This is so we can make our own error
  529. * that does not look like it originated from the call watcher.
  530. *
  531. * We use toolkit/loader's parseStack and serializeStack rather than the
  532. * parsing done in the local `getStack` function, because it does not expose
  533. * column number, would have to change the protocol models `call-stack-items` and `call-details`
  534. * which hurts backwards compatibility, and the local `getStack` is an optimized, hot function.
  535. */
  536. function createContentError(e, win) {
  537. let { message, name, stack } = e;
  538. let parsedStack = parseStack(stack);
  539. let { fileName, lineNumber, columnNumber } = parsedStack[parsedStack.length - 1];
  540. let error;
  541. let isDOMException = e instanceof Ci.nsIDOMDOMException;
  542. let constructor = isDOMException ? win.DOMException : (win[e.name] || win.Error);
  543. if (isDOMException) {
  544. error = new constructor(message, name);
  545. Object.defineProperties(error, {
  546. code: { value: e.code },
  547. columnNumber: { value: 0 }, // columnNumber is always 0 for DOMExceptions?
  548. filename: { value: fileName }, // note the lowercase `filename`
  549. lineNumber: { value: lineNumber },
  550. result: { value: e.result },
  551. stack: { value: serializeStack(parsedStack) }
  552. });
  553. }
  554. else {
  555. // Constructing an error here retains all the stack information,
  556. // and we can add message, fileName and lineNumber via constructor, though
  557. // need to manually add columnNumber.
  558. error = new constructor(message, fileName, lineNumber);
  559. Object.defineProperty(error, "columnNumber", {
  560. value: columnNumber
  561. });
  562. }
  563. return error;
  564. }