You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
436 lines
11 KiB
436 lines
11 KiB
/* 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); |
|
} |
|
} |
|
};
|
|
|