Bumping manifests a=b2g-bump
[gecko.git] / browser / modules / BrowserNewTabPreloader.jsm
blobddb7759a0029eb757b3e8eaa22eb0e1eafafb705
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/. */
5 "use strict";
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);
43   return timer;
46 function clearTimer(timer) {
47   if (timer) {
48     timer.cancel();
49   }
50   return null;
53 this.BrowserNewTabPreloader = {
54   init: function Preloader_init() {
55     Initializer.start();
56   },
58   uninit: function Preloader_uninit() {
59     Initializer.stop();
60     HostFrame.destroy();
61     Preferences.uninit();
62     HiddenBrowsers.uninit();
63   },
65   newTab: function Preloader_newTab(aTab) {
66     let win = aTab.ownerDocument.defaultView;
67     if (win.gBrowser) {
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)
73       if (hiddenBrowser) {
74         return hiddenBrowser.swapWithNewTab(aTab);
75       }
76     }
78     return false;
79   }
82 Object.freeze(BrowserNewTabPreloader);
84 let Initializer = {
85   _timer: null,
86   _observing: false,
88   start: function Initializer_start() {
89     Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
90     this._observing = true;
91   },
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;
99     }
100   },
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;
106       this._startTimer();
107     } else if (aTopic == TOPIC_TIMER_CALLBACK) {
108       this._timer = null;
109       this._startPreloader();
110     }
111   },
113   _startTimer: function Initializer_startTimer() {
114     this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
115   },
117   _startPreloader: function Initializer_startPreloader() {
118     Preferences.init();
119     if (Preferences.enabled) {
120       HiddenBrowsers.init();
121     }
122   }
125 let Preferences = {
126   _enabled: null,
127   _branch: null,
129   get enabled() {
130     if (this._enabled === null) {
131       this._enabled = this._branch.getBoolPref("preload") &&
132                       !this._branch.prefHasUserValue("url");
133     }
135     return this._enabled;
136   },
138   init: function Preferences_init() {
139     this._branch = Services.prefs.getBranch(PREF_BRANCH);
140     this._branch.addObserver("", this, false);
141   },
143   uninit: function Preferences_uninit() {
144     if (this._branch) {
145       this._branch.removeObserver("", this);
146       this._branch = null;
147     }
148   },
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();
158     }
159   },
162 let HiddenBrowsers = {
163   _browsers: null,
164   _updateTimer: null,
166   _topics: [
167     TOPIC_DELAYED_STARTUP,
168     TOPIC_XUL_WINDOW_CLOSED
169   ],
171   init: function () {
172     this._browsers = new Map();
173     this._updateBrowserSizes();
174     this._topics.forEach(t => Services.obs.addObserver(this, t, false));
175   },
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) {
183         browser.destroy();
184       }
185       this._browsers = null;
186     }
187   },
189   get: function (width, height) {
190     // We haven't been initialized, yet.
191     if (!this._browsers) {
192       return null;
193     }
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();
199     }
201     // We should now have a matching browser.
202     if (this._browsers.has(key)) {
203       return this._browsers.get(key);
204     }
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) {
209       return browser;
210     }
212     // We should really never be here.
213     Cu.reportError("NewTabPreloader: not even a single browser was found?");
214     return null;
215   },
217   observe: function (subject, topic, data) {
218     if (topic === TOPIC_TIMER_CALLBACK) {
219       this._updateTimer = null;
220       this._updateBrowserSizes();
221     } else {
222       this._updateTimer = clearTimer(this._updateTimer);
223       this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
224     }
225   },
227   _updateBrowserSizes: function () {
228     let sizes = this._collectTabBrowserSizes();
229     let toRemove = [];
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!
236         sizes.delete(key);
237       } else {
238         // This browser is superfluous or needs to be resized.
239         toRemove.push(browser);
240         this._browsers.delete(key);
241       }
242     }
244     // Iterate all sizes that we couldn't find a browser for.
245     for (let [key, {width, height}] of sizes) {
246       let browser;
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);
252       } else {
253         // No more browsers to reuse, create a new one.
254         browser = new HiddenBrowser(width, height);
255       }
257       this._browsers.set(key, browser);
258     }
260     // Finally, remove all browsers we don't need anymore.
261     toRemove.forEach(b => b.destroy());
262   },
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();
271         if (win.gBrowser) {
272           let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
273                          .getInterface(Ci.nsIDOMWindowUtils);
274           yield utils.getBoundsWithoutFlushing(win.gBrowser);
275         }
276       }
277     }
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});
285         }
286       }
287     }
289     return sizes;
290   }
293 function HiddenBrowser(width, height) {
294   this.resize(width, height);
295   this._createBrowser();
298 HiddenBrowser.prototype = {
299   _width: null,
300   _height: null,
301   _timer: null,
303   get isPreloaded() {
304     return this._browser &&
305            this._browser.contentDocument &&
306            this._browser.contentDocument.readyState === "complete" &&
307            this._browser.currentURI.spec === NEWTAB_URL;
308   },
310   swapWithNewTab: function (aTab) {
311     if (!this.isPreloaded || this._timer) {
312       return false;
313     }
315     let win = aTab.ownerDocument.defaultView;
316     let tabbrowser = win.gBrowser;
318     if (!tabbrowser) {
319       return false;
320     }
322     // Swap docShells.
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);
332       }
333     });
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.
342     return true;
343   },
345   observe: function () {
346     this._timer = null;
348     // Start pre-loading the new tab page.
349     this._createBrowser();
350   },
352   resize: function (width, height) {
353     this._width = width;
354     this._height = height;
355     this._applySize();
356   },
358   destroy: function () {
359     this._removeBrowser();
360     this._timer = clearTimer(this._timer);
361   },
363   _applySize: function () {
364     if (this._browser) {
365       this._browser.style.width = this._width + "px";
366       this._browser.style.height = this._height + "px";
367     }
368   },
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);
376       this._applySize();
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,
383                                                    true);
384     });
385   },
387   _removeBrowser: function () {
388     if (this._browser) {
389       this._browser.remove();
390       this._browser = null;
391     }
392   }
395 let HostFrame = {
396   _frame: null,
397   _deferred: null,
399   get hiddenDOMDocument() {
400     return Services.appShell.hiddenDOMWindow.document;
401   },
403   get isReady() {
404     return this.hiddenDOMDocument.readyState === "complete";
405   },
407   get: function () {
408     if (!this._deferred) {
409       this._deferred = Promise.defer();
410       this._create();
411     }
413     return this._deferred.promise;
414   },
416   destroy: function () {
417     if (this._frame) {
418       if (!Cu.isDeadWrapper(this._frame)) {
419         this._frame.removeEventListener("load", this, true);
420         this._frame.remove();
421       }
423       this._frame = null;
424       this._deferred = null;
425     }
426   },
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);
433     } else {
434       contentWindow.location = XUL_PAGE;
435     }
436   },
438   _create: function () {
439     if (this.isReady) {
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);
444     } else {
445       let flags = Ci.nsIThread.DISPATCH_NORMAL;
446       Services.tm.currentThread.dispatch(() => this._create(), flags);
447     }
448   }