1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/modules/BrowserNewTabPreloader.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,445 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"]; 1.11 + 1.12 +const Cu = Components.utils; 1.13 +const Cc = Components.classes; 1.14 +const Ci = Components.interfaces; 1.15 + 1.16 +Cu.import("resource://gre/modules/Services.jsm"); 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 +Cu.import("resource://gre/modules/Promise.jsm"); 1.19 + 1.20 +const HTML_NS = "http://www.w3.org/1999/xhtml"; 1.21 +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 1.22 +const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>"; 1.23 +const NEWTAB_URL = "about:newtab"; 1.24 +const PREF_BRANCH = "browser.newtab."; 1.25 + 1.26 +// The interval between swapping in a preload docShell and kicking off the 1.27 +// next preload in the background. 1.28 +const PRELOADER_INTERVAL_MS = 600; 1.29 +// The initial delay before we start preloading our first new tab page. The 1.30 +// timer is started after the first 'browser-delayed-startup' has been sent. 1.31 +const PRELOADER_INIT_DELAY_MS = 5000; 1.32 +// The number of miliseconds we'll wait after we received a notification that 1.33 +// causes us to update our list of browsers and tabbrowser sizes. This acts as 1.34 +// kind of a damper when too many events are occuring in quick succession. 1.35 +const PRELOADER_UPDATE_DELAY_MS = 3000; 1.36 + 1.37 +const TOPIC_TIMER_CALLBACK = "timer-callback"; 1.38 +const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished"; 1.39 +const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed"; 1.40 + 1.41 +const FRAME_SCRIPT_URL = "chrome://browser/content/newtab/preloaderContent.js"; 1.42 + 1.43 +function createTimer(obj, delay) { 1.44 + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.45 + timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT); 1.46 + return timer; 1.47 +} 1.48 + 1.49 +function clearTimer(timer) { 1.50 + if (timer) { 1.51 + timer.cancel(); 1.52 + } 1.53 + return null; 1.54 +} 1.55 + 1.56 +this.BrowserNewTabPreloader = { 1.57 + init: function Preloader_init() { 1.58 + Initializer.start(); 1.59 + }, 1.60 + 1.61 + uninit: function Preloader_uninit() { 1.62 + Initializer.stop(); 1.63 + HostFrame.destroy(); 1.64 + Preferences.uninit(); 1.65 + HiddenBrowsers.uninit(); 1.66 + }, 1.67 + 1.68 + newTab: function Preloader_newTab(aTab) { 1.69 + let swapped = false; 1.70 + let win = aTab.ownerDocument.defaultView; 1.71 + if (win.gBrowser) { 1.72 + let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.73 + .getInterface(Ci.nsIDOMWindowUtils); 1.74 + 1.75 + let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser); 1.76 + let hiddenBrowser = HiddenBrowsers.get(width, height) 1.77 + if (hiddenBrowser) { 1.78 + swapped = hiddenBrowser.swapWithNewTab(aTab); 1.79 + } 1.80 + 1.81 + // aTab's browser is now visible and is therefore allowed to make 1.82 + // background captures. 1.83 + let msgMan = aTab.linkedBrowser.messageManager; 1.84 + msgMan.loadFrameScript(FRAME_SCRIPT_URL, false); 1.85 + msgMan.sendAsyncMessage("BrowserNewTabPreloader:allowBackgroundCaptures"); 1.86 + } 1.87 + 1.88 + return swapped; 1.89 + } 1.90 +}; 1.91 + 1.92 +Object.freeze(BrowserNewTabPreloader); 1.93 + 1.94 +let Initializer = { 1.95 + _timer: null, 1.96 + _observing: false, 1.97 + 1.98 + start: function Initializer_start() { 1.99 + Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false); 1.100 + this._observing = true; 1.101 + }, 1.102 + 1.103 + stop: function Initializer_stop() { 1.104 + this._timer = clearTimer(this._timer); 1.105 + 1.106 + if (this._observing) { 1.107 + Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP); 1.108 + this._observing = false; 1.109 + } 1.110 + }, 1.111 + 1.112 + observe: function Initializer_observe(aSubject, aTopic, aData) { 1.113 + if (aTopic == TOPIC_DELAYED_STARTUP) { 1.114 + Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP); 1.115 + this._observing = false; 1.116 + this._startTimer(); 1.117 + } else if (aTopic == TOPIC_TIMER_CALLBACK) { 1.118 + this._timer = null; 1.119 + this._startPreloader(); 1.120 + } 1.121 + }, 1.122 + 1.123 + _startTimer: function Initializer_startTimer() { 1.124 + this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS); 1.125 + }, 1.126 + 1.127 + _startPreloader: function Initializer_startPreloader() { 1.128 + Preferences.init(); 1.129 + if (Preferences.enabled) { 1.130 + HiddenBrowsers.init(); 1.131 + } 1.132 + } 1.133 +}; 1.134 + 1.135 +let Preferences = { 1.136 + _enabled: null, 1.137 + _branch: null, 1.138 + 1.139 + get enabled() { 1.140 + if (this._enabled === null) { 1.141 + this._enabled = this._branch.getBoolPref("preload") && 1.142 + !this._branch.prefHasUserValue("url"); 1.143 + } 1.144 + 1.145 + return this._enabled; 1.146 + }, 1.147 + 1.148 + init: function Preferences_init() { 1.149 + this._branch = Services.prefs.getBranch(PREF_BRANCH); 1.150 + this._branch.addObserver("", this, false); 1.151 + }, 1.152 + 1.153 + uninit: function Preferences_uninit() { 1.154 + if (this._branch) { 1.155 + this._branch.removeObserver("", this); 1.156 + this._branch = null; 1.157 + } 1.158 + }, 1.159 + 1.160 + observe: function Preferences_observe() { 1.161 + let prevEnabled = this._enabled; 1.162 + this._enabled = null; 1.163 + 1.164 + if (prevEnabled && !this.enabled) { 1.165 + HiddenBrowsers.uninit(); 1.166 + } else if (!prevEnabled && this.enabled) { 1.167 + HiddenBrowsers.init(); 1.168 + } 1.169 + }, 1.170 +}; 1.171 + 1.172 +let HiddenBrowsers = { 1.173 + _browsers: null, 1.174 + _updateTimer: null, 1.175 + 1.176 + _topics: [ 1.177 + TOPIC_DELAYED_STARTUP, 1.178 + TOPIC_XUL_WINDOW_CLOSED 1.179 + ], 1.180 + 1.181 + init: function () { 1.182 + this._browsers = new Map(); 1.183 + this._updateBrowserSizes(); 1.184 + this._topics.forEach(t => Services.obs.addObserver(this, t, false)); 1.185 + }, 1.186 + 1.187 + uninit: function () { 1.188 + if (this._browsers) { 1.189 + this._topics.forEach(t => Services.obs.removeObserver(this, t, false)); 1.190 + this._updateTimer = clearTimer(this._updateTimer); 1.191 + 1.192 + for (let [key, browser] of this._browsers) { 1.193 + browser.destroy(); 1.194 + } 1.195 + this._browsers = null; 1.196 + } 1.197 + }, 1.198 + 1.199 + get: function (width, height) { 1.200 + // We haven't been initialized, yet. 1.201 + if (!this._browsers) { 1.202 + return null; 1.203 + } 1.204 + 1.205 + let key = width + "x" + height; 1.206 + if (!this._browsers.has(key)) { 1.207 + // Update all browsers' sizes if we can't find a matching one. 1.208 + this._updateBrowserSizes(); 1.209 + } 1.210 + 1.211 + // We should now have a matching browser. 1.212 + if (this._browsers.has(key)) { 1.213 + return this._browsers.get(key); 1.214 + } 1.215 + 1.216 + // We should never be here. Return the first browser we find. 1.217 + Cu.reportError("NewTabPreloader: no matching browser found after updating"); 1.218 + for (let [size, browser] of this._browsers) { 1.219 + return browser; 1.220 + } 1.221 + 1.222 + // We should really never be here. 1.223 + Cu.reportError("NewTabPreloader: not even a single browser was found?"); 1.224 + return null; 1.225 + }, 1.226 + 1.227 + observe: function (subject, topic, data) { 1.228 + if (topic === TOPIC_TIMER_CALLBACK) { 1.229 + this._updateTimer = null; 1.230 + this._updateBrowserSizes(); 1.231 + } else { 1.232 + this._updateTimer = clearTimer(this._updateTimer); 1.233 + this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS); 1.234 + } 1.235 + }, 1.236 + 1.237 + _updateBrowserSizes: function () { 1.238 + let sizes = this._collectTabBrowserSizes(); 1.239 + let toRemove = []; 1.240 + 1.241 + // Iterate all browsers and check that they 1.242 + // each can be assigned to one of the sizes. 1.243 + for (let [key, browser] of this._browsers) { 1.244 + if (sizes.has(key)) { 1.245 + // We already have a browser for that size, great! 1.246 + sizes.delete(key); 1.247 + } else { 1.248 + // This browser is superfluous or needs to be resized. 1.249 + toRemove.push(browser); 1.250 + this._browsers.delete(key); 1.251 + } 1.252 + } 1.253 + 1.254 + // Iterate all sizes that we couldn't find a browser for. 1.255 + for (let [key, {width, height}] of sizes) { 1.256 + let browser; 1.257 + if (toRemove.length) { 1.258 + // Let's just resize one of the superfluous 1.259 + // browsers and put it back into the map. 1.260 + browser = toRemove.shift(); 1.261 + browser.resize(width, height); 1.262 + } else { 1.263 + // No more browsers to reuse, create a new one. 1.264 + browser = new HiddenBrowser(width, height); 1.265 + } 1.266 + 1.267 + this._browsers.set(key, browser); 1.268 + } 1.269 + 1.270 + // Finally, remove all browsers we don't need anymore. 1.271 + toRemove.forEach(b => b.destroy()); 1.272 + }, 1.273 + 1.274 + _collectTabBrowserSizes: function () { 1.275 + let sizes = new Map(); 1.276 + 1.277 + function tabBrowserBounds() { 1.278 + let wins = Services.ww.getWindowEnumerator("navigator:browser"); 1.279 + while (wins.hasMoreElements()) { 1.280 + let win = wins.getNext(); 1.281 + if (win.gBrowser) { 1.282 + let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.283 + .getInterface(Ci.nsIDOMWindowUtils); 1.284 + yield utils.getBoundsWithoutFlushing(win.gBrowser); 1.285 + } 1.286 + } 1.287 + } 1.288 + 1.289 + // Collect the sizes of all <tabbrowser>s out there. 1.290 + for (let {width, height} of tabBrowserBounds()) { 1.291 + if (width > 0 && height > 0) { 1.292 + let key = width + "x" + height; 1.293 + if (!sizes.has(key)) { 1.294 + sizes.set(key, {width: width, height: height}); 1.295 + } 1.296 + } 1.297 + } 1.298 + 1.299 + return sizes; 1.300 + } 1.301 +}; 1.302 + 1.303 +function HiddenBrowser(width, height) { 1.304 + this.resize(width, height); 1.305 + this._createBrowser(); 1.306 +} 1.307 + 1.308 +HiddenBrowser.prototype = { 1.309 + _width: null, 1.310 + _height: null, 1.311 + _timer: null, 1.312 + 1.313 + get isPreloaded() { 1.314 + return this._browser && 1.315 + this._browser.contentDocument && 1.316 + this._browser.contentDocument.readyState === "complete" && 1.317 + this._browser.currentURI.spec === NEWTAB_URL; 1.318 + }, 1.319 + 1.320 + swapWithNewTab: function (aTab) { 1.321 + if (!this.isPreloaded || this._timer) { 1.322 + return false; 1.323 + } 1.324 + 1.325 + let win = aTab.ownerDocument.defaultView; 1.326 + let tabbrowser = win.gBrowser; 1.327 + 1.328 + if (!tabbrowser) { 1.329 + return false; 1.330 + } 1.331 + 1.332 + // Swap docShells. 1.333 + tabbrowser.swapNewTabWithBrowser(aTab, this._browser); 1.334 + 1.335 + // Load all default frame scripts attached to the target window. 1.336 + let mm = aTab.linkedBrowser.messageManager; 1.337 + let scripts = win.messageManager.getDelayedFrameScripts(); 1.338 + Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal)); 1.339 + 1.340 + // Remove the browser, it will be recreated by a timer. 1.341 + this._removeBrowser(); 1.342 + 1.343 + // Start a timer that will kick off preloading the next newtab page. 1.344 + this._timer = createTimer(this, PRELOADER_INTERVAL_MS); 1.345 + 1.346 + // Signal that we swapped docShells. 1.347 + return true; 1.348 + }, 1.349 + 1.350 + observe: function () { 1.351 + this._timer = null; 1.352 + 1.353 + // Start pre-loading the new tab page. 1.354 + this._createBrowser(); 1.355 + }, 1.356 + 1.357 + resize: function (width, height) { 1.358 + this._width = width; 1.359 + this._height = height; 1.360 + this._applySize(); 1.361 + }, 1.362 + 1.363 + destroy: function () { 1.364 + this._removeBrowser(); 1.365 + this._timer = clearTimer(this._timer); 1.366 + }, 1.367 + 1.368 + _applySize: function () { 1.369 + if (this._browser) { 1.370 + this._browser.style.width = this._width + "px"; 1.371 + this._browser.style.height = this._height + "px"; 1.372 + } 1.373 + }, 1.374 + 1.375 + _createBrowser: function () { 1.376 + HostFrame.get().then(aFrame => { 1.377 + let doc = aFrame.document; 1.378 + this._browser = doc.createElementNS(XUL_NS, "browser"); 1.379 + this._browser.setAttribute("type", "content"); 1.380 + this._browser.setAttribute("src", NEWTAB_URL); 1.381 + this._applySize(); 1.382 + doc.getElementById("win").appendChild(this._browser); 1.383 + }); 1.384 + }, 1.385 + 1.386 + _removeBrowser: function () { 1.387 + if (this._browser) { 1.388 + this._browser.remove(); 1.389 + this._browser = null; 1.390 + } 1.391 + } 1.392 +}; 1.393 + 1.394 +let HostFrame = { 1.395 + _frame: null, 1.396 + _deferred: null, 1.397 + 1.398 + get hiddenDOMDocument() { 1.399 + return Services.appShell.hiddenDOMWindow.document; 1.400 + }, 1.401 + 1.402 + get isReady() { 1.403 + return this.hiddenDOMDocument.readyState === "complete"; 1.404 + }, 1.405 + 1.406 + get: function () { 1.407 + if (!this._deferred) { 1.408 + this._deferred = Promise.defer(); 1.409 + this._create(); 1.410 + } 1.411 + 1.412 + return this._deferred.promise; 1.413 + }, 1.414 + 1.415 + destroy: function () { 1.416 + if (this._frame) { 1.417 + if (!Cu.isDeadWrapper(this._frame)) { 1.418 + this._frame.removeEventListener("load", this, true); 1.419 + this._frame.remove(); 1.420 + } 1.421 + 1.422 + this._frame = null; 1.423 + this._deferred = null; 1.424 + } 1.425 + }, 1.426 + 1.427 + handleEvent: function () { 1.428 + let contentWindow = this._frame.contentWindow; 1.429 + if (contentWindow.location.href === XUL_PAGE) { 1.430 + this._frame.removeEventListener("load", this, true); 1.431 + this._deferred.resolve(contentWindow); 1.432 + } else { 1.433 + contentWindow.location = XUL_PAGE; 1.434 + } 1.435 + }, 1.436 + 1.437 + _create: function () { 1.438 + if (this.isReady) { 1.439 + let doc = this.hiddenDOMDocument; 1.440 + this._frame = doc.createElementNS(HTML_NS, "iframe"); 1.441 + this._frame.addEventListener("load", this, true); 1.442 + doc.documentElement.appendChild(this._frame); 1.443 + } else { 1.444 + let flags = Ci.nsIThread.DISPATCH_NORMAL; 1.445 + Services.tm.currentThread.dispatch(() => this._create(), flags); 1.446 + } 1.447 + } 1.448 +};