123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- /* 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);
- }
- }
|