123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- /* 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";
- var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
- const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
- const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
- const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
- const promise = require("promise");
- const Services = require("Services");
- const EventEmitter = require("devtools/shared/event-emitter");
- const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
- const { CanvasFront } = require("devtools/shared/fronts/canvas");
- const DevToolsUtils = require("devtools/shared/DevToolsUtils");
- const flags = require("devtools/shared/flags");
- const { LocalizationHelper } = require("devtools/shared/l10n");
- const { PluralForm } = require("devtools/shared/plural-form");
- const { Heritage, WidgetMethods, setNamedTimeout, clearNamedTimeout,
- setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers");
- const CANVAS_ACTOR_RECORDING_ATTEMPT = flags.testing ? 500 : 5000;
- const { Task } = require("devtools/shared/task");
- XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
- "resource://gre/modules/FileUtils.jsm");
- XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
- XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function () {
- return require("devtools/shared/webconsole/network-helper");
- });
- // 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, and
- // whether it was completed succesfully or cancelled.
- SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted",
- SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished",
- SNAPSHOT_RECORDING_COMPLETED: "CanvasDebugger:SnapshotRecordingCompleted",
- SNAPSHOT_RECORDING_CANCELLED: "CanvasDebugger:SnapshotRecordingCancelled",
- // 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"
- };
- XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
- const HTML_NS = "http://www.w3.org/1999/xhtml";
- const STRINGS_URI = "devtools/client/locales/canvasdebugger.properties";
- const SHARED_STRINGS_URI = "devtools/client/locales/shared.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.
- */
- var 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.
- */
- var EventsHandler = {
- /**
- * Listen for events emitted by the current tab target.
- */
- initialize: function () {
- // Make sure the backend is prepared to handle <canvas> contexts.
- // Since actors are created lazily on the first request to them, we need to send an
- // early request to ensure the CallWatcherActor is running and watching for new window
- // globals.
- gFront.setup({ reload: false });
- 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 () {
- 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;
- }
- // 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;
- $("#waiting-notice").hidden = true;
- $("#debugging-pane-contents").hidden = true;
- $("#screenshot-container").hidden = true;
- $("#snapshot-filmstrip").hidden = true;
- window.emit(EVENTS.UI_RESET);
- }
- };
- /**
- * Localization convenience methods.
- */
- var L10N = new LocalizationHelper(STRINGS_URI);
- var SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
- /**
- * Convenient way of emitting events from the panel window.
- */
- EventEmitter.decorate(this);
- /**
- * DOM query helpers.
- */
- var $ = (selector, target = document) => target.querySelector(selector);
- var $all = (selector, target = document) => target.querySelectorAll(selector);
- /**
- * Gets the fileName part of a string which happens to be an URL.
- */
- function getFileName(url) {
- try {
- let { fileName } = NetworkHelper.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 array 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 imageData = getImageDataStorage(ctx, width, height);
- imageData.data.set(pixels);
- 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 array 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;
- }
|