Bug 1436361 - Extract the async tab switcher from tabbrowser.js into its own JSM. r?mconley draft
authorDão Gottwald <dao@mozilla.com>
Fri, 02 Mar 2018 12:51:15 +0100
changeset 762377 bbc9275a730d565fe5c526a9384254edc308f534
parent 762376 916103b8675d9fdb28b891cac235d74f9f475942
push id101166
push userdgottwald@mozilla.com
push dateFri, 02 Mar 2018 11:51:52 +0000
reviewersmconley
bugs1436361
milestone60.0a1
Bug 1436361 - Extract the async tab switcher from tabbrowser.js into its own JSM. r?mconley MozReview-Commit-ID: AQaVOmQ548v
browser/base/content/tabbrowser.js
browser/modules/AsyncTabSwitcher.jsm
browser/modules/moz.build
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -20,16 +20,19 @@ class TabBrowser {
     this.setAttribute = this.container.setAttribute.bind(this.container);
     this.removeAttribute = this.container.removeAttribute.bind(this.container);
     this.appendChild = this.container.appendChild.bind(this.container);
     this.ownerGlobal = this.container.ownerGlobal;
     this.ownerDocument = this.container.ownerDocument;
     this.namespaceURI = this.container.namespaceURI;
     this.style = this.container.style;
 
+    ChromeUtils.defineModuleGetter(this, "AsyncTabSwitcher",
+      "resource:///modules/AsyncTabSwitcher.jsm");
+
     XPCOMUtils.defineLazyServiceGetters(this, {
       _unifiedComplete: ["@mozilla.org/autocomplete/search;1?name=unifiedcomplete", "mozIPlacesAutoComplete"],
       serializationHelper: ["@mozilla.org/network/serialization-helper;1", "nsISerializationHelper"],
       mURIFixup: ["@mozilla.org/docshell/urifixup;1", "nsIURIFixup"],
     });
 
     XPCOMUtils.defineLazyGetter(this, "initialBrowser", () => {
       return document.getAnonymousElementByAttribute(this.container, "anonid", "initialBrowser");
@@ -3596,1095 +3599,21 @@ class TabBrowser {
       return this._switcher.shouldActivateDocShell(aBrowser);
     }
     return (aBrowser == this.selectedBrowser &&
             window.windowState != window.STATE_MINIMIZED &&
             !window.isFullyOccluded) ||
             this._printPreviewBrowsers.has(aBrowser);
   }
 
-  /**
-   * 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
-   * maintains state about whether the layer tree for the tab is
-   * available, being loaded, being unloaded, or unavailable. It
-   * also keeps track of the tab currently being displayed, the tab
-   * it's trying to load, and the tab the user has asked to switch
-   * to. The switcher object is created upon tab switch. It is
-   * released when there are no pending tabs to load or unload.
-   *
-   * The following general principles have guided the design:
-   *
-   * 1. We only request one layer tree at a time. If the user
-   * switches to a different tab while waiting, we don't request
-   * the new layer tree until the old tab has loaded or timed out.
-   *
-   * 2. If loading the layers for a tab times out, we show the
-   * spinner and possibly request the layer tree for another tab if
-   * the user has requested one.
-   *
-   * 3. We discard layer trees on a delay. This way, if the user is
-   * switching among the same tabs frequently, we don't continually
-   * load the same tabs.
-   *
-   * It's important that we always show either the spinner or a tab
-   * whose layers are available. Otherwise the compositor will draw
-   * an entirely black frame, which is very jarring. To ensure this
-   * never happens when switching away from a tab, we assume the
-   * old tab might still be drawn until a MozAfterPaint event
-   * occurs. Because layout and compositing happen asynchronously,
-   * we don't have any other way of knowing when the switch
-   * actually takes place. Therefore, we don't unload the old tab
-   * until the next MozAfterPaint event.
-   */
   _getSwitcher() {
-    if (this._switcher) {
-      return this._switcher;
+    if (!this._switcher) {
+      this._switcher = new this.AsyncTabSwitcher(this);
     }
-
-    let switcher = {
-      // How long to wait for a tab's layers to load. After this
-      // time elapses, we're free to put up the spinner and start
-      // trying to load a different tab.
-      TAB_SWITCH_TIMEOUT: 400 /* ms */,
-
-      // When the user hasn't switched tabs for this long, we unload
-      // layers for all tabs that aren't in use.
-      UNLOAD_DELAY: 300 /* ms */,
-
-      // The next three tabs form the principal state variables.
-      // See the assertions in postActions for their invariants.
-
-      // Tab the user requested most recently.
-      requestedTab: this.selectedTab,
-
-      // Tab we're currently trying to load.
-      loadingTab: null,
-
-      // We show this tab in case the requestedTab hasn't loaded yet.
-      lastVisibleTab: this.selectedTab,
-
-      // Auxilliary state variables:
-
-      visibleTab: this.selectedTab, // Tab that's on screen.
-      spinnerTab: null, // Tab showing a spinner.
-      blankTab: null, // Tab showing blank.
-      lastPrimaryTab: this.selectedTab, // Tab with primary="true"
-
-      tabbrowser: this, // Reference to gBrowser.
-      loadTimer: null, // TAB_SWITCH_TIMEOUT nsITimer instance.
-      unloadTimer: null, // UNLOAD_DELAY nsITimer instance.
-
-      // Map from tabs to STATE_* (below).
-      tabState: new Map(),
-
-      // True if we're in the midst of switching tabs.
-      switchInProgress: false,
-
-      // Keep an exact list of content processes (tabParent) in which
-      // we're actively suppressing the display port. This gives a robust
-      // way to make sure we don't forget to un-suppress.
-      activeSuppressDisplayport: new Set(),
-
-      // 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,
-
-      // Wraps nsITimer. Must not use the vanilla setTimeout and
-      // clearTimeout, because they will be blocked by nsIPromptService
-      // dialogs.
-      setTimer(callback, timeout) {
-        let event = {
-          notify: callback
-        };
-
-        var timer = Cc["@mozilla.org/timer;1"]
-          .createInstance(Ci.nsITimer);
-        timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
-        return timer;
-      },
-
-      clearTimer(timer) {
-        timer.cancel();
-      },
-
-      getTabState(tab) {
-        let state = this.tabState.get(tab);
-
-        // As an optimization, we lazily evaluate the state of tabs
-        // that we've never seen before. Once we've figured it out,
-        // we stash it in our state map.
-        if (state === undefined) {
-          state = this.STATE_UNLOADED;
-
-          if (tab && tab.linkedPanel) {
-            let b = tab.linkedBrowser;
-            if (b.renderLayers && b.hasLayers) {
-              state = this.STATE_LOADED;
-            } else if (b.renderLayers && !b.hasLayers) {
-              state = this.STATE_LOADING;
-            } else if (!b.renderLayers && b.hasLayers) {
-              state = this.STATE_UNLOADING;
-            }
-          }
-
-          this.setTabStateNoAction(tab, state);
-        }
-
-        return state;
-      },
-
-      setTabStateNoAction(tab, state) {
-        if (state == this.STATE_UNLOADED) {
-          this.tabState.delete(tab);
-        } else {
-          this.tabState.set(tab, state);
-        }
-      },
-
-      setTabState(tab, state) {
-        if (state == this.getTabState(tab)) {
-          return;
-        }
-
-        this.setTabStateNoAction(tab, state);
-
-        let browser = tab.linkedBrowser;
-        let { tabParent } = browser.frameLoader;
-        if (state == this.STATE_LOADING) {
-          this.assert(!this.minimizedOrFullyOccluded);
-
-          if (!this.tabbrowser.tabWarmingEnabled) {
-            browser.docShellIsActive = true;
-          }
-
-          if (tabParent) {
-            browser.renderLayers = true;
-          } else {
-            this.onLayersReady(browser);
-          }
-        } else if (state == this.STATE_UNLOADING) {
-          this.unwarmTab(tab);
-          // Setting the docShell to be inactive will also cause it
-          // to stop rendering layers.
-          browser.docShellIsActive = false;
-          if (!tabParent) {
-            this.onLayersCleared(browser);
-          }
-        } else if (state == this.STATE_LOADED) {
-          this.maybeActivateDocShell(tab);
-        }
-
-        if (!tab.linkedBrowser.isRemoteBrowser) {
-          // setTabState is potentially re-entrant in the non-remote case,
-          // so we must re-get the state for this assertion.
-          let nonRemoteState = this.getTabState(tab);
-          // Non-remote tabs can never stay in the STATE_LOADING
-          // or STATE_UNLOADING states. By the time this function
-          // exits, a non-remote tab must be in STATE_LOADED or
-          // STATE_UNLOADED, since the painting and the layer
-          // upload happen synchronously.
-          this.assert(nonRemoteState == this.STATE_UNLOADED ||
-            nonRemoteState == this.STATE_LOADED);
-        }
-      },
-
-      get minimizedOrFullyOccluded() {
-        return window.windowState == window.STATE_MINIMIZED ||
-          window.isFullyOccluded;
-      },
-
-      init() {
-        this.log("START");
-
-        window.addEventListener("MozAfterPaint", this);
-        window.addEventListener("MozLayerTreeReady", this);
-        window.addEventListener("MozLayerTreeCleared", this);
-        window.addEventListener("TabRemotenessChange", this);
-        window.addEventListener("sizemodechange", this);
-        window.addEventListener("occlusionstatechange", this);
-        window.addEventListener("SwapDocShells", this, true);
-        window.addEventListener("EndSwapDocShells", this, true);
-
-        let initialTab = this.requestedTab;
-        let initialBrowser = initialTab.linkedBrowser;
-
-        let tabIsLoaded = !initialBrowser.isRemoteBrowser ||
-          initialBrowser.frameLoader.tabParent.hasLayers;
-
-        // 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.
-        initialBrowser.preserveLayers(false);
-
-        if (!this.minimizedOrFullyOccluded) {
-          this.log("Initial tab is loaded?: " + tabIsLoaded);
-          this.setTabState(initialTab, tabIsLoaded ? this.STATE_LOADED
-                                                   : this.STATE_LOADING);
-        }
-
-        for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
-          let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
-          let state = ppBrowser.hasLayers ? this.STATE_LOADED
-                                          : this.STATE_LOADING;
-          this.setTabState(ppTab, state);
-        }
-      },
-
-      destroy() {
-        if (this.unloadTimer) {
-          this.clearTimer(this.unloadTimer);
-          this.unloadTimer = null;
-        }
-        if (this.loadTimer) {
-          this.clearTimer(this.loadTimer);
-          this.loadTimer = null;
-        }
-
-        window.removeEventListener("MozAfterPaint", this);
-        window.removeEventListener("MozLayerTreeReady", this);
-        window.removeEventListener("MozLayerTreeCleared", this);
-        window.removeEventListener("TabRemotenessChange", this);
-        window.removeEventListener("sizemodechange", this);
-        window.removeEventListener("occlusionstatechange", this);
-        window.removeEventListener("SwapDocShells", this, true);
-        window.removeEventListener("EndSwapDocShells", this, true);
-
-        this.tabbrowser._switcher = null;
-
-        this.activeSuppressDisplayport.forEach(function(tabParent) {
-          tabParent.suppressDisplayport(false);
-        });
-        this.activeSuppressDisplayport.clear();
-      },
-
-      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);
-        this.assert(!this.loadingTab);
-        this.assert(this.lastVisibleTab === this.requestedTab);
-        this.assert(this.minimizedOrFullyOccluded ||
-          this.getTabState(this.requestedTab) == this.STATE_LOADED);
-
-        this.destroy();
-
-        document.commandDispatcher.unlock();
-
-        let event = new CustomEvent("TabSwitchDone", {
-          bubbles: true,
-          cancelable: true
-        });
-        this.tabbrowser.dispatchEvent(event);
-      },
-
-      // This function is called after all the main state changes to
-      // make sure we display the right tab.
-      updateDisplay() {
-        let requestedTabState = this.getTabState(this.requestedTab);
-        let requestedBrowser = this.requestedTab.linkedBrowser;
-
-        // It is often more desirable to show a blank tab when appropriate than
-        // the tab switch spinner - especially since the spinner is usually
-        // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
-        // tab switch. We can hide this lag, and hide the time being spent
-        // constructing TabChild's, layer trees, etc, by showing a blank
-        // tab instead and focusing it immediately.
-        let shouldBeBlank = false;
-        if (requestedBrowser.isRemoteBrowser) {
-          // If a tab is remote and the window is not minimized, we can show a
-          // blank tab instead of a spinner in the following cases:
-          //
-          // 1. The tab has just crashed, and we haven't started showing the
-          //    tab crashed page yet (in this case, the TabParent is null)
-          // 2. The tab has never presented, and has not finished loading
-          //    a non-local-about: page.
-          //
-          // For (2), "finished loading a non-local-about: page" is
-          // determined by the busy state on the tab element and checking
-          // if the loaded URI is local.
-          let hasSufficientlyLoaded = !this.requestedTab.hasAttribute("busy") &&
-            !this.tabbrowser._isLocalAboutURI(requestedBrowser.currentURI);
-
-          let fl = requestedBrowser.frameLoader;
-          shouldBeBlank = !this.minimizedOrFullyOccluded &&
-            (!fl.tabParent ||
-              (!hasSufficientlyLoaded && !fl.tabParent.hasPresented));
-        }
-
-        this.log("Tab should be blank: " + shouldBeBlank);
-        this.log("Requested tab is remote?: " + requestedBrowser.isRemoteBrowser);
-
-        // Figure out which tab we actually want visible right now.
-        let showTab = null;
-        if (requestedTabState != this.STATE_LOADED &&
-            this.lastVisibleTab && this.loadTimer && !shouldBeBlank) {
-          // If we can't show the requestedTab, and lastVisibleTab is
-          // available, show it.
-          showTab = this.lastVisibleTab;
-        } else {
-          // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
-          showTab = this.requestedTab;
-        }
-
-        // First, let's deal with blank tabs, which we show instead
-        // of the spinner when the tab is not currently set up
-        // properly in the content process.
-        if (!shouldBeBlank && this.blankTab) {
-          this.blankTab.linkedBrowser.removeAttribute("blank");
-          this.blankTab = null;
-        } else if (shouldBeBlank && this.blankTab !== showTab) {
-          if (this.blankTab) {
-            this.blankTab.linkedBrowser.removeAttribute("blank");
-          }
-          this.blankTab = showTab;
-          this.blankTab.linkedBrowser.setAttribute("blank", "true");
-        }
-
-        // Show or hide the spinner as needed.
-        let needSpinner = this.getTabState(showTab) != this.STATE_LOADED &&
-                          !this.minimizedOrFullyOccluded &&
-                          !shouldBeBlank;
-
-        if (!needSpinner && this.spinnerTab) {
-          this.spinnerHidden();
-          this.tabbrowser.removeAttribute("pendingpaint");
-          this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
-          this.spinnerTab = null;
-        } else if (needSpinner && this.spinnerTab !== showTab) {
-          if (this.spinnerTab) {
-            this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
-          } else {
-            this.spinnerDisplayed();
-          }
-          this.spinnerTab = showTab;
-          this.tabbrowser.setAttribute("pendingpaint", "true");
-          this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
-        }
-
-        // Switch to the tab we've decided to make visible.
-        if (this.visibleTab !== showTab) {
-          this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
-          this.visibleTab = showTab;
-
-          this.maybeVisibleTabs.add(showTab);
-
-          let tabs = this.tabbrowser.tabbox.tabs;
-          let tabPanel = this.tabbrowser.mPanelContainer;
-          let showPanel = tabs.getRelatedElement(showTab);
-          let index = Array.indexOf(tabPanel.childNodes, showPanel);
-          if (index != -1) {
-            this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
-            tabPanel.setAttribute("selectedIndex", index);
-            if (showTab === this.requestedTab) {
-              if (this._requestingTab) {
-                /*
-                 * If _requestingTab is set, that means that we're switching the
-                 * visibility of the tab synchronously, and we need to wait for
-                 * the "select" event before shifting focus so that
-                 * _adjustFocusAfterTabSwitch runs with the right information for
-                 * the tab switch.
-                 */
-                this.tabbrowser.addEventListener("select", () => {
-                  this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
-                }, { once: true });
-              } else {
-                this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
-              }
-
-              this.maybeActivateDocShell(this.requestedTab);
-            }
-          }
-
-          // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
-          if (this.lastVisibleTab)
-            this.lastVisibleTab._visuallySelected = false;
-
-          this.visibleTab._visuallySelected = true;
-        }
-
-        this.lastVisibleTab = this.visibleTab;
-      },
-
-      assert(cond) {
-        if (!cond) {
-          dump("Assertion failure\n" + Error().stack);
-
-          // Don't break a user's browser if an assertion fails.
-          if (AppConstants.DEBUG) {
-            throw new Error("Assertion failure");
-          }
-        }
-      },
-
-      // We've decided to try to load requestedTab.
-      loadRequestedTab() {
-        this.assert(!this.loadTimer);
-        this.assert(!this.minimizedOrFullyOccluded);
-
-        // loadingTab can be non-null here if we timed out loading the current tab.
-        // In that case we just overwrite it with a different tab; it's had its chance.
-        this.loadingTab = this.requestedTab;
-        this.log("Loading tab " + this.tinfo(this.loadingTab));
-
-        this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
-        this.setTabState(this.requestedTab, this.STATE_LOADING);
-      },
-
-      maybeActivateDocShell(tab) {
-        // If we've reached the point where the requested tab has entered
-        // the loaded state, but the DocShell is still not yet active, we
-        // should activate it.
-        let browser = tab.linkedBrowser;
-        let state = this.getTabState(tab);
-        let canCheckDocShellState = !browser.mDestroyed &&
-          (browser.docShell || browser.frameLoader.tabParent);
-        if (tab == this.requestedTab &&
-            canCheckDocShellState &&
-            state == this.STATE_LOADED &&
-            !browser.docShellIsActive &&
-            !this.minimizedOrFullyOccluded) {
-          browser.docShellIsActive = true;
-          this.logState("Set requested tab docshell to active and preserveLayers to false");
-          // 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.
-          browser.preserveLayers(false);
-        }
-      },
-
-      // 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 [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.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
-          this.lastPrimaryTab = null;
-        }
-        if (this.blankTab && !this.blankTab.linkedBrowser) {
-          this.blankTab = null;
-        }
-        if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
-          this.spinnerHidden();
-          this.spinnerTab = null;
-        }
-        if (this.loadingTab && !this.loadingTab.linkedBrowser) {
-          this.loadingTab = null;
-          this.clearTimer(this.loadTimer);
-          this.loadTimer = null;
-        }
-      },
-
-      // This code runs after we've responded to an event or requested a new
-      // tab. It's expected that we've already updated all the principal
-      // state variables. This function takes care of updating any auxilliary
-      // state.
-      postActions() {
-        // Once we finish loading loadingTab, we null it out. So the state should
-        // always be LOADING.
-        this.assert(!this.loadingTab ||
-          this.getTabState(this.loadingTab) == this.STATE_LOADING);
-
-        // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
-        // the timer is set only when we're loading something.
-        this.assert(!this.loadTimer || this.loadingTab);
-        this.assert(!this.loadingTab || this.loadTimer);
-
-        // If we're switching to a non-remote tab, there's no need to wait
-        // for it to send layers to the compositor, as this will happen
-        // synchronously. Clearing this here means that in the next step,
-        // we can load the non-remote browser immediately.
-        if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
-          this.loadingTab = null;
-          if (this.loadTimer) {
-            this.clearTimer(this.loadTimer);
-            this.loadTimer = null;
-          }
-        }
-
-        // If we're not loading anything, try loading the requested tab.
-        let stateOfRequestedTab = this.getTabState(this.requestedTab);
-        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();
-        }
-
-        // 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;
-        }
-
-        this.maybeFinishTabSwitch();
-
-        if (numWarming > this.tabbrowser.tabWarmingMax) {
-          this.logState("Hit tabWarmingMax");
-          if (this.unloadTimer) {
-            this.clearTimer(this.unloadTimer);
-          }
-          this.unloadNonRequiredTabs();
-        }
-
-        if (numPending == 0) {
-          this.finish();
-        }
-
-        this.logState("done");
-      },
-
-      // Fires when we're ready to unload unused tabs.
-      onUnloadTimeout() {
-        this.logState("onUnloadTimeout");
-        this.preActions();
-        this.unloadTimer = null;
-
-        this.unloadNonRequiredTabs();
-
-        this.postActions();
-      },
-
-      // 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;
-
-        // Unload any tabs that can be unloaded.
-        for (let [tab, state] of this.tabState) {
-          if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
-            continue;
-          }
-
-          if (state == this.STATE_LOADED &&
-              !this.maybeVisibleTabs.has(tab) &&
-              tab !== this.lastVisibleTab &&
-              tab !== this.loadingTab &&
-              tab !== this.requestedTab) {
-            this.setTabState(tab, this.STATE_UNLOADING);
-          }
-
-          if (state != this.STATE_UNLOADED && tab !== this.requestedTab) {
-            numPending++;
-          }
-        }
-
-        if (numPending) {
-          // Keep the timer going since there may be more tabs to unload.
-          this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
-        }
-      },
-
-      // Fires when an ongoing load has taken too long.
-      onLoadTimeout() {
-        this.logState("onLoadTimeout");
-        this.preActions();
-        this.loadTimer = null;
-        this.loadingTab = null;
-        this.postActions();
-      },
-
-      // Fires when the layers become available for a tab.
-      onLayersReady(browser) {
-        let tab = this.tabbrowser.getTabForBrowser(browser);
-        if (!tab) {
-          // We probably got a layer update from a tab that got before
-          // the switcher was created, or for browser that's not being
-          // tracked by the async tab switcher (like the preloaded about:newtab).
-          return;
-        }
-
-        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.unwarmTab(tab);
-
-        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();
-      },
-
-      // 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 ||
-            this.getTabState(tab) == this.STATE_UNLOADED);
-          this.setTabState(tab, this.STATE_UNLOADED);
-        }
-      },
-
-      // Called when a tab switches from remote to non-remote. In this case
-      // a MozLayerTreeReady notification that we requested may never fire,
-      // so we need to simulate it.
-      onRemotenessChange(tab) {
-        this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`);
-        if (!tab.linkedBrowser.isRemoteBrowser) {
-          if (this.getTabState(tab) == this.STATE_LOADING) {
-            this.onLayersReady(tab.linkedBrowser);
-          } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
-            this.onLayersCleared(tab.linkedBrowser);
-          }
-        } else if (this.getTabState(tab) == this.STATE_LOADED) {
-          // A tab just changed from non-remote to remote, which means
-          // that it's gone back into the STATE_LOADING state until
-          // it sends up a layer tree.
-          this.setTabState(tab, this.STATE_LOADING);
-        }
-      },
-
-      // Called when a tab has been removed, and the browser node is
-      // about to be removed from the DOM.
-      onTabRemoved(tab) {
-        if (this.lastVisibleTab == tab) {
-          // The browser that was being presented to the user is
-          // going to be removed during this tick of the event loop.
-          // This will cause us to show a tab spinner instead.
-          this.preActions();
-          this.lastVisibleTab = null;
-          this.postActions();
-        }
-      },
-
-      onSizeModeOrOcclusionStateChange() {
-        if (this.minimizedOrFullyOccluded) {
-          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_LOADING || state == this.STATE_LOADED) {
-              this.setTabState(tab, this.STATE_UNLOADING);
-            }
-          }
-          if (this.loadTimer) {
-            this.clearTimer(this.loadTimer);
-            this.loadTimer = null;
-          }
-          this.loadingTab = null;
-        } else {
-          // We're no longer minimized or occluded. This means we might want
-          // to activate the current tab's docShell.
-          this.maybeActivateDocShell(gBrowser.selectedTab);
-        }
-      },
-
-      onSwapDocShells(ourBrowser, otherBrowser) {
-        // This event fires before the swap. ourBrowser is from
-        // our window. We save the state of otherBrowser since ourBrowser
-        // needs to take on that state at the end of the swap.
-
-        let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
-        let otherState;
-        if (otherTabbrowser && otherTabbrowser._switcher) {
-          let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
-          let otherSwitcher = otherTabbrowser._switcher;
-          otherState = otherSwitcher.getTabState(otherTab);
-        } else {
-          otherState = otherBrowser.docShellIsActive ? this.STATE_LOADED : this.STATE_UNLOADED;
-        }
-        if (!this.swapMap) {
-          this.swapMap = new WeakMap();
-        }
-        this.swapMap.set(otherBrowser, {
-          state: otherState,
-        });
-      },
-
-      onEndSwapDocShells(ourBrowser, otherBrowser) {
-        // The swap has happened. We reset the loadingTab in
-        // case it has been swapped. We also set ourBrowser's state
-        // to whatever otherBrowser's state was before the swap.
-
-        if (this.loadTimer) {
-          // Clearing the load timer means that we will
-          // immediately display a spinner if ourBrowser isn't
-          // ready yet. Typically it will already be ready
-          // though. If it's not, we're probably in a new window,
-          // in which case we have no other tabs to display anyway.
-          this.clearTimer(this.loadTimer);
-          this.loadTimer = null;
-        }
-        this.loadingTab = null;
-
-        let { state: otherState } = this.swapMap.get(otherBrowser);
-
-        this.swapMap.delete(otherBrowser);
-
-        let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
-        if (ourTab) {
-          this.setTabStateNoAction(ourTab, otherState);
-        }
-      },
-
-      shouldActivateDocShell(browser) {
-        let tab = this.tabbrowser.getTabForBrowser(browser);
-        let state = this.getTabState(tab);
-        return state == this.STATE_LOADING || state == this.STATE_LOADED;
-      },
-
-      activateBrowserForPrintPreview(browser) {
-        let tab = this.tabbrowser.getTabForBrowser(browser);
-        let state = this.getTabState(tab);
-        if (state != this.STATE_LOADING &&
-            state != this.STATE_LOADED) {
-          this.setTabState(tab, this.STATE_LOADING);
-          this.logState("Activated browser " + this.tinfo(tab) + " for print preview");
-        }
-      },
-
-      canWarmTab(tab) {
-        if (!this.tabbrowser.tabWarmingEnabled) {
-          return false;
-        }
-
-        if (!tab) {
-          return false;
-        }
-
-        // 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.tabbrowser.tabWarmingUnloadDelay);
-      },
-
-      // Called when the user asks to switch to a given tab.
-      requestTab(tab) {
-        if (tab === this.requestedTab) {
-          return;
-        }
-
-        if (this.tabbrowser.tabWarmingEnabled) {
-          let warmingState = "disqualified";
-
-          if (this.warmingTabs.has(tab)) {
-            let tabState = this.getTabState(tab);
-            if (tabState == this.STATE_LOADING) {
-              warmingState = "stillLoading";
-            } else if (tabState == this.STATE_LOADED) {
-              warmingState = "loaded";
-            }
-          } else if (this.canWarmTab(tab)) {
-            warmingState = "notWarmed";
-          }
-
-          Services.telemetry
-            .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
-            .add(warmingState);
-        }
-
-        this._requestingTab = true;
-        this.logState("requestTab " + this.tinfo(tab));
-        this.startTabSwitch();
-
-        this.requestedTab = tab;
-
-        tab.linkedBrowser.setAttribute("primary", "true");
-        if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
-          this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
-        }
-        this.lastPrimaryTab = tab;
-
-        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(), unloadTimeout);
-
-        this.postActions();
-      },
-
-      handleEvent(event, delayed = false) {
-        if (this._processing) {
-          this.setTimer(() => this.handleEvent(event, true), 0);
-          return;
-        }
-        if (delayed && this.tabbrowser._switcher != this) {
-          // if we delayed processing this event, we might be out of date, in which
-          // case we drop the delayed events
-          return;
-        }
-        this._processing = true;
-        this.preActions();
-
-        if (event.type == "MozLayerTreeReady") {
-          this.onLayersReady(event.originalTarget);
-        }
-        if (event.type == "MozAfterPaint") {
-          this.onPaint();
-        } else if (event.type == "MozLayerTreeCleared") {
-          this.onLayersCleared(event.originalTarget);
-        } else if (event.type == "TabRemotenessChange") {
-          this.onRemotenessChange(event.target);
-        } else if (event.type == "sizemodechange" ||
-          event.type == "occlusionstatechange") {
-          this.onSizeModeOrOcclusionStateChange();
-        } else if (event.type == "SwapDocShells") {
-          this.onSwapDocShells(event.originalTarget, event.detail);
-        } else if (event.type == "EndSwapDocShells") {
-          this.onEndSwapDocShells(event.originalTarget, event.detail);
-        }
-
-        this.postActions();
-        this._processing = false;
-      },
-
-      /*
-       * Telemetry and Profiler related helpers for recording tab switch
-       * timing.
-       */
-
-      startTabSwitch() {
-        TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
-        TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
-        this.addMarker("AsyncTabSwitch:Start");
-        this.switchInProgress = true;
-      },
-
-      /**
-       * Something has occurred that might mean that we've completed
-       * 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)) {
-          // 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", window);
-          if (time != -1) {
-            TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
-            this.log("DEBUG: tab switch time = " + time);
-            this.addMarker("AsyncTabSwitch:Finish");
-          }
-          this.switchInProgress = false;
-        }
-      },
-
-      spinnerDisplayed() {
-        this.assert(!this.spinnerTab);
-        let browser = this.requestedTab.linkedBrowser;
-        this.assert(browser.isRemoteBrowser);
-        TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
-        // We have a second, similar probe for capturing recordings of
-        // when the spinner is displayed for very long periods.
-        TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
-        this.addMarker("AsyncTabSwitch:SpinnerShown");
-      },
-
-      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
-      },
-
-      addMarker(marker) {
-        if (Services.profiler) {
-          Services.profiler.AddMarker(marker);
-        }
-      },
-
-      /*
-       * Debug related logging for switcher.
-       */
-
-      _useDumpForLogging: false,
-      _logInit: false,
-
-      logging() {
-        if (this._useDumpForLogging)
-          return true;
-        if (this._logInit)
-          return this._shouldLog;
-        let result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming", false);
-        this._shouldLog = result;
-        this._logInit = true;
-        return this._shouldLog;
-      },
-
-      tinfo(tab) {
-        if (tab) {
-          return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
-        }
-        return "null";
-      },
-
-      log(s) {
-        if (!this.logging())
-          return;
-        if (this._useDumpForLogging) {
-          dump(s + "\n");
-        } else {
-          Services.console.logStringMessage(s);
-        }
-      },
-
-      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");
-        } else {
-          Services.console.logStringMessage(accum);
-        }
-      },
-    };
-    this._switcher = switcher;
-    switcher.init();
-    return switcher;
+    return this._switcher;
   }
 
   warmupTab(aTab) {
     if (gMultiProcessBrowser) {
       this._getSwitcher().warmupTab(aTab);
     }
   }
 
copy from browser/base/content/tabbrowser.js
copy to browser/modules/AsyncTabSwitcher.jsm
--- a/browser/base/content/tabbrowser.js
+++ b/browser/modules/AsyncTabSwitcher.jsm
@@ -1,5730 +1,1088 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * 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/. */
 
-/* eslint-env mozilla/browser-window */
-
-class TabBrowser {
-  constructor(container) {
-    this.container = container;
-    this.requiresAddonInterpositions = true;
+"use strict";
 
-    // Pass along any used DOM methods to the container node. When this object turns
-    // into a custom element this won't be needed anymore.
-    this.addEventListener = this.container.addEventListener.bind(this.container);
-    this.removeEventListener = this.container.removeEventListener.bind(this.container);
-    this.dispatchEvent = this.container.dispatchEvent.bind(this.container);
-    this.getAttribute = this.container.getAttribute.bind(this.container);
-    this.hasAttribute = this.container.hasAttribute.bind(this.container);
-    this.setAttribute = this.container.setAttribute.bind(this.container);
-    this.removeAttribute = this.container.removeAttribute.bind(this.container);
-    this.appendChild = this.container.appendChild.bind(this.container);
-    this.ownerGlobal = this.container.ownerGlobal;
-    this.ownerDocument = this.container.ownerDocument;
-    this.namespaceURI = this.container.namespaceURI;
-    this.style = this.container.style;
-
-    XPCOMUtils.defineLazyServiceGetters(this, {
-      _unifiedComplete: ["@mozilla.org/autocomplete/search;1?name=unifiedcomplete", "mozIPlacesAutoComplete"],
-      serializationHelper: ["@mozilla.org/network/serialization-helper;1", "nsISerializationHelper"],
-      mURIFixup: ["@mozilla.org/docshell/urifixup;1", "nsIURIFixup"],
-    });
+var EXPORTED_SYMBOLS = ["AsyncTabSwitcher"];
 
-    XPCOMUtils.defineLazyGetter(this, "initialBrowser", () => {
-      return document.getAnonymousElementByAttribute(this.container, "anonid", "initialBrowser");
-    });
-    XPCOMUtils.defineLazyGetter(this, "tabContainer", () => {
-      return document.getElementById(this.getAttribute("tabcontainer"));
-    });
-    XPCOMUtils.defineLazyGetter(this, "tabs", () => {
-      return this.tabContainer.childNodes;
-    });
-    XPCOMUtils.defineLazyGetter(this, "tabbox", () => {
-      return document.getAnonymousElementByAttribute(this.container, "anonid", "tabbox");
-    });
-    XPCOMUtils.defineLazyGetter(this, "mPanelContainer", () => {
-      return document.getAnonymousElementByAttribute(this.container, "anonid", "panelcontainer");
-    });
-
-    this.closingTabsEnum = { ALL: 0, OTHER: 1, TO_END: 2 };
-
-    this._visibleTabs = null;
-
-    this.mCurrentTab = null;
-
-    this._lastRelatedTabMap = new WeakMap();
-
-    this.mCurrentBrowser = null;
-
-    this.mProgressListeners = [];
-
-    this.mTabsProgressListeners = [];
-
-    this._tabListeners = new Map();
-
-    this._tabFilters = new Map();
+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",
+});
 
-    this.mIsBusy = false;
-
-    this._outerWindowIDBrowserMap = new Map();
-
-    this.arrowKeysShouldWrap = AppConstants == "macosx";
-
-    this._autoScrollPopup = null;
-
-    this._previewMode = false;
-
-    this._lastFindValue = "";
-
-    this._contentWaitingCount = 0;
-
-    this.tabAnimationsInProgress = 0;
-
-    /**
-     * Binding from browser to tab
-     */
-    this._tabForBrowser = new WeakMap();
-
-    /**
-     * Holds a unique ID for the tab change that's currently being timed.
-     * Used to make sure that multiple, rapid tab switches do not try to
-     * create overlapping timers.
-     */
-    this._tabSwitchID = null;
-
-    this._preloadedBrowser = null;
+/**
+ * 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
+ * maintains state about whether the layer tree for the tab is
+ * available, being loaded, being unloaded, or unavailable. It
+ * also keeps track of the tab currently being displayed, the tab
+ * it's trying to load, and the tab the user has asked to switch
+ * to. The switcher object is created upon tab switch. It is
+ * released when there are no pending tabs to load or unload.
+ *
+ * The following general principles have guided the design:
+ *
+ * 1. We only request one layer tree at a time. If the user
+ * switches to a different tab while waiting, we don't request
+ * the new layer tree until the old tab has loaded or timed out.
+ *
+ * 2. If loading the layers for a tab times out, we show the
+ * spinner and possibly request the layer tree for another tab if
+ * the user has requested one.
+ *
+ * 3. We discard layer trees on a delay. This way, if the user is
+ * switching among the same tabs frequently, we don't continually
+ * load the same tabs.
+ *
+ * It's important that we always show either the spinner or a tab
+ * whose layers are available. Otherwise the compositor will draw
+ * an entirely black frame, which is very jarring. To ensure this
+ * never happens when switching away from a tab, we assume the
+ * old tab might still be drawn until a MozAfterPaint event
+ * occurs. Because layout and compositing happen asynchronously,
+ * we don't have any other way of knowing when the switch
+ * actually takes place. Therefore, we don't unload the old tab
+ * until the next MozAfterPaint event.
+ */
+class AsyncTabSwitcher {
+  constructor(tabbrowser) {
+    this.log("START");
 
-    /**
-     * `_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.
-     */
-    this._browserBindingProperties = [
-      "canGoBack", "canGoForward", "goBack", "goForward", "permitUnload",
-      "reload", "reloadWithFlags", "stop", "loadURI", "loadURIWithFlags",
-      "goHome", "homePage", "gotoIndex", "currentURI", "documentURI",
-      "preferences", "imageDocument", "isRemoteBrowser", "messageManager",
-      "getTabBrowser", "finder", "fastFind", "sessionHistory", "contentTitle",
-      "characterSet", "fullZoom", "textZoom", "webProgress",
-      "addProgressListener", "removeProgressListener", "audioPlaybackStarted",
-      "audioPlaybackStopped", "pauseMedia", "stopMedia",
-      "resumeMedia", "mute", "unmute", "blockedPopups", "lastURI",
-      "purgeSessionHistory", "stopScroll", "startScroll",
-      "userTypedValue", "userTypedClear", "mediaBlocked",
-      "didStartLoadSinceLastUserTyping"
-    ];
+    // How long to wait for a tab's layers to load. After this
+    // time elapses, we're free to put up the spinner and start
+    // trying to load a different tab.
+    this.TAB_SWITCH_TIMEOUT = 400; // ms
+
+    // When the user hasn't switched tabs for this long, we unload
+    // layers for all tabs that aren't in use.
+    this.UNLOAD_DELAY = 300; // ms
 
-    this._removingTabs = [];
+    // The next three tabs form the principal state variables.
+    // See the assertions in postActions for their invariants.
 
-    /**
-     * Tab close requests are ignored if the window is closing anyway,
-     * e.g. when holding Ctrl+W.
-     */
-    this._windowIsClosing = false;
+    // Tab the user requested most recently.
+    this.requestedTab = tabbrowser.selectedTab;
+
+    // Tab we're currently trying to load.
+    this.loadingTab = null;
 
-    // This defines a proxy which allows us to access browsers by
-    // index without actually creating a full array of browsers.
-    this.browsers = new Proxy([], {
-      has: (target, name) => {
-        if (typeof name == "string" && Number.isInteger(parseInt(name))) {
-          return (name in this.tabs);
-        }
-        return false;
-      },
-      get: (target, name) => {
-        if (name == "length") {
-          return this.tabs.length;
-        }
-        if (typeof name == "string" && Number.isInteger(parseInt(name))) {
-          if (!(name in this.tabs)) {
-            return undefined;
-          }
-          return this.tabs[name].linkedBrowser;
-        }
-        return target[name];
-      }
-    });
+    // We show this tab in case the requestedTab hasn't loaded yet.
+    this.lastVisibleTab = tabbrowser.selectedTab;
+
+    // Auxilliary state variables:
 
-    /**
-     * List of browsers whose docshells must be active in order for print preview
-     * to work.
-     */
-    this._printPreviewBrowsers = new Set();
+    this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen.
+    this.spinnerTab = null; // Tab showing a spinner.
+    this.blankTab = null; // Tab showing blank.
+    this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true"
 
-    this._switcher = null;
-
-    this._tabMinWidthLimit = 50;
-
-    this._soundPlayingAttrRemovalTimer = 0;
+    this.tabbrowser = tabbrowser;
+    this.window = tabbrowser.ownerGlobal;
+    this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance.
+    this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance.
 
-    this._hoverTabTimer = null;
-
-    this.mCurrentBrowser = document.getAnonymousElementByAttribute(this.container, "anonid", "initialBrowser");
-    this.mCurrentBrowser.permanentKey = {};
+    // Map from tabs to STATE_* (below).
+    this.tabState = new Map();
 
-    CustomizableUI.addListener(this);
-    this._updateNewTabVisibility();
-
-    Services.obs.addObserver(this, "contextual-identity-updated");
+    // True if we're in the midst of switching tabs.
+    this.switchInProgress = false;
 
-    this.mCurrentTab = this.tabContainer.firstChild;
-    const nsIEventListenerService =
-      Ci.nsIEventListenerService;
-    let els = Cc["@mozilla.org/eventlistenerservice;1"]
-      .getService(nsIEventListenerService);
-    els.addSystemEventListener(document, "keydown", this, false);
-    if (AppConstants.platform == "macosx") {
-      els.addSystemEventListener(document, "keypress", this, false);
-    }
-    window.addEventListener("sizemodechange", this);
-    window.addEventListener("occlusionstatechange", this);
+    // Keep an exact list of content processes (tabParent) in which
+    // we're actively suppressing the display port. This gives a robust
+    // way to make sure we don't forget to un-suppress.
+    this.activeSuppressDisplayport = new Set();
 
-    var uniqueId = this._generateUniquePanelID();
-    this.mPanelContainer.childNodes[0].id = uniqueId;
-    this.mCurrentTab.linkedPanel = uniqueId;
-    this.mCurrentTab.permanentKey = this.mCurrentBrowser.permanentKey;
-    this.mCurrentTab._tPos = 0;
-    this.mCurrentTab._fullyOpen = true;
-    this.mCurrentTab.linkedBrowser = this.mCurrentBrowser;
-    this._tabForBrowser.set(this.mCurrentBrowser, this.mCurrentTab);
-
-    // set up the shared autoscroll popup
-    this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
-    this._autoScrollPopup.id = "autoscroller";
-    this.appendChild(this._autoScrollPopup);
-    this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
-    this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
+    // 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.
+    this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]);
 
