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/. */
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 BROWSER_CONTENT_SCRIPT = "chrome://browser/content/content.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);
46 function clearTimer(timer) {
53 this.BrowserNewTabPreloader = {
54 init: function Preloader_init() {
58 uninit: function Preloader_uninit() {
62 HiddenBrowsers.uninit();
65 newTab: function Preloader_newTab(aTab) {
66 let win = aTab.ownerDocument.defaultView;
68 let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
69 .getInterface(Ci.nsIDOMWindowUtils);
71 let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
72 let hiddenBrowser = HiddenBrowsers.get(width, height)
74 return hiddenBrowser.swapWithNewTab(aTab);
82 Object.freeze(BrowserNewTabPreloader);
88 start: function Initializer_start() {
89 Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
90 this._observing = true;
93 stop: function Initializer_stop() {
94 this._timer = clearTimer(this._timer);
96 if (this._observing) {
97 Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
98 this._observing = false;
102 observe: function Initializer_observe(aSubject, aTopic, aData) {
103 if (aTopic == TOPIC_DELAYED_STARTUP) {
104 Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
105 this._observing = false;
107 } else if (aTopic == TOPIC_TIMER_CALLBACK) {
109 this._startPreloader();
113 _startTimer: function Initializer_startTimer() {
114 this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
117 _startPreloader: function Initializer_startPreloader() {
119 if (Preferences.enabled) {
120 HiddenBrowsers.init();
130 if (this._enabled === null) {
131 this._enabled = this._branch.getBoolPref("preload") &&
132 !this._branch.prefHasUserValue("url");
135 return this._enabled;
138 init: function Preferences_init() {
139 this._branch = Services.prefs.getBranch(PREF_BRANCH);
140 this._branch.addObserver("", this, false);
143 uninit: function Preferences_uninit() {
145 this._branch.removeObserver("", this);
150 observe: function Preferences_observe() {
151 let prevEnabled = this._enabled;
152 this._enabled = null;
154 if (prevEnabled && !this.enabled) {
155 HiddenBrowsers.uninit();
156 } else if (!prevEnabled && this.enabled) {
157 HiddenBrowsers.init();
162 let HiddenBrowsers = {
167 TOPIC_DELAYED_STARTUP,
168 TOPIC_XUL_WINDOW_CLOSED
172 this._browsers = new Map();
173 this._updateBrowserSizes();
174 this._topics.forEach(t => Services.obs.addObserver(this, t, false));
177 uninit: function () {
178 if (this._browsers) {
179 this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
180 this._updateTimer = clearTimer(this._updateTimer);
182 for (let [key, browser] of this._browsers) {
185 this._browsers = null;
189 get: function (width, height) {
190 // We haven't been initialized, yet.
191 if (!this._browsers) {
195 let key = width + "x" + height;
196 if (!this._browsers.has(key)) {
197 // Update all browsers' sizes if we can't find a matching one.
198 this._updateBrowserSizes();
201 // We should now have a matching browser.
202 if (this._browsers.has(key)) {
203 return this._browsers.get(key);
206 // We should never be here. Return the first browser we find.
207 Cu.reportError("NewTabPreloader: no matching browser found after updating");
208 for (let [size, browser] of this._browsers) {
212 // We should really never be here.
213 Cu.reportError("NewTabPreloader: not even a single browser was found?");
217 observe: function (subject, topic, data) {
218 if (topic === TOPIC_TIMER_CALLBACK) {
219 this._updateTimer = null;
220 this._updateBrowserSizes();
222 this._updateTimer = clearTimer(this._updateTimer);
223 this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
227 _updateBrowserSizes: function () {
228 let sizes = this._collectTabBrowserSizes();
231 // Iterate all browsers and check that they
232 // each can be assigned to one of the sizes.
233 for (let [key, browser] of this._browsers) {
234 if (sizes.has(key)) {
235 // We already have a browser for that size, great!
238 // This browser is superfluous or needs to be resized.
239 toRemove.push(browser);
240 this._browsers.delete(key);
244 // Iterate all sizes that we couldn't find a browser for.
245 for (let [key, {width, height}] of sizes) {
247 if (toRemove.length) {
248 // Let's just resize one of the superfluous
249 // browsers and put it back into the map.
250 browser = toRemove.shift();
251 browser.resize(width, height);
253 // No more browsers to reuse, create a new one.
254 browser = new HiddenBrowser(width, height);
257 this._browsers.set(key, browser);
260 // Finally, remove all browsers we don't need anymore.
261 toRemove.forEach(b => b.destroy());
264 _collectTabBrowserSizes: function () {
265 let sizes = new Map();
267 function tabBrowserBounds() {
268 let wins = Services.ww.getWindowEnumerator("navigator:browser");
269 while (wins.hasMoreElements()) {
270 let win = wins.getNext();
272 let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
273 .getInterface(Ci.nsIDOMWindowUtils);
274 yield utils.getBoundsWithoutFlushing(win.gBrowser);
279 // Collect the sizes of all <tabbrowser>s out there.
280 for (let {width, height} of tabBrowserBounds()) {
281 if (width > 0 && height > 0) {
282 let key = width + "x" + height;
283 if (!sizes.has(key)) {
284 sizes.set(key, {width: width, height: height});
293 function HiddenBrowser(width, height) {
294 this.resize(width, height);
295 this._createBrowser();
298 HiddenBrowser.prototype = {
304 return this._browser &&
305 this._browser.contentDocument &&
306 this._browser.contentDocument.readyState === "complete" &&
307 this._browser.currentURI.spec === NEWTAB_URL;
310 swapWithNewTab: function (aTab) {
311 if (!this.isPreloaded || this._timer) {
315 let win = aTab.ownerDocument.defaultView;
316 let tabbrowser = win.gBrowser;
323 tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
325 // Load all delayed frame scripts attached to the "browers" message manager.
326 // The browser content script was already loaded, so don't load it again.
327 let mm = aTab.linkedBrowser.messageManager;
328 let scripts = win.getGroupMessageManager("browsers").getDelayedFrameScripts();
329 Array.forEach(scripts, ([script, runGlobal]) => {
330 if (script != BROWSER_CONTENT_SCRIPT) {
331 mm.loadFrameScript(script, true, runGlobal);
335 // Remove the browser, it will be recreated by a timer.
336 this._removeBrowser();
338 // Start a timer that will kick off preloading the next newtab page.
339 this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
341 // Signal that we swapped docShells.
345 observe: function () {
348 // Start pre-loading the new tab page.
349 this._createBrowser();
352 resize: function (width, height) {
354 this._height = height;
358 destroy: function () {
359 this._removeBrowser();
360 this._timer = clearTimer(this._timer);
363 _applySize: function () {
365 this._browser.style.width = this._width + "px";
366 this._browser.style.height = this._height + "px";
370 _createBrowser: function () {
371 HostFrame.get().then(aFrame => {
372 let doc = aFrame.document;
373 this._browser = doc.createElementNS(XUL_NS, "browser");
374 this._browser.setAttribute("type", "content");
375 this._browser.setAttribute("src", NEWTAB_URL);
377 doc.getElementById("win").appendChild(this._browser);
379 // Let the docShell be inactive so that document.hidden=true.
380 this._browser.docShell.isActive = false;
382 this._browser.messageManager.loadFrameScript(BROWSER_CONTENT_SCRIPT,
387 _removeBrowser: function () {
389 this._browser.remove();
390 this._browser = null;
399 get hiddenDOMDocument() {
400 return Services.appShell.hiddenDOMWindow.document;
404 return this.hiddenDOMDocument.readyState === "complete";
408 if (!this._deferred) {
409 this._deferred = Promise.defer();
413 return this._deferred.promise;
416 destroy: function () {
418 if (!Cu.isDeadWrapper(this._frame)) {
419 this._frame.removeEventListener("load", this, true);
420 this._frame.remove();
424 this._deferred = null;
428 handleEvent: function () {
429 let contentWindow = this._frame.contentWindow;
430 if (contentWindow.location.href === XUL_PAGE) {
431 this._frame.removeEventListener("load", this, true);
432 this._deferred.resolve(contentWindow);
434 contentWindow.location = XUL_PAGE;
438 _create: function () {
440 let doc = this.hiddenDOMDocument;
441 this._frame = doc.createElementNS(HTML_NS, "iframe");
442 this._frame.addEventListener("load", this, true);
443 doc.documentElement.appendChild(this._frame);
445 let flags = Ci.nsIThread.DISPATCH_NORMAL;
446 Services.tm.currentThread.dispatch(() => this._create(), flags);