|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Cc = Components.classes; |
|
11 const Ci = Components.interfaces; |
|
12 |
|
13 Cu.import("resource://gre/modules/Services.jsm"); |
|
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
15 Cu.import("resource://gre/modules/Promise.jsm"); |
|
16 |
|
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."; |
|
22 |
|
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; |
|
33 |
|
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"; |
|
37 |
|
38 const FRAME_SCRIPT_URL = "chrome://browser/content/newtab/preloaderContent.js"; |
|
39 |
|
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 } |
|
45 |
|
46 function clearTimer(timer) { |
|
47 if (timer) { |
|
48 timer.cancel(); |
|
49 } |
|
50 return null; |
|
51 } |
|
52 |
|
53 this.BrowserNewTabPreloader = { |
|
54 init: function Preloader_init() { |
|
55 Initializer.start(); |
|
56 }, |
|
57 |
|
58 uninit: function Preloader_uninit() { |
|
59 Initializer.stop(); |
|
60 HostFrame.destroy(); |
|
61 Preferences.uninit(); |
|
62 HiddenBrowsers.uninit(); |
|
63 }, |
|
64 |
|
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); |
|
71 |
|
72 let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser); |
|
73 let hiddenBrowser = HiddenBrowsers.get(width, height) |
|
74 if (hiddenBrowser) { |
|
75 swapped = hiddenBrowser.swapWithNewTab(aTab); |
|
76 } |
|
77 |
|
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 } |
|
84 |
|
85 return swapped; |
|
86 } |
|
87 }; |
|
88 |
|
89 Object.freeze(BrowserNewTabPreloader); |
|
90 |
|
91 let Initializer = { |
|
92 _timer: null, |
|
93 _observing: false, |
|
94 |
|
95 start: function Initializer_start() { |
|
96 Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false); |
|
97 this._observing = true; |
|
98 }, |
|
99 |
|
100 stop: function Initializer_stop() { |
|
101 this._timer = clearTimer(this._timer); |
|
102 |
|
103 if (this._observing) { |
|
104 Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP); |
|
105 this._observing = false; |
|
106 } |
|
107 }, |
|
108 |
|
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 }, |
|
119 |
|
120 _startTimer: function Initializer_startTimer() { |
|
121 this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS); |
|
122 }, |
|
123 |
|
124 _startPreloader: function Initializer_startPreloader() { |
|
125 Preferences.init(); |
|
126 if (Preferences.enabled) { |
|
127 HiddenBrowsers.init(); |
|
128 } |
|
129 } |
|
130 }; |
|
131 |
|
132 let Preferences = { |
|
133 _enabled: null, |
|
134 _branch: null, |
|
135 |
|
136 get enabled() { |
|
137 if (this._enabled === null) { |
|
138 this._enabled = this._branch.getBoolPref("preload") && |
|
139 !this._branch.prefHasUserValue("url"); |
|
140 } |
|
141 |
|
142 return this._enabled; |
|
143 }, |
|
144 |
|
145 init: function Preferences_init() { |
|
146 this._branch = Services.prefs.getBranch(PREF_BRANCH); |
|
147 this._branch.addObserver("", this, false); |
|
148 }, |
|
149 |
|
150 uninit: function Preferences_uninit() { |
|
151 if (this._branch) { |
|
152 this._branch.removeObserver("", this); |
|
153 this._branch = null; |
|
154 } |
|
155 }, |
|
156 |
|
157 observe: function Preferences_observe() { |
|
158 let prevEnabled = this._enabled; |
|
159 this._enabled = null; |
|
160 |
|
161 if (prevEnabled && !this.enabled) { |
|
162 HiddenBrowsers.uninit(); |
|
163 } else if (!prevEnabled && this.enabled) { |
|
164 HiddenBrowsers.init(); |
|
165 } |
|
166 }, |
|
167 }; |
|
168 |
|
169 let HiddenBrowsers = { |
|
170 _browsers: null, |
|
171 _updateTimer: null, |
|
172 |
|
173 _topics: [ |
|
174 TOPIC_DELAYED_STARTUP, |
|
175 TOPIC_XUL_WINDOW_CLOSED |
|
176 ], |
|
177 |
|
178 init: function () { |
|
179 this._browsers = new Map(); |
|
180 this._updateBrowserSizes(); |
|
181 this._topics.forEach(t => Services.obs.addObserver(this, t, false)); |
|
182 }, |
|
183 |
|
184 uninit: function () { |
|
185 if (this._browsers) { |
|
186 this._topics.forEach(t => Services.obs.removeObserver(this, t, false)); |
|
187 this._updateTimer = clearTimer(this._updateTimer); |
|
188 |
|
189 for (let [key, browser] of this._browsers) { |
|
190 browser.destroy(); |
|
191 } |
|
192 this._browsers = null; |
|
193 } |
|
194 }, |
|
195 |
|
196 get: function (width, height) { |
|
197 // We haven't been initialized, yet. |
|
198 if (!this._browsers) { |
|
199 return null; |
|
200 } |
|
201 |
|
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 } |
|
207 |
|
208 // We should now have a matching browser. |
|
209 if (this._browsers.has(key)) { |
|
210 return this._browsers.get(key); |
|
211 } |
|
212 |
|
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 } |
|
218 |
|
219 // We should really never be here. |
|
220 Cu.reportError("NewTabPreloader: not even a single browser was found?"); |
|
221 return null; |
|
222 }, |
|
223 |
|
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 }, |
|
233 |
|
234 _updateBrowserSizes: function () { |
|
235 let sizes = this._collectTabBrowserSizes(); |
|
236 let toRemove = []; |
|
237 |
|
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 } |
|
250 |
|
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 } |
|
263 |
|
264 this._browsers.set(key, browser); |
|
265 } |
|
266 |
|
267 // Finally, remove all browsers we don't need anymore. |
|
268 toRemove.forEach(b => b.destroy()); |
|
269 }, |
|
270 |
|
271 _collectTabBrowserSizes: function () { |
|
272 let sizes = new Map(); |
|
273 |
|
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 } |
|
285 |
|
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 } |
|
295 |
|
296 return sizes; |
|
297 } |
|
298 }; |
|
299 |
|
300 function HiddenBrowser(width, height) { |
|
301 this.resize(width, height); |
|
302 this._createBrowser(); |
|
303 } |
|
304 |
|
305 HiddenBrowser.prototype = { |
|
306 _width: null, |
|
307 _height: null, |
|
308 _timer: null, |
|
309 |
|
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 }, |
|
316 |
|
317 swapWithNewTab: function (aTab) { |
|
318 if (!this.isPreloaded || this._timer) { |
|
319 return false; |
|
320 } |
|
321 |
|
322 let win = aTab.ownerDocument.defaultView; |
|
323 let tabbrowser = win.gBrowser; |
|
324 |
|
325 if (!tabbrowser) { |
|
326 return false; |
|
327 } |
|
328 |
|
329 // Swap docShells. |
|
330 tabbrowser.swapNewTabWithBrowser(aTab, this._browser); |
|
331 |
|
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)); |
|
336 |
|
337 // Remove the browser, it will be recreated by a timer. |
|
338 this._removeBrowser(); |
|
339 |
|
340 // Start a timer that will kick off preloading the next newtab page. |
|
341 this._timer = createTimer(this, PRELOADER_INTERVAL_MS); |
|
342 |
|
343 // Signal that we swapped docShells. |
|
344 return true; |
|
345 }, |
|
346 |
|
347 observe: function () { |
|
348 this._timer = null; |
|
349 |
|
350 // Start pre-loading the new tab page. |
|
351 this._createBrowser(); |
|
352 }, |
|
353 |
|
354 resize: function (width, height) { |
|
355 this._width = width; |
|
356 this._height = height; |
|
357 this._applySize(); |
|
358 }, |
|
359 |
|
360 destroy: function () { |
|
361 this._removeBrowser(); |
|
362 this._timer = clearTimer(this._timer); |
|
363 }, |
|
364 |
|
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 }, |
|
371 |
|
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 }, |
|
382 |
|
383 _removeBrowser: function () { |
|
384 if (this._browser) { |
|
385 this._browser.remove(); |
|
386 this._browser = null; |
|
387 } |
|
388 } |
|
389 }; |
|
390 |
|
391 let HostFrame = { |
|
392 _frame: null, |
|
393 _deferred: null, |
|
394 |
|
395 get hiddenDOMDocument() { |
|
396 return Services.appShell.hiddenDOMWindow.document; |
|
397 }, |
|
398 |
|
399 get isReady() { |
|
400 return this.hiddenDOMDocument.readyState === "complete"; |
|
401 }, |
|
402 |
|
403 get: function () { |
|
404 if (!this._deferred) { |
|
405 this._deferred = Promise.defer(); |
|
406 this._create(); |
|
407 } |
|
408 |
|
409 return this._deferred.promise; |
|
410 }, |
|
411 |
|
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 } |
|
418 |
|
419 this._frame = null; |
|
420 this._deferred = null; |
|
421 } |
|
422 }, |
|
423 |
|
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 }, |
|
433 |
|
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 }; |