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