-    // Hook up the event listeners to the first browser
-    var tabListener = new TabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true, false);
-    const nsIWebProgress = Ci.nsIWebProgress;
-    const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
-      .createInstance(nsIWebProgress);
-    filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
-    this._tabListeners.set(this.mCurrentTab, tabListener);
-    this._tabFilters.set(this.mCurrentTab, filter);
-    this.webProgress.addProgressListener(filter, nsIWebProgress.NOTIFY_ALL);
-
-    if (Services.prefs.getBoolPref("browser.display.use_system_colors"))
-      this.style.backgroundColor = "-moz-default-background-color";
-    else if (Services.prefs.getIntPref("browser.display.document_color_use") == 2)
-      this.style.backgroundColor = Services.prefs.getCharPref("browser.display.background_color");
-
-    let messageManager = window.getGroupMessageManager("browsers");
+    // 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.
+    this.warmingTabs = new WeakSet();
 
-    let remote = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIWebNavigation)
-                       .QueryInterface(Ci.nsILoadContext)
-                       .useRemoteTabs;
-    if (remote) {
-      messageManager.addMessageListener("DOMTitleChanged", this);
-      messageManager.addMessageListener("DOMWindowClose", this);
-      window.messageManager.addMessageListener("contextmenu", this);
-      messageManager.addMessageListener("Browser:Init", this);
+    this.STATE_UNLOADED = 0;
+    this.STATE_LOADING = 1;
+    this.STATE_LOADED = 2;
+    this.STATE_UNLOADING = 3;
 
-      // If this window has remote tabs, switch to our tabpanels fork
-      // which does asynchronous tab switching.
-      this.mPanelContainer.classList.add("tabbrowser-tabpanels");
-    } else {
-      this._outerWindowIDBrowserMap.set(this.mCurrentBrowser.outerWindowID,
-        this.mCurrentBrowser);
-    }
-    messageManager.addMessageListener("DOMWindowFocus", this);
-    messageManager.addMessageListener("RefreshBlocker:Blocked", this);
-    messageManager.addMessageListener("Browser:WindowCreated", this);
+    // re-entrancy guard:
+    this._processing = false;
 
-    // To correctly handle keypresses for potential FindAsYouType, while
-    // the tab's find bar is not yet initialized.
-    this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
-    Services.prefs.addObserver("accessibility.typeaheadfind", this);
-    messageManager.addMessageListener("Findbar:Keypress", this);
+    this._useDumpForLogging = false;
+    this._logInit = false;
 
-    XPCOMUtils.defineLazyPreferenceGetter(this, "animationsEnabled",
-      "toolkit.cosmeticAnimations.enabled", true);
-    XPCOMUtils.defineLazyPreferenceGetter(this, "schedulePressureDefaultCount",
-      "browser.schedulePressure.defaultCount", 3);
-    XPCOMUtils.defineLazyPreferenceGetter(this, "tabWarmingEnabled",
-      "browser.tabs.remote.warmup.enabled", false);
-    XPCOMUtils.defineLazyPreferenceGetter(this, "tabWarmingMax",
-      "browser.tabs.remote.warmup.maxTabs", 3);
-    XPCOMUtils.defineLazyPreferenceGetter(this, "tabWarmingUnloadDelay" /* ms */,
-      "browser.tabs.remote.warmup.unloadDelayMs", 2000);
-    XPCOMUtils.defineLazyPreferenceGetter(this, "tabMinWidthPref",
-      "browser.tabs.tabMinWidth", this._tabMinWidthLimit,
-      (pref, prevValue, newValue) => this.tabMinWidth = newValue,
-      newValue => Math.max(newValue, this._tabMinWidthLimit),
-    );
+    this.window.addEventListener("MozAfterPaint", this);
+    this.window.addEventListener("MozLayerTreeReady", this);
+    this.window.addEventListener("MozLayerTreeCleared", this);
+    this.window.addEventListener("TabRemotenessChange", this);
+    this.window.addEventListener("sizemodechange", this);
+    this.window.addEventListener("occlusionstatechange", this);
+    this.window.addEventListener("SwapDocShells", this, true);
+    this.window.addEventListener("EndSwapDocShells", this, true);
 
-    this.tabMinWidth = this.tabMinWidthPref;
-
-    this._setupEventListeners();
-  }
+    let initialTab = this.requestedTab;
+    let initialBrowser = initialTab.linkedBrowser;
 
-  get tabContextMenu() {
-    return this.tabContainer.contextMenu;
-  }
-
-  get visibleTabs() {
-    if (!this._visibleTabs)
-      this._visibleTabs = Array.filter(this.tabs,
-        tab => !tab.hidden && !tab.closing);
-    return this._visibleTabs;
-  }
+    let tabIsLoaded = !initialBrowser.isRemoteBrowser ||
+      initialBrowser.frameLoader.tabParent.hasLayers;
 
-  get _numPinnedTabs() {
-    for (var i = 0; i < this.tabs.length; i++) {
-      if (!this.tabs[i].pinned)
-        break;
-    }
-    return i;
-  }
+    // 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.
+    initialBrowser.preserveLayers(false);
 
-  get popupAnchor() {
-    if (this.mCurrentTab._popupAnchor) {
-      return this.mCurrentTab._popupAnchor;
+    if (!this.minimizedOrFullyOccluded) {
+      this.log("Initial tab is loaded?: " + tabIsLoaded);
+      this.setTabState(initialTab, tabIsLoaded ? this.STATE_LOADED
+                                               : this.STATE_LOADING);
     }
-    let stack = this.mCurrentBrowser.parentNode;
-    // Create an anchor for the popup
-    const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    let popupAnchor = document.createElementNS(NS_XUL, "hbox");
-    popupAnchor.className = "popup-anchor";
-    popupAnchor.hidden = true;
-    stack.appendChild(popupAnchor);
-    return this.mCurrentTab._popupAnchor = popupAnchor;
-  }
 
-  set selectedTab(val) {
-    if (gNavToolbox.collapsed && !this._allowTabChange) {
-      return this.tabbox.selectedTab;
-    }
-    // Update the tab
-    this.tabbox.selectedTab = val;
-    return val;
-  }
-
-  get selectedTab() {
-    return this.mCurrentTab;
-  }
-
-  get selectedBrowser() {
-    return this.mCurrentBrowser;
-  }
-
-  /**
-   * BEGIN FORWARDED BROWSER PROPERTIES.  IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
-   * MAKE SURE TO ADD IT HERE AS WELL.
-   */
-  get canGoBack() {
-    return this.mCurrentBrowser.canGoBack;
-  }
-
-  get canGoForward() {
-    return this.mCurrentBrowser.canGoForward;
-  }
-
-  goBack() {
-    return this.mCurrentBrowser.goBack();
-  }
-
-  goForward() {
-    return this.mCurrentBrowser.goForward();
-  }
-
-  reload() {
-    return this.mCurrentBrowser.reload();
-  }
-
-  reloadWithFlags(aFlags) {
-    return this.mCurrentBrowser.reloadWithFlags(aFlags);
-  }
-
-  stop() {
-    return this.mCurrentBrowser.stop();
-  }
-
-  /**
-   * throws exception for unknown schemes
-   */
-  loadURI(aURI, aReferrerURI, aCharset) {
-    return this.mCurrentBrowser.loadURI(aURI, aReferrerURI, aCharset);
-  }
-
-  /**
-   * throws exception for unknown schemes
-   */
-  loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData) {
-    // Note - the callee understands both:
-    // (a) loadURIWithFlags(aURI, aFlags, ...)
-    // (b) loadURIWithFlags(aURI, { flags: aFlags, ... })
-    // Forwarding it as (a) here actually supports both (a) and (b),
-    // so you can call us either way too.
-    return this.mCurrentBrowser.loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData);
-  }
-
-  goHome() {
-    return this.mCurrentBrowser.goHome();
-  }
-
-  gotoIndex(aIndex) {
-    return this.mCurrentBrowser.gotoIndex(aIndex);
-  }
-
-  set homePage(val) {
-    this.mCurrentBrowser.homePage = val;
-    return val;
-  }
-
-  get homePage() {
-    return this.mCurrentBrowser.homePage;
-  }
-
-  get currentURI() {
-    return this.mCurrentBrowser.currentURI;
-  }
-
-  get finder() {
-    return this.mCurrentBrowser.finder;
-  }
-
-  get docShell() {
-    return this.mCurrentBrowser.docShell;
-  }
-
-  get webNavigation() {
-    return this.mCurrentBrowser.webNavigation;
-  }
-
-  get webBrowserFind() {
-    return this.mCurrentBrowser.webBrowserFind;
-  }
-
-  get webProgress() {
-    return this.mCurrentBrowser.webProgress;
-  }
-
-  get contentWindow() {
-    return this.mCurrentBrowser.contentWindow;
-  }
-
-  get contentWindowAsCPOW() {
-    return this.mCurrentBrowser.contentWindowAsCPOW;
-  }
-
-  get sessionHistory() {
-    return this.mCurrentBrowser.sessionHistory;
-  }
-
-  get markupDocumentViewer() {
-    return this.mCurrentBrowser.markupDocumentViewer;
-  }
-
-  get contentDocument() {
-    return this.mCurrentBrowser.contentDocument;
-  }
-
-  get contentDocumentAsCPOW() {
-    return this.mCurrentBrowser.contentDocumentAsCPOW;
-  }
-
-  get contentTitle() {
-    return this.mCurrentBrowser.contentTitle;
-  }
-
-  get contentPrincipal() {
-    return this.mCurrentBrowser.contentPrincipal;
-  }
-
-  get securityUI() {
-    return this.mCurrentBrowser.securityUI;
-  }
-
-  set fullZoom(val) {
-    this.mCurrentBrowser.fullZoom = val;
-  }
-
-  get fullZoom() {
-    return this.mCurrentBrowser.fullZoom;
-  }
-
-  set textZoom(val) {
-    this.mCurrentBrowser.textZoom = val;
-  }
-
-  get textZoom() {
-    return this.mCurrentBrowser.textZoom;
-  }
-
-  get isSyntheticDocument() {
-    return this.mCurrentBrowser.isSyntheticDocument;
-  }
-
-  set userTypedValue(val) {
-    return this.mCurrentBrowser.userTypedValue = val;
-  }
-
-  get userTypedValue() {
-    return this.mCurrentBrowser.userTypedValue;
-  }
-
-  set tabMinWidth(val) {
-    this.tabContainer.style.setProperty("--tab-min-width", val + "px");
-    return val;
-  }
-
-  isFindBarInitialized(aTab) {
-    return (aTab || this.selectedTab)._findBar != undefined;
-  }
-
-  getFindBar(aTab) {
-    if (!aTab)
-      aTab = this.selectedTab;
-
-    if (aTab._findBar)
-      return aTab._findBar;
-
-    let findBar = document.createElementNS(this.namespaceURI, "findbar");
-    let browser = this.getBrowserForTab(aTab);
-    let browserContainer = this.getBrowserContainer(browser);
-    browserContainer.appendChild(findBar);
-
-    // Force a style flush to ensure that our binding is attached.
-    findBar.clientTop;
-
-    findBar.browser = browser;
-    findBar._findField.value = this._lastFindValue;
-
-    aTab._findBar = findBar;
-
-    let event = document.createEvent("Events");
-    event.initEvent("TabFindInitialized", true, false);
-    aTab.dispatchEvent(event);
-
-    return findBar;
-  }
-
-  getStatusPanel() {
-    if (!this._statusPanel) {
-      this._statusPanel = document.createElementNS(this.namespaceURI, "statuspanel");
-      this._statusPanel.setAttribute("inactive", "true");
-      this._statusPanel.setAttribute("layer", "true");
-      this._appendStatusPanel();
-    }
-    return this._statusPanel;
-  }
-
-  _appendStatusPanel() {
-    if (this._statusPanel) {
-      let browser = this.selectedBrowser;
-      let browserContainer = this.getBrowserContainer(browser);
-      browserContainer.insertBefore(this._statusPanel, browser.parentNode.nextSibling);
-    }
-  }
-
-  pinTab(aTab) {
-    if (aTab.pinned)
-      return;
-
-    if (aTab.hidden)
-      this.showTab(aTab);
-
-    this.moveTabTo(aTab, this._numPinnedTabs);
-    aTab.setAttribute("pinned", "true");
-    this.tabContainer._unlockTabSizing();
-    this.tabContainer._positionPinnedTabs();
-    this.tabContainer._updateCloseButtons();
-
-    this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: true });
-
-    let event = document.createEvent("Events");
-    event.initEvent("TabPinned", true, false);
-    aTab.dispatchEvent(event);
-  }
-
-  unpinTab(aTab) {
-    if (!aTab.pinned)
-      return;
-
-    this.moveTabTo(aTab, this._numPinnedTabs - 1);
-    aTab.removeAttribute("pinned");
-    aTab.style.marginInlineStart = "";
-    this.tabContainer._unlockTabSizing();
-    this.tabContainer._positionPinnedTabs();
-    this.tabContainer._updateCloseButtons();
-
-    this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: false });
-
-    let event = document.createEvent("Events");
-    event.initEvent("TabUnpinned", true, false);
-    aTab.dispatchEvent(event);
-  }
-
-  previewTab(aTab, aCallback) {
-    let currentTab = this.selectedTab;
-    try {
-      // Suppress focus, ownership and selected tab changes
-      this._previewMode = true;
-      this.selectedTab = aTab;
-      aCallback();
-    } finally {
-      this.selectedTab = currentTab;
-      this._previewMode = false;
+    for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
+      let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
+      let state = ppBrowser.hasLayers ? this.STATE_LOADED
+                                      : this.STATE_LOADING;
+      this.setTabState(ppTab, state);
     }
   }
 
