Bug 1041225 - Generating a screenshot is very slow when the content canvas has obnoxious dimensions, r=rcampbell
authorVictor Porof <vporof@mozilla.com>
Tue, 22 Jul 2014 12:43:24 -0400
changeset 195538 3d091524a1b8b81722ef94b1bab23d70cf2e5a02
parent 195537 fb21d6d2fdfa4b9cec9d48221a6fb1033d1d6117
child 195539 ab11981ffeb0522984fa0fbbcd318748843061c6
push id27185
push userkwierso@gmail.com
push dateWed, 23 Jul 2014 01:05:43 +0000
treeherdermozilla-central@5683746bac22 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell
bugs1041225
milestone34.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 1041225 - Generating a screenshot is very slow when the content canvas has obnoxious dimensions, r=rcampbell
browser/devtools/canvasdebugger/canvasdebugger.js
browser/devtools/canvasdebugger/test/browser_canvas-actor-test-10.js
browser/devtools/canvasdebugger/test/doc_webgl-bindings.html
toolkit/devtools/server/actors/canvas.js
--- a/browser/devtools/canvasdebugger/canvasdebugger.js
+++ b/browser/devtools/canvasdebugger/canvasdebugger.js
@@ -202,18 +202,18 @@ let SnapshotsListView = Heritage.extend(
    *         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_HEIGHT;
-    thumbnail.height = CanvasFront.THUMBNAIL_HEIGHT;
+    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";
@@ -707,24 +707,26 @@ let CallsListView = Heritage.extend(Widg
   /**
    * Displays an image in the rendering preview of this container, generated
    * for the specified draw call in the recorded animation frame snapshot.
    *
    * @param array screenshot
    *        A single "snapshot-image" instance received from the backend.
    */
   showScreenshot: function(screenshot) {
-    let { index, width, height, flipped, pixels } = screenshot;
+    let { index, width, height, scaling, flipped, pixels } = screenshot;
 
     let screenshotNode = $("#screenshot-image");
     screenshotNode.setAttribute("flipped", flipped);
     drawBackground("screenshot-rendering", width, height, pixels);
 
     let dimensionsNode = $("#screenshot-dimensions");
-    dimensionsNode.setAttribute("value", ~~width + " x " + ~~height);
+    let actualWidth = (width / scaling) | 0;
+    let actualHeight = (height / scaling) | 0;
+    dimensionsNode.setAttribute("value", actualWidth + " x " + actualHeight);
 
     window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED);
   },
 
   /**
    * Populates this container's footer with a list of thumbnails, one generated
    * for each draw call in the recorded animation frame snapshot.
    *
@@ -749,18 +751,18 @@ let CallsListView = Heritage.extend(Widg
    * @param array thumbnail
    *        A single "snapshot-image" instance received from the backend.
    */
   appendThumbnail: function(thumbnail) {
     let { index, width, height, flipped, pixels } = thumbnail;
 
     let thumbnailNode = document.createElementNS(HTML_NS, "canvas");
     thumbnailNode.setAttribute("flipped", flipped);
-    thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_HEIGHT, width);
-    thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_HEIGHT, height);
+    thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_SIZE, width);
+    thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_SIZE, height);
     drawImage(thumbnailNode, width, height, pixels, { centered: true });
 
     thumbnailNode.className = "filmstrip-thumbnail";
     thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index);
     thumbnailNode.setAttribute("index", index);
     this._filmstrip.appendChild(thumbnailNode);
   },
 
