browser/devtools/canvasdebugger/canvasdebugger.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.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4 "use strict";
michael@0 5
michael@0 6 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
michael@0 7
michael@0 8 Cu.import("resource://gre/modules/Services.jsm");
michael@0 9 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 10 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
michael@0 11 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
michael@0 12
michael@0 13 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
michael@0 14 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
michael@0 15 const EventEmitter = require("devtools/toolkit/event-emitter");
michael@0 16 const { CallWatcherFront } = require("devtools/server/actors/call-watcher");
michael@0 17 const { CanvasFront } = require("devtools/server/actors/canvas");
michael@0 18
michael@0 19 XPCOMUtils.defineLazyModuleGetter(this, "Task",
michael@0 20 "resource://gre/modules/Task.jsm");
michael@0 21
michael@0 22 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
michael@0 23 "resource://gre/modules/PluralForm.jsm");
michael@0 24
michael@0 25 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
michael@0 26 "resource://gre/modules/FileUtils.jsm");
michael@0 27
michael@0 28 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 29 "resource://gre/modules/NetUtil.jsm");
michael@0 30
michael@0 31 XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
michael@0 32 "resource://gre/modules/devtools/DevToolsUtils.jsm");
michael@0 33
michael@0 34 // The panel's window global is an EventEmitter firing the following events:
michael@0 35 const EVENTS = {
michael@0 36 // When the UI is reset from tab navigation.
michael@0 37 UI_RESET: "CanvasDebugger:UIReset",
michael@0 38
michael@0 39 // When all the animation frame snapshots are removed by the user.
michael@0 40 SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared",
michael@0 41
michael@0 42 // When an animation frame snapshot starts/finishes being recorded.
michael@0 43 SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted",
michael@0 44 SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished",
michael@0 45
michael@0 46 // When an animation frame snapshot was selected and all its data displayed.
michael@0 47 SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected",
michael@0 48
michael@0 49 // After all the function calls associated with an animation frame snapshot
michael@0 50 // are displayed in the UI.
michael@0 51 CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated",
michael@0 52
michael@0 53 // After the stack associated with a call in an animation frame snapshot
michael@0 54 // is displayed in the UI.
michael@0 55 CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed",
michael@0 56
michael@0 57 // After a screenshot associated with a call in an animation frame snapshot
michael@0 58 // is displayed in the UI.
michael@0 59 CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed",
michael@0 60
michael@0 61 // After all the thumbnails associated with an animation frame snapshot
michael@0 62 // are displayed in the UI.
michael@0 63 THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed",
michael@0 64
michael@0 65 // When a source is shown in the JavaScript Debugger at a specific location.
michael@0 66 SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger",
michael@0 67 SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger"
michael@0 68 };
michael@0 69
michael@0 70 const HTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 71 const STRINGS_URI = "chrome://browser/locale/devtools/canvasdebugger.properties"
michael@0 72
michael@0 73 const SNAPSHOT_START_RECORDING_DELAY = 10; // ms
michael@0 74 const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms
michael@0 75 const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms
michael@0 76 const SCREENSHOT_DISPLAY_DELAY = 100; // ms
michael@0 77 const STACK_FUNC_INDENTATION = 14; // px
michael@0 78
michael@0 79 // This identifier string is simply used to tentatively ascertain whether or not
michael@0 80 // a JSON loaded from disk is actually something generated by this tool or not.
michael@0 81 // It isn't, of course, a definitive verification, but a Good Enoughâ„¢
michael@0 82 // approximation before continuing the import. Don't localize this.
michael@0 83 const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot";
michael@0 84 const CALLS_LIST_SERIALIZER_VERSION = 1;
michael@0 85 const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms
michael@0 86
michael@0 87 /**
michael@0 88 * The current target and the Canvas front, set by this tool's host.
michael@0 89 */
michael@0 90 let gToolbox, gTarget, gFront;
michael@0 91
michael@0 92 /**
michael@0 93 * Initializes the canvas debugger controller and views.
michael@0 94 */
michael@0 95 function startupCanvasDebugger() {
michael@0 96 return promise.all([
michael@0 97 EventsHandler.initialize(),
michael@0 98 SnapshotsListView.initialize(),
michael@0 99 CallsListView.initialize()
michael@0 100 ]);
michael@0 101 }
michael@0 102
michael@0 103 /**
michael@0 104 * Destroys the canvas debugger controller and views.
michael@0 105 */
michael@0 106 function shutdownCanvasDebugger() {
michael@0 107 return promise.all([
michael@0 108 EventsHandler.destroy(),
michael@0 109 SnapshotsListView.destroy(),
michael@0 110 CallsListView.destroy()
michael@0 111 ]);
michael@0 112 }
michael@0 113
michael@0 114 /**
michael@0 115 * Functions handling target-related lifetime events.
michael@0 116 */
michael@0 117 let EventsHandler = {
michael@0 118 /**
michael@0 119 * Listen for events emitted by the current tab target.
michael@0 120 */
michael@0 121 initialize: function() {
michael@0 122 this._onTabNavigated = this._onTabNavigated.bind(this);
michael@0 123 gTarget.on("will-navigate", this._onTabNavigated);
michael@0 124 gTarget.on("navigate", this._onTabNavigated);
michael@0 125 },
michael@0 126
michael@0 127 /**
michael@0 128 * Remove events emitted by the current tab target.
michael@0 129 */
michael@0 130 destroy: function() {
michael@0 131 gTarget.off("will-navigate", this._onTabNavigated);
michael@0 132 gTarget.off("navigate", this._onTabNavigated);
michael@0 133 },
michael@0 134
michael@0 135 /**
michael@0 136 * Called for each location change in the debugged tab.
michael@0 137 */
michael@0 138 _onTabNavigated: function(event) {
michael@0 139 if (event != "will-navigate") {
michael@0 140 return;
michael@0 141 }
michael@0 142 // Make sure the backend is prepared to handle <canvas> contexts.
michael@0 143 gFront.setup({ reload: false });
michael@0 144
michael@0 145 // Reset UI.
michael@0 146 SnapshotsListView.empty();
michael@0 147 CallsListView.empty();
michael@0 148
michael@0 149 $("#record-snapshot").removeAttribute("checked");
michael@0 150 $("#record-snapshot").removeAttribute("disabled");
michael@0 151 $("#record-snapshot").hidden = false;
michael@0 152
michael@0 153 $("#reload-notice").hidden = true;
michael@0 154 $("#empty-notice").hidden = false;
michael@0 155 $("#import-notice").hidden = true;
michael@0 156
michael@0 157 $("#debugging-pane-contents").hidden = true;
michael@0 158 $("#screenshot-container").hidden = true;
michael@0 159 $("#snapshot-filmstrip").hidden = true;
michael@0 160
michael@0 161 window.emit(EVENTS.UI_RESET);
michael@0 162 }
michael@0 163 };
michael@0 164
michael@0 165 /**
michael@0 166 * Functions handling the recorded animation frame snapshots UI.
michael@0 167 */
michael@0 168 let SnapshotsListView = Heritage.extend(WidgetMethods, {
michael@0 169 /**
michael@0 170 * Initialization function, called when the tool is started.
michael@0 171 */
michael@0 172 initialize: function() {
michael@0 173 this.widget = new SideMenuWidget($("#snapshots-list"), {
michael@0 174 showArrows: true
michael@0 175 });
michael@0 176
michael@0 177 this._onSelect = this._onSelect.bind(this);
michael@0 178 this._onClearButtonClick = this._onClearButtonClick.bind(this);
michael@0 179 this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
michael@0 180 this._onImportButtonClick = this._onImportButtonClick.bind(this);
michael@0 181 this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
michael@0 182
michael@0 183 this.emptyText = L10N.getStr("noSnapshotsText");
michael@0 184 this.widget.addEventListener("select", this._onSelect, false);
michael@0 185 },
michael@0 186
michael@0 187 /**
michael@0 188 * Destruction function, called when the tool is closed.
michael@0 189 */
michael@0 190 destroy: function() {
michael@0 191 this.widget.removeEventListener("select", this._onSelect, false);
michael@0 192 },
michael@0 193
michael@0 194 /**
michael@0 195 * Adds a snapshot entry to this container.
michael@0 196 *
michael@0 197 * @return object
michael@0 198 * The newly inserted item.
michael@0 199 */
michael@0 200 addSnapshot: function() {
michael@0 201 let contents = document.createElement("hbox");
michael@0 202 contents.className = "snapshot-item";
michael@0 203
michael@0 204 let thumbnail = document.createElementNS(HTML_NS, "canvas");
michael@0 205 thumbnail.className = "snapshot-item-thumbnail";
michael@0 206 thumbnail.width = CanvasFront.THUMBNAIL_HEIGHT;
michael@0 207 thumbnail.height = CanvasFront.THUMBNAIL_HEIGHT;
michael@0 208
michael@0 209 let title = document.createElement("label");
michael@0 210 title.className = "plain snapshot-item-title";
michael@0 211 title.setAttribute("value",
michael@0 212 L10N.getFormatStr("snapshotsList.itemLabel", this.itemCount + 1));
michael@0 213
michael@0 214 let calls = document.createElement("label");
michael@0 215 calls.className = "plain snapshot-item-calls";
michael@0 216 calls.setAttribute("value",
michael@0 217 L10N.getStr("snapshotsList.loadingLabel"));
michael@0 218
michael@0 219 let save = document.createElement("label");
michael@0 220 save.className = "plain snapshot-item-save";
michael@0 221 save.addEventListener("click", this._onSaveButtonClick, false);
michael@0 222
michael@0 223 let spacer = document.createElement("spacer");
michael@0 224 spacer.setAttribute("flex", "1");
michael@0 225
michael@0 226 let footer = document.createElement("hbox");
michael@0 227 footer.className = "snapshot-item-footer";
michael@0 228 footer.appendChild(save);
michael@0 229
michael@0 230 let details = document.createElement("vbox");
michael@0 231 details.className = "snapshot-item-details";
michael@0 232 details.appendChild(title);
michael@0 233 details.appendChild(calls);
michael@0 234 details.appendChild(spacer);
michael@0 235 details.appendChild(footer);
michael@0 236
michael@0 237 contents.appendChild(thumbnail);
michael@0 238 contents.appendChild(details);
michael@0 239
michael@0 240 // Append a recorded snapshot item to this container.
michael@0 241 return this.push([contents], {
michael@0 242 attachment: {
michael@0 243 // The snapshot and function call actors, along with the thumbnails
michael@0 244 // will be available as soon as recording finishes.
michael@0 245 actor: null,
michael@0 246 calls: null,
michael@0 247 thumbnails: null,
michael@0 248 screenshot: null
michael@0 249 }
michael@0 250 });
michael@0 251 },
michael@0 252
michael@0 253 /**
michael@0 254 * Customizes a shapshot in this container.
michael@0 255 *
michael@0 256 * @param Item snapshotItem
michael@0 257 * An item inserted via `SnapshotsListView.addSnapshot`.
michael@0 258 * @param object snapshotActor
michael@0 259 * The frame snapshot actor received from the backend.
michael@0 260 * @param object snapshotOverview
michael@0 261 * Additional data about the snapshot received from the backend.
michael@0 262 */
michael@0 263 customizeSnapshot: function(snapshotItem, snapshotActor, snapshotOverview) {
michael@0 264 // Make sure the function call actors are stored on the item,
michael@0 265 // to be used when populating the CallsListView.
michael@0 266 snapshotItem.attachment.actor = snapshotActor;
michael@0 267 let functionCalls = snapshotItem.attachment.calls = snapshotOverview.calls;
michael@0 268 let thumbnails = snapshotItem.attachment.thumbnails = snapshotOverview.thumbnails;
michael@0 269 let screenshot = snapshotItem.attachment.screenshot = snapshotOverview.screenshot;
michael@0 270
michael@0 271 let lastThumbnail = thumbnails[thumbnails.length - 1];
michael@0 272 let { width, height, flipped, pixels } = lastThumbnail;
michael@0 273
michael@0 274 let thumbnailNode = $(".snapshot-item-thumbnail", snapshotItem.target);
michael@0 275 thumbnailNode.setAttribute("flipped", flipped);
michael@0 276 drawImage(thumbnailNode, width, height, pixels, { centered: true });
michael@0 277
michael@0 278 let callsNode = $(".snapshot-item-calls", snapshotItem.target);
michael@0 279 let drawCalls = functionCalls.filter(e => CanvasFront.DRAW_CALLS.has(e.name));
michael@0 280
michael@0 281 let drawCallsStr = PluralForm.get(drawCalls.length,
michael@0 282 L10N.getStr("snapshotsList.drawCallsLabel"));
michael@0 283 let funcCallsStr = PluralForm.get(functionCalls.length,
michael@0 284 L10N.getStr("snapshotsList.functionCallsLabel"));
michael@0 285
michael@0 286 callsNode.setAttribute("value",
michael@0 287 drawCallsStr.replace("#1", drawCalls.length) + ", " +
michael@0 288 funcCallsStr.replace("#1", functionCalls.length));
michael@0 289
michael@0 290 let saveNode = $(".snapshot-item-save", snapshotItem.target);
michael@0 291 saveNode.setAttribute("disabled", !!snapshotItem.isLoadedFromDisk);
michael@0 292 saveNode.setAttribute("value", snapshotItem.isLoadedFromDisk
michael@0 293 ? L10N.getStr("snapshotsList.loadedLabel")
michael@0 294 : L10N.getStr("snapshotsList.saveLabel"));
michael@0 295
michael@0 296 // Make sure there's always a selected item available.
michael@0 297 if (!this.selectedItem) {
michael@0 298 this.selectedIndex = 0;
michael@0 299 }
michael@0 300 },
michael@0 301
michael@0 302 /**
michael@0 303 * The select listener for this container.
michael@0 304 */
michael@0 305 _onSelect: function({ detail: snapshotItem }) {
michael@0 306 if (!snapshotItem) {
michael@0 307 return;
michael@0 308 }
michael@0 309 let { calls, thumbnails, screenshot } = snapshotItem.attachment;
michael@0 310
michael@0 311 $("#reload-notice").hidden = true;
michael@0 312 $("#empty-notice").hidden = true;
michael@0 313 $("#import-notice").hidden = false;
michael@0 314
michael@0 315 $("#debugging-pane-contents").hidden = true;
michael@0 316 $("#screenshot-container").hidden = true;
michael@0 317 $("#snapshot-filmstrip").hidden = true;
michael@0 318
michael@0 319 Task.spawn(function*() {
michael@0 320 // Wait for a few milliseconds between presenting the function calls,
michael@0 321 // screenshot and thumbnails, to allow each component being
michael@0 322 // sequentially drawn. This gives the illusion of snappiness.
michael@0 323
michael@0 324 yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
michael@0 325 CallsListView.showCalls(calls);
michael@0 326 $("#debugging-pane-contents").hidden = false;
michael@0 327 $("#import-notice").hidden = true;
michael@0 328
michael@0 329 yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
michael@0 330 CallsListView.showThumbnails(thumbnails);
michael@0 331 $("#snapshot-filmstrip").hidden = false;
michael@0 332
michael@0 333 yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
michael@0 334 CallsListView.showScreenshot(screenshot);
michael@0 335 $("#screenshot-container").hidden = false;
michael@0 336
michael@0 337 window.emit(EVENTS.SNAPSHOT_RECORDING_SELECTED);
michael@0 338 });
michael@0 339 },
michael@0 340
michael@0 341 /**
michael@0 342 * The click listener for the "clear" button in this container.
michael@0 343 */
michael@0 344 _onClearButtonClick: function() {
michael@0 345 Task.spawn(function*() {
michael@0 346 SnapshotsListView.empty();
michael@0 347 CallsListView.empty();
michael@0 348
michael@0 349 $("#reload-notice").hidden = true;
michael@0 350 $("#empty-notice").hidden = true;
michael@0 351 $("#import-notice").hidden = true;
michael@0 352
michael@0 353 if (yield gFront.isInitialized()) {
michael@0 354 $("#empty-notice").hidden = false;
michael@0 355 } else {
michael@0 356 $("#reload-notice").hidden = false;
michael@0 357 }
michael@0 358
michael@0 359 $("#debugging-pane-contents").hidden = true;
michael@0 360 $("#screenshot-container").hidden = true;
michael@0 361 $("#snapshot-filmstrip").hidden = true;
michael@0 362
michael@0 363 window.emit(EVENTS.SNAPSHOTS_LIST_CLEARED);
michael@0 364 });
michael@0 365 },
michael@0 366
michael@0 367 /**
michael@0 368 * The click listener for the "record" button in this container.
michael@0 369 */
michael@0 370 _onRecordButtonClick: function() {
michael@0 371 Task.spawn(function*() {
michael@0 372 $("#record-snapshot").setAttribute("checked", "true");
michael@0 373 $("#record-snapshot").setAttribute("disabled", "true");
michael@0 374
michael@0 375 // Insert a "dummy" snapshot item in the view, to hint that recording
michael@0 376 // has now started. However, wait for a few milliseconds before actually
michael@0 377 // starting the recording, since that might block rendering and prevent
michael@0 378 // the dummy snapshot item from being drawn.
michael@0 379 let snapshotItem = this.addSnapshot();
michael@0 380
michael@0 381 // If this is the first item, immediately show the "Loading…" notice.
michael@0 382 if (this.itemCount == 1) {
michael@0 383 $("#empty-notice").hidden = true;
michael@0 384 $("#import-notice").hidden = false;
michael@0 385 }
michael@0 386
michael@0 387 yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
michael@0 388 window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED);
michael@0 389
michael@0 390 let snapshotActor = yield gFront.recordAnimationFrame();
michael@0 391 let snapshotOverview = yield snapshotActor.getOverview();
michael@0 392 this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview);
michael@0 393
michael@0 394 $("#record-snapshot").removeAttribute("checked");
michael@0 395 $("#record-snapshot").removeAttribute("disabled");
michael@0 396
michael@0 397 window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
michael@0 398 }.bind(this));
michael@0 399 },
michael@0 400
michael@0 401 /**
michael@0 402 * The click listener for the "import" button in this container.
michael@0 403 */
michael@0 404 _onImportButtonClick: function() {
michael@0 405 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
michael@0 406 fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen);
michael@0 407 fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json");
michael@0 408 fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*");
michael@0 409
michael@0 410 if (fp.show() != Ci.nsIFilePicker.returnOK) {
michael@0 411 return;
michael@0 412 }
michael@0 413
michael@0 414 let channel = NetUtil.newChannel(fp.file);
michael@0 415 channel.contentType = "text/plain";
michael@0 416
michael@0 417 NetUtil.asyncFetch(channel, (inputStream, status) => {
michael@0 418 if (!Components.isSuccessCode(status)) {
michael@0 419 console.error("Could not import recorded animation frame snapshot file.");
michael@0 420 return;
michael@0 421 }
michael@0 422 try {
michael@0 423 let string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
michael@0 424 var data = JSON.parse(string);
michael@0 425 } catch (e) {
michael@0 426 console.error("Could not read animation frame snapshot file.");
michael@0 427 return;
michael@0 428 }
michael@0 429 if (data.fileType != CALLS_LIST_SERIALIZER_IDENTIFIER) {
michael@0 430 console.error("Unrecognized animation frame snapshot file.");
michael@0 431 return;
michael@0 432 }
michael@0 433
michael@0 434 // Add a `isLoadedFromDisk` flag on everything to avoid sending invalid
michael@0 435 // requests to the backend, since we're not dealing with actors anymore.
michael@0 436 let snapshotItem = this.addSnapshot();
michael@0 437 snapshotItem.isLoadedFromDisk = true;
michael@0 438 data.calls.forEach(e => e.isLoadedFromDisk = true);
michael@0 439
michael@0 440 // Create array buffers from the parsed pixel arrays.
michael@0 441 for (let thumbnail of data.thumbnails) {
michael@0 442 let thumbnailPixelsArray = thumbnail.pixels.split(",");
michael@0 443 thumbnail.pixels = new Uint32Array(thumbnailPixelsArray);
michael@0 444 }
michael@0 445 let screenshotPixelsArray = data.screenshot.pixels.split(",");
michael@0 446 data.screenshot.pixels = new Uint32Array(screenshotPixelsArray);
michael@0 447
michael@0 448 this.customizeSnapshot(snapshotItem, data.calls, data);
michael@0 449 });
michael@0 450 },
michael@0 451
michael@0 452 /**
michael@0 453 * The click listener for the "save" button of each item in this container.
michael@0 454 */
michael@0 455 _onSaveButtonClick: function(e) {
michael@0 456 let snapshotItem = this.getItemForElement(e.target);
michael@0 457
michael@0 458 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
michael@0 459 fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
michael@0 460 fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json");
michael@0 461 fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*");
michael@0 462 fp.defaultString = "snapshot.json";
michael@0 463
michael@0 464 // Start serializing all the function call actors for the specified snapshot,
michael@0 465 // while the nsIFilePicker dialog is being opened. Snappy.
michael@0 466 let serialized = Task.spawn(function*() {
michael@0 467 let data = {
michael@0 468 fileType: CALLS_LIST_SERIALIZER_IDENTIFIER,
michael@0 469 version: CALLS_LIST_SERIALIZER_VERSION,
michael@0 470 calls: [],
michael@0 471 thumbnails: [],
michael@0 472 screenshot: null
michael@0 473 };
michael@0 474 let functionCalls = snapshotItem.attachment.calls;
michael@0 475 let thumbnails = snapshotItem.attachment.thumbnails;
michael@0 476 let screenshot = snapshotItem.attachment.screenshot;
michael@0 477
michael@0 478 // Prepare all the function calls for serialization.
michael@0 479 yield DevToolsUtils.yieldingEach(functionCalls, (call, i) => {
michael@0 480 let { type, name, file, line, argsPreview, callerPreview } = call;
michael@0 481 return call.getDetails().then(({ stack }) => {
michael@0 482 data.calls[i] = {
michael@0 483 type: type,
michael@0 484 name: name,
michael@0 485 file: file,
michael@0 486 line: line,
michael@0 487 stack: stack,
michael@0 488 argsPreview: argsPreview,
michael@0 489 callerPreview: callerPreview
michael@0 490 };
michael@0 491 });
michael@0 492 });
michael@0 493
michael@0 494 // Prepare all the thumbnails for serialization.
michael@0 495 yield DevToolsUtils.yieldingEach(thumbnails, (thumbnail, i) => {
michael@0 496 let { index, width, height, flipped, pixels } = thumbnail;
michael@0 497 data.thumbnails.push({
michael@0 498 index: index,
michael@0 499 width: width,
michael@0 500 height: height,
michael@0 501 flipped: flipped,
michael@0 502 pixels: Array.join(pixels, ",")
michael@0 503 });
michael@0 504 });
michael@0 505
michael@0 506 // Prepare the screenshot for serialization.
michael@0 507 let { index, width, height, flipped, pixels } = screenshot;
michael@0 508 data.screenshot = {
michael@0 509 index: index,
michael@0 510 width: width,
michael@0 511 height: height,
michael@0 512 flipped: flipped,
michael@0 513 pixels: Array.join(pixels, ",")
michael@0 514 };
michael@0 515
michael@0 516 let string = JSON.stringify(data);
michael@0 517 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
michael@0 518 createInstance(Ci.nsIScriptableUnicodeConverter);
michael@0 519
michael@0 520 converter.charset = "UTF-8";
michael@0 521 return converter.convertToInputStream(string);
michael@0 522 });
michael@0 523
michael@0 524 // Open the nsIFilePicker and wait for the function call actors to finish
michael@0 525 // being serialized, in order to save the generated JSON data to disk.
michael@0 526 fp.open({ done: result => {
michael@0 527 if (result == Ci.nsIFilePicker.returnCancel) {
michael@0 528 return;
michael@0 529 }
michael@0 530 let footer = $(".snapshot-item-footer", snapshotItem.target);
michael@0 531 let save = $(".snapshot-item-save", snapshotItem.target);
michael@0 532
michael@0 533 // Show a throbber and a "Saving…" label if serializing isn't immediate.
michael@0 534 setNamedTimeout("call-list-save", CALLS_LIST_SLOW_SAVE_DELAY, () => {
michael@0 535 footer.setAttribute("saving", "");
michael@0 536 save.setAttribute("disabled", "true");
michael@0 537 save.setAttribute("value", L10N.getStr("snapshotsList.savingLabel"));
michael@0 538 });
michael@0 539
michael@0 540 serialized.then(inputStream => {
michael@0 541 let outputStream = FileUtils.openSafeFileOutputStream(fp.file);
michael@0 542
michael@0 543 NetUtil.asyncCopy(inputStream, outputStream, status => {
michael@0 544 if (!Components.isSuccessCode(status)) {
michael@0 545 console.error("Could not save recorded animation frame snapshot file.");
michael@0 546 }
michael@0 547 clearNamedTimeout("call-list-save");
michael@0 548 footer.removeAttribute("saving");
michael@0 549 save.removeAttribute("disabled");
michael@0 550 save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel"));
michael@0 551 });
michael@0 552 });
michael@0 553 }});
michael@0 554 }
michael@0 555 });
michael@0 556
michael@0 557 /**
michael@0 558 * Functions handling details about a single recorded animation frame snapshot
michael@0 559 * (the calls list, rendering preview, thumbnails filmstrip etc.).
michael@0 560 */
michael@0 561 let CallsListView = Heritage.extend(WidgetMethods, {
michael@0 562 /**
michael@0 563 * Initialization function, called when the tool is started.
michael@0 564 */
michael@0 565 initialize: function() {
michael@0 566 this.widget = new SideMenuWidget($("#calls-list"));
michael@0 567 this._slider = $("#calls-slider");
michael@0 568 this._searchbox = $("#calls-searchbox");
michael@0 569 this._filmstrip = $("#snapshot-filmstrip");
michael@0 570
michael@0 571 this._onSelect = this._onSelect.bind(this);
michael@0 572 this._onSlideMouseDown = this._onSlideMouseDown.bind(this);
michael@0 573 this._onSlideMouseUp = this._onSlideMouseUp.bind(this);
michael@0 574 this._onSlide = this._onSlide.bind(this);
michael@0 575 this._onSearch = this._onSearch.bind(this);
michael@0 576 this._onScroll = this._onScroll.bind(this);
michael@0 577 this._onExpand = this._onExpand.bind(this);
michael@0 578 this._onStackFileClick = this._onStackFileClick.bind(this);
michael@0 579 this._onThumbnailClick = this._onThumbnailClick.bind(this);
michael@0 580
michael@0 581 this.widget.addEventListener("select", this._onSelect, false);
michael@0 582 this._slider.addEventListener("mousedown", this._onSlideMouseDown, false);
michael@0 583 this._slider.addEventListener("mouseup", this._onSlideMouseUp, false);
michael@0 584 this._slider.addEventListener("change", this._onSlide, false);
michael@0 585 this._searchbox.addEventListener("input", this._onSearch, false);
michael@0 586 this._filmstrip.addEventListener("wheel", this._onScroll, false);
michael@0 587 },
michael@0 588
michael@0 589 /**
michael@0 590 * Destruction function, called when the tool is closed.
michael@0 591 */
michael@0 592 destroy: function() {
michael@0 593 this.widget.removeEventListener("select", this._onSelect, false);
michael@0 594 this._slider.removeEventListener("mousedown", this._onSlideMouseDown, false);
michael@0 595 this._slider.removeEventListener("mouseup", this._onSlideMouseUp, false);
michael@0 596 this._slider.removeEventListener("change", this._onSlide, false);
michael@0 597 this._searchbox.removeEventListener("input", this._onSearch, false);
michael@0 598 this._filmstrip.removeEventListener("wheel", this._onScroll, false);
michael@0 599 },
michael@0 600
michael@0 601 /**
michael@0 602 * Populates this container with a list of function calls.
michael@0 603 *
michael@0 604 * @param array functionCalls
michael@0 605 * A list of function call actors received from the backend.
michael@0 606 */
michael@0 607 showCalls: function(functionCalls) {
michael@0 608 this.empty();
michael@0 609
michael@0 610 for (let i = 0, len = functionCalls.length; i < len; i++) {
michael@0 611 let call = functionCalls[i];
michael@0 612
michael@0 613 let view = document.createElement("vbox");
michael@0 614 view.className = "call-item-view devtools-monospace";
michael@0 615 view.setAttribute("flex", "1");
michael@0 616
michael@0 617 let contents = document.createElement("hbox");
michael@0 618 contents.className = "call-item-contents";
michael@0 619 contents.setAttribute("align", "center");
michael@0 620 contents.addEventListener("dblclick", this._onExpand);
michael@0 621 view.appendChild(contents);
michael@0 622
michael@0 623 let index = document.createElement("label");
michael@0 624 index.className = "plain call-item-index";
michael@0 625 index.setAttribute("flex", "1");
michael@0 626 index.setAttribute("value", i + 1);
michael@0 627
michael@0 628 let gutter = document.createElement("hbox");
michael@0 629 gutter.className = "call-item-gutter";
michael@0 630 gutter.appendChild(index);
michael@0 631 contents.appendChild(gutter);
michael@0 632
michael@0 633 // Not all function calls have a caller that was stringified (e.g.
michael@0 634 // context calls have a "gl" or "ctx" caller preview).
michael@0 635 if (call.callerPreview) {
michael@0 636 let context = document.createElement("label");
michael@0 637 context.className = "plain call-item-context";
michael@0 638 context.setAttribute("value", call.callerPreview);
michael@0 639 contents.appendChild(context);
michael@0 640
michael@0 641 let separator = document.createElement("label");
michael@0 642 separator.className = "plain call-item-separator";
michael@0 643 separator.setAttribute("value", ".");
michael@0 644 contents.appendChild(separator);
michael@0 645 }
michael@0 646
michael@0 647 let name = document.createElement("label");
michael@0 648 name.className = "plain call-item-name";
michael@0 649 name.setAttribute("value", call.name);
michael@0 650 contents.appendChild(name);
michael@0 651
michael@0 652 let argsPreview = document.createElement("label");
michael@0 653 argsPreview.className = "plain call-item-args";
michael@0 654 argsPreview.setAttribute("crop", "end");
michael@0 655 argsPreview.setAttribute("flex", "100");
michael@0 656 // Getters and setters are displayed differently from regular methods.
michael@0 657 if (call.type == CallWatcherFront.METHOD_FUNCTION) {
michael@0 658 argsPreview.setAttribute("value", "(" + call.argsPreview + ")");
michael@0 659 } else {
michael@0 660 argsPreview.setAttribute("value", " = " + call.argsPreview);
michael@0 661 }
michael@0 662 contents.appendChild(argsPreview);
michael@0 663
michael@0 664 let location = document.createElement("label");
michael@0 665 location.className = "plain call-item-location";
michael@0 666 location.setAttribute("value", getFileName(call.file) + ":" + call.line);
michael@0 667 location.setAttribute("crop", "start");
michael@0 668 location.setAttribute("flex", "1");
michael@0 669 location.addEventListener("mousedown", this._onExpand);
michael@0 670 contents.appendChild(location);
michael@0 671
michael@0 672 // Append a function call item to this container.
michael@0 673 this.push([view], {
michael@0 674 staged: true,
michael@0 675 attachment: {
michael@0 676 actor: call
michael@0 677 }
michael@0 678 });
michael@0 679
michael@0 680 // Highlight certain calls that are probably more interesting than
michael@0 681 // everything else, making it easier to quickly glance over them.
michael@0 682 if (CanvasFront.DRAW_CALLS.has(call.name)) {
michael@0 683 view.setAttribute("draw-call", "");
michael@0 684 }
michael@0 685 if (CanvasFront.INTERESTING_CALLS.has(call.name)) {
michael@0 686 view.setAttribute("interesting-call", "");
michael@0 687 }
michael@0 688 }
michael@0 689
michael@0 690 // Flushes all the prepared function call items into this container.
michael@0 691 this.commit();
michael@0 692 window.emit(EVENTS.CALL_LIST_POPULATED);
michael@0 693
michael@0 694 // Resetting the function selection slider's value (shown in this
michael@0 695 // container's toolbar) would trigger a selection event, which should be
michael@0 696 // ignored in this case.
michael@0 697 this._ignoreSliderChanges = true;
michael@0 698 this._slider.value = 0;
michael@0 699 this._slider.max = functionCalls.length - 1;
michael@0 700 this._ignoreSliderChanges = false;
michael@0 701 },
michael@0 702
michael@0 703 /**
michael@0 704 * Displays an image in the rendering preview of this container, generated
michael@0 705 * for the specified draw call in the recorded animation frame snapshot.
michael@0 706 *
michael@0 707 * @param array screenshot
michael@0 708 * A single "snapshot-image" instance received from the backend.
michael@0 709 */
michael@0 710 showScreenshot: function(screenshot) {
michael@0 711 let { index, width, height, flipped, pixels } = screenshot;
michael@0 712
michael@0 713 let screenshotNode = $("#screenshot-image");
michael@0 714 screenshotNode.setAttribute("flipped", flipped);
michael@0 715 drawBackground("screenshot-rendering", width, height, pixels);
michael@0 716
michael@0 717 let dimensionsNode = $("#screenshot-dimensions");
michael@0 718 dimensionsNode.setAttribute("value", ~~width + " x " + ~~height);
michael@0 719
michael@0 720 window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED);
michael@0 721 },
michael@0 722
michael@0 723 /**
michael@0 724 * Populates this container's footer with a list of thumbnails, one generated
michael@0 725 * for each draw call in the recorded animation frame snapshot.
michael@0 726 *
michael@0 727 * @param array thumbnails
michael@0 728 * An array of "snapshot-image" instances received from the backend.
michael@0 729 */
michael@0 730 showThumbnails: function(thumbnails) {
michael@0 731 while (this._filmstrip.hasChildNodes()) {
michael@0 732 this._filmstrip.firstChild.remove();
michael@0 733 }
michael@0 734 for (let thumbnail of thumbnails) {
michael@0 735 this.appendThumbnail(thumbnail);
michael@0 736 }
michael@0 737
michael@0 738 window.emit(EVENTS.THUMBNAILS_DISPLAYED);
michael@0 739 },
michael@0 740
michael@0 741 /**
michael@0 742 * Displays an image in the thumbnails list of this container, generated
michael@0 743 * for the specified draw call in the recorded animation frame snapshot.
michael@0 744 *
michael@0 745 * @param array thumbnail
michael@0 746 * A single "snapshot-image" instance received from the backend.
michael@0 747 */
michael@0 748 appendThumbnail: function(thumbnail) {
michael@0 749 let { index, width, height, flipped, pixels } = thumbnail;
michael@0 750
michael@0 751 let thumbnailNode = document.createElementNS(HTML_NS, "canvas");
michael@0 752 thumbnailNode.setAttribute("flipped", flipped);
michael@0 753 thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_HEIGHT, width);
michael@0 754 thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_HEIGHT, height);
michael@0 755 drawImage(thumbnailNode, width, height, pixels, { centered: true });
michael@0 756
michael@0 757 thumbnailNode.className = "filmstrip-thumbnail";
michael@0 758 thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index);
michael@0 759 thumbnailNode.setAttribute("index", index);
michael@0 760 this._filmstrip.appendChild(thumbnailNode);
michael@0 761 },
michael@0 762
michael@0 763 /**
michael@0 764 * Sets the currently highlighted thumbnail in this container.
michael@0 765 * A screenshot will always correlate to a thumbnail in the filmstrip,
michael@0 766 * both being identified by the same 'index' of the context function call.
michael@0 767 *
michael@0 768 * @param number index
michael@0 769 * The context function call's index.
michael@0 770 */
michael@0 771 set highlightedThumbnail(index) {
michael@0 772 let currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']");
michael@0 773 if (currHighlightedThumbnail == null) {
michael@0 774 return;
michael@0 775 }
michael@0 776
michael@0 777 let prevIndex = this._highlightedThumbnailIndex
michael@0 778 let prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']");
michael@0 779 if (prevHighlightedThumbnail) {
michael@0 780 prevHighlightedThumbnail.removeAttribute("highlighted");
michael@0 781 }
michael@0 782
michael@0 783 currHighlightedThumbnail.setAttribute("highlighted", "");
michael@0 784 currHighlightedThumbnail.scrollIntoView();
michael@0 785 this._highlightedThumbnailIndex = index;
michael@0 786 },
michael@0 787
michael@0 788 /**
michael@0 789 * Gets the currently highlighted thumbnail in this container.
michael@0 790 * @return number
michael@0 791 */
michael@0 792 get highlightedThumbnail() {
michael@0 793 return this._highlightedThumbnailIndex;
michael@0 794 },
michael@0 795
michael@0 796 /**
michael@0 797 * The select listener for this container.
michael@0 798 */
michael@0 799 _onSelect: function({ detail: callItem }) {
michael@0 800 if (!callItem) {
michael@0 801 return;
michael@0 802 }
michael@0 803
michael@0 804 // Some of the stepping buttons don't make sense specifically while the
michael@0 805 // last function call is selected.
michael@0 806 if (this.selectedIndex == this.itemCount - 1) {
michael@0 807 $("#resume").setAttribute("disabled", "true");
michael@0 808 $("#step-over").setAttribute("disabled", "true");
michael@0 809 $("#step-out").setAttribute("disabled", "true");
michael@0 810 } else {
michael@0 811 $("#resume").removeAttribute("disabled");
michael@0 812 $("#step-over").removeAttribute("disabled");
michael@0 813 $("#step-out").removeAttribute("disabled");
michael@0 814 }
michael@0 815
michael@0 816 // Correlate the currently selected item with the function selection
michael@0 817 // slider's value. Avoid triggering a redundant selection event.
michael@0 818 this._ignoreSliderChanges = true;
michael@0 819 this._slider.value = this.selectedIndex;
michael@0 820 this._ignoreSliderChanges = false;
michael@0 821
michael@0 822 // Can't generate screenshots for function call actors loaded from disk.
michael@0 823 // XXX: Bug 984844.
michael@0 824 if (callItem.attachment.actor.isLoadedFromDisk) {
michael@0 825 return;
michael@0 826 }
michael@0 827
michael@0 828 // To keep continuous selection buttery smooth (for example, while pressing
michael@0 829 // the DOWN key or moving the slider), only display the screenshot after
michael@0 830 // any kind of user input stops.
michael@0 831 setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => {
michael@0 832 return !this._isSliding;
michael@0 833 }, () => {
michael@0 834 let frameSnapshot = SnapshotsListView.selectedItem.attachment.actor
michael@0 835 let functionCall = callItem.attachment.actor;
michael@0 836 frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => {
michael@0 837 this.showScreenshot(screenshot);
michael@0 838 this.highlightedThumbnail = screenshot.index;
michael@0 839 });
michael@0 840 });
michael@0 841 },
michael@0 842
michael@0 843 /**
michael@0 844 * The mousedown listener for the call selection slider.
michael@0 845 */
michael@0 846 _onSlideMouseDown: function() {
michael@0 847 this._isSliding = true;
michael@0 848 },
michael@0 849
michael@0 850 /**
michael@0 851 * The mouseup listener for the call selection slider.
michael@0 852 */
michael@0 853 _onSlideMouseUp: function() {
michael@0 854 this._isSliding = false;
michael@0 855 },
michael@0 856
michael@0 857 /**
michael@0 858 * The change listener for the call selection slider.
michael@0 859 */
michael@0 860 _onSlide: function() {
michael@0 861 // Avoid performing any operations when programatically changing the value.
michael@0 862 if (this._ignoreSliderChanges) {
michael@0 863 return;
michael@0 864 }
michael@0 865 let selectedFunctionCallIndex = this.selectedIndex = this._slider.value;
michael@0 866
michael@0 867 // While sliding, immediately show the most relevant thumbnail for a
michael@0 868 // function call, for a nice diff-like animation effect between draws.
michael@0 869 let thumbnails = SnapshotsListView.selectedItem.attachment.thumbnails;
michael@0 870 let thumbnail = getThumbnailForCall(thumbnails, selectedFunctionCallIndex);
michael@0 871
michael@0 872 // Avoid drawing and highlighting if the selected function call has the
michael@0 873 // same thumbnail as the last one.
michael@0 874 if (thumbnail.index == this.highlightedThumbnail) {
michael@0 875 return;
michael@0 876 }
michael@0 877 // If a thumbnail wasn't found (e.g. the backend avoids creating thumbnails
michael@0 878 // when rendering offscreen), simply defer to the first available one.
michael@0 879 if (thumbnail.index == -1) {
michael@0 880 thumbnail = thumbnails[0];
michael@0 881 }
michael@0 882
michael@0 883 let { index, width, height, flipped, pixels } = thumbnail;
michael@0 884 this.highlightedThumbnail = index;
michael@0 885
michael@0 886 let screenshotNode = $("#screenshot-image");
michael@0 887 screenshotNode.setAttribute("flipped", flipped);
michael@0 888 drawBackground("screenshot-rendering", width, height, pixels);
michael@0 889 },
michael@0 890
michael@0 891 /**
michael@0 892 * The input listener for the calls searchbox.
michael@0 893 */
michael@0 894 _onSearch: function(e) {
michael@0 895 let lowerCaseSearchToken = this._searchbox.value.toLowerCase();
michael@0 896
michael@0 897 this.filterContents(e => {
michael@0 898 let call = e.attachment.actor;
michael@0 899 let name = call.name.toLowerCase();
michael@0 900 let file = call.file.toLowerCase();
michael@0 901 let line = call.line.toString().toLowerCase();
michael@0 902 let args = call.argsPreview.toLowerCase();
michael@0 903
michael@0 904 return name.contains(lowerCaseSearchToken) ||
michael@0 905 file.contains(lowerCaseSearchToken) ||
michael@0 906 line.contains(lowerCaseSearchToken) ||
michael@0 907 args.contains(lowerCaseSearchToken);
michael@0 908 });
michael@0 909 },
michael@0 910
michael@0 911 /**
michael@0 912 * The wheel listener for the filmstrip that contains all the thumbnails.
michael@0 913 */
michael@0 914 _onScroll: function(e) {
michael@0 915 this._filmstrip.scrollLeft += e.deltaX;
michael@0 916 },
michael@0 917
michael@0 918 /**
michael@0 919 * The click/dblclick listener for an item or location url in this container.
michael@0 920 * When expanding an item, it's corresponding call stack will be displayed.
michael@0 921 */
michael@0 922 _onExpand: function(e) {
michael@0 923 let callItem = this.getItemForElement(e.target);
michael@0 924 let view = $(".call-item-view", callItem.target);
michael@0 925
michael@0 926 // If the call stack nodes were already created, simply re-show them
michael@0 927 // or jump to the corresponding file and line in the Debugger if a
michael@0 928 // location link was clicked.
michael@0 929 if (view.hasAttribute("call-stack-populated")) {
michael@0 930 let isExpanded = view.getAttribute("call-stack-expanded") == "true";
michael@0 931
michael@0 932 // If clicking on the location, jump to the Debugger.
michael@0 933 if (e.target.classList.contains("call-item-location")) {
michael@0 934 let { file, line } = callItem.attachment.actor;
michael@0 935 viewSourceInDebugger(file, line);
michael@0 936 return;
michael@0 937 }
michael@0 938 // Otherwise hide the call stack.
michael@0 939 else {
michael@0 940 view.setAttribute("call-stack-expanded", !isExpanded);
michael@0 941 $(".call-item-stack", view).hidden = isExpanded;
michael@0 942 return;
michael@0 943 }
michael@0 944 }
michael@0 945
michael@0 946 let list = document.createElement("vbox");
michael@0 947 list.className = "call-item-stack";
michael@0 948 view.setAttribute("call-stack-populated", "");
michael@0 949 view.setAttribute("call-stack-expanded", "true");
michael@0 950 view.appendChild(list);
michael@0 951
michael@0 952 /**
michael@0 953 * Creates a function call nodes in this container for a stack.
michael@0 954 */
michael@0 955 let display = stack => {
michael@0 956 for (let i = 1; i < stack.length; i++) {
michael@0 957 let call = stack[i];
michael@0 958
michael@0 959 let contents = document.createElement("hbox");
michael@0 960 contents.className = "call-item-stack-fn";
michael@0 961 contents.style.MozPaddingStart = (i * STACK_FUNC_INDENTATION) + "px";
michael@0 962
michael@0 963 let name = document.createElement("label");
michael@0 964 name.className = "plain call-item-stack-fn-name";
michael@0 965 name.setAttribute("value", "↳ " + call.name + "()");
michael@0 966 contents.appendChild(name);
michael@0 967
michael@0 968 let spacer = document.createElement("spacer");
michael@0 969 spacer.setAttribute("flex", "100");
michael@0 970 contents.appendChild(spacer);
michael@0 971
michael@0 972 let location = document.createElement("label");
michael@0 973 location.className = "plain call-item-stack-fn-location";
michael@0 974 location.setAttribute("value", getFileName(call.file) + ":" + call.line);
michael@0 975 location.setAttribute("crop", "start");
michael@0 976 location.setAttribute("flex", "1");
michael@0 977 location.addEventListener("mousedown", e => this._onStackFileClick(e, call));
michael@0 978 contents.appendChild(location);
michael@0 979
michael@0 980 list.appendChild(contents);
michael@0 981 }
michael@0 982
michael@0 983 window.emit(EVENTS.CALL_STACK_DISPLAYED);
michael@0 984 };
michael@0 985
michael@0 986 // If this animation snapshot is loaded from disk, there are no corresponding
michael@0 987 // backend actors available and the data is immediately available.
michael@0 988 let functionCall = callItem.attachment.actor;
michael@0 989 if (functionCall.isLoadedFromDisk) {
michael@0 990 display(functionCall.stack);
michael@0 991 }
michael@0 992 // ..otherwise we need to request the function call stack from the backend.
michael@0 993 else {
michael@0 994 callItem.attachment.actor.getDetails().then(fn => display(fn.stack));
michael@0 995 }
michael@0 996 },
michael@0 997
michael@0 998 /**
michael@0 999 * The click listener for a location link in the call stack.
michael@0 1000 *
michael@0 1001 * @param string file
michael@0 1002 * The url of the source owning the function.
michael@0 1003 * @param number line
michael@0 1004 * The line of the respective function.
michael@0 1005 */
michael@0 1006 _onStackFileClick: function(e, { file, line }) {
michael@0 1007 viewSourceInDebugger(file, line);
michael@0 1008 },
michael@0 1009
michael@0 1010 /**
michael@0 1011 * The click listener for a thumbnail in the filmstrip.
michael@0 1012 *
michael@0 1013 * @param number index
michael@0 1014 * The function index in the recorded animation frame snapshot.
michael@0 1015 */
michael@0 1016 _onThumbnailClick: function(e, index) {
michael@0 1017 this.selectedIndex = index;
michael@0 1018 },
michael@0 1019
michael@0 1020 /**
michael@0 1021 * The click listener for the "resume" button in this container's toolbar.
michael@0 1022 */
michael@0 1023 _onResume: function() {
michael@0 1024 // Jump to the next draw call in the recorded animation frame snapshot.
michael@0 1025 let drawCall = getNextDrawCall(this.items, this.selectedItem);
michael@0 1026 if (drawCall) {
michael@0 1027 this.selectedItem = drawCall;
michael@0 1028 return;
michael@0 1029 }
michael@0 1030
michael@0 1031 // If there are no more draw calls, just jump to the last context call.
michael@0 1032 this._onStepOut();
michael@0 1033 },
michael@0 1034
michael@0 1035 /**
michael@0 1036 * The click listener for the "step over" button in this container's toolbar.
michael@0 1037 */
michael@0 1038 _onStepOver: function() {
michael@0 1039 this.selectedIndex++;
michael@0 1040 },
michael@0 1041
michael@0 1042 /**
michael@0 1043 * The click listener for the "step in" button in this container's toolbar.
michael@0 1044 */
michael@0 1045 _onStepIn: function() {
michael@0 1046 if (this.selectedIndex == -1) {
michael@0 1047 this._onResume();
michael@0 1048 return;
michael@0 1049 }
michael@0 1050 let callItem = this.selectedItem;
michael@0 1051 let { file, line } = callItem.attachment.actor;
michael@0 1052 viewSourceInDebugger(file, line);
michael@0 1053 },
michael@0 1054
michael@0 1055 /**
michael@0 1056 * The click listener for the "step out" button in this container's toolbar.
michael@0 1057 */
michael@0 1058 _onStepOut: function() {
michael@0 1059 this.selectedIndex = this.itemCount - 1;
michael@0 1060 }
michael@0 1061 });
michael@0 1062
michael@0 1063 /**
michael@0 1064 * Localization convenience methods.
michael@0 1065 */
michael@0 1066 let L10N = new ViewHelpers.L10N(STRINGS_URI);
michael@0 1067
michael@0 1068 /**
michael@0 1069 * Convenient way of emitting events from the panel window.
michael@0 1070 */
michael@0 1071 EventEmitter.decorate(this);
michael@0 1072
michael@0 1073 /**
michael@0 1074 * DOM query helpers.
michael@0 1075 */
michael@0 1076 function $(selector, target = document) target.querySelector(selector);
michael@0 1077 function $all(selector, target = document) target.querySelectorAll(selector);
michael@0 1078
michael@0 1079 /**
michael@0 1080 * Helper for getting an nsIURL instance out of a string.
michael@0 1081 */
michael@0 1082 function nsIURL(url, store = nsIURL.store) {
michael@0 1083 if (store.has(url)) {
michael@0 1084 return store.get(url);
michael@0 1085 }
michael@0 1086 let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
michael@0 1087 store.set(url, uri);
michael@0 1088 return uri;
michael@0 1089 }
michael@0 1090
michael@0 1091 // The cache used in the `nsIURL` function.
michael@0 1092 nsIURL.store = new Map();
michael@0 1093
michael@0 1094 /**
michael@0 1095 * Gets the fileName part of a string which happens to be an URL.
michael@0 1096 */
michael@0 1097 function getFileName(url) {
michael@0 1098 try {
michael@0 1099 let { fileName } = nsIURL(url);
michael@0 1100 return fileName || "/";
michael@0 1101 } catch (e) {
michael@0 1102 // This doesn't look like a url, or nsIURL can't handle it.
michael@0 1103 return "";
michael@0 1104 }
michael@0 1105 }
michael@0 1106
michael@0 1107 /**
michael@0 1108 * Gets an image data object containing a buffer large enough to hold
michael@0 1109 * width * height pixels.
michael@0 1110 *
michael@0 1111 * This method avoids allocating memory and tries to reuse a common buffer
michael@0 1112 * as much as possible.
michael@0 1113 *
michael@0 1114 * @param number w
michael@0 1115 * The desired image data storage width.
michael@0 1116 * @param number h
michael@0 1117 * The desired image data storage height.
michael@0 1118 * @return ImageData
michael@0 1119 * The requested image data buffer.
michael@0 1120 */
michael@0 1121 function getImageDataStorage(ctx, w, h) {
michael@0 1122 let storage = getImageDataStorage.cache;
michael@0 1123 if (storage && storage.width == w && storage.height == h) {
michael@0 1124 return storage;
michael@0 1125 }
michael@0 1126 return getImageDataStorage.cache = ctx.createImageData(w, h);
michael@0 1127 }
michael@0 1128
michael@0 1129 // The cache used in the `getImageDataStorage` function.
michael@0 1130 getImageDataStorage.cache = null;
michael@0 1131
michael@0 1132 /**
michael@0 1133 * Draws image data into a canvas.
michael@0 1134 *
michael@0 1135 * This method makes absolutely no assumptions about the canvas element
michael@0 1136 * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels.
michael@0 1137 *
michael@0 1138 * @param HTMLCanvasElement canvas
michael@0 1139 * The canvas element to put the image data into.
michael@0 1140 * @param number width
michael@0 1141 * The image data width.
michael@0 1142 * @param number height
michael@0 1143 * The image data height.
michael@0 1144 * @param pixels
michael@0 1145 * An array buffer view of the image data.
michael@0 1146 * @param object options
michael@0 1147 * Additional options supported by this operation:
michael@0 1148 * - centered: specifies whether the image data should be centered
michael@0 1149 * when copied in the canvas; this is useful when the
michael@0 1150 * supplied pixels don't completely cover the canvas.
michael@0 1151 */
michael@0 1152 function drawImage(canvas, width, height, pixels, options = {}) {
michael@0 1153 let ctx = canvas.getContext("2d");
michael@0 1154
michael@0 1155 // FrameSnapshot actors return "snapshot-image" type instances with just an
michael@0 1156 // empty pixel array if the source image is completely transparent.
michael@0 1157 if (pixels.length <= 1) {
michael@0 1158 ctx.clearRect(0, 0, canvas.width, canvas.height);
michael@0 1159 return;
michael@0 1160 }
michael@0 1161
michael@0 1162 let arrayBuffer = new Uint8Array(pixels.buffer);
michael@0 1163 let imageData = getImageDataStorage(ctx, width, height);
michael@0 1164 imageData.data.set(arrayBuffer);
michael@0 1165
michael@0 1166 if (options.centered) {
michael@0 1167 let left = (canvas.width - width) / 2;
michael@0 1168 let top = (canvas.height - height) / 2;
michael@0 1169 ctx.putImageData(imageData, left, top);
michael@0 1170 } else {
michael@0 1171 ctx.putImageData(imageData, 0, 0);
michael@0 1172 }
michael@0 1173 }
michael@0 1174
michael@0 1175 /**
michael@0 1176 * Draws image data into a canvas, and sets that as the rendering source for
michael@0 1177 * an element with the specified id as the -moz-element background image.
michael@0 1178 *
michael@0 1179 * @param string id
michael@0 1180 * The id of the -moz-element background image.
michael@0 1181 * @param number width
michael@0 1182 * The image data width.
michael@0 1183 * @param number height
michael@0 1184 * The image data height.
michael@0 1185 * @param pixels
michael@0 1186 * An array buffer view of the image data.
michael@0 1187 */
michael@0 1188 function drawBackground(id, width, height, pixels) {
michael@0 1189 let canvas = document.createElementNS(HTML_NS, "canvas");
michael@0 1190 canvas.width = width;
michael@0 1191 canvas.height = height;
michael@0 1192
michael@0 1193 drawImage(canvas, width, height, pixels);
michael@0 1194 document.mozSetImageElement(id, canvas);
michael@0 1195
michael@0 1196 // Used in tests. Not emitting an event because this shouldn't be "interesting".
michael@0 1197 if (window._onMozSetImageElement) {
michael@0 1198 window._onMozSetImageElement(pixels);
michael@0 1199 }
michael@0 1200 }
michael@0 1201
michael@0 1202 /**
michael@0 1203 * Iterates forward to find the next draw call in a snapshot.
michael@0 1204 */
michael@0 1205 function getNextDrawCall(calls, call) {
michael@0 1206 for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) {
michael@0 1207 let nextCall = calls[i];
michael@0 1208 let name = nextCall.attachment.actor.name;
michael@0 1209 if (CanvasFront.DRAW_CALLS.has(name)) {
michael@0 1210 return nextCall;
michael@0 1211 }
michael@0 1212 }
michael@0 1213 return null;
michael@0 1214 }
michael@0 1215
michael@0 1216 /**
michael@0 1217 * Iterates backwards to find the most recent screenshot for a function call
michael@0 1218 * in a snapshot loaded from disk.
michael@0 1219 */
michael@0 1220 function getScreenshotFromCallLoadedFromDisk(calls, call) {
michael@0 1221 for (let i = calls.indexOf(call); i >= 0; i--) {
michael@0 1222 let prevCall = calls[i];
michael@0 1223 let screenshot = prevCall.screenshot;
michael@0 1224 if (screenshot) {
michael@0 1225 return screenshot;
michael@0 1226 }
michael@0 1227 }
michael@0 1228 return CanvasFront.INVALID_SNAPSHOT_IMAGE;
michael@0 1229 }
michael@0 1230
michael@0 1231 /**
michael@0 1232 * Iterates backwards to find the most recent thumbnail for a function call.
michael@0 1233 */
michael@0 1234 function getThumbnailForCall(thumbnails, index) {
michael@0 1235 for (let i = thumbnails.length - 1; i >= 0; i--) {
michael@0 1236 let thumbnail = thumbnails[i];
michael@0 1237 if (thumbnail.index <= index) {
michael@0 1238 return thumbnail;
michael@0 1239 }
michael@0 1240 }
michael@0 1241 return CanvasFront.INVALID_SNAPSHOT_IMAGE;
michael@0 1242 }
michael@0 1243
michael@0 1244 /**
michael@0 1245 * Opens/selects the debugger in this toolbox and jumps to the specified
michael@0 1246 * file name and line number.
michael@0 1247 */
michael@0 1248 function viewSourceInDebugger(url, line) {
michael@0 1249 let showSource = ({ DebuggerView }) => {
michael@0 1250 if (DebuggerView.Sources.containsValue(url)) {
michael@0 1251 DebuggerView.setEditorLocation(url, line, { noDebug: true }).then(() => {
michael@0 1252 window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
michael@0 1253 }, () => {
michael@0 1254 window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
michael@0 1255 });
michael@0 1256 }
michael@0 1257 }
michael@0 1258
michael@0 1259 // If the Debugger was already open, switch to it and try to show the
michael@0 1260 // source immediately. Otherwise, initialize it and wait for the sources
michael@0 1261 // to be added first.
michael@0 1262 let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger");
michael@0 1263 gToolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
michael@0 1264 if (debuggerAlreadyOpen) {
michael@0 1265 showSource(dbg);
michael@0 1266 } else {
michael@0 1267 dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
michael@0 1268 }
michael@0 1269 });
michael@0 1270 }

mercurial