Bug 1041225 - Generating a screenshot is very slow when the content canvas has obnoxious dimensions, r=rcampbell

This commit is contained in:
Victor Porof 2014-07-22 12:43:24 -04:00
Родитель 80412ceb9d
Коммит a8901b62ce
4 изменённых файлов: 97 добавлений и 25 удалений

Просмотреть файл

@ -207,8 +207,8 @@ let SnapshotsListView = Heritage.extend(WidgetMethods, {
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";
@ -712,14 +712,16 @@ let CallsListView = Heritage.extend(WidgetMethods, {
* 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);
},
@ -754,8 +756,8 @@ let CallsListView = Heritage.extend(WidgetMethods, {
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";

Просмотреть файл

@ -24,9 +24,11 @@ function ifTestingSupported() {
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.");
@ -40,17 +42,27 @@ function ifTestingSupported() {
"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.");
@ -68,6 +80,14 @@ function ifTestingSupported() {
"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();

Просмотреть файл

@ -9,7 +9,7 @@
</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";
@ -30,7 +30,7 @@
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);
@ -38,11 +38,12 @@
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);

Просмотреть файл

@ -80,6 +80,7 @@ protocol.types.addDictType("snapshot-image", {
index: "number",
width: "number",
height: "number",
scaling: "number",
flipped: "boolean",
pixels: "uint32-array"
});
@ -156,16 +157,17 @@ let FrameSnapshotActor = protocol.ActorClass({
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;
}
@ -173,6 +175,7 @@ let FrameSnapshotActor = protocol.ActorClass({
// binding to the original value, after generating the screenshot.
doCleanup();
screenshot.scaling = replayContextScaling;
screenshot.index = lastDrawCallIndex;
return screenshot;
}, {
@ -375,6 +378,7 @@ let CanvasActor = exports.CanvasActor = protocol.ActorClass({
index: index,
width: width,
height: height,
scaling: 1,
flipped: flipped,
pixels: pixels.subarray(0, width * height)
};
@ -407,7 +411,7 @@ let CanvasActor = exports.CanvasActor = protocol.ActorClass({
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
@ -528,7 +532,7 @@ let ContextUtils = {
*/
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);
@ -539,8 +543,8 @@ let ContextUtils = {
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];
@ -594,6 +598,8 @@ let ContextUtils = {
let h = canvas.height;
let replayContext;
let replayContextScaling;
let customViewport;
let customFramebuffer;
let lastDrawCallIndex = -1;
let doCleanup = () => {};
@ -605,10 +611,27 @@ let ContextUtils = {
// 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) {
@ -617,7 +640,8 @@ let ContextUtils = {
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.
@ -630,6 +654,15 @@ let ContextUtils = {
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) {
@ -644,6 +677,8 @@ let ContextUtils = {
return {
replayContext: replayContext,
replayContextScaling: replayContextScaling,
replayViewport: customViewport,
lastDrawCallIndex: lastDrawCallIndex,
doCleanup: doCleanup
};
@ -728,6 +763,20 @@ let ContextUtils = {
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 };
}
};
@ -748,8 +797,8 @@ 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,