-  syncThrobberAnimations(aTab) {
-    aTab.ownerGlobal.promiseDocumentFlushed(() => {
-      if (!aTab.parentNode) {
-        return;
-      }
-
-      const animations =
-        Array.from(aTab.parentNode.getElementsByTagName("tab"))
-        .map(tab => {
-          const throbber =
-            document.getAnonymousElementByAttribute(tab, "anonid", "tab-throbber");
-          return throbber ? throbber.getAnimations({ subtree: true }) : [];
-        })
-        .reduce((a, b) => a.concat(b))
-        .filter(anim =>
-          anim instanceof CSSAnimation &&
-          (anim.animationName === "tab-throbber-animation" ||
-            anim.animationName === "tab-throbber-animation-rtl") &&
-          (anim.playState === "running" || anim.playState === "pending"));
-
-      // Synchronize with the oldest running animation, if any.
-      const firstStartTime = Math.min(
-        ...animations.map(anim => anim.startTime === null ? Infinity : anim.startTime)
-      );
-      if (firstStartTime === Infinity) {
-        return;
-      }
-      requestAnimationFrame(() => {
-        for (let animation of animations) {
-          // If |animation| has been cancelled since this rAF callback
-          // was scheduled we don't want to set its startTime since
-          // that would restart it. We check for a cancelled animation
-          // by looking for a null currentTime rather than checking
-          // the playState, since reading the playState of
-          // a CSSAnimation object will flush style.
-          if (animation.currentTime !== null) {
-            animation.startTime = firstStartTime;
-          }
-        }
-      });
-    });
-  }
-
-  getBrowserAtIndex(aIndex) {
-    return this.browsers[aIndex];
-  }
-
-  getBrowserIndexForDocument(aDocument) {
-    var tab = this._getTabForContentWindow(aDocument.defaultView);
-    return tab ? tab._tPos : -1;
-  }
-
-  getBrowserForDocument(aDocument) {
-    var tab = this._getTabForContentWindow(aDocument.defaultView);
-    return tab ? tab.linkedBrowser : null;
-  }
-
-  getBrowserForContentWindow(aWindow) {
-    var tab = this._getTabForContentWindow(aWindow);
-    return tab ? tab.linkedBrowser : null;
-  }
-
-  getBrowserForOuterWindowID(aID) {
-    return this._outerWindowIDBrowserMap.get(aID);
-  }
-
-  _getTabForContentWindow(aWindow) {
-    // When not using remote browsers, we can take a fast path by getting
-    // directly from the content window to the browser without looping
-    // over all browsers.
-    if (!gMultiProcessBrowser) {
-      let browser = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIWebNavigation)
-                           .QueryInterface(Ci.nsIDocShell)
-                           .chromeEventHandler;
-      return this.getTabForBrowser(browser);
+  destroy() {
+    if (this.unloadTimer) {
+      this.clearTimer(this.unloadTimer);
+      this.unloadTimer = null;
+    }
+    if (this.loadTimer) {
+      this.clearTimer(this.loadTimer);
+      this.loadTimer = null;
     }
 
-    for (let i = 0; i < this.browsers.length; i++) {
-      // NB: We use contentWindowAsCPOW so that this code works both
-      // for remote browsers as well. aWindow may be a CPOW.
-      if (this.browsers[i].contentWindowAsCPOW == aWindow)
-        return this.tabs[i];
-    }
-    return null;
-  }
+    this.window.removeEventListener("MozAfterPaint", this);
+    this.window.removeEventListener("MozLayerTreeReady", this);
+    this.window.removeEventListener("MozLayerTreeCleared", this);
+    this.window.removeEventListener("TabRemotenessChange", this);
+    this.window.removeEventListener("sizemodechange", this);
+    this.window.removeEventListener("occlusionstatechange", this);
+    this.window.removeEventListener("SwapDocShells", this, true);
+    this.window.removeEventListener("EndSwapDocShells", this, true);
 
-  getTabForBrowser(aBrowser) {
-    return this._tabForBrowser.get(aBrowser);
+    this.tabbrowser._switcher = null;
+
+    this.activeSuppressDisplayport.forEach(function(tabParent) {
+      tabParent.suppressDisplayport(false);
+    });
+    this.activeSuppressDisplayport.clear();
   }
 
-  getNotificationBox(aBrowser) {
-    return this.getSidebarContainer(aBrowser).parentNode;
-  }
+  // Wraps nsITimer. Must not use the vanilla setTimeout and
+  // clearTimeout, because they will be blocked by nsIPromptService
+  // dialogs.
+  setTimer(callback, timeout) {
+    let event = {
+      notify: callback
+    };
 
-  getSidebarContainer(aBrowser) {
-    return this.getBrowserContainer(aBrowser).parentNode;
+    var timer = Cc["@mozilla.org/timer;1"]
+      .createInstance(Ci.nsITimer);
+    timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+    return timer;
   }
 
-  getBrowserContainer(aBrowser) {
-    return (aBrowser || this.mCurrentBrowser).parentNode.parentNode;
-  }
-
-  getTabModalPromptBox(aBrowser) {
-    let browser = (aBrowser || this.mCurrentBrowser);
-    if (!browser.tabModalPromptBox) {
-      browser.tabModalPromptBox = new TabModalPromptBox(browser);
-    }
-    return browser.tabModalPromptBox;
+  clearTimer(timer) {
+    timer.cancel();
   }
 
-  getTabFromAudioEvent(aEvent) {
-    if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
-      !aEvent.isTrusted) {
-      return null;
-    }
+  getTabState(tab) {
+    let state = this.tabState.get(tab);
 
-    var browser = aEvent.originalTarget;
-    var tab = this.getTabForBrowser(browser);
-    return tab;
-  }
+    // As an optimization, we lazily evaluate the state of tabs
+    // that we've never seen before. Once we've figured it out,
+    // we stash it in our state map.
+    if (state === undefined) {
+      state = this.STATE_UNLOADED;
 
-  _callProgressListeners(aBrowser, aMethod, aArguments, aCallGlobalListeners, aCallTabsListeners) {
-    var rv = true;
-
-    function callListeners(listeners, args) {
-      for (let p of listeners) {
-        if (aMethod in p) {
-          try {
-            if (!p[aMethod].apply(p, args))
-              rv = false;
-          } catch (e) {
-            // don't inhibit other listeners
-            Cu.reportError(e);
-          }
+      if (tab && tab.linkedPanel) {
+        let b = tab.linkedBrowser;
+        if (b.renderLayers && b.hasLayers) {
+          state = this.STATE_LOADED;
+        } else if (b.renderLayers && !b.hasLayers) {
+          state = this.STATE_LOADING;
+        } else if (!b.renderLayers && b.hasLayers) {
+          state = this.STATE_UNLOADING;
         }
       }
+
+      this.setTabStateNoAction(tab, state);
     }
 
-    if (!aBrowser)
-      aBrowser = this.mCurrentBrowser;
-
-    // eslint-disable-next-line mozilla/no-compare-against-boolean-literals
-    if (aCallGlobalListeners != false &&
-        aBrowser == this.mCurrentBrowser) {
-      callListeners(this.mProgressListeners, aArguments);
-    }
-
-    // eslint-disable-next-line mozilla/no-compare-against-boolean-literals
-    if (aCallTabsListeners != false) {
-      aArguments.unshift(aBrowser);
-
-      callListeners(this.mTabsProgressListeners, aArguments);
-    }
-
-    return rv;
+    return state;
   }
 
-  /**
-   * Determine if a URI is an about: page pointing to a local resource.
-   */
-  _isLocalAboutURI(aURI, aResolvedURI) {
-    if (!aURI.schemeIs("about")) {
-      return false;
-    }
-
-    // Specially handle about:blank as local
-    if (aURI.pathQueryRef === "blank") {
-      return true;
-    }
-
-    try {
-      // Use the passed in resolvedURI if we have one
-      const resolvedURI = aResolvedURI || Services.io.newChannelFromURI2(
-        aURI,
-        null, // loadingNode
-        Services.scriptSecurityManager.getSystemPrincipal(), // loadingPrincipal
-        null, // triggeringPrincipal
-        Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, // securityFlags
-        Ci.nsIContentPolicy.TYPE_OTHER // contentPolicyType
-      ).URI;
-      return resolvedURI.schemeIs("jar") || resolvedURI.schemeIs("file");
-    } catch (ex) {
-      // aURI might be invalid.
-      return false;
+  setTabStateNoAction(tab, state) {
+    if (state == this.STATE_UNLOADED) {
+      this.tabState.delete(tab);
+    } else {
+      this.tabState.set(tab, state);
     }
   }
 
-  storeIcon(aBrowser, aURI, aLoadingPrincipal, aRequestContextID) {
-    try {
-      if (!(aURI instanceof Ci.nsIURI)) {
-        aURI = makeURI(aURI);
-      }
-      PlacesUIUtils.loadFavicon(aBrowser, aLoadingPrincipal, aURI, aRequestContextID);
-    } catch (ex) {
-      Cu.reportError(ex);
-    }
-  }
-
-  setIcon(aTab, aURI, aLoadingPrincipal, aRequestContextID) {
-    let browser = this.getBrowserForTab(aTab);
-    browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
-    let loadingPrincipal = aLoadingPrincipal ||
-      Services.scriptSecurityManager.getSystemPrincipal();
-    let requestContextID = aRequestContextID || 0;
-    let sizedIconUrl = browser.mIconURL || "";
-    if (sizedIconUrl != aTab.getAttribute("image")) {
-      if (sizedIconUrl) {
-        if (!browser.mIconLoadingPrincipal ||
-          !browser.mIconLoadingPrincipal.equals(loadingPrincipal)) {
-          aTab.setAttribute("iconloadingprincipal",
-            this.serializationHelper.serializeToString(loadingPrincipal));
-          aTab.setAttribute("requestcontextid", requestContextID);
-          browser.mIconLoadingPrincipal = loadingPrincipal;
-        }
-        aTab.setAttribute("image", sizedIconUrl);
-      } else {
-        aTab.removeAttribute("iconloadingprincipal");
-        delete browser.mIconLoadingPrincipal;
-        aTab.removeAttribute("image");
-      }
-      this._tabAttrModified(aTab, ["image"]);
+  setTabState(tab, state) {
+    if (state == this.getTabState(tab)) {
+      return;
     }
 
-    this._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]);
-  }
+    this.setTabStateNoAction(tab, state);
+
+    let browser = tab.linkedBrowser;
+    let { tabParent } = browser.frameLoader;
+    if (state == this.STATE_LOADING) {
+      this.assert(!this.minimizedOrFullyOccluded);
+
+      if (!this.tabbrowser.tabWarmingEnabled) {
+        browser.docShellIsActive = true;
+      }
 
-  getIcon(aTab) {
-    let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
-    return browser.mIconURL;
-  }
+      if (tabParent) {
+        browser.renderLayers = true;
+      } else {
+        this.onLayersReady(browser);
+      }
+    } else if (state == this.STATE_UNLOADING) {
+      this.unwarmTab(tab);
+      // Setting the docShell to be inactive will also cause it
+      // to stop rendering layers.
+      browser.docShellIsActive = false;
+      if (!tabParent) {
+        this.onLayersCleared(browser);
+      }
+    } else if (state == this.STATE_LOADED) {
+      this.maybeActivateDocShell(tab);
+    }
 
-  setPageInfo(aURL, aDescription, aPreviewImage) {
-    if (aURL) {
-      let pageInfo = { url: aURL, description: aDescription, previewImageURL: aPreviewImage };
-      PlacesUtils.history.update(pageInfo).catch(Cu.reportError);
+    if (!tab.linkedBrowser.isRemoteBrowser) {
+      // setTabState is potentially re-entrant in the non-remote case,
+      // so we must re-get the state for this assertion.
+      let nonRemoteState = this.getTabState(tab);
+      // Non-remote tabs can never stay in the STATE_LOADING
+      // or STATE_UNLOADING states. By the time this function
+      // exits, a non-remote tab must be in STATE_LOADED or
+      // STATE_UNLOADED, since the painting and the layer
+      // upload happen synchronously.
+      this.assert(nonRemoteState == this.STATE_UNLOADED ||
+        nonRemoteState == this.STATE_LOADED);
     }
   }
 
-  shouldLoadFavIcon(aURI) {
-    return (aURI &&
-            Services.prefs.getBoolPref("browser.chrome.site_icons") &&
-            Services.prefs.getBoolPref("browser.chrome.favicons") &&
-            ("schemeIs" in aURI) && (aURI.schemeIs("http") || aURI.schemeIs("https")));
+  get minimizedOrFullyOccluded() {
+    return this.window.windowState == this.window.STATE_MINIMIZED ||
+           this.window.isFullyOccluded;
+  }
+
+  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);
+    this.assert(!this.loadingTab);
+    this.assert(this.lastVisibleTab === this.requestedTab);
+    this.assert(this.minimizedOrFullyOccluded ||
+      this.getTabState(this.requestedTab) == this.STATE_LOADED);
+
+    this.destroy();
+
+    this.window.document.commandDispatcher.unlock();
+
+    let event = new this.window.CustomEvent("TabSwitchDone", {
+      bubbles: true,
+      cancelable: true
+    });
+    this.tabbrowser.dispatchEvent(event);
   }
 
-  useDefaultIcon(aTab) {
-    let browser = this.getBrowserForTab(aTab);
-    let documentURI = browser.documentURI;
-    let requestContextID = browser.contentRequestContextID;
-    let loadingPrincipal = browser.contentPrincipal;
-    let icon = null;
-
-    if (browser.imageDocument) {
-      if (Services.prefs.getBoolPref("browser.chrome.site_icons")) {
-        let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size");
-        if (browser.imageDocument.width <= sz &&
-            browser.imageDocument.height <= sz) {
-          // Don't try to store the icon in Places, regardless it would
-          // be skipped (see Bug 403651).
-          icon = browser.currentURI;
-        }
-      }
-    }
+  // This function is called after all the main state changes to
+  // make sure we display the right tab.
+  updateDisplay() {
+    let requestedTabState = this.getTabState(this.requestedTab);
+    let requestedBrowser = this.requestedTab.linkedBrowser;
 
-    // Use documentURIObject in the check for shouldLoadFavIcon so that we
-    // do the right thing with about:-style error pages.  Bug 453442
-    if (!icon && this.shouldLoadFavIcon(documentURI)) {
-      let url = documentURI.prePath + "/favicon.ico";
-      if (!this.isFailedIcon(url)) {
-        icon = url;
-        this.storeIcon(browser, icon, loadingPrincipal, requestContextID);
-      }
-    }
-
-    this.setIcon(aTab, icon, loadingPrincipal, requestContextID);
-  }
+    // It is often more desirable to show a blank tab when appropriate than
+    // the tab switch spinner - especially since the spinner is usually
+    // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
+    // tab switch. We can hide this lag, and hide the time being spent
+    // constructing TabChild's, layer trees, etc, by showing a blank
+    // tab instead and focusing it immediately.
+    let shouldBeBlank = false;
+    if (requestedBrowser.isRemoteBrowser) {
+      // If a tab is remote and the window is not minimized, we can show a
+      // blank tab instead of a spinner in the following cases:
+      //
+      // 1. The tab has just crashed, and we haven't started showing the
+      //    tab crashed page yet (in this case, the TabParent is null)
+      // 2. The tab has never presented, and has not finished loading
+      //    a non-local-about: page.
+      //
+      // For (2), "finished loading a non-local-about: page" is
+      // determined by the busy state on the tab element and checking
+      // if the loaded URI is local.
+      let hasSufficientlyLoaded = !this.requestedTab.hasAttribute("busy") &&
+        !this.tabbrowser._isLocalAboutURI(requestedBrowser.currentURI);
 
-  isFailedIcon(aURI) {
-    if (!(aURI instanceof Ci.nsIURI))
-      aURI = makeURI(aURI);
-    return PlacesUtils.favicons.isFailedFavicon(aURI);
-  }
-
-  getWindowTitleForBrowser(aBrowser) {
-    var newTitle = "";
-    var docElement = this.ownerDocument.documentElement;
-    var sep = docElement.getAttribute("titlemenuseparator");
-    let tab = this.getTabForBrowser(aBrowser);
-    let docTitle;
-
-    if (tab._labelIsContentTitle) {
-      // Strip out any null bytes in the content title, since the
-      // underlying widget implementations of nsWindow::SetTitle pass
-      // null-terminated strings to system APIs.
-      docTitle = tab.getAttribute("label").replace(/\0/g, "");
+      let fl = requestedBrowser.frameLoader;
+      shouldBeBlank = !this.minimizedOrFullyOccluded &&
+        (!fl.tabParent ||
+          (!hasSufficientlyLoaded && !fl.tabParent.hasPresented));
     }
 
-    if (!docTitle)
-      docTitle = docElement.getAttribute("titledefault");
-
-    var modifier = docElement.getAttribute("titlemodifier");
-    if (docTitle) {
-      newTitle += docElement.getAttribute("titlepreface");
-      newTitle += docTitle;
-      if (modifier)
-        newTitle += sep;
-    }
-    newTitle += modifier;
-
-    // If location bar is hidden and the URL type supports a host,
-    // add the scheme and host to the title to prevent spoofing.
-    // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
-    try {
-      if (docElement.getAttribute("chromehidden").includes("location")) {
-        var uri = this.mURIFixup.createExposableURI(
-          aBrowser.currentURI);
-        if (uri.scheme == "about")
-          newTitle = uri.spec + sep + newTitle;
-        else
-          newTitle = uri.prePath + sep + newTitle;
-      }
-    } catch (e) {}
-
-    return newTitle;
-  }
-
-  updateTitlebar() {
-    this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
-  }
-
-  updateCurrentBrowser(aForceUpdate) {
-    var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
-    if (this.mCurrentBrowser == newBrowser && !aForceUpdate)
-      return;
-
-    if (!aForceUpdate) {
-      document.commandDispatcher.lock();
+    this.log("Tab should be blank: " + shouldBeBlank);
+    this.log("Requested tab is remote?: " + requestedBrowser.isRemoteBrowser);
 
-      TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
-      if (!gMultiProcessBrowser) {
-        // old way of measuring tab paint which is not valid with e10s.
-        // Waiting until the next MozAfterPaint ensures that we capture
-        // the time it takes to paint, upload the textures to the compositor,
-        // and then composite.
-        if (this._tabSwitchID) {
-          TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_MS");
-        }
-
-        let tabSwitchID = Symbol();
-
-        TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS");
-        this._tabSwitchID = tabSwitchID;
-
-        let onMozAfterPaint = () => {
-          if (this._tabSwitchID === tabSwitchID) {
-            TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS");
-            this._tabSwitchID = null;
-          }
-          window.removeEventListener("MozAfterPaint", onMozAfterPaint);
-        };
-        window.addEventListener("MozAfterPaint", onMozAfterPaint);
-      }
-    }
-
-    var oldTab = this.mCurrentTab;
-
-    // Preview mode should not reset the owner
-    if (!this._previewMode && !oldTab.selected)
-      oldTab.owner = null;
-
-    let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
-    if (lastRelatedTab) {
-      if (!lastRelatedTab.selected)
-        lastRelatedTab.owner = null;
-    }
-    this._lastRelatedTabMap = new WeakMap();
-
-    var oldBrowser = this.mCurrentBrowser;
-
-    if (!gMultiProcessBrowser) {
-      oldBrowser.removeAttribute("primary");
-      oldBrowser.docShellIsActive = false;
-      newBrowser.setAttribute("primary", "true");
-      newBrowser.docShellIsActive =
-        (window.windowState != window.STATE_MINIMIZED &&
-          !window.isFullyOccluded);
+    // Figure out which tab we actually want visible right now.
+    let showTab = null;
+    if (requestedTabState != this.STATE_LOADED &&
+        this.lastVisibleTab && this.loadTimer && !shouldBeBlank) {
+      // If we can't show the requestedTab, and lastVisibleTab is
+      // available, show it.
+      showTab = this.lastVisibleTab;
+    } else {
+      // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
+      showTab = this.requestedTab;
     }
 
-    var updateBlockedPopups = false;
-    if ((oldBrowser.blockedPopups && !newBrowser.blockedPopups) ||
-      (!oldBrowser.blockedPopups && newBrowser.blockedPopups))
-      updateBlockedPopups = true;
-
-    this.mCurrentBrowser = newBrowser;
-    this.mCurrentTab = this.tabContainer.selectedItem;
-    this.showTab(this.mCurrentTab);
-
-    gURLBar.setAttribute("switchingtabs", "true");
-    window.addEventListener("MozAfterPaint", function() {
-      gURLBar.removeAttribute("switchingtabs");
-    }, { once: true });
-
-    this._appendStatusPanel();
-
-    if (updateBlockedPopups)
-      this.mCurrentBrowser.updateBlockedPopups();
-
-    // Update the URL bar.
-    var loc = this.mCurrentBrowser.currentURI;
-
-    var webProgress = this.mCurrentBrowser.webProgress;
-    var securityUI = this.mCurrentBrowser.securityUI;
-
-    this._callProgressListeners(null, "onLocationChange",
-                                [webProgress, null, loc, 0],
-                                true, false);
-
-    if (securityUI) {
-      // Include the true final argument to indicate that this event is
-      // simulated (instead of being observed by the webProgressListener).
-      this._callProgressListeners(null, "onSecurityChange",
-                                  [webProgress, null, securityUI.state, true],
-                                  true, false);
-    }
-
-    var listener = this._tabListeners.get(this.mCurrentTab);
-    if (listener && listener.mStateFlags) {
-      this._callProgressListeners(null, "onUpdateCurrentBrowser",
-                                  [listener.mStateFlags, listener.mStatus,
-                                  listener.mMessage, listener.mTotalProgress],
-                                  true, false);
+    // First, let's deal with blank tabs, which we show instead
+    // of the spinner when the tab is not currently set up
+    // properly in the content process.
+    if (!shouldBeBlank && this.blankTab) {
+      this.blankTab.linkedBrowser.removeAttribute("blank");
+      this.blankTab = null;
+    } else if (shouldBeBlank && this.blankTab !== showTab) {
+      if (this.blankTab) {
+        this.blankTab.linkedBrowser.removeAttribute("blank");
+      }
+      this.blankTab = showTab;
+      this.blankTab.linkedBrowser.setAttribute("blank", "true");
     }
 
-    if (!this._previewMode) {
-      this.mCurrentTab.updateLastAccessed();
-      this.mCurrentTab.removeAttribute("unread");
-      oldTab.updateLastAccessed();
-
-      let oldFindBar = oldTab._findBar;
-      if (oldFindBar &&
-          oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
-          !oldFindBar.hidden)
-        this._lastFindValue = oldFindBar._findField.value;
-
-      this.updateTitlebar();
-
-      this.mCurrentTab.removeAttribute("titlechanged");
-      this.mCurrentTab.removeAttribute("attention");
+    // Show or hide the spinner as needed.
+    let needSpinner = this.getTabState(showTab) != this.STATE_LOADED &&
+                      !this.minimizedOrFullyOccluded &&
+                      !shouldBeBlank;
 
-      // The tab has been selected, it's not unselected anymore.
-      // (1) Call the current tab's finishUnselectedTabHoverTimer()
-      //     to save a telemetry record.
-      // (2) Call the current browser's unselectedTabHover() with false
-      //     to dispatch an event.
-      this.mCurrentTab.finishUnselectedTabHoverTimer();
-      this.mCurrentBrowser.unselectedTabHover(false);
-    }
-
-    // If the new tab is busy, and our current state is not busy, then
-    // we need to fire a start to all progress listeners.
-    const nsIWebProgressListener = Ci.nsIWebProgressListener;
-    if (this.mCurrentTab.hasAttribute("busy") && !this.mIsBusy) {
-      this.mIsBusy = true;
-      this._callProgressListeners(null, "onStateChange",
-                                  [webProgress, null,
-                                  nsIWebProgressListener.STATE_START |
-                                  nsIWebProgressListener.STATE_IS_NETWORK, 0],
-                                  true, false);
-    }
-
-    // If the new tab is not busy, and our current state is busy, then
-    // we need to fire a stop to all progress listeners.
-    if (!this.mCurrentTab.hasAttribute("busy") && this.mIsBusy) {
-      this.mIsBusy = false;
-      this._callProgressListeners(null, "onStateChange",
-                                  [webProgress, null,
-                                  nsIWebProgressListener.STATE_STOP |
-                                  nsIWebProgressListener.STATE_IS_NETWORK, 0],
-                                  true, false);
+    if (!needSpinner && this.spinnerTab) {
+      this.spinnerHidden();
+      this.tabbrowser.removeAttribute("pendingpaint");
+      this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
+      this.spinnerTab = null;
+    } else if (needSpinner && this.spinnerTab !== showTab) {
+      if (this.spinnerTab) {
+        this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
+      } else {
+        this.spinnerDisplayed();
+      }
+      this.spinnerTab = showTab;
+      this.tabbrowser.setAttribute("pendingpaint", "true");
+      this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
     }
 
-    // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
-    // that might rely upon the other changes suppressed.
-    // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
-    if (!this._previewMode) {
-      // We've selected the new tab, so go ahead and notify listeners.
-      let event = new CustomEvent("TabSelect", {
-        bubbles: true,
-        cancelable: false,
-        detail: {
-          previousTab: oldTab
-        }
-      });
-      this.mCurrentTab.dispatchEvent(event);
+    // Switch to the tab we've decided to make visible.
+    if (this.visibleTab !== showTab) {
+      this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
+      this.visibleTab = showTab;
 
-      this._tabAttrModified(oldTab, ["selected"]);
-      this._tabAttrModified(this.mCurrentTab, ["selected"]);
+      this.maybeVisibleTabs.add(showTab);
 
-      if (oldBrowser != newBrowser &&
-          oldBrowser.getInPermitUnload) {
-        oldBrowser.getInPermitUnload(inPermitUnload => {
-          if (!inPermitUnload) {
-            return;
+      let tabs = this.tabbrowser.tabbox.tabs;
+      let tabPanel = this.tabbrowser.mPanelContainer;
+      let showPanel = tabs.getRelatedElement(showTab);
+      let index = Array.indexOf(tabPanel.childNodes, showPanel);
+      if (index != -1) {
+        this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
+        tabPanel.setAttribute("selectedIndex", index);
+        if (showTab === this.requestedTab) {
+          if (this._requestingTab) {
+            /*
+             * If _requestingTab is set, that means that we're switching the
+             * visibility of the tab synchronously, and we need to wait for
+             * the "select" event before shifting focus so that
+             * _adjustFocusAfterTabSwitch runs with the right information for
+             * the tab switch.
+             */
+            this.tabbrowser.addEventListener("select", () => {
+              this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
+            }, { once: true });
+          } else {
+            this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
           }
-          // Since the user is switching away from a tab that has
-          // a beforeunload prompt active, we remove the prompt.
-          // This prevents confusing user flows like the following:
-          //   1. User attempts to close Firefox
-          //   2. User switches tabs (ingoring a beforeunload prompt)
-          //   3. User returns to tab, presses "Leave page"
-          let promptBox = this.getTabModalPromptBox(oldBrowser);
-          let prompts = promptBox.listPrompts();
-          // There might not be any prompts here if the tab was closed
-          // while in an onbeforeunload prompt, which will have
-          // destroyed aforementioned prompt already, so check there's
-          // something to remove, first:
-          if (prompts.length) {
-            // NB: This code assumes that the beforeunload prompt
-            //     is the top-most prompt on the tab.
-            prompts[prompts.length - 1].abortPrompt();
-          }
-        });
+
+          this.maybeActivateDocShell(this.requestedTab);
+        }
       }
 
-      if (!gMultiProcessBrowser) {
-        this._adjustFocusBeforeTabSwitch(oldTab, this.mCurrentTab);
-        this._adjustFocusAfterTabSwitch(this.mCurrentTab);
-      }
+      // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
+      if (this.lastVisibleTab)
+        this.lastVisibleTab._visuallySelected = false;
+
+      this.visibleTab._visuallySelected = true;
     }
 
-    updateUserContextUIIndicator();
-    gIdentityHandler.updateSharingIndicator();
-
-    this.tabContainer._setPositionalAttributes();
-
-    // Enable touch events to start a native dragging
-    // session to allow the user to easily drag the selected tab.
-    // This is currently only supported on Windows.
-    oldTab.removeAttribute("touchdownstartsdrag");
-    this.mCurrentTab.setAttribute("touchdownstartsdrag", "true");
-
-    if (!gMultiProcessBrowser) {
-      document.commandDispatcher.unlock();
-
-      let event = new CustomEvent("TabSwitchDone", {
-        bubbles: true,
-        cancelable: true
-      });
-      this.dispatchEvent(event);
-    }
-
-    if (!aForceUpdate)
-      TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
+    this.lastVisibleTab = this.visibleTab;
   }
 
-  _adjustFocusBeforeTabSwitch(oldTab, newTab) {
-    if (this._previewMode) {
-      return;
-    }
-
-    let oldBrowser = oldTab.linkedBrowser;
-    let newBrowser = newTab.linkedBrowser;
-
-    oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
-
-    if (this.isFindBarInitialized(oldTab)) {
-      let findBar = this.getFindBar(oldTab);
-      oldTab._findBarFocused = (!findBar.hidden &&
-        findBar._findField.getAttribute("focused") == "true");
-    }
+  assert(cond) {
+    if (!cond) {
+      dump("Assertion failure\n" + Error().stack);
 
-    let activeEl = document.activeElement;
-    // If focus is on the old tab, move it to the new tab.
-    if (activeEl == oldTab) {
-      newTab.focus();
-    } else if (gMultiProcessBrowser && activeEl != newBrowser && activeEl != newTab) {
-      // In e10s, if focus isn't already in the tabstrip or on the new browser,
-      // and the new browser's previous focus wasn't in the url bar but focus is
-      // there now, we need to adjust focus further.
-      let keepFocusOnUrlBar = newBrowser &&
-        newBrowser._urlbarFocused &&
-        gURLBar &&
-        gURLBar.focused;
-      if (!keepFocusOnUrlBar) {
-        // Clear focus so that _adjustFocusAfterTabSwitch can detect if
-        // some element has been focused and respect that.
-        document.activeElement.blur();
+      // Don't break a user's browser if an assertion fails.
+      if (AppConstants.DEBUG) {
+        throw new Error("Assertion failure");
       }
     }
   }
 
-  _adjustFocusAfterTabSwitch(newTab) {
-    // Don't steal focus from the tab bar.
-    if (document.activeElement == newTab)
-      return;
-
-    let newBrowser = this.getBrowserForTab(newTab);
-
-    // If there's a tabmodal prompt showing, focus it.
-    if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
-      let XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-      let prompts = newBrowser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
-      let prompt = prompts[prompts.length - 1];
-      prompt.Dialog.setDefaultFocus();
-      return;
-    }
-
-    // Focus the location bar if it was previously focused for that tab.
-    // In full screen mode, only bother making the location bar visible
-    // if the tab is a blank one.
-    if (newBrowser._urlbarFocused && gURLBar) {
-      // Explicitly close the popup if the URL bar retains focus
-      gURLBar.closePopup();
-
-      // If the user happened to type into the URL bar for this browser
-      // by the time we got here, focusing will cause the text to be
-      // selected which could cause them to overwrite what they've
-      // already typed in.
-      if (gURLBar.focused && newBrowser.userTypedValue) {
-        return;
-      }
+  // We've decided to try to load requestedTab.
+  loadRequestedTab() {
+    this.assert(!this.loadTimer);
+    this.assert(!this.minimizedOrFullyOccluded);
 
-      if (!window.fullScreen || isTabEmpty(newTab)) {
-        focusAndSelectUrlBar();
-        return;
-      }
-    }
-
-    // Focus the find bar if it was previously focused for that tab.
-    if (gFindBarInitialized && !gFindBar.hidden &&
-        this.selectedTab._findBarFocused) {
-      gFindBar._findField.focus();
-      return;
-    }
+    // loadingTab can be non-null here if we timed out loading the current tab.
+    // In that case we just overwrite it with a different tab; it's had its chance.
+    this.loadingTab = this.requestedTab;
+    this.log("Loading tab " + this.tinfo(this.loadingTab));
 
-    // Don't focus the content area if something has been focused after the
-    // tab switch was initiated.
-    if (gMultiProcessBrowser &&
-        document.activeElement != document.documentElement)
-      return;
-
-    // We're now committed to focusing the content area.
-    let fm = Services.focus;
-    let focusFlags = fm.FLAG_NOSCROLL;
-
-    if (!gMultiProcessBrowser) {
-      let newFocusedElement = fm.getFocusedElementForWindow(window.content, true, {});
-
-      // for anchors, use FLAG_SHOWRING so that it is clear what link was
-      // last clicked when switching back to that tab
-      if (newFocusedElement &&
-          (newFocusedElement instanceof HTMLAnchorElement ||
-            newFocusedElement.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple"))
-        focusFlags |= fm.FLAG_SHOWRING;
-    }
-
-    fm.setFocus(newBrowser, focusFlags);
+    this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
+    this.setTabState(this.requestedTab, this.STATE_LOADING);
   }
 
-  _tabAttrModified(aTab, aChanged) {
-    if (aTab.closing)
-      return;
-
-    let event = new CustomEvent("TabAttrModified", {
-      bubbles: true,
-      cancelable: false,
-      detail: {
-        changed: aChanged,
-      }
-    });
-    aTab.dispatchEvent(event);
-  }
-
-  setBrowserSharing(aBrowser, aState) {
-    let tab = this.getTabForBrowser(aBrowser);
-    if (!tab)
-      return;
-
-    if (aState.sharing) {
-      tab._sharingState = aState;
-      if (aState.paused) {
-        tab.removeAttribute("sharing");
-      } else {
-        tab.setAttribute("sharing", aState.sharing);
-      }
-    } else {
-      tab._sharingState = null;
-      tab.removeAttribute("sharing");
-    }
-    this._tabAttrModified(tab, ["sharing"]);
-
-    if (aBrowser == this.mCurrentBrowser)
-      gIdentityHandler.updateSharingIndicator();
-  }
-
-  getTabSharingState(aTab) {
-    // Normalize the state object for consumers (ie.extensions).
-    let state = Object.assign({}, aTab._sharingState);
-    return {
-      camera: !!state.camera,
-      microphone: !!state.microphone,
-      screen: state.screen && state.screen.replace("Paused", ""),
-    };
-  }
-
-  setInitialTabTitle(aTab, aTitle, aOptions) {
-    if (aTitle) {
-      if (!aTab.getAttribute("label")) {
-        aTab._labelIsInitialTitle = true;
-      }
-
-      this._setTabLabel(aTab, aTitle, aOptions);
+  maybeActivateDocShell(tab) {
+    // If we've reached the point where the requested tab has entered
+    // the loaded state, but the DocShell is still not yet active, we
+    // should activate it.
+    let browser = tab.linkedBrowser;
+    let state = this.getTabState(tab);
+    let canCheckDocShellState = !browser.mDestroyed &&
+      (browser.docShell || browser.frameLoader.tabParent);
+    if (tab == this.requestedTab &&
+        canCheckDocShellState &&
+        state == this.STATE_LOADED &&
+        !browser.docShellIsActive &&
+        !this.minimizedOrFullyOccluded) {
+      browser.docShellIsActive = true;
+      this.logState("Set requested tab docshell to active and preserveLayers to false");
+      // 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.
+      browser.preserveLayers(false);
     }
   }
 
-  setTabTitle(aTab) {
-    var browser = this.getBrowserForTab(aTab);
-    var title = browser.contentTitle;
-
-    // Don't replace an initially set label with the URL while the tab
-    // is loading.
-    if (aTab._labelIsInitialTitle) {
-      if (!title) {
-        return false;
-      }
-      delete aTab._labelIsInitialTitle;
-    }
-
-    let isContentTitle = false;
-    if (title) {
-      isContentTitle = true;
-    } else if (aTab.hasAttribute("customizemode")) {
-      let brandBundle = document.getElementById("bundle_brand");
-      let brandShortName = brandBundle.getString("brandShortName");
-      title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle",
-                                                  [brandShortName]);
-      isContentTitle = true;
-    } else {
-      if (browser.currentURI.displaySpec) {
-        try {
-          title = this.mURIFixup.createExposableURI(browser.currentURI).displaySpec;
-        } catch (ex) {
-          title = browser.currentURI.displaySpec;
-        }
-      }
-
-      if (title && !isBlankPageURL(title)) {
-        // At this point, we now have a URI.
-        // Let's try to unescape it using a character set
-        // in case the URI is not ASCII.
-        try {
-          // If it's a long data: URI that uses base64 encoding, truncate to
-          // a reasonable length rather than trying to display the entire thing.
-          // We can't shorten arbitrary URIs like this, as bidi etc might mean
-          // we need the trailing characters for display. But a base64-encoded
-          // data-URI is plain ASCII, so this is OK for tab-title display.
-          // (See bug 1408854.)
-          if (title.length > 500 && title.match(/^data:[^,]+;base64,/)) {
-            title = title.substring(0, 500) + "\u2026";
-          } else {
-            var characterSet = browser.characterSet;
-            title = Services.textToSubURI.unEscapeNonAsciiURI(characterSet, title);
-          }
-        } catch (ex) { /* Do nothing. */ }
-      } else {
-        // Still no title? Fall back to our untitled string.
-        title = gTabBrowserBundle.GetStringFromName("tabs.emptyTabTitle");
-      }
-    }
-
-    return this._setTabLabel(aTab, title, { isContentTitle });
-  }
-
-  _setTabLabel(aTab, aLabel, aOptions) {
-    if (!aLabel) {
-      return false;
-    }
-
-    aTab._fullLabel = aLabel;
-
-    aOptions = aOptions || {};
-    if (!aOptions.isContentTitle) {
-      // Remove protocol and "www."
-      if (!("_regex_shortenURLForTabLabel" in this)) {
-        this._regex_shortenURLForTabLabel = /^[^:]+:\/\/(?:www\.)?/;
-      }
-      aLabel = aLabel.replace(this._regex_shortenURLForTabLabel, "");
-    }
-
-    aTab._labelIsContentTitle = aOptions.isContentTitle;
-
-    if (aTab.getAttribute("label") == aLabel) {
-      return false;
-    }
-
-    let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                    .getInterface(Ci.nsIDOMWindowUtils);
-    let isRTL = dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL;
-
-    aTab.setAttribute("label", aLabel);
-    aTab.setAttribute("labeldirection", isRTL ? "rtl" : "ltr");
-
-    // Dispatch TabAttrModified event unless we're setting the label
-    // before the TabOpen event was dispatched.
-    if (!aOptions.beforeTabOpen) {
-      this._tabAttrModified(aTab, ["label"]);
-    }
-
-    if (aTab.selected) {
-      this.updateTitlebar();
-    }
-
-    return true;
-  }
-
-  loadOneTab(aURI, aReferrerURI, aCharset, aPostData, aLoadInBackground, aAllowThirdPartyFixup) {
-    var aTriggeringPrincipal;
-    var aReferrerPolicy;
-    var aFromExternal;
-    var aRelatedToCurrent;
-    var aAllowMixedContent;
-    var aSkipAnimation;
-    var aForceNotRemote;
-    var aPreferredRemoteType;
-    var aNoReferrer;
-    var aUserContextId;
-    var aSameProcessAsFrameLoader;
-    var aOriginPrincipal;
-    var aOpener;
-    var aOpenerBrowser;
-    var aCreateLazyBrowser;
-    var aNextTabParentId;
-    var aFocusUrlBar;
-    var aName;
-    if (arguments.length == 2 &&
-        typeof arguments[1] == "object" &&
-        !(arguments[1] instanceof Ci.nsIURI)) {
-      let params = arguments[1];
-      aTriggeringPrincipal = params.triggeringPrincipal;
-      aReferrerURI = params.referrerURI;
-      aReferrerPolicy = params.referrerPolicy;
-      aCharset = params.charset;
-      aPostData = params.postData;
-      aLoadInBackground = params.inBackground;
-      aAllowThirdPartyFixup = params.allowThirdPartyFixup;
-      aFromExternal = params.fromExternal;
-      aRelatedToCurrent = params.relatedToCurrent;
-      aAllowMixedContent = params.allowMixedContent;
-      aSkipAnimation = params.skipAnimation;
-      aForceNotRemote = params.forceNotRemote;
-      aPreferredRemoteType = params.preferredRemoteType;
-      aNoReferrer = params.noReferrer;
-      aUserContextId = params.userContextId;
-      aSameProcessAsFrameLoader = params.sameProcessAsFrameLoader;
-      aOriginPrincipal = params.originPrincipal;
-      aOpener = params.opener;
-      aOpenerBrowser = params.openerBrowser;
-      aCreateLazyBrowser = params.createLazyBrowser;
-      aNextTabParentId = params.nextTabParentId;
-      aFocusUrlBar = params.focusUrlBar;
-      aName = params.name;
-    }
-
-    var bgLoad = (aLoadInBackground != null) ? aLoadInBackground :
-      Services.prefs.getBoolPref("browser.tabs.loadInBackground");
-    var owner = bgLoad ? null : this.selectedTab;
+  // 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);
 
-    var tab = this.addTab(aURI, {
-      triggeringPrincipal: aTriggeringPrincipal,
-      referrerURI: aReferrerURI,
-      referrerPolicy: aReferrerPolicy,
-      charset: aCharset,
-      postData: aPostData,
-      ownerTab: owner,
-      allowThirdPartyFixup: aAllowThirdPartyFixup,
-      fromExternal: aFromExternal,
-      relatedToCurrent: aRelatedToCurrent,
-      skipAnimation: aSkipAnimation,
-      allowMixedContent: aAllowMixedContent,
-      forceNotRemote: aForceNotRemote,
-      createLazyBrowser: aCreateLazyBrowser,
-      preferredRemoteType: aPreferredRemoteType,
-      noReferrer: aNoReferrer,
-      userContextId: aUserContextId,
-      originPrincipal: aOriginPrincipal,
-      sameProcessAsFrameLoader: aSameProcessAsFrameLoader,
-      opener: aOpener,
-      openerBrowser: aOpenerBrowser,
-      nextTabParentId: aNextTabParentId,
-      focusUrlBar: aFocusUrlBar,
-      name: aName
-    });
-    if (!bgLoad)
-      this.selectedTab = tab;
-
-    return tab;
-  }
-
-  loadTabs(aURIs, aLoadInBackground, aReplace) {
-    let aTriggeringPrincipal;
-    let aAllowThirdPartyFixup;
-    let aTargetTab;
-    let aNewIndex = -1;
-    let aPostDatas = [];
-    let aUserContextId;
-    if (arguments.length == 2 &&
-        typeof arguments[1] == "object") {
-      let params = arguments[1];
-      aLoadInBackground = params.inBackground;
-      aReplace = params.replace;
-      aAllowThirdPartyFixup = params.allowThirdPartyFixup;
-      aTargetTab = params.targetTab;
-      aNewIndex = typeof params.newIndex === "number" ?
-        params.newIndex : aNewIndex;
-      aPostDatas = params.postDatas || aPostDatas;
-      aUserContextId = params.userContextId;
-      aTriggeringPrincipal = params.triggeringPrincipal;
-    }
-
-    if (!aURIs.length)
-      return;
-
-    // The tab selected after this new tab is closed (i.e. the new tab's
-    // "owner") is the next adjacent tab (i.e. not the previously viewed tab)
-    // when several urls are opened here (i.e. closing the first should select
-    // the next of many URLs opened) or if the pref to have UI links opened in
-    // the background is set (i.e. the link is not being opened modally)
-    //
-    // i.e.
-    //    Number of URLs    Load UI Links in BG       Focus Last Viewed?
-    //    == 1              false                     YES
-    //    == 1              true                      NO
-    //    > 1               false/true                NO
-    var multiple = aURIs.length > 1;
-    var owner = multiple || aLoadInBackground ? null : this.selectedTab;
-    var firstTabAdded = null;
-    var targetTabIndex = -1;
-
-    if (aReplace) {
-      let browser;
-      if (aTargetTab) {
-        browser = this.getBrowserForTab(aTargetTab);
-        targetTabIndex = aTargetTab._tPos;
-      } else {
-        browser = this.mCurrentBrowser;
-        targetTabIndex = this.tabContainer.selectedIndex;
-      }
-      let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
-      if (aAllowThirdPartyFixup) {
-        flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
-          Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
-      }
-      try {
-        browser.loadURIWithFlags(aURIs[0], {
-          flags,
-          postData: aPostDatas[0],
-          triggeringPrincipal: aTriggeringPrincipal,
-        });
-      } catch (e) {
-        // Ignore failure in case a URI is wrong, so we can continue
-        // opening the next ones.
-      }
-    } else {
-      firstTabAdded = this.addTab(aURIs[0], {
-        ownerTab: owner,
-        skipAnimation: multiple,
-        allowThirdPartyFixup: aAllowThirdPartyFixup,
-        postData: aPostDatas[0],
-        userContextId: aUserContextId,
-        triggeringPrincipal: aTriggeringPrincipal,
-      });
-      if (aNewIndex !== -1) {
-        this.moveTabTo(firstTabAdded, aNewIndex);
-        targetTabIndex = firstTabAdded._tPos;
-      }
-    }
-
-    let tabNum = targetTabIndex;
-    for (let i = 1; i < aURIs.length; ++i) {
-      let tab = this.addTab(aURIs[i], {
-        skipAnimation: true,
-        allowThirdPartyFixup: aAllowThirdPartyFixup,
-        postData: aPostDatas[i],
-        userContextId: aUserContextId,
-        triggeringPrincipal: aTriggeringPrincipal,
-      });
-      if (targetTabIndex !== -1)
-        this.moveTabTo(tab, ++tabNum);
-    }
-
-    if (firstTabAdded && !aLoadInBackground) {
-      this.selectedTab = firstTabAdded;
-    }
-  }
-
-  updateBrowserRemoteness(aBrowser, aShouldBeRemote, aOptions) {
-    aOptions = aOptions || {};
-    let isRemote = aBrowser.getAttribute("remote") == "true";
-
-    if (!gMultiProcessBrowser && aShouldBeRemote) {
-      throw new Error("Cannot switch to remote browser in a window " +
-        "without the remote tabs load context.");
-    }
-
-    // Default values for remoteType
-    if (!aOptions.remoteType) {
-      aOptions.remoteType = aShouldBeRemote ? E10SUtils.DEFAULT_REMOTE_TYPE : E10SUtils.NOT_REMOTE;
-    }
-
-    // If we are passed an opener, we must be making the browser non-remote, and
-    // if the browser is _currently_ non-remote, we need the openers to match,
-    // because it is already too late to change it.
-    if (aOptions.opener) {
-      if (aShouldBeRemote) {
-        throw new Error("Cannot set an opener on a browser which should be remote!");
-      }
-      if (!isRemote && aBrowser.contentWindow.opener != aOptions.opener) {
-        throw new Error("Cannot change opener on an already non-remote browser!");
+    for (let [tab, ] of this.tabState) {
+      if (!tab.linkedBrowser) {
+        this.tabState.delete(tab);
+        this.unwarmTab(tab);
       }
     }
 
-    // Abort if we're not going to change anything
-    let currentRemoteType = aBrowser.getAttribute("remoteType");
-    if (isRemote == aShouldBeRemote && !aOptions.newFrameloader &&
-        (!isRemote || currentRemoteType == aOptions.remoteType)) {
-      return false;
+    if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
+      this.lastVisibleTab = null;
     }
-
-    let tab = this.getTabForBrowser(aBrowser);
-    // aBrowser needs to be inserted now if it hasn't been already.
-    this._insertBrowser(tab);
-
-    let evt = document.createEvent("Events");
-    evt.initEvent("BeforeTabRemotenessChange", true, false);
-    tab.dispatchEvent(evt);
-
-    let wasActive = document.activeElement == aBrowser;
-
-    // Unmap the old outerWindowID.
-    this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID);
-
-    // Unhook our progress listener.
-    let filter = this._tabFilters.get(tab);
-    let listener = this._tabListeners.get(tab);
-    aBrowser.webProgress.removeProgressListener(filter);
-    filter.removeProgressListener(listener);
-
-    // We'll be creating a new listener, so destroy the old one.
-    listener.destroy();
-
-    let oldUserTypedValue = aBrowser.userTypedValue;
-    let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
-
-    // Make sure the browser is destroyed so it unregisters from observer notifications
-    aBrowser.destroy();
-
-    // Make sure to restore the original droppedLinkHandler and
-    // sameProcessAsFrameLoader.
-    let droppedLinkHandler = aBrowser.droppedLinkHandler;
-    let sameProcessAsFrameLoader = aBrowser.sameProcessAsFrameLoader;
-
-    // Change the "remote" attribute.
-    let parent = aBrowser.parentNode;
-    parent.removeChild(aBrowser);
-    if (aShouldBeRemote) {
-      aBrowser.setAttribute("remote", "true");
-      aBrowser.setAttribute("remoteType", aOptions.remoteType);
-    } else {
-      aBrowser.setAttribute("remote", "false");
-      aBrowser.removeAttribute("remoteType");
+    if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
+      this.lastPrimaryTab = null;
     }
-
-    // NB: This works with the hack in the browser constructor that
-    // turns this normal property into a field.
-    if (aOptions.sameProcessAsFrameLoader) {
-      // Always set sameProcessAsFrameLoader when passed in aOptions.
-      aBrowser.sameProcessAsFrameLoader = aOptions.sameProcessAsFrameLoader;
-    } else if (!aShouldBeRemote || currentRemoteType == aOptions.remoteType) {
-      // Only copy existing sameProcessAsFrameLoader when not switching
-      // remote type otherwise it would stop the switch.
-      aBrowser.sameProcessAsFrameLoader = sameProcessAsFrameLoader;
-    }
-
-    if (aOptions.opener) {
-      // Set the opener window on the browser, such that when the frame
-      // loader is created the opener is set correctly.
-      aBrowser.presetOpenerWindow(aOptions.opener);
-    }
-
-    parent.appendChild(aBrowser);
-
-    aBrowser.userTypedValue = oldUserTypedValue;
-    if (hadStartedLoad) {
-      aBrowser.urlbarChangeTracker.startedLoad();
+    if (this.blankTab && !this.blankTab.linkedBrowser) {
+      this.blankTab = null;
     }
-
-    aBrowser.droppedLinkHandler = droppedLinkHandler;
-
-    // Switching a browser's remoteness will create a new frameLoader.
-    // As frameLoaders start out with an active docShell we have to
-    // deactivate it if this is not the selected tab's browser or the
-    // browser window is minimized.
-    aBrowser.docShellIsActive = this.shouldActivateDocShell(aBrowser);
-
-    // Create a new tab progress listener for the new browser we just injected,
-    // since tab progress listeners have logic for handling the initial about:blank
-    // load
-    listener = new TabProgressListener(tab, aBrowser, true, false);
-    this._tabListeners.set(tab, listener);
-    filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
-
-    // Restore the progress listener.
-    aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
-
-    // Restore the securityUI state.
-    let securityUI = aBrowser.securityUI;
-    let state = securityUI ? securityUI.state :
-      Ci.nsIWebProgressListener.STATE_IS_INSECURE;
-    // Include the true final argument to indicate that this event is
-    // simulated (instead of being observed by the webProgressListener).
-    this._callProgressListeners(aBrowser, "onSecurityChange",
-                                [aBrowser.webProgress, null, state, true],
-                                true, false);
-
-    if (aShouldBeRemote) {
-      // Switching the browser to be remote will connect to a new child
-      // process so the browser can no longer be considered to be
-      // crashed.
-      tab.removeAttribute("crashed");
-    } else {
-      aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned });
-
-      // Register the new outerWindowID.
-      this._outerWindowIDBrowserMap.set(aBrowser.outerWindowID, aBrowser);
+    if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
+      this.spinnerHidden();
+      this.spinnerTab = null;
     }
-
-    if (wasActive)
-      aBrowser.focus();
-
-    // If the findbar has been initialised, reset its browser reference.
-    if (this.isFindBarInitialized(tab)) {
-      this.getFindBar(tab).browser = aBrowser;
-    }
-
-    evt = document.createEvent("Events");
-    evt.initEvent("TabRemotenessChange", true, false);
-    tab.dispatchEvent(evt);
-
-    return true;
-  }
-
-  updateBrowserRemotenessByURL(aBrowser, aURL, aOptions) {
-    aOptions = aOptions || {};
-
-    if (!gMultiProcessBrowser)
-      return this.updateBrowserRemoteness(aBrowser, false);
-
-    let currentRemoteType = aBrowser.getAttribute("remoteType") || null;
-
-    aOptions.remoteType =
-      E10SUtils.getRemoteTypeForURI(aURL,
-        gMultiProcessBrowser,
-        currentRemoteType,
-        aBrowser.currentURI);
-
-    // If this URL can't load in the current browser then flip it to the
-    // correct type.
-    if (currentRemoteType != aOptions.remoteType ||
-      aOptions.newFrameloader) {
-      let remote = aOptions.remoteType != E10SUtils.NOT_REMOTE;
-      return this.updateBrowserRemoteness(aBrowser, remote, aOptions);
-    }
-
-    return false;
-  }
-
-  removePreloadedBrowser() {
-    if (!this._isPreloadingEnabled()) {
-      return;
-    }
-
-    let browser = this._getPreloadedBrowser();
-
-    if (browser) {
-      browser.remove();
+    if (this.loadingTab && !this.loadingTab.linkedBrowser) {
+      this.loadingTab = null;
+      this.clearTimer(this.loadTimer);
+      this.loadTimer = null;
     }
   }
 
-  _getPreloadedBrowser() {
-    if (!this._isPreloadingEnabled()) {
-      return null;
-    }
-
-    // The preloaded browser might be null.
-    let browser = this._preloadedBrowser;
-
-    // Consume the browser.
-    this._preloadedBrowser = null;
+  // This code runs after we've responded to an event or requested a new
+  // tab. It's expected that we've already updated all the principal
+  // state variables. This function takes care of updating any auxilliary
+  // state.
+  postActions() {
+    // Once we finish loading loadingTab, we null it out. So the state should
+    // always be LOADING.
+    this.assert(!this.loadingTab ||
+      this.getTabState(this.loadingTab) == this.STATE_LOADING);
 
-    // Attach the nsIFormFillController now that we know the browser
-    // will be used. If we do that before and the preloaded browser
-    // won't be consumed until shutdown then we leak a docShell.
-    // Also, we do not need to take care of attaching nsIFormFillControllers
-    // in the case that the browser is remote, as remote browsers take
-    // care of that themselves.
-    if (browser) {
-      browser.setAttribute("preloadedState", "consumed");
-      if (this.hasAttribute("autocompletepopup")) {
-        browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
+    // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
+    // the timer is set only when we're loading something.
+    this.assert(!this.loadTimer || this.loadingTab);
+    this.assert(!this.loadingTab || this.loadTimer);
+
+    // If we're switching to a non-remote tab, there's no need to wait
+    // for it to send layers to the compositor, as this will happen
+    // synchronously. Clearing this here means that in the next step,
+    // we can load the non-remote browser immediately.
+    if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
+      this.loadingTab = null;
+      if (this.loadTimer) {
+        this.clearTimer(this.loadTimer);
+        this.loadTimer = null;
       }
     }
 
-    return browser;
-  }
-
-  _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;
-  }
-
-  _createPreloadBrowser() {
-    // Do nothing if we have a preloaded browser already
-    // or preloading of newtab pages is disabled.
-    if (this._preloadedBrowser || !this._isPreloadingEnabled()) {
-      return;
-    }
-
-    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.mPanelContainer.appendChild(notificationbox);
-
-    if (remoteType != E10SUtils.NOT_REMOTE) {
-      // For remote browsers, we need to make sure that the webProgress is
-      // instantiated, otherwise the parent won't get informed about the state
-      // of the preloaded browser until it gets attached to a tab.
-      browser.webProgress;
-    }
-
-    browser.loadURI(BROWSER_NEW_TAB_URL);
-    browser.docShellIsActive = false;
-
-    // Make sure the preloaded browser is loaded with desired zoom level
-    let tabURI = Services.io.newURI(BROWSER_NEW_TAB_URL);
-    FullZoom.onLocationChange(tabURI, false, browser);
-  }
-
-  _createBrowser(aParams) {
-    // Supported parameters:
-    // userContextId, remote, remoteType, isPreloadBrowser,
-    // uriIsAboutBlank, sameProcessAsFrameLoader
-
-    const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-    let b = document.createElementNS(NS_XUL, "browser");
-    b.permanentKey = {};
-    b.setAttribute("type", "content");
-    b.setAttribute("message", "true");
-    b.setAttribute("messagemanagergroup", "browsers");
-    b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
-    b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
-
-    if (aParams.userContextId) {
-      b.setAttribute("usercontextid", aParams.userContextId);
-    }
-
-    // remote parameter used by some addons, use default in this case.
-    if (aParams.remote && !aParams.remoteType) {
-      aParams.remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
-    }
-
-    if (aParams.remoteType) {
-      b.setAttribute("remoteType", aParams.remoteType);
-      b.setAttribute("remote", "true");
-    }
-
-    if (aParams.openerWindow) {
-      if (aParams.remoteType) {
-        throw new Error("Cannot set opener window on a remote browser!");
-      }
-      b.presetOpenerWindow(aParams.openerWindow);
-    }
-
-    if (!aParams.isPreloadBrowser && this.hasAttribute("autocompletepopup")) {
-      b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
-    }
-
-    /*
-     * This attribute is meant to describe if the browser is the
-     * preloaded browser. There are 2 defined states: "preloaded" or
-     * "consumed". The order of events goes as follows:
-     *   1. The preloaded browser is created and the 'preloadedState'
-     *      attribute for that browser is set to "preloaded".
-     *   2. When a new tab is opened and it is time to show that
-     *      preloaded browser, the 'preloadedState' attribute for that
-     *      browser is set to "consumed"
-     *   3. When we then navigate away from about:newtab, the "consumed"
-     *      browsers will attempt to switch to a new content process,
-     *      therefore the 'preloadedState' attribute is removed from
-     *      that browser altogether
-     * See more details on Bug 1420285.
-     */
-    if (aParams.isPreloadBrowser) {
-      b.setAttribute("preloadedState", "preloaded");
-    }
-
-    if (this.hasAttribute("selectmenulist"))
-      b.setAttribute("selectmenulist", this.getAttribute("selectmenulist"));
-
-    if (this.hasAttribute("datetimepicker")) {
-      b.setAttribute("datetimepicker", this.getAttribute("datetimepicker"));
-    }
-
-    b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
-
-    if (aParams.nextTabParentId) {
-      if (!aParams.remoteType) {
-        throw new Error("Cannot have nextTabParentId without a remoteType");
-      }
-      // Gecko is going to read this attribute and use it.
-      b.setAttribute("nextTabParentId", aParams.nextTabParentId.toString());
-    }
-
-    if (aParams.sameProcessAsFrameLoader) {
-      b.sameProcessAsFrameLoader = aParams.sameProcessAsFrameLoader;
-    }
-
-    // This will be used by gecko to control the name of the opened
-    // window.
-    if (aParams.name) {
-      // XXX: The `name` property is special in HTML and XUL. Should
-      // we use a different attribute name for this?
-      b.setAttribute("name", aParams.name);
+    // If we're not loading anything, try loading the requested tab.
+    let stateOfRequestedTab = this.getTabState(this.requestedTab);
+    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();
     }
 
-    // Create the browserStack container
-    var stack = document.createElementNS(NS_XUL, "stack");
-    stack.className = "browserStack";
-    stack.appendChild(b);
-    stack.setAttribute("flex", "1");
-
-    // Create the browserContainer
-    var browserContainer = document.createElementNS(NS_XUL, "vbox");
-    browserContainer.className = "browserContainer";
-    browserContainer.appendChild(stack);
-    browserContainer.setAttribute("flex", "1");
-
-    // Create the sidebar container
-    var browserSidebarContainer = document.createElementNS(NS_XUL,
-      "hbox");
-    browserSidebarContainer.className = "browserSidebarContainer";
-    browserSidebarContainer.appendChild(browserContainer);
-    browserSidebarContainer.setAttribute("flex", "1");
-
-    // Add the Message and the Browser to the box
-    var notificationbox = document.createElementNS(NS_XUL,
-      "notificationbox");
-    notificationbox.setAttribute("flex", "1");
-    notificationbox.setAttribute("notificationside", "top");
-    notificationbox.appendChild(browserSidebarContainer);
-
-    // Prevent the superfluous initial load of a blank document
-    // if we're going to load something other than about:blank.
-    if (!aParams.uriIsAboutBlank) {
-      b.setAttribute("nodefaultsrc", "true");
-    }
-
-    return b;
-  }
-
-  _createLazyBrowser(aTab) {
-    let browser = aTab.linkedBrowser;
-
-    let names = this._browserBindingProperties;
+    // 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;
+      }
 
-    for (let i = 0; i < names.length; i++) {
-      let name = names[i];
-      let getter;
-      let setter;
-      switch (name) {
-        case "audioMuted":
-          getter = () => false;
-          break;
-        case "contentTitle":
-          getter = () => SessionStore.getLazyTabValue(aTab, "title");
-          break;
-        case "currentURI":
-          getter = () => {
-            let url = SessionStore.getLazyTabValue(aTab, "url");
-            return Services.io.newURI(url);
-          };
-          break;
-        case "didStartLoadSinceLastUserTyping":
-          getter = () => () => false;
-          break;
-        case "fullZoom":
-        case "textZoom":
-          getter = () => 1;
-          break;
-        case "getTabBrowser":
-          getter = () => () => this;
-          break;
-        case "isRemoteBrowser":
-          getter = () => browser.getAttribute("remote") == "true";
-          break;
-        case "permitUnload":
-          getter = () => () => ({ permitUnload: true, timedOut: false });
-          break;
-        case "reload":
-        case "reloadWithFlags":
-          getter = () =>
-            params => {
-              // Wait for load handler to be instantiated before
-              // initializing the reload.
-              aTab.addEventListener("SSTabRestoring", () => {
-                browser[name](params);
-              }, { once: true });
-              gBrowser._insertBrowser(aTab);
-            };
-          break;
-        case "resumeMedia":
-          getter = () =>
-            () => {
-              // No need to insert a browser, so we just call the browser's
-              // method.
-              aTab.addEventListener("SSTabRestoring", () => {
-                browser[name]();
-              }, { once: true });
-            };
-          break;
-        case "userTypedValue":
-        case "userTypedClear":
-        case "mediaBlocked":
-          getter = () => SessionStore.getLazyTabValue(aTab, name);
-          break;
-        default:
-          getter = () => {
-            if (AppConstants.NIGHTLY_BUILD) {
-              let message =
-                `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
-              console.log(message + new Error().stack);
-            }
-            this._insertBrowser(aTab);
-            return browser[name];
-          };
-          setter = value => {
-            if (AppConstants.NIGHTLY_BUILD) {
-              let message =
-                `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
-              console.log(message + new Error().stack);
-            }
-            this._insertBrowser(aTab);
-            return browser[name] = value;
-          };
+      if (state == this.STATE_LOADED && tab !== this.requestedTab) {
+        numPending++;
+
+        if (tab !== this.visibleTab) {
+          numWarming++;
+        }
       }
-      Object.defineProperty(browser, name, {
-        get: getter,
-        set: setter,
-        configurable: true,
-        enumerable: true
-      });
-    }
-  }
-
-  _insertBrowser(aTab, aInsertedOnTabCreation) {
-    "use strict";
-
-    // If browser is already inserted or window is closed don't do anything.
-    if (aTab.linkedPanel || window.closed) {
-      return;
-    }
-
-    let browser = aTab.linkedBrowser;
-
-    // If browser is a lazy browser, delete the substitute properties.
-    if (this._browserBindingProperties[0] in browser) {
-      for (let name of this._browserBindingProperties) {
-        delete browser[name];
+      if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
+        numPending++;
       }
     }
 
-    let { uriIsAboutBlank, remoteType, usingPreloadedContent } =
-    aTab._browserParams;
-    delete aTab._browserParams;
-
-    let notificationbox = this.getNotificationBox(browser);
-    let uniqueId = this._generateUniquePanelID();
-    notificationbox.id = uniqueId;
-    aTab.linkedPanel = uniqueId;
-
-    // Inject the <browser> into the DOM if necessary.
-    if (!notificationbox.parentNode) {
-      // NB: this appendChild call causes us to run constructors for the
-      // browser element, which fires off a bunch of notifications. Some
-      // of those notifications can cause code to run that inspects our
-      // state, so it is important that the tab element is fully
-      // initialized by this point.
-      this.mPanelContainer.appendChild(notificationbox);
-    }
-
-    // wire up a progress listener for the new browser object.
-    let tabListener = new TabProgressListener(aTab, browser, uriIsAboutBlank, usingPreloadedContent);
-    const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
-      .createInstance(Ci.nsIWebProgress);
-    filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
-    browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
-    this._tabListeners.set(aTab, tabListener);
-    this._tabFilters.set(aTab, filter);
-
-    browser.droppedLinkHandler = handleDroppedLink;
+    this.updateDisplay();
 
-    // We start our browsers out as inactive, and then maintain
-    // activeness in the tab switcher.
-    browser.docShellIsActive = false;
-
-    // When addTab() is called with an URL that is not "about:blank" we
-    // set the "nodefaultsrc" attribute that prevents a frameLoader
-    // from being created as soon as the linked <browser> is inserted
-    // into the DOM. We thus have to register the new outerWindowID
-    // for non-remote browsers after we have called browser.loadURI().
-    if (remoteType == E10SUtils.NOT_REMOTE) {
-      this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
-    }
-
-    var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: { insertedOnTabCreation: aInsertedOnTabCreation } });
-    aTab.dispatchEvent(evt);
-  }
-
-  discardBrowser(aBrowser, aForceDiscard) {
-    "use strict";
-
-    let tab = this.getTabForBrowser(aBrowser);
-
-    let permitUnloadFlags = aForceDiscard ? aBrowser.dontPromptAndUnload : aBrowser.dontPromptAndDontUnload;
-
-    if (!tab ||
-      tab.selected ||
-      tab.closing ||
-      this._windowIsClosing ||
-      !aBrowser.isConnected ||
-      !aBrowser.isRemoteBrowser ||
-      !aBrowser.permitUnload(permitUnloadFlags).permitUnload) {
+    // 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;
     }
 
-    // Set browser parameters for when browser is restored.  Also remove
-    // listeners and set up lazy restore data in SessionStore. This must
-    // be done before aBrowser is destroyed and removed from the document.
-    tab._browserParams = {
-      uriIsAboutBlank: aBrowser.currentURI.spec == "about:blank",
-      remoteType: aBrowser.remoteType,
-      usingPreloadedContent: false
-    };
-
-    SessionStore.resetBrowserToLazyState(tab);
-
-    this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID);
+    this.maybeFinishTabSwitch();
 
-    // Remove the tab's filter and progress listener.
-    let filter = this._tabFilters.get(tab);
-    let listener = this._tabListeners.get(tab);
-    aBrowser.webProgress.removeProgressListener(filter);
-    filter.removeProgressListener(listener);
-    listener.destroy();
-
-    this._tabListeners.delete(tab);
-    this._tabFilters.delete(tab);
-
-    // Reset the findbar and remove it if it is attached to the tab.
-    if (tab._findBar) {
-      tab._findBar.close(true);
-      tab._findBar.remove();
-      delete tab._findBar;
+    if (numWarming > this.tabbrowser.tabWarmingMax) {
+      this.logState("Hit tabWarmingMax");
+      if (this.unloadTimer) {
+        this.clearTimer(this.unloadTimer);
+      }
+      this.unloadNonRequiredTabs();
     }
 
-    aBrowser.destroy();
-
-    let notificationbox = this.getNotificationBox(aBrowser);
-    this.mPanelContainer.removeChild(notificationbox);
-    tab.removeAttribute("linkedpanel");
-
-    this._createLazyBrowser(tab);
-
-    let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
-    tab.dispatchEvent(evt);
-  }
-
-  // eslint-disable-next-line complexity
-  addTab(aURI, aReferrerURI, aCharset, aPostData, aOwner, aAllowThirdPartyFixup) {
-    "use strict";
-
-    const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    var aTriggeringPrincipal;
-    var aReferrerPolicy;
-    var aFromExternal;
-    var aRelatedToCurrent;
-    var aSkipAnimation;
-    var aAllowMixedContent;
-    var aForceNotRemote;
-    var aPreferredRemoteType;
-    var aNoReferrer;
-    var aUserContextId;
-    var aEventDetail;
-    var aSameProcessAsFrameLoader;
-    var aOriginPrincipal;
-    var aDisallowInheritPrincipal;
-    var aOpener;
-    var aOpenerBrowser;
-    var aCreateLazyBrowser;
-    var aSkipBackgroundNotify;
-    var aNextTabParentId;
-    var aNoInitialLabel;
-    var aFocusUrlBar;
-    var aName;
-    if (arguments.length == 2 &&
-        typeof arguments[1] == "object" &&
-        !(arguments[1] instanceof Ci.nsIURI)) {
-      let params = arguments[1];
-      aTriggeringPrincipal = params.triggeringPrincipal;
-      aReferrerURI = params.referrerURI;
-      aReferrerPolicy = params.referrerPolicy;
-      aCharset = params.charset;
-      aPostData = params.postData;
-      aOwner = params.ownerTab;
-      aAllowThirdPartyFixup = params.allowThirdPartyFixup;
-      aFromExternal = params.fromExternal;
-      aRelatedToCurrent = params.relatedToCurrent;
-      aSkipAnimation = params.skipAnimation;
-      aAllowMixedContent = params.allowMixedContent;
-      aForceNotRemote = params.forceNotRemote;
-      aPreferredRemoteType = params.preferredRemoteType;
-      aNoReferrer = params.noReferrer;
-      aUserContextId = params.userContextId;
-      aEventDetail = params.eventDetail;
-      aSameProcessAsFrameLoader = params.sameProcessAsFrameLoader;
-      aOriginPrincipal = params.originPrincipal;
-      aDisallowInheritPrincipal = params.disallowInheritPrincipal;
-      aOpener = params.opener;
-      aOpenerBrowser = params.openerBrowser;
-      aCreateLazyBrowser = params.createLazyBrowser;
-      aSkipBackgroundNotify = params.skipBackgroundNotify;
-      aNextTabParentId = params.nextTabParentId;
-      aNoInitialLabel = params.noInitialLabel;
-      aFocusUrlBar = params.focusUrlBar;
-      aName = params.name;
-    }
-
-    // if we're adding tabs, we're past interrupt mode, ditch the owner
-    if (this.mCurrentTab.owner)
-      this.mCurrentTab.owner = null;
-
-    // Find the tab that opened this one, if any. This is used for
-    // determining positioning, and inherited attributes such as the
-    // user context ID.
-    //
-    // If we have a browser opener (which is usually the browser
-    // element from a remote window.open() call), use that.
-    //
-    // Otherwise, if the tab is related to the current tab (e.g.,
-    // because it was opened by a link click), use the selected tab as
-    // the owner. If aReferrerURI is set, and we don't have an
-    // explicit relatedToCurrent arg, we assume that the tab is
-    // related to the current tab, since aReferrerURI is null or
-    // undefined if the tab is opened from an external application or
-    // bookmark (i.e. somewhere other than an existing tab).
-    let relatedToCurrent = aRelatedToCurrent == null ? !!aReferrerURI : aRelatedToCurrent;
-    let openerTab = ((aOpenerBrowser && this.getTabForBrowser(aOpenerBrowser)) ||
-      (relatedToCurrent && this.selectedTab));
-
-    var t = document.createElementNS(NS_XUL, "tab");
-
-    t.openerTab = openerTab;
-
-    aURI = aURI || "about:blank";
-    let aURIObject = null;
-    try {
-      aURIObject = Services.io.newURI(aURI);
-    } catch (ex) { /* we'll try to fix up this URL later */ }
-
-    let lazyBrowserURI;
-    if (aCreateLazyBrowser && aURI != "about:blank") {
-      lazyBrowserURI = aURIObject;
-      aURI = "about:blank";
-    }
-
-    var uriIsAboutBlank = aURI == "about:blank";
-
-    if (!aNoInitialLabel) {
-      if (isBlankPageURL(aURI)) {
-        t.setAttribute("label", gTabBrowserBundle.GetStringFromName("tabs.emptyTabTitle"));
-      } else {
-        // Set URL as label so that the tab isn't empty initially.
-        this.setInitialTabTitle(t, aURI, { beforeTabOpen: true });
-      }
-    }
-
-    // Related tab inherits current tab's user context unless a different
-    // usercontextid is specified
-    if (aUserContextId == null && openerTab) {
-      aUserContextId = openerTab.getAttribute("usercontextid") || 0;
-    }
-
-    if (aUserContextId) {
-      t.setAttribute("usercontextid", aUserContextId);
-      ContextualIdentityService.setTabStyle(t);
-    }
-
-    t.setAttribute("onerror", "this.removeAttribute('image');");
-
-    if (aSkipBackgroundNotify) {
-      t.setAttribute("skipbackgroundnotify", true);
+    if (numPending == 0) {
+      this.finish();
     }
 
-    t.className = "tabbrowser-tab";
-
-    this.tabContainer._unlockTabSizing();
-
-    // When overflowing, new tabs are scrolled into view smoothly, which
-    // doesn't go well together with the width transition. So we skip the
-    // transition in that case.
-    let animate = !aSkipAnimation &&
-      this.tabContainer.getAttribute("overflow") != "true" &&
-      this.animationsEnabled;
-    if (!animate) {
-      t.setAttribute("fadein", "true");
+    this.logState("done");
+  }
 
-      // Call _handleNewTab asynchronously as it needs to know if the
-      // new tab is selected.
-      setTimeout(function(tabContainer) {
-        tabContainer._handleNewTab(t);
-      }, 0, this.tabContainer);
-    }
-
-    // invalidate cache
-    this._visibleTabs = null;
-
-    this.tabContainer.appendChild(t);
+  // Fires when we're ready to unload unused tabs.
+  onUnloadTimeout() {
+    this.logState("onUnloadTimeout");
+    this.preActions();
+    this.unloadTimer = null;
 
-    let usingPreloadedContent = false;
-    let b;
-
-    try {
-      // If this new tab is owned by another, assert that relationship
-      if (aOwner)
-        t.owner = aOwner;
-
-      var position = this.tabs.length - 1;
-      t._tPos = position;
-      this.tabContainer._setPositionalAttributes();
-
-      this.tabContainer.updateVisibility();
+    this.unloadNonRequiredTabs();
 
-      // If we don't have a preferred remote type, and we have a remote
-      // opener, use the opener's remote type.
-      if (!aPreferredRemoteType && aOpenerBrowser) {
-        aPreferredRemoteType = aOpenerBrowser.remoteType;
-      }
-
-      // If URI is about:blank and we don't have a preferred remote type,
-      // then we need to use the referrer, if we have one, to get the
-      // correct remote type for the new tab.
-      if (uriIsAboutBlank && !aPreferredRemoteType && aReferrerURI) {
-        aPreferredRemoteType =
-          E10SUtils.getRemoteTypeForURI(aReferrerURI.spec,
-            gMultiProcessBrowser);
-      }
-
-      let remoteType =
-        aForceNotRemote ? E10SUtils.NOT_REMOTE :
-        E10SUtils.getRemoteTypeForURI(aURI, gMultiProcessBrowser,
-          aPreferredRemoteType);
+    this.postActions();
+  }
 
-      // If we open a new tab with the newtab URL in the default
-      // userContext, check if there is a preloaded browser ready.
-      // Private windows are not included because both the label and the
-      // icon for the tab would be set incorrectly (see bug 1195981).
-      if (aURI == BROWSER_NEW_TAB_URL &&
-          !aUserContextId &&
-          !PrivateBrowsingUtils.isWindowPrivate(window)) {
-        b = this._getPreloadedBrowser();
-        if (b) {
-          usingPreloadedContent = true;
-        }
-      }
+  // 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;
 
-      if (!b) {
-        // No preloaded browser found, create one.
-        b = this._createBrowser({
-          remoteType,
-          uriIsAboutBlank,
-          userContextId: aUserContextId,
-          sameProcessAsFrameLoader: aSameProcessAsFrameLoader,
-          openerWindow: aOpener,
-          nextTabParentId: aNextTabParentId,
-          name: aName
-        });
+    // Unload any tabs that can be unloaded.
+    for (let [tab, state] of this.tabState) {
+      if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+        continue;
       }
 
-      t.linkedBrowser = b;
-
-      if (aFocusUrlBar) {
-        b._urlbarFocused = true;
+      if (state == this.STATE_LOADED &&
+          !this.maybeVisibleTabs.has(tab) &&
+          tab !== this.lastVisibleTab &&
+          tab !== this.loadingTab &&
+          tab !== this.requestedTab) {
+        this.setTabState(tab, this.STATE_UNLOADING);
       }
 
-      this._tabForBrowser.set(b, t);
-      t.permanentKey = b.permanentKey;
-      t._browserParams = {
-        uriIsAboutBlank,
-        remoteType,
-        usingPreloadedContent
-      };
-
-      // If the caller opts in, create a lazy browser.
-      if (aCreateLazyBrowser) {
-        this._createLazyBrowser(t);
-
-        if (lazyBrowserURI) {
-          // Lazy browser must be explicitly registered so tab will appear as
-          // a switch-to-tab candidate in autocomplete.
-          this._unifiedComplete.registerOpenPage(lazyBrowserURI, aUserContextId);
-          b.registeredOpenURI = lazyBrowserURI;
-        }
-      } else {
-        this._insertBrowser(t, true);
-      }
-    } catch (e) {
-      Cu.reportError("Failed to create tab");
-      Cu.reportError(e);
-      t.remove();
-      if (t.linkedBrowser) {
-        this._tabFilters.delete(t);
-        this._tabListeners.delete(t);
-        let notificationbox = this.getNotificationBox(t.linkedBrowser);
-        notificationbox.remove();
-      }
-      throw e;
-    }
-
-    // Hack to ensure that the about:newtab favicon is loaded
-    // instantaneously, to avoid flickering and improve perceived performance.
-    if (aURI == "about:newtab") {
-      this.setIcon(t, "chrome://branding/content/icon32.png");
-    } else if (aURI == "about:privatebrowsing") {
-      this.setIcon(t, "chrome://browser/skin/privatebrowsing/favicon.svg");
-    }
-
-    // Dispatch a new tab notification.  We do this once we're
-    // entirely done, so that things are in a consistent state
-    // even if the event listener opens or closes tabs.
-    var detail = aEventDetail || {};
-    var evt = new CustomEvent("TabOpen", { bubbles: true, detail });
-    t.dispatchEvent(evt);
-
-    if (!usingPreloadedContent && aOriginPrincipal && aURI) {
-      let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
-      // Unless we know for sure we're not inheriting principals,
-      // force the about:blank viewer to have the right principal:
-      if (!aURIObject ||
-        (doGetProtocolFlags(aURIObject) & URI_INHERITS_SECURITY_CONTEXT)) {
-        b.createAboutBlankContentViewer(aOriginPrincipal);
-      }
-    }
-
-    // If we didn't swap docShells with a preloaded browser
-    // then let's just continue loading the page normally.
-    if (!usingPreloadedContent && (!uriIsAboutBlank || aDisallowInheritPrincipal)) {
-      // pretend the user typed this so it'll be available till
-      // the document successfully loads
-      if (aURI && !gInitialPages.includes(aURI))
-        b.userTypedValue = aURI;
-
-      let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
-      if (aAllowThirdPartyFixup) {
-        flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
-        flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
-      }
-      if (aFromExternal)
-        flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
-      if (aAllowMixedContent)
-        flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
-      if (aDisallowInheritPrincipal)
-        flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
-      try {
-        b.loadURIWithFlags(aURI, {
-          flags,
-          triggeringPrincipal: aTriggeringPrincipal,
-          referrerURI: aNoReferrer ? null : aReferrerURI,
-          referrerPolicy: aReferrerPolicy,
-          charset: aCharset,
-          postData: aPostData,
-        });
-      } catch (ex) {
-        Cu.reportError(ex);
+      if (state != this.STATE_UNLOADED && tab !== this.requestedTab) {
+        numPending++;
       }
     }
 
-    // If we're opening a tab related to the an existing tab, move it
-    // to a position after that tab.
-    if (openerTab &&
-        Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) {
-
-      let lastRelatedTab = this._lastRelatedTabMap.get(openerTab);
-      let newTabPos = (lastRelatedTab || openerTab)._tPos + 1;
-      if (lastRelatedTab)
-        lastRelatedTab.owner = null;
-      else
-        t.owner = openerTab;
-      this.moveTabTo(t, newTabPos, true);
-      this._lastRelatedTabMap.set(openerTab, t);
-    }
-
-    // This field is updated regardless if we actually animate
-    // since it's important that we keep this count correct in all cases.
-    this.tabAnimationsInProgress++;
-
-    if (animate) {
-      requestAnimationFrame(function() {
-        // kick the animation off
-        t.setAttribute("fadein", "true");
-      });
-    }
-
-    return t;
-  }
-
-  warnAboutClosingTabs(aCloseTabs, aTab) {
-    var tabsToClose;
-    switch (aCloseTabs) {
-      case this.closingTabsEnum.ALL:
-        tabsToClose = this.tabs.length - this._removingTabs.length -
-          gBrowser._numPinnedTabs;
-        break;
-      case this.closingTabsEnum.OTHER:
-        tabsToClose = this.visibleTabs.length - 1 - gBrowser._numPinnedTabs;
-        break;
-      case this.closingTabsEnum.TO_END:
-        if (!aTab)
-          throw new Error("Required argument missing: aTab");
-
-        tabsToClose = this.getTabsToTheEndFrom(aTab).length;
-        break;
-      default:
-        throw new Error("Invalid argument: " + aCloseTabs);
-    }
-
-    if (tabsToClose <= 1)
-      return true;
-
-    const pref = aCloseTabs == this.closingTabsEnum.ALL ?
-      "browser.tabs.warnOnClose" : "browser.tabs.warnOnCloseOtherTabs";
-    var shouldPrompt = Services.prefs.getBoolPref(pref);
-    if (!shouldPrompt)
-      return true;
-
-    var ps = Services.prompt;
-
-    // default to true: if it were false, we wouldn't get this far
-    var warnOnClose = { value: true };
-
-    // focus the window before prompting.
-    // this will raise any minimized window, which will
-    // make it obvious which window the prompt is for and will
-    // solve the problem of windows "obscuring" the prompt.
-    // see bug #350299 for more details
-    window.focus();
-    var warningMessage =
-      PluralForm.get(tabsToClose, gTabBrowserBundle.GetStringFromName("tabs.closeWarningMultiple"))
-      .replace("#1", tabsToClose);
-    var buttonPressed =
-      ps.confirmEx(window,
-        gTabBrowserBundle.GetStringFromName("tabs.closeWarningTitle"),
-        warningMessage,
-        (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
-        (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1),
-        gTabBrowserBundle.GetStringFromName("tabs.closeButtonMultiple"),
-        null, null,
-        aCloseTabs == this.closingTabsEnum.ALL ?
-        gTabBrowserBundle.GetStringFromName("tabs.closeWarningPromptMe") : null,
-        warnOnClose);
-    var reallyClose = (buttonPressed == 0);
-
-    // don't set the pref unless they press OK and it's false
-    if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value)
-      Services.prefs.setBoolPref(pref, false);
-
-    return reallyClose;
-  }
-
-  getTabsToTheEndFrom(aTab) {
-    let tabsToEnd = [];
-    let tabs = this.visibleTabs;
-    for (let i = tabs.length - 1; i >= 0; --i) {
-      if (tabs[i] == aTab || tabs[i].pinned) {
-        break;
-      }
-      tabsToEnd.push(tabs[i]);
-    }
-    return tabsToEnd;
-  }
-
-  removeTabsToTheEndFrom(aTab, aParams) {
-    if (!this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab))
-      return;
-
-    let removeTab = tab => {
-      // Avoid changing the selected browser several times.
-      if (tab.selected)
-        this.selectedTab = aTab;
-
-      this.removeTab(tab, aParams);
-    };
-
-    let tabs = this.getTabsToTheEndFrom(aTab);
-    let tabsWithBeforeUnload = [];
-    for (let i = tabs.length - 1; i >= 0; --i) {
-      let tab = tabs[i];
-      if (this._hasBeforeUnload(tab))
-        tabsWithBeforeUnload.push(tab);
-      else
-        removeTab(tab);
-    }
-    tabsWithBeforeUnload.forEach(removeTab);
-  }
-
-  removeAllTabsBut(aTab) {
-    if (!this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) {
-      return;
-    }
-
-    let tabs = this.visibleTabs.reverse();
-    this.selectedTab = aTab;
-
-    let tabsWithBeforeUnload = [];
-    for (let i = tabs.length - 1; i >= 0; --i) {
-      let tab = tabs[i];
-      if (tab != aTab && !tab.pinned) {
-        if (this._hasBeforeUnload(tab))
-          tabsWithBeforeUnload.push(tab);
-        else
-          this.removeTab(tab, { animate: true });
-      }
-    }
-    for (let tab of tabsWithBeforeUnload) {
-      this.removeTab(tab, { animate: true });
-    }
-  }
-
-  removeCurrentTab(aParams) {
-    this.removeTab(this.mCurrentTab, aParams);
-  }
-
-  removeTab(aTab, aParams) {
-    if (aParams) {
-      var animate = aParams.animate;
-      var byMouse = aParams.byMouse;
-      var skipPermitUnload = aParams.skipPermitUnload;
-    }
-
-    // Telemetry stopwatches may already be running if removeTab gets
-    // called again for an already closing tab.
-    if (!TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_ANIM_MS", aTab) &&
-        !TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab)) {
-      // Speculatevely start both stopwatches now. We'll cancel one of
-      // the two later depending on whether we're animating.
-      TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
-      TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
-    }
-    window.maybeRecordAbandonmentTelemetry(aTab, "tabClosed");
-
-    // Handle requests for synchronously removing an already
-    // asynchronously closing tab.
-    if (!animate &&
-        aTab.closing) {
-      this._endRemoveTab(aTab);
-      return;
-    }
-
-    var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
-
-    if (!this._beginRemoveTab(aTab, null, null, true, skipPermitUnload)) {
-      TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
-      TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
-      return;
-    }
-
-    if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse)
-      this.tabContainer._lockTabSizing(aTab);
-    else
-      this.tabContainer._unlockTabSizing();
-
-    if (!animate /* the caller didn't opt in */ ||
-      isLastTab ||
-      aTab.pinned ||
-      aTab.hidden ||
-      this._removingTabs.length > 3 /* don't want lots of concurrent animations */ ||
-      aTab.getAttribute("fadein") != "true" /* fade-in transition hasn't been triggered yet */ ||
-      window.getComputedStyle(aTab).maxWidth == "0.1px" /* fade-in transition hasn't moved yet */ ||
-      !this.animationsEnabled) {
-      // We're not animating, so we can cancel the animation stopwatch.
-      TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
-      this._endRemoveTab(aTab);
-      return;
-    }
-
-    // We're animating, so we can cancel the non-animation stopwatch.
-    TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
-
-    aTab.style.maxWidth = ""; // ensure that fade-out transition happens
-    aTab.removeAttribute("fadein");
-    aTab.removeAttribute("bursting");
-
-    setTimeout(function(tab, tabbrowser) {
-      if (tab.parentNode &&
-          window.getComputedStyle(tab).maxWidth == "0.1px") {
-        NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)");
-        tabbrowser._endRemoveTab(tab);
-      }
-    }, 3000, aTab, this);
-  }
-
-  _hasBeforeUnload(aTab) {
-    let browser = aTab.linkedBrowser;
-    return browser.isRemoteBrowser && browser.frameLoader &&
-           browser.frameLoader.tabParent &&
-           browser.frameLoader.tabParent.hasBeforeUnload;
-  }
-
-  _beginRemoveTab(aTab, aAdoptedByTab, aCloseWindowWithLastTab, aCloseWindowFastpath, aSkipPermitUnload) {
-    if (aTab.closing ||
-      this._windowIsClosing)
-      return false;
-
-    var browser = this.getBrowserForTab(aTab);
-    if (!aSkipPermitUnload && !aAdoptedByTab &&
-        aTab.linkedPanel && !aTab._pendingPermitUnload &&
-        (!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))) {
-      TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
-
-      // We need to block while calling permitUnload() because it
-      // processes the event queue and may lead to another removeTab()
-      // call before permitUnload() returns.
-      aTab._pendingPermitUnload = true;
-      let { permitUnload, timedOut } = browser.permitUnload();
-      delete aTab._pendingPermitUnload;
-
-      TelemetryStopwatch.finish("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
-
-      // 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._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");
-
-      if (closeWindow) {
-        // We've already called beforeunload on all the relevant tabs if we get here,
-        // so avoid calling it again:
-        window.skipNextCanClose = true;
-      }
-
-      // Closing the tab and replacing it with a blank one is notably slower
-      // than closing the window right away. If the caller opts in, take
-      // the fast path.
-      if (closeWindow &&
-          aCloseWindowFastpath &&
-          this._removingTabs.length == 0) {
-        // This call actually closes the window, unless the user
-        // cancels the operation.  We are finished here in both cases.
-        this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow);
-        return false;
-      }
-
-      newTab = true;
-    }
-    aTab._endRemoveArgs = [closeWindow, newTab];
-
-    // swapBrowsersAndCloseOther will take care of closing the window without animation.
-    if (closeWindow && aAdoptedByTab) {
-      // Remove the tab's filter to avoid leaking.
-      if (aTab.linkedPanel) {
-        this._tabFilters.delete(aTab);
-      }
-      return true;
-    }
-
-    if (!aTab._fullyOpen) {
-      // If the opening tab animation hasn't finished before we start closing the
-      // tab, decrement the animation count since _handleNewTab will not get called.
-      this.tabAnimationsInProgress--;
-    }
-
-    this.tabAnimationsInProgress++;
-
-    // Mute audio immediately to improve perceived speed of tab closure.
-    if (!aAdoptedByTab && aTab.hasAttribute("soundplaying")) {
-      // Don't persist the muted state as this wasn't a user action.
-      // This lets undo-close-tab return it to an unmuted state.
-      aTab.linkedBrowser.mute(true);
-    }
-
-    aTab.closing = true;
-    this._removingTabs.push(aTab);
-    this._visibleTabs = null; // invalidate cache
-
-    // Invalidate hovered tab state tracking for this closing tab.
-    if (this.tabContainer._hoveredTab == aTab)
-      aTab._mouseleave();
-
-    if (newTab)
-      this.addTab(BROWSER_NEW_TAB_URL, { skipAnimation: true });
-    else
-      this.tabContainer.updateVisibility();
-
-    // We're committed to closing the tab now.
-    // Dispatch a notification.
-    // We dispatch it before any teardown so that event listeners can
-    // inspect the tab that's about to close.
-    var evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: aAdoptedByTab } });
-    aTab.dispatchEvent(evt);
-
-    if (aTab.linkedPanel) {
-      if (!aAdoptedByTab && !gMultiProcessBrowser) {
-        // Prevent this tab from showing further dialogs, since we're closing it
-        var windowUtils = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                                               .getInterface(Ci.nsIDOMWindowUtils);
-        windowUtils.disableDialogs();
-      }
-
-      // Remove the tab's filter and progress listener.
-      const filter = this._tabFilters.get(aTab);
-
-      browser.webProgress.removeProgressListener(filter);
-
-      const listener = this._tabListeners.get(aTab);
-      filter.removeProgressListener(listener);
-      listener.destroy();
-    }
-
-    if (browser.registeredOpenURI && !aAdoptedByTab) {
-      this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
-        browser.getAttribute("usercontextid") || 0);
-      delete browser.registeredOpenURI;
-    }
-
-    // We are no longer the primary content area.
-    browser.removeAttribute("primary");
-
-    // Remove this tab as the owner of any other tabs, since it's going away.
-    for (let tab of this.tabs) {
-      if ("owner" in tab && tab.owner == aTab)
-        // |tab| is a child of the tab we're removing, make it an orphan
-        tab.owner = null;
-    }
-
-    return true;
-  }
-
-  _endRemoveTab(aTab) {
-    if (!aTab || !aTab._endRemoveArgs)
-      return;
-
-    var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
-    aTab._endRemoveArgs = null;
-
-    if (this._windowIsClosing) {
-      aCloseWindow = false;
-      aNewTab = false;
-    }
-
-    this.tabAnimationsInProgress--;
-
-    this._lastRelatedTabMap = new WeakMap();
-
-    // update the UI early for responsiveness
-    aTab.collapsed = true;
-    this._blurTab(aTab);
-
-    this._removingTabs.splice(this._removingTabs.indexOf(aTab), 1);
-
-    if (aCloseWindow) {
-      this._windowIsClosing = true;
-      while (this._removingTabs.length)
-        this._endRemoveTab(this._removingTabs[0]);
-    } else if (!this._windowIsClosing) {
-      if (aNewTab)
-        focusAndSelectUrlBar();
-
-      // workaround for bug 345399
-      this.tabContainer.arrowScrollbox._updateScrollButtonsDisabledState();
-    }
-
-    // We're going to remove the tab and the browser now.
-    this._tabFilters.delete(aTab);
-    this._tabListeners.delete(aTab);
-
-    var browser = this.getBrowserForTab(aTab);
-
-    if (aTab.linkedPanel) {
-      this._outerWindowIDBrowserMap.delete(browser.outerWindowID);
-
-      // Because of the way XBL works (fields just set JS
-      // properties on the element) and the code we have in place
-      // to preserve the JS objects for any elements that have
-      // JS properties set on them, the browser element won't be
-      // destroyed until the document goes away.  So we force a
-      // cleanup ourselves.
-      // This has to happen before we remove the child so that the
-      // XBL implementation of nsIObserver still works.
-      browser.destroy();
-    }
-
-    var wasPinned = aTab.pinned;
-
-    // Remove the tab ...
-    this.tabContainer.removeChild(aTab);
-
-    // ... and fix up the _tPos properties immediately.
-    for (let i = aTab._tPos; i < this.tabs.length; i++)
-      this.tabs[i]._tPos = i;
-
-    if (!this._windowIsClosing) {
-      if (wasPinned)
-        this.tabContainer._positionPinnedTabs();
-
-      // update tab close buttons state
-      this.tabContainer._updateCloseButtons();
-
-      setTimeout(function(tabs) {
-        tabs._lastTabClosedByMouse = false;
-      }, 0, this.tabContainer);
-    }
-
-    // update tab positional properties and attributes
-    this.selectedTab._selected = true;
-    this.tabContainer._setPositionalAttributes();
-
-    // Removing the panel requires fixing up selectedPanel immediately
-    // (see below), which would be hindered by the potentially expensive
-    // browser removal. So we remove the browser and the panel in two
-    // steps.
-
-    var panel = this.getNotificationBox(browser);
-
-    // In the multi-process case, it's possible an asynchronous tab switch
-    // is still underway. If so, then it's possible that the last visible
-    // browser is the one we're in the process of removing. There's the
-    // risk of displaying preloaded browsers that are at the end of the
-    // deck if we remove the browser before the switch is complete, so
-    // we alert the switcher in order to show a spinner instead.
-    if (this._switcher) {
-      this._switcher.onTabRemoved(aTab);
-    }
-
-    // This will unload the document. An unload handler could remove
-    // dependant tabs, so it's important that the tabbrowser is now in
-    // a consistent state (tab removed, tab positions updated, etc.).
-    browser.remove();
-
-    // Release the browser in case something is erroneously holding a
-    // reference to the tab after its removal.
-    this._tabForBrowser.delete(aTab.linkedBrowser);
-    aTab.linkedBrowser = null;
-
-    panel.remove();
-
-    // closeWindow might wait an arbitrary length of time if we're supposed
-    // to warn about closing the window, so we'll just stop the tab close
-    // stopwatches here instead.
-    TelemetryStopwatch.finish("FX_TAB_CLOSE_TIME_ANIM_MS", aTab,
-      true /* aCanceledOkay */ );
-    TelemetryStopwatch.finish("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab,
-      true /* aCanceledOkay */ );
-
-    if (aCloseWindow)
-      this._windowIsClosing = closeWindow(true, window.warnAboutClosingWindow);
-  }
-
-  _findTabToBlurTo(aTab) {
-    if (!aTab.selected) {
-      return null;
-    }
-
-    if (aTab.owner &&
-        !aTab.owner.hidden &&
-        !aTab.owner.closing &&
-        Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) {
-      return aTab.owner;
-    }
-
-    // Switch to a visible tab unless there aren't any others remaining
-    let remainingTabs = this.visibleTabs;
-    let numTabs = remainingTabs.length;
-    if (numTabs == 0 || numTabs == 1 && remainingTabs[0] == aTab) {
-      remainingTabs = Array.filter(this.tabs, function(tab) {
-        return !tab.closing;
-      }, this);
-    }
-
-    // Try to find a remaining tab that comes after the given tab
-    let tab = aTab;
-    do {
-      tab = tab.nextSibling;
-    } while (tab && !remainingTabs.includes(tab));
-
-    if (!tab) {
-      tab = aTab;
-
-      do {
-        tab = tab.previousSibling;
-      } while (tab && !remainingTabs.includes(tab));
-    }
-
-    return tab;
-  }
-
-  _blurTab(aTab) {
-    this.selectedTab = this._findTabToBlurTo(aTab);
-  }
-
-  swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
-    // Do not allow transfering a private tab to a non-private window
-    // and vice versa.
-    if (PrivateBrowsingUtils.isWindowPrivate(window) !=
-      PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal))
-      return;
-
-    let ourBrowser = this.getBrowserForTab(aOurTab);
-    let otherBrowser = aOtherTab.linkedBrowser;
-
-    // Can't swap between chrome and content processes.
-    if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser)
-      return;
-
-    // Keep the userContextId if set on other browser
-    if (otherBrowser.hasAttribute("usercontextid")) {
-      ourBrowser.setAttribute("usercontextid", otherBrowser.getAttribute("usercontextid"));
-    }
-
-    // That's gBrowser for the other window, not the tab's browser!
-    var remoteBrowser = aOtherTab.ownerGlobal.gBrowser;
-    var isPending = aOtherTab.hasAttribute("pending");
-
-    let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
-    let stateFlags = otherTabListener.mStateFlags;
-
-    // Expedite the removal of the icon if it was already scheduled.
-    if (aOtherTab._soundPlayingAttrRemovalTimer) {
-      clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
-      aOtherTab._soundPlayingAttrRemovalTimer = 0;
-      aOtherTab.removeAttribute("soundplaying");
-      remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
-    }
-
-    // First, start teardown of the other browser.  Make sure to not
-    // fire the beforeunload event in the process.  Close the other
-    // window if this was its last tab.
-    if (!remoteBrowser._beginRemoveTab(aOtherTab, aOurTab, true))
-      return;
-
-    // If this is the last tab of the window, hide the window
-    // immediately without animation before the docshell swap, to avoid
-    // about:blank being painted.
-    let [closeWindow] = aOtherTab._endRemoveArgs;
-    if (closeWindow) {
-      let win = aOtherTab.ownerGlobal;
-      let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                   .getInterface(Ci.nsIDOMWindowUtils);
-      dwu.suppressAnimation(true);
-      // Only suppressing window animations isn't enough to avoid
-      // an empty content area being painted.
-      let baseWin = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell)
-                       .QueryInterface(Ci.nsIDocShellTreeItem)
-                       .treeOwner
-                       .QueryInterface(Ci.nsIBaseWindow);
-      baseWin.visibility = false;
-    }
-
-    let modifiedAttrs = [];
-    if (aOtherTab.hasAttribute("muted")) {
-      aOurTab.setAttribute("muted", "true");
-      aOurTab.muteReason = aOtherTab.muteReason;
-      ourBrowser.mute();
-      modifiedAttrs.push("muted");
-    }
-    if (aOtherTab.hasAttribute("soundplaying")) {
-      aOurTab.setAttribute("soundplaying", "true");
-      modifiedAttrs.push("soundplaying");
-    }
-    if (aOtherTab.hasAttribute("usercontextid")) {
-      aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
-      modifiedAttrs.push("usercontextid");
-    }
-    if (aOtherTab.hasAttribute("sharing")) {
-      aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
-      modifiedAttrs.push("sharing");
-      aOurTab._sharingState = aOtherTab._sharingState;
-      webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
-    }
-
-    SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser);
-
-    // If the other tab is pending (i.e. has not been restored, yet)
-    // then do not switch docShells but retrieve the other tab's state
-    // and apply it to our tab.
-    if (isPending) {
-      SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
-
-      // Make sure to unregister any open URIs.
-      this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
-    } else {
-      // Workarounds for bug 458697
-      // Icon might have been set on DOMLinkAdded, don't override that.
-      if (!ourBrowser.mIconURL && otherBrowser.mIconURL)
-        this.setIcon(aOurTab, otherBrowser.mIconURL, otherBrowser.contentPrincipal, otherBrowser.contentRequestContextID);
-      var isBusy = aOtherTab.hasAttribute("busy");
-      if (isBusy) {
-        aOurTab.setAttribute("busy", "true");
-        modifiedAttrs.push("busy");
-        if (aOurTab.selected)
-          this.mIsBusy = true;
-      }
-
-      this._swapBrowserDocShells(aOurTab, otherBrowser, Ci.nsIBrowser.SWAP_DEFAULT, stateFlags);
-    }
-
-    // Unregister the previously opened URI
-    if (otherBrowser.registeredOpenURI) {
-      this._unifiedComplete.unregisterOpenPage(otherBrowser.registeredOpenURI,
-        otherBrowser.getAttribute("usercontextid") || 0);
-      delete otherBrowser.registeredOpenURI;
-    }
-
-    // Handle findbar data (if any)
-    let otherFindBar = aOtherTab._findBar;
-    if (otherFindBar &&
-        otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
-      let ourFindBar = this.getFindBar(aOurTab);
-      ourFindBar._findField.value = otherFindBar._findField.value;
-      if (!otherFindBar.hidden)
-        ourFindBar.onFindCommand();
-    }
-
-    // Finish tearing down the tab that's going away.
-    if (closeWindow) {
-      aOtherTab.ownerGlobal.close();
-    } else {
-      remoteBrowser._endRemoveTab(aOtherTab);
-    }
-
-    this.setTabTitle(aOurTab);
-
-    // If the tab was already selected (this happpens in the scenario
-    // of replaceTabWithWindow), notify onLocationChange, etc.
-    if (aOurTab.selected)
-      this.updateCurrentBrowser(true);
-
-    if (modifiedAttrs.length) {
-      this._tabAttrModified(aOurTab, modifiedAttrs);
-    }
-  }
-
-  swapBrowsers(aOurTab, aOtherTab, aFlags) {
-    let otherBrowser = aOtherTab.linkedBrowser;
-    let otherTabBrowser = otherBrowser.getTabBrowser();
-
-    // We aren't closing the other tab so, we also need to swap its tablisteners.
-    let filter = otherTabBrowser._tabFilters.get(aOtherTab);
-    let tabListener = otherTabBrowser._tabListeners.get(aOtherTab);
-    otherBrowser.webProgress.removeProgressListener(filter);
-    filter.removeProgressListener(tabListener);
-
-    // Perform the docshell swap through the common mechanism.
-    this._swapBrowserDocShells(aOurTab, otherBrowser, aFlags);
-
-    // Restore the listeners for the swapped in tab.
-    tabListener = new otherTabBrowser.ownerGlobal.TabProgressListener(aOtherTab, otherBrowser, false, false);
-    otherTabBrowser._tabListeners.set(aOtherTab, tabListener);
-
-    const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
-    filter.addProgressListener(tabListener, notifyAll);
-    otherBrowser.webProgress.addProgressListener(filter, notifyAll);
-  }
-
-  _swapBrowserDocShells(aOurTab, aOtherBrowser, aFlags, aStateFlags) {
-    // aOurTab's browser needs to be inserted now if it hasn't already.
-    this._insertBrowser(aOurTab);
-
-    // Unhook our progress listener
-    const filter = this._tabFilters.get(aOurTab);
-    let tabListener = this._tabListeners.get(aOurTab);
-    let ourBrowser = this.getBrowserForTab(aOurTab);
-    ourBrowser.webProgress.removeProgressListener(filter);
-    filter.removeProgressListener(tabListener);
-
-    // Make sure to unregister any open URIs.
-    this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
-
-    // Unmap old outerWindowIDs.
-    this._outerWindowIDBrowserMap.delete(ourBrowser.outerWindowID);
-    let remoteBrowser = aOtherBrowser.ownerGlobal.gBrowser;
-    if (remoteBrowser) {
-      remoteBrowser._outerWindowIDBrowserMap.delete(aOtherBrowser.outerWindowID);
-    }
-
-    // If switcher is active, it will intercept swap events and
-    // react as needed.
-    if (!this._switcher) {
-      aOtherBrowser.docShellIsActive = this.shouldActivateDocShell(ourBrowser);
-    }
-
-    // Swap the docshells
-    ourBrowser.swapDocShells(aOtherBrowser);
-
-    if (ourBrowser.isRemoteBrowser) {
-      // Switch outerWindowIDs for remote browsers.
-      let ourOuterWindowID = ourBrowser._outerWindowID;
-      ourBrowser._outerWindowID = aOtherBrowser._outerWindowID;
-      aOtherBrowser._outerWindowID = ourOuterWindowID;
-    }
-
-    // Register new outerWindowIDs.
-    this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser);
-    if (remoteBrowser) {
-      remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser);
-    }
-
-    if (!(aFlags & Ci.nsIBrowser.SWAP_KEEP_PERMANENT_KEY)) {
-      // Swap permanentKey properties.
-      let ourPermanentKey = ourBrowser.permanentKey;
-      ourBrowser.permanentKey = aOtherBrowser.permanentKey;
-      aOtherBrowser.permanentKey = ourPermanentKey;
-      aOurTab.permanentKey = ourBrowser.permanentKey;
-      if (remoteBrowser) {
-        let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
-        if (otherTab) {
-          otherTab.permanentKey = aOtherBrowser.permanentKey;
-        }
-      }
-    }
-
-    // Restore the progress listener
-    tabListener = new TabProgressListener(aOurTab, ourBrowser, false, false, aStateFlags);
-    this._tabListeners.set(aOurTab, tabListener);
-
-    const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
-    filter.addProgressListener(tabListener, notifyAll);
-    ourBrowser.webProgress.addProgressListener(filter, notifyAll);
-  }
-
-  _swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) {
-    // Swap the registeredOpenURI properties of the two browsers
-    let tmp = aOurBrowser.registeredOpenURI;
-    delete aOurBrowser.registeredOpenURI;
-    if (aOtherBrowser.registeredOpenURI) {
-      aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
-      delete aOtherBrowser.registeredOpenURI;
-    }
-    if (tmp) {
-      aOtherBrowser.registeredOpenURI = tmp;
+    if (numPending) {
+      // Keep the timer going since there may be more tabs to unload.
+      this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
     }
   }
 
-  reloadAllTabs() {
-    let tabs = this.visibleTabs;
-    let l = tabs.length;
-    for (var i = 0; i < l; i++) {
-      try {
-        this.getBrowserForTab(tabs[i]).reload();
-      } catch (e) {
-        // ignore failure to reload so others will be reloaded
-      }
-    }
-  }
-
-  reloadTab(aTab) {
-    let browser = this.getBrowserForTab(aTab);
-    // Reset temporary permissions on the current tab. This is done here
-    // because we only want to reset permissions on user reload.
-    SitePermissions.clearTemporaryPermissions(browser);
-    browser.reload();
+  // Fires when an ongoing load has taken too long.
+  onLoadTimeout() {
+    this.logState("onLoadTimeout");
+    this.preActions();
+    this.loadTimer = null;
+    this.loadingTab = null;
+    this.postActions();
   }
 
-  addProgressListener(aListener) {
-    if (arguments.length != 1) {
-      Cu.reportError("gBrowser.addProgressListener was " +
-        "called with a second argument, " +
-        "which is not supported. See bug " +
-        "608628. Call stack: " + new Error().stack);
-    }
-
-    this.mProgressListeners.push(aListener);
-  }
-
-  removeProgressListener(aListener) {
-    this.mProgressListeners =
-      this.mProgressListeners.filter(l => l != aListener);
-  }
-
-  addTabsProgressListener(aListener) {
-    this.mTabsProgressListeners.push(aListener);
-  }
-
-  removeTabsProgressListener(aListener) {
-    this.mTabsProgressListeners =
-      this.mTabsProgressListeners.filter(l => l != aListener);
-  }
-
-  getBrowserForTab(aTab) {
-    return aTab.linkedBrowser;
-  }
-
-  showOnlyTheseTabs(aTabs) {
-    for (let tab of this.tabs) {
-      if (!aTabs.includes(tab))
-        this.hideTab(tab);
-      else
-        this.showTab(tab);
+  // Fires when the layers become available for a tab.
+  onLayersReady(browser) {
+    let tab = this.tabbrowser.getTabForBrowser(browser);
+    if (!tab) {
+      // We probably got a layer update from a tab that got before
+      // the switcher was created, or for browser that's not being
+      // tracked by the async tab switcher (like the preloaded about:newtab).
+      return;
     }
 
-    this.tabContainer._handleTabSelect(true);
-  }
-
-  showTab(aTab) {
-    if (aTab.hidden) {
-      aTab.removeAttribute("hidden");
-      this._visibleTabs = null; // invalidate cache
-
-      this.tabContainer._updateCloseButtons();
-
-      this.tabContainer._setPositionalAttributes();
-
-      let event = document.createEvent("Events");
-      event.initEvent("TabShow", true, false);
-      aTab.dispatchEvent(event);
-      SessionStore.deleteTabValue(aTab, "hiddenBy");
-    }
-  }
-
-  hideTab(aTab, aSource) {
-    if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
-        !aTab.closing && !aTab._sharingState) {
-      aTab.setAttribute("hidden", "true");
-      this._visibleTabs = null; // invalidate cache
-
-      this.tabContainer._updateCloseButtons();
-
-      this.tabContainer._setPositionalAttributes();
+    this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
 
-      let event = document.createEvent("Events");
-      event.initEvent("TabHide", true, false);
-      aTab.dispatchEvent(event);
-      if (aSource) {
-        SessionStore.setTabValue(aTab, "hiddenBy", aSource);
-      }
-    }
-  }
-
-  selectTabAtIndex(aIndex, aEvent) {
-    let tabs = this.visibleTabs;
+    this.assert(this.getTabState(tab) == this.STATE_LOADING ||
+      this.getTabState(tab) == this.STATE_LOADED);
+    this.setTabState(tab, this.STATE_LOADED);
+    this.unwarmTab(tab);
 
-    // count backwards for aIndex < 0
-    if (aIndex < 0) {
-      aIndex += tabs.length;
-      // clamp at index 0 if still negative.
-      if (aIndex < 0)
-        aIndex = 0;
-    } else if (aIndex >= tabs.length) {
-      // clamp at right-most tab if out of range.
-      aIndex = tabs.length - 1;
-    }
-
-    this.selectedTab = tabs[aIndex];
-
-    if (aEvent) {
-      aEvent.preventDefault();
-      aEvent.stopPropagation();
+    if (this.loadingTab === tab) {
+      this.clearTimer(this.loadTimer);
+      this.loadTimer = null;
+      this.loadingTab = null;
     }
   }
 
-  /**
-   * Moves a tab to a new browser window, unless it's already the only tab
-   * in the current window, in which case this will do nothing.
-   */
-  replaceTabWithWindow(aTab, aOptions) {
-    if (this.tabs.length == 1)
-      return null;
-
-    var options = "chrome,dialog=no,all";
-    for (var name in aOptions)
-      options += "," + name + "=" + aOptions[name];
-
-    // Play the tab closing animation to give immediate feedback while
-    // waiting for the new window to appear.
-    // content area when the docshells are swapped.
-    if (this.animationsEnabled) {
-      aTab.style.maxWidth = ""; // ensure that fade-out transition happens
-      aTab.removeAttribute("fadein");
-    }
-
-    // tell a new window to take the "dropped" tab
-    return window.openDialog(getBrowserURL(), "_blank", options, aTab);
+  // 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();
   }
 
-  moveTabTo(aTab, aIndex, aKeepRelatedTabs) {
-    var oldPosition = aTab._tPos;
-    if (oldPosition == aIndex)
-      return;
-
-    // Don't allow mixing pinned and unpinned tabs.
-    if (aTab.pinned)
-      aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
-    else
-      aIndex = Math.max(aIndex, this._numPinnedTabs);
-    if (oldPosition == aIndex)
-      return;
-
-    if (!aKeepRelatedTabs) {
-      this._lastRelatedTabMap = new WeakMap();
-    }
-
-    let wasFocused = (document.activeElement == this.mCurrentTab);
-
-    aIndex = aIndex < aTab._tPos ? aIndex : aIndex + 1;
-
-    // invalidate cache
-    this._visibleTabs = null;
-
-    // use .item() instead of [] because dragging to the end of the strip goes out of
-    // bounds: .item() returns null (so it acts like appendChild), but [] throws
-    this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex));
-
-    for (let i = 0; i < this.tabs.length; i++) {
-      this.tabs[i]._tPos = i;
-      this.tabs[i]._selected = false;
+  // 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 ||
+        this.getTabState(tab) == this.STATE_UNLOADED);
+      this.setTabState(tab, this.STATE_UNLOADED);
     }
-
-    // If we're in the midst of an async tab switch while calling
-    // moveTabTo, we can get into a case where _visuallySelected
-    // is set to true on two different tabs.
-    //
-    // What we want to do in moveTabTo is to remove logical selection
-    // from all tabs, and then re-add logical selection to mCurrentTab
-    // (and visual selection as well if we're not running with e10s, which
-    // setting _selected will do automatically).
-    //
-    // If we're running with e10s, then the visual selection will not
-    // be changed, which is fine, since if we weren't in the midst of a
-    // tab switch, the previously visually selected tab should still be
-    // correct, and if we are in the midst of a tab switch, then the async
-    // tab switcher will set the visually selected tab once the tab switch
-    // has completed.
-    this.mCurrentTab._selected = true;
-
-    if (wasFocused)
-      this.mCurrentTab.focus();
-
-    this.tabContainer._handleTabSelect(true);
-
-    if (aTab.pinned)
-      this.tabContainer._positionPinnedTabs();
-
-    this.tabContainer._setPositionalAttributes();
-
-    var evt = document.createEvent("UIEvents");
-    evt.initUIEvent("TabMove", true, false, window, oldPosition);
-    aTab.dispatchEvent(evt);
-  }
-
-  moveTabForward() {
-    let nextTab = this.mCurrentTab.nextSibling;
-    while (nextTab && nextTab.hidden)
-      nextTab = nextTab.nextSibling;
-
-    if (nextTab)
-      this.moveTabTo(this.mCurrentTab, nextTab._tPos);
-    else if (this.arrowKeysShouldWrap)
-      this.moveTabToStart();
   }
 
-  /**
-   * Adopts a tab from another browser window, and inserts it at aIndex
-   */
-  adoptTab(aTab, aIndex, aSelectTab) {
-    // Swap the dropped tab with a new one we create and then close
-    // it in the other window (making it seem to have moved between
-    // windows). We also ensure that the tab we create to swap into has
-    // the same remote type and process as the one we're swapping in.
-    // This makes sure we don't get a short-lived process for the new tab.
-    let linkedBrowser = aTab.linkedBrowser;
-    let params = {
-      eventDetail: { adoptedTab: aTab },
-      preferredRemoteType: linkedBrowser.remoteType,
-      sameProcessAsFrameLoader: linkedBrowser.frameLoader,
-      skipAnimation: true
-    };
-    if (aTab.hasAttribute("usercontextid")) {
-      // new tab must have the same usercontextid as the old one
-      params.userContextId = aTab.getAttribute("usercontextid");
+  // Called when a tab switches from remote to non-remote. In this case
+  // a MozLayerTreeReady notification that we requested may never fire,
+  // so we need to simulate it.
+  onRemotenessChange(tab) {
+    this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`);
+    if (!tab.linkedBrowser.isRemoteBrowser) {
+      if (this.getTabState(tab) == this.STATE_LOADING) {
+        this.onLayersReady(tab.linkedBrowser);
+      } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
+        this.onLayersCleared(tab.linkedBrowser);
+      }
+    } else if (this.getTabState(tab) == this.STATE_LOADED) {
+      // A tab just changed from non-remote to remote, which means
+      // that it's gone back into the STATE_LOADING state until
+      // it sends up a layer tree.
+      this.setTabState(tab, this.STATE_LOADING);
     }
-    let newTab = this.addTab("about:blank", params);
-    let newBrowser = this.getBrowserForTab(newTab);
-
-    // Stop the about:blank load.
-    newBrowser.stop();
-    // Make sure it has a docshell.
-    newBrowser.docShell;
-
-    let numPinned = this._numPinnedTabs;
-    if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
-      this.pinTab(newTab);
-    }
-
-    this.moveTabTo(newTab, aIndex);
-
-    // We need to select the tab before calling swapBrowsersAndCloseOther
-    // so that window.content in chrome windows points to the right tab
-    // when pagehide/show events are fired. This is no longer necessary
-    // for any exiting browser code, but it may be necessary for add-on
-    // compatibility.
-    if (aSelectTab) {
-      this.selectedTab = newTab;
-    }
-
-    aTab.parentNode._finishAnimateTabMove();
-    this.swapBrowsersAndCloseOther(newTab, aTab);
-
-    if (aSelectTab) {
-      // Call updateCurrentBrowser to make sure the URL bar is up to date
-      // for our new tab after we've done swapBrowsersAndCloseOther.
-      this.updateCurrentBrowser(true);
-    }
-
-    return newTab;
   }
 
-  moveTabBackward() {
-    let previousTab = this.mCurrentTab.previousSibling;
-    while (previousTab && previousTab.hidden)
-      previousTab = previousTab.previousSibling;
-
-    if (previousTab)
-      this.moveTabTo(this.mCurrentTab, previousTab._tPos);
-    else if (this.arrowKeysShouldWrap)
-      this.moveTabToEnd();
-  }
-
-  moveTabToStart() {
-    var tabPos = this.mCurrentTab._tPos;
-    if (tabPos > 0)
-      this.moveTabTo(this.mCurrentTab, 0);
-  }
-
-  moveTabToEnd() {
-    var tabPos = this.mCurrentTab._tPos;
-    if (tabPos < this.browsers.length - 1)
-      this.moveTabTo(this.mCurrentTab, this.browsers.length - 1);
-  }
-
-  moveTabOver(aEvent) {
-    var direction = window.getComputedStyle(this.container.parentNode).direction;
-    if ((direction == "ltr" && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
-      (direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT))
-      this.moveTabForward();
-    else
-      this.moveTabBackward();
-  }
-
-  /**
-   * @param   aTab
-   *          Can be from a different window as well
-   * @param   aRestoreTabImmediately
-   *          Can defer loading of the tab contents
-   */
-  duplicateTab(aTab, aRestoreTabImmediately) {
-    return SessionStore.duplicateTab(window, aTab, 0, aRestoreTabImmediately);
-  }
-
-  activateBrowserForPrintPreview(aBrowser) {
-    this._printPreviewBrowsers.add(aBrowser);
-    if (this._switcher) {
-      this._switcher.activateBrowserForPrintPreview(aBrowser);
-    }
-    aBrowser.docShellIsActive = true;
-  }
-
-  deactivatePrintPreviewBrowsers() {
-    let browsers = this._printPreviewBrowsers;
-    this._printPreviewBrowsers = new Set();
-    for (let browser of browsers) {
-      browser.docShellIsActive = this.shouldActivateDocShell(browser);
+  // Called when a tab has been removed, and the browser node is
+  // about to be removed from the DOM.
+  onTabRemoved(tab) {
+    if (this.lastVisibleTab == tab) {
+      // The browser that was being presented to the user is
+      // going to be removed during this tick of the event loop.
+      // This will cause us to show a tab spinner instead.
+      this.preActions();
+      this.lastVisibleTab = null;
+      this.postActions();
     }
   }
 
-  /**
-   * Returns true if a given browser's docshell should be active.
-   */
-  shouldActivateDocShell(aBrowser) {
-    if (this._switcher) {
-      return this._switcher.shouldActivateDocShell(aBrowser);
-    }
-    return (aBrowser == this.selectedBrowser &&
-            window.windowState != window.STATE_MINIMIZED &&
-            !window.isFullyOccluded) ||
-            this._printPreviewBrowsers.has(aBrowser);
-  }
-
-  /**
-   * 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
-   * maintains state about whether the layer tree for the tab is
-   * available, being loaded, being unloaded, or unavailable. It
-   * also keeps track of the tab currently being displayed, the tab
-   * it's trying to load, and the tab the user has asked to switch
-   * to. The switcher object is created upon tab switch. It is
-   * released when there are no pending tabs to load or unload.
-   *
-   * The following general principles have guided the design:
-   *
-   * 1. We only request one layer tree at a time. If the user
-   * switches to a different tab while waiting, we don't request
-   * the new layer tree until the old tab has loaded or timed out.
-   *
-   * 2. If loading the layers for a tab times out, we show the
-   * spinner and possibly request the layer tree for another tab if
-   * the user has requested one.
-   *
-   * 3. We discard layer trees on a delay. This way, if the user is
-   * switching among the same tabs frequently, we don't continually
-   * load the same tabs.
-   *
-   * It's important that we always show either the spinner or a tab
-   * whose layers are available. Otherwise the compositor will draw
-   * an entirely black frame, which is very jarring. To ensure this
-   * never happens when switching away from a tab, we assume the
-   * old tab might still be drawn until a MozAfterPaint event
-   * occurs. Because layout and compositing happen asynchronously,
-   * we don't have any other way of knowing when the switch
-   * actually takes place. Therefore, we don't unload the old tab
-   * until the next MozAfterPaint event.
-   */
-  _getSwitcher() {
-    if (this._switcher) {
-      return this._switcher;
-    }
-
-    let switcher = {
-      // How long to wait for a tab's layers to load. After this
-      // time elapses, we're free to put up the spinner and start
-      // trying to load a different tab.
-      TAB_SWITCH_TIMEOUT: 400 /* ms */,
-
-      // When the user hasn't switched tabs for this long, we unload
-      // layers for all tabs that aren't in use.
-      UNLOAD_DELAY: 300 /* ms */,
-
-      // The next three tabs form the principal state variables.
-      // See the assertions in postActions for their invariants.
-
-      // Tab the user requested most recently.
-      requestedTab: this.selectedTab,
-
-      // Tab we're currently trying to load.
-      loadingTab: null,
-
-      // We show this tab in case the requestedTab hasn't loaded yet.
-      lastVisibleTab: this.selectedTab,
-
-      // Auxilliary state variables:
-
-      visibleTab: this.selectedTab, // Tab that's on screen.
-      spinnerTab: null, // Tab showing a spinner.
-      blankTab: null, // Tab showing blank.
-      lastPrimaryTab: this.selectedTab, // Tab with primary="true"
-
-      tabbrowser: this, // Reference to gBrowser.
-      loadTimer: null, // TAB_SWITCH_TIMEOUT nsITimer instance.
-      unloadTimer: null, // UNLOAD_DELAY nsITimer instance.
-
-      // Map from tabs to STATE_* (below).
-      tabState: new Map(),
-
-      // True if we're in the midst of switching tabs.
-      switchInProgress: false,
-
-      // Keep an exact list of content processes (tabParent) in which
-      // we're actively suppressing the display port. This gives a robust
-      // way to make sure we don't forget to un-suppress.
-      activeSuppressDisplayport: new Set(),
-
-      // 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,
-
-      // Wraps nsITimer. Must not use the vanilla setTimeout and
-      // clearTimeout, because they will be blocked by nsIPromptService
-      // dialogs.
-      setTimer(callback, timeout) {
-        let event = {
-          notify: callback
-        };
-
-        var timer = Cc["@mozilla.org/timer;1"]
-          .createInstance(Ci.nsITimer);
-        timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
-        return timer;
-      },
-
-      clearTimer(timer) {
-        timer.cancel();
-      },
-
-      getTabState(tab) {
-        let state = this.tabState.get(tab);
-
-        // As an optimization, we lazily evaluate the state of tabs
-        // that we've never seen before. Once we've figured it out,
-        // we stash it in our state map.
-        if (state === undefined) {
-          state = this.STATE_UNLOADED;
-
-          if (tab && tab.linkedPanel) {
-            let b = tab.linkedBrowser;
-            if (b.renderLayers && b.hasLayers) {
-              state = this.STATE_LOADED;
-            } else if (b.renderLayers && !b.hasLayers) {
-              state = this.STATE_LOADING;
-            } else if (!b.renderLayers && b.hasLayers) {
-              state = this.STATE_UNLOADING;
-            }
-          }
-
-          this.setTabStateNoAction(tab, state);
-        }
-
-        return state;
-      },
-
-      setTabStateNoAction(tab, state) {
-        if (state == this.STATE_UNLOADED) {
-          this.tabState.delete(tab);
-        } else {
-          this.tabState.set(tab, state);
-        }
-      },
-
-      setTabState(tab, state) {
-        if (state == this.getTabState(tab)) {
-          return;
-        }
-
-        this.setTabStateNoAction(tab, state);
-
-        let browser = tab.linkedBrowser;
-        let { tabParent } = browser.frameLoader;
-        if (state == this.STATE_LOADING) {
-          this.assert(!this.minimizedOrFullyOccluded);
-
-          if (!this.tabbrowser.tabWarmingEnabled) {
-            browser.docShellIsActive = true;
-          }
-
-          if (tabParent) {
-            browser.renderLayers = true;
-          } else {
-            this.onLayersReady(browser);
-          }
-        } else if (state == this.STATE_UNLOADING) {
-          this.unwarmTab(tab);
-          // Setting the docShell to be inactive will also cause it
-          // to stop rendering layers.
-          browser.docShellIsActive = false;
-          if (!tabParent) {
-            this.onLayersCleared(browser);
-          }
-        } else if (state == this.STATE_LOADED) {
-          this.maybeActivateDocShell(tab);
-        }
-
-        if (!tab.linkedBrowser.isRemoteBrowser) {
-          // setTabState is potentially re-entrant in the non-remote case,
-          // so we must re-get the state for this assertion.
-          let nonRemoteState = this.getTabState(tab);
-          // Non-remote tabs can never stay in the STATE_LOADING
-          // or STATE_UNLOADING states. By the time this function
-          // exits, a non-remote tab must be in STATE_LOADED or
-          // STATE_UNLOADED, since the painting and the layer
-          // upload happen synchronously.
-          this.assert(nonRemoteState == this.STATE_UNLOADED ||
-            nonRemoteState == this.STATE_LOADED);
-        }
-      },
-
-      get minimizedOrFullyOccluded() {
-        return window.windowState == window.STATE_MINIMIZED ||
-          window.isFullyOccluded;
-      },
-
-      init() {
-        this.log("START");
-
-        window.addEventListener("MozAfterPaint", this);
-        window.addEventListener("MozLayerTreeReady", this);
-        window.addEventListener("MozLayerTreeCleared", this);
-        window.addEventListener("TabRemotenessChange", this);
-        window.addEventListener("sizemodechange", this);
-        window.addEventListener("occlusionstatechange", this);
-        window.addEventListener("SwapDocShells", this, true);
-        window.addEventListener("EndSwapDocShells", this, true);
-
-        let initialTab = this.requestedTab;
-        let initialBrowser = initialTab.linkedBrowser;
-
-        let tabIsLoaded = !initialBrowser.isRemoteBrowser ||
-          initialBrowser.frameLoader.tabParent.hasLayers;
-
-        // 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.
-        initialBrowser.preserveLayers(false);
-
-        if (!this.minimizedOrFullyOccluded) {
-          this.log("Initial tab is loaded?: " + tabIsLoaded);
-          this.setTabState(initialTab, tabIsLoaded ? this.STATE_LOADED
-                                                   : this.STATE_LOADING);
-        }
-
-        for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
-          let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
-          let state = ppBrowser.hasLayers ? this.STATE_LOADED
-                                          : this.STATE_LOADING;
-          this.setTabState(ppTab, state);
-        }
-      },
-
-      destroy() {
-        if (this.unloadTimer) {
-          this.clearTimer(this.unloadTimer);
-          this.unloadTimer = null;
-        }
-        if (this.loadTimer) {
-          this.clearTimer(this.loadTimer);
-          this.loadTimer = null;
-        }
-
-        window.removeEventListener("MozAfterPaint", this);
-        window.removeEventListener("MozLayerTreeReady", this);
-        window.removeEventListener("MozLayerTreeCleared", this);
-        window.removeEventListener("TabRemotenessChange", this);
-        window.removeEventListener("sizemodechange", this);
-        window.removeEventListener("occlusionstatechange", this);
-        window.removeEventListener("SwapDocShells", this, true);
-        window.removeEventListener("EndSwapDocShells", this, true);
-
-        this.tabbrowser._switcher = null;
-
-        this.activeSuppressDisplayport.forEach(function(tabParent) {
-          tabParent.suppressDisplayport(false);
-        });
-        this.activeSuppressDisplayport.clear();
-      },
-
-      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);
-        this.assert(!this.loadingTab);
-        this.assert(this.lastVisibleTab === this.requestedTab);
-        this.assert(this.minimizedOrFullyOccluded ||
-          this.getTabState(this.requestedTab) == this.STATE_LOADED);
-
-        this.destroy();
-
-        document.commandDispatcher.unlock();
-
-        let event = new CustomEvent("TabSwitchDone", {
-          bubbles: true,
-          cancelable: true
-        });
-        this.tabbrowser.dispatchEvent(event);
-      },
-
-      // This function is called after all the main state changes to
-      // make sure we display the right tab.
-      updateDisplay() {
-        let requestedTabState = this.getTabState(this.requestedTab);
-        let requestedBrowser = this.requestedTab.linkedBrowser;
-
-        // It is often more desirable to show a blank tab when appropriate than
-        // the tab switch spinner - especially since the spinner is usually
-        // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
-        // tab switch. We can hide this lag, and hide the time being spent
-        // constructing TabChild's, layer trees, etc, by showing a blank
-        // tab instead and focusing it immediately.
-        let shouldBeBlank = false;
-        if (requestedBrowser.isRemoteBrowser) {
-          // If a tab is remote and the window is not minimized, we can show a
-          // blank tab instead of a spinner in the following cases:
-          //
-          // 1. The tab has just crashed, and we haven't started showing the
-          //    tab crashed page yet (in this case, the TabParent is null)
-          // 2. The tab has never presented, and has not finished loading
-          //    a non-local-about: page.
-          //
-          // For (2), "finished loading a non-local-about: page" is
-          // determined by the busy state on the tab element and checking
-          // if the loaded URI is local.
-          let hasSufficientlyLoaded = !this.requestedTab.hasAttribute("busy") &&
-            !this.tabbrowser._isLocalAboutURI(requestedBrowser.currentURI);
-
-          let fl = requestedBrowser.frameLoader;
-          shouldBeBlank = !this.minimizedOrFullyOccluded &&
-            (!fl.tabParent ||
-              (!hasSufficientlyLoaded && !fl.tabParent.hasPresented));
-        }
-
-        this.log("Tab should be blank: " + shouldBeBlank);
-        this.log("Requested tab is remote?: " + requestedBrowser.isRemoteBrowser);
-
-        // Figure out which tab we actually want visible right now.
-        let showTab = null;
-        if (requestedTabState != this.STATE_LOADED &&
-            this.lastVisibleTab && this.loadTimer && !shouldBeBlank) {
-          // If we can't show the requestedTab, and lastVisibleTab is
-          // available, show it.
-          showTab = this.lastVisibleTab;
-        } else {
-          // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
-          showTab = this.requestedTab;
-        }
-
-        // First, let's deal with blank tabs, which we show instead
-        // of the spinner when the tab is not currently set up
-        // properly in the content process.
-        if (!shouldBeBlank && this.blankTab) {
-          this.blankTab.linkedBrowser.removeAttribute("blank");
-          this.blankTab = null;
-        } else if (shouldBeBlank && this.blankTab !== showTab) {
-          if (this.blankTab) {
-            this.blankTab.linkedBrowser.removeAttribute("blank");
-          }
-          this.blankTab = showTab;
-          this.blankTab.linkedBrowser.setAttribute("blank", "true");
-        }
-
-        // Show or hide the spinner as needed.
-        let needSpinner = this.getTabState(showTab) != this.STATE_LOADED &&
-                          !this.minimizedOrFullyOccluded &&
-                          !shouldBeBlank;
-
-        if (!needSpinner && this.spinnerTab) {
-          this.spinnerHidden();
-          this.tabbrowser.removeAttribute("pendingpaint");
-          this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
-          this.spinnerTab = null;
-        } else if (needSpinner && this.spinnerTab !== showTab) {
-          if (this.spinnerTab) {
-            this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
-          } else {
-            this.spinnerDisplayed();
-          }
-          this.spinnerTab = showTab;
-          this.tabbrowser.setAttribute("pendingpaint", "true");
-          this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
-        }
-
-        // Switch to the tab we've decided to make visible.
-        if (this.visibleTab !== showTab) {
-          this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
-          this.visibleTab = showTab;
-
-          this.maybeVisibleTabs.add(showTab);
-
-          let tabs = this.tabbrowser.tabbox.tabs;
-          let tabPanel = this.tabbrowser.mPanelContainer;
-          let showPanel = tabs.getRelatedElement(showTab);
-          let index = Array.indexOf(tabPanel.childNodes, showPanel);
-          if (index != -1) {
-            this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
-            tabPanel.setAttribute("selectedIndex", index);
-            if (showTab === this.requestedTab) {
-              if (this._requestingTab) {
-                /*
-                 * If _requestingTab is set, that means that we're switching the
-                 * visibility of the tab synchronously, and we need to wait for
-                 * the "select" event before shifting focus so that
-                 * _adjustFocusAfterTabSwitch runs with the right information for
-                 * the tab switch.
-                 */
-                this.tabbrowser.addEventListener("select", () => {
-                  this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
-                }, { once: true });
-              } else {
-                this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
-              }
-
-              this.maybeActivateDocShell(this.requestedTab);
-            }
-          }
-
-          // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
-          if (this.lastVisibleTab)
-            this.lastVisibleTab._visuallySelected = false;
-
-          this.visibleTab._visuallySelected = true;
-        }
-
-        this.lastVisibleTab = this.visibleTab;
-      },
-
-      assert(cond) {
-        if (!cond) {
-          dump("Assertion failure\n" + Error().stack);
-
-          // Don't break a user's browser if an assertion fails.
-          if (AppConstants.DEBUG) {
-            throw new Error("Assertion failure");
-          }
-        }
-      },
-
-      // We've decided to try to load requestedTab.
-      loadRequestedTab() {
-        this.assert(!this.loadTimer);
-        this.assert(!this.minimizedOrFullyOccluded);
-
-        // loadingTab can be non-null here if we timed out loading the current tab.
-        // In that case we just overwrite it with a different tab; it's had its chance.
-        this.loadingTab = this.requestedTab;
-        this.log("Loading tab " + this.tinfo(this.loadingTab));
-
-        this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
-        this.setTabState(this.requestedTab, this.STATE_LOADING);
-      },
-
-      maybeActivateDocShell(tab) {
-        // If we've reached the point where the requested tab has entered
-        // the loaded state, but the DocShell is still not yet active, we
-        // should activate it.
-        let browser = tab.linkedBrowser;
-        let state = this.getTabState(tab);
-        let canCheckDocShellState = !browser.mDestroyed &&
-          (browser.docShell || browser.frameLoader.tabParent);
-        if (tab == this.requestedTab &&
-            canCheckDocShellState &&
-            state == this.STATE_LOADED &&
-            !browser.docShellIsActive &&
-            !this.minimizedOrFullyOccluded) {
-          browser.docShellIsActive = true;
-          this.logState("Set requested tab docshell to active and preserveLayers to false");
-          // 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.
-          browser.preserveLayers(false);
-        }
-      },
-
-      // 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 [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.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
-          this.lastPrimaryTab = null;
-        }
-        if (this.blankTab && !this.blankTab.linkedBrowser) {
-          this.blankTab = null;
-        }
-        if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
-          this.spinnerHidden();
-          this.spinnerTab = null;
-        }
-        if (this.loadingTab && !this.loadingTab.linkedBrowser) {
-          this.loadingTab = null;
-          this.clearTimer(this.loadTimer);
-          this.loadTimer = null;
-        }
-      },
-
-      // This code runs after we've responded to an event or requested a new
-      // tab. It's expected that we've already updated all the principal
-      // state variables. This function takes care of updating any auxilliary
-      // state.
-      postActions() {
-        // Once we finish loading loadingTab, we null it out. So the state should
-        // always be LOADING.
-        this.assert(!this.loadingTab ||
-          this.getTabState(this.loadingTab) == this.STATE_LOADING);
-
-        // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
-        // the timer is set only when we're loading something.
-        this.assert(!this.loadTimer || this.loadingTab);
-        this.assert(!this.loadingTab || this.loadTimer);
-
-        // If we're switching to a non-remote tab, there's no need to wait
-        // for it to send layers to the compositor, as this will happen
-        // synchronously. Clearing this here means that in the next step,
-        // we can load the non-remote browser immediately.
-        if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
-          this.loadingTab = null;
-          if (this.loadTimer) {
-            this.clearTimer(this.loadTimer);
-            this.loadTimer = null;
-          }
+  onSizeModeOrOcclusionStateChange() {
+    if (this.minimizedOrFullyOccluded) {
+      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 we're not loading anything, try loading the requested tab.
-        let stateOfRequestedTab = this.getTabState(this.requestedTab);
-        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();
-        }
-
-        // 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;
-        }
-
-        this.maybeFinishTabSwitch();
-
-        if (numWarming > this.tabbrowser.tabWarmingMax) {
-          this.logState("Hit tabWarmingMax");
-          if (this.unloadTimer) {
-            this.clearTimer(this.unloadTimer);
-          }
-          this.unloadNonRequiredTabs();
-        }
-
-        if (numPending == 0) {
-          this.finish();
-        }
-
-        this.logState("done");
-      },
-
-      // Fires when we're ready to unload unused tabs.
-      onUnloadTimeout() {
-        this.logState("onUnloadTimeout");
-        this.preActions();
-        this.unloadTimer = null;
-
-        this.unloadNonRequiredTabs();
-
-        this.postActions();
-      },
-
-      // 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;
-
-        // Unload any tabs that can be unloaded.
-        for (let [tab, state] of this.tabState) {
-          if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
-            continue;
-          }
-
-          if (state == this.STATE_LOADED &&
-              !this.maybeVisibleTabs.has(tab) &&
-              tab !== this.lastVisibleTab &&
-              tab !== this.loadingTab &&
-              tab !== this.requestedTab) {
-            this.setTabState(tab, this.STATE_UNLOADING);
-          }
-
-          if (state != this.STATE_UNLOADED && tab !== this.requestedTab) {
-            numPending++;
-          }
-        }
-
-        if (numPending) {
-          // Keep the timer going since there may be more tabs to unload.
-          this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
-        }
-      },
-
-      // Fires when an ongoing load has taken too long.
-      onLoadTimeout() {
-        this.logState("onLoadTimeout");
-        this.preActions();
-        this.loadTimer = null;
-        this.loadingTab = null;
-        this.postActions();
-      },
-
-      // Fires when the layers become available for a tab.
-      onLayersReady(browser) {
-        let tab = this.tabbrowser.getTabForBrowser(browser);
-        if (!tab) {
-          // We probably got a layer update from a tab that got before
-          // the switcher was created, or for browser that's not being
-          // tracked by the async tab switcher (like the preloaded about:newtab).
-          return;
-        }
-
-        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.unwarmTab(tab);
-
-        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();
-      },
-
-      // 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 ||
-            this.getTabState(tab) == this.STATE_UNLOADED);
-          this.setTabState(tab, this.STATE_UNLOADED);
-        }
-      },
-
-      // Called when a tab switches from remote to non-remote. In this case
-      // a MozLayerTreeReady notification that we requested may never fire,
-      // so we need to simulate it.
-      onRemotenessChange(tab) {
-        this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`);
-        if (!tab.linkedBrowser.isRemoteBrowser) {
-          if (this.getTabState(tab) == this.STATE_LOADING) {
-            this.onLayersReady(tab.linkedBrowser);
-          } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
-            this.onLayersCleared(tab.linkedBrowser);
-          }
-        } else if (this.getTabState(tab) == this.STATE_LOADED) {
-          // A tab just changed from non-remote to remote, which means
-          // that it's gone back into the STATE_LOADING state until
-          // it sends up a layer tree.
-          this.setTabState(tab, this.STATE_LOADING);
-        }
-      },
-
-      // Called when a tab has been removed, and the browser node is
-      // about to be removed from the DOM.
-      onTabRemoved(tab) {
-        if (this.lastVisibleTab == tab) {
-          // The browser that was being presented to the user is
-          // going to be removed during this tick of the event loop.
-          // This will cause us to show a tab spinner instead.
-          this.preActions();
-          this.lastVisibleTab = null;
-          this.postActions();
-        }
-      },
-
-      onSizeModeOrOcclusionStateChange() {
-        if (this.minimizedOrFullyOccluded) {
-          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_LOADING || state == this.STATE_LOADED) {
-              this.setTabState(tab, this.STATE_UNLOADING);
-            }
-          }
-          if (this.loadTimer) {
-            this.clearTimer(this.loadTimer);
-            this.loadTimer = null;
-          }
-          this.loadingTab = null;
-        } else {
-          // We're no longer minimized or occluded. This means we might want
-          // to activate the current tab's docShell.
-          this.maybeActivateDocShell(gBrowser.selectedTab);
-        }
-      },
-
-      onSwapDocShells(ourBrowser, otherBrowser) {
-        // This event fires before the swap. ourBrowser is from
-        // our window. We save the state of otherBrowser since ourBrowser
-        // needs to take on that state at the end of the swap.
-
-        let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
-        let otherState;
-        if (otherTabbrowser && otherTabbrowser._switcher) {
-          let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
-          let otherSwitcher = otherTabbrowser._switcher;
-          otherState = otherSwitcher.getTabState(otherTab);
-        } else {
-          otherState = otherBrowser.docShellIsActive ? this.STATE_LOADED : this.STATE_UNLOADED;
-        }
-        if (!this.swapMap) {
-          this.swapMap = new WeakMap();
-        }
-        this.swapMap.set(otherBrowser, {
-          state: otherState,
-        });
-      },
-
-      onEndSwapDocShells(ourBrowser, otherBrowser) {
-        // The swap has happened. We reset the loadingTab in
-        // case it has been swapped. We also set ourBrowser's state
-        // to whatever otherBrowser's state was before the swap.
-
-        if (this.loadTimer) {
-          // Clearing the load timer means that we will
-          // immediately display a spinner if ourBrowser isn't
-          // ready yet. Typically it will already be ready
-          // though. If it's not, we're probably in a new window,
-          // in which case we have no other tabs to display anyway.
-          this.clearTimer(this.loadTimer);
-          this.loadTimer = null;
-        }
-        this.loadingTab = null;
-
-        let { state: otherState } = this.swapMap.get(otherBrowser);
-
-        this.swapMap.delete(otherBrowser);
-
-        let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
-        if (ourTab) {
-          this.setTabStateNoAction(ourTab, otherState);
-        }
-      },
-
-      shouldActivateDocShell(browser) {
-        let tab = this.tabbrowser.getTabForBrowser(browser);
-        let state = this.getTabState(tab);
-        return state == this.STATE_LOADING || state == this.STATE_LOADED;
-      },
-
-      activateBrowserForPrintPreview(browser) {
-        let tab = this.tabbrowser.getTabForBrowser(browser);
-        let state = this.getTabState(tab);
-        if (state != this.STATE_LOADING &&
-            state != this.STATE_LOADED) {
-          this.setTabState(tab, this.STATE_LOADING);
-          this.logState("Activated browser " + this.tinfo(tab) + " for print preview");
+        if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
+          this.setTabState(tab, this.STATE_UNLOADING);
         }
-      },
-
-      canWarmTab(tab) {
-        if (!this.tabbrowser.tabWarmingEnabled) {
-          return false;
-        }
-
-        if (!tab) {
-          return false;
-        }
-
-        // 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.tabbrowser.tabWarmingUnloadDelay);
-      },
-
-      // Called when the user asks to switch to a given tab.
-      requestTab(tab) {
-        if (tab === this.requestedTab) {
-          return;
-        }
-
-        if (this.tabbrowser.tabWarmingEnabled) {
-          let warmingState = "disqualified";
-
-          if (this.warmingTabs.has(tab)) {
-            let tabState = this.getTabState(tab);
-            if (tabState == this.STATE_LOADING) {
-              warmingState = "stillLoading";
-            } else if (tabState == this.STATE_LOADED) {
-              warmingState = "loaded";
-            }
-          } else if (this.canWarmTab(tab)) {
-            warmingState = "notWarmed";
-          }
-
-          Services.telemetry
-            .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
-            .add(warmingState);
-        }
-
-        this._requestingTab = true;
-        this.logState("requestTab " + this.tinfo(tab));
-        this.startTabSwitch();
-
-        this.requestedTab = tab;
-
-        tab.linkedBrowser.setAttribute("primary", "true");
-        if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
-          this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
-        }
-        this.lastPrimaryTab = tab;
-
-        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(), unloadTimeout);
-
-        this.postActions();
-      },
-
-      handleEvent(event, delayed = false) {
-        if (this._processing) {
-          this.setTimer(() => this.handleEvent(event, true), 0);
-          return;
-        }
-        if (delayed && this.tabbrowser._switcher != this) {
-          // if we delayed processing this event, we might be out of date, in which
-          // case we drop the delayed events
-          return;
-        }
-        this._processing = true;
-        this.preActions();
-
-        if (event.type == "MozLayerTreeReady") {
-          this.onLayersReady(event.originalTarget);
-        }
-        if (event.type == "MozAfterPaint") {
-          this.onPaint();
-        } else if (event.type == "MozLayerTreeCleared") {
-          this.onLayersCleared(event.originalTarget);
-        } else if (event.type == "TabRemotenessChange") {
-          this.onRemotenessChange(event.target);
-        } else if (event.type == "sizemodechange" ||
-          event.type == "occlusionstatechange") {
-          this.onSizeModeOrOcclusionStateChange();
-        } else if (event.type == "SwapDocShells") {
-          this.onSwapDocShells(event.originalTarget, event.detail);
-        } else if (event.type == "EndSwapDocShells") {
-          this.onEndSwapDocShells(event.originalTarget, event.detail);
-        }
-
-        this.postActions();
-        this._processing = false;
-      },
-
-      /*
-       * Telemetry and Profiler related helpers for recording tab switch
-       * timing.
-       */
-
-      startTabSwitch() {
-        TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
-        TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
-        this.addMarker("AsyncTabSwitch:Start");
-        this.switchInProgress = true;
-      },
-
-      /**
-       * Something has occurred that might mean that we've completed
-       * 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)) {
-          // 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", window);
-          if (time != -1) {
-            TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
-            this.log("DEBUG: tab switch time = " + time);
-            this.addMarker("AsyncTabSwitch:Finish");
-          }
-          this.switchInProgress = false;
-        }
-      },
-
-      spinnerDisplayed() {
-        this.assert(!this.spinnerTab);
-        let browser = this.requestedTab.linkedBrowser;
-        this.assert(browser.isRemoteBrowser);
-        TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
-        // We have a second, similar probe for capturing recordings of
-        // when the spinner is displayed for very long periods.
-        TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
-        this.addMarker("AsyncTabSwitch:SpinnerShown");
-      },
-
-      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
-      },
-
-      addMarker(marker) {
-        if (Services.profiler) {
-          Services.profiler.AddMarker(marker);
-        }
-      },
-
-      /*
-       * Debug related logging for switcher.
-       */
-
-      _useDumpForLogging: false,
-      _logInit: false,
-
-      logging() {
-        if (this._useDumpForLogging)
-          return true;
-        if (this._logInit)
-          return this._shouldLog;
-        let result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming", false);
-        this._shouldLog = result;
-        this._logInit = true;
-        return this._shouldLog;
-      },
-
-      tinfo(tab) {
-        if (tab) {
-          return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
-        }
-        return "null";
-      },
-
-      log(s) {
-        if (!this.logging())
-          return;
-        if (this._useDumpForLogging) {
-          dump(s + "\n");
-        } else {
-          Services.console.logStringMessage(s);
-        }
-      },
-
-      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");
-        } else {
-          Services.console.logStringMessage(accum);
-        }
-      },
-    };
-    this._switcher = switcher;
-    switcher.init();
-    return switcher;
-  }
-
-  warmupTab(aTab) {
-    if (gMultiProcessBrowser) {
-      this._getSwitcher().warmupTab(aTab);
+      }
+      if (this.loadTimer) {
+        this.clearTimer(this.loadTimer);
+        this.loadTimer = null;
+      }
+      this.loadingTab = null;
+    } else {
+      // We're no longer minimized or occluded. This means we might want
+      // to activate the current tab's docShell.
+      this.maybeActivateDocShell(this.tabbrowser.selectedTab);
     }
   }
 
