michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: "use strict";
michael@0:
michael@0: this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"];
michael@0:
michael@0: const Cu = Components.utils;
michael@0: const Cc = Components.classes;
michael@0: const Ci = Components.interfaces;
michael@0:
michael@0: Cu.import("resource://gre/modules/Services.jsm");
michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0: Cu.import("resource://gre/modules/Promise.jsm");
michael@0:
michael@0: const HTML_NS = "http://www.w3.org/1999/xhtml";
michael@0: const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
michael@0: const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,";
michael@0: const NEWTAB_URL = "about:newtab";
michael@0: const PREF_BRANCH = "browser.newtab.";
michael@0:
michael@0: // The interval between swapping in a preload docShell and kicking off the
michael@0: // next preload in the background.
michael@0: const PRELOADER_INTERVAL_MS = 600;
michael@0: // The initial delay before we start preloading our first new tab page. The
michael@0: // timer is started after the first 'browser-delayed-startup' has been sent.
michael@0: const PRELOADER_INIT_DELAY_MS = 5000;
michael@0: // The number of miliseconds we'll wait after we received a notification that
michael@0: // causes us to update our list of browsers and tabbrowser sizes. This acts as
michael@0: // kind of a damper when too many events are occuring in quick succession.
michael@0: const PRELOADER_UPDATE_DELAY_MS = 3000;
michael@0:
michael@0: const TOPIC_TIMER_CALLBACK = "timer-callback";
michael@0: const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished";
michael@0: const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed";
michael@0:
michael@0: const FRAME_SCRIPT_URL = "chrome://browser/content/newtab/preloaderContent.js";
michael@0:
michael@0: function createTimer(obj, delay) {
michael@0: let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0: timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT);
michael@0: return timer;
michael@0: }
michael@0:
michael@0: function clearTimer(timer) {
michael@0: if (timer) {
michael@0: timer.cancel();
michael@0: }
michael@0: return null;
michael@0: }
michael@0:
michael@0: this.BrowserNewTabPreloader = {
michael@0: init: function Preloader_init() {
michael@0: Initializer.start();
michael@0: },
michael@0:
michael@0: uninit: function Preloader_uninit() {
michael@0: Initializer.stop();
michael@0: HostFrame.destroy();
michael@0: Preferences.uninit();
michael@0: HiddenBrowsers.uninit();
michael@0: },
michael@0:
michael@0: newTab: function Preloader_newTab(aTab) {
michael@0: let swapped = false;
michael@0: let win = aTab.ownerDocument.defaultView;
michael@0: if (win.gBrowser) {
michael@0: let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0: .getInterface(Ci.nsIDOMWindowUtils);
michael@0:
michael@0: let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
michael@0: let hiddenBrowser = HiddenBrowsers.get(width, height)
michael@0: if (hiddenBrowser) {
michael@0: swapped = hiddenBrowser.swapWithNewTab(aTab);
michael@0: }
michael@0:
michael@0: // aTab's browser is now visible and is therefore allowed to make
michael@0: // background captures.
michael@0: let msgMan = aTab.linkedBrowser.messageManager;
michael@0: msgMan.loadFrameScript(FRAME_SCRIPT_URL, false);
michael@0: msgMan.sendAsyncMessage("BrowserNewTabPreloader:allowBackgroundCaptures");
michael@0: }
michael@0:
michael@0: return swapped;
michael@0: }
michael@0: };
michael@0:
michael@0: Object.freeze(BrowserNewTabPreloader);
michael@0:
michael@0: let Initializer = {
michael@0: _timer: null,
michael@0: _observing: false,
michael@0:
michael@0: start: function Initializer_start() {
michael@0: Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
michael@0: this._observing = true;
michael@0: },
michael@0:
michael@0: stop: function Initializer_stop() {
michael@0: this._timer = clearTimer(this._timer);
michael@0:
michael@0: if (this._observing) {
michael@0: Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
michael@0: this._observing = false;
michael@0: }
michael@0: },
michael@0:
michael@0: observe: function Initializer_observe(aSubject, aTopic, aData) {
michael@0: if (aTopic == TOPIC_DELAYED_STARTUP) {
michael@0: Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
michael@0: this._observing = false;
michael@0: this._startTimer();
michael@0: } else if (aTopic == TOPIC_TIMER_CALLBACK) {
michael@0: this._timer = null;
michael@0: this._startPreloader();
michael@0: }
michael@0: },
michael@0:
michael@0: _startTimer: function Initializer_startTimer() {
michael@0: this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
michael@0: },
michael@0:
michael@0: _startPreloader: function Initializer_startPreloader() {
michael@0: Preferences.init();
michael@0: if (Preferences.enabled) {
michael@0: HiddenBrowsers.init();
michael@0: }
michael@0: }
michael@0: };
michael@0:
michael@0: let Preferences = {
michael@0: _enabled: null,
michael@0: _branch: null,
michael@0:
michael@0: get enabled() {
michael@0: if (this._enabled === null) {
michael@0: this._enabled = this._branch.getBoolPref("preload") &&
michael@0: !this._branch.prefHasUserValue("url");
michael@0: }
michael@0:
michael@0: return this._enabled;
michael@0: },
michael@0:
michael@0: init: function Preferences_init() {
michael@0: this._branch = Services.prefs.getBranch(PREF_BRANCH);
michael@0: this._branch.addObserver("", this, false);
michael@0: },
michael@0:
michael@0: uninit: function Preferences_uninit() {
michael@0: if (this._branch) {
michael@0: this._branch.removeObserver("", this);
michael@0: this._branch = null;
michael@0: }
michael@0: },
michael@0:
michael@0: observe: function Preferences_observe() {
michael@0: let prevEnabled = this._enabled;
michael@0: this._enabled = null;
michael@0:
michael@0: if (prevEnabled && !this.enabled) {
michael@0: HiddenBrowsers.uninit();
michael@0: } else if (!prevEnabled && this.enabled) {
michael@0: HiddenBrowsers.init();
michael@0: }
michael@0: },
michael@0: };
michael@0:
michael@0: let HiddenBrowsers = {
michael@0: _browsers: null,
michael@0: _updateTimer: null,
michael@0:
michael@0: _topics: [
michael@0: TOPIC_DELAYED_STARTUP,
michael@0: TOPIC_XUL_WINDOW_CLOSED
michael@0: ],
michael@0:
michael@0: init: function () {
michael@0: this._browsers = new Map();
michael@0: this._updateBrowserSizes();
michael@0: this._topics.forEach(t => Services.obs.addObserver(this, t, false));
michael@0: },
michael@0:
michael@0: uninit: function () {
michael@0: if (this._browsers) {
michael@0: this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
michael@0: this._updateTimer = clearTimer(this._updateTimer);
michael@0:
michael@0: for (let [key, browser] of this._browsers) {
michael@0: browser.destroy();
michael@0: }
michael@0: this._browsers = null;
michael@0: }
michael@0: },
michael@0:
michael@0: get: function (width, height) {
michael@0: // We haven't been initialized, yet.
michael@0: if (!this._browsers) {
michael@0: return null;
michael@0: }
michael@0:
michael@0: let key = width + "x" + height;
michael@0: if (!this._browsers.has(key)) {
michael@0: // Update all browsers' sizes if we can't find a matching one.
michael@0: this._updateBrowserSizes();
michael@0: }
michael@0:
michael@0: // We should now have a matching browser.
michael@0: if (this._browsers.has(key)) {
michael@0: return this._browsers.get(key);
michael@0: }
michael@0:
michael@0: // We should never be here. Return the first browser we find.
michael@0: Cu.reportError("NewTabPreloader: no matching browser found after updating");
michael@0: for (let [size, browser] of this._browsers) {
michael@0: return browser;
michael@0: }
michael@0:
michael@0: // We should really never be here.
michael@0: Cu.reportError("NewTabPreloader: not even a single browser was found?");
michael@0: return null;
michael@0: },
michael@0:
michael@0: observe: function (subject, topic, data) {
michael@0: if (topic === TOPIC_TIMER_CALLBACK) {
michael@0: this._updateTimer = null;
michael@0: this._updateBrowserSizes();
michael@0: } else {
michael@0: this._updateTimer = clearTimer(this._updateTimer);
michael@0: this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
michael@0: }
michael@0: },
michael@0:
michael@0: _updateBrowserSizes: function () {
michael@0: let sizes = this._collectTabBrowserSizes();
michael@0: let toRemove = [];
michael@0:
michael@0: // Iterate all browsers and check that they
michael@0: // each can be assigned to one of the sizes.
michael@0: for (let [key, browser] of this._browsers) {
michael@0: if (sizes.has(key)) {
michael@0: // We already have a browser for that size, great!
michael@0: sizes.delete(key);
michael@0: } else {
michael@0: // This browser is superfluous or needs to be resized.
michael@0: toRemove.push(browser);
michael@0: this._browsers.delete(key);
michael@0: }
michael@0: }
michael@0:
michael@0: // Iterate all sizes that we couldn't find a browser for.
michael@0: for (let [key, {width, height}] of sizes) {
michael@0: let browser;
michael@0: if (toRemove.length) {
michael@0: // Let's just resize one of the superfluous
michael@0: // browsers and put it back into the map.
michael@0: browser = toRemove.shift();
michael@0: browser.resize(width, height);
michael@0: } else {
michael@0: // No more browsers to reuse, create a new one.
michael@0: browser = new HiddenBrowser(width, height);
michael@0: }
michael@0:
michael@0: this._browsers.set(key, browser);
michael@0: }
michael@0:
michael@0: // Finally, remove all browsers we don't need anymore.
michael@0: toRemove.forEach(b => b.destroy());
michael@0: },
michael@0:
michael@0: _collectTabBrowserSizes: function () {
michael@0: let sizes = new Map();
michael@0:
michael@0: function tabBrowserBounds() {
michael@0: let wins = Services.ww.getWindowEnumerator("navigator:browser");
michael@0: while (wins.hasMoreElements()) {
michael@0: let win = wins.getNext();
michael@0: if (win.gBrowser) {
michael@0: let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0: .getInterface(Ci.nsIDOMWindowUtils);
michael@0: yield utils.getBoundsWithoutFlushing(win.gBrowser);
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: // Collect the sizes of all s out there.
michael@0: for (let {width, height} of tabBrowserBounds()) {
michael@0: if (width > 0 && height > 0) {
michael@0: let key = width + "x" + height;
michael@0: if (!sizes.has(key)) {
michael@0: sizes.set(key, {width: width, height: height});
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: return sizes;
michael@0: }
michael@0: };
michael@0:
michael@0: function HiddenBrowser(width, height) {
michael@0: this.resize(width, height);
michael@0: this._createBrowser();
michael@0: }
michael@0:
michael@0: HiddenBrowser.prototype = {
michael@0: _width: null,
michael@0: _height: null,
michael@0: _timer: null,
michael@0:
michael@0: get isPreloaded() {
michael@0: return this._browser &&
michael@0: this._browser.contentDocument &&
michael@0: this._browser.contentDocument.readyState === "complete" &&
michael@0: this._browser.currentURI.spec === NEWTAB_URL;
michael@0: },
michael@0:
michael@0: swapWithNewTab: function (aTab) {
michael@0: if (!this.isPreloaded || this._timer) {
michael@0: return false;
michael@0: }
michael@0:
michael@0: let win = aTab.ownerDocument.defaultView;
michael@0: let tabbrowser = win.gBrowser;
michael@0:
michael@0: if (!tabbrowser) {
michael@0: return false;
michael@0: }
michael@0:
michael@0: // Swap docShells.
michael@0: tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
michael@0:
michael@0: // Load all default frame scripts attached to the target window.
michael@0: let mm = aTab.linkedBrowser.messageManager;
michael@0: let scripts = win.messageManager.getDelayedFrameScripts();
michael@0: Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal));
michael@0:
michael@0: // Remove the browser, it will be recreated by a timer.
michael@0: this._removeBrowser();
michael@0:
michael@0: // Start a timer that will kick off preloading the next newtab page.
michael@0: this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
michael@0:
michael@0: // Signal that we swapped docShells.
michael@0: return true;
michael@0: },
michael@0:
michael@0: observe: function () {
michael@0: this._timer = null;
michael@0:
michael@0: // Start pre-loading the new tab page.
michael@0: this._createBrowser();
michael@0: },
michael@0:
michael@0: resize: function (width, height) {
michael@0: this._width = width;
michael@0: this._height = height;
michael@0: this._applySize();
michael@0: },
michael@0:
michael@0: destroy: function () {
michael@0: this._removeBrowser();
michael@0: this._timer = clearTimer(this._timer);
michael@0: },
michael@0:
michael@0: _applySize: function () {
michael@0: if (this._browser) {
michael@0: this._browser.style.width = this._width + "px";
michael@0: this._browser.style.height = this._height + "px";
michael@0: }
michael@0: },
michael@0:
michael@0: _createBrowser: function () {
michael@0: HostFrame.get().then(aFrame => {
michael@0: let doc = aFrame.document;
michael@0: this._browser = doc.createElementNS(XUL_NS, "browser");
michael@0: this._browser.setAttribute("type", "content");
michael@0: this._browser.setAttribute("src", NEWTAB_URL);
michael@0: this._applySize();
michael@0: doc.getElementById("win").appendChild(this._browser);
michael@0: });
michael@0: },
michael@0:
michael@0: _removeBrowser: function () {
michael@0: if (this._browser) {
michael@0: this._browser.remove();
michael@0: this._browser = null;
michael@0: }
michael@0: }
michael@0: };
michael@0:
michael@0: let HostFrame = {
michael@0: _frame: null,
michael@0: _deferred: null,
michael@0:
michael@0: get hiddenDOMDocument() {
michael@0: return Services.appShell.hiddenDOMWindow.document;
michael@0: },
michael@0:
michael@0: get isReady() {
michael@0: return this.hiddenDOMDocument.readyState === "complete";
michael@0: },
michael@0:
michael@0: get: function () {
michael@0: if (!this._deferred) {
michael@0: this._deferred = Promise.defer();
michael@0: this._create();
michael@0: }
michael@0:
michael@0: return this._deferred.promise;
michael@0: },
michael@0:
michael@0: destroy: function () {
michael@0: if (this._frame) {
michael@0: if (!Cu.isDeadWrapper(this._frame)) {
michael@0: this._frame.removeEventListener("load", this, true);
michael@0: this._frame.remove();
michael@0: }
michael@0:
michael@0: this._frame = null;
michael@0: this._deferred = null;
michael@0: }
michael@0: },
michael@0:
michael@0: handleEvent: function () {
michael@0: let contentWindow = this._frame.contentWindow;
michael@0: if (contentWindow.location.href === XUL_PAGE) {
michael@0: this._frame.removeEventListener("load", this, true);
michael@0: this._deferred.resolve(contentWindow);
michael@0: } else {
michael@0: contentWindow.location = XUL_PAGE;
michael@0: }
michael@0: },
michael@0:
michael@0: _create: function () {
michael@0: if (this.isReady) {
michael@0: let doc = this.hiddenDOMDocument;
michael@0: this._frame = doc.createElementNS(HTML_NS, "iframe");
michael@0: this._frame.addEventListener("load", this, true);
michael@0: doc.documentElement.appendChild(this._frame);
michael@0: } else {
michael@0: let flags = Ci.nsIThread.DISPATCH_NORMAL;
michael@0: Services.tm.currentThread.dispatch(() => this._create(), flags);
michael@0: }
michael@0: }
michael@0: };