Bug 1031527 - Remove dup fd from ParamTraits<MagicGrallocBufferHandle>::Read(). r...
[gecko.git] / browser / modules / BrowserNewTabPreloader.jsm
blob5eb9a8e73154c57806d613fae9d26fe91dea4ead
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 function createTimer(obj, delay) {
39   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
40   timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT);
41   return timer;
44 function clearTimer(timer) {
45   if (timer) {
46     timer.cancel();
47   }
48   return null;
51 this.BrowserNewTabPreloader = {
52   init: function Preloader_init() {
53     Initializer.start();
54   },
56   uninit: function Preloader_uninit() {
57     Initializer.stop();
58     HostFrame.destroy();
59     Preferences.uninit();
60     HiddenBrowsers.uninit();
61   },
63   newTab: function Preloader_newTab(aTab) {
64     let win = aTab.ownerDocument.defaultView;
65     if (win.gBrowser) {
66       let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
67                      .getInterface(Ci.nsIDOMWindowUtils);
69       let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
70       let hiddenBrowser = HiddenBrowsers.get(width, height)
71       if (hiddenBrowser) {
72         return hiddenBrowser.swapWithNewTab(aTab);
73       }
74     }
76     return false;
77   }
80 Object.freeze(BrowserNewTabPreloader);
82 let Initializer = {
83   _timer: null,
84   _observing: false,
86   start: function Initializer_start() {
87     Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
88     this._observing = true;
89   },
91   stop: function Initializer_stop() {
92     this._timer = clearTimer(this._timer);
94     if (this._observing) {
95       Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
96       this._observing = false;
97     }
98   },
100   observe: function Initializer_observe(aSubject, aTopic, aData) {
101     if (aTopic == TOPIC_DELAYED_STARTUP) {
102       Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
103       this._observing = false;
104       this._startTimer();
105     } else if (aTopic == TOPIC_TIMER_CALLBACK) {
106       this._timer = null;
107       this._startPreloader();
108     }
109   },
111   _startTimer: function Initializer_startTimer() {
112     this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
113   },
115   _startPreloader: function Initializer_startPreloader() {
116     Preferences.init();
117     if (Preferences.enabled) {
118       HiddenBrowsers.init();
119     }
120   }
123 let Preferences = {
124   _enabled: null,
125   _branch: null,
127   get enabled() {
128     if (this._enabled === null) {
129       this._enabled = this._branch.getBoolPref("preload") &&
130                       !this._branch.prefHasUserValue("url");
131     }
133     return this._enabled;
134   },
136   init: function Preferences_init() {
137     this._branch = Services.prefs.getBranch(PREF_BRANCH);
138     this._branch.addObserver("", this, false);
139   },
141   uninit: function Preferences_uninit() {
142     if (this._branch) {
143       this._branch.removeObserver("", this);
144       this._branch = null;
145     }
146   },
148   observe: function Preferences_observe() {
149     let prevEnabled = this._enabled;
150     this._enabled = null;
152     if (prevEnabled && !this.enabled) {
153       HiddenBrowsers.uninit();
154     } else if (!prevEnabled && this.enabled) {
155       HiddenBrowsers.init();
156     }
157   },
160 let HiddenBrowsers = {
161   _browsers: null,
162   _updateTimer: null,
164   _topics: [
165     TOPIC_DELAYED_STARTUP,
166     TOPIC_XUL_WINDOW_CLOSED
167   ],
169   init: function () {
170     this._browsers = new Map();
171     this._updateBrowserSizes();
172     this._topics.forEach(t => Services.obs.addObserver(this, t, false));
173   },
175   uninit: function () {
176     if (this._browsers) {
177       this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
178       this._updateTimer = clearTimer(this._updateTimer);
180       for (let [key, browser] of this._browsers) {
181         browser.destroy();
182       }
183       this._browsers = null;
184     }
185   },
187   get: function (width, height) {
188     // We haven't been initialized, yet.
189     if (!this._browsers) {
190       return null;
191     }
193     let key = width + "x" + height;
194     if (!this._browsers.has(key)) {
195       // Update all browsers' sizes if we can't find a matching one.
196       this._updateBrowserSizes();
197     }
199     // We should now have a matching browser.
200     if (this._browsers.has(key)) {
201       return this._browsers.get(key);
202     }
204     // We should never be here. Return the first browser we find.
205     Cu.reportError("NewTabPreloader: no matching browser found after updating");
206     for (let [size, browser] of this._browsers) {
207       return browser;
208     }
210     // We should really never be here.
211     Cu.reportError("NewTabPreloader: not even a single browser was found?");
212     return null;
213   },
215   observe: function (subject, topic, data) {
216     if (topic === TOPIC_TIMER_CALLBACK) {
217       this._updateTimer = null;
218       this._updateBrowserSizes();
219     } else {
220       this._updateTimer = clearTimer(this._updateTimer);
221       this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
222     }
223   },
225   _updateBrowserSizes: function () {
226     let sizes = this._collectTabBrowserSizes();
227     let toRemove = [];
229     // Iterate all browsers and check that they
230     // each can be assigned to one of the sizes.
231     for (let [key, browser] of this._browsers) {
232       if (sizes.has(key)) {
233         // We already have a browser for that size, great!
234         sizes.delete(key);
235       } else {
236         // This browser is superfluous or needs to be resized.
237         toRemove.push(browser);
238         this._browsers.delete(key);
239       }
240     }
242     // Iterate all sizes that we couldn't find a browser for.
243     for (let [key, {width, height}] of sizes) {
244       let browser;
245       if (toRemove.length) {
246         // Let's just resize one of the superfluous
247         // browsers and put it back into the map.
248         browser = toRemove.shift();
249         browser.resize(width, height);
250       } else {
251         // No more browsers to reuse, create a new one.
252         browser = new HiddenBrowser(width, height);
253       }
255       this._browsers.set(key, browser);
256     }
258     // Finally, remove all browsers we don't need anymore.
259     toRemove.forEach(b => b.destroy());
260   },
262   _collectTabBrowserSizes: function () {
263     let sizes = new Map();
265     function tabBrowserBounds() {
266       let wins = Services.ww.getWindowEnumerator("navigator:browser");
267       while (wins.hasMoreElements()) {
268         let win = wins.getNext();
269         if (win.gBrowser) {
270           let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
271                          .getInterface(Ci.nsIDOMWindowUtils);
272           yield utils.getBoundsWithoutFlushing(win.gBrowser);
273         }
274       }
275     }
277     // Collect the sizes of all <tabbrowser>s out there.
278     for (let {width, height} of tabBrowserBounds()) {
279       if (width > 0 && height > 0) {
280         let key = width + "x" + height;
281         if (!sizes.has(key)) {
282           sizes.set(key, {width: width, height: height});
283         }
284       }
285     }
287     return sizes;
288   }
291 function HiddenBrowser(width, height) {
292   this.resize(width, height);
293   this._createBrowser();
296 HiddenBrowser.prototype = {
297   _width: null,
298   _height: null,
299   _timer: null,
301   get isPreloaded() {
302     return this._browser &&
303            this._browser.contentDocument &&
304            this._browser.contentDocument.readyState === "complete" &&
305            this._browser.currentURI.spec === NEWTAB_URL;
306   },
308   swapWithNewTab: function (aTab) {
309     if (!this.isPreloaded || this._timer) {
310       return false;
311     }
313     let win = aTab.ownerDocument.defaultView;
314     let tabbrowser = win.gBrowser;
316     if (!tabbrowser) {
317       return false;
318     }
320     // Swap docShells.
321     tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
323     // Load all delayed frame scripts attached to the "browers" message manager.
324     let mm = aTab.linkedBrowser.messageManager;
325     let scripts = win.getGroupMessageManager("browsers").getDelayedFrameScripts();
326     Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal));
328     // Remove the browser, it will be recreated by a timer.
329     this._removeBrowser();
331     // Start a timer that will kick off preloading the next newtab page.
332     this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
334     // Signal that we swapped docShells.
335     return true;
336   },
338   observe: function () {
339     this._timer = null;
341     // Start pre-loading the new tab page.
342     this._createBrowser();
343   },
345   resize: function (width, height) {
346     this._width = width;
347     this._height = height;
348     this._applySize();
349   },
351   destroy: function () {
352     this._removeBrowser();
353     this._timer = clearTimer(this._timer);
354   },
356   _applySize: function () {
357     if (this._browser) {
358       this._browser.style.width = this._width + "px";
359       this._browser.style.height = this._height + "px";
360     }
361   },
363   _createBrowser: function () {
364     HostFrame.get().then(aFrame => {
365       let doc = aFrame.document;
366       this._browser = doc.createElementNS(XUL_NS, "browser");
367       this._browser.setAttribute("type", "content");
368       this._browser.setAttribute("src", NEWTAB_URL);
369       this._applySize();
370       doc.getElementById("win").appendChild(this._browser);
372       // Let the docShell be inactive so that document.hidden=true.
373       this._browser.docShell.isActive = false;
374     });
375   },
377   _removeBrowser: function () {
378     if (this._browser) {
379       this._browser.remove();
380       this._browser = null;
381     }
382   }
385 let HostFrame = {
386   _frame: null,
387   _deferred: null,
389   get hiddenDOMDocument() {
390     return Services.appShell.hiddenDOMWindow.document;
391   },
393   get isReady() {
394     return this.hiddenDOMDocument.readyState === "complete";
395   },
397   get: function () {
398     if (!this._deferred) {
399       this._deferred = Promise.defer();
400       this._create();
401     }
403     return this._deferred.promise;
404   },
406   destroy: function () {
407     if (this._frame) {
408       if (!Cu.isDeadWrapper(this._frame)) {
409         this._frame.removeEventListener("load", this, true);
410         this._frame.remove();
411       }
413       this._frame = null;
414       this._deferred = null;
415     }
416   },
418   handleEvent: function () {
419     let contentWindow = this._frame.contentWindow;
420     if (contentWindow.location.href === XUL_PAGE) {
421       this._frame.removeEventListener("load", this, true);
422       this._deferred.resolve(contentWindow);
423     } else {
424       contentWindow.location = XUL_PAGE;
425     }
426   },
428   _create: function () {
429     if (this.isReady) {
430       let doc = this.hiddenDOMDocument;
431       this._frame = doc.createElementNS(HTML_NS, "iframe");
432       this._frame.addEventListener("load", this, true);
433       doc.documentElement.appendChild(this._frame);
434     } else {
435       let flags = Ci.nsIThread.DISPATCH_NORMAL;
436       Services.tm.currentThread.dispatch(() => this._create(), flags);
437     }
438   }