browser/modules/BrowserNewTabPreloader.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"];
michael@0 8
michael@0 9 const Cu = Components.utils;
michael@0 10 const Cc = Components.classes;
michael@0 11 const Ci = Components.interfaces;
michael@0 12
michael@0 13 Cu.import("resource://gre/modules/Services.jsm");
michael@0 14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 15 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 16
michael@0 17 const HTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 18 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
michael@0 19 const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
michael@0 20 const NEWTAB_URL = "about:newtab";
michael@0 21 const PREF_BRANCH = "browser.newtab.";
michael@0 22
michael@0 23 // The interval between swapping in a preload docShell and kicking off the
michael@0 24 // next preload in the background.
michael@0 25 const PRELOADER_INTERVAL_MS = 600;
michael@0 26 // The initial delay before we start preloading our first new tab page. The
michael@0 27 // timer is started after the first 'browser-delayed-startup' has been sent.
michael@0 28 const PRELOADER_INIT_DELAY_MS = 5000;
michael@0 29 // The number of miliseconds we'll wait after we received a notification that
michael@0 30 // causes us to update our list of browsers and tabbrowser sizes. This acts as
michael@0 31 // kind of a damper when too many events are occuring in quick succession.
michael@0 32 const PRELOADER_UPDATE_DELAY_MS = 3000;
michael@0 33
michael@0 34 const TOPIC_TIMER_CALLBACK = "timer-callback";
michael@0 35 const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished";
michael@0 36 const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed";
michael@0 37
michael@0 38 const FRAME_SCRIPT_URL = "chrome://browser/content/newtab/preloaderContent.js";
michael@0 39
michael@0 40 function createTimer(obj, delay) {
michael@0 41 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 42 timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT);
michael@0 43 return timer;
michael@0 44 }
michael@0 45
michael@0 46 function clearTimer(timer) {
michael@0 47 if (timer) {
michael@0 48 timer.cancel();
michael@0 49 }
michael@0 50 return null;
michael@0 51 }
michael@0 52
michael@0 53 this.BrowserNewTabPreloader = {
michael@0 54 init: function Preloader_init() {
michael@0 55 Initializer.start();
michael@0 56 },
michael@0 57
michael@0 58 uninit: function Preloader_uninit() {
michael@0 59 Initializer.stop();
michael@0 60 HostFrame.destroy();
michael@0 61 Preferences.uninit();
michael@0 62 HiddenBrowsers.uninit();
michael@0 63 },
michael@0 64
michael@0 65 newTab: function Preloader_newTab(aTab) {
michael@0 66 let swapped = false;
michael@0 67 let win = aTab.ownerDocument.defaultView;
michael@0 68 if (win.gBrowser) {
michael@0 69 let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 70 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 71
michael@0 72 let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
michael@0 73 let hiddenBrowser = HiddenBrowsers.get(width, height)
michael@0 74 if (hiddenBrowser) {
michael@0 75 swapped = hiddenBrowser.swapWithNewTab(aTab);
michael@0 76 }
michael@0 77
michael@0 78 // aTab's browser is now visible and is therefore allowed to make
michael@0 79 // background captures.
michael@0 80 let msgMan = aTab.linkedBrowser.messageManager;
michael@0 81 msgMan.loadFrameScript(FRAME_SCRIPT_URL, false);
michael@0 82 msgMan.sendAsyncMessage("BrowserNewTabPreloader:allowBackgroundCaptures");
michael@0 83 }
michael@0 84
michael@0 85 return swapped;
michael@0 86 }
michael@0 87 };
michael@0 88
michael@0 89 Object.freeze(BrowserNewTabPreloader);
michael@0 90
michael@0 91 let Initializer = {
michael@0 92 _timer: null,
michael@0 93 _observing: false,
michael@0 94
michael@0 95 start: function Initializer_start() {
michael@0 96 Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
michael@0 97 this._observing = true;
michael@0 98 },
michael@0 99
michael@0 100 stop: function Initializer_stop() {
michael@0 101 this._timer = clearTimer(this._timer);
michael@0 102
michael@0 103 if (this._observing) {
michael@0 104 Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
michael@0 105 this._observing = false;
michael@0 106 }
michael@0 107 },
michael@0 108
michael@0 109 observe: function Initializer_observe(aSubject, aTopic, aData) {
michael@0 110 if (aTopic == TOPIC_DELAYED_STARTUP) {
michael@0 111 Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
michael@0 112 this._observing = false;
michael@0 113 this._startTimer();
michael@0 114 } else if (aTopic == TOPIC_TIMER_CALLBACK) {
michael@0 115 this._timer = null;
michael@0 116 this._startPreloader();
michael@0 117 }
michael@0 118 },
michael@0 119
michael@0 120 _startTimer: function Initializer_startTimer() {
michael@0 121 this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
michael@0 122 },
michael@0 123
michael@0 124 _startPreloader: function Initializer_startPreloader() {
michael@0 125 Preferences.init();
michael@0 126 if (Preferences.enabled) {
michael@0 127 HiddenBrowsers.init();
michael@0 128 }
michael@0 129 }
michael@0 130 };
michael@0 131
michael@0 132 let Preferences = {
michael@0 133 _enabled: null,
michael@0 134 _branch: null,
michael@0 135
michael@0 136 get enabled() {
michael@0 137 if (this._enabled === null) {
michael@0 138 this._enabled = this._branch.getBoolPref("preload") &&
michael@0 139 !this._branch.prefHasUserValue("url");
michael@0 140 }
michael@0 141
michael@0 142 return this._enabled;
michael@0 143 },
michael@0 144
michael@0 145 init: function Preferences_init() {
michael@0 146 this._branch = Services.prefs.getBranch(PREF_BRANCH);
michael@0 147 this._branch.addObserver("", this, false);
michael@0 148 },
michael@0 149
michael@0 150 uninit: function Preferences_uninit() {
michael@0 151 if (this._branch) {
michael@0 152 this._branch.removeObserver("", this);
michael@0 153 this._branch = null;
michael@0 154 }
michael@0 155 },
michael@0 156
michael@0 157 observe: function Preferences_observe() {
michael@0 158 let prevEnabled = this._enabled;
michael@0 159 this._enabled = null;
michael@0 160
michael@0 161 if (prevEnabled && !this.enabled) {
michael@0 162 HiddenBrowsers.uninit();
michael@0 163 } else if (!prevEnabled && this.enabled) {
michael@0 164 HiddenBrowsers.init();
michael@0 165 }
michael@0 166 },
michael@0 167 };
michael@0 168
michael@0 169 let HiddenBrowsers = {
michael@0 170 _browsers: null,
michael@0 171 _updateTimer: null,
michael@0 172
michael@0 173 _topics: [
michael@0 174 TOPIC_DELAYED_STARTUP,
michael@0 175 TOPIC_XUL_WINDOW_CLOSED
michael@0 176 ],
michael@0 177
michael@0 178 init: function () {
michael@0 179 this._browsers = new Map();
michael@0 180 this._updateBrowserSizes();
michael@0 181 this._topics.forEach(t => Services.obs.addObserver(this, t, false));
michael@0 182 },
michael@0 183
michael@0 184 uninit: function () {
michael@0 185 if (this._browsers) {
michael@0 186 this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
michael@0 187 this._updateTimer = clearTimer(this._updateTimer);
michael@0 188
michael@0 189 for (let [key, browser] of this._browsers) {
michael@0 190 browser.destroy();
michael@0 191 }
michael@0 192 this._browsers = null;
michael@0 193 }
michael@0 194 },
michael@0 195
michael@0 196 get: function (width, height) {
michael@0 197 // We haven't been initialized, yet.
michael@0 198 if (!this._browsers) {
michael@0 199 return null;
michael@0 200 }
michael@0 201
michael@0 202 let key = width + "x" + height;
michael@0 203 if (!this._browsers.has(key)) {
michael@0 204 // Update all browsers' sizes if we can't find a matching one.
michael@0 205 this._updateBrowserSizes();
michael@0 206 }
michael@0 207
michael@0 208 // We should now have a matching browser.
michael@0 209 if (this._browsers.has(key)) {
michael@0 210 return this._browsers.get(key);
michael@0 211 }
michael@0 212
michael@0 213 // We should never be here. Return the first browser we find.
michael@0 214 Cu.reportError("NewTabPreloader: no matching browser found after updating");
michael@0 215 for (let [size, browser] of this._browsers) {
michael@0 216 return browser;
michael@0 217 }
michael@0 218
michael@0 219 // We should really never be here.
michael@0 220 Cu.reportError("NewTabPreloader: not even a single browser was found?");
michael@0 221 return null;
michael@0 222 },
michael@0 223
michael@0 224 observe: function (subject, topic, data) {
michael@0 225 if (topic === TOPIC_TIMER_CALLBACK) {
michael@0 226 this._updateTimer = null;
michael@0 227 this._updateBrowserSizes();
michael@0 228 } else {
michael@0 229 this._updateTimer = clearTimer(this._updateTimer);
michael@0 230 this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
michael@0 231 }
michael@0 232 },
michael@0 233
michael@0 234 _updateBrowserSizes: function () {
michael@0 235 let sizes = this._collectTabBrowserSizes();
michael@0 236 let toRemove = [];
michael@0 237
michael@0 238 // Iterate all browsers and check that they
michael@0 239 // each can be assigned to one of the sizes.
michael@0 240 for (let [key, browser] of this._browsers) {
michael@0 241 if (sizes.has(key)) {
michael@0 242 // We already have a browser for that size, great!
michael@0 243 sizes.delete(key);
michael@0 244 } else {
michael@0 245 // This browser is superfluous or needs to be resized.
michael@0 246 toRemove.push(browser);
michael@0 247 this._browsers.delete(key);
michael@0 248 }
michael@0 249 }
michael@0 250
michael@0 251 // Iterate all sizes that we couldn't find a browser for.
michael@0 252 for (let [key, {width, height}] of sizes) {
michael@0 253 let browser;
michael@0 254 if (toRemove.length) {
michael@0 255 // Let's just resize one of the superfluous
michael@0 256 // browsers and put it back into the map.
michael@0 257 browser = toRemove.shift();
michael@0 258 browser.resize(width, height);
michael@0 259 } else {
michael@0 260 // No more browsers to reuse, create a new one.
michael@0 261 browser = new HiddenBrowser(width, height);
michael@0 262 }
michael@0 263
michael@0 264 this._browsers.set(key, browser);
michael@0 265 }
michael@0 266
michael@0 267 // Finally, remove all browsers we don't need anymore.
michael@0 268 toRemove.forEach(b => b.destroy());
michael@0 269 },
michael@0 270
michael@0 271 _collectTabBrowserSizes: function () {
michael@0 272 let sizes = new Map();
michael@0 273
michael@0 274 function tabBrowserBounds() {
michael@0 275 let wins = Services.ww.getWindowEnumerator("navigator:browser");
michael@0 276 while (wins.hasMoreElements()) {
michael@0 277 let win = wins.getNext();
michael@0 278 if (win.gBrowser) {
michael@0 279 let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 280 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 281 yield utils.getBoundsWithoutFlushing(win.gBrowser);
michael@0 282 }
michael@0 283 }
michael@0 284 }
michael@0 285
michael@0 286 // Collect the sizes of all <tabbrowser>s out there.
michael@0 287 for (let {width, height} of tabBrowserBounds()) {
michael@0 288 if (width > 0 && height > 0) {
michael@0 289 let key = width + "x" + height;
michael@0 290 if (!sizes.has(key)) {
michael@0 291 sizes.set(key, {width: width, height: height});
michael@0 292 }
michael@0 293 }
michael@0 294 }
michael@0 295
michael@0 296 return sizes;
michael@0 297 }
michael@0 298 };
michael@0 299
michael@0 300 function HiddenBrowser(width, height) {
michael@0 301 this.resize(width, height);
michael@0 302 this._createBrowser();
michael@0 303 }
michael@0 304
michael@0 305 HiddenBrowser.prototype = {
michael@0 306 _width: null,
michael@0 307 _height: null,
michael@0 308 _timer: null,
michael@0 309
michael@0 310 get isPreloaded() {
michael@0 311 return this._browser &&
michael@0 312 this._browser.contentDocument &&
michael@0 313 this._browser.contentDocument.readyState === "complete" &&
michael@0 314 this._browser.currentURI.spec === NEWTAB_URL;
michael@0 315 },
michael@0 316
michael@0 317 swapWithNewTab: function (aTab) {
michael@0 318 if (!this.isPreloaded || this._timer) {
michael@0 319 return false;
michael@0 320 }
michael@0 321
michael@0 322 let win = aTab.ownerDocument.defaultView;
michael@0 323 let tabbrowser = win.gBrowser;
michael@0 324
michael@0 325 if (!tabbrowser) {
michael@0 326 return false;
michael@0 327 }
michael@0 328
michael@0 329 // Swap docShells.
michael@0 330 tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
michael@0 331
michael@0 332 // Load all default frame scripts attached to the target window.
michael@0 333 let mm = aTab.linkedBrowser.messageManager;
michael@0 334 let scripts = win.messageManager.getDelayedFrameScripts();
michael@0 335 Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal));
michael@0 336
michael@0 337 // Remove the browser, it will be recreated by a timer.
michael@0 338 this._removeBrowser();
michael@0 339
michael@0 340 // Start a timer that will kick off preloading the next newtab page.
michael@0 341 this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
michael@0 342
michael@0 343 // Signal that we swapped docShells.
michael@0 344 return true;
michael@0 345 },
michael@0 346
michael@0 347 observe: function () {
michael@0 348 this._timer = null;
michael@0 349
michael@0 350 // Start pre-loading the new tab page.
michael@0 351 this._createBrowser();
michael@0 352 },
michael@0 353
michael@0 354 resize: function (width, height) {
michael@0 355 this._width = width;
michael@0 356 this._height = height;
michael@0 357 this._applySize();
michael@0 358 },
michael@0 359
michael@0 360 destroy: function () {
michael@0 361 this._removeBrowser();
michael@0 362 this._timer = clearTimer(this._timer);
michael@0 363 },
michael@0 364
michael@0 365 _applySize: function () {
michael@0 366 if (this._browser) {
michael@0 367 this._browser.style.width = this._width + "px";
michael@0 368 this._browser.style.height = this._height + "px";
michael@0 369 }
michael@0 370 },
michael@0 371
michael@0 372 _createBrowser: function () {
michael@0 373 HostFrame.get().then(aFrame => {
michael@0 374 let doc = aFrame.document;
michael@0 375 this._browser = doc.createElementNS(XUL_NS, "browser");
michael@0 376 this._browser.setAttribute("type", "content");
michael@0 377 this._browser.setAttribute("src", NEWTAB_URL);
michael@0 378 this._applySize();
michael@0 379 doc.getElementById("win").appendChild(this._browser);
michael@0 380 });
michael@0 381 },
michael@0 382
michael@0 383 _removeBrowser: function () {
michael@0 384 if (this._browser) {
michael@0 385 this._browser.remove();
michael@0 386 this._browser = null;
michael@0 387 }
michael@0 388 }
michael@0 389 };
michael@0 390
michael@0 391 let HostFrame = {
michael@0 392 _frame: null,
michael@0 393 _deferred: null,
michael@0 394
michael@0 395 get hiddenDOMDocument() {
michael@0 396 return Services.appShell.hiddenDOMWindow.document;
michael@0 397 },
michael@0 398
michael@0 399 get isReady() {
michael@0 400 return this.hiddenDOMDocument.readyState === "complete";
michael@0 401 },
michael@0 402
michael@0 403 get: function () {
michael@0 404 if (!this._deferred) {
michael@0 405 this._deferred = Promise.defer();
michael@0 406 this._create();
michael@0 407 }
michael@0 408
michael@0 409 return this._deferred.promise;
michael@0 410 },
michael@0 411
michael@0 412 destroy: function () {
michael@0 413 if (this._frame) {
michael@0 414 if (!Cu.isDeadWrapper(this._frame)) {
michael@0 415 this._frame.removeEventListener("load", this, true);
michael@0 416 this._frame.remove();
michael@0 417 }
michael@0 418
michael@0 419 this._frame = null;
michael@0 420 this._deferred = null;
michael@0 421 }
michael@0 422 },
michael@0 423
michael@0 424 handleEvent: function () {
michael@0 425 let contentWindow = this._frame.contentWindow;
michael@0 426 if (contentWindow.location.href === XUL_PAGE) {
michael@0 427 this._frame.removeEventListener("load", this, true);
michael@0 428 this._deferred.resolve(contentWindow);
michael@0 429 } else {
michael@0 430 contentWindow.location = XUL_PAGE;
michael@0 431 }
michael@0 432 },
michael@0 433
michael@0 434 _create: function () {
michael@0 435 if (this.isReady) {
michael@0 436 let doc = this.hiddenDOMDocument;
michael@0 437 this._frame = doc.createElementNS(HTML_NS, "iframe");
michael@0 438 this._frame.addEventListener("load", this, true);
michael@0 439 doc.documentElement.appendChild(this._frame);
michael@0 440 } else {
michael@0 441 let flags = Ci.nsIThread.DISPATCH_NORMAL;
michael@0 442 Services.tm.currentThread.dispatch(() => this._create(), flags);
michael@0 443 }
michael@0 444 }
michael@0 445 };

mercurial