diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 0f60230fb7fb..efabcc10b786 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -139,6 +139,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", XPCOMUtils.defineLazyModuleGetter(this, "gBrowserNewTabPreloader", "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader"); +XPCOMUtils.defineLazyModuleGetter(this, "gCustomizationTabPreloader", + "resource:///modules/CustomizationTabPreloader.jsm", "CustomizationTabPreloader"); + XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index deb9874198b7..5d1f17844abb 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1594,6 +1594,8 @@ !PrivateBrowsingUtils.isWindowPrivate(window) && !gMultiProcessBrowser) { docShellsSwapped = gBrowserNewTabPreloader.newTab(t); + } else if (aURI == "about:customizing") { + docShellsSwapped = gCustomizationTabPreloader.newTab(t); } // Dispatch a new tab notification. We do this once we're diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index 29e24d8ab0fc..33c312bd3c5b 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -50,6 +50,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader", "resource:///modules/BrowserNewTabPreloader.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "CustomizationTabPreloader", + "resource:///modules/CustomizationTabPreloader.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "PdfJs", "resource://pdf.js/PdfJs.jsm"); @@ -469,6 +472,7 @@ BrowserGlue.prototype = { PageThumbs.init(); NewTabUtils.init(); BrowserNewTabPreloader.init(); + CustomizationTabPreloader.init(); SignInToWebsiteUX.init(); PdfJs.init(); #ifdef NIGHTLY_BUILD @@ -650,6 +654,7 @@ BrowserGlue.prototype = { } BrowserNewTabPreloader.uninit(); + CustomizationTabPreloader.uninit(); webappsUI.uninit(); SignInToWebsiteUX.uninit(); webrtcUI.uninit(); diff --git a/browser/modules/CustomizationTabPreloader.jsm b/browser/modules/CustomizationTabPreloader.jsm new file mode 100644 index 000000000000..bc4c9d61021b --- /dev/null +++ b/browser/modules/CustomizationTabPreloader.jsm @@ -0,0 +1,245 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["CustomizationTabPreloader"]; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,"; +const CUSTOMIZATION_URL = "about:customizing"; + +// The interval between swapping in a preload docShell and kicking off the +// next preload in the background. +const PRELOADER_INTERVAL_MS = 600; +// The initial delay before we start preloading our first customization page. The +// timer is started after the first 'browser-delayed-startup' has been sent. +const PRELOADER_INIT_DELAY_MS = 7000; + +const TOPIC_TIMER_CALLBACK = "timer-callback"; +const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished"; + +function createTimer(obj, delay) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT); + return timer; +} + +function clearTimer(timer) { + if (timer) { + timer.cancel(); + } + return null; +} + +this.CustomizationTabPreloader = { + init: function() { + CustomizationTabPreloaderInternal.init(); + }, + + uninit: function () { + CustomizationTabPreloaderInternal.uninit(); + }, + + newTab: function (aTab) { + return CustomizationTabPreloaderInternal.newTab(aTab); + }, +}; + +Object.freeze(CustomizationTabPreloader); + +this.CustomizationTabPreloaderInternal = { + _browser: null, + _timer: null, + _observing: false, + + init: function () { + Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false); + this._observing = true; + }, + + uninit: function () { + this._timer = clearTimer(this._timer); + + if (this._observing) { + Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP); + this._observing = false; + } + + HostFrame.destroy(); + + if (this._browser) { + this._browser.destroy(); + this._browser = null; + } + }, + + newTab: function (aTab) { + let win = aTab.ownerDocument.defaultView; + if (win.gBrowser && this._browser) { + return this._browser.swapWithNewTab(aTab); + } + + return false; + }, + + observe: function (aSubject, aTopic, aData) { + if (aTopic == TOPIC_DELAYED_STARTUP) { + Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP); + this._observing = false; + this._startTimer(); + } else if (aTopic == TOPIC_TIMER_CALLBACK) { + this._timer = null; + this._startPreloader(); + } + }, + + _startTimer: function () { + this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS); + }, + + _startPreloader: function () { + this._browser = new HiddenBrowser(); + } +}; + +function HiddenBrowser() { + this._createBrowser(); +} + +HiddenBrowser.prototype = { + _timer: null, + + get isPreloaded() { + return this._browser && + this._browser.contentDocument && + this._browser.contentDocument.readyState === "complete" && + this._browser.currentURI.spec === CUSTOMIZATION_URL; + }, + + swapWithNewTab: function (aTab) { + if (!this.isPreloaded || this._timer) { + return false; + } + + let win = aTab.ownerDocument.defaultView; + let tabbrowser = win.gBrowser; + + if (!tabbrowser) { + return false; + } + + // Swap docShells. + tabbrowser.swapNewTabWithBrowser(aTab, this._browser); + + // Load all default frame scripts attached to the target window. + let mm = aTab.linkedBrowser.messageManager; + let scripts = win.messageManager.getDelayedFrameScripts(); + Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal)); + + // Remove the browser, it will be recreated by a timer. + this._removeBrowser(); + + // Start a timer that will kick off preloading the next page. + this._timer = createTimer(this, PRELOADER_INTERVAL_MS); + + // Signal that we swapped docShells. + return true; + }, + + observe: function () { + this._timer = null; + + // Start pre-loading the customization page. + this._createBrowser(); + }, + + destroy: function () { + this._removeBrowser(); + this._timer = clearTimer(this._timer); + }, + + _createBrowser: function () { + HostFrame.get().then(aFrame => { + let doc = aFrame.document; + this._browser = doc.createElementNS(XUL_NS, "browser"); + this._browser.setAttribute("type", "content"); + this._browser.setAttribute("src", CUSTOMIZATION_URL); + this._browser.style.width = "400px"; + this._browser.style.height = "400px"; + doc.getElementById("win").appendChild(this._browser); + }); + }, + + _removeBrowser: function () { + if (this._browser) { + this._browser.remove(); + this._browser = null; + } + } +}; + +let HostFrame = { + _frame: null, + _deferred: null, + + get hiddenDOMDocument() { + return Services.appShell.hiddenDOMWindow.document; + }, + + get isReady() { + return this.hiddenDOMDocument.readyState === "complete"; + }, + + get: function () { + if (!this._deferred) { + this._deferred = Promise.defer(); + this._create(); + } + + return this._deferred.promise; + }, + + destroy: function () { + if (this._frame) { + if (!Cu.isDeadWrapper(this._frame)) { + this._frame.removeEventListener("load", this, true); + this._frame.remove(); + } + + this._frame = null; + this._deferred = null; + } + }, + + handleEvent: function () { + let contentWindow = this._frame.contentWindow; + if (contentWindow.location.href === XUL_PAGE) { + this._frame.removeEventListener("load", this, true); + this._deferred.resolve(contentWindow); + } else { + contentWindow.location = XUL_PAGE; + } + }, + + _create: function () { + if (this.isReady) { + let doc = this.hiddenDOMDocument; + this._frame = doc.createElementNS(HTML_NS, "iframe"); + this._frame.addEventListener("load", this, true); + doc.documentElement.appendChild(this._frame); + } else { + let flags = Ci.nsIThread.DISPATCH_NORMAL; + Services.tm.currentThread.dispatch(() => this._create(), flags); + } + } +}; diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 11d59e4936fa..b7724ec46498 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -11,6 +11,7 @@ EXTRA_JS_MODULES += [ 'BrowserUITelemetry.jsm', 'ContentClick.jsm', 'ContentLinkHandler.jsm', + 'CustomizationTabPreloader.jsm', 'Feeds.jsm', 'NetworkPrioritizer.jsm', 'offlineAppCache.jsm',