Bug 1176019 - Cache layers of background tabs r?mconley draft
authorDoug Thayer <dothayer@mozilla.com>
Mon, 16 Apr 2018 15:35:41 -0700
changeset 797869 b9923dba79e461e2d500480c723491de4de16da4
parent 797745 77c06979d9e88979ec96263eccdbd750cb9221a4
child 797870 79e9917b4096119ee713002dccfd359b53499d5f
push id110611
push userbmo:dothayer@mozilla.com
push dateMon, 21 May 2018 22:29:47 +0000
reviewersmconley
bugs1176019
milestone62.0a1
Bug 1176019 - Cache layers of background tabs r?mconley We maintain a simple LRU cache of tab layers by setting their docShellIsActive = false with preserveLayers(true). Once they are pushed out of the cache by more recently used tabs, their layers are discarded. Luckily most of the complexity of this could be contained in the AsyncTabSwitcher - the one change that had to sit outside of that was moving the aTab.closing = true earlier in the removeTab call, so that we could use that information to eagerly evict tabs from the cache. This was to address a leak in a few tests on try. MozReview-Commit-ID: 2E3uU8LEYkD
browser/app/profile/firefox.js
browser/base/content/docs/tabbrowser/async-tab-switcher.rst
browser/base/content/tabbrowser.js
browser/modules/AsyncTabSwitcher.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1538,16 +1538,24 @@ pref("browser.tabs.remote.desktopbehavio
 // until bug 1453080 is fixed.
 //
 #if !defined(XP_MACOSX) || defined(NIGHTLY_BUILD)
 pref("browser.tabs.remote.warmup.enabled", true);
 #else
 pref("browser.tabs.remote.warmup.enabled", false);
 #endif
 
+// Caches tab layers to improve perceived performance
+// of tab switches.
+#if defined(NIGHTLY_BUILD)
+pref("browser.tabs.remote.tabCacheSize", 5);
+#else
+pref("browser.tabs.remote.tabCacheSize", 0);
+#endif
+
 pref("browser.tabs.remote.warmup.maxTabs", 3);
 pref("browser.tabs.remote.warmup.unloadDelayMs", 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);
--- a/browser/base/content/docs/tabbrowser/async-tab-switcher.rst
+++ b/browser/base/content/docs/tabbrowser/async-tab-switcher.rst
@@ -190,16 +190,22 @@ We use a few tricks and optimizations to
 1. Sometimes users switch between the same tabs quickly. We want to optimize for this case by not releasing the layers for tabs until some time has gone by. That way, quick switching just resolves in a re-composite in the compositor, as opposed to a full re-paint and re-upload of the layers from a remote tab’s content process.
 
 2. When a tab hasn’t ever been seen before, and is still in the process of loading (right now, dubiously checked by looking for the “busy” attribute on the ``<xul:tab>``) we show a blank content area until its layers are finally ready. The idea here is to shift perceived lag from the async tab switcher to the network by showing the blank space instead of the tab switch spinner.
 
 3. “Warming” is a nascent optimization that will allow us to pre-emptively render and cache the layers for tabs that we think the user is likely to switch to soon. After a timeout (``browser.tabs.remote.warmup.unloadDelayMs``), “warmed” tabs that aren’t switched to have their layers unloaded and cleared from the cache.
 
 4. On platforms that support ``occlusionstatechange`` events (as of this writing, only macOS) and ``sizemodechange`` events (Windows, macOS and Linux), we stop rendering the layers for the currently selected tab when the window is minimized or fully occluded by another window.
 
+5. Based on the browser.tabs.remote.tabCacheSize pref, we keep recently used tabs'
+layers around to speed up tab switches by avoiding the round trip to the content
+process. This uses a simple array (``_tabLayerCache``) inside tabbrowser.js, which
+we examine when determining if we want to unload a tab's layers or not. This is still
+experimental as of Nightly 62.
+
 .. _async-tab-switcher.warming:
 
 Warming
 =======
 
 Tab warming allows the browser to proactively render and upload layers to the compositor for tabs that the user is likely to switch to. The simplest example is when a user's mouse cursor is hovering over a tab. When this occurs, the async tab switcher is told to put that tab into a warming list, and to set its state to ``STATE_LOADING``, even though the user hasn't yet clicked on it.
 
 Warming a tab queues up a timer to unload background tabs (if no such timer already exists), which will clear out the warmed tab if the user doesn't eventually click on it. The unload will occur even if the user continues to hover the tab.
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -92,16 +92,18 @@ window._gBrowser = {
   _autoScrollPopup: null,
 
   _previewMode: false,
 
   _lastFindValue: "",
 
   _contentWaitingCount: 0,
 
+  _tabLayerCache: [],
+
   tabAnimationsInProgress: 0,
 
   _XUL_NS: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
 
   /**
    * Binding from browser to tab
    */
   _tabForBrowser: new WeakMap(),
@@ -2746,16 +2748,23 @@ window._gBrowser = {
       // If we were closed during onbeforeunload, we return false now
       // so we don't (try to) close the same tab again. Of course, we
       // also stop if the unload was cancelled by the user:
       if (aTab.closing || (!timedOut && !permitUnload)) {
         return false;
       }
     }
 
+    // this._switcher would normally cover removing a tab from this
+    // cache, but we may not have one at this time.
+    let tabCacheIndex = this._tabLayerCache.indexOf(aTab);
+    if (tabCacheIndex != -1) {
+      this._tabLayerCache.splice(tabCacheIndex, 1);
+    }
+
     this._blurTab(aTab);
 
     var closeWindow = false;
     var newTab = false;
     if (this.tabs.length - this._removingTabs.length == 1) {
       closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
         !window.toolbar.visible ||
         Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
--- a/browser/modules/AsyncTabSwitcher.jsm
+++ b/browser/modules/AsyncTabSwitcher.jsm
@@ -15,16 +15,18 @@ XPCOMUtils.defineLazyModuleGetters(this,
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingEnabled",
   "browser.tabs.remote.warmup.enabled");
 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingMax",
   "browser.tabs.remote.warmup.maxTabs");
 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingUnloadDelayMs",
   "browser.tabs.remote.warmup.unloadDelayMs");
+XPCOMUtils.defineLazyPreferenceGetter(this, "gTabCacheSize",
+  "browser.tabs.remote.tabCacheSize");
 
 /**
  * The tab switcher is responsible for asynchronously switching
  * tabs in e10s. It waits until the new tab is ready (i.e., the
  * layer tree is available) before switching to it. Then it
  * unloads the layer tree for the old tab.
  *
  * The tab switcher is a state machine. For each tab, it
@@ -286,16 +288,20 @@ class AsyncTabSwitcher {
     }
   }
 
   get minimizedOrFullyOccluded() {
     return this.window.windowState == this.window.STATE_MINIMIZED ||
            this.window.isFullyOccluded;
   }
 
+  get tabLayerCache() {
+    return this.tabbrowser._tabLayerCache;
+  }
+
   finish() {
     this.log("FINISH");
 
     this.assert(this.tabbrowser._switcher);
     this.assert(this.tabbrowser._switcher === this);
     this.assert(!this.spinnerTab);
     this.assert(!this.blankTab);
     this.assert(!this.loadTimer);
@@ -503,16 +509,25 @@ class AsyncTabSwitcher {
   }
 
   // This function runs before every event. It fixes up the state
   // to account for closed tabs.
   preActions() {
     this.assert(this.tabbrowser._switcher);
     this.assert(this.tabbrowser._switcher === this);
 
+    for (let i = 0; i < this.tabLayerCache.length; i++) {
+      let tab = this.tabLayerCache[i];
+      if (!tab.linkedBrowser) {
+        this.tabState.delete(tab);
+        this.tabLayerCache.splice(i, 1);
+        i--;
+      }
+    }
+
     for (let [tab, ] of this.tabState) {
       if (!tab.linkedBrowser) {
         this.tabState.delete(tab);
         this.unwarmTab(tab);
       }
     }
 
     if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
@@ -561,26 +576,35 @@ class AsyncTabSwitcher {
     if (!this.loadTimer && !this.minimizedOrFullyOccluded &&
         (stateOfRequestedTab == this.STATE_UNLOADED ||
         stateOfRequestedTab == this.STATE_UNLOADING ||
         this.warmingTabs.has(this.requestedTab))) {
       this.assert(stateOfRequestedTab != this.STATE_LOADED);
       this.loadRequestedTab();
     }
 
+    let numBackgroundCached = 0;
+    for (let tab of this.tabLayerCache) {
+      if (tab !== this.requestedTab) {
+        numBackgroundCached++;
+      }
+    }
+
     // 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) {
+      if (state == this.STATE_LOADED &&
+          tab !== this.requestedTab &&
+          !this.tabLayerCache.includes(tab)) {
         numPending++;
 
         if (tab !== this.visibleTab) {
           numWarming++;
         }
       }
       if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
         numPending++;
@@ -593,18 +617,20 @@ class AsyncTabSwitcher {
     // handlers, which might cause finish() to already have been called.
     // Check for that before calling finish() again.
     if (!this.tabbrowser._switcher) {
       return;
     }
 
     this.maybeFinishTabSwitch();
 
-    if (numWarming > gTabWarmingMax) {
-      this.logState("Hit tabWarmingMax");
+    if (numWarming > gTabWarmingMax || numBackgroundCached > 0) {
+      if (numWarming > gTabWarmingMax) {
+        this.logState("Hit tabWarmingMax");
+      }
       if (this.unloadTimer) {
         this.clearTimer(this.unloadTimer);
       }
       this.unloadNonRequiredTabs();
     }
 
     if (numPending == 0) {
       this.finish();
@@ -627,31 +653,44 @@ class AsyncTabSwitcher {
   // If there are any non-visible and non-requested tabs in
   // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
   // up the unloadTimer to run onUnloadTimeout if there are still
   // tabs in the process of unloading.
   unloadNonRequiredTabs() {
     this.warmingTabs = new WeakSet();
     let numPending = 0;
 
+    for (let tab of this.tabLayerCache) {
+      if (tab !== this.requestedTab) {
+        let browser = tab.linkedBrowser;
+        browser.preserveLayers(true);
+        browser.docShellIsActive = false;
+      }
+    }
+
     // Unload any tabs that can be unloaded.
     for (let [tab, state] of this.tabState) {
       if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
         continue;
       }
 
+      let isInLayerCache = this.tabLayerCache.includes(tab);
+
       if (state == this.STATE_LOADED &&
           !this.maybeVisibleTabs.has(tab) &&
           tab !== this.lastVisibleTab &&
           tab !== this.loadingTab &&
-          tab !== this.requestedTab) {
+          tab !== this.requestedTab &&
+          !isInLayerCache) {
         this.setTabState(tab, this.STATE_UNLOADING);
       }
 
-      if (state != this.STATE_UNLOADED && tab !== this.requestedTab) {
+      if (state != this.STATE_UNLOADED &&
+          tab !== this.requestedTab &&
+          !isInLayerCache) {
         numPending++;
       }
     }
 
     if (numPending) {
       // Keep the timer going since there may be more tabs to unload.
       this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
     }
@@ -865,27 +904,59 @@ class AsyncTabSwitcher {
 
     this.logState("warmupTab " + this.tinfo(tab));
 
     this.warmingTabs.add(tab);
     this.setTabState(tab, this.STATE_LOADING);
     this.queueUnload(gTabWarmingUnloadDelayMs);
   }
 
+  cleanUpTabAfterEviction(tab) {
+    this.assert(tab !== this.requestedTab);
+    let browser = tab.linkedBrowser;
+    if (browser) {
+      browser.preserveLayers(false);
+    }
+    this.setTabState(tab, this.STATE_UNLOADING);
+  }
+
+  evictOldestTabFromCache() {
+    let tab = this.tabLayerCache.shift();
+    this.cleanUpTabAfterEviction(tab);
+  }
+
+  maybePromoteTabInLayerCache(tab) {
+    if (gTabCacheSize > 1 &&
+        tab.linkedBrowser.isRemoteBrowser &&
+        tab.linkedBrowser.currentURI.spec != "about:blank") {
+      let tabIndex = this.tabLayerCache.indexOf(tab);
+
+      if (tabIndex != -1) {
+        this.tabLayerCache.splice(tabIndex, 1);
+      }
+
+      this.tabLayerCache.push(tab);
+
+      if (this.tabLayerCache.length > gTabCacheSize) {
+        this.evictOldestTabFromCache();
+      }
+    }
+  }
+
   // Called when the user asks to switch to a given tab.
   requestTab(tab) {
     if (tab === this.requestedTab) {
       return;
     }
 
+    let tabState = this.getTabState(tab);
     if (gTabWarmingEnabled) {
       let warmingState = "disqualified";
 
       if (this.canWarmTab(tab)) {
-        let tabState = this.getTabState(tab);
         if (tabState == this.STATE_LOADING) {
           warmingState = "stillLoading";
         } else if (tabState == this.STATE_LOADED) {
           warmingState = "loaded";
         } else if (tabState == this.STATE_UNLOADING ||
                    tabState == this.STATE_UNLOADED) {
           // At this point, if the tab's browser was being inserted
           // lazily, we never had a chance to warm it up, and unfortunately
@@ -901,16 +972,19 @@ class AsyncTabSwitcher {
         .add(warmingState);
     }
 
     this._requestingTab = true;
     this.logState("requestTab " + this.tinfo(tab));
     this.startTabSwitch();
 
     this.requestedTab = tab;
+    if (tabState == this.STATE_LOADED) {
+      this.maybeVisibleTabs.clear();
+    }
 
     tab.linkedBrowser.setAttribute("primary", "true");
     if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
       this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
     }
     this.lastPrimaryTab = tab;
 
     this.queueUnload(this.UNLOAD_DELAY);
@@ -987,16 +1061,20 @@ class AsyncTabSwitcher {
    * the tab switch (layers are ready, paints are done, spinners
    * are hidden). This checks to make sure all conditions are
    * satisfied, and then records the tab switch as finished.
    */
   maybeFinishTabSwitch() {
     if (this.switchInProgress && this.requestedTab &&
         (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
           this.requestedTab === this.blankTab)) {
+      if (this.requestedTab !== this.blankTab) {
+        this.maybePromoteTabInLayerCache(this.requestedTab);
+      }
+
       // After this point the tab has switched from the content thread's point of view.
       // The changes will be visible after the next refresh driver tick + composite.
       let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
       if (time != -1) {
         TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
         this.log("DEBUG: tab switch time = " + time);
         this.addMarker("AsyncTabSwitch:Finish");
       }
@@ -1073,29 +1151,47 @@ class AsyncTabSwitcher {
     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);
+      let isCached = this.tabLayerCache.includes(tab);
+      let isClosing = tab.closing;
+      let linkedBrowser = tab.linkedBrowser;
+      let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
+      let isRendered = linkedBrowser && linkedBrowser.renderLayers;
 
       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)";
+
+      let extraStates = "";
+      if (isWarming) extraStates += "W";
+      if (isCached) extraStates += "C";
+      if (isClosing) extraStates += "X";
+      if (isActive) extraStates += "A";
+      if (isRendered) extraStates += "R";
+      if (extraStates != "") {
+        accum += `(${extraStates})`;
+      }
+
       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 += " ";
     }
+
+    accum += "cached: " + this.tabLayerCache.length;
+
     if (this._useDumpForLogging) {
       dump(accum + "\n");
     } else {
       Services.console.logStringMessage(accum);
     }
   }
 }