summaryrefslogtreecommitdiffstats
path: root/devtools/client/canvasdebugger/snapshotslist.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/canvasdebugger/snapshotslist.js')
-rw-r--r--devtools/client/canvasdebugger/snapshotslist.js495
1 files changed, 495 insertions, 0 deletions
diff --git a/devtools/client/canvasdebugger/snapshotslist.js b/devtools/client/canvasdebugger/snapshotslist.js
new file mode 100644
index 000000000..da3b4a7eb
--- /dev/null
+++ b/devtools/client/canvasdebugger/snapshotslist.js
@@ -0,0 +1,495 @@
+/* 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/. */
+/* import-globals-from canvasdebugger.js */
+/* globals window, document */
+"use strict";
+
+/**
+ * Functions handling the recorded animation frame snapshots UI.
+ */
+var 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._onRecordSuccess = this._onRecordSuccess.bind(this);
+ this._onRecordFailure = this._onRecordFailure.bind(this);
+ this._stopRecordingAnimation = this._stopRecordingAnimation.bind(this);
+
+ window.on(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton);
+ this.emptyText = L10N.getStr("noSnapshotsText");
+ this.widget.addEventListener("select", this._onSelect, false);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ clearNamedTimeout("canvas-actor-recording");
+ window.off(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton);
+ 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
+ }
+ });
+ },
+
+ /**
+ * Removes the last snapshot added, in the event no requestAnimationFrame loop was found.
+ */
+ removeLastSnapshot: function () {
+ this.removeAt(this.itemCount - 1);
+ // If this is the only item, revert back to the empty notice
+ if (this.itemCount === 0) {
+ $("#empty-notice").hidden = false;
+ $("#waiting-notice").hidden = true;
+ }
+ },
+
+ /**
+ * 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 }) {
+ // Check to ensure the attachment has an actor, like
+ // an in-progress recording.
+ if (!snapshotItem || !snapshotItem.attachment.actor) {
+ return;
+ }
+ let { calls, thumbnails, screenshot } = snapshotItem.attachment;
+
+ $("#reload-notice").hidden = true;
+ $("#empty-notice").hidden = true;
+ $("#waiting-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;
+ $("#waiting-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;
+ $("#waiting-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 () {
+ this._disableRecordButton();
+
+ if (this._recording) {
+ this._stopRecordingAnimation();
+ return;
+ }
+
+ // 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.
+ this.addSnapshot();
+
+ // If this is the first item, immediately show the "Loading…" notice.
+ if (this.itemCount == 1) {
+ $("#empty-notice").hidden = true;
+ $("#waiting-notice").hidden = false;
+ }
+
+ this._recordAnimation();
+ },
+
+ /**
+ * Makes the record button able to be clicked again.
+ */
+ _enableRecordButton: function () {
+ $("#record-snapshot").removeAttribute("disabled");
+ },
+
+ /**
+ * Makes the record button unable to be clicked.
+ */
+ _disableRecordButton: function () {
+ $("#record-snapshot").setAttribute("disabled", true);
+ },
+
+ /**
+ * Begins recording an animation.
+ */
+ _recordAnimation: Task.async(function* () {
+ if (this._recording) {
+ return;
+ }
+ this._recording = true;
+ $("#record-snapshot").setAttribute("checked", "true");
+
+ setNamedTimeout("canvas-actor-recording", CANVAS_ACTOR_RECORDING_ATTEMPT, this._stopRecordingAnimation);
+
+ yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
+ window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED);
+
+ gFront.recordAnimationFrame().then(snapshot => {
+ if (snapshot) {
+ this._onRecordSuccess(snapshot);
+ } else {
+ this._onRecordFailure();
+ }
+ });
+
+ // Wait another delay before reenabling the button to stop the recording
+ // if a recording is not found.
+ yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
+ this._enableRecordButton();
+ }),
+
+ /**
+ * Stops recording animation. Called when a click on the stopwatch occurs during a recording,
+ * or if a recording times out.
+ */
+ _stopRecordingAnimation: Task.async(function* () {
+ clearNamedTimeout("canvas-actor-recording");
+ let actorCanStop = yield gTarget.actorHasMethod("canvas", "stopRecordingAnimationFrame");
+
+ if (actorCanStop) {
+ yield gFront.stopRecordingAnimationFrame();
+ }
+ // If actor does not have the method to stop recording (Fx39+),
+ // manually call the record failure method. This will call a connection failure
+ // on disconnect as a result of `gFront.recordAnimationFrame()` never resolving,
+ // but this is better than it hanging when there is no requestAnimationFrame anyway.
+ else {
+ this._onRecordFailure();
+ }
+
+ this._recording = false;
+ $("#record-snapshot").removeAttribute("checked");
+ this._enableRecordButton();
+ }),
+
+ /**
+ * Resolves from the front's recordAnimationFrame to setup the interface with the screenshots.
+ */
+ _onRecordSuccess: Task.async(function* (snapshotActor) {
+ // Clear bail-out case if frame found in CANVAS_ACTOR_RECORDING_ATTEMPT milliseconds
+ clearNamedTimeout("canvas-actor-recording");
+ let snapshotItem = this.getItemAtIndex(this.itemCount - 1);
+ let snapshotOverview = yield snapshotActor.getOverview();
+ this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview);
+
+ this._recording = false;
+ $("#record-snapshot").removeAttribute("checked");
+
+ window.emit(EVENTS.SNAPSHOT_RECORDING_COMPLETED);
+ window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ }),
+
+ /**
+ * Called as a reject from the front's recordAnimationFrame.
+ */
+ _onRecordFailure: function () {
+ clearNamedTimeout("canvas-actor-recording");
+ showNotification(gToolbox, "canvas-debugger-timeout", L10N.getStr("recordingTimeoutFailure"));
+ window.emit(EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+ window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
+ this.removeLastSnapshot();
+ },
+
+ /**
+ * 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({
+ uri: NetUtil.newURI(fp.file), loadUsingSystemPrincipal: true});
+ 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);
+
+ 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, timestamp, argsPreview, callerPreview } = call;
+ return call.getDetails().then(({ stack }) => {
+ data.calls[i] = {
+ type: type,
+ name: name,
+ file: file,
+ line: line,
+ stack: stack,
+ timestamp: timestamp,
+ 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, width, height, flipped, pixels });
+ });
+
+ // Prepare the screenshot for serialization.
+ let { index, width, height, flipped, pixels } = screenshot;
+ data.screenshot = { index, width, height, flipped, 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.classList.add("devtools-throbber");
+ 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.classList.remove("devtools-throbber");
+ save.removeAttribute("disabled");
+ save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel"));
+ });
+ });
+ }});
+ }
+});
+
+function showNotification(toolbox, name, message) {
+ let notificationBox = toolbox.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue(name);
+ if (!notification) {
+ notificationBox.appendNotification(message, name, "", notificationBox.PRIORITY_WARNING_HIGH);
+ }
+}