michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); michael@0: Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); michael@0: michael@0: const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; michael@0: const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; michael@0: const EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: const { CallWatcherFront } = require("devtools/server/actors/call-watcher"); michael@0: const { CanvasFront } = require("devtools/server/actors/canvas"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", michael@0: "resource://gre/modules/PluralForm.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils", michael@0: "resource://gre/modules/devtools/DevToolsUtils.jsm"); michael@0: michael@0: // The panel's window global is an EventEmitter firing the following events: michael@0: const EVENTS = { michael@0: // When the UI is reset from tab navigation. michael@0: UI_RESET: "CanvasDebugger:UIReset", michael@0: michael@0: // When all the animation frame snapshots are removed by the user. michael@0: SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared", michael@0: michael@0: // When an animation frame snapshot starts/finishes being recorded. michael@0: SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted", michael@0: SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished", michael@0: michael@0: // When an animation frame snapshot was selected and all its data displayed. michael@0: SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected", michael@0: michael@0: // After all the function calls associated with an animation frame snapshot michael@0: // are displayed in the UI. michael@0: CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated", michael@0: michael@0: // After the stack associated with a call in an animation frame snapshot michael@0: // is displayed in the UI. michael@0: CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed", michael@0: michael@0: // After a screenshot associated with a call in an animation frame snapshot michael@0: // is displayed in the UI. michael@0: CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed", michael@0: michael@0: // After all the thumbnails associated with an animation frame snapshot michael@0: // are displayed in the UI. michael@0: THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed", michael@0: michael@0: // When a source is shown in the JavaScript Debugger at a specific location. michael@0: SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger", michael@0: SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger" michael@0: }; michael@0: michael@0: const HTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const STRINGS_URI = "chrome://browser/locale/devtools/canvasdebugger.properties" michael@0: michael@0: const SNAPSHOT_START_RECORDING_DELAY = 10; // ms michael@0: const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms michael@0: const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms michael@0: const SCREENSHOT_DISPLAY_DELAY = 100; // ms michael@0: const STACK_FUNC_INDENTATION = 14; // px michael@0: michael@0: // This identifier string is simply used to tentatively ascertain whether or not michael@0: // a JSON loaded from disk is actually something generated by this tool or not. michael@0: // It isn't, of course, a definitive verification, but a Good Enough™ michael@0: // approximation before continuing the import. Don't localize this. michael@0: const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot"; michael@0: const CALLS_LIST_SERIALIZER_VERSION = 1; michael@0: const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms michael@0: michael@0: /** michael@0: * The current target and the Canvas front, set by this tool's host. michael@0: */ michael@0: let gToolbox, gTarget, gFront; michael@0: michael@0: /** michael@0: * Initializes the canvas debugger controller and views. michael@0: */ michael@0: function startupCanvasDebugger() { michael@0: return promise.all([ michael@0: EventsHandler.initialize(), michael@0: SnapshotsListView.initialize(), michael@0: CallsListView.initialize() michael@0: ]); michael@0: } michael@0: michael@0: /** michael@0: * Destroys the canvas debugger controller and views. michael@0: */ michael@0: function shutdownCanvasDebugger() { michael@0: return promise.all([ michael@0: EventsHandler.destroy(), michael@0: SnapshotsListView.destroy(), michael@0: CallsListView.destroy() michael@0: ]); michael@0: } michael@0: michael@0: /** michael@0: * Functions handling target-related lifetime events. michael@0: */ michael@0: let EventsHandler = { michael@0: /** michael@0: * Listen for events emitted by the current tab target. michael@0: */ michael@0: initialize: function() { michael@0: this._onTabNavigated = this._onTabNavigated.bind(this); michael@0: gTarget.on("will-navigate", this._onTabNavigated); michael@0: gTarget.on("navigate", this._onTabNavigated); michael@0: }, michael@0: michael@0: /** michael@0: * Remove events emitted by the current tab target. michael@0: */ michael@0: destroy: function() { michael@0: gTarget.off("will-navigate", this._onTabNavigated); michael@0: gTarget.off("navigate", this._onTabNavigated); michael@0: }, michael@0: michael@0: /** michael@0: * Called for each location change in the debugged tab. michael@0: */ michael@0: _onTabNavigated: function(event) { michael@0: if (event != "will-navigate") { michael@0: return; michael@0: } michael@0: // Make sure the backend is prepared to handle contexts. michael@0: gFront.setup({ reload: false }); michael@0: michael@0: // Reset UI. michael@0: SnapshotsListView.empty(); michael@0: CallsListView.empty(); michael@0: michael@0: $("#record-snapshot").removeAttribute("checked"); michael@0: $("#record-snapshot").removeAttribute("disabled"); michael@0: $("#record-snapshot").hidden = false; michael@0: michael@0: $("#reload-notice").hidden = true; michael@0: $("#empty-notice").hidden = false; michael@0: $("#import-notice").hidden = true; michael@0: michael@0: $("#debugging-pane-contents").hidden = true; michael@0: $("#screenshot-container").hidden = true; michael@0: $("#snapshot-filmstrip").hidden = true; michael@0: michael@0: window.emit(EVENTS.UI_RESET); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Functions handling the recorded animation frame snapshots UI. michael@0: */ michael@0: let SnapshotsListView = Heritage.extend(WidgetMethods, { michael@0: /** michael@0: * Initialization function, called when the tool is started. michael@0: */ michael@0: initialize: function() { michael@0: this.widget = new SideMenuWidget($("#snapshots-list"), { michael@0: showArrows: true michael@0: }); michael@0: michael@0: this._onSelect = this._onSelect.bind(this); michael@0: this._onClearButtonClick = this._onClearButtonClick.bind(this); michael@0: this._onRecordButtonClick = this._onRecordButtonClick.bind(this); michael@0: this._onImportButtonClick = this._onImportButtonClick.bind(this); michael@0: this._onSaveButtonClick = this._onSaveButtonClick.bind(this); michael@0: michael@0: this.emptyText = L10N.getStr("noSnapshotsText"); michael@0: this.widget.addEventListener("select", this._onSelect, false); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the tool is closed. michael@0: */ michael@0: destroy: function() { michael@0: this.widget.removeEventListener("select", this._onSelect, false); michael@0: }, michael@0: michael@0: /** michael@0: * Adds a snapshot entry to this container. michael@0: * michael@0: * @return object michael@0: * The newly inserted item. michael@0: */ michael@0: addSnapshot: function() { michael@0: let contents = document.createElement("hbox"); michael@0: contents.className = "snapshot-item"; michael@0: michael@0: let thumbnail = document.createElementNS(HTML_NS, "canvas"); michael@0: thumbnail.className = "snapshot-item-thumbnail"; michael@0: thumbnail.width = CanvasFront.THUMBNAIL_HEIGHT; michael@0: thumbnail.height = CanvasFront.THUMBNAIL_HEIGHT; michael@0: michael@0: let title = document.createElement("label"); michael@0: title.className = "plain snapshot-item-title"; michael@0: title.setAttribute("value", michael@0: L10N.getFormatStr("snapshotsList.itemLabel", this.itemCount + 1)); michael@0: michael@0: let calls = document.createElement("label"); michael@0: calls.className = "plain snapshot-item-calls"; michael@0: calls.setAttribute("value", michael@0: L10N.getStr("snapshotsList.loadingLabel")); michael@0: michael@0: let save = document.createElement("label"); michael@0: save.className = "plain snapshot-item-save"; michael@0: save.addEventListener("click", this._onSaveButtonClick, false); michael@0: michael@0: let spacer = document.createElement("spacer"); michael@0: spacer.setAttribute("flex", "1"); michael@0: michael@0: let footer = document.createElement("hbox"); michael@0: footer.className = "snapshot-item-footer"; michael@0: footer.appendChild(save); michael@0: michael@0: let details = document.createElement("vbox"); michael@0: details.className = "snapshot-item-details"; michael@0: details.appendChild(title); michael@0: details.appendChild(calls); michael@0: details.appendChild(spacer); michael@0: details.appendChild(footer); michael@0: michael@0: contents.appendChild(thumbnail); michael@0: contents.appendChild(details); michael@0: michael@0: // Append a recorded snapshot item to this container. michael@0: return this.push([contents], { michael@0: attachment: { michael@0: // The snapshot and function call actors, along with the thumbnails michael@0: // will be available as soon as recording finishes. michael@0: actor: null, michael@0: calls: null, michael@0: thumbnails: null, michael@0: screenshot: null michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Customizes a shapshot in this container. michael@0: * michael@0: * @param Item snapshotItem michael@0: * An item inserted via `SnapshotsListView.addSnapshot`. michael@0: * @param object snapshotActor michael@0: * The frame snapshot actor received from the backend. michael@0: * @param object snapshotOverview michael@0: * Additional data about the snapshot received from the backend. michael@0: */ michael@0: customizeSnapshot: function(snapshotItem, snapshotActor, snapshotOverview) { michael@0: // Make sure the function call actors are stored on the item, michael@0: // to be used when populating the CallsListView. michael@0: snapshotItem.attachment.actor = snapshotActor; michael@0: let functionCalls = snapshotItem.attachment.calls = snapshotOverview.calls; michael@0: let thumbnails = snapshotItem.attachment.thumbnails = snapshotOverview.thumbnails; michael@0: let screenshot = snapshotItem.attachment.screenshot = snapshotOverview.screenshot; michael@0: michael@0: let lastThumbnail = thumbnails[thumbnails.length - 1]; michael@0: let { width, height, flipped, pixels } = lastThumbnail; michael@0: michael@0: let thumbnailNode = $(".snapshot-item-thumbnail", snapshotItem.target); michael@0: thumbnailNode.setAttribute("flipped", flipped); michael@0: drawImage(thumbnailNode, width, height, pixels, { centered: true }); michael@0: michael@0: let callsNode = $(".snapshot-item-calls", snapshotItem.target); michael@0: let drawCalls = functionCalls.filter(e => CanvasFront.DRAW_CALLS.has(e.name)); michael@0: michael@0: let drawCallsStr = PluralForm.get(drawCalls.length, michael@0: L10N.getStr("snapshotsList.drawCallsLabel")); michael@0: let funcCallsStr = PluralForm.get(functionCalls.length, michael@0: L10N.getStr("snapshotsList.functionCallsLabel")); michael@0: michael@0: callsNode.setAttribute("value", michael@0: drawCallsStr.replace("#1", drawCalls.length) + ", " + michael@0: funcCallsStr.replace("#1", functionCalls.length)); michael@0: michael@0: let saveNode = $(".snapshot-item-save", snapshotItem.target); michael@0: saveNode.setAttribute("disabled", !!snapshotItem.isLoadedFromDisk); michael@0: saveNode.setAttribute("value", snapshotItem.isLoadedFromDisk michael@0: ? L10N.getStr("snapshotsList.loadedLabel") michael@0: : L10N.getStr("snapshotsList.saveLabel")); michael@0: michael@0: // Make sure there's always a selected item available. michael@0: if (!this.selectedItem) { michael@0: this.selectedIndex = 0; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The select listener for this container. michael@0: */ michael@0: _onSelect: function({ detail: snapshotItem }) { michael@0: if (!snapshotItem) { michael@0: return; michael@0: } michael@0: let { calls, thumbnails, screenshot } = snapshotItem.attachment; michael@0: michael@0: $("#reload-notice").hidden = true; michael@0: $("#empty-notice").hidden = true; michael@0: $("#import-notice").hidden = false; michael@0: michael@0: $("#debugging-pane-contents").hidden = true; michael@0: $("#screenshot-container").hidden = true; michael@0: $("#snapshot-filmstrip").hidden = true; michael@0: michael@0: Task.spawn(function*() { michael@0: // Wait for a few milliseconds between presenting the function calls, michael@0: // screenshot and thumbnails, to allow each component being michael@0: // sequentially drawn. This gives the illusion of snappiness. michael@0: michael@0: yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); michael@0: CallsListView.showCalls(calls); michael@0: $("#debugging-pane-contents").hidden = false; michael@0: $("#import-notice").hidden = true; michael@0: michael@0: yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); michael@0: CallsListView.showThumbnails(thumbnails); michael@0: $("#snapshot-filmstrip").hidden = false; michael@0: michael@0: yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); michael@0: CallsListView.showScreenshot(screenshot); michael@0: $("#screenshot-container").hidden = false; michael@0: michael@0: window.emit(EVENTS.SNAPSHOT_RECORDING_SELECTED); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "clear" button in this container. michael@0: */ michael@0: _onClearButtonClick: function() { michael@0: Task.spawn(function*() { michael@0: SnapshotsListView.empty(); michael@0: CallsListView.empty(); michael@0: michael@0: $("#reload-notice").hidden = true; michael@0: $("#empty-notice").hidden = true; michael@0: $("#import-notice").hidden = true; michael@0: michael@0: if (yield gFront.isInitialized()) { michael@0: $("#empty-notice").hidden = false; michael@0: } else { michael@0: $("#reload-notice").hidden = false; michael@0: } michael@0: michael@0: $("#debugging-pane-contents").hidden = true; michael@0: $("#screenshot-container").hidden = true; michael@0: $("#snapshot-filmstrip").hidden = true; michael@0: michael@0: window.emit(EVENTS.SNAPSHOTS_LIST_CLEARED); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "record" button in this container. michael@0: */ michael@0: _onRecordButtonClick: function() { michael@0: Task.spawn(function*() { michael@0: $("#record-snapshot").setAttribute("checked", "true"); michael@0: $("#record-snapshot").setAttribute("disabled", "true"); michael@0: michael@0: // Insert a "dummy" snapshot item in the view, to hint that recording michael@0: // has now started. However, wait for a few milliseconds before actually michael@0: // starting the recording, since that might block rendering and prevent michael@0: // the dummy snapshot item from being drawn. michael@0: let snapshotItem = this.addSnapshot(); michael@0: michael@0: // If this is the first item, immediately show the "Loading…" notice. michael@0: if (this.itemCount == 1) { michael@0: $("#empty-notice").hidden = true; michael@0: $("#import-notice").hidden = false; michael@0: } michael@0: michael@0: yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY); michael@0: window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED); michael@0: michael@0: let snapshotActor = yield gFront.recordAnimationFrame(); michael@0: let snapshotOverview = yield snapshotActor.getOverview(); michael@0: this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview); michael@0: michael@0: $("#record-snapshot").removeAttribute("checked"); michael@0: $("#record-snapshot").removeAttribute("disabled"); michael@0: michael@0: window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "import" button in this container. michael@0: */ michael@0: _onImportButtonClick: function() { michael@0: let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); michael@0: fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen); michael@0: fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json"); michael@0: fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*"); michael@0: michael@0: if (fp.show() != Ci.nsIFilePicker.returnOK) { michael@0: return; michael@0: } michael@0: michael@0: let channel = NetUtil.newChannel(fp.file); michael@0: channel.contentType = "text/plain"; michael@0: michael@0: NetUtil.asyncFetch(channel, (inputStream, status) => { michael@0: if (!Components.isSuccessCode(status)) { michael@0: console.error("Could not import recorded animation frame snapshot file."); michael@0: return; michael@0: } michael@0: try { michael@0: let string = NetUtil.readInputStreamToString(inputStream, inputStream.available()); michael@0: var data = JSON.parse(string); michael@0: } catch (e) { michael@0: console.error("Could not read animation frame snapshot file."); michael@0: return; michael@0: } michael@0: if (data.fileType != CALLS_LIST_SERIALIZER_IDENTIFIER) { michael@0: console.error("Unrecognized animation frame snapshot file."); michael@0: return; michael@0: } michael@0: michael@0: // Add a `isLoadedFromDisk` flag on everything to avoid sending invalid michael@0: // requests to the backend, since we're not dealing with actors anymore. michael@0: let snapshotItem = this.addSnapshot(); michael@0: snapshotItem.isLoadedFromDisk = true; michael@0: data.calls.forEach(e => e.isLoadedFromDisk = true); michael@0: michael@0: // Create array buffers from the parsed pixel arrays. michael@0: for (let thumbnail of data.thumbnails) { michael@0: let thumbnailPixelsArray = thumbnail.pixels.split(","); michael@0: thumbnail.pixels = new Uint32Array(thumbnailPixelsArray); michael@0: } michael@0: let screenshotPixelsArray = data.screenshot.pixels.split(","); michael@0: data.screenshot.pixels = new Uint32Array(screenshotPixelsArray); michael@0: michael@0: this.customizeSnapshot(snapshotItem, data.calls, data); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "save" button of each item in this container. michael@0: */ michael@0: _onSaveButtonClick: function(e) { michael@0: let snapshotItem = this.getItemForElement(e.target); michael@0: michael@0: let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); michael@0: fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave); michael@0: fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json"); michael@0: fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*"); michael@0: fp.defaultString = "snapshot.json"; michael@0: michael@0: // Start serializing all the function call actors for the specified snapshot, michael@0: // while the nsIFilePicker dialog is being opened. Snappy. michael@0: let serialized = Task.spawn(function*() { michael@0: let data = { michael@0: fileType: CALLS_LIST_SERIALIZER_IDENTIFIER, michael@0: version: CALLS_LIST_SERIALIZER_VERSION, michael@0: calls: [], michael@0: thumbnails: [], michael@0: screenshot: null michael@0: }; michael@0: let functionCalls = snapshotItem.attachment.calls; michael@0: let thumbnails = snapshotItem.attachment.thumbnails; michael@0: let screenshot = snapshotItem.attachment.screenshot; michael@0: michael@0: // Prepare all the function calls for serialization. michael@0: yield DevToolsUtils.yieldingEach(functionCalls, (call, i) => { michael@0: let { type, name, file, line, argsPreview, callerPreview } = call; michael@0: return call.getDetails().then(({ stack }) => { michael@0: data.calls[i] = { michael@0: type: type, michael@0: name: name, michael@0: file: file, michael@0: line: line, michael@0: stack: stack, michael@0: argsPreview: argsPreview, michael@0: callerPreview: callerPreview michael@0: }; michael@0: }); michael@0: }); michael@0: michael@0: // Prepare all the thumbnails for serialization. michael@0: yield DevToolsUtils.yieldingEach(thumbnails, (thumbnail, i) => { michael@0: let { index, width, height, flipped, pixels } = thumbnail; michael@0: data.thumbnails.push({ michael@0: index: index, michael@0: width: width, michael@0: height: height, michael@0: flipped: flipped, michael@0: pixels: Array.join(pixels, ",") michael@0: }); michael@0: }); michael@0: michael@0: // Prepare the screenshot for serialization. michael@0: let { index, width, height, flipped, pixels } = screenshot; michael@0: data.screenshot = { michael@0: index: index, michael@0: width: width, michael@0: height: height, michael@0: flipped: flipped, michael@0: pixels: Array.join(pixels, ",") michael@0: }; michael@0: michael@0: let string = JSON.stringify(data); michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. michael@0: createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: michael@0: converter.charset = "UTF-8"; michael@0: return converter.convertToInputStream(string); michael@0: }); michael@0: michael@0: // Open the nsIFilePicker and wait for the function call actors to finish michael@0: // being serialized, in order to save the generated JSON data to disk. michael@0: fp.open({ done: result => { michael@0: if (result == Ci.nsIFilePicker.returnCancel) { michael@0: return; michael@0: } michael@0: let footer = $(".snapshot-item-footer", snapshotItem.target); michael@0: let save = $(".snapshot-item-save", snapshotItem.target); michael@0: michael@0: // Show a throbber and a "Saving…" label if serializing isn't immediate. michael@0: setNamedTimeout("call-list-save", CALLS_LIST_SLOW_SAVE_DELAY, () => { michael@0: footer.setAttribute("saving", ""); michael@0: save.setAttribute("disabled", "true"); michael@0: save.setAttribute("value", L10N.getStr("snapshotsList.savingLabel")); michael@0: }); michael@0: michael@0: serialized.then(inputStream => { michael@0: let outputStream = FileUtils.openSafeFileOutputStream(fp.file); michael@0: michael@0: NetUtil.asyncCopy(inputStream, outputStream, status => { michael@0: if (!Components.isSuccessCode(status)) { michael@0: console.error("Could not save recorded animation frame snapshot file."); michael@0: } michael@0: clearNamedTimeout("call-list-save"); michael@0: footer.removeAttribute("saving"); michael@0: save.removeAttribute("disabled"); michael@0: save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel")); michael@0: }); michael@0: }); michael@0: }}); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Functions handling details about a single recorded animation frame snapshot michael@0: * (the calls list, rendering preview, thumbnails filmstrip etc.). michael@0: */ michael@0: let CallsListView = Heritage.extend(WidgetMethods, { michael@0: /** michael@0: * Initialization function, called when the tool is started. michael@0: */ michael@0: initialize: function() { michael@0: this.widget = new SideMenuWidget($("#calls-list")); michael@0: this._slider = $("#calls-slider"); michael@0: this._searchbox = $("#calls-searchbox"); michael@0: this._filmstrip = $("#snapshot-filmstrip"); michael@0: michael@0: this._onSelect = this._onSelect.bind(this); michael@0: this._onSlideMouseDown = this._onSlideMouseDown.bind(this); michael@0: this._onSlideMouseUp = this._onSlideMouseUp.bind(this); michael@0: this._onSlide = this._onSlide.bind(this); michael@0: this._onSearch = this._onSearch.bind(this); michael@0: this._onScroll = this._onScroll.bind(this); michael@0: this._onExpand = this._onExpand.bind(this); michael@0: this._onStackFileClick = this._onStackFileClick.bind(this); michael@0: this._onThumbnailClick = this._onThumbnailClick.bind(this); michael@0: michael@0: this.widget.addEventListener("select", this._onSelect, false); michael@0: this._slider.addEventListener("mousedown", this._onSlideMouseDown, false); michael@0: this._slider.addEventListener("mouseup", this._onSlideMouseUp, false); michael@0: this._slider.addEventListener("change", this._onSlide, false); michael@0: this._searchbox.addEventListener("input", this._onSearch, false); michael@0: this._filmstrip.addEventListener("wheel", this._onScroll, false); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the tool is closed. michael@0: */ michael@0: destroy: function() { michael@0: this.widget.removeEventListener("select", this._onSelect, false); michael@0: this._slider.removeEventListener("mousedown", this._onSlideMouseDown, false); michael@0: this._slider.removeEventListener("mouseup", this._onSlideMouseUp, false); michael@0: this._slider.removeEventListener("change", this._onSlide, false); michael@0: this._searchbox.removeEventListener("input", this._onSearch, false); michael@0: this._filmstrip.removeEventListener("wheel", this._onScroll, false); michael@0: }, michael@0: michael@0: /** michael@0: * Populates this container with a list of function calls. michael@0: * michael@0: * @param array functionCalls michael@0: * A list of function call actors received from the backend. michael@0: */ michael@0: showCalls: function(functionCalls) { michael@0: this.empty(); michael@0: michael@0: for (let i = 0, len = functionCalls.length; i < len; i++) { michael@0: let call = functionCalls[i]; michael@0: michael@0: let view = document.createElement("vbox"); michael@0: view.className = "call-item-view devtools-monospace"; michael@0: view.setAttribute("flex", "1"); michael@0: michael@0: let contents = document.createElement("hbox"); michael@0: contents.className = "call-item-contents"; michael@0: contents.setAttribute("align", "center"); michael@0: contents.addEventListener("dblclick", this._onExpand); michael@0: view.appendChild(contents); michael@0: michael@0: let index = document.createElement("label"); michael@0: index.className = "plain call-item-index"; michael@0: index.setAttribute("flex", "1"); michael@0: index.setAttribute("value", i + 1); michael@0: michael@0: let gutter = document.createElement("hbox"); michael@0: gutter.className = "call-item-gutter"; michael@0: gutter.appendChild(index); michael@0: contents.appendChild(gutter); michael@0: michael@0: // Not all function calls have a caller that was stringified (e.g. michael@0: // context calls have a "gl" or "ctx" caller preview). michael@0: if (call.callerPreview) { michael@0: let context = document.createElement("label"); michael@0: context.className = "plain call-item-context"; michael@0: context.setAttribute("value", call.callerPreview); michael@0: contents.appendChild(context); michael@0: michael@0: let separator = document.createElement("label"); michael@0: separator.className = "plain call-item-separator"; michael@0: separator.setAttribute("value", "."); michael@0: contents.appendChild(separator); michael@0: } michael@0: michael@0: let name = document.createElement("label"); michael@0: name.className = "plain call-item-name"; michael@0: name.setAttribute("value", call.name); michael@0: contents.appendChild(name); michael@0: michael@0: let argsPreview = document.createElement("label"); michael@0: argsPreview.className = "plain call-item-args"; michael@0: argsPreview.setAttribute("crop", "end"); michael@0: argsPreview.setAttribute("flex", "100"); michael@0: // Getters and setters are displayed differently from regular methods. michael@0: if (call.type == CallWatcherFront.METHOD_FUNCTION) { michael@0: argsPreview.setAttribute("value", "(" + call.argsPreview + ")"); michael@0: } else { michael@0: argsPreview.setAttribute("value", " = " + call.argsPreview); michael@0: } michael@0: contents.appendChild(argsPreview); michael@0: michael@0: let location = document.createElement("label"); michael@0: location.className = "plain call-item-location"; michael@0: location.setAttribute("value", getFileName(call.file) + ":" + call.line); michael@0: location.setAttribute("crop", "start"); michael@0: location.setAttribute("flex", "1"); michael@0: location.addEventListener("mousedown", this._onExpand); michael@0: contents.appendChild(location); michael@0: michael@0: // Append a function call item to this container. michael@0: this.push([view], { michael@0: staged: true, michael@0: attachment: { michael@0: actor: call michael@0: } michael@0: }); michael@0: michael@0: // Highlight certain calls that are probably more interesting than michael@0: // everything else, making it easier to quickly glance over them. michael@0: if (CanvasFront.DRAW_CALLS.has(call.name)) { michael@0: view.setAttribute("draw-call", ""); michael@0: } michael@0: if (CanvasFront.INTERESTING_CALLS.has(call.name)) { michael@0: view.setAttribute("interesting-call", ""); michael@0: } michael@0: } michael@0: michael@0: // Flushes all the prepared function call items into this container. michael@0: this.commit(); michael@0: window.emit(EVENTS.CALL_LIST_POPULATED); michael@0: michael@0: // Resetting the function selection slider's value (shown in this michael@0: // container's toolbar) would trigger a selection event, which should be michael@0: // ignored in this case. michael@0: this._ignoreSliderChanges = true; michael@0: this._slider.value = 0; michael@0: this._slider.max = functionCalls.length - 1; michael@0: this._ignoreSliderChanges = false; michael@0: }, michael@0: michael@0: /** michael@0: * Displays an image in the rendering preview of this container, generated michael@0: * for the specified draw call in the recorded animation frame snapshot. michael@0: * michael@0: * @param array screenshot michael@0: * A single "snapshot-image" instance received from the backend. michael@0: */ michael@0: showScreenshot: function(screenshot) { michael@0: let { index, width, height, flipped, pixels } = screenshot; michael@0: michael@0: let screenshotNode = $("#screenshot-image"); michael@0: screenshotNode.setAttribute("flipped", flipped); michael@0: drawBackground("screenshot-rendering", width, height, pixels); michael@0: michael@0: let dimensionsNode = $("#screenshot-dimensions"); michael@0: dimensionsNode.setAttribute("value", ~~width + " x " + ~~height); michael@0: michael@0: window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED); michael@0: }, michael@0: michael@0: /** michael@0: * Populates this container's footer with a list of thumbnails, one generated michael@0: * for each draw call in the recorded animation frame snapshot. michael@0: * michael@0: * @param array thumbnails michael@0: * An array of "snapshot-image" instances received from the backend. michael@0: */ michael@0: showThumbnails: function(thumbnails) { michael@0: while (this._filmstrip.hasChildNodes()) { michael@0: this._filmstrip.firstChild.remove(); michael@0: } michael@0: for (let thumbnail of thumbnails) { michael@0: this.appendThumbnail(thumbnail); michael@0: } michael@0: michael@0: window.emit(EVENTS.THUMBNAILS_DISPLAYED); michael@0: }, michael@0: michael@0: /** michael@0: * Displays an image in the thumbnails list of this container, generated michael@0: * for the specified draw call in the recorded animation frame snapshot. michael@0: * michael@0: * @param array thumbnail michael@0: * A single "snapshot-image" instance received from the backend. michael@0: */ michael@0: appendThumbnail: function(thumbnail) { michael@0: let { index, width, height, flipped, pixels } = thumbnail; michael@0: michael@0: let thumbnailNode = document.createElementNS(HTML_NS, "canvas"); michael@0: thumbnailNode.setAttribute("flipped", flipped); michael@0: thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_HEIGHT, width); michael@0: thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_HEIGHT, height); michael@0: drawImage(thumbnailNode, width, height, pixels, { centered: true }); michael@0: michael@0: thumbnailNode.className = "filmstrip-thumbnail"; michael@0: thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index); michael@0: thumbnailNode.setAttribute("index", index); michael@0: this._filmstrip.appendChild(thumbnailNode); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the currently highlighted thumbnail in this container. michael@0: * A screenshot will always correlate to a thumbnail in the filmstrip, michael@0: * both being identified by the same 'index' of the context function call. michael@0: * michael@0: * @param number index michael@0: * The context function call's index. michael@0: */ michael@0: set highlightedThumbnail(index) { michael@0: let currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']"); michael@0: if (currHighlightedThumbnail == null) { michael@0: return; michael@0: } michael@0: michael@0: let prevIndex = this._highlightedThumbnailIndex michael@0: let prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']"); michael@0: if (prevHighlightedThumbnail) { michael@0: prevHighlightedThumbnail.removeAttribute("highlighted"); michael@0: } michael@0: michael@0: currHighlightedThumbnail.setAttribute("highlighted", ""); michael@0: currHighlightedThumbnail.scrollIntoView(); michael@0: this._highlightedThumbnailIndex = index; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the currently highlighted thumbnail in this container. michael@0: * @return number michael@0: */ michael@0: get highlightedThumbnail() { michael@0: return this._highlightedThumbnailIndex; michael@0: }, michael@0: michael@0: /** michael@0: * The select listener for this container. michael@0: */ michael@0: _onSelect: function({ detail: callItem }) { michael@0: if (!callItem) { michael@0: return; michael@0: } michael@0: michael@0: // Some of the stepping buttons don't make sense specifically while the michael@0: // last function call is selected. michael@0: if (this.selectedIndex == this.itemCount - 1) { michael@0: $("#resume").setAttribute("disabled", "true"); michael@0: $("#step-over").setAttribute("disabled", "true"); michael@0: $("#step-out").setAttribute("disabled", "true"); michael@0: } else { michael@0: $("#resume").removeAttribute("disabled"); michael@0: $("#step-over").removeAttribute("disabled"); michael@0: $("#step-out").removeAttribute("disabled"); michael@0: } michael@0: michael@0: // Correlate the currently selected item with the function selection michael@0: // slider's value. Avoid triggering a redundant selection event. michael@0: this._ignoreSliderChanges = true; michael@0: this._slider.value = this.selectedIndex; michael@0: this._ignoreSliderChanges = false; michael@0: michael@0: // Can't generate screenshots for function call actors loaded from disk. michael@0: // XXX: Bug 984844. michael@0: if (callItem.attachment.actor.isLoadedFromDisk) { michael@0: return; michael@0: } michael@0: michael@0: // To keep continuous selection buttery smooth (for example, while pressing michael@0: // the DOWN key or moving the slider), only display the screenshot after michael@0: // any kind of user input stops. michael@0: setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => { michael@0: return !this._isSliding; michael@0: }, () => { michael@0: let frameSnapshot = SnapshotsListView.selectedItem.attachment.actor michael@0: let functionCall = callItem.attachment.actor; michael@0: frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => { michael@0: this.showScreenshot(screenshot); michael@0: this.highlightedThumbnail = screenshot.index; michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The mousedown listener for the call selection slider. michael@0: */ michael@0: _onSlideMouseDown: function() { michael@0: this._isSliding = true; michael@0: }, michael@0: michael@0: /** michael@0: * The mouseup listener for the call selection slider. michael@0: */ michael@0: _onSlideMouseUp: function() { michael@0: this._isSliding = false; michael@0: }, michael@0: michael@0: /** michael@0: * The change listener for the call selection slider. michael@0: */ michael@0: _onSlide: function() { michael@0: // Avoid performing any operations when programatically changing the value. michael@0: if (this._ignoreSliderChanges) { michael@0: return; michael@0: } michael@0: let selectedFunctionCallIndex = this.selectedIndex = this._slider.value; michael@0: michael@0: // While sliding, immediately show the most relevant thumbnail for a michael@0: // function call, for a nice diff-like animation effect between draws. michael@0: let thumbnails = SnapshotsListView.selectedItem.attachment.thumbnails; michael@0: let thumbnail = getThumbnailForCall(thumbnails, selectedFunctionCallIndex); michael@0: michael@0: // Avoid drawing and highlighting if the selected function call has the michael@0: // same thumbnail as the last one. michael@0: if (thumbnail.index == this.highlightedThumbnail) { michael@0: return; michael@0: } michael@0: // If a thumbnail wasn't found (e.g. the backend avoids creating thumbnails michael@0: // when rendering offscreen), simply defer to the first available one. michael@0: if (thumbnail.index == -1) { michael@0: thumbnail = thumbnails[0]; michael@0: } michael@0: michael@0: let { index, width, height, flipped, pixels } = thumbnail; michael@0: this.highlightedThumbnail = index; michael@0: michael@0: let screenshotNode = $("#screenshot-image"); michael@0: screenshotNode.setAttribute("flipped", flipped); michael@0: drawBackground("screenshot-rendering", width, height, pixels); michael@0: }, michael@0: michael@0: /** michael@0: * The input listener for the calls searchbox. michael@0: */ michael@0: _onSearch: function(e) { michael@0: let lowerCaseSearchToken = this._searchbox.value.toLowerCase(); michael@0: michael@0: this.filterContents(e => { michael@0: let call = e.attachment.actor; michael@0: let name = call.name.toLowerCase(); michael@0: let file = call.file.toLowerCase(); michael@0: let line = call.line.toString().toLowerCase(); michael@0: let args = call.argsPreview.toLowerCase(); michael@0: michael@0: return name.contains(lowerCaseSearchToken) || michael@0: file.contains(lowerCaseSearchToken) || michael@0: line.contains(lowerCaseSearchToken) || michael@0: args.contains(lowerCaseSearchToken); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The wheel listener for the filmstrip that contains all the thumbnails. michael@0: */ michael@0: _onScroll: function(e) { michael@0: this._filmstrip.scrollLeft += e.deltaX; michael@0: }, michael@0: michael@0: /** michael@0: * The click/dblclick listener for an item or location url in this container. michael@0: * When expanding an item, it's corresponding call stack will be displayed. michael@0: */ michael@0: _onExpand: function(e) { michael@0: let callItem = this.getItemForElement(e.target); michael@0: let view = $(".call-item-view", callItem.target); michael@0: michael@0: // If the call stack nodes were already created, simply re-show them michael@0: // or jump to the corresponding file and line in the Debugger if a michael@0: // location link was clicked. michael@0: if (view.hasAttribute("call-stack-populated")) { michael@0: let isExpanded = view.getAttribute("call-stack-expanded") == "true"; michael@0: michael@0: // If clicking on the location, jump to the Debugger. michael@0: if (e.target.classList.contains("call-item-location")) { michael@0: let { file, line } = callItem.attachment.actor; michael@0: viewSourceInDebugger(file, line); michael@0: return; michael@0: } michael@0: // Otherwise hide the call stack. michael@0: else { michael@0: view.setAttribute("call-stack-expanded", !isExpanded); michael@0: $(".call-item-stack", view).hidden = isExpanded; michael@0: return; michael@0: } michael@0: } michael@0: michael@0: let list = document.createElement("vbox"); michael@0: list.className = "call-item-stack"; michael@0: view.setAttribute("call-stack-populated", ""); michael@0: view.setAttribute("call-stack-expanded", "true"); michael@0: view.appendChild(list); michael@0: michael@0: /** michael@0: * Creates a function call nodes in this container for a stack. michael@0: */ michael@0: let display = stack => { michael@0: for (let i = 1; i < stack.length; i++) { michael@0: let call = stack[i]; michael@0: michael@0: let contents = document.createElement("hbox"); michael@0: contents.className = "call-item-stack-fn"; michael@0: contents.style.MozPaddingStart = (i * STACK_FUNC_INDENTATION) + "px"; michael@0: michael@0: let name = document.createElement("label"); michael@0: name.className = "plain call-item-stack-fn-name"; michael@0: name.setAttribute("value", "↳ " + call.name + "()"); michael@0: contents.appendChild(name); michael@0: michael@0: let spacer = document.createElement("spacer"); michael@0: spacer.setAttribute("flex", "100"); michael@0: contents.appendChild(spacer); michael@0: michael@0: let location = document.createElement("label"); michael@0: location.className = "plain call-item-stack-fn-location"; michael@0: location.setAttribute("value", getFileName(call.file) + ":" + call.line); michael@0: location.setAttribute("crop", "start"); michael@0: location.setAttribute("flex", "1"); michael@0: location.addEventListener("mousedown", e => this._onStackFileClick(e, call)); michael@0: contents.appendChild(location); michael@0: michael@0: list.appendChild(contents); michael@0: } michael@0: michael@0: window.emit(EVENTS.CALL_STACK_DISPLAYED); michael@0: }; michael@0: michael@0: // If this animation snapshot is loaded from disk, there are no corresponding michael@0: // backend actors available and the data is immediately available. michael@0: let functionCall = callItem.attachment.actor; michael@0: if (functionCall.isLoadedFromDisk) { michael@0: display(functionCall.stack); michael@0: } michael@0: // ..otherwise we need to request the function call stack from the backend. michael@0: else { michael@0: callItem.attachment.actor.getDetails().then(fn => display(fn.stack)); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for a location link in the call stack. michael@0: * michael@0: * @param string file michael@0: * The url of the source owning the function. michael@0: * @param number line michael@0: * The line of the respective function. michael@0: */ michael@0: _onStackFileClick: function(e, { file, line }) { michael@0: viewSourceInDebugger(file, line); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for a thumbnail in the filmstrip. michael@0: * michael@0: * @param number index michael@0: * The function index in the recorded animation frame snapshot. michael@0: */ michael@0: _onThumbnailClick: function(e, index) { michael@0: this.selectedIndex = index; michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "resume" button in this container's toolbar. michael@0: */ michael@0: _onResume: function() { michael@0: // Jump to the next draw call in the recorded animation frame snapshot. michael@0: let drawCall = getNextDrawCall(this.items, this.selectedItem); michael@0: if (drawCall) { michael@0: this.selectedItem = drawCall; michael@0: return; michael@0: } michael@0: michael@0: // If there are no more draw calls, just jump to the last context call. michael@0: this._onStepOut(); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "step over" button in this container's toolbar. michael@0: */ michael@0: _onStepOver: function() { michael@0: this.selectedIndex++; michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "step in" button in this container's toolbar. michael@0: */ michael@0: _onStepIn: function() { michael@0: if (this.selectedIndex == -1) { michael@0: this._onResume(); michael@0: return; michael@0: } michael@0: let callItem = this.selectedItem; michael@0: let { file, line } = callItem.attachment.actor; michael@0: viewSourceInDebugger(file, line); michael@0: }, michael@0: michael@0: /** michael@0: * The click listener for the "step out" button in this container's toolbar. michael@0: */ michael@0: _onStepOut: function() { michael@0: this.selectedIndex = this.itemCount - 1; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Localization convenience methods. michael@0: */ michael@0: let L10N = new ViewHelpers.L10N(STRINGS_URI); michael@0: michael@0: /** michael@0: * Convenient way of emitting events from the panel window. michael@0: */ michael@0: EventEmitter.decorate(this); michael@0: michael@0: /** michael@0: * DOM query helpers. michael@0: */ michael@0: function $(selector, target = document) target.querySelector(selector); michael@0: function $all(selector, target = document) target.querySelectorAll(selector); michael@0: michael@0: /** michael@0: * Helper for getting an nsIURL instance out of a string. michael@0: */ michael@0: function nsIURL(url, store = nsIURL.store) { michael@0: if (store.has(url)) { michael@0: return store.get(url); michael@0: } michael@0: let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL); michael@0: store.set(url, uri); michael@0: return uri; michael@0: } michael@0: michael@0: // The cache used in the `nsIURL` function. michael@0: nsIURL.store = new Map(); michael@0: michael@0: /** michael@0: * Gets the fileName part of a string which happens to be an URL. michael@0: */ michael@0: function getFileName(url) { michael@0: try { michael@0: let { fileName } = nsIURL(url); michael@0: return fileName || "/"; michael@0: } catch (e) { michael@0: // This doesn't look like a url, or nsIURL can't handle it. michael@0: return ""; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Gets an image data object containing a buffer large enough to hold michael@0: * width * height pixels. michael@0: * michael@0: * This method avoids allocating memory and tries to reuse a common buffer michael@0: * as much as possible. michael@0: * michael@0: * @param number w michael@0: * The desired image data storage width. michael@0: * @param number h michael@0: * The desired image data storage height. michael@0: * @return ImageData michael@0: * The requested image data buffer. michael@0: */ michael@0: function getImageDataStorage(ctx, w, h) { michael@0: let storage = getImageDataStorage.cache; michael@0: if (storage && storage.width == w && storage.height == h) { michael@0: return storage; michael@0: } michael@0: return getImageDataStorage.cache = ctx.createImageData(w, h); michael@0: } michael@0: michael@0: // The cache used in the `getImageDataStorage` function. michael@0: getImageDataStorage.cache = null; michael@0: michael@0: /** michael@0: * Draws image data into a canvas. michael@0: * michael@0: * This method makes absolutely no assumptions about the canvas element michael@0: * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels. michael@0: * michael@0: * @param HTMLCanvasElement canvas michael@0: * The canvas element to put the image data into. michael@0: * @param number width michael@0: * The image data width. michael@0: * @param number height michael@0: * The image data height. michael@0: * @param pixels michael@0: * An array buffer view of the image data. michael@0: * @param object options michael@0: * Additional options supported by this operation: michael@0: * - centered: specifies whether the image data should be centered michael@0: * when copied in the canvas; this is useful when the michael@0: * supplied pixels don't completely cover the canvas. michael@0: */ michael@0: function drawImage(canvas, width, height, pixels, options = {}) { michael@0: let ctx = canvas.getContext("2d"); michael@0: michael@0: // FrameSnapshot actors return "snapshot-image" type instances with just an michael@0: // empty pixel array if the source image is completely transparent. michael@0: if (pixels.length <= 1) { michael@0: ctx.clearRect(0, 0, canvas.width, canvas.height); michael@0: return; michael@0: } michael@0: michael@0: let arrayBuffer = new Uint8Array(pixels.buffer); michael@0: let imageData = getImageDataStorage(ctx, width, height); michael@0: imageData.data.set(arrayBuffer); michael@0: michael@0: if (options.centered) { michael@0: let left = (canvas.width - width) / 2; michael@0: let top = (canvas.height - height) / 2; michael@0: ctx.putImageData(imageData, left, top); michael@0: } else { michael@0: ctx.putImageData(imageData, 0, 0); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Draws image data into a canvas, and sets that as the rendering source for michael@0: * an element with the specified id as the -moz-element background image. michael@0: * michael@0: * @param string id michael@0: * The id of the -moz-element background image. michael@0: * @param number width michael@0: * The image data width. michael@0: * @param number height michael@0: * The image data height. michael@0: * @param pixels michael@0: * An array buffer view of the image data. michael@0: */ michael@0: function drawBackground(id, width, height, pixels) { michael@0: let canvas = document.createElementNS(HTML_NS, "canvas"); michael@0: canvas.width = width; michael@0: canvas.height = height; michael@0: michael@0: drawImage(canvas, width, height, pixels); michael@0: document.mozSetImageElement(id, canvas); michael@0: michael@0: // Used in tests. Not emitting an event because this shouldn't be "interesting". michael@0: if (window._onMozSetImageElement) { michael@0: window._onMozSetImageElement(pixels); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Iterates forward to find the next draw call in a snapshot. michael@0: */ michael@0: function getNextDrawCall(calls, call) { michael@0: for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) { michael@0: let nextCall = calls[i]; michael@0: let name = nextCall.attachment.actor.name; michael@0: if (CanvasFront.DRAW_CALLS.has(name)) { michael@0: return nextCall; michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Iterates backwards to find the most recent screenshot for a function call michael@0: * in a snapshot loaded from disk. michael@0: */ michael@0: function getScreenshotFromCallLoadedFromDisk(calls, call) { michael@0: for (let i = calls.indexOf(call); i >= 0; i--) { michael@0: let prevCall = calls[i]; michael@0: let screenshot = prevCall.screenshot; michael@0: if (screenshot) { michael@0: return screenshot; michael@0: } michael@0: } michael@0: return CanvasFront.INVALID_SNAPSHOT_IMAGE; michael@0: } michael@0: michael@0: /** michael@0: * Iterates backwards to find the most recent thumbnail for a function call. michael@0: */ michael@0: function getThumbnailForCall(thumbnails, index) { michael@0: for (let i = thumbnails.length - 1; i >= 0; i--) { michael@0: let thumbnail = thumbnails[i]; michael@0: if (thumbnail.index <= index) { michael@0: return thumbnail; michael@0: } michael@0: } michael@0: return CanvasFront.INVALID_SNAPSHOT_IMAGE; michael@0: } michael@0: michael@0: /** michael@0: * Opens/selects the debugger in this toolbox and jumps to the specified michael@0: * file name and line number. michael@0: */ michael@0: function viewSourceInDebugger(url, line) { michael@0: let showSource = ({ DebuggerView }) => { michael@0: if (DebuggerView.Sources.containsValue(url)) { michael@0: DebuggerView.setEditorLocation(url, line, { noDebug: true }).then(() => { michael@0: window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); michael@0: }, () => { michael@0: window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER); michael@0: }); michael@0: } michael@0: } michael@0: michael@0: // If the Debugger was already open, switch to it and try to show the michael@0: // source immediately. Otherwise, initialize it and wait for the sources michael@0: // to be added first. michael@0: let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger"); michael@0: gToolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => { michael@0: if (debuggerAlreadyOpen) { michael@0: showSource(dbg); michael@0: } else { michael@0: dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg)); michael@0: } michael@0: }); michael@0: }