diff options
author | Matt A. Tobin <email@mattatobin.com> | 2022-02-12 13:57:21 -0600 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2022-02-12 13:57:21 -0600 |
commit | ba7d67bb0711c9066c71bd33e55d9a5d2f9b2cbf (patch) | |
tree | a5c0cfad71c17114c78d8a7d1f31112eb53896df /browser/modules/BrowserNewTabPreloader.jsm | |
parent | c054e324210895e7e2c5b3e84437cba43f201ec8 (diff) | |
download | palemoon-gre-ba7d67bb0711c9066c71bd33e55d9a5d2f9b2cbf.tar.gz |
Lay down Pale Moon 30
Diffstat (limited to 'browser/modules/BrowserNewTabPreloader.jsm')
-rw-r--r-- | browser/modules/BrowserNewTabPreloader.jsm | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/browser/modules/BrowserNewTabPreloader.jsm b/browser/modules/BrowserNewTabPreloader.jsm new file mode 100644 index 000000000..15d7ee286 --- /dev/null +++ b/browser/modules/BrowserNewTabPreloader.jsm @@ -0,0 +1,436 @@ +/* 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 = ["BrowserNewTabPreloader"]; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.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,<window%20id='win'/>"; +const NEWTAB_URL = "about:newtab"; +const PREF_BRANCH = "browser.newtab."; + +// 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 new tab page. The +// timer is started after the first 'browser-delayed-startup' has been sent. +const PRELOADER_INIT_DELAY_MS = 5000; +// The number of miliseconds we'll wait after we received a notification that +// causes us to update our list of browsers and tabbrowser sizes. This acts as +// kind of a damper when too many events are occuring in quick succession. +const PRELOADER_UPDATE_DELAY_MS = 3000; + +const TOPIC_TIMER_CALLBACK = "timer-callback"; +const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished"; +const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed"; + +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.BrowserNewTabPreloader = { + init: function() { + Initializer.start(); + }, + + uninit: function() { + Initializer.stop(); + HostFrame.destroy(); + Preferences.uninit(); + HiddenBrowsers.uninit(); + }, + + newTab: function(aTab) { + let win = aTab.ownerDocument.defaultView; + if (win.gBrowser) { + let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser); + let hiddenBrowser = HiddenBrowsers.get(width, height) + if (hiddenBrowser) { + return hiddenBrowser.swapWithNewTab(aTab); + } + } + + return false; + } +}; + +Object.freeze(BrowserNewTabPreloader); + +var Initializer = { + _timer: null, + _observing: false, + + start: function() { + Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false); + this._observing = true; + }, + + stop: function() { + this._timer = clearTimer(this._timer); + + if (this._observing) { + Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP); + this._observing = 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() { + Preferences.init(); + if (Preferences.enabled) { + HiddenBrowsers.init(); + } + } +}; + +var Preferences = { + _enabled: null, + _branch: null, + + get enabled() { + if (this._enabled === null) { + this._enabled = this._branch.getBoolPref("preload") && + !this._branch.prefHasUserValue("url"); + } + + return this._enabled; + }, + + init: function() { + this._branch = Services.prefs.getBranch(PREF_BRANCH); + this._branch.addObserver("", this, false); + }, + + uninit: function() { + if (this._branch) { + this._branch.removeObserver("", this); + this._branch = null; + } + }, + + observe: function() { + let prevEnabled = this._enabled; + this._enabled = null; + + if (prevEnabled && !this.enabled) { + HiddenBrowsers.uninit(); + } else if (!prevEnabled && this.enabled) { + HiddenBrowsers.init(); + } + }, +}; + +var HiddenBrowsers = { + _browsers: null, + _updateTimer: null, + + _topics: [ + TOPIC_DELAYED_STARTUP, + TOPIC_XUL_WINDOW_CLOSED + ], + + init: function() { + this._browsers = new Map(); + this._updateBrowserSizes(); + this._topics.forEach(t => Services.obs.addObserver(this, t, false)); + }, + + uninit: function() { + if (this._browsers) { + this._topics.forEach(t => Services.obs.removeObserver(this, t, false)); + this._updateTimer = clearTimer(this._updateTimer); + + for (let [key, browser] of this._browsers) { + browser.destroy(); + } + this._browsers = null; + } + }, + + get: function(width, height) { + // We haven't been initialized, yet. + if (!this._browsers) { + return null; + } + + let key = width + "x" + height; + if (!this._browsers.has(key)) { + // Update all browsers' sizes if we can't find a matching one. + this._updateBrowserSizes(); + } + + // We should now have a matching browser. + if (this._browsers.has(key)) { + return this._browsers.get(key); + } + + // We should never be here. Return the first browser we find. + Cu.reportError("NewTabPreloader: no matching browser found after updating"); + for (let [size, browser] of this._browsers) { + return browser; + } + + // We should really never be here. + Cu.reportError("NewTabPreloader: not even a single browser was found?"); + return null; + }, + + observe: function(subject, topic, data) { + if (topic === TOPIC_TIMER_CALLBACK) { + this._updateTimer = null; + this._updateBrowserSizes(); + } else { + this._updateTimer = clearTimer(this._updateTimer); + this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS); + } + }, + + _updateBrowserSizes: function() { + let sizes = this._collectTabBrowserSizes(); + let toRemove = []; + + // Iterate all browsers and check that they + // each can be assigned to one of the sizes. + for (let [key, browser] of this._browsers) { + if (sizes.has(key)) { + // We already have a browser for that size, great! + sizes.delete(key); + } else { + // This browser is superfluous or needs to be resized. + toRemove.push(browser); + this._browsers.delete(key); + } + } + + // Iterate all sizes that we couldn't find a browser for. + for (let [key, {width, height}] of sizes) { + let browser; + if (toRemove.length) { + // Let's just resize one of the superfluous + // browsers and put it back into the map. + browser = toRemove.shift(); + browser.resize(width, height); + } else { + // No more browsers to reuse, create a new one. + browser = new HiddenBrowser(width, height); + } + + this._browsers.set(key, browser); + } + + // Finally, remove all browsers we don't need anymore. + toRemove.forEach(b => b.destroy()); + }, + + _collectTabBrowserSizes: function() { + let sizes = new Map(); + + function tabBrowserBounds() { + let wins = Services.ww.getWindowEnumerator("navigator:browser"); + while (wins.hasMoreElements()) { + let win = wins.getNext(); + if (win.gBrowser) { + let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + yield utils.getBoundsWithoutFlushing(win.gBrowser); + } + } + } + + // Collect the sizes of all <tabbrowser>s out there. + for (let {width, height} of tabBrowserBounds()) { + if (width > 0 && height > 0) { + let key = width + "x" + height; + if (!sizes.has(key)) { + sizes.set(key, {width: width, height: height}); + } + } + } + + return sizes; + } +}; + +function HiddenBrowser(width, height) { + this.resize(width, height); + + HostFrame.get().then(aFrame => { + let doc = aFrame.document; + this._browser = doc.createElementNS(XUL_NS, "browser"); + this._browser.setAttribute("type", "content"); + this._browser.setAttribute("src", NEWTAB_URL); + this._applySize(); + doc.getElementById("win").appendChild(this._browser); + }); +} + +HiddenBrowser.prototype = { + _width: null, + _height: null, + _timer: null, + _needsFrameScripts: true, + + get isPreloaded() { + return this._browser && + this._browser.contentDocument && + this._browser.contentDocument.readyState === "complete" && + this._browser.currentURI.spec === NEWTAB_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. + if (this._needsFrameScripts) { + this._needsFrameScripts = false; + + let mm = aTab.linkedBrowser.messageManager; + mm.loadFrameScript("chrome://browser/content/content.js", true); + mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true); + + if ("TabView" in win) { + mm.loadFrameScript("chrome://browser/content/tabview-content.js", true); + } + } + + // Start a timer that will kick off preloading the next newtab page. + this._timer = createTimer(this, PRELOADER_INTERVAL_MS); + + // Signal that we swapped docShells. + return true; + }, + + observe: function() { + this._timer = null; + + // Start pre-loading the new tab page. + this._browser.loadURI(NEWTAB_URL); + }, + + resize: function(width, height) { + this._width = width; + this._height = height; + this._applySize(); + }, + + _applySize: function() { + if (this._browser) { + this._browser.style.width = this._width + "px"; + this._browser.style.height = this._height + "px"; + } + }, + + destroy: function() { + if (this._browser) { + this._browser.remove(); + this._browser = null; + } + + this._timer = clearTimer(this._timer); + } +}; + +var 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); + } + } +}; |