--- a/browser/devtools/canvasdebugger/test/browser_canvas-actor-test-10.js
+++ b/browser/devtools/canvasdebugger/test/browser_canvas-actor-test-10.js
@@ -19,43 +19,55 @@ function ifTestingSupported() {
 
   let snapshotActor = yield front.recordAnimationFrame();
   let animationOverview = yield snapshotActor.getOverview();
   let functionCalls = animationOverview.calls;
 
   let firstScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
   is(firstScreenshot.index, -1,
     "The first screenshot didn't encounter any draw call.");
-  is(firstScreenshot.width, 128,
+  is(firstScreenshot.scaling, 0.25,
+    "The first screenshot has the correct scaling.");
+  is(firstScreenshot.width, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
     "The first screenshot has the correct width.");
-  is(firstScreenshot.height, 128,
+  is(firstScreenshot.height, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
     "The first screenshot has the correct height.");
   is(firstScreenshot.flipped, true,
     "The first screenshot has the correct 'flipped' flag.");
   is(firstScreenshot.pixels.length, 0,
     "The first screenshot should be empty.");
 
   let gl = debuggee.gl;
   is(gl.getParameter(gl.FRAMEBUFFER_BINDING), debuggee.customFramebuffer,
     "The debuggee's gl context framebuffer wasn't changed.");
   is(gl.getParameter(gl.RENDERBUFFER_BINDING), debuggee.customRenderbuffer,
     "The debuggee's gl context renderbuffer wasn't changed.");
   is(gl.getParameter(gl.TEXTURE_BINDING_2D), debuggee.customTexture,
     "The debuggee's gl context texture binding wasn't changed.");
+  is(gl.getParameter(gl.VIEWPORT)[0], 128,
+    "The debuggee's gl context viewport's left coord. wasn't changed.");
+  is(gl.getParameter(gl.VIEWPORT)[1], 256,
+    "The debuggee's gl context viewport's left coord. wasn't changed.");
+  is(gl.getParameter(gl.VIEWPORT)[2], 384,
+    "The debuggee's gl context viewport's left coord. wasn't changed.");
+  is(gl.getParameter(gl.VIEWPORT)[3], 512,
+    "The debuggee's gl context viewport's left coord. wasn't changed.");
 
   let secondScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[1]);
   is(secondScreenshot.index, 1,
     "The second screenshot has the correct index.");
-  is(secondScreenshot.width, 128,
+  is(secondScreenshot.width, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
     "The second screenshot has the correct width.");
-  is(secondScreenshot.height, 128,
+  is(secondScreenshot.height, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
     "The second screenshot has the correct height.");
+  is(secondScreenshot.scaling, 0.25,
+    "The second screenshot has the correct scaling.");
   is(secondScreenshot.flipped, true,
     "The second screenshot has the correct 'flipped' flag.");
-  is(secondScreenshot.pixels.length, 128 * 128,
+  is(secondScreenshot.pixels.length, Math.pow(CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, 2),
     "The second screenshot should not be empty.");
   is(new Uint8Array(secondScreenshot.pixels.buffer)[0], 0,
     "The second screenshot has the correct red component.");
   is(new Uint8Array(secondScreenshot.pixels.buffer)[1], 0,
     "The second screenshot has the correct green component.");
   is(new Uint8Array(secondScreenshot.pixels.buffer)[2], 255,
     "The second screenshot has the correct blue component.");
   is(new Uint8Array(secondScreenshot.pixels.buffer)[3], 255,
@@ -63,12 +75,20 @@ function ifTestingSupported() {
 
   let gl = debuggee.gl;
   is(gl.getParameter(gl.FRAMEBUFFER_BINDING), debuggee.customFramebuffer,
     "The debuggee's gl context framebuffer still wasn't changed.");
   is(gl.getParameter(gl.RENDERBUFFER_BINDING), debuggee.customRenderbuffer,
     "The debuggee's gl context renderbuffer still wasn't changed.");
   is(gl.getParameter(gl.TEXTURE_BINDING_2D), debuggee.customTexture,
     "The debuggee's gl context texture binding still wasn't changed.");
+  is(gl.getParameter(gl.VIEWPORT)[0], 128,
+    "The debuggee's gl context viewport's left coord. still wasn't changed.");
+  is(gl.getParameter(gl.VIEWPORT)[1], 256,
+    "The debuggee's gl context viewport's left coord. still wasn't changed.");
+  is(gl.getParameter(gl.VIEWPORT)[2], 384,
+    "The debuggee's gl context viewport's left coord. still wasn't changed.");
+  is(gl.getParameter(gl.VIEWPORT)[3], 512,
+    "The debuggee's gl context viewport's left coord. still wasn't changed.");
 
   yield removeTab(target.tab);
   finish();
 }
--- a/browser/devtools/canvasdebugger/test/doc_webgl-bindings.html
+++ b/browser/devtools/canvasdebugger/test/doc_webgl-bindings.html
@@ -4,17 +4,17 @@
 
 <html>
   <head>
     <meta charset="utf-8"/>
     <title>WebGL editor test page</title>
   </head>
 
   <body>
-    <canvas id="canvas" width="128" height="128"></canvas>
+    <canvas id="canvas" width="1024" height="1024"></canvas>
 
     <script type="text/javascript;version=1.8">
       "use strict";
 
       let canvas, gl;
       let customFramebuffer;
       let customRenderbuffer;
       let customTexture;
@@ -25,29 +25,30 @@
         gl.clearColor(1.0, 0.0, 0.0, 1.0);
         gl.clear(gl.COLOR_BUFFER_BIT);
 
         customFramebuffer = gl.createFramebuffer();
         gl.bindFramebuffer(gl.FRAMEBUFFER, customFramebuffer);
 
         customRenderbuffer = gl.createRenderbuffer();
         gl.bindRenderbuffer(gl.RENDERBUFFER, customRenderbuffer);
-        gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 128, 128);
+        gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 1024, 1024);
 
         customTexture = gl.createTexture();
         gl.bindTexture(gl.TEXTURE_2D, customTexture);
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
-        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 128, 128, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1024, 1024, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
 
         gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, customTexture, 0);
         gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, customRenderbuffer);
 
+        gl.viewport(128, 256, 384, 512);
         gl.clearColor(0.0, 1.0, 0.0, 1.0);
         gl.clear(gl.COLOR_BUFFER_BIT);
 
         drawScene();
       }
 
       function drawScene() {
         gl.clearColor(0.0, 0.0, 1.0, 1.0);
--- a/toolkit/devtools/server/actors/canvas.js
+++ b/toolkit/devtools/server/actors/canvas.js
@@ -75,16 +75,17 @@ protocol.types.addType("uint32-array", {
 
 /**
  * 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: "uint32-array"
 });
 
 /**
  * Type describing an overview of a recorded animation frame.
  */
 protocol.types.addDictType("snapshot-overview", {
@@ -151,33 +152,35 @@ let FrameSnapshotActor = protocol.ActorC
     let replayData = ContextUtils.replayAnimationFrame({
       contextType: global,
       canvas: canvas,
       calls: calls,
       first: 0,
       last: index
     });
 
-    let { replayContext, lastDrawCallIndex, doCleanup } = replayData;
+    let { replayContext, replayContextScaling, lastDrawCallIndex, doCleanup } = replayData;
+    let [left, top, width, height] = replayData.replayViewport;
     let screenshot;
 
     // Depending on the canvas' context, generating a screenshot is done
     // in different ways.
     if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
-      screenshot = ContextUtils.getPixelsForWebGL(replayContext);
+      screenshot = ContextUtils.getPixelsForWebGL(replayContext, left, top, width, height);
       screenshot.flipped = true;
     } else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
-      screenshot = ContextUtils.getPixelsFor2D(replayContext);
+      screenshot = ContextUtils.getPixelsFor2D(replayContext, left, top, width, height);
       screenshot.flipped = false;
     }
 
     // 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") }
   })
 });
 
