canvasdebugger.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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 file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
  6. const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
  7. const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
  8. const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
  9. const promise = require("promise");
  10. const Services = require("Services");
  11. const EventEmitter = require("devtools/shared/event-emitter");
  12. const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
  13. const { CanvasFront } = require("devtools/shared/fronts/canvas");
  14. const DevToolsUtils = require("devtools/shared/DevToolsUtils");
  15. const flags = require("devtools/shared/flags");
  16. const { LocalizationHelper } = require("devtools/shared/l10n");
  17. const { PluralForm } = require("devtools/shared/plural-form");
  18. const { Heritage, WidgetMethods, setNamedTimeout, clearNamedTimeout,
  19. setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers");
  20. const CANVAS_ACTOR_RECORDING_ATTEMPT = flags.testing ? 500 : 5000;
  21. const { Task } = require("devtools/shared/task");
  22. XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
  23. "resource://gre/modules/FileUtils.jsm");
  24. XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
  25. "resource://gre/modules/NetUtil.jsm");
  26. XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function () {
  27. return require("devtools/shared/webconsole/network-helper");
  28. });
  29. // The panel's window global is an EventEmitter firing the following events:
  30. const EVENTS = {
  31. // When the UI is reset from tab navigation.
  32. UI_RESET: "CanvasDebugger:UIReset",
  33. // When all the animation frame snapshots are removed by the user.
  34. SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared",
  35. // When an animation frame snapshot starts/finishes being recorded, and
  36. // whether it was completed succesfully or cancelled.
  37. SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted",
  38. SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished",
  39. SNAPSHOT_RECORDING_COMPLETED: "CanvasDebugger:SnapshotRecordingCompleted",
  40. SNAPSHOT_RECORDING_CANCELLED: "CanvasDebugger:SnapshotRecordingCancelled",
  41. // When an animation frame snapshot was selected and all its data displayed.
  42. SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected",
  43. // After all the function calls associated with an animation frame snapshot
  44. // are displayed in the UI.
  45. CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated",
  46. // After the stack associated with a call in an animation frame snapshot
  47. // is displayed in the UI.
  48. CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed",
  49. // After a screenshot associated with a call in an animation frame snapshot
  50. // is displayed in the UI.
  51. CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed",
  52. // After all the thumbnails associated with an animation frame snapshot
  53. // are displayed in the UI.
  54. THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed",
  55. // When a source is shown in the JavaScript Debugger at a specific location.
  56. SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger",
  57. SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger"
  58. };
  59. XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
  60. const HTML_NS = "http://www.w3.org/1999/xhtml";
  61. const STRINGS_URI = "devtools/client/locales/canvasdebugger.properties";
  62. const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
  63. const SNAPSHOT_START_RECORDING_DELAY = 10; // ms
  64. const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms
  65. const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms
  66. const SCREENSHOT_DISPLAY_DELAY = 100; // ms
  67. const STACK_FUNC_INDENTATION = 14; // px
  68. // This identifier string is simply used to tentatively ascertain whether or not
  69. // a JSON loaded from disk is actually something generated by this tool or not.
  70. // It isn't, of course, a definitive verification, but a Good Enough™
  71. // approximation before continuing the import. Don't localize this.
  72. const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot";
  73. const CALLS_LIST_SERIALIZER_VERSION = 1;
  74. const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms
  75. /**
  76. * The current target and the Canvas front, set by this tool's host.
  77. */
  78. var gToolbox, gTarget, gFront;
  79. /**
  80. * Initializes the canvas debugger controller and views.
  81. */
  82. function startupCanvasDebugger() {
  83. return promise.all([
  84. EventsHandler.initialize(),
  85. SnapshotsListView.initialize(),
  86. CallsListView.initialize()
  87. ]);
  88. }
  89. /**
  90. * Destroys the canvas debugger controller and views.
  91. */
  92. function shutdownCanvasDebugger() {
  93. return promise.all([
  94. EventsHandler.destroy(),
  95. SnapshotsListView.destroy(),
  96. CallsListView.destroy()
  97. ]);
  98. }
  99. /**
  100. * Functions handling target-related lifetime events.
  101. */
  102. var EventsHandler = {
  103. /**
  104. * Listen for events emitted by the current tab target.
  105. */
  106. initialize: function () {
  107. // Make sure the backend is prepared to handle <canvas> contexts.
  108. // Since actors are created lazily on the first request to them, we need to send an
  109. // early request to ensure the CallWatcherActor is running and watching for new window
  110. // globals.
  111. gFront.setup({ reload: false });
  112. this._onTabNavigated = this._onTabNavigated.bind(this);
  113. gTarget.on("will-navigate", this._onTabNavigated);
  114. gTarget.on("navigate", this._onTabNavigated);
  115. },
  116. /**
  117. * Remove events emitted by the current tab target.
  118. */
  119. destroy: function () {
  120. gTarget.off("will-navigate", this._onTabNavigated);
  121. gTarget.off("navigate", this._onTabNavigated);
  122. },
  123. /**
  124. * Called for each location change in the debugged tab.
  125. */
  126. _onTabNavigated: function (event) {
  127. if (event != "will-navigate") {
  128. return;
  129. }
  130. // Reset UI.
  131. SnapshotsListView.empty();
  132. CallsListView.empty();
  133. $("#record-snapshot").removeAttribute("checked");
  134. $("#record-snapshot").removeAttribute("disabled");
  135. $("#record-snapshot").hidden = false;
  136. $("#reload-notice").hidden = true;
  137. $("#empty-notice").hidden = false;
  138. $("#waiting-notice").hidden = true;
  139. $("#debugging-pane-contents").hidden = true;
  140. $("#screenshot-container").hidden = true;
  141. $("#snapshot-filmstrip").hidden = true;
  142. window.emit(EVENTS.UI_RESET);
  143. }
  144. };
  145. /**
  146. * Localization convenience methods.
  147. */
  148. var L10N = new LocalizationHelper(STRINGS_URI);
  149. var SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
  150. /**
  151. * Convenient way of emitting events from the panel window.
  152. */
  153. EventEmitter.decorate(this);
  154. /**
  155. * DOM query helpers.
  156. */
  157. var $ = (selector, target = document) => target.querySelector(selector);
  158. var $all = (selector, target = document) => target.querySelectorAll(selector);
  159. /**
  160. * Gets the fileName part of a string which happens to be an URL.
  161. */
  162. function getFileName(url) {
  163. try {
  164. let { fileName } = NetworkHelper.nsIURL(url);
  165. return fileName || "/";
  166. } catch (e) {
  167. // This doesn't look like a url, or nsIURL can't handle it.
  168. return "";
  169. }
  170. }
  171. /**
  172. * Gets an image data object containing a buffer large enough to hold
  173. * width * height pixels.
  174. *
  175. * This method avoids allocating memory and tries to reuse a common buffer
  176. * as much as possible.
  177. *
  178. * @param number w
  179. * The desired image data storage width.
  180. * @param number h
  181. * The desired image data storage height.
  182. * @return ImageData
  183. * The requested image data buffer.
  184. */
  185. function getImageDataStorage(ctx, w, h) {
  186. let storage = getImageDataStorage.cache;
  187. if (storage && storage.width == w && storage.height == h) {
  188. return storage;
  189. }
  190. return getImageDataStorage.cache = ctx.createImageData(w, h);
  191. }
  192. // The cache used in the `getImageDataStorage` function.
  193. getImageDataStorage.cache = null;
  194. /**
  195. * Draws image data into a canvas.
  196. *
  197. * This method makes absolutely no assumptions about the canvas element
  198. * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels.
  199. *
  200. * @param HTMLCanvasElement canvas
  201. * The canvas element to put the image data into.
  202. * @param number width
  203. * The image data width.
  204. * @param number height
  205. * The image data height.
  206. * @param array pixels
  207. * An array buffer view of the image data.
  208. * @param object options
  209. * Additional options supported by this operation:
  210. * - centered: specifies whether the image data should be centered
  211. * when copied in the canvas; this is useful when the
  212. * supplied pixels don't completely cover the canvas.
  213. */
  214. function drawImage(canvas, width, height, pixels, options = {}) {
  215. let ctx = canvas.getContext("2d");
  216. // FrameSnapshot actors return "snapshot-image" type instances with just an
  217. // empty pixel array if the source image is completely transparent.
  218. if (pixels.length <= 1) {
  219. ctx.clearRect(0, 0, canvas.width, canvas.height);
  220. return;
  221. }
  222. let imageData = getImageDataStorage(ctx, width, height);
  223. imageData.data.set(pixels);
  224. if (options.centered) {
  225. let left = (canvas.width - width) / 2;
  226. let top = (canvas.height - height) / 2;
  227. ctx.putImageData(imageData, left, top);
  228. } else {
  229. ctx.putImageData(imageData, 0, 0);
  230. }
  231. }
  232. /**
  233. * Draws image data into a canvas, and sets that as the rendering source for
  234. * an element with the specified id as the -moz-element background image.
  235. *
  236. * @param string id
  237. * The id of the -moz-element background image.
  238. * @param number width
  239. * The image data width.
  240. * @param number height
  241. * The image data height.
  242. * @param array pixels
  243. * An array buffer view of the image data.
  244. */
  245. function drawBackground(id, width, height, pixels) {
  246. let canvas = document.createElementNS(HTML_NS, "canvas");
  247. canvas.width = width;
  248. canvas.height = height;
  249. drawImage(canvas, width, height, pixels);
  250. document.mozSetImageElement(id, canvas);
  251. // Used in tests. Not emitting an event because this shouldn't be "interesting".
  252. if (window._onMozSetImageElement) {
  253. window._onMozSetImageElement(pixels);
  254. }
  255. }
  256. /**
  257. * Iterates forward to find the next draw call in a snapshot.
  258. */
  259. function getNextDrawCall(calls, call) {
  260. for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) {
  261. let nextCall = calls[i];
  262. let name = nextCall.attachment.actor.name;
  263. if (CanvasFront.DRAW_CALLS.has(name)) {
  264. return nextCall;
  265. }
  266. }
  267. return null;
  268. }
  269. /**
  270. * Iterates backwards to find the most recent screenshot for a function call
  271. * in a snapshot loaded from disk.
  272. */
  273. function getScreenshotFromCallLoadedFromDisk(calls, call) {
  274. for (let i = calls.indexOf(call); i >= 0; i--) {
  275. let prevCall = calls[i];
  276. let screenshot = prevCall.screenshot;
  277. if (screenshot) {
  278. return screenshot;
  279. }
  280. }
  281. return CanvasFront.INVALID_SNAPSHOT_IMAGE;
  282. }
  283. /**
  284. * Iterates backwards to find the most recent thumbnail for a function call.
  285. */
  286. function getThumbnailForCall(thumbnails, index) {
  287. for (let i = thumbnails.length - 1; i >= 0; i--) {
  288. let thumbnail = thumbnails[i];
  289. if (thumbnail.index <= index) {
  290. return thumbnail;
  291. }
  292. }
  293. return CanvasFront.INVALID_SNAPSHOT_IMAGE;
  294. }