-  _handleKeyDownEvent(aEvent) {
-    if (!aEvent.isTrusted) {
-      // Don't let untrusted events mess with tabs.
-      return;
-    }
-
-    if (aEvent.altKey)
-      return;
-
-    // Don't check if the event was already consumed because tab
-    // navigation should always work for better user experience.
-
-    if (aEvent.ctrlKey && aEvent.shiftKey && !aEvent.metaKey) {
-      switch (aEvent.keyCode) {
-        case aEvent.DOM_VK_PAGE_UP:
-          this.moveTabBackward();
-          aEvent.preventDefault();
-          return;
-        case aEvent.DOM_VK_PAGE_DOWN:
-          this.moveTabForward();
-          aEvent.preventDefault();
-          return;
-      }
-    }
-
-    if (AppConstants.platform != "macosx") {
-      if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey &&
-          aEvent.keyCode == KeyEvent.DOM_VK_F4 &&
-          !this.mCurrentTab.pinned) {
-        this.removeCurrentTab({ animate: true });
-        aEvent.preventDefault();
-      }
-    }
-  }
-
-  _handleKeyPressEventMac(aEvent) {
-    if (!aEvent.isTrusted) {
-      // Don't let untrusted events mess with tabs.
-      return;
-    }
-
-    if (aEvent.altKey)
-      return;
-
-    if (AppConstants.platform == "macosx") {
-      if (!aEvent.metaKey)
-        return;
-
-      var offset = 1;
-      switch (aEvent.charCode) {
-        case "}".charCodeAt(0):
-          offset = -1;
-        case "{".charCodeAt(0):
-          if (window.getComputedStyle(this.container).direction == "ltr")
-            offset *= -1;
-          this.tabContainer.advanceSelectedTab(offset, true);
-          aEvent.preventDefault();
-      }
-    }
-  }
+  onSwapDocShells(ourBrowser, otherBrowser) {
+    // This event fires before the swap. ourBrowser is from
+    // our window. We save the state of otherBrowser since ourBrowser
+    // needs to take on that state at the end of the swap.
 
-  createTooltip(event) {
-    event.stopPropagation();
-    var tab = document.tooltipNode;
-    if (tab.localName != "tab") {
-      event.preventDefault();
-      return;
+    let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
+    let otherState;
+    if (otherTabbrowser && otherTabbrowser._switcher) {
+      let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
+      let otherSwitcher = otherTabbrowser._switcher;
+      otherState = otherSwitcher.getTabState(otherTab);
+    } else {
+      otherState = otherBrowser.docShellIsActive ? this.STATE_LOADED : this.STATE_UNLOADED;
     }
-
-    let stringWithShortcut = (stringId, keyElemId) => {
-      let keyElem = document.getElementById(keyElemId);
-      let shortcut = ShortcutUtils.prettifyShortcut(keyElem);
-      return gTabBrowserBundle.formatStringFromName(stringId, [shortcut], 1);
-    };
-
-    var label;
-    if (tab.mOverCloseButton) {
-      label = tab.selected ?
-        stringWithShortcut("tabs.closeSelectedTab.tooltip", "key_close") :
-        gTabBrowserBundle.GetStringFromName("tabs.closeTab.tooltip");
-    } else if (tab._overPlayingIcon) {
-      let stringID;
-      if (tab.selected) {
-        stringID = tab.linkedBrowser.audioMuted ?
-          "tabs.unmuteAudio.tooltip" :
-          "tabs.muteAudio.tooltip";
-        label = stringWithShortcut(stringID, "key_toggleMute");
-      } else {
-        if (tab.hasAttribute("activemedia-blocked")) {
-          stringID = "tabs.unblockAudio.tooltip";
-        } else {
-          stringID = tab.linkedBrowser.audioMuted ?
-            "tabs.unmuteAudio.background.tooltip" :
-            "tabs.muteAudio.background.tooltip";
-        }
-
-        label = gTabBrowserBundle.GetStringFromName(stringID);
-      }
-    } else {
-      label = tab._fullLabel || tab.getAttribute("label");
-      if (AppConstants.NIGHTLY_BUILD &&
-          tab.linkedBrowser &&
-          tab.linkedBrowser.isRemoteBrowser &&
-          tab.linkedBrowser.frameLoader) {
-        label += " (pid " + tab.linkedBrowser.frameLoader.tabParent.osPid + ")";
-      }
-      if (tab.userContextId) {
-        label = gTabBrowserBundle.formatStringFromName("tabs.containers.tooltip", [label, ContextualIdentityService.getUserContextLabel(tab.userContextId)], 2);
-      }
+    if (!this.swapMap) {
+      this.swapMap = new WeakMap();
     }
-
-    event.target.setAttribute("label", label);
-  }
-
-  handleEvent(aEvent) {
-    switch (aEvent.type) {
-      case "keydown":
-        this._handleKeyDownEvent(aEvent);
-        break;
-      case "keypress":
-        this._handleKeyPressEventMac(aEvent);
-        break;
-      case "sizemodechange":
-      case "occlusionstatechange":
-        if (aEvent.target == window && !this._switcher) {
-          this.mCurrentBrowser.preserveLayers(
-            window.windowState == window.STATE_MINIMIZED || window.isFullyOccluded);
-          this.mCurrentBrowser.docShellIsActive = this.shouldActivateDocShell(this.mCurrentBrowser);
-        }
-        break;
-    }
+    this.swapMap.set(otherBrowser, {
+      state: otherState,
+    });
   }
 
-  receiveMessage(aMessage) {
-    let data = aMessage.data;
-    let browser = aMessage.target;
-
-    switch (aMessage.name) {
-      case "DOMTitleChanged":
-      {
-        let tab = this.getTabForBrowser(browser);
-        if (!tab || tab.hasAttribute("pending"))
-          return undefined;
-        let titleChanged = this.setTabTitle(tab);
-        if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
-          tab.setAttribute("titlechanged", "true");
-        break;
-      }
-      case "DOMWindowClose":
-      {
-        if (this.tabs.length == 1) {
-          // We already did PermitUnload in the content process
-          // for this tab (the only one in the window). So we don't
-          // need to do it again for any tabs.
-          window.skipNextCanClose = true;
-          window.close();
-          return undefined;
-        }
-
-        let tab = this.getTabForBrowser(browser);
-        if (tab) {
-          // Skip running PermitUnload since it already happened in
-          // the content process.
-          this.removeTab(tab, { skipPermitUnload: true });
-        }
-        break;
-      }
-      case "contextmenu":
-      {
-        openContextMenu(aMessage);
-        break;
-      }
-      case "DOMWindowFocus":
-      {
-        let tab = this.getTabForBrowser(browser);
-        if (!tab)
-          return undefined;
-        this.selectedTab = tab;
-        window.focus();
-        break;
-      }
-      case "Browser:Init":
-      {
-        let tab = this.getTabForBrowser(browser);
-        if (!tab)
-          return undefined;
-
-        this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
-        browser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned });
-        break;
-      }
-      case "Browser:WindowCreated":
-      {
-        let tab = this.getTabForBrowser(browser);
-        if (tab && data.userContextId) {
-          ContextualIdentityService.telemetry(data.userContextId);
-          tab.setUserContextId(data.userContextId);
-        }
-
-        // We don't want to update the container icon and identifier if
-        // this is not the selected browser.
-        if (browser == gBrowser.selectedBrowser) {
-          updateUserContextUIIndicator();
-        }
+  onEndSwapDocShells(ourBrowser, otherBrowser) {
+    // The swap has happened. We reset the loadingTab in
+    // case it has been swapped. We also set ourBrowser's state
+    // to whatever otherBrowser's state was before the swap.
 
-        break;
-      }
-      case "Findbar:Keypress":
-      {
-        let tab = this.getTabForBrowser(browser);
-        // If the find bar for this tab is not yet alive, only initialize
-        // it if there's a possibility FindAsYouType will be used.
-        // There's no point in doing it for most random keypresses.
-        if (!this.isFindBarInitialized(tab) &&
-            data.shouldFastFind) {
-          let shouldFastFind = this._findAsYouType;
-          if (!shouldFastFind) {
-            // Please keep in sync with toolkit/content/widgets/findbar.xml
-            const FAYT_LINKS_KEY = "'";
-            const FAYT_TEXT_KEY = "/";
-            let charCode = data.fakeEvent.charCode;
-            let key = charCode ? String.fromCharCode(charCode) : null;
-            shouldFastFind = key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY;
-          }
-          if (shouldFastFind) {
-            // Make sure we return the result.
-            return this.getFindBar(tab).receiveMessage(aMessage);
-          }
-        }
-        break;
-      }
-      case "RefreshBlocker:Blocked":
-      {
-        // The data object is expected to contain the following properties:
-        //  - URI (string)
-        //     The URI that a page is attempting to refresh or redirect to.
-        //  - delay (int)
-        //     The delay (in milliseconds) before the page was going to
-        //     reload or redirect.
-        //  - sameURI (bool)
-        //     true if we're refreshing the page. false if we're redirecting.
-        //  - outerWindowID (int)
-        //     The outerWindowID of the frame that requested the refresh or
-        //     redirect.
-
-        let brandBundle = document.getElementById("bundle_brand");
-        let brandShortName = brandBundle.getString("brandShortName");
-        let message =
-          gNavigatorBundle.getFormattedString("refreshBlocked." +
-                                              (data.sameURI ? "refreshLabel"
-                                                            : "redirectLabel"),
-                                              [brandShortName]);
-
-        let notificationBox = this.getNotificationBox(browser);
-        let notification = notificationBox.getNotificationWithValue("refresh-blocked");
-
-        if (notification) {
-          notification.label = message;
-        } else {
-          let refreshButtonText =
-            gNavigatorBundle.getString("refreshBlocked.goButton");
-          let refreshButtonAccesskey =
-            gNavigatorBundle.getString("refreshBlocked.goButton.accesskey");
+    if (this.loadTimer) {
+      // Clearing the load timer means that we will
+      // immediately display a spinner if ourBrowser isn't
+      // ready yet. Typically it will already be ready
+      // though. If it's not, we're probably in a new window,
+      // in which case we have no other tabs to display anyway.
+      this.clearTimer(this.loadTimer);
+      this.loadTimer = null;
+    }
+    this.loadingTab = null;
 
-          let buttons = [{
-            label: refreshButtonText,
-            accessKey: refreshButtonAccesskey,
-            callback() {
-              if (browser.messageManager) {
-                browser.messageManager.sendAsyncMessage("RefreshBlocker:Refresh", data);
-              }
-            }
-          }];
+    let { state: otherState } = this.swapMap.get(otherBrowser);
 
-          notificationBox.appendNotification(message, "refresh-blocked",
-            "chrome://browser/skin/notification-icons/popup.svg",
-            notificationBox.PRIORITY_INFO_MEDIUM,
-            buttons);
-        }
-        break;
-      }
-    }
-    return undefined;
-  }
+    this.swapMap.delete(otherBrowser);
 
-  observe(aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case "contextual-identity-updated":
-      {
-        for (let tab of this.tabs) {
-          if (tab.getAttribute("usercontextid") == aData) {
-            ContextualIdentityService.setTabStyle(tab);
-          }
-        }
-        break;
-      }
-      case "nsPref:changed":
-      {
-        // This is the only pref observed.
-        this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
-        break;
-      }
-    }
-  }
-
-  _updateNewTabVisibility() {
-    // Helper functions to help deal with customize mode wrapping some items
-    let wrap = n => n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
-    let unwrap = n => n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
-
-    let sib = this.tabContainer;
-    do {
-      sib = unwrap(wrap(sib).nextElementSibling);
-    } while (sib && sib.hidden);
-
-    const kAttr = "hasadjacentnewtabbutton";
-    if (sib && sib.id == "new-tab-button") {
-      this.tabContainer.setAttribute(kAttr, "true");
-    } else {
-      this.tabContainer.removeAttribute(kAttr);
-    }
-  }
-
-  onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
-    if (aContainer.ownerDocument == document &&
-        aContainer.id == "TabsToolbar") {
-      this._updateNewTabVisibility();
+    let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
+    if (ourTab) {
+      this.setTabStateNoAction(ourTab, otherState);
     }
   }
 
-  onAreaNodeRegistered(aArea, aContainer) {
-    if (aContainer.ownerDocument == document &&
-        aArea == "TabsToolbar") {
-      this._updateNewTabVisibility();
+  shouldActivateDocShell(browser) {
+    let tab = this.tabbrowser.getTabForBrowser(browser);
+    let state = this.getTabState(tab);
+    return state == this.STATE_LOADING || state == this.STATE_LOADED;
+  }
+
+  activateBrowserForPrintPreview(browser) {
+    let tab = this.tabbrowser.getTabForBrowser(browser);
+    let state = this.getTabState(tab);
+    if (state != this.STATE_LOADING &&
+        state != this.STATE_LOADED) {
+      this.setTabState(tab, this.STATE_LOADING);
+      this.logState("Activated browser " + this.tinfo(tab) + " for print preview");
     }
   }
 
-  onAreaReset(aArea, aContainer) {
-    this.onAreaNodeRegistered(aArea, aContainer);
-  }
-
-  _generateUniquePanelID() {
-    if (!this._uniquePanelIDCounter) {
-      this._uniquePanelIDCounter = 0;
+  canWarmTab(tab) {
+    if (!this.tabbrowser.tabWarmingEnabled) {
+      return false;
     }
 
-    let outerID = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                        .getInterface(Ci.nsIDOMWindowUtils)
-                        .outerWindowID;
-
-    // We want panel IDs to be globally unique, that's why we include the
-    // window ID. We switched to a monotonic counter as Date.now() lead
-    // to random failures because of colliding IDs.
-    return "panel-" + outerID + "-" + (++this._uniquePanelIDCounter);
-  }
-
-  disconnectedCallback() {
-    Services.obs.removeObserver(this, "contextual-identity-updated");
-
-    CustomizableUI.removeListener(this);
-
-    for (let tab of this.tabs) {
-      let browser = tab.linkedBrowser;
-      if (browser.registeredOpenURI) {
-        this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
-          browser.getAttribute("usercontextid") || 0);
-        delete browser.registeredOpenURI;
-      }
-
-      let filter = this._tabFilters.get(tab);
-      if (filter) {
-        browser.webProgress.removeProgressListener(filter);
-
-        let listener = this._tabListeners.get(tab);
-        if (listener) {
-          filter.removeProgressListener(listener);
-          listener.destroy();
-        }
-
-        this._tabFilters.delete(tab);
-        this._tabListeners.delete(tab);
-      }
-    }
-    const nsIEventListenerService =
-      Ci.nsIEventListenerService;
-    let els = Cc["@mozilla.org/eventlistenerservice;1"]
-      .getService(nsIEventListenerService);
-    els.removeSystemEventListener(document, "keydown", this, false);
-    if (AppConstants.platform == "macosx") {
-      els.removeSystemEventListener(document, "keypress", this, false);
-    }
-    window.removeEventListener("sizemodechange", this);
-    window.removeEventListener("occlusionstatechange", this);
-
-    if (gMultiProcessBrowser) {
-      let messageManager = window.getGroupMessageManager("browsers");
-      messageManager.removeMessageListener("DOMTitleChanged", this);
-      window.messageManager.removeMessageListener("contextmenu", this);
-
-      if (this._switcher) {
-        this._switcher.destroy();
-      }
+    if (!tab) {
+      return false;
     }
 
-    Services.prefs.removeObserver("accessibility.typeaheadfind", this);
-  }
-
-  _setupEventListeners() {
-    this.addEventListener("DOMWindowClose", (event) => {
-      if (!event.isTrusted)
-        return;
-
-      if (this.tabs.length == 1) {
-        // We already did PermitUnload in nsGlobalWindow::Close
-        // for this tab. There are no other tabs we need to do
-        // PermitUnload for.
-        window.skipNextCanClose = true;
-        return;
-      }
-
-      var tab = this._getTabForContentWindow(event.target);
-      if (tab) {
-        // Skip running PermitUnload since it already happened.
-        this.removeTab(tab, { skipPermitUnload: true });
-        event.preventDefault();
-      }
-    }, true);
-
-    this.addEventListener("DOMWillOpenModalDialog", (event) => {
-      if (!event.isTrusted)
-        return;
-
-      let targetIsWindow = event.target instanceof Window;
-
-      // We're about to open a modal dialog, so figure out for which tab:
-      // If this is a same-process modal dialog, then we're given its DOM
-      // window as the event's target. For remote dialogs, we're given the
-      // browser, but that's in the originalTarget and not the target,
-      // because it's across the tabbrowser's XBL boundary.
-      let tabForEvent = targetIsWindow ?
-        this._getTabForContentWindow(event.target.top) :
-        this.getTabForBrowser(event.originalTarget);
-
-      // Focus window for beforeunload dialog so it is seen but don't
-      // steal focus from other applications.
-      if (event.detail &&
-          event.detail.tabPrompt &&
-          event.detail.inPermitUnload &&
-          Services.focus.activeWindow)
-        window.focus();
-
-      // Don't need to act if the tab is already selected or if there isn't
-      // a tab for the event (e.g. for the webextensions options_ui remote
-      // browsers embedded in the "about:addons" page):
-      if (!tabForEvent || tabForEvent.selected)
-        return;
-
-      // We always switch tabs for beforeunload tab-modal prompts.
-      if (event.detail &&
-          event.detail.tabPrompt &&
-          !event.detail.inPermitUnload) {
-        let docPrincipal = targetIsWindow ? event.target.document.nodePrincipal : null;
-        // At least one of these should/will be non-null:
-        let promptPrincipal = event.detail.promptPrincipal || docPrincipal ||
-          tabForEvent.linkedBrowser.contentPrincipal;
-        // For null principals, we bail immediately and don't show the checkbox:
-        if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
-          tabForEvent.setAttribute("attention", "true");
-          return;
-        }
-
-        // For non-system/expanded principals, we bail and show the checkbox
-        if (promptPrincipal.URI &&
-            !Services.scriptSecurityManager.isSystemPrincipal(promptPrincipal)) {
-          let permission = Services.perms.testPermissionFromPrincipal(promptPrincipal,
-            "focus-tab-by-prompt");
-          if (permission != Services.perms.ALLOW_ACTION) {
-            // Tell the prompt box we want to show the user a checkbox:
-            let tabPrompt = this.getTabModalPromptBox(tabForEvent.linkedBrowser);
-            tabPrompt.onNextPromptShowAllowFocusCheckboxFor(promptPrincipal);
-            tabForEvent.setAttribute("attention", "true");
-            return;
-          }
-        }
-        // ... so system and expanded principals, as well as permitted "normal"
-        // URI-based principals, always get to steal focus for the tab when prompting.
-      }
-
-      // If permissions/origins dictate so, bring tab to the front.
-      this.selectedTab = tabForEvent;
-    }, true);
-
-    this.addEventListener("DOMTitleChanged", (event) => {
-      if (!event.isTrusted)
-        return;
-
-      var contentWin = event.target.defaultView;
-      if (contentWin != contentWin.top)
-        return;
-
-      var tab = this._getTabForContentWindow(contentWin);
-      if (!tab || tab.hasAttribute("pending"))
-        return;
-
-      var titleChanged = this.setTabTitle(tab);
-      if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
-        tab.setAttribute("titlechanged", "true");
-    });
-
-    this.addEventListener("oop-browser-crashed", (event) => {
-      if (!event.isTrusted)
-        return;
-
-      let browser = event.originalTarget;
-
-      // Preloaded browsers do not actually have any tabs. If one crashes,
-      // it should be released and removed.
-      if (browser === this._preloadedBrowser) {
-        this.removePreloadedBrowser();
-        return;
-      }
-
-      let icon = browser.mIconURL;
-      let tab = this.getTabForBrowser(browser);
-
-      if (this.selectedBrowser == browser) {
-        TabCrashHandler.onSelectedBrowserCrash(browser);
-      } else {
-        this.updateBrowserRemoteness(browser, false);
-        SessionStore.reviveCrashedTab(tab);
-      }
-
-      tab.removeAttribute("soundplaying");
-      this.setIcon(tab, icon, browser.contentPrincipal, browser.contentRequestContextID);
-    });
-
-    this.addEventListener("DOMAudioPlaybackStarted", (event) => {
-      var tab = this.getTabFromAudioEvent(event);
-      if (!tab) {
-        return;
-      }
-
-      clearTimeout(tab._soundPlayingAttrRemovalTimer);
-      tab._soundPlayingAttrRemovalTimer = 0;
-
-      let modifiedAttrs = [];
-      if (tab.hasAttribute("soundplaying-scheduledremoval")) {
-        tab.removeAttribute("soundplaying-scheduledremoval");
-        modifiedAttrs.push("soundplaying-scheduledremoval");
-      }
-
-      if (!tab.hasAttribute("soundplaying")) {
-        tab.setAttribute("soundplaying", true);
-        modifiedAttrs.push("soundplaying");
-      }
-
-      if (modifiedAttrs.length) {
-        // Flush style so that the opacity takes effect immediately, in
-        // case the media is stopped before the style flushes naturally.
-        getComputedStyle(tab).opacity;
-      }
-
-      this._tabAttrModified(tab, modifiedAttrs);
-    });
-
-    this.addEventListener("DOMAudioPlaybackStopped", (event) => {
-      var tab = this.getTabFromAudioEvent(event);
-      if (!tab) {
-        return;
-      }
-
-      if (tab.hasAttribute("soundplaying")) {
-        let removalDelay = Services.prefs.getIntPref("browser.tabs.delayHidingAudioPlayingIconMS");
-
-        tab.style.setProperty("--soundplaying-removal-delay", `${removalDelay - 300}ms`);
-        tab.setAttribute("soundplaying-scheduledremoval", "true");
-        this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);
-
-        tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
-          tab.removeAttribute("soundplaying-scheduledremoval");
-          tab.removeAttribute("soundplaying");
-          this._tabAttrModified(tab, ["soundplaying", "soundplaying-scheduledremoval"]);
-        }, removalDelay);
-      }
-    });
-
-    this.addEventListener("DOMAudioPlaybackBlockStarted", (event) => {
-      var tab = this.getTabFromAudioEvent(event);
-      if (!tab) {
-        return;
-      }
-
-      if (!tab.hasAttribute("activemedia-blocked")) {
-        tab.setAttribute("activemedia-blocked", true);
-        this._tabAttrModified(tab, ["activemedia-blocked"]);
-        tab.startMediaBlockTimer();
-      }
-    });
-
-    this.addEventListener("DOMAudioPlaybackBlockStopped", (event) => {
-      var tab = this.getTabFromAudioEvent(event);
-      if (!tab) {
-        return;
-      }
-
-      if (tab.hasAttribute("activemedia-blocked")) {
-        tab.removeAttribute("activemedia-blocked");
-        this._tabAttrModified(tab, ["activemedia-blocked"]);
-        let hist = Services.telemetry.getHistogramById("TAB_AUDIO_INDICATOR_USED");
-        hist.add(2 /* unblockByVisitingTab */ );
-        tab.finishMediaBlockTimer();
-      }
-    });
-  }
-}
-
-/**
- * A web progress listener object definition for a given tab.
- */
-class TabProgressListener {
-  constructor(aTab, aBrowser, aStartsBlank, aWasPreloadedBrowser, aOrigStateFlags) {
-    let stateFlags = aOrigStateFlags || 0;
-    // Initialize mStateFlags to non-zero e.g. when creating a progress
-    // listener for preloaded browsers as there was no progress listener
-    // around when the content started loading. If the content didn't
-    // quite finish loading yet, mStateFlags will very soon be overridden
-    // with the correct value and end up at STATE_STOP again.
-    if (aWasPreloadedBrowser) {
-      stateFlags = Ci.nsIWebProgressListener.STATE_STOP |
-        Ci.nsIWebProgressListener.STATE_IS_REQUEST;
+    // 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;
     }
 
-    this.mTab = aTab;
-    this.mBrowser = aBrowser;
-    this.mBlank = aStartsBlank;
-
-    // cache flags for correct status UI update after tab switching
-    this.mStateFlags = stateFlags;
-    this.mStatus = 0;
-    this.mMessage = "";
-    this.mTotalProgress = 0;
-
-    // count of open requests (should always be 0 or 1)
-    this.mRequestCount = 0;
-  }
-
-  destroy() {
-    delete this.mTab;
-    delete this.mBrowser;
-  }
-
-  _callProgressListeners() {
-    Array.unshift(arguments, this.mBrowser);
-    return gBrowser._callProgressListeners.apply(gBrowser, arguments);
-  }
-
-  _shouldShowProgress(aRequest) {
-    if (this.mBlank)
-      return false;
-
-    // Don't show progress indicators in tabs for about: URIs
-    // pointing to local resources.
-    if ((aRequest instanceof Ci.nsIChannel) &&
-        gBrowser._isLocalAboutURI(aRequest.originalURI, aRequest.URI)) {
+    // 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;
   }
 
-  _isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
-    if (!this.mBlank || !aWebProgress.isTopLevel) {
-      return false;
-    }
-
-    // If the state has STATE_STOP, and no requests were in flight, then this
-    // must be the initial "stop" for the initial about:blank document.
-    const nsIWebProgressListener = Ci.nsIWebProgressListener;
-    if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
-        this.mRequestCount == 0 &&
-        !aLocation) {
-      return true;
-    }
-
-    let location = aLocation ? aLocation.spec : "";
-    return location == "about:blank";
+  unwarmTab(tab) {
+    this.warmingTabs.delete(tab);
   }
 
-  onProgressChange(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress,
-                   aCurTotalProgress, aMaxTotalProgress) {
-    this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0;
-
-    if (!this._shouldShowProgress(aRequest))
+  warmupTab(tab) {
+    if (!this.canWarmTab(tab)) {
       return;
-
-    if (this.mTotalProgress && this.mTab.hasAttribute("busy"))
-      this.mTab.setAttribute("progress", "true");
+    }
 
-    this._callProgressListeners("onProgressChange",
-                                [aWebProgress, aRequest,
-                                 aCurSelfProgress, aMaxSelfProgress,
-                                 aCurTotalProgress, aMaxTotalProgress]);
-  }
+    this.logState("warmupTab " + this.tinfo(tab));
 
-  onProgressChange64(aWebProgress, aRequest, aCurSelfProgress,
-                     aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
-    return this.onProgressChange(aWebProgress, aRequest,
-      aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
-      aMaxTotalProgress);
+    this.warmingTabs.add(tab);
+    this.setTabState(tab, this.STATE_LOADING);
+    this.suppressDisplayPortAndQueueUnload(tab,
+      this.tabbrowser.tabWarmingUnloadDelay);
   }
 
-  /* eslint-disable complexity */
-  onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
-    if (!aRequest)
+  // Called when the user asks to switch to a given tab.
+  requestTab(tab) {
+    if (tab === this.requestedTab) {
       return;
-
-    const nsIWebProgressListener = Ci.nsIWebProgressListener;
-    const nsIChannel = Ci.nsIChannel;
-    let location, originalLocation;
-    try {
-      aRequest.QueryInterface(nsIChannel);
-      location = aRequest.URI;
-      originalLocation = aRequest.originalURI;
-    } catch (ex) {}
-
-    let ignoreBlank = this._isForInitialAboutBlank(aWebProgress, aStateFlags,
-      location);
-
-    // If we were ignoring some messages about the initial about:blank, and we
-    // got the STATE_STOP for it, we'll want to pay attention to those messages
-    // from here forward. Similarly, if we conclude that this state change
-    // is one that we shouldn't be ignoring, then stop ignoring.
-    if ((ignoreBlank &&
-        aStateFlags & nsIWebProgressListener.STATE_STOP &&
-        aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) ||
-        !ignoreBlank && this.mBlank) {
-      this.mBlank = false;
     }
 
-    if (aStateFlags & nsIWebProgressListener.STATE_START) {
-      this.mRequestCount++;
-    } else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
-      const NS_ERROR_UNKNOWN_HOST = 2152398878;
-      if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
-        // to prevent bug 235825: wait for the request handled
-        // by the automatic keyword resolver
-        return;
-      }
-      // since we (try to) only handle STATE_STOP of the last request,
-      // the count of open requests should now be 0
-      this.mRequestCount = 0;
-    }
+    if (this.tabbrowser.tabWarmingEnabled) {
+      let warmingState = "disqualified";
 
-    if (aStateFlags & nsIWebProgressListener.STATE_START &&
-        aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
-      if (aWebProgress.isTopLevel) {
-        // Need to use originalLocation rather than location because things
-        // like about:home and about:privatebrowsing arrive with nsIRequest
-        // pointing to their resolved jar: or file: URIs.
-        if (!(originalLocation && gInitialPages.includes(originalLocation.spec) &&
-            originalLocation != "about:blank" &&
-            this.mBrowser.initialPageLoadedFromURLBar != originalLocation.spec &&
-            this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) {
-          // Indicating that we started a load will allow the location
-          // bar to be cleared when the load finishes.
-          // In order to not overwrite user-typed content, we avoid it
-          // (see if condition above) in a very specific case:
-          // If the load is of an 'initial' page (e.g. about:privatebrowsing,
-          // about:newtab, etc.), was not explicitly typed in the location
-          // bar by the user, is not about:blank (because about:blank can be
-          // loaded by websites under their principal), and the current
-          // page in the browser is about:blank (indicating it is a newly
-          // created or re-created browser, e.g. because it just switched
-          // remoteness or is a new tab/window).
-          this.mBrowser.urlbarChangeTracker.startedLoad();
+      if (this.warmingTabs.has(tab)) {
+        let tabState = this.getTabState(tab);
+        if (tabState == this.STATE_LOADING) {
+          warmingState = "stillLoading";
+        } else if (tabState == this.STATE_LOADED) {
+          warmingState = "loaded";
         }
-        delete this.mBrowser.initialPageLoadedFromURLBar;
-        // If the browser is loading it must not be crashed anymore
-        this.mTab.removeAttribute("crashed");
+      } else if (this.canWarmTab(tab)) {
+        warmingState = "notWarmed";
       }
 
-      if (this._shouldShowProgress(aRequest)) {
-        if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING) &&
-            aWebProgress && aWebProgress.isTopLevel) {
-          this.mTab.setAttribute("busy", "true");
-          this.mTab._notselectedsinceload = !this.mTab.selected;
-          SchedulePressure.startMonitoring(window, {
-            highPressureFn() {
-              // Only switch back to the SVG loading indicator after getting
-              // three consecutive low pressure callbacks. Used to prevent
-              // switching quickly between the SVG and APNG loading indicators.
-              gBrowser.tabContainer._schedulePressureCount = gBrowser.schedulePressureDefaultCount;
-              gBrowser.tabContainer.setAttribute("schedulepressure", "true");
-            },
-            lowPressureFn() {
-              if (!gBrowser.tabContainer._schedulePressureCount ||
-                --gBrowser.tabContainer._schedulePressureCount <= 0) {
-                gBrowser.tabContainer.removeAttribute("schedulepressure");
-              }
+      Services.telemetry
+        .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
+        .add(warmingState);
+    }
 
-              // If tabs are closed while they are loading we need to
-              // stop monitoring schedule pressure. We don't stop monitoring
-              // during high pressure times because we want to eventually
-              // return to the SVG tab loading animations.
-              let continueMonitoring = true;
-              if (!document.querySelector(".tabbrowser-tab[busy]")) {
-                SchedulePressure.stopMonitoring(window);
-                continueMonitoring = false;
-              }
-              return { continueMonitoring };
-            },
-          });
-          gBrowser.syncThrobberAnimations(this.mTab);
-        }
+    this._requestingTab = true;
+    this.logState("requestTab " + this.tinfo(tab));
+    this.startTabSwitch();
 
-        if (this.mTab.selected) {
-          gBrowser.mIsBusy = true;
-        }
-      }
-    } else if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
-               aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
-
-      if (this.mTab.hasAttribute("busy")) {
-        this.mTab.removeAttribute("busy");
-        if (!document.querySelector(".tabbrowser-tab[busy]")) {
-          SchedulePressure.stopMonitoring(window);
-          gBrowser.tabContainer.removeAttribute("schedulepressure");
-        }
+    this.requestedTab = tab;
 
-        // Only animate the "burst" indicating the page has loaded if
-        // the top-level page is the one that finished loading.
-        if (aWebProgress.isTopLevel && !aWebProgress.isLoadingDocument &&
-            Components.isSuccessCode(aStatus) &&
-            !gBrowser.tabAnimationsInProgress &&
-            Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled")) {
-          if (this.mTab._notselectedsinceload) {
-            this.mTab.setAttribute("notselectedsinceload", "true");
-          } else {
-            this.mTab.removeAttribute("notselectedsinceload");
-          }
-
-          this.mTab.setAttribute("bursting", "true");
-        }
-
-        gBrowser._tabAttrModified(this.mTab, ["busy"]);
-        if (!this.mTab.selected)
-          this.mTab.setAttribute("unread", "true");
-      }
-      this.mTab.removeAttribute("progress");
-
-      if (aWebProgress.isTopLevel) {
-        let isSuccessful = Components.isSuccessCode(aStatus);
-        if (!isSuccessful && !isTabEmpty(this.mTab)) {
-          // Restore the current document's location in case the
-          // request was stopped (possibly from a content script)
-          // before the location changed.
-
-          this.mBrowser.userTypedValue = null;
+    tab.linkedBrowser.setAttribute("primary", "true");
+    if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
+      this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
+    }
+    this.lastPrimaryTab = tab;
 
-          let inLoadURI = this.mBrowser.inLoadURI;
-          if (this.mTab.selected && gURLBar && !inLoadURI) {
-            URLBarSetURI();
-          }
-        } else if (isSuccessful) {
-          this.mBrowser.urlbarChangeTracker.finishedLoad();
-        }
+    this.suppressDisplayPortAndQueueUnload(this.requestedTab, this.UNLOAD_DELAY);
+    this._requestingTab = false;
+  }
 
-        // Ignore initial about:blank to prevent flickering.
-        if (!this.mBrowser.mIconURL && !ignoreBlank) {
-          // Don't switch to the default icon on about:home or about:newtab,
-          // since these pages get their favicon set in browser code to
-          // improve perceived performance.
-          let isNewTab = originalLocation &&
-            (originalLocation.spec == "about:newtab" ||
-              originalLocation.spec == "about:privatebrowsing" ||
-              originalLocation.spec == "about:home");
-          if (!isNewTab) {
-            gBrowser.useDefaultIcon(this.mTab);
-          }
-        }
-      }
+  suppressDisplayPortAndQueueUnload(tab, unloadTimeout) {
+    let browser = tab.linkedBrowser;
+    let fl = browser.frameLoader;
 
-      // For keyword URIs clear the user typed value since they will be changed into real URIs
-      if (location.scheme == "keyword")
-        this.mBrowser.userTypedValue = null;
-
-      if (this.mTab.selected)
-        gBrowser.mIsBusy = false;
+    if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) {
+      fl.tabParent.suppressDisplayport(true);
+      this.activeSuppressDisplayport.add(fl.tabParent);
     }
 
-    if (ignoreBlank) {
-      this._callProgressListeners("onUpdateCurrentBrowser",
-                                  [aStateFlags, aStatus, "", 0],
-                                  true, false);
-    } else {
-      this._callProgressListeners("onStateChange",
-                                  [aWebProgress, aRequest, aStateFlags, aStatus],
-                                  true, false);
+    this.preActions();
+
+    if (this.unloadTimer) {
+      this.clearTimer(this.unloadTimer);
+    }
+    this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), unloadTimeout);
+
+    this.postActions();
+  }
+
+  handleEvent(event, delayed = false) {
+    if (this._processing) {
+      this.setTimer(() => this.handleEvent(event, true), 0);
+      return;
+    }
+    if (delayed && this.tabbrowser._switcher != this) {
+      // if we delayed processing this event, we might be out of date, in which
+      // case we drop the delayed events
+      return;
+    }
+    this._processing = true;
+    this.preActions();
+
+    if (event.type == "MozLayerTreeReady") {
+      this.onLayersReady(event.originalTarget);
+    }
+    if (event.type == "MozAfterPaint") {
+      this.onPaint();
+    } else if (event.type == "MozLayerTreeCleared") {
+      this.onLayersCleared(event.originalTarget);
+    } else if (event.type == "TabRemotenessChange") {
+      this.onRemotenessChange(event.target);
+    } else if (event.type == "sizemodechange" ||
+      event.type == "occlusionstatechange") {
+      this.onSizeModeOrOcclusionStateChange();
+    } else if (event.type == "SwapDocShells") {
+      this.onSwapDocShells(event.originalTarget, event.detail);
+    } else if (event.type == "EndSwapDocShells") {
+      this.onEndSwapDocShells(event.originalTarget, event.detail);
     }
 
-    this._callProgressListeners("onStateChange",
-                                [aWebProgress, aRequest, aStateFlags, aStatus],
-                                false);
-
-    if (aStateFlags & (nsIWebProgressListener.STATE_START |
-        nsIWebProgressListener.STATE_STOP)) {
-      // reset cached temporary values at beginning and end
-      this.mMessage = "";
-      this.mTotalProgress = 0;
-    }
-    this.mStateFlags = aStateFlags;
-    this.mStatus = aStatus;
+    this.postActions();
+    this._processing = false;
   }
