Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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 FRAME_SCRIPT_URL = "chrome://browser/content/newtab/preloaderContent.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;
44 }
46 function clearTimer(timer) {
47 if (timer) {
48 timer.cancel();
49 }
50 return null;
51 }
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 swapped = false;
67 let win = aTab.ownerDocument.defaultView;
68 if (win.gBrowser) {
69 let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
70 .getInterface(Ci.nsIDOMWindowUtils);
72 let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
73 let hiddenBrowser = HiddenBrowsers.get(width, height)
74 if (hiddenBrowser) {
75 swapped = hiddenBrowser.swapWithNewTab(aTab);
76 }
78 // aTab's browser is now visible and is therefore allowed to make
79 // background captures.
80 let msgMan = aTab.linkedBrowser.messageManager;
81 msgMan.loadFrameScript(FRAME_SCRIPT_URL, false);
82 msgMan.sendAsyncMessage("BrowserNewTabPreloader:allowBackgroundCaptures");
83 }
85 return swapped;
86 }
87 };
89 Object.freeze(BrowserNewTabPreloader);
91 let Initializer = {
92 _timer: null,
93 _observing: false,
95 start: function Initializer_start() {
96 Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
97 this._observing = true;
98 },
100 stop: function Initializer_stop() {
101 this._timer = clearTimer(this._timer);
103 if (this._observing) {
104 Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
105 this._observing = false;
106 }
107 },
109 observe: function Initializer_observe(aSubject, aTopic, aData) {
110 if (aTopic == TOPIC_DELAYED_STARTUP) {
111 Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
112 this._observing = false;
113 this._startTimer();
114 } else if (aTopic == TOPIC_TIMER_CALLBACK) {
115 this._timer = null;
116 this._startPreloader();
117 }
118 },
120 _startTimer: function Initializer_startTimer() {
121 this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
122 },
124 _startPreloader: function Initializer_startPreloader() {
125 Preferences.init();
126 if (Preferences.enabled) {
127 HiddenBrowsers.init();
128 }
129 }
130 };
132 let Preferences = {
133 _enabled: null,
134 _branch: null,
136 get enabled() {
137 if (this._enabled === null) {
138 this._enabled = this._branch.getBoolPref("preload") &&
139 !this._branch.prefHasUserValue("url");
140 }
142 return this._enabled;
143 },
145 init: function Preferences_init() {
146 this._branch = Services.prefs.getBranch(PREF_BRANCH);
147 this._branch.addObserver("", this, false);
148 },
150 uninit: function Preferences_uninit() {
151 if (this._branch) {
152 this._branch.removeObserver("", this);
153 this._branch = null;
154 }
155 },
157 observe: function Preferences_observe() {
158 let prevEnabled = this._enabled;
159 this._enabled = null;
161 if (prevEnabled && !this.enabled) {
162 HiddenBrowsers.uninit();
163 } else if (!prevEnabled && this.enabled) {
164 HiddenBrowsers.init();
165 }
166 },
167 };
169 let HiddenBrowsers = {
170 _browsers: null,
171 _updateTimer: null,
173 _topics: [
174 TOPIC_DELAYED_STARTUP,
175 TOPIC_XUL_WINDOW_CLOSED
176 ],
178 init: function () {
179 this._browsers = new Map();
180 this._updateBrowserSizes();
181 this._topics.forEach(t => Services.obs.addObserver(this, t, false));
182 },
184 uninit: function () {
185 if (this._browsers) {
186 this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
187 this._updateTimer = clearTimer(this._updateTimer);
189 for (let [key, browser] of this._browsers) {
190 browser.destroy();
191 }
192 this._browsers = null;
193 }
194 },
196 get: function (width, height) {
197 // We haven't been initialized, yet.
198 if (!this._browsers) {
199 return null;
200 }
202 let key = width + "x" + height;
203 if (!this._browsers.has(key)) {
204 // Update all browsers' sizes if we can't find a matching one.
205 this._updateBrowserSizes();
206 }
208 // We should now have a matching browser.
209 if (this._browsers.has(key)) {
210 return this._browsers.get(key);
211 }
213 // We should never be here. Return the first browser we find.
214 Cu.reportError("NewTabPreloader: no matching browser found after updating");
215 for (let [size, browser] of this._browsers) {
216 return browser;
217 }
219 // We should really never be here.
220 Cu.reportError("NewTabPreloader: not even a single browser was found?");
221 return null;
222 },
224 observe: function (subject, topic, data) {
225 if (topic === TOPIC_TIMER_CALLBACK) {
226 this._updateTimer = null;
227 this._updateBrowserSizes();
228 } else {
229 this._updateTimer = clearTimer(this._updateTimer);
230 this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
231 }
232 },
234 _updateBrowserSizes: function () {
235 let sizes = this._collectTabBrowserSizes();
236 let toRemove = [];
238 // Iterate all browsers and check that they
239 // each can be assigned to one of the sizes.
240 for (let [key, browser] of this._browsers) {
241 if (sizes.has(key)) {
242 // We already have a browser for that size, great!
243 sizes.delete(key);
244 } else {
245 // This browser is superfluous or needs to be resized.
246 toRemove.push(browser);
247 this._browsers.delete(key);
248 }
249 }
251 // Iterate all sizes that we couldn't find a browser for.
252 for (let [key, {width, height}] of sizes) {
253 let browser;
254 if (toRemove.length) {
255 // Let's just resize one of the superfluous
256 // browsers and put it back into the map.
257 browser = toRemove.shift();
258 browser.resize(width, height);
259 } else {
260 // No more browsers to reuse, create a new one.
261 browser = new HiddenBrowser(width, height);
262 }
264 this._browsers.set(key, browser);
265 }
267 // Finally, remove all browsers we don't need anymore.
268 toRemove.forEach(b => b.destroy());
269 },
271 _collectTabBrowserSizes: function () {
272 let sizes = new Map();
274 function tabBrowserBounds() {
275 let wins = Services.ww.getWindowEnumerator("navigator:browser");
276 while (wins.hasMoreElements()) {
277 let win = wins.getNext();
278 if (win.gBrowser) {
279 let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
280 .getInterface(Ci.nsIDOMWindowUtils);
281 yield utils.getBoundsWithoutFlushing(win.gBrowser);
282 }
283 }
284 }
286 // Collect the sizes of all <tabbrowser>s out there.
287 for (let {width, height} of tabBrowserBounds()) {
288 if (width > 0 && height > 0) {
289 let key = width + "x" + height;
290 if (!sizes.has(key)) {
291 sizes.set(key, {width: width, height: height});
292 }
293 }
294 }
296 return sizes;
297 }
298 };
300 function HiddenBrowser(width, height) {
301 this.resize(width, height);
302 this._createBrowser();
303 }
305 HiddenBrowser.prototype = {
306 _width: null,
307 _height: null,
308 _timer: null,
310 get isPreloaded() {
311 return this._browser &&
312 this._browser.contentDocument &&
313 this._browser.contentDocument.readyState === "complete" &&
314 this._browser.currentURI.spec === NEWTAB_URL;
315 },
317 swapWithNewTab: function (aTab) {
318 if (!this.isPreloaded || this._timer) {
319 return false;
320 }
322 let win = aTab.ownerDocument.defaultView;
323 let tabbrowser = win.gBrowser;
325 if (!tabbrowser) {
326 return false;
327 }
329 // Swap docShells.
330 tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
332 // Load all default frame scripts attached to the target window.
333 let mm = aTab.linkedBrowser.messageManager;
334 let scripts = win.messageManager.getDelayedFrameScripts();
335 Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal));
337 // Remove the browser, it will be recreated by a timer.
338 this._removeBrowser();
340 // Start a timer that will kick off preloading the next newtab page.
341 this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
343 // Signal that we swapped docShells.
344 return true;
345 },
347 observe: function () {
348 this._timer = null;
350 // Start pre-loading the new tab page.
351 this._createBrowser();
352 },
354 resize: function (width, height) {
355 this._width = width;
356 this._height = height;
357 this._applySize();
358 },
360 destroy: function () {
361 this._removeBrowser();
362 this._timer = clearTimer(this._timer);
363 },
365 _applySize: function () {
366 if (this._browser) {
367 this._browser.style.width = this._width + "px";
368 this._browser.style.height = this._height + "px";
369 }
370 },
372 _createBrowser: function () {
373 HostFrame.get().then(aFrame => {
374 let doc = aFrame.document;
375 this._browser = doc.createElementNS(XUL_NS, "browser");
376 this._browser.setAttribute("type", "content");
377 this._browser.setAttribute("src", NEWTAB_URL);
378 this._applySize();
379 doc.getElementById("win").appendChild(this._browser);
380 });
381 },
383 _removeBrowser: function () {
384 if (this._browser) {
385 this._browser.remove();
386 this._browser = null;
387 }
388 }
389 };
391 let HostFrame = {
392 _frame: null,
393 _deferred: null,
395 get hiddenDOMDocument() {
396 return Services.appShell.hiddenDOMWindow.document;
397 },
399 get isReady() {
400 return this.hiddenDOMDocument.readyState === "complete";
401 },
403 get: function () {
404 if (!this._deferred) {
405 this._deferred = Promise.defer();
406 this._create();
407 }
409 return this._deferred.promise;
410 },
412 destroy: function () {
413 if (this._frame) {
414 if (!Cu.isDeadWrapper(this._frame)) {
415 this._frame.removeEventListener("load", this, true);
416 this._frame.remove();
417 }
419 this._frame = null;
420 this._deferred = null;
421 }
422 },
424 handleEvent: function () {
425 let contentWindow = this._frame.contentWindow;
426 if (contentWindow.location.href === XUL_PAGE) {
427 this._frame.removeEventListener("load", this, true);
428 this._deferred.resolve(contentWindow);
429 } else {
430 contentWindow.location = XUL_PAGE;
431 }
432 },
434 _create: function () {
435 if (this.isReady) {
436 let doc = this.hiddenDOMDocument;
437 this._frame = doc.createElementNS(HTML_NS, "iframe");
438 this._frame.addEventListener("load", this, true);
439 doc.documentElement.appendChild(this._frame);
440 } else {
441 let flags = Ci.nsIThread.DISPATCH_NORMAL;
442 Services.tm.currentThread.dispatch(() => this._create(), flags);
443 }
444 }
445 };