@@ -370,16 +373,17 @@ let CanvasActor = exports.CanvasActor = 
     let width = this._lastContentCanvasWidth;
     let height = this._lastContentCanvasHeight;
     let flipped = !!this._lastThumbnailFlipped; // undefined -> false
     let pixels = ContextUtils.getPixelStorage()["32bit"];
     let animationFrameEndScreenshot = {
       index: index,
       width: width,
       height: height,
+      scaling: 1,
       flipped: flipped,
       pixels: pixels.subarray(0, width * height)
     };
 
     // Wrap the function calls and screenshot in a FrameSnapshotActor instance,
     // which will resolve the promise returned by `recordAnimationFrame`.
     let frameSnapshot = new FrameSnapshotActor(this.conn, {
       canvas: this._lastDrawCallCanvas,
@@ -402,17 +406,17 @@ let CanvasActor = exports.CanvasActor = 
     let global = functionCall.meta.global;
 
     let contentCanvas = this._lastDrawCallCanvas = caller.canvas;
     let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall);
     let w = this._lastContentCanvasWidth = contentCanvas.width;
     let h = this._lastContentCanvasHeight = contentCanvas.height;
 
     // To keep things fast, generate images of small and fixed dimensions.
-    let dimensions = CanvasFront.THUMBNAIL_HEIGHT;
+    let dimensions = CanvasFront.THUMBNAIL_SIZE;
     let thumbnail;
 
     // Create a thumbnail on every draw call on the canvas context, to augment
     // the respective function call actor with this additional data.
     if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
       // Check if drawing to a custom framebuffer (when rendering to texture).
       // Don't create a thumbnail in this particular case.
       let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING);
