From de3a8e256645718a6fee08a559b324db8d1bfbcf Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Fri, 3 Jun 2016 10:45:10 -0700 Subject: [PATCH] Bug 1265719 - Decouple the CanvasFront and FrameSnapshotFront from the CanvasActor and FrameSnapshotActor respectively; r=ejpbruel --- .../client/canvasdebugger/canvasdebugger.js | 2 +- devtools/client/canvasdebugger/panel.js | 2 +- devtools/client/canvasdebugger/test/head.js | 2 +- devtools/server/actors/canvas.js | 218 +++--------------- devtools/shared/fronts/canvas.js | 91 ++++++++ devtools/shared/fronts/moz.build | 1 + devtools/shared/specs/canvas.js | 131 +++++++++++ devtools/shared/specs/moz.build | 1 + 8 files changed, 255 insertions(+), 193 deletions(-) create mode 100644 devtools/shared/fronts/canvas.js create mode 100644 devtools/shared/specs/canvas.js diff --git a/devtools/client/canvasdebugger/canvasdebugger.js b/devtools/client/canvasdebugger/canvasdebugger.js index 41e8cfb98eb0..889af6000740 100644 --- a/devtools/client/canvasdebugger/canvasdebugger.js +++ b/devtools/client/canvasdebugger/canvasdebugger.js @@ -13,7 +13,7 @@ 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/server/actors/canvas"); +const { CanvasFront } = require("devtools/shared/fronts/canvas"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { LocalizationHelper } = require("devtools/client/shared/l10n"); const { Heritage, WidgetMethods, setNamedTimeout, clearNamedTimeout, diff --git a/devtools/client/canvasdebugger/panel.js b/devtools/client/canvasdebugger/panel.js index c112f9faf764..4535886c7135 100644 --- a/devtools/client/canvasdebugger/panel.js +++ b/devtools/client/canvasdebugger/panel.js @@ -8,7 +8,7 @@ const { Cc, Ci, Cu, Cr } = require("chrome"); const promise = require("promise"); const EventEmitter = require("devtools/shared/event-emitter"); -const { CanvasFront } = require("devtools/server/actors/canvas"); +const { CanvasFront } = require("devtools/shared/fronts/canvas"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); function CanvasDebuggerPanel(iframeWindow, toolbox) { diff --git a/devtools/client/canvasdebugger/test/head.js b/devtools/client/canvasdebugger/test/head.js index bee375becc72..fd1b842a7a6b 100644 --- a/devtools/client/canvasdebugger/test/head.js +++ b/devtools/client/canvasdebugger/test/head.js @@ -13,7 +13,7 @@ var { gDevTools } = require("devtools/client/framework/devtools"); var { DebuggerClient } = require("devtools/shared/client/main"); var { DebuggerServer } = require("devtools/server/main"); var { CallWatcherFront } = require("devtools/shared/fronts/call-watcher"); -var { CanvasFront } = require("devtools/server/actors/canvas"); +var { CanvasFront } = require("devtools/shared/fronts/canvas"); var { setTimeout } = require("sdk/timers"); var DevToolsUtils = require("devtools/shared/DevToolsUtils"); var { TargetFactory } = require("devtools/client/framework/target"); diff --git a/devtools/server/actors/canvas.js b/devtools/server/actors/canvas.js index b26642c37570..0525370bd475 100644 --- a/devtools/server/actors/canvas.js +++ b/devtools/server/actors/canvas.js @@ -11,98 +11,26 @@ const {CallWatcherActor} = require("devtools/server/actors/call-watcher"); const {CallWatcherFront} = require("devtools/shared/fronts/call-watcher"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const {WebGLPrimitiveCounter} = require("devtools/server/primitive"); +const { + frameSnapshotSpec, + canvasSpec, + CANVAS_CONTEXTS, + ANIMATION_GENERATORS, + LOOP_GENERATORS, + DRAW_CALLS, + INTERESTING_CALLS, +} = require("devtools/shared/specs/canvas"); +const {CanvasFront} = require("devtools/shared/fronts/canvas"); const {on, once, off, emit} = events; const {method, custom, Arg, Option, RetVal} = protocol; -const CANVAS_CONTEXTS = [ - "CanvasRenderingContext2D", - "WebGLRenderingContext" -]; - -const ANIMATION_GENERATORS = [ - "requestAnimationFrame" -]; - -const LOOP_GENERATORS = [ - "setTimeout" -]; - -const DRAW_CALLS = [ - // 2D canvas - "fill", - "stroke", - "clearRect", - "fillRect", - "strokeRect", - "fillText", - "strokeText", - "drawImage", - - // WebGL - "clear", - "drawArrays", - "drawElements", - "finish", - "flush" -]; - -const INTERESTING_CALLS = [ - // 2D canvas - "save", - "restore", - - // WebGL - "useProgram" -]; - -/** - * Type representing an ArrayBufferView, serialized fast(er). - * - * Don't create a new array buffer view from the parsed array on the frontend. - * Consumers may copy the data into an existing buffer, or create a new one if - * necesasry. For example, this avoids the need for a redundant copy when - * populating ImageData objects, at the expense of transferring char views - * of a pixel buffer over the protocol instead of a packed int view. - * - * XXX: It would be nice if on local connections (only), we could just *give* - * the buffer directly to the front, instead of going through all this - * serialization redundancy. - */ -protocol.types.addType("array-buffer-view", { - write: (v) => "[" + Array.join(v, ",") + "]", - read: (v) => JSON.parse(v) -}); - -/** - * Type describing a thumbnail or screenshot in a recorded animation frame. - */ -protocol.types.addDictType("snapshot-image", { - index: "number", - width: "number", - height: "number", - scaling: "number", - flipped: "boolean", - pixels: "array-buffer-view" -}); - -/** - * Type describing an overview of a recorded animation frame. - */ -protocol.types.addDictType("snapshot-overview", { - calls: "array:function-call", - thumbnails: "array:snapshot-image", - screenshot: "snapshot-image" -}); - /** * This actor represents a recorded animation frame snapshot, along with * all the corresponding canvas' context methods invoked in that frame, * thumbnails for each draw call and a screenshot of the end result. */ -var FrameSnapshotActor = protocol.ActorClass({ - typeName: "frame-snapshot", - +var FrameSnapshotActor = protocol.ActorClassWithSpec(frameSnapshotSpec, { /** * Creates the frame snapshot call actor. * @@ -126,7 +54,7 @@ var FrameSnapshotActor = protocol.ActorClass({ /** * Gets as much data about this snapshot without computing anything costly. */ - getOverview: method(function () { + getOverview: function () { return { calls: this._functionCalls, thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e), @@ -138,15 +66,13 @@ var FrameSnapshotActor = protocol.ActorClass({ lines: this._primitive.lines } }; - }, { - response: { overview: RetVal("snapshot-overview") } - }), + }, /** * Gets a screenshot of the canvas's contents after the specified * function was called. */ - generateScreenshotFor: method(function (functionCall) { + generateScreenshotFor: function (functionCall) { let caller = functionCall.details.caller; let global = functionCall.details.global; @@ -186,54 +112,7 @@ var FrameSnapshotActor = protocol.ActorClass({ screenshot.scaling = replayContextScaling; screenshot.index = lastDrawCallIndex; return screenshot; - }, { - request: { call: Arg(0, "function-call") }, - response: { screenshot: RetVal("snapshot-image") } - }) -}); - -/** - * The corresponding Front object for the FrameSnapshotActor. - */ -var FrameSnapshotFront = protocol.FrontClass(FrameSnapshotActor, { - initialize: function (client, form) { - protocol.Front.prototype.initialize.call(this, client, form); - this._animationFrameEndScreenshot = null; - this._cachedScreenshots = new WeakMap(); - }, - - /** - * This implementation caches the animation frame end screenshot to optimize - * frontend requests to `generateScreenshotFor`. - */ - getOverview: custom(function () { - return this._getOverview().then(data => { - this._animationFrameEndScreenshot = data.screenshot; - return data; - }); - }, { - impl: "_getOverview" - }), - - /** - * This implementation saves a roundtrip to the backend if the screenshot - * was already generated and retrieved once. - */ - generateScreenshotFor: custom(function (functionCall) { - if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name) || - CanvasFront.LOOP_GENERATORS.has(functionCall.name)) { - return promise.resolve(this._animationFrameEndScreenshot); - } - let cachedScreenshot = this._cachedScreenshots.get(functionCall); - if (cachedScreenshot) { - return cachedScreenshot; - } - let screenshot = this._generateScreenshotFor(functionCall); - this._cachedScreenshots.set(functionCall, screenshot); - return screenshot; - }, { - impl: "_generateScreenshotFor" - }) + } }); /** @@ -241,12 +120,11 @@ var FrameSnapshotFront = protocol.FrontClass(FrameSnapshotActor, { * of a 2D or WebGL context, to provide information regarding all the calls * made when drawing frame inside an animation loop. */ -var CanvasActor = exports.CanvasActor = protocol.ActorClass({ +var CanvasActor = exports.CanvasActor = protocol.ActorClassWithSpec(canvasSpec, { // Reset for each recording, boolean indicating whether or not // any draw calls were called for a recording. _animationContainsDrawCall: false, - typeName: "canvas", initialize: function (conn, tabActor) { protocol.Actor.prototype.initialize.call(this, conn); this.tabActor = tabActor; @@ -262,7 +140,7 @@ var CanvasActor = exports.CanvasActor = protocol.ActorClass({ /** * Starts listening for function calls. */ - setup: method(function ({ reload }) { + setup: function ({ reload }) { if (this._initialized) { return; } @@ -276,15 +154,12 @@ var CanvasActor = exports.CanvasActor = protocol.ActorClass({ performReload: reload, storeCalls: true }); - }, { - request: { reload: Option(0, "boolean") }, - oneway: true - }), + }, /** * Stops listening for function calls. */ - finalize: method(function () { + finalize: function () { if (!this._initialized) { return; } @@ -292,35 +167,29 @@ var CanvasActor = exports.CanvasActor = protocol.ActorClass({ this._callWatcher.finalize(); this._callWatcher = null; - }, { - oneway: true - }), + }, /** * Returns whether this actor has been set up. */ - isInitialized: method(function () { + isInitialized: function () { return !!this._initialized; - }, { - response: { initialized: RetVal("boolean") } - }), + }, /** * Returns whether or not the CanvasActor is recording an animation. * Used in tests. */ - isRecording: method(function () { + isRecording: function () { return !!this._callWatcher.isRecording(); - }, { - response: { recording: RetVal("boolean") } - }), + }, /** * Records a snapshot of all the calls made during the next animation frame. * The animation should be implemented via the de-facto requestAnimationFrame * utility, or inside recursive `setTimeout`s. `setInterval` at this time are not supported. */ - recordAnimationFrame: method(function () { + recordAnimationFrame: function () { if (this._callWatcher.isRecording()) { return this._currentAnimationFrameSnapshot.promise; } @@ -333,14 +202,12 @@ var CanvasActor = exports.CanvasActor = protocol.ActorClass({ let deferred = this._currentAnimationFrameSnapshot = promise.defer(); return deferred.promise; - }, { - response: { snapshot: RetVal("nullable:frame-snapshot") } - }), + }, /** * Cease attempts to record an animation frame. */ - stopRecordingAnimationFrame: method(function () { + stopRecordingAnimationFrame: function () { if (!this._callWatcher.isRecording()) { return; } @@ -349,9 +216,7 @@ var CanvasActor = exports.CanvasActor = protocol.ActorClass({ this._callWatcher.eraseRecording(); this._currentAnimationFrameSnapshot.resolve(null); this._currentAnimationFrameSnapshot = null; - }, { - oneway: true - }), + }, /** * Invoked whenever an instrumented function is called, be it on a @@ -842,33 +707,6 @@ var ContextUtils = { } }; -/** - * The corresponding Front object for the CanvasActor. - */ -var CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, { - initialize: function (client, { canvasActor }) { - protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor }); - this.manage(this); - } -}); - -/** - * Constants. - */ -CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS); -CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS); -CanvasFront.LOOP_GENERATORS = new Set(LOOP_GENERATORS); -CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS); -CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS); -CanvasFront.THUMBNAIL_SIZE = 50; // px -CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT = 256; // px -CanvasFront.INVALID_SNAPSHOT_IMAGE = { - index: -1, - width: 0, - height: 0, - pixels: [] -}; - /** * Goes through all the arguments and creates a one-level shallow copy * of all arrays and array buffers. diff --git a/devtools/shared/fronts/canvas.js b/devtools/shared/fronts/canvas.js new file mode 100644 index 000000000000..f3a1a6075579 --- /dev/null +++ b/devtools/shared/fronts/canvas.js @@ -0,0 +1,91 @@ +/* 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 { + frameSnapshotSpec, + canvasSpec, + CANVAS_CONTEXTS, + ANIMATION_GENERATORS, + LOOP_GENERATORS, + DRAW_CALLS, + INTERESTING_CALLS, +} = require("devtools/shared/specs/canvas"); +const protocol = require("devtools/shared/protocol"); +const promise = require("promise"); + +/** + * The corresponding Front object for the FrameSnapshotActor. + */ +const FrameSnapshotFront = protocol.FrontClassWithSpec(frameSnapshotSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + this._animationFrameEndScreenshot = null; + this._cachedScreenshots = new WeakMap(); + }, + + /** + * This implementation caches the animation frame end screenshot to optimize + * frontend requests to `generateScreenshotFor`. + */ + getOverview: protocol.custom(function () { + return this._getOverview().then(data => { + this._animationFrameEndScreenshot = data.screenshot; + return data; + }); + }, { + impl: "_getOverview" + }), + + /** + * This implementation saves a roundtrip to the backend if the screenshot + * was already generated and retrieved once. + */ + generateScreenshotFor: protocol.custom(function (functionCall) { + if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name) || + CanvasFront.LOOP_GENERATORS.has(functionCall.name)) { + return promise.resolve(this._animationFrameEndScreenshot); + } + let cachedScreenshot = this._cachedScreenshots.get(functionCall); + if (cachedScreenshot) { + return cachedScreenshot; + } + let screenshot = this._generateScreenshotFor(functionCall); + this._cachedScreenshots.set(functionCall, screenshot); + return screenshot; + }, { + impl: "_generateScreenshotFor" + }) +}); + +exports.FrameSnapshotFront = FrameSnapshotFront; + +/** + * The corresponding Front object for the CanvasActor. + */ +const CanvasFront = protocol.FrontClassWithSpec(canvasSpec, { + initialize: function (client, { canvasActor }) { + protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor }); + this.manage(this); + } +}); + +/** + * Constants. + */ +CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS); +CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS); +CanvasFront.LOOP_GENERATORS = new Set(LOOP_GENERATORS); +CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS); +CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS); +CanvasFront.THUMBNAIL_SIZE = 50; +CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT = 256; +CanvasFront.INVALID_SNAPSHOT_IMAGE = { + index: -1, + width: 0, + height: 0, + pixels: [] +}; + +exports.CanvasFront = CanvasFront; diff --git a/devtools/shared/fronts/moz.build b/devtools/shared/fronts/moz.build index 3cb377751e53..a89cd00f5ca5 100644 --- a/devtools/shared/fronts/moz.build +++ b/devtools/shared/fronts/moz.build @@ -9,6 +9,7 @@ DevToolsModules( 'addons.js', 'animation.js', 'call-watcher.js', + 'canvas.js', 'css-properties.js', 'highlighters.js', 'inspector.js', diff --git a/devtools/shared/specs/canvas.js b/devtools/shared/specs/canvas.js new file mode 100644 index 000000000000..dcb26f9a945e --- /dev/null +++ b/devtools/shared/specs/canvas.js @@ -0,0 +1,131 @@ +/* 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 protocol = require("devtools/shared/protocol"); +const {Arg, Option, RetVal, generateActorSpec} = protocol; + +/** + * Type representing an ArrayBufferView, serialized fast(er). + * + * Don't create a new array buffer view from the parsed array on the frontend. + * Consumers may copy the data into an existing buffer, or create a new one if + * necesasry. For example, this avoids the need for a redundant copy when + * populating ImageData objects, at the expense of transferring char views + * of a pixel buffer over the protocol instead of a packed int view. + * + * XXX: It would be nice if on local connections (only), we could just *give* + * the buffer directly to the front, instead of going through all this + * serialization redundancy. + */ +protocol.types.addType("array-buffer-view", { + write: (v) => "[" + Array.join(v, ",") + "]", + read: (v) => JSON.parse(v) +}); + +/** + * Type describing a thumbnail or screenshot in a recorded animation frame. + */ +protocol.types.addDictType("snapshot-image", { + index: "number", + width: "number", + height: "number", + scaling: "number", + flipped: "boolean", + pixels: "array-buffer-view" +}); + +/** + * Type describing an overview of a recorded animation frame. + */ +protocol.types.addDictType("snapshot-overview", { + calls: "array:function-call", + thumbnails: "array:snapshot-image", + screenshot: "snapshot-image" +}); + +exports.CANVAS_CONTEXTS = [ + "CanvasRenderingContext2D", + "WebGLRenderingContext" +]; + +exports.ANIMATION_GENERATORS = [ + "requestAnimationFrame" +]; + +exports.LOOP_GENERATORS = [ + "setTimeout" +]; + +exports.DRAW_CALLS = [ + // 2D canvas + "fill", + "stroke", + "clearRect", + "fillRect", + "strokeRect", + "fillText", + "strokeText", + "drawImage", + + // WebGL + "clear", + "drawArrays", + "drawElements", + "finish", + "flush" +]; + +exports.INTERESTING_CALLS = [ + // 2D canvas + "save", + "restore", + + // WebGL + "useProgram" +]; + +const frameSnapshotSpec = generateActorSpec({ + typeName: "frame-snapshot", + + methods: { + getOverview: { + response: { overview: RetVal("snapshot-overview") } + }, + generateScreenshotFor: { + request: { call: Arg(0, "function-call") }, + response: { screenshot: RetVal("snapshot-image") } + }, + }, +}); + +exports.frameSnapshotSpec = frameSnapshotSpec; + +const canvasSpec = generateActorSpec({ + typeName: "canvas", + + methods: { + setup: { + request: { reload: Option(0, "boolean") }, + oneway: true + }, + finalize: { + oneway: true + }, + isInitialized: { + response: { initialized: RetVal("boolean") } + }, + isRecording: { + response: { recording: RetVal("boolean") } + }, + recordAnimationFrame: { + response: { snapshot: RetVal("nullable:frame-snapshot") } + }, + stopRecordingAnimationFrame: { + oneway: true + }, + } +}); + +exports.canvasSpec = canvasSpec; diff --git a/devtools/shared/specs/moz.build b/devtools/shared/specs/moz.build index 6b80a4e64551..e52214d31255 100644 --- a/devtools/shared/specs/moz.build +++ b/devtools/shared/specs/moz.build @@ -9,6 +9,7 @@ DevToolsModules( 'addons.js', 'animation.js', 'call-watcher.js', + 'canvas.js', 'css-properties.js', 'heap-snapshot-file.js', 'highlighters.js',