Bug 1265719 - Decouple the CanvasFront and FrameSnapshotFront from the CanvasActor and FrameSnapshotActor respectively; r=ejpbruel
authorNick Fitzgerald <fitzgen@gmail.com>
Fri, 03 Jun 2016 10:45:10 -0700
changeset 341373 4bacacb4d1cf8ea75b75a3bd9933fce249fc4d74
parent 341372 03b6ea8fded30783e73cc947b4b111e7746c12c3
child 341374 b6511016fd244575b9114c9f36382c1db402385d
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersejpbruel
bugs1265719
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1265719 - Decouple the CanvasFront and FrameSnapshotFront from the CanvasActor and FrameSnapshotActor respectively; r=ejpbruel
devtools/client/canvasdebugger/canvasdebugger.js
devtools/client/canvasdebugger/panel.js
devtools/client/canvasdebugger/test/head.js
devtools/server/actors/canvas.js
devtools/shared/fronts/canvas.js
devtools/shared/fronts/moz.build
devtools/shared/specs/canvas.js
devtools/shared/specs/moz.build
--- a/devtools/client/canvasdebugger/canvasdebugger.js
+++ b/devtools/client/canvasdebugger/canvasdebugger.js
@@ -8,17 +8,17 @@ var { classes: Cc, interfaces: Ci, utils
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 
 const { require } = Cu.import("resource://devtools/shared/Loader.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/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,
         setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers");
 
 const CANVAS_ACTOR_RECORDING_ATTEMPT = DevToolsUtils.testing ? 500 : 5000;
 
 const { Task } = require("devtools/shared/task");
--- a/devtools/client/canvasdebugger/panel.js
+++ b/devtools/client/canvasdebugger/panel.js
@@ -3,17 +3,17 @@
 /* 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 { 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) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
   this._destroyer = null;
 
   EventEmitter.decorate(this);
--- a/devtools/client/canvasdebugger/test/head.js
+++ b/devtools/client/canvasdebugger/test/head.js
@@ -8,17 +8,17 @@ var { generateUUID } = Cc["@mozilla.org/
 var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 
 var Services = require("Services");
 var promise = require("promise");
 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");
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var { isWebGLSupported } = require("devtools/client/shared/webgl-utils");
 var mm = null;
 
 const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
--- a/devtools/server/actors/canvas.js
+++ b/devtools/server/actors/canvas.js
@@ -6,108 +6,36 @@
 const {Cc, Ci, Cu, Cr} = require("chrome");
 const events = require("sdk/event/core");
 const promise = require("promise");
 const protocol = require("devtools/shared/protocol");
 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.
    *
    * @param DebuggerServerConnection conn
    *        The server connection.
    * @param HTMLCanvasElement canvas
    *        A reference to the content canvas.
    * @param array calls
@@ -121,37 +49,35 @@ var FrameSnapshotActor = protocol.ActorC
     this._functionCalls = calls;
     this._animationFrameEndScreenshot = screenshot;
     this._primitive = primitive;
   },
 
   /**
    * 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),
       screenshot: this._animationFrameEndScreenshot,
       primitive: {
         tris: this._primitive.tris,
         vertices: this._primitive.vertices,
         points: this._primitive.points,
         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;
 
     let canvas = this._contentCanvas;
     let calls = this._functionCalls;
     let index = calls.indexOf(functionCall);
 
     // To get a screenshot, replay all the steps necessary to render the frame,
@@ -181,182 +107,121 @@ var FrameSnapshotActor = protocol.ActorC
 
     // In case of the WebGL context, we also need to reset the framebuffer
     // binding to the original value, after generating the screenshot.
     doCleanup();
 
     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"
-  })
+  }
 });
 
 /**
  * This Canvas Actor handles simple instrumentation of all the methods
  * 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;
     this._webGLPrimitiveCounter = new WebGLPrimitiveCounter(tabActor);
     this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
   },
   destroy: function (conn) {
     protocol.Actor.prototype.destroy.call(this, conn);
     this._webGLPrimitiveCounter.destroy();
     this.finalize();
   },
 
   /**
    * Starts listening for function calls.
    */
-  setup: method(function ({ reload }) {
+  setup: function ({ reload }) {
     if (this._initialized) {
       return;
     }
     this._initialized = true;
 
     this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
     this._callWatcher.onCall = this._onContentFunctionCall;
     this._callWatcher.setup({
       tracedGlobals: CANVAS_CONTEXTS,
       tracedFunctions: [...ANIMATION_GENERATORS, ...LOOP_GENERATORS],
       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;
     }
     this._initialized = false;
 
     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;
     }
 
     this._recordingContainsDrawCall = false;
     this._callWatcher.eraseRecording();
     this._callWatcher.initTimestampEpoch();
     this._webGLPrimitiveCounter.resetCounts();
     this._callWatcher.resumeRecording();
 
     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;
     }
     this._animationStarted = false;
     this._callWatcher.pauseRecording();
     this._callWatcher.eraseRecording();
     this._currentAnimationFrameSnapshot.resolve(null);
     this._currentAnimationFrameSnapshot = null;
-  }, {
-    oneway: true
-  }),
+  },
 
   /**
    * Invoked whenever an instrumented function is called, be it on a
    * 2d or WebGL context, or an animation generator like requestAnimationFrame.
    */
   _onContentFunctionCall: function (functionCall) {
     let { window, name, args } = functionCall.details;
 
@@ -838,43 +703,16 @@ var ContextUtils = {
     let newViewport = [0, 0, width, height];
     gl.viewport.apply(gl, newViewport);
 
     return { oldViewport, newViewport };
   }
 };
 
 /**
- * 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.
  */
 function inplaceShallowCloneArrays(functionArguments, contentWindow) {
   let { Object, Array, ArrayBuffer } = contentWindow;
 
   functionArguments.forEach((arg, index, store) => {
     if (arg instanceof Array) {
new file mode 100644
--- /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;
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -4,16 +4,17 @@
 # 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/.
 
 DevToolsModules(
     'actor-registry.js',
     'addons.js',
     'animation.js',
     'call-watcher.js',
+    'canvas.js',
     'css-properties.js',
     'highlighters.js',
     'inspector.js',
     'storage.js',
     'styles.js',
     'stylesheets.js',
     'webaudio.js'
 )
new file mode 100644
--- /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;
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -4,16 +4,17 @@
 # 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/.
 
 DevToolsModules(
     'actor-registry.js',
     'addons.js',
     'animation.js',
     'call-watcher.js',
+    'canvas.js',
     'css-properties.js',
     'heap-snapshot-file.js',
     'highlighters.js',
     'inspector.js',
     'node.js',
     'storage.js',
     'styleeditor.js',
     'styles.js',