toolkit/components/thumbnails/BackgroundPageThumbs.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 const EXPORTED_SYMBOLS = [
     6   "BackgroundPageThumbs",
     7 ];
     9 const DEFAULT_CAPTURE_TIMEOUT = 30000; // ms
    10 const DESTROY_BROWSER_TIMEOUT = 60000; // ms
    11 const FRAME_SCRIPT_URL = "chrome://global/content/backgroundPageThumbsContent.js";
    13 const TELEMETRY_HISTOGRAM_ID_PREFIX = "FX_THUMBNAILS_BG_";
    15 // possible FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2 telemetry values
    16 const TEL_CAPTURE_DONE_OK = 0;
    17 const TEL_CAPTURE_DONE_TIMEOUT = 1;
    18 // 2 and 3 were used when we had special handling for private-browsing.
    19 const TEL_CAPTURE_DONE_CRASHED = 4;
    21 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    22 const HTML_NS = "http://www.w3.org/1999/xhtml";
    24 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    26 Cu.import("resource://gre/modules/PageThumbs.jsm");
    27 Cu.import("resource://gre/modules/Services.jsm");
    29 const BackgroundPageThumbs = {
    31   /**
    32    * Asynchronously captures a thumbnail of the given URL.
    33    *
    34    * The page is loaded anonymously, and plug-ins are disabled.
    35    *
    36    * @param url      The URL to capture.
    37    * @param options  An optional object that configures the capture.  Its
    38    *                 properties are the following, and all are optional:
    39    * @opt onDone     A function that will be asynchronously called when the
    40    *                 capture is complete or times out.  It's called as
    41    *                   onDone(url),
    42    *                 where `url` is the captured URL.
    43    * @opt timeout    The capture will time out after this many milliseconds have
    44    *                 elapsed after the capture has progressed to the head of
    45    *                 the queue and started.  Defaults to 30000 (30 seconds).
    46    */
    47   capture: function (url, options={}) {
    48     if (!PageThumbs._prefEnabled()) {
    49       if (options.onDone)
    50         schedule(() => options.onDone(url));
    51       return;
    52     }
    53     this._captureQueue = this._captureQueue || [];
    54     this._capturesByURL = this._capturesByURL || new Map();
    56     tel("QUEUE_SIZE_ON_CAPTURE", this._captureQueue.length);
    58     // We want to avoid duplicate captures for the same URL.  If there is an
    59     // existing one, we just add the callback to that one and we are done.
    60     let existing = this._capturesByURL.get(url);
    61     if (existing) {
    62       if (options.onDone)
    63         existing.doneCallbacks.push(options.onDone);
    64       // The queue is already being processed, so nothing else to do...
    65       return;
    66     }
    67     let cap = new Capture(url, this._onCaptureOrTimeout.bind(this), options);
    68     this._captureQueue.push(cap);
    69     this._capturesByURL.set(url, cap);
    70     this._processCaptureQueue();
    71   },
    73   /**
    74    * Asynchronously captures a thumbnail of the given URL if one does not
    75    * already exist.  Otherwise does nothing.
    76    *
    77    * @param url      The URL to capture.
    78    * @param options  An optional object that configures the capture.  See
    79    *                 capture() for description.
    80    */
    81   captureIfMissing: function (url, options={}) {
    82     // The fileExistsForURL call is an optimization, potentially but unlikely
    83     // incorrect, and no big deal when it is.  After the capture is done, we
    84     // atomically test whether the file exists before writing it.
    85     PageThumbsStorage.fileExistsForURL(url).then(exists => {
    86       if (exists.ok) {
    87         if (options.onDone)
    88           options.onDone(url);
    89         return;
    90       }
    91       this.capture(url, options);
    92     }, err => {
    93       if (options.onDone)
    94         options.onDone(url);
    95     });
    96   },
    98   /**
    99    * Ensures that initialization of the thumbnail browser's parent window has
   100    * begun.
   101    *
   102    * @return  True if the parent window is completely initialized and can be
   103    *          used, and false if initialization has started but not completed.
   104    */
   105   _ensureParentWindowReady: function () {
   106     if (this._parentWin)
   107       // Already fully initialized.
   108       return true;
   109     if (this._startedParentWinInit)
   110       // Already started initializing.
   111       return false;
   113     this._startedParentWinInit = true;
   115     // Create an html:iframe, stick it in the parent document, and
   116     // use it to host the browser.  about:blank will not have the system
   117     // principal, so it can't host, but a document with a chrome URI will.
   118     let hostWindow = Services.appShell.hiddenDOMWindow;
   119     let iframe = hostWindow.document.createElementNS(HTML_NS, "iframe");
   120     iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
   121     let onLoad = function onLoadFn() {
   122       iframe.removeEventListener("load", onLoad, true);
   123       this._parentWin = iframe.contentWindow;
   124       this._processCaptureQueue();
   125     }.bind(this);
   126     iframe.addEventListener("load", onLoad, true);
   127     hostWindow.document.documentElement.appendChild(iframe);
   128     this._hostIframe = iframe;
   130     return false;
   131   },
   133   /**
   134    * Destroys the service.  Queued and pending captures will never complete, and
   135    * their consumer callbacks will never be called.
   136    */
   137   _destroy: function () {
   138     if (this._captureQueue)
   139       this._captureQueue.forEach(cap => cap.destroy());
   140     this._destroyBrowser();
   141     if (this._hostIframe)
   142       this._hostIframe.remove();
   143     delete this._captureQueue;
   144     delete this._hostIframe;
   145     delete this._startedParentWinInit;
   146     delete this._parentWin;
   147   },
   149   /**
   150    * Creates the thumbnail browser if it doesn't already exist.
   151    */
   152   _ensureBrowser: function () {
   153     if (this._thumbBrowser)
   154       return;
   156     let browser = this._parentWin.document.createElementNS(XUL_NS, "browser");
   157     browser.setAttribute("type", "content");
   158     browser.setAttribute("remote", "true");
   160     // Size the browser.  Make its aspect ratio the same as the canvases' that
   161     // the thumbnails are drawn into; the canvases' aspect ratio is the same as
   162     // the screen's, so use that.  Aim for a size in the ballpark of 1024x768.
   163     let [swidth, sheight] = [{}, {}];
   164     Cc["@mozilla.org/gfx/screenmanager;1"].
   165       getService(Ci.nsIScreenManager).
   166       primaryScreen.
   167       GetRectDisplayPix({}, {}, swidth, sheight);
   168     let bwidth = Math.min(1024, swidth.value);
   169     // Setting the width and height attributes doesn't work -- the resulting
   170     // thumbnails are blank and transparent -- but setting the style does.
   171     browser.style.width = bwidth + "px";
   172     browser.style.height = (bwidth * sheight.value / swidth.value) + "px";
   174     this._parentWin.document.documentElement.appendChild(browser);
   176     // an event that is sent if the remote process crashes - no need to remove
   177     // it as we want it to be there as long as the browser itself lives.
   178     browser.addEventListener("oop-browser-crashed", () => {
   179       Cu.reportError("BackgroundThumbnails remote process crashed - recovering");
   180       this._destroyBrowser();
   181       let curCapture = this._captureQueue.length ? this._captureQueue[0] : null;
   182       // we could retry the pending capture, but it's possible the crash
   183       // was due directly to it, so trying again might just crash again.
   184       // We could keep a flag to indicate if it previously crashed, but
   185       // "resetting" the capture requires more work - so for now, we just
   186       // discard it.
   187       if (curCapture && curCapture.pending) {
   188         curCapture._done(null, TEL_CAPTURE_DONE_CRASHED);
   189         // _done automatically continues queue processing.
   190       }
   191       // else: we must have been idle and not currently doing a capture (eg,
   192       // maybe a GC or similar crashed) - so there's no need to attempt a
   193       // queue restart - the next capture request will set everything up.
   194     });
   196     browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
   197     this._thumbBrowser = browser;
   198   },
   200   _destroyBrowser: function () {
   201     if (!this._thumbBrowser)
   202       return;
   203     this._thumbBrowser.remove();
   204     delete this._thumbBrowser;
   205   },
   207   /**
   208    * Starts the next capture if the queue is not empty and the service is fully
   209    * initialized.
   210    */
   211   _processCaptureQueue: function () {
   212     if (!this._captureQueue.length ||
   213         this._captureQueue[0].pending ||
   214         !this._ensureParentWindowReady())
   215       return;
   217     // Ready to start the first capture in the queue.
   218     this._ensureBrowser();
   219     this._captureQueue[0].start(this._thumbBrowser.messageManager);
   220     if (this._destroyBrowserTimer) {
   221       this._destroyBrowserTimer.cancel();
   222       delete this._destroyBrowserTimer;
   223     }
   224   },
   226   /**
   227    * Called when the current capture completes or fails (eg, times out, remote
   228    * process crashes.)
   229    */
   230   _onCaptureOrTimeout: function (capture) {
   231     // Since timeouts start as an item is being processed, only the first
   232     // item in the queue can be passed to this method.
   233     if (capture !== this._captureQueue[0])
   234       throw new Error("The capture should be at the head of the queue.");
   235     this._captureQueue.shift();
   236     this._capturesByURL.delete(capture.url);
   238     // Start the destroy-browser timer *before* processing the capture queue.
   239     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   240     timer.initWithCallback(this._destroyBrowser.bind(this),
   241                            this._destroyBrowserTimeout,
   242                            Ci.nsITimer.TYPE_ONE_SHOT);
   243     this._destroyBrowserTimer = timer;
   245     this._processCaptureQueue();
   246   },
   248   _destroyBrowserTimeout: DESTROY_BROWSER_TIMEOUT,
   249 };
   251 /**
   252  * Represents a single capture request in the capture queue.
   253  *
   254  * @param url              The URL to capture.
   255  * @param captureCallback  A function you want called when the capture
   256  *                         completes.
   257  * @param options          The capture options.
   258  */
   259 function Capture(url, captureCallback, options) {
   260   this.url = url;
   261   this.captureCallback = captureCallback;
   262   this.options = options;
   263   this.id = Capture.nextID++;
   264   this.creationDate = new Date();
   265   this.doneCallbacks = [];
   266   if (options.onDone)
   267     this.doneCallbacks.push(options.onDone);
   268 }
   270 Capture.prototype = {
   272   get pending() {
   273     return !!this._msgMan;
   274   },
   276   /**
   277    * Sends a message to the content script to start the capture.
   278    *
   279    * @param messageManager  The nsIMessageSender of the thumbnail browser.
   280    */
   281   start: function (messageManager) {
   282     this.startDate = new Date();
   283     tel("CAPTURE_QUEUE_TIME_MS", this.startDate - this.creationDate);
   285     // timeout timer
   286     let timeout = typeof(this.options.timeout) == "number" ?
   287                   this.options.timeout :
   288                   DEFAULT_CAPTURE_TIMEOUT;
   289     this._timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   290     this._timeoutTimer.initWithCallback(this, timeout,
   291                                         Ci.nsITimer.TYPE_ONE_SHOT);
   293     // didCapture registration
   294     this._msgMan = messageManager;
   295     this._msgMan.sendAsyncMessage("BackgroundPageThumbs:capture",
   296                                   { id: this.id, url: this.url });
   297     this._msgMan.addMessageListener("BackgroundPageThumbs:didCapture", this);
   298   },
   300   /**
   301    * The only intended external use of this method is by the service when it's
   302    * uninitializing and doing things like destroying the thumbnail browser.  In
   303    * that case the consumer's completion callback will never be called.
   304    */
   305   destroy: function () {
   306     // This method may be called for captures that haven't started yet, so
   307     // guard against not yet having _timeoutTimer, _msgMan etc properties...
   308     if (this._timeoutTimer) {
   309       this._timeoutTimer.cancel();
   310       delete this._timeoutTimer;
   311     }
   312     if (this._msgMan) {
   313       this._msgMan.removeMessageListener("BackgroundPageThumbs:didCapture",
   314                                          this);
   315       delete this._msgMan;
   316     }
   317     delete this.captureCallback;
   318     delete this.doneCallbacks;
   319     delete this.options;
   320   },
   322   // Called when the didCapture message is received.
   323   receiveMessage: function (msg) {
   324     tel("CAPTURE_SERVICE_TIME_MS", new Date() - this.startDate);
   326     // A different timed-out capture may have finally successfully completed, so
   327     // discard messages that aren't meant for this capture.
   328     if (msg.json.id == this.id)
   329       this._done(msg.json, TEL_CAPTURE_DONE_OK);
   330   },
   332   // Called when the timeout timer fires.
   333   notify: function () {
   334     this._done(null, TEL_CAPTURE_DONE_TIMEOUT);
   335   },
   337   _done: function (data, reason) {
   338     // Note that _done will be called only once, by either receiveMessage or
   339     // notify, since it calls destroy here, which cancels the timeout timer and
   340     // removes the didCapture message listener.
   341     let { captureCallback, doneCallbacks, options } = this;
   342     this.destroy();
   344     if (typeof(reason) != "number")
   345       throw new Error("A done reason must be given.");
   346     tel("CAPTURE_DONE_REASON_2", reason);
   347     if (data && data.telemetry) {
   348       // Telemetry is currently disabled in the content process (bug 680508).
   349       for (let id in data.telemetry) {
   350         tel(id, data.telemetry[id]);
   351       }
   352     }
   354     let done = () => {
   355       captureCallback(this);
   356       for (let callback of doneCallbacks) {
   357         try {
   358           callback.call(options, this.url);
   359         }
   360         catch (err) {
   361           Cu.reportError(err);
   362         }
   363       }
   364     };
   366     if (!data) {
   367       done();
   368       return;
   369     }
   371     PageThumbs._store(this.url, data.finalURL, data.imageData, true)
   372               .then(done, done);
   373   },
   374 };
   376 Capture.nextID = 0;
   378 /**
   379  * Adds a value to one of this module's telemetry histograms.
   380  *
   381  * @param histogramID  This is prefixed with this module's ID.
   382  * @param value        The value to add.
   383  */
   384 function tel(histogramID, value) {
   385   let id = TELEMETRY_HISTOGRAM_ID_PREFIX + histogramID;
   386   Services.telemetry.getHistogramById(id).add(value);
   387 }
   389 function schedule(callback) {
   390   Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
   391 }

mercurial