-  /* eslint-enable complexity */
-
-  onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
-    // OnLocationChange is called for both the top-level content
-    // and the subframes.
-    let topLevel = aWebProgress.isTopLevel;
 
-    if (topLevel) {
-      let isSameDocument = !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
-      // We need to clear the typed value
-      // if the document failed to load, to make sure the urlbar reflects the
-      // failed URI (particularly for SSL errors). However, don't clear the value
-      // if the error page's URI is about:blank, because that causes complete
-      // loss of urlbar contents for invalid URI errors (see bug 867957).
-      // Another reason to clear the userTypedValue is if this was an anchor
-      // navigation initiated by the user.
-      if (this.mBrowser.didStartLoadSinceLastUserTyping() ||
-          ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
-            aLocation.spec != "about:blank") ||
-          (isSameDocument && this.mBrowser.inLoadURI)) {
-        this.mBrowser.userTypedValue = null;
-      }
+  /*
+   * Telemetry and Profiler related helpers for recording tab switch
+   * timing.
+   */
 
-      // If the tab has been set to "busy" outside the stateChange
-      // handler below (e.g. by sessionStore.navigateAndRestore), and
-      // the load results in an error page, it's possible that there
-      // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy
-      // attribute being removed. In this case we should remove the
-      // attribute here.
-      if ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
-          this.mTab.hasAttribute("busy")) {
-        this.mTab.removeAttribute("busy");
-        gBrowser._tabAttrModified(this.mTab, ["busy"]);
-      }
+  startTabSwitch() {
+    TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
+    TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
+    this.addMarker("AsyncTabSwitch:Start");
+    this.switchInProgress = true;
+  }
 
