Bug 1385453 - Add API to tabbrowser to speculatively warm-up tabs for tab switching. r?billm draft
authorMike Conley <mconley@mozilla.com>
Mon, 21 Aug 2017 10:11:33 -0400
changeset 652633 f7ba43e6fa9c6567651e928a3e1e80cbae7c2132
parent 652622 2306e153fba9ca55726ffcce889eaca7a479c29f
child 652634 65dc59818c00f8aecd5513a5913e65e23eef7271
push id76103
push usermconley@mozilla.com
push dateFri, 25 Aug 2017 01:57:10 +0000
reviewersbillm
bugs1385453
milestone57.0a1
Bug 1385453 - Add API to tabbrowser to speculatively warm-up tabs for tab switching. r?billm MozReview-Commit-ID: FIVx5d6ZOqq
browser/app/profile/firefox.js
browser/base/content/tabbrowser.xml
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1546,16 +1546,21 @@ pref("browser.tabs.remote.desktopbehavio
 #if !defined(RELEASE_OR_BETA) || defined(MOZ_DEV_EDITION)
 // At the moment, autostart.2 is used, while autostart.1 is unused.
 // We leave it here set to false to reset users' defaults and allow
 // us to change everybody to true in the future, when desired.
 pref("browser.tabs.remote.autostart.1", false);
 pref("browser.tabs.remote.autostart.2", true);
 #endif
 
+// For speculatively warming up tabs to improve perceived
+// performance while using the async tab switcher.
+pref("browser.tabs.remote.maxWarmingTabs", 3);
+pref("browser.tabs.remote.warmingUnloadDelayMs", 2000);
+
 // For the about:tabcrashed page
 pref("browser.tabs.crashReporting.sendReport", true);
 pref("browser.tabs.crashReporting.includeURL", false);
 pref("browser.tabs.crashReporting.requestEmail", false);
 pref("browser.tabs.crashReporting.emailMe", false);
 pref("browser.tabs.crashReporting.email", "");
 
 // Enable e10s add-on interposition by default.
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -4095,16 +4095,22 @@
 
             // Set of tabs that might be visible right now. We maintain
             // this set because we can't be sure when a tab is actually
             // drawn. A tab is added to this set when we ask to make it
             // visible. All tabs but the most recently shown tab are
             // removed from the set upon MozAfterPaint.
             maybeVisibleTabs: new Set([this.selectedTab]),
 
+            // This holds onto the set of tabs that we've been asked to warm up.
+            // This is used only for Telemetry and logging, and (in order to not
+            // over-complicate the async tab switcher any further) has nothing to do
+            // with how warmed tabs are loaded and unloaded.
+            warmingTabs: new WeakSet(),
+
             STATE_UNLOADED: 0,
             STATE_LOADING: 1,
             STATE_LOADED: 2,
             STATE_UNLOADING: 3,
 
             // re-entrancy guard:
             _processing: false,
 
@@ -4149,16 +4155,17 @@
               let {tabParent} = browser.frameLoader;
               if (state == this.STATE_LOADING) {
                 this.assert(!this.minimizedOrFullyOccluded);
                 browser.docShellIsActive = true;
                 if (!tabParent) {
                   this.onLayersReady(browser);
                 }
               } else if (state == this.STATE_UNLOADING) {
+                this.unwarmTab(tab);
                 browser.docShellIsActive = false;
                 if (!tabParent) {
                   this.onLayersCleared(browser);
                 }
               }
 
               if (!tab.linkedBrowser.isRemoteBrowser) {
                 // setTabState is potentially re-entrant in the non-remote case,
@@ -4177,16 +4184,21 @@
             get minimizedOrFullyOccluded() {
               return window.windowState == window.STATE_MINIMIZED ||
                      window.isFullyOccluded;
             },
 
             init() {
               this.log("START");
 
+              XPCOMUtils.defineLazyPreferenceGetter(this, "MAX_WARMING_TABS",
+                                                    "browser.tabs.remote.maxWarmingTabs", 3);
+              XPCOMUtils.defineLazyPreferenceGetter(this, "WARMING_UNLOAD_DELAY" /* ms */,
+                                                    "browser.tabs.remote.warmingUnloadDelayMs", 2000);
+
               // If we minimized the window before the switcher was activated,
               // we might have set  the preserveLayers flag for the current
               // browser. Let's clear it.
               this.tabbrowser.mCurrentBrowser.preserveLayers(false);
 
               window.addEventListener("MozAfterPaint", this);
               window.addEventListener("MozLayerTreeReady", this);
               window.addEventListener("MozLayerTreeCleared", this);
@@ -4426,16 +4438,17 @@
             // to account for closed tabs.
             preActions() {
               this.assert(this.tabbrowser._switcher);
               this.assert(this.tabbrowser._switcher === this);
 
               for (let [tab, ] of this.tabState) {
                 if (!tab.linkedBrowser) {
                   this.tabState.delete(tab);
+                  this.unwarmTab(tab);
                 }
               }
 
               if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
                 this.lastVisibleTab = null;
               }
               if (this.blankTab && !this.blankTab.linkedBrowser) {
                 this.blankTab = null;
@@ -4483,54 +4496,66 @@
               if (!this.loadTimer && !this.minimizedOrFullyOccluded &&
                   (requestedState == this.STATE_UNLOADED ||
                    requestedState == this.STATE_UNLOADING)) {
                 this.loadRequestedTab();
               }
 
               // See how many tabs still have work to do.
               let numPending = 0;
+              let numWarming = 0;
               for (let [tab, state] of this.tabState) {
                 // Skip print preview browsers since they shouldn't affect tab switching.
                 if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
                   continue;
                 }
 
                 if (state == this.STATE_LOADED && tab !== this.requestedTab) {
                   numPending++;
+
+                  if (tab !== this.visibleTab) {
+                    numWarming++;
+                  }
                 }
                 if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
                   numPending++;
                 }
               }
 
               this.updateDisplay();
 
               // It's possible for updateDisplay to trigger one of our own event
               // handlers, which might cause finish() to already have been called.
               // Check for that before calling finish() again.
               if (!this.tabbrowser._switcher) {
                 return;
               }
 
-              if (this.blankTab) {
-                this.maybeFinishTabSwitch();
+              this.maybeFinishTabSwitch();
+
+              if (numWarming > this.MAX_WARMING_TABS) {
+                this.logState("Hit MAX_WARMING_TABS");
+                if (this.unloadTimer) {
+                  this.clearTimer(this.unloadTimer);
+                }
+                this.onUnloadTimeout();
               }
 
               if (numPending == 0) {
                 this.finish();
               }
 
               this.logState("done");
             },
 
             // Fires when we're ready to unload unused tabs.
             onUnloadTimeout() {
               this.logState("onUnloadTimeout");
               this.unloadTimer = null;
+              this.warmingTabs = new WeakSet();
               this.preActions();
 
               let numPending = 0;
 
               // Unload any tabs that can be unloaded.
               for (let [tab, state] of this.tabState) {
                 if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
                   continue;
@@ -4570,31 +4595,28 @@
             onLayersReady(browser) {
               let tab = this.tabbrowser.getTabForBrowser(browser);
               this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
 
               this.assert(this.getTabState(tab) == this.STATE_LOADING ||
                           this.getTabState(tab) == this.STATE_LOADED);
               this.setTabState(tab, this.STATE_LOADED);
 
-              this.maybeFinishTabSwitch();
-
               if (this.loadingTab === tab) {
                 this.clearTimer(this.loadTimer);
                 this.loadTimer = null;
                 this.loadingTab = null;
               }
             },
 
             // Fires when we paint the screen. Any tab switches we initiated
             // previously are done, so there's no need to keep the old layers
             // around.
             onPaint() {
               this.maybeVisibleTabs.clear();
-              this.maybeFinishTabSwitch();
             },
 
             // Called when we're done clearing the layers for a tab.
             onLayersCleared(browser) {
               let tab = this.tabbrowser.getTabForBrowser(browser);
               if (tab) {
                 this.logState(`onLayersCleared(${tab._tPos})`);
                 this.assert(this.getTabState(tab) == this.STATE_UNLOADING ||
@@ -4715,45 +4737,91 @@
               return state == this.STATE_LOADING || state == this.STATE_LOADED;
             },
 
             activateBrowserForPrintPreview(browser) {
               let tab = this.tabbrowser.getTabForBrowser(browser);
               this.setTabState(tab, this.STATE_LOADING);
             },
 
+            canWarmTab(tab) {
+              // If the tab is not yet inserted, closing, not remote,
+              // crashed, already visible, or already requested, warming
+              // up the tab makes no sense.
+              if (this.minimizedOrFullyOccluded ||
+                  !tab.linkedPanel ||
+                  tab.closing ||
+                  !tab.linkedBrowser.isRemoteBrowser ||
+                  !tab.linkedBrowser.frameLoader.tabParent) {
+                return false;
+              }
+
+              // Similarly, if the tab is already in STATE_LOADING or
+              // STATE_LOADED somehow, there's no point in trying to
+              // warm it up.
+              let state = this.getTabState(tab);
+              if (state === this.STATE_LOADING ||
+                  state === this.STATE_LOADED) {
+                return false;
+              }
+
+              return true;
+            },
+
+            unwarmTab(tab) {
+              this.warmingTabs.delete(tab);
+            },
+
+            warmupTab(tab) {
+              if (!this.canWarmTab(tab)) {
+                return;
+              }
+
+              this.logState("warmupTab " + this.tinfo(tab));
+
+              this.warmingTabs.add(tab);
+              this.setTabState(tab, this.STATE_LOADING);
+              this.suppressDisplayPortAndQueueUnload(tab, this.WARMING_UNLOAD_DELAY);
+            },
+
             // Called when the user asks to switch to a given tab.
             requestTab(tab) {
               if (tab === this.requestedTab) {
                 return;
               }
 
+              this.unwarmTab(tab);
+
               this._requestingTab = true;
               this.logState("requestTab " + this.tinfo(tab));
               this.startTabSwitch();
 
               this.requestedTab = tab;
 
-              let browser = this.requestedTab.linkedBrowser;
+              this.suppressDisplayPortAndQueueUnload(this.requestedTab, this.UNLOAD_DELAY);
+              this._requestingTab = false;
+            },
+
+            suppressDisplayPortAndQueueUnload(tab, unloadTimeout) {
+              let browser = tab.linkedBrowser;
               let fl = browser.frameLoader;
 
               if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) {
                 fl.tabParent.suppressDisplayport(true);
                 this.activeSuppressDisplayport.add(fl.tabParent);
               }
 
               this.preActions();
 
               if (this.unloadTimer) {
                 this.clearTimer(this.unloadTimer);
               }
-              this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
+              this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), unloadTimeout);
 
               this.postActions();
-              this._requestingTab = false;
             },
 
             handleEvent(event, delayed = false) {
               if (this._processing) {
                 this.setTimer(() => this.handleEvent(event, true), 0);
                 return;
               }
               if (delayed && this.tabbrowser._switcher != this) {
@@ -4833,17 +4901,16 @@
             spinnerHidden() {
               this.assert(this.spinnerTab);
               this.log("DEBUG: spinner time = " +
                        TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window));
               TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
               TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
               this.addMarker("AsyncTabSwitch:SpinnerHidden");
               // we do not get a onPaint after displaying the spinner
-              this.maybeFinishTabSwitch();
             },
 
             addMarker(marker) {
               if (Services.profiler) {
                 Services.profiler.AddMarker(marker);
               }
             },
 
@@ -4885,22 +4952,24 @@
             logState(prefix) {
               if (!this.logging())
                 return;
 
               let accum = prefix + " ";
               for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
                 let tab = this.tabbrowser.tabs[i];
                 let state = this.getTabState(tab);
+                let isWarming = this.warmingTabs.has(tab);
 
                 accum += i + ":";
                 if (tab === this.lastVisibleTab) accum += "V";
                 if (tab === this.loadingTab) accum += "L";
                 if (tab === this.requestedTab) accum += "R";
                 if (tab === this.blankTab) accum += "B";
+                if (isWarming) accum += "(W)";
                 if (state == this.STATE_LOADED) accum += "(+)";
                 if (state == this.STATE_LOADING) accum += "(+?)";
                 if (state == this.STATE_UNLOADED) accum += "(-)";
                 if (state == this.STATE_UNLOADING) accum += "(-?)";
                 accum += " ";
               }
               if (this._useDumpForLogging) {
                 dump(accum + "\n");
@@ -4910,16 +4979,25 @@
             },
           };
           this._switcher = switcher;
           switcher.init();
           return switcher;
         ]]></body>
       </method>
 
+      <method name="warmupTab">
+        <parameter name="aTab"/>
+        <body>
+          <![CDATA[
+            this._getSwitcher().warmupTab(aTab);
+          ]]>
+        </body>
+      </method>
+
       <!-- BEGIN FORWARDED BROWSER PROPERTIES.  IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
            MAKE SURE TO ADD IT HERE AS WELL. -->
       <property name="canGoBack"
                 onget="return this.mCurrentBrowser.canGoBack;"
                 readonly="true"/>
 
       <property name="canGoForward"
                 onget="return this.mCurrentBrowser.canGoForward;"