Bug 1353013 - Schedule preloading about:newtab via idleCallback. r?florian draft
authorMike Conley <mconley@mozilla.com>
Wed, 28 Feb 2018 17:59:10 -0500
changeset 774432 a3786b7c424a4a1974be9f785bed695d9470959d
parent 774431 4f1ec690d43324003f854624e43f6c743f095196
push id104397
push usermconley@mozilla.com
push dateWed, 28 Mar 2018 21:50:30 +0000
reviewersflorian
bugs1353013
milestone61.0a1
Bug 1353013 - Schedule preloading about:newtab via idleCallback. r?florian Based on a patch that Dão Gottwald <dao+bmo@mozilla.com> wrote. We used to preload about:newtab as soon as a tab had finished being opened, which meant that the first opened tab was _never_ preloaded, and that we risked janking the browser immediately after the user opened a new tab (which is, arguably, the worst time to do it, since the user is probably about to navigate that tab somewhere). This patch makes it so that about:newtab is preloaded after: 1) 1 second of user inactivity, and 2) When we have at least 40ms of idle time to spend in an idle callback. The 1s and 40ms thresholds were chosen arbitrarily, and we might tune them over time. MozReview-Commit-ID: GrklIgMOycH
browser/base/content/browser.js
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
browser/modules/AsyncTabSwitcher.jsm
testing/mochitest/browser-test.js
toolkit/modules/Services.jsm
toolkit/modules/tests/xpcshell/test_Services.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1790,16 +1790,21 @@ var gBrowserInit = {
       try {
         DownloadsCommon.initializeAllDataLinks();
         ChromeUtils.import("resource:///modules/DownloadsTaskbar.jsm", {})
           .DownloadsTaskbar.registerIndicator(window);
       } catch (ex) {
         Cu.reportError(ex);
       }
     }, {timeout: 10000});