-      // If the browser was playing audio, we should remove the playing state.
-      if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) {
-        clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
-        this.mTab._soundPlayingAttrRemovalTimer = 0;
-        this.mTab.removeAttribute("soundplaying");
-        gBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
-      }
-
-      // If the browser was previously muted, we should restore the muted state.
-      if (this.mTab.hasAttribute("muted")) {
-        this.mTab.linkedBrowser.mute();
-      }
-
-      if (gBrowser.isFindBarInitialized(this.mTab)) {
-        let findBar = gBrowser.getFindBar(this.mTab);
-
-        // Close the Find toolbar if we're in old-style TAF mode
-        if (findBar.findMode != findBar.FIND_NORMAL) {
-          findBar.close();
-        }
+  /**
+   * Something has occurred that might mean that we've completed
+   * 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)) {
+      // 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");
       }
-
-      gBrowser.setTabTitle(this.mTab);
-
-      // Don't clear the favicon if this tab is in the pending
-      // state, as SessionStore will have set the icon for us even
-      // though we're pointed at an about:blank. Also don't clear it
-      // if onLocationChange was triggered by a pushState or a
-      // replaceState (bug 550565) or a hash change (bug 408415).
-      if (!this.mTab.hasAttribute("pending") &&
-          aWebProgress.isLoadingDocument &&
-          !isSameDocument) {
-        this.mBrowser.mIconURL = null;
-      }
-
-      let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
-      if (this.mBrowser.registeredOpenURI) {
-        gBrowser._unifiedComplete
-          .unregisterOpenPage(this.mBrowser.registeredOpenURI, userContextId);
-        delete this.mBrowser.registeredOpenURI;
-      }
-      // Tabs in private windows aren't registered as "Open" so
-      // that they don't appear as switch-to-tab candidates.
-      if (!isBlankPageURL(aLocation.spec) &&
-          (!PrivateBrowsingUtils.isWindowPrivate(window) ||
-            PrivateBrowsingUtils.permanentPrivateBrowsing)) {
-        gBrowser._unifiedComplete.registerOpenPage(aLocation, userContextId);
-        this.mBrowser.registeredOpenURI = aLocation;
-      }
-    }
-
-    if (!this.mBlank) {
-      this._callProgressListeners("onLocationChange",
-                                  [aWebProgress, aRequest, aLocation, aFlags]);
-    }
-
-    if (topLevel) {
-      this.mBrowser.lastURI = aLocation;
-      this.mBrowser.lastLocationChange = Date.now();
+      this.switchInProgress = false;
     }
   }
 
-  onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
-    if (this.mBlank)
-      return;
+  spinnerDisplayed() {
+    this.assert(!this.spinnerTab);
+    let browser = this.requestedTab.linkedBrowser;
+    this.assert(browser.isRemoteBrowser);
+    TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
+    // We have a second, similar probe for capturing recordings of
+    // when the spinner is displayed for very long periods.
+    TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", this.window);
+    this.addMarker("AsyncTabSwitch:SpinnerShown");
+  }
 
-    this._callProgressListeners("onStatusChange",
-                                [aWebProgress, aRequest, aStatus, aMessage]);
+  spinnerHidden() {
+    this.assert(this.spinnerTab);
+    this.log("DEBUG: spinner time = " +
+      TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window));
+    TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
+    TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", this.window);
+    this.addMarker("AsyncTabSwitch:SpinnerHidden");
+    // we do not get a onPaint after displaying the spinner
+  }
 
-    this.mMessage = aMessage;
+  addMarker(marker) {
+    if (Services.profiler) {
+      Services.profiler.AddMarker(marker);
+    }
+  }
+
+  /*
+   * Debug related logging for switcher.
+   */
+  logging() {
+    if (this._useDumpForLogging)
+      return true;
+    if (this._logInit)
+      return this._shouldLog;
+    let result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming", false);
+    this._shouldLog = result;
+    this._logInit = true;
+    return this._shouldLog;
   }
 
-  onSecurityChange(aWebProgress, aRequest, aState) {
-    this._callProgressListeners("onSecurityChange",
-                                [aWebProgress, aRequest, aState]);
+  tinfo(tab) {
+    if (tab) {
+      return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
+    }
+    return "null";
+  }
+
+  log(s) {
+    if (!this.logging())
+      return;
+    if (this._useDumpForLogging) {
+      dump(s + "\n");
+    } else {
+      Services.console.logStringMessage(s);
+    }
   }
 
-  onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
-    return this._callProgressListeners("onRefreshAttempted",
-                                       [aWebProgress, aURI, aDelay, aSameURI]);
-  }
+  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);
 
