diff options
Diffstat (limited to 'devtools/client/canvasdebugger/canvasdebugger.js')
-rw-r--r-- | devtools/client/canvasdebugger/canvasdebugger.js | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/devtools/client/canvasdebugger/canvasdebugger.js b/devtools/client/canvasdebugger/canvasdebugger.js new file mode 100644 index 000000000..c46cc6d0c --- /dev/null +++ b/devtools/client/canvasdebugger/canvasdebugger.js @@ -0,0 +1,341 @@ +/* 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; +} |