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.

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

mercurial