@@ -523,29 +527,29 @@ let ContextUtils = {
    *        The source pixel data height.
    * @param number dstHeight [optional]
    *        The desired resized pixel data height.
    * @return object
    *         An objet containing the resized pixels width, height and data.
    */
   resizePixels: function(srcPixels, srcWidth, srcHeight, dstHeight) {
     let screenshotRatio = dstHeight / srcHeight;
-    let dstWidth = Math.floor(srcWidth * screenshotRatio);
+    let dstWidth = (srcWidth * screenshotRatio) | 0;
 
     // Use a plain array instead of a Uint32Array to make serializing faster.
     let dstPixels = new Array(dstWidth * dstHeight);
 
     // If the resized image ends up being completely transparent, returning
     // an empty array will skip some redundant serialization cycles.
     let isTransparent = true;
 
     for (let dstX = 0; dstX < dstWidth; dstX++) {
       for (let dstY = 0; dstY < dstHeight; dstY++) {
-        let srcX = Math.floor(dstX / screenshotRatio);
-        let srcY = Math.floor(dstY / screenshotRatio);
+        let srcX = (dstX / screenshotRatio) | 0;
+        let srcY = (dstY / screenshotRatio) | 0;
         let cPos = srcX + srcWidth * srcY;
         let dPos = dstX + dstWidth * dstY;
         let color = dstPixels[dPos] = srcPixels[cPos];
         if (color) {
           isTransparent = false;
         }
       }
     }
@@ -589,66 +593,97 @@ let ContextUtils = {
    *         last registered draw call's index and a cleanup function, which
    *         needs to be called whenever any potential followup work is finished.
    */
   replayAnimationFrame: function({ contextType, canvas, calls, first, last }) {
     let w = canvas.width;
     let h = canvas.height;
 
     let replayContext;
+    let replayContextScaling;
+    let customViewport;
     let customFramebuffer;
     let lastDrawCallIndex = -1;
     let doCleanup = () => {};
 
     // In case of WebGL contexts, rendering will be done offscreen, in a
     // custom framebuffer, but using the same provided context. This is
     // necessary because it's very memory-unfriendly to rebuild all the
     // required GL state (like recompiling shaders, setting global flags, etc.)
     // in an entirely new canvas. However, special care is needed to not
     // permanently affect the existing GL state in the process.
     if (contextType == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
+      // To keep things fast, replay the context calls on a framebuffer
+      // of smaller dimensions than the actual canvas (maximum 512x512 pixels).
+      let scaling = Math.min(CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, h) / h;
+      replayContextScaling = scaling;
+      w = (w * scaling) | 0;
+      h = (h * scaling) | 0;
+
+      // Fetch the same WebGL context and bind a new framebuffer.
       let gl = replayContext = this.getWebGLContext(canvas);
       let { newFramebuffer, oldFramebuffer } = this.createBoundFramebuffer(gl, w, h);
       customFramebuffer = newFramebuffer;
-      doCleanup = () => gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
+
+      // Set the viewport to match the new framebuffer's dimensions.
+      let { newViewport, oldViewport } = this.setCustomViewport(gl, w, h);
+      customViewport = newViewport;
+
+      // Revert the framebuffer and viewport to the original values.
+      doCleanup = () => {
+        gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
+        gl.viewport.apply(gl, oldViewport);
+      };
     }
     // In case of 2D contexts, draw everything on a separate canvas context.
     else if (contextType == CallWatcherFront.CANVAS_2D_CONTEXT) {
       let contentDocument = canvas.ownerDocument;
       let replayCanvas = contentDocument.createElement("canvas");
       replayCanvas.width = w;
       replayCanvas.height = h;
       replayContext = replayCanvas.getContext("2d");
-      replayContext.clearRect(0, 0, w, h);
+      replayContextScaling = 1;
+      customViewport = [0, 0, w, h];
     }
 
     // Replay all the context calls up to and including the specified one.
     for (let i = first; i <= last; i++) {
       let { type, name, args } = calls[i].details;
 
       // Prevent WebGL context calls that try to reset the framebuffer binding
       // to the default value, since we want to perform the rendering offscreen.
       if (name == "bindFramebuffer" && args[1] == null) {
         replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer);
         continue;
       }
+      // Also prevent WebGL context calls that try to change the viewport
+      // while our custom framebuffer is bound.
+      if (name == "viewport") {
+        let framebufferBinding = replayContext.getParameter(replayContext.FRAMEBUFFER_BINDING);
+        if (framebufferBinding == customFramebuffer) {
+          replayContext.viewport.apply(replayContext, customViewport);
+          continue;
+        }
+      }
       if (type == CallWatcherFront.METHOD_FUNCTION) {
         replayContext[name].apply(replayContext, args);
       } else if (type == CallWatcherFront.SETTER_FUNCTION) {
         replayContext[name] = args;
       } else {
         // Ignore getter calls.
       }
       if (CanvasFront.DRAW_CALLS.has(name)) {
         lastDrawCallIndex = i;
       }
     }
 
     return {
       replayContext: replayContext,
+      replayContextScaling: replayContextScaling,
+      replayViewport: customViewport,
       lastDrawCallIndex: lastDrawCallIndex,
       doCleanup: doCleanup
     };
   },
 
   /**
    * Gets an object containing a buffer large enough to hold width * height
    * pixels, assuming 32bit/pixel and 4 color components.
@@ -723,16 +758,30 @@ let ContextUtils = {
 
     gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0);
     gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
 
     gl.bindTexture(gl.TEXTURE_2D, oldTextureBinding);
     gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbufferBinding);
 
     return { oldFramebuffer, newFramebuffer };
+  },
+
+  /**
+   * Sets the viewport of the drawing buffer for a WebGL context.
+   * @param WebGLRenderingContext gl
+   * @param number width
+   * @param number height
+   */
+  setCustomViewport: function(gl, width, height) {
+    let oldViewport = XPCNativeWrapper.unwrap(gl.getParameter(gl.VIEWPORT));
+    let newViewport = [0, 0, width, height];
+    gl.viewport.apply(gl, newViewport);
+
+    return { oldViewport, newViewport };
   }
 };
 
 /**
  * The corresponding Front object for the CanvasActor.
  */
 let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, {
   initialize: function(client, { canvasActor }) {
@@ -743,18 +792,18 @@ let CanvasFront = exports.CanvasFront = 
 
 /**
  * Constants.
  */
 CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS);
 CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS);
 CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS);
 CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS);
-CanvasFront.THUMBNAIL_HEIGHT = 50; // px
-CanvasFront.SCREENSHOT_HEIGHT_MAX = 256; // px
+CanvasFront.THUMBNAIL_SIZE = 50; // px
+CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT = 256; // px
 CanvasFront.INVALID_SNAPSHOT_IMAGE = {
   index: -1,
   width: 0,
   height: 0,
   pixels: []
 };
 
 /**