-  QueryInterface(aIID) {
-    if (aIID.equals(Ci.nsIWebProgressListener) ||
-        aIID.equals(Ci.nsIWebProgressListener2) ||
-        aIID.equals(Ci.nsISupportsWeakReference) ||
-        aIID.equals(Ci.nsISupports))
-      return this;
-    throw Cr.NS_NOINTERFACE;
+      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");
+    } else {
+      Services.console.logStringMessage(accum);
+    }
   }
 }
 
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -41,16 +41,19 @@ with Files("test/unit/test_LaterRun.js")
     BUG_COMPONENT = ("Firefox", "Tours")
 
 with Files("test/unit/test_SitePermissions.js"):
     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
 
 with Files("AboutNewTab.jsm"):
     BUG_COMPONENT = ("Firefox", "New Tab Page")
 
+with Files('AsyncTabSwitcher.jsm'):
+    BUG_COMPONENT = ('Firefox', 'Tabbed Browser')
+
 with Files("AttributionCode.jsm"):
     BUG_COMPONENT = ("Toolkit", "Telemetry")
 
 with Files("*Telemetry.jsm"):
     BUG_COMPONENT = ("Toolkit", "Telemetry")
 
 with Files("ContentCrashHandlers.jsm"):
     BUG_COMPONENT = ("Toolkit", "Crash Reporting")
@@ -123,16 +126,17 @@ BROWSER_CHROME_MANIFESTS += [
     'test/browser/browser.ini',
     'test/browser/formValidation/browser.ini',
 ]
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 EXTRA_JS_MODULES += [
     'AboutHome.jsm',
     'AboutNewTab.jsm',
+    'AsyncTabSwitcher.jsm',
     'AttributionCode.jsm',
     'BrowserErrorReporter.jsm',
     'BrowserUITelemetry.jsm',
     'BrowserUsageTelemetry.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.jsm',
     'ContentMetaHandler.jsm',