browser/modules/BrowserNewTabPreloader.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 file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"];
     9 const Cu = Components.utils;
    10 const Cc = Components.classes;
    11 const Ci = Components.interfaces;
    13 Cu.import("resource://gre/modules/Services.jsm");
    14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    15 Cu.import("resource://gre/modules/Promise.jsm");
    17 const HTML_NS = "http://www.w3.org/1999/xhtml";
    18 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    19 const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
    20 const NEWTAB_URL = "about:newtab";
    21 const PREF_BRANCH = "browser.newtab.";
    23 // The interval between swapping in a preload docShell and kicking off the
    24 // next preload in the background.
    25 const PRELOADER_INTERVAL_MS = 600;
    26 // The initial delay before we start preloading our first new tab page. The
    27 // timer is started after the first 'browser-delayed-startup' has been sent.
    28 const PRELOADER_INIT_DELAY_MS = 5000;
    29 // The number of miliseconds we'll wait after we received a notification that
    30 // causes us to update our list of browsers and tabbrowser sizes. This acts as
    31 // kind of a damper when too many events are occuring in quick succession.
    32 const PRELOADER_UPDATE_DELAY_MS = 3000;
    34 const TOPIC_TIMER_CALLBACK = "timer-callback";
    35 const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished";
    36 const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed";
    38 const FRAME_SCRIPT_URL = "chrome://browser/content/newtab/preloaderContent.js";
    40 function createTimer(obj, delay) {
    41   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    42   timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT);
    43   return timer;
    44 }
    46 function clearTimer(timer) {
    47   if (timer) {
    48     timer.cancel();
    49   }
    50   return null;
    51 }
    53 this.BrowserNewTabPreloader = {
    54   init: function Preloader_init() {
    55     Initializer.start();
    56   },
    58   uninit: function Preloader_uninit() {
    59     Initializer.stop();
    60     HostFrame.destroy();
    61     Preferences.uninit();
    62     HiddenBrowsers.uninit();
    63   },
    65   newTab: function Preloader_newTab(aTab) {
    66     let swapped = false;
    67     let win = aTab.ownerDocument.defaultView;
    68     if (win.gBrowser) {
    69       let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
    70                      .getInterface(Ci.nsIDOMWindowUtils);
    72       let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
    73       let hiddenBrowser = HiddenBrowsers.get(width, height)
    74       if (hiddenBrowser) {
    75         swapped = hiddenBrowser.swapWithNewTab(aTab);
    76       }
    78       // aTab's browser is now visible and is therefore allowed to make
    79       // background captures.
    80       let msgMan = aTab.linkedBrowser.messageManager;
    81       msgMan.loadFrameScript(FRAME_SCRIPT_URL, false);
    82       msgMan.sendAsyncMessage("BrowserNewTabPreloader:allowBackgroundCaptures");
    83     }
    85     return swapped;
    86   }
    87 };
    89 Object.freeze(BrowserNewTabPreloader);
    91 let Initializer = {
    92   _timer: null,
    93   _observing: false,
    95   start: function Initializer_start() {
    96     Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
    97     this._observing = true;
    98   },
   100   stop: function Initializer_stop() {
   101     this._timer = clearTimer(this._timer);
   103     if (this._observing) {
   104       Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
   105       this._observing = false;
   106     }
   107   },
   109   observe: function Initializer_observe(aSubject, aTopic, aData) {
   110     if (aTopic == TOPIC_DELAYED_STARTUP) {
   111       Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
   112       this._observing = false;
   113       this._startTimer();
   114     } else if (aTopic == TOPIC_TIMER_CALLBACK) {
   115       this._timer = null;
   116       this._startPreloader();
   117     }
   118   },
   120   _startTimer: function Initializer_startTimer() {
   121     this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
   122   },
   124   _startPreloader: function Initializer_startPreloader() {
   125     Preferences.init();
   126     if (Preferences.enabled) {
   127       HiddenBrowsers.init();
   128     }
   129   }
   130 };
   132 let Preferences = {
   133   _enabled: null,
   134   _branch: null,
   136   get enabled() {
   137     if (this._enabled === null) {
   138       this._enabled = this._branch.getBoolPref("preload") &&
   139                       !this._branch.prefHasUserValue("url");
   140     }
   142     return this._enabled;
   143   },
   145   init: function Preferences_init() {
   146     this._branch = Services.prefs.getBranch(PREF_BRANCH);
   147     this._branch.addObserver("", this, false);
   148   },
   150   uninit: function Preferences_uninit() {
   151     if (this._branch) {
   152       this._branch.removeObserver("", this);
   153       this._branch = null;
   154     }
   155   },
   157   observe: function Preferences_observe() {
   158     let prevEnabled = this._enabled;
   159     this._enabled = null;
   161     if (prevEnabled && !this.enabled) {
   162       HiddenBrowsers.uninit();
   163     } else if (!prevEnabled && this.enabled) {
   164       HiddenBrowsers.init();
   165     }
   166   },
   167 };
   169 let HiddenBrowsers = {
   170   _browsers: null,
   171   _updateTimer: null,
   173   _topics: [
   174     TOPIC_DELAYED_STARTUP,
   175     TOPIC_XUL_WINDOW_CLOSED
   176   ],
   178   init: function () {
   179     this._browsers = new Map();
   180     this._updateBrowserSizes();
   181     this._topics.forEach(t => Services.obs.addObserver(this, t, false));
   182   },
   184   uninit: function () {
   185     if (this._browsers) {
   186       this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
   187       this._updateTimer = clearTimer(this._updateTimer);
   189       for (let [key, browser] of this._browsers) {
   190         browser.destroy();
   191       }
   192       this._browsers = null;
   193     }
   194   },
   196   get: function (width, height) {
   197     // We haven't been initialized, yet.
   198     if (!this._browsers) {
   199       return null;
   200     }
   202     let key = width + "x" + height;
   203     if (!this._browsers.has(key)) {
   204       // Update all browsers' sizes if we can't find a matching one.
   205       this._updateBrowserSizes();
   206     }
   208     // We should now have a matching browser.
   209     if (this._browsers.has(key)) {
   210       return this._browsers.get(key);
   211     }
   213     // We should never be here. Return the first browser we find.
   214     Cu.reportError("NewTabPreloader: no matching browser found after updating");
   215     for (let [size, browser] of this._browsers) {
   216       return browser;
   217     }
   219     // We should really never be here.
   220     Cu.reportError("NewTabPreloader: not even a single browser was found?");
   221     return null;
   222   },
   224   observe: function (subject, topic, data) {
   225     if (topic === TOPIC_TIMER_CALLBACK) {
   226       this._updateTimer = null;
   227       this._updateBrowserSizes();
   228     } else {
   229       this._updateTimer = clearTimer(this._updateTimer);
   230       this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
   231     }
   232   },
   234   _updateBrowserSizes: function () {
   235     let sizes = this._collectTabBrowserSizes();
   236     let toRemove = [];
   238     // Iterate all browsers and check that they
   239     // each can be assigned to one of the sizes.
   240     for (let [key, browser] of this._browsers) {
   241       if (sizes.has(key)) {
   242         // We already have a browser for that size, great!
   243         sizes.delete(key);
   244       } else {
   245         // This browser is superfluous or needs to be resized.
   246         toRemove.push(browser);
   247         this._browsers.delete(key);
   248       }
   249     }
   251     // Iterate all sizes that we couldn't find a browser for.
   252     for (let [key, {width, height}] of sizes) {
   253       let browser;
   254       if (toRemove.length) {
   255         // Let's just resize one of the superfluous
   256         // browsers and put it back into the map.
   257         browser = toRemove.shift();
   258         browser.resize(width, height);
   259       } else {
   260         // No more browsers to reuse, create a new one.
   261         browser = new HiddenBrowser(width, height);
   262       }
   264       this._browsers.set(key, browser);
   265     }
   267     // Finally, remove all browsers we don't need anymore.
   268     toRemove.forEach(b => b.destroy());
   269   },
   271   _collectTabBrowserSizes: function () {
   272     let sizes = new Map();
   274     function tabBrowserBounds() {
   275       let wins = Services.ww.getWindowEnumerator("navigator:browser");
   276       while (wins.hasMoreElements()) {
   277         let win = wins.getNext();
   278         if (win.gBrowser) {
   279           let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
   280                          .getInterface(Ci.nsIDOMWindowUtils);
   281           yield utils.getBoundsWithoutFlushing(win.gBrowser);
   282         }
   283       }
   284     }
   286     // Collect the sizes of all <tabbrowser>s out there.
   287     for (let {width, height} of tabBrowserBounds()) {
   288       if (width > 0 && height > 0) {
   289         let key = width + "x" + height;
   290         if (!sizes.has(key)) {
   291           sizes.set(key, {width: width, height: height});
   292         }
   293       }
   294     }
   296     return sizes;
   297   }
   298 };
   300 function HiddenBrowser(width, height) {
   301   this.resize(width, height);
   302   this._createBrowser();
   303 }
   305 HiddenBrowser.prototype = {
   306   _width: null,
   307   _height: null,
   308   _timer: null,
   310   get isPreloaded() {
   311     return this._browser &&
   312            this._browser.contentDocument &&
   313            this._browser.contentDocument.readyState === "complete" &&
   314            this._browser.currentURI.spec === NEWTAB_URL;
   315   },
   317   swapWithNewTab: function (aTab) {
   318     if (!this.isPreloaded || this._timer) {
   319       return false;
   320     }
   322     let win = aTab.ownerDocument.defaultView;
   323     let tabbrowser = win.gBrowser;
   325     if (!tabbrowser) {
   326       return false;
   327     }
   329     // Swap docShells.
   330     tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
   332     // Load all default frame scripts attached to the target window.
   333     let mm = aTab.linkedBrowser.messageManager;
   334     let scripts = win.messageManager.getDelayedFrameScripts();
   335     Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal));
   337     // Remove the browser, it will be recreated by a timer.
   338     this._removeBrowser();
   340     // Start a timer that will kick off preloading the next newtab page.
   341     this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
   343     // Signal that we swapped docShells.
   344     return true;
   345   },
   347   observe: function () {
   348     this._timer = null;
   350     // Start pre-loading the new tab page.
   351     this._createBrowser();
   352   },
   354   resize: function (width, height) {
   355     this._width = width;
   356     this._height = height;
   357     this._applySize();
   358   },
   360   destroy: function () {
   361     this._removeBrowser();
   362     this._timer = clearTimer(this._timer);
   363   },
   365   _applySize: function () {
   366     if (this._browser) {
   367       this._browser.style.width = this._width + "px";
   368       this._browser.style.height = this._height + "px";
   369     }
   370   },
   372   _createBrowser: function () {
   373     HostFrame.get().then(aFrame => {
   374       let doc = aFrame.document;
   375       this._browser = doc.createElementNS(XUL_NS, "browser");
   376       this._browser.setAttribute("type", "content");
   377       this._browser.setAttribute("src", NEWTAB_URL);
   378       this._applySize();
   379       doc.getElementById("win").appendChild(this._browser);
   380     });
   381   },
   383   _removeBrowser: function () {
   384     if (this._browser) {
   385       this._browser.remove();
   386       this._browser = null;
   387     }
   388   }
   389 };
   391 let HostFrame = {
   392   _frame: null,
   393   _deferred: null,
   395   get hiddenDOMDocument() {
   396     return Services.appShell.hiddenDOMWindow.document;
   397   },
   399   get isReady() {
   400     return this.hiddenDOMDocument.readyState === "complete";
   401   },
   403   get: function () {
   404     if (!this._deferred) {
   405       this._deferred = Promise.defer();
   406       this._create();
   407     }
   409     return this._deferred.promise;
   410   },
   412   destroy: function () {
   413     if (this._frame) {
   414       if (!Cu.isDeadWrapper(this._frame)) {
   415         this._frame.removeEventListener("load", this, true);
   416         this._frame.remove();
   417       }
   419       this._frame = null;
   420       this._deferred = null;
   421     }
   422   },
   424   handleEvent: function () {
   425     let contentWindow = this._frame.contentWindow;
   426     if (contentWindow.location.href === XUL_PAGE) {
   427       this._frame.removeEventListener("load", this, true);
   428       this._deferred.resolve(contentWindow);
   429     } else {
   430       contentWindow.location = XUL_PAGE;
   431     }
   432   },
   434   _create: function () {
   435     if (this.isReady) {
   436       let doc = this.hiddenDOMDocument;
   437       this._frame = doc.createElementNS(HTML_NS, "iframe");
   438       this._frame.addEventListener("load", this, true);
   439       doc.documentElement.appendChild(this._frame);
   440     } else {
   441       let flags = Ci.nsIThread.DISPATCH_NORMAL;
   442       Services.tm.currentThread.dispatch(() => this._create(), flags);
   443     }
   444   }
   445 };

mercurial