Bug 698371 - Add async thumbnail support for remote browsers. r=dao

This commit is contained in:
Jim Mathies 2014-10-18 09:09:32 -05:00
parent 57a7dc5a9d
commit dd646c1d81
3 changed files with 188 additions and 67 deletions

View File

@ -10,13 +10,14 @@ const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
const LATEST_STORAGE_VERSION = 3;
const EXPIRATION_MIN_CHUNK_SIZE = 50;
const EXPIRATION_INTERVAL_SECS = 3600;
var gRemoteThumbId = 0;
// If a request for a thumbnail comes in and we find one that is "stale"
// (or don't find one at all) we automatically queue a request to generate a
// new one.
@ -27,11 +28,6 @@ const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs.
*/
const THUMBNAIL_DIRECTORY = "thumbnails";
/**
* The default background color for page thumbnails.
*/
const THUMBNAIL_BG_COLOR = "#fff";
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
@ -69,6 +65,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageThumbUtils",
"resource://gre/modules/PageThumbUtils.jsm");
/**
* Utilities for dealing with promises and Task.jsm
@ -168,72 +166,76 @@ this.PageThumbs = {
},
/**
* Captures a thumbnail for the given window.
* @param aWindow The DOM window to capture a thumbnail from.
* @param aCallback The function to be called when the thumbnail has been
* captured. The first argument will be the data stream
* containing the image data.
*/
capture: function PageThumbs_capture(aWindow, aCallback) {
if (!this._prefEnabled()) {
return;
}
let canvas = this.createCanvas();
this.captureToCanvas(aWindow, canvas);
// Fetch the canvas data on the next event loop tick so that we allow
// some event processing in between drawing to the canvas and encoding
// its data. We want to block the UI as short as possible. See bug 744100.
Services.tm.currentThread.dispatch(function () {
canvas.mozFetchAsStream(aCallback, this.contentType);
}.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
},
/**
* Captures a thumbnail for the given window.
* Asynchronously returns a thumbnail as a blob for the given
* window.
*
* @param aWindow The DOM window to capture a thumbnail from.
* @param aBrowser The <browser> to capture a thumbnail from.
* @return {Promise}
* @resolve {Blob} The thumbnail, as a Blob.
*/
captureToBlob: function PageThumbs_captureToBlob(aWindow) {
captureToBlob: function PageThumbs_captureToBlob(aBrowser) {
if (!this._prefEnabled()) {
return null;
}
let canvas = this.createCanvas();
this.captureToCanvas(aWindow, canvas);
let deferred = Promise.defer();
let type = this.contentType;
// Fetch the canvas data on the next event loop tick so that we allow
// some event processing in between drawing to the canvas and encoding
// its data. We want to block the UI as short as possible. See bug 744100.
canvas.toBlob(function asBlob(blob) {
deferred.resolve(blob, type);
let canvas = this.createCanvas();
this.captureToCanvas(aBrowser, canvas, () => {
canvas.toBlob(blob => {
deferred.resolve(blob, this.contentType);
});
});
return deferred.promise;
},
/**
* Captures a thumbnail from a given window and draws it to the given canvas.
* @param aWindow The DOM window to capture a thumbnail from.
* Note, when dealing with remote content, this api draws into the passed
* canvas asynchronously. Pass aCallback to receive an async callback after
* canvas painting has completed.
* @param aBrowser The browser to capture a thumbnail from.
* @param aCanvas The canvas to draw to.
* @param aCallback (optional) A callback invoked once the thumbnail has been
* rendered to aCanvas.
*/
captureToCanvas: function PageThumbs_captureToCanvas(aWindow, aCanvas) {
captureToCanvas: function PageThumbs_captureToCanvas(aBrowser, aCanvas, aCallback) {
let telemetryCaptureTime = new Date();
this._captureToCanvas(aWindow, aCanvas);
let telemetry = Services.telemetry;
telemetry.getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS")
.add(new Date() - telemetryCaptureTime);
this._captureToCanvas(aBrowser, aCanvas, function () {
Services.telemetry
.getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS")
.add(new Date() - telemetryCaptureTime);
if (aCallback) {
aCallback(aCanvas);
}
});
},
// The background thumbnail service captures to canvas but doesn't want to
// participate in this service's telemetry, which is why this method exists.
_captureToCanvas: function PageThumbs__captureToCanvas(aWindow, aCanvas) {
let [sw, sh, scale] = this._determineCropSize(aWindow, aCanvas);
_captureToCanvas: function (aBrowser, aCanvas, aCallback) {
if (aBrowser.isRemoteBrowser) {
let [sw, sh, scale] =
PageThumbUtils.determineCropSize(aBrowser.contentWindowAsCPOW, aCanvas);
Task.spawn(function () {
let data =
yield this._captureRemoteThumbnail(aBrowser, sw, sh, scale,
PageThumbUtils.THUMBNAIL_BG_COLOR);
let canvas = data.thumbnail;
let ctx = canvas.getContext("2d");
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
aCanvas.getContext("2d").putImageData(imgData, 0, 0);
if (aCallback) {
aCallback(aCanvas);
}
}.bind(this));
return;
}
// Generate in-process content thumbnail
let [sw, sh, scale] =
PageThumbUtils.determineCropSize(aBrowser.contentWindow, aCanvas);
let ctx = aCanvas.getContext("2d");
// Scale the canvas accordingly.
@ -242,15 +244,88 @@ this.PageThumbs = {
try {
// Draw the window contents to the canvas.
ctx.drawWindow(aWindow, 0, 0, sw, sh, THUMBNAIL_BG_COLOR,
ctx.drawWindow(aBrowser.contentWindow, 0, 0, sw, sh,
PageThumbUtils.THUMBNAIL_BG_COLOR,
ctx.DRAWWINDOW_DO_NOT_FLUSH);
} catch (e) {
// We couldn't draw to the canvas for some reason.
}
ctx.restore();
if (aCallback) {
aCallback(aCanvas);
}
},
/**
* Request a thumbnail using requested bounds and scale factor.
* @param aWidth - (optional) a width value less than or equal to the
* innerWidth of the dom window. Defaults to the visible frame.
* @param aHeight - (optional) a height value less than or equal to the
* innerHeight of the dom window. Defaults to the visible frame.
* @param aScaleFactor - (optional) 0.0 - 1.0 scale factor applied to the
* returned thumbnail. Defaults to 1.0.
* @param aCssBackground - (optional) a css '#fff' color value to use as
* the background color of the thumbnail.
*/
_captureRemoteThumbnail: function (aBrowser, aWidth, aHeight,
aScaleFactor, aCssBackground) {
let deferred = Promise.defer();
// The index we send with the request so we can identify the
// correct response.
let index = gRemoteThumbId++;
// Thumbnail request response handler
let mm = aBrowser.messageManager;
// Browser:Thumbnail:Response handler
let thumbFunc = function (aMsg) {
// Ignore events unrelated to our request
if (aMsg.data.id != index) {
return;
}
mm.removeMessageListener("Browser:Thumbnail:Response", thumbFunc);
let imageBlob = aMsg.data.thumbnail;
let doc = aBrowser.parentElement.ownerDocument;
let reader = Cc["@mozilla.org/files/filereader;1"].
createInstance(Ci.nsIDOMFileReader);
reader.addEventListener("loadend", function() {
let image = doc.createElementNS(PageThumbUtils.HTML_NAMESPACE, "img");
image.onload = function () {
let thumbnail = doc.createElementNS(PageThumbUtils.HTML_NAMESPACE, "canvas");
thumbnail.width = image.naturalWidth;
thumbnail.height = image.naturalHeight;
let ctx = thumbnail.getContext("2d");
ctx.drawImage(image, 0, 0);
deferred.resolve({
thumbnail: thumbnail
});
}
image.src = reader.result;
});
// xxx wish there was a way to skip this encoding step
reader.readAsDataURL(imageBlob);
}
mm.addMessageListener("Browser:Thumbnail:Response", thumbFunc);
// Send a thumbnail request
let width = aWidth || 0;
let height = aHeight || 0;
let scale = aScaleFactor || 1.0;
let background = aCssBackground || "#fff";
mm.sendAsyncMessage("Browser:Thumbnail:Request", {
width: width,
height: height,
scale: scale,
background: background,
id: index
});
return deferred.promise;
},
/**
* Captures a thumbnail for the given browser and stores it to the cache.
* @param aBrowser The browser to capture a thumbnail for.
@ -262,19 +337,27 @@ this.PageThumbs = {
}
let url = aBrowser.currentURI.spec;
let channel = aBrowser.docShell.currentDocumentChannel;
let originalURL = channel.originalURI.spec;
let originalURL;
let channelError = false;
// see if this was an error response.
let wasError = this._isChannelErrorResponse(channel);
if (!aBrowser.isRemoteBrowser) {
let channel = aBrowser.docShell.currentDocumentChannel;
originalURL = channel.originalURI.spec;
// see if this was an error response.
channelError = this._isChannelErrorResponse(channel);
} else {
// We need channel info (bug 1073957)
originalURL = url;
}
Task.spawn((function task() {
let isSuccess = true;
try {
let blob = yield this.captureToBlob(aBrowser.contentWindow);
let blob = yield this.captureToBlob(aBrowser);
let buffer = yield TaskUtils.readBlob(blob);
yield this._store(originalURL, url, buffer, wasError);
} catch (_) {
yield this._store(originalURL, url, buffer, channelError);
} catch (ex) {
Components.utils.reportError("Exception thrown during thumbnail capture: '" + ex + "'");
isSuccess = false;
}
if (aCallback) {

View File

@ -2,13 +2,11 @@
* 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/. */
(function () { // bug 673569 workaround :(
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.importGlobalProperties(['Blob']);
Cu.import("resource://gre/modules/PageThumbs.jsm");
Cu.import("resource://gre/modules/PageThumbUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
@ -49,7 +47,7 @@ const backgroundPageThumbsContent = {
// in the parent (eg, auth) aren't prevented, but alert() etc are.
// disableDialogs only works on the current inner window, so it has
// to be called every page load, but before scripts run.
if (subj == content.document) {
if (content && subj == content.document) {
content.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils).
@ -129,9 +127,19 @@ const backgroundPageThumbsContent = {
capture.finalURL = this._webNav.currentURI.spec;
capture.pageLoadTime = new Date() - capture.pageLoadStartDate;
let canvas = PageThumbs.createCanvas(content);
let canvasDrawDate = new Date();
PageThumbs._captureToCanvas(content, canvas);
let canvas = PageThumbUtils.createCanvas(content);
let [sw, sh, scale] = PageThumbUtils.determineCropSize(content, canvas);
let ctx = canvas.getContext("2d");
ctx.save();
ctx.scale(scale, scale);
ctx.drawWindow(content, 0, 0, sw, sh,
PageThumbUtils.THUMBNAIL_BG_COLOR,
ctx.DRAWWINDOW_DO_NOT_FLUSH);
ctx.restore();
capture.canvasDrawTime = new Date() - canvasDrawDate;
canvas.toBlob(blob => {
@ -184,5 +192,3 @@ const backgroundPageThumbsContent = {
};
backgroundPageThumbsContent.init();
})();

View File

@ -11,6 +11,8 @@ Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import("resource://gre/modules/RemoteAddonsChild.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
#ifdef MOZ_CRASHREPORTER
XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
"@mozilla.org/xre/app-info;1",
@ -368,6 +370,36 @@ addMessageListener("UpdateCharacterSet", function (aMessage) {
docShell.gatherCharsetMenuTelemetry();
});
/**
* Remote thumbnail request handler for PageThumbs thumbnails.
*/
addMessageListener("Browser:Thumbnail:Request", function (aMessage) {
let thumbnail = content.document.createElementNS(HTML_NAMESPACE, "canvas");
thumbnail.mozOpaque = true;
thumbnail.mozImageSmoothingEnabled = true;
// width and height are crop dims
let width = aMessage.data.width || content.innerWidth;
let height = aMessage.data.height || content.innerHeight;
thumbnail.width = Math.round(width * aMessage.data.scale);
thumbnail.height = Math.round(height * aMessage.data.scale);
let ctx = thumbnail.getContext("2d");
ctx.save();
ctx.scale(aMessage.data.scale, aMessage.data.scale);
ctx.drawWindow(content, 0, 0, width, height,
aMessage.data.background,
ctx.DRAWWINDOW_DO_NOT_FLUSH);
ctx.restore();
thumbnail.toBlob(function (aBlob) {
sendAsyncMessage("Browser:Thumbnail:Response", {
thumbnail: aBlob,
id: aMessage.data.id
});
});
});
// The AddonsChild needs to be rooted so that it stays alive as long as
// the tab.
let AddonsChild;