+
+    scheduleIdleTask(() => {
+      // Prepare preloaded browser for the next new tab to open.
+      gBrowser.requestPreloadBrowser();
+    });
   },
 
   // Returns the URI(s) to load at startup if it is immediately known, or a
   // promise resolving to the URI to load.
   get _uriToLoadPromise() {
     delete this._uriToLoadPromise;
     return this._uriToLoadPromise = function() {
       // window.arguments[0]: URI to load (string), or an nsIArray of
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -104,16 +104,22 @@ window._gBrowser = {
 
   /**
    * Binding from browser to tab
    */
   _tabForBrowser: new WeakMap(),
 
   _preloadedBrowser: null,
 
+  _preloadBrowserMinUserIdleSeconds: 1, // seconds
+
+  _preloadBrowserMinIdlePeriodMs: 40, // ms
+
+  _preloadBrowserIdleObserver: null,
+
   /**
    * `_createLazyBrowser` will define properties on the unbound lazy browser
    * which correspond to properties defined in XBL which will be bound to
    * the browser when it is inserted into the document.  If any of these
    * properties are accessed by consumers, `_insertBrowser` is called and
    * the browser is inserted to ensure that things don't break.  This list
    * provides the names of properties that may be called while the browser
    * is in its unbound (lazy) state.
@@ -1749,23 +1755,72 @@ window._gBrowser = {
   _isPreloadingEnabled() {
     // Preloading for the newtab page is enabled when the pref is true
     // and the URL is "about:newtab". We do not support preloading for
     // custom newtab URLs.
     return Services.prefs.getBoolPref("browser.newtab.preload") &&
       !aboutNewTabService.overridden;
   },
 
+  cancelPreloadBrowser() {
+    if (this._preloadBrowserIdleObserver) {
+      Services.idle.removeIdleObserver(this._preloadBrowserIdleObserver,
+                                       this._preloadBrowserMinUserIdleSeconds);
+      this._preloadBrowserIdleObserver = null;
+    }
+    if (this._preloadBrowserIdleCallback) {
+      window.cancelIdleCallback(this._preloadBrowserIdleCallback);
+      this._preloadBrowserIdleCallback = null;
+    }
+  },
+
+  requestPreloadBrowser() {
+    // Do nothing if we have a preloaded browser already or we're about
+    // to create one or preloading is disabled.
+    if (this._preloadedBrowser ||
+        this._preloadBrowserIdleObserver ||
+        !this._isPreloadingEnabled()) {
+      return;
+    }
+
+    this._preloadBrowserIdleObserver = (aSubject, aTopic) => {
+      if (aTopic != "idle") {
+        return;
+      }
+
+      Services.idle.removeIdleObserver(this._preloadBrowserIdleObserver,
+                                       this._preloadBrowserMinUserIdleSeconds);
+      this._preloadBrowserIdleObserver = null;
+
+      let idleCallback = deadline => {
+        if (deadline.timeRemaining() < this._preloadBrowserMinIdlePeriodMs) {
+          this._preloadBrowserIdleCallback = window.requestIdleCallback(idleCallback);
+        } else {
+          this._preloadBrowserIdleCallback = null;
+          this._createPreloadBrowser();
+        }
+      };
+      this._preloadBrowserIdleCallback = window.requestIdleCallback(idleCallback);
+    };
+    Services.idle.addIdleObserver(this._preloadBrowserIdleObserver, this._preloadBrowserMinUserIdleSeconds);
+  },
+
   _createPreloadBrowser() {
     // Do nothing if we have a preloaded browser already
     // or preloading of newtab pages is disabled.
-    if (this._preloadedBrowser || !this._isPreloadingEnabled()) {
+    if (window.closed || this._preloadedBrowser || !this._isPreloadingEnabled()) {
       return;
     }
 
+    if (this._preloadBrowserIdleObserver) {
+      Services.idle.removeIdleObserver(this._preloadBrowserIdleObserver,
+                                       this._preloadBrowserMinUserIdleSeconds);
+      this._preloadBrowserIdleObserver = null;
+    }
+
     let remoteType =
       E10SUtils.getRemoteTypeForURI(BROWSER_NEW_TAB_URL,
         gMultiProcessBrowser);
     let browser = this._createBrowser({ isPreloadBrowser: true, remoteType });
     this._preloadedBrowser = browser;
 
     let notificationbox = this.getNotificationBox(browser);
     this.tabpanels.appendChild(notificationbox);
@@ -3939,16 +3994,22 @@ window._gBrowser = {
       window.messageManager.removeMessageListener("contextmenu", this);
 
       if (this._switcher) {
         this._switcher.destroy();
       }
     }
 
     Services.prefs.removeObserver("accessibility.typeaheadfind", this);
+
+    if (this._preloadBrowserIdleObserver) {
+      Services.idle.removeIdleObserver(this._preloadBrowserIdleObserver,
+                                       this._preloadBrowserMinUserIdleSeconds);
+      this._preloadBrowserIdleObserver = null;
+    }
   },
 
   _setupEventListeners() {
     this.addEventListener("DOMWindowClose", (event) => {
       if (!event.isTrusted)
         return;
 
       if (this.tabs.length == 1) {
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -915,17 +915,17 @@
           // XXXmano: this is a temporary workaround for bug 345399
           // We need to manually update the scroll buttons disabled state
           // if a tab was inserted to the overflow area or removed from it
           // without any scrolling and when the tabbar has already
           // overflowed.
           this.arrowScrollbox._updateScrollButtonsDisabledState();
 
           // Preload the next about:newtab if there isn't one already.
-          gBrowser._createPreloadBrowser();
+          gBrowser.requestPreloadBrowser();
         ]]></body>
       </method>
 
       <method name="_canAdvanceToTab">
         <parameter name="aTab"/>
         <body>
         <![CDATA[
           return !aTab.closing;
--- a/browser/modules/AsyncTabSwitcher.jsm
+++ b/browser/modules/AsyncTabSwitcher.jsm
@@ -2,16 +2,17 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["AsyncTabSwitcher"];
 
+
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   Services: "resource://gre/modules/Services.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingEnabled",
@@ -1107,9 +1108,8 @@ class AsyncTabSwitcher {
     }
     if (this._useDumpForLogging) {
       dump(accum + "\n");
     } else {
       Services.console.logStringMessage(accum);
     }
   }
 }
-
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -898,16 +898,18 @@ Tester.prototype = {
           BackgroundPageThumbs._destroy();
 
           // Destroy preloaded browsers.
           if (gBrowser._preloadedBrowser) {
             let browser = gBrowser._preloadedBrowser;
             gBrowser._preloadedBrowser = null;
             gBrowser.getNotificationBox(browser).remove();
           }
+
+          gBrowser.cancelPreloadBrowser();
         }
 
         // Schedule GC and CC runs before finishing in order to detect
         // DOM windows leaked by our tests or the tested code. Note that we
         // use a shrinking GC so that the JS engine will discard JIT code and
         // JIT caches more aggressively.
 
         let shutdownCleanup = aCallback => {
--- a/toolkit/modules/Services.jsm
+++ b/toolkit/modules/Services.jsm
@@ -61,16 +61,17 @@ var initTable = {
   appShell: ["@mozilla.org/appshell/appShellService;1", "nsIAppShellService"],
   cache2: ["@mozilla.org/netwerk/cache-storage-service;1", "nsICacheStorageService"],
   cpmm: ["@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"],
   console: ["@mozilla.org/consoleservice;1", "nsIConsoleService"],
   cookies: ["@mozilla.org/cookiemanager;1", "nsICookieManager"],
   droppedLinkHandler: ["@mozilla.org/content/dropped-link-handler;1", "nsIDroppedLinkHandler"],
   els: ["@mozilla.org/eventlistenerservice;1", "nsIEventListenerService"],
   eTLD: ["@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"],
+  idle: ["@mozilla.org/widget/idleservice;1", "nsIIdleService"],
   intl: ["@mozilla.org/mozintl;1", "mozIMozIntl"],
   locale: ["@mozilla.org/intl/localeservice;1", "mozILocaleService"],
   logins: ["@mozilla.org/login-manager;1", "nsILoginManager"],
   mm: ["@mozilla.org/globalmessagemanager;1", "nsISupports"],
   obs: ["@mozilla.org/observer-service;1", "nsIObserverService"],
   perms: ["@mozilla.org/permissionmanager;1", "nsIPermissionManager"],
   ppmm: ["@mozilla.org/parentprocessmessagemanager;1", "nsISupports"],
   prompt: ["@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"],
--- a/toolkit/modules/tests/xpcshell/test_Services.js
+++ b/toolkit/modules/tests/xpcshell/test_Services.js
@@ -30,16 +30,17 @@ function run_test() {
   checkService("cookies", Ci.nsICookieManager);
   checkService("dirsvc", Ci.nsIDirectoryService);
   checkService("dirsvc", Ci.nsIProperties);
   checkService("DOMRequest", Ci.nsIDOMRequestService);
   checkService("domStorageManager", Ci.nsIDOMStorageManager);
   checkService("droppedLinkHandler", Ci.nsIDroppedLinkHandler);
   checkService("eTLD", Ci.nsIEffectiveTLDService);
   checkService("focus", Ci.nsIFocusManager);
+  checkService("idle", Ci.nsIIdleService);
   checkService("io", Ci.nsIIOService);
   checkService("intl", Ci.mozIMozIntl);
   checkService("locale", Ci.mozILocaleService);
   checkService("logins", Ci.nsILoginManager);
   checkService("obs", Ci.nsIObserverService);
   checkService("perms", Ci.nsIPermissionManager);
   checkService("prefs", Ci.nsIPrefBranch);
   checkService("prefs", Ci.nsIPrefService);