Bug 1132072 - Tab switch refactoring (r=mconley)
authorBill McCloskey <bill.mccloskey@gmail.com>
Thu, 26 Feb 2015 22:34:03 -0800
changeset 266427 564a9e11d9ec89b0d9a6e2ea7e93cd531c9a468d
parent 266426 a96b3ce0784caa3ea35f57124d137b3b632bb686
child 266428 8126fbd73f26abf3bce8d5b5dd8d2507544b4f57
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1132072
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1132072 - Tab switch refactoring (r=mconley)
browser/base/content/tabbrowser.xml
browser/base/content/test/general/browser_selectTabAtIndex.js
browser/base/content/test/general/browser_tabfocus.js
docshell/test/browser/frame-head.js
docshell/test/browser/head.js
dom/base/nsFrameLoader.cpp
testing/mochitest/browser-test.js
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1198,28 +1198,36 @@
                   findBar._findField.getAttribute("focused") == "true");
               }
 
               // If focus is in the tab bar, retain it there.
               if (document.activeElement == oldTab) {
                 // We need to explicitly focus the new tab, because
                 // tabbox.xml does this only in some cases.
                 this.mCurrentTab.focus();
-              } else if (gMultiProcessBrowser) {
+              } else if (gMultiProcessBrowser && document.activeElement !== newBrowser) {
                 // Clear focus so that _adjustFocusAfterTabSwitch can detect if
                 // some element has been focused and respect that.
                 document.activeElement.blur();
               }
 
               if (!gMultiProcessBrowser)
                 this._adjustFocusAfterTabSwitch(this.mCurrentTab);
             }
 
             this.tabContainer._setPositionalAttributes();
 
+            if (!gMultiProcessBrowser) {
+              let event = new CustomEvent("TabSwitchDone", {
+                bubbles: true,
+                cancelable: true
+              });
+              this.dispatchEvent(event);
+            }
+
             if (!aForceUpdate)
               TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
           ]]>
         </body>
       </method>
 
       <method name="_adjustFocusAfterTabSwitch">
         <parameter name="newTab"/>
@@ -2861,16 +2869,521 @@
         <parameter name="aTab"/><!-- can be from a different window as well -->
         <body>
           <![CDATA[
             return SessionStore.duplicateTab(window, aTab);
           ]]>
         </body>
       </method>
 
+      <!--
+        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, we do the following:
+
+        1. 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.
+
+        2. Suppose that the user switches from tab A to B and then
+        back to A. Suppose that we ask for tab A's layers to be
+        unloaded via message M1 after the first switch and then
+        request them again via message M2 once the second switch
+        happens. Both loading and unloading of layers happens
+        asynchronously, and this can cause problems. It's possible
+        that the content process publishes one last layer tree before
+        M1 is received. The parent process doesn't know that this
+        layer tree was published before M1 and not after M2, so it
+        will display the tab. However, once M1 arrives, the content
+        process will destroy the layer tree for A and now we will
+        display black for it.
+
+        To counter this problem, we keep tab A in a separate
+        "unloading" state until the layer tree is actually dropped in
+        the compositor thread. While the tab is in the "unloading"
+        state, we're not allowed to request layers for it. Once the
+        layers are dropped in the compositor, an event will fire and
+        we will transition the tab to the "unloaded" state. Then we
+        are free to request the tab's layers again.
+      -->
+      <field name="_switcher">null</field>
+      <method name="_getSwitcher">
+        <body><![CDATA[
+          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: 300 /* 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.
+            originalTab: this.selectedTab,  // Tab that we started on.
+
+            tabbrowser: this,  // Reference to gBrowser.
+            loadTimer: null,   // TAB_SWITCH_TIMEOUT timer.
+            unloadTimer: null, // UNLOAD_DELAY timer.
+
+            // Map from tabs to STATE_* (below).
+            tabState: new Map(),
+
+            // 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]),
+
+            STATE_UNLOADED: 0,
+            STATE_LOADING: 1,
+            STATE_LOADED: 2,
+            STATE_UNLOADING: 3,
+
+            logging: false,
+
+            getTabState: function(tab) {
+              let state = this.tabState.get(tab);
+              if (state === undefined) {
+                return this.STATE_UNLOADED;
+              }
+              return state;
+            },
+
+            setTabState: function(tab, state) {
+              if (state == this.STATE_UNLOADED) {
+                this.tabState.delete(tab);
+              } else {
+                this.tabState.set(tab, state);
+              }
+
+              let browser = tab.linkedBrowser;
+              let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+              if (state == this.STATE_LOADING) {
+                // Ask for a MozLayerTreeReady event.
+                fl.requestNotifyLayerTreeReady();
+                browser.docShellIsActive = true;
+              } else if (state == this.STATE_UNLOADING) {
+                // Ask for MozLayerTreeCleared event.
+                fl.requestNotifyLayerTreeCleared();
+                browser.docShellIsActive = false;
+              }
+            },
+
+            init: function() {
+              this.log("START");
+
+              window.addEventListener("MozAfterPaint", this);
+              window.addEventListener("MozLayerTreeReady", this);
+              window.addEventListener("MozLayerTreeCleared", this);
+              window.addEventListener("TabRemotenessChange", this);
+              this.setTabState(this.requestedTab, this.STATE_LOADED);
+            },
+
+            destroy: function() {
+              clearTimeout(this.unloadTimer);
+              clearTimeout(this.loadTimer);
+
+              window.removeEventListener("MozAfterPaint", this);
+              window.removeEventListener("MozLayerTreeReady", this);
+              window.removeEventListener("MozLayerTreeCleared", this);
+              window.removeEventListener("TabRemotenessChange", this);
+
+              this.tabbrowser._switcher = null;
+            },
+
+            finish: function() {
+              this.log("FINISH");
+
+              this.assert(this.tabbrowser._switcher);
+              this.assert(this.tabbrowser._switcher === this);
+              this.assert(!this.spinnerTab);
+              this.assert(!this.loadTimer);
+              this.assert(!this.loadingTab);
+              this.assert(this.lastVisibleTab === this.requestedTab);
+              this.assert(this.getTabState(this.requestedTab) == this.STATE_LOADED);
+
+              this.destroy();
+
+              let toBrowser = this.requestedTab.linkedBrowser;
+              toBrowser.setAttribute("type", "content-primary");
+
+              this.tabbrowser._adjustFocusAfterTabSwitch(this.requestedTab);
+
+              let fromBrowser = this.originalTab.linkedBrowser;
+              // It's possible that the tab we're switching from closed
+              // before we were able to finalize, in which case, fromBrowser
+              // doesn't exist.
+              if (fromBrowser) {
+                fromBrowser.setAttribute("type", "content-targetable");
+              }
+
+              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: function() {
+              // Figure out which tab we actually want visible right now.
+              let showTab = null;
+              if (this.getTabState(this.requestedTab) != this.STATE_LOADED &&
+                  this.lastVisibleTab && this.loadTimer) {
+                // 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.
+                showTab = this.requestedTab;
+              }
+
+              // Show or hide the spinner as needed.
+              let needSpinner = this.getTabState(showTab) != this.STATE_LOADED;
+              if (!needSpinner && this.spinnerTab) {
+                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");
+                }
+                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.visibleTab = showTab;
+
+                this.maybeVisibleTabs.add(showTab);
+
+                let tabs = this.tabbrowser.mTabBox.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) {
+                    this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
+                  }
+                }
+              }
+
+              this.lastVisibleTab = this.visibleTab;
+            },
+
+            assert: function(cond) {
+              if (!cond) {
+                dump("Assertion failure\n" + Error().stack);
+                throw new Error("Assertion failure");
+              }
+            },
+
+            // We've decided to try to load requestedTab.
+            loadRequestedTab: function() {
+              this.assert(!this.loadTimer);
+
+              // 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.setTabState(this.requestedTab, this.STATE_LOADING);
+              this.loadTimer = setTimeout(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
+            },
+
+            // This function runs before every event. It fixes up the state
+            // to account for closed tabs.
+            preActions: function() {
+              this.assert(this.tabbrowser._switcher);
+              this.assert(this.tabbrowser._switcher === this);
+
+              for (let [tab, state] of this.tabState) {
+                if (!tab.linkedBrowser) {
+                  this.tabState.delete(tab);
+                }
+              }
+
+              if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
+                this.lastVisibleTab = null;
+              }
+              if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
+                this.spinnerTab = null;
+              }
+              if (this.loadingTab && !this.loadingTab.linkedBrowser) {
+                this.loadingTab = null;
+                clearTimeout(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: function() {
+              // 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 not loading anything, try loading the requested tab.
+              if (!this.loadTimer && this.getTabState(this.requestedTab) == this.STATE_UNLOADED) {
+                this.loadRequestedTab();
+              }
+
+              // See how many tabs still have work to do.
+              let numPending = 0;
+              for (let [tab, state] of this.tabState) {
+                if (state == this.STATE_LOADED && tab !== this.requestedTab) {
+                  numPending++;
+                }
+                if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
+                  numPending++;
+                }
+              }
+
+              this.updateDisplay();
+
+              // It's possible for updateDisplay to trigger one of our own event
+              // handlers, which might cause finish() to already have been called.
+              // Check for that before calling finish() again.
+              if (!this.tabbrowser._switcher) {
+                return;
+              }
+
+              if (numPending == 0) {
+                this.finish();
+              }
+
+              this.logState("done");
+            },
+
+            // Fires when we're ready to unload unused tabs.
+            onUnloadTimeout: function() {
+              this.logState("onUnloadTimeout");
+              this.preActions();
+
+              let numPending = 0;
+
+              // Unload any tabs that can be unloaded.
+              for (let [tab, state] of this.tabState) {
+                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 = setTimeout(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
+              }
+
+              this.postActions();
+            },
+
+            // Fires when an ongoing load has taken too long.
+            onLoadTimeout: function() {
+              this.logState("onLoadTimeout");
+              this.preActions();
+              this.loadTimer = null;
+              this.loadingTab = null;
+              this.postActions();
+            },
+
+            // Fires when the layers become available for a tab.
+            onLayersReady: function(browser) {
+              this.logState("onLayersReady");
+
+              let tab = this.tabbrowser.getTabForBrowser(browser);
+              this.setTabState(tab, this.STATE_LOADED);
+
+              if (this.loadingTab === tab) {
+                clearTimeout(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: function() {
+              this.maybeVisibleTabs.clear();
+            },
+
+            // Called when we're done clearing the layers for a tab.
+            onLayersCleared: function(browser) {
+              this.logState("onLayersCleared");
+
+              let tab = this.tabbrowser.getTabForBrowser(browser);
+              if (tab) {
+                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: function(tab) {
+              this.logState("onRemotenessChange");
+              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);
+                }
+              }
+            },
+
+            // Called when the user asks to switch to a given tab.
+            requestTab: function(tab) {
+              if (tab === this.requestedTab) {
+                return;
+              }
+
+              this.logState("requestTab " + this.tinfo(tab));
+
+              this.requestedTab = tab;
+
+              this.preActions();
+
+              clearTimeout(this.unloadTimer);
+              this.unloadTimer = setTimeout(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
+
+              this.postActions();
+            },
+
+            handleEvent: function(event) {
+              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);
+              }
+
+              this.postActions();
+            },
+
+            tinfo: function(tab) {
+              if (tab) {
+                return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
+              } else {
+                return "null";
+              }
+            },
+
+            log: function(s) {
+              if (!this.logging)
+                return;
+              dump(s + "\n");
+            },
+
+            logState: function(prefix) {
+              if (!this.logging)
+                return;
+
+              dump(prefix + " ");
+              for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
+                let tab = this.tabbrowser.tabs[i];
+                let state = this.getTabState(tab);
+
+                dump(i + ":");
+                if (tab === this.lastVisibleTab) dump("V");
+                if (tab === this.loadingTab) dump("L");
+                if (tab === this.requestedTab) dump("R");
+                if (state == this.STATE_LOADED) dump("(+)");
+                if (state == this.STATE_LOADING) dump("(+?)");
+                if (state == this.STATE_UNLOADED) dump("(-)");
+                if (state == this.STATE_UNLOADING) dump("(-?)");
+                dump(" ");
+              }
+              dump("\n");
+            },
+          };
+          this._switcher = switcher;
+          switcher.init();
+          return switcher;
+        ]]></body>
+      </method>
+
       <!-- BEGIN FORWARDED BROWSER PROPERTIES.  IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
            MAKE SURE TO ADD IT HERE AS WELL. -->
       <property name="canGoBack"
                 onget="return this.mCurrentBrowser.canGoBack;"
                 readonly="true"/>
 
       <property name="canGoForward"
                 onget="return this.mCurrentBrowser.canGoForward;"
@@ -3375,16 +3888,20 @@
                               .getService(nsIEventListenerService);
           els.removeSystemEventListener(document, "keydown", this, false);
           els.removeSystemEventListener(document, "keypress", this, false);
           window.removeEventListener("sizemodechange", this, false);
 
           if (gMultiProcessBrowser) {
             messageManager.removeMessageListener("DOMTitleChanged", this);
             messageManager.removeMessageListener("contextmenu", this);
+
+            if (this._switcher) {
+              this._switcher.destroy();
+            }
           }
         ]]>
       </destructor>
 
       <!-- Deprecated stuff, implemented for backwards compatibility. -->
       <method name="enterTabbedMode">
         <body>
           Application.console.log("enterTabbedMode is an obsolete method and " +
@@ -3399,164 +3916,16 @@
         </body>
       </method>
       <method name="getStripVisibility">
         <body>
           return this.tabContainer.visible;
         </body>
       </method>
 
-      <method name="_showBusySpinnerRemoteBrowser">
-        <parameter name="aBrowser"/>
-        <body><![CDATA[
-          aBrowser.setAttribute("pendingpaint", "true");
-          if (this._contentWaitingCount <= 0) {
-            // We are not currently spinning
-            this.setAttribute("pendingpaint", "true");
-            this._contentWaitingCount = 1;
-          } else {
-            this._contentWaitingCount++;
-          }
-        ]]></body>
-      </method>
-
-      <method name="_hideBusySpinnerRemoteBrowser">
-        <parameter name="aBrowser"/>
-        <body><![CDATA[
-          aBrowser.removeAttribute("pendingpaint");
-          this._contentWaitingCount--;
-          if (this._contentWaitingCount <= 0) {
-            this.removeAttribute("pendingpaint");
-          }
-        ]]></body>
-      </method>
-
-      <method name="_prepareForTabSwitch">
-        <parameter name="toTab"/>
-        <parameter name="fromTab"/>
-        <body><![CDATA[
-          const kTabSwitchTimeout = 300;
-          let toBrowser = this.getBrowserForTab(toTab);
-          let fromBrowser = fromTab ? this.getBrowserForTab(fromTab)
-                                    : null;
-
-          // We only want to wait for the MozAfterRemotePaint event if
-          // the tab we're switching to is a remote tab, and if the tab
-          // we're switching to isn't the one we've already got. The latter
-          // case can occur when closing tabs before the currently selected
-          // one.
-          let shouldWait = toBrowser.getAttribute("remote") == "true" &&
-                           toBrowser != fromBrowser;
-
-          let switchPromise;
-
-          if (shouldWait) {
-            let timeoutId;
-            let panels = this.mPanelContainer;
-
-            // Both the timeout and MozAfterPaint promises use this same
-            // logic to determine whether they should carry out the tab
-            // switch, or reject it outright.
-            let attemptTabSwitch = (aResolve, aReject) => {
-              if (this.selectedBrowser == toBrowser) {
-                aResolve();
-              } else {
-                // We switched away or closed the browser before we timed
-                // out. We reject, which will cancel the tab switch.
-                aReject();
-              }
-            };
-
-            let timeoutPromise = new Promise((aResolve, aReject) => {
-              timeoutId = setTimeout(() => {
-                if (toBrowser.isRemoteBrowser) {
-                  // The browser's remoteness could have changed since we
-                  // setTimeout so only show the spinner if it's still remote.
-                  this._showBusySpinnerRemoteBrowser(toBrowser);
-                }
-                attemptTabSwitch(aResolve, aReject);
-              }, kTabSwitchTimeout);
-            });
-
-            let paintPromise = new Promise((aResolve, aReject) => {
-              let onRemotePaint = () => {
-                toBrowser.removeEventListener("MozAfterRemotePaint", onRemotePaint);
-                this._hideBusySpinnerRemoteBrowser(toBrowser);
-                clearTimeout(timeoutId);
-                attemptTabSwitch(aResolve, aReject);
-              };
-              toBrowser.addEventListener("MozAfterRemotePaint", onRemotePaint);
-              toBrowser.QueryInterface(Ci.nsIFrameLoaderOwner)
-                       .frameLoader
-                       .requestNotifyAfterRemotePaint();
-               // We need to activate the docShell on the tab we're switching
-               // to - otherwise, we won't initiate a remote paint request and
-               // therefore we won't get the MozAfterRemotePaint event that we're
-               // waiting for.
-               // Note that this happens, as we require, even if the timeout in the
-               // timeoutPromise triggers before the paintPromise even runs.
-               toBrowser.docShellIsActive = true;
-            });
-
-            switchPromise = Promise.race([paintPromise, timeoutPromise]);
-          } else {
-            // Activate the docShell on the tab we're switching to.
-            toBrowser.docShellIsActive = true;
-
-            // No need to wait - just resolve immediately to do the switch ASAP.
-            switchPromise = Promise.resolve();
-          }
-
-          return switchPromise;
-        ]]></body>
-      </method>
-
-      <method name="_deactivateContent">
-        <parameter name="tab"/>
-        <body><![CDATA[
-          // It's unlikely, yet possible, that while we were waiting
-          // to deactivate this tab, that something closed it and wiped
-          // out the browser. For example, during a tab switch, while waiting
-          // for the MozAfterRemotePaint event to fire, something closes the
-          // original tab that the user had selected. If that's the case, then
-          // there's nothing to deactivate.
-          let browser = this.getBrowserForTab(tab);
-          if (browser && this.selectedBrowser != browser) {
-            browser.docShellIsActive = false;
-          }
-        ]]></body>
-      </method>
-
-      <method name="_finalizeTabSwitch">
-        <parameter name="toTab"/>
-        <parameter name="fromTab"/>
-        <body><![CDATA[
-          this._adjustFocusAfterTabSwitch(toTab);
-          this._deactivateContent(fromTab);
-
-          let toBrowser = this.getBrowserForTab(toTab);
-          toBrowser.setAttribute("type", "content-primary");
-
-          let fromBrowser = this.getBrowserForTab(fromTab);
-          // It's possible that the tab we're switching from closed
-          // before we were able to finalize, in which case, fromBrowser
-          // doesn't exist.
-          if (fromBrowser) {
-            fromBrowser.setAttribute("type", "content-targetable");
-          }
-        ]]></body>
-      </method>
-
-      <method name="_cancelTabSwitch">
-        <parameter name="toTab"/>
-        <body><![CDATA[
-          this._deactivateContent(toTab);
-        ]]></body>
-      </method>
-
       <property name="mContextTab" readonly="true"
                 onget="return TabContextMenu.contextTab;"/>
       <property name="mPrefs" readonly="true"
                 onget="return Services.prefs;"/>
       <property name="mTabContainer" readonly="true"
                 onget="return this.tabContainer;"/>
       <property name="mTabs" readonly="true"
                 onget="return this.tabs;"/>
@@ -5509,47 +5878,27 @@
         <![CDATA[
           if (val < 0 || val >= this.childNodes.length)
             return val;
 
           let toTab = this.getRelatedElement(this.childNodes[val]);
           let fromTab = this._selectedPanel ? this.getRelatedElement(this._selectedPanel)
                                             : null;
 
-          let switchPromise = gBrowser._prepareForTabSwitch(toTab, fromTab);
+          gBrowser._getSwitcher().requestTab(toTab);
 
           var panel = this._selectedPanel;
           var newPanel = this.childNodes[val];
           this._selectedPanel = newPanel;
           if (this._selectedPanel != panel) {
             var event = document.createEvent("Events");
             event.initEvent("select", true, true);
             this.dispatchEvent(event);
 
             this._selectedIndex = val;
-
-            switchPromise.then(() => {
-              // If we cannot find the tabpanel that we were trying to switch to, then
-              // it must have been removed before our Promise could be resolved. In
-              // that case, we just cancel the tab switch.
-              var updatedTabIndex = Array.indexOf(this.childNodes, newPanel);
-              if (updatedTabIndex == -1) {
-                gBrowser._cancelTabSwitch(toTab);
-              } else {
-                this.setAttribute("selectedIndex", updatedTabIndex);
-                gBrowser._finalizeTabSwitch(toTab, fromTab);
-              }
-            }, () => {
-              // If the promise rejected, that means we don't want to actually
-              // flip the deck, so we cancel the tab switch.
-              // We need to nullcheck the method we're about to call because
-              // the binding might be dead at this point.
-              if (gBrowser._cancelTabSwitch)
-                gBrowser._cancelTabSwitch(toTab);
-            });
           }
 
           return val;
         ]]>
         </setter>
       </property>
     </implementation>
   </binding>
--- a/browser/base/content/test/general/browser_selectTabAtIndex.js
+++ b/browser/base/content/test/general/browser_selectTabAtIndex.js
@@ -1,14 +1,17 @@
 function test() {
   for (let i = 0; i < 9; i++)
     gBrowser.addTab();
 
   var isLinux = navigator.platform.indexOf("Linux") == 0;
   for (let i = 9; i >= 1; i--) {
+    // Make sure the keystroke goes to chrome.
+    document.activeElement.blur();
+
     EventUtils.synthesizeKey(i.toString(), { altKey: isLinux, accelKey: !isLinux });
 
     is(gBrowser.tabContainer.selectedIndex, (i == 9 ? gBrowser.tabs.length : i) - 1,
        (isLinux ? "Alt" : "Accel") + "+" + i + " selects expected tab");
   }
 
   gBrowser.selectTabAtIndex(-3);
   is(gBrowser.tabContainer.selectedIndex, gBrowser.tabs.length - 3,
--- a/browser/base/content/test/general/browser_tabfocus.js
+++ b/browser/base/content/test/general/browser_tabfocus.js
@@ -211,37 +211,36 @@ add_task(function*() {
   // When focus is in the tab bar, it should be retained there
   yield expectFocusShift(function () gBrowser.selectedTab.focus(),
                          "main-window", "tab2", true,
                          "focusing tab element");
   yield expectFocusShift(function () gBrowser.selectedTab = tab1,
                          "main-window", "tab1", true,
                          "tab change when selected tab element was focused");
 
-  let paintWaiter;
+  let switchWaiter;
   if (gMultiProcessBrowser) {
-    paintWaiter = new Promise((resolve, reject) => {
-      browser2.addEventListener("MozAfterRemotePaint", function paintListener() {
-        browser2.removeEventListener("MozAfterRemotePaint", paintListener, false);
+    switchWaiter = new Promise((resolve, reject) => {
+      gBrowser.addEventListener("TabSwitchDone", function listener() {
+        gBrowser.removeEventListener("TabSwitchDone", listener);
         executeSoon(resolve);
-      }, false);
-      browser2.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.requestNotifyAfterRemotePaint();
+      });
     });
   }
 
   yield expectFocusShift(function () gBrowser.selectedTab = tab2,
                          "main-window", "tab2", true,
                          "another tab change when selected tab element was focused");
 
   // When this a remote browser, wait for the paint on the second browser so that
   // any post tab-switching stuff has time to complete before blurring the tab.
   // Otherwise, the _adjustFocusAfterTabSwitch in tabbrowser gets confused and
   // isn't sure what tab is really focused.
   if (gMultiProcessBrowser) {
-    yield paintWaiter;
+    yield switchWaiter;
   }
 
   yield expectFocusShift(function () gBrowser.selectedTab.blur(),
                          "main-window", null, true,
                          "blurring tab element");
 
   // focusing the url field should switch active focus away from the browser but
   // not clear what would be the focus in the browser
--- a/docshell/test/browser/frame-head.js
+++ b/docshell/test/browser/frame-head.js
@@ -3,16 +3,18 @@
 
 // Functions that are automatically loaded as frame scripts for
 // timeline tests.
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 let { Promise } = Cu.import('resource://gre/modules/Promise.jsm', {});
 
+Cu.import("resource://gre/modules/Timer.jsm");
+
 // Functions that look like mochitest functions but forward to the
 // browser process.
 
 this.ok = function(value, message) {
   sendAsyncMessage("browser:test:ok", {
     value: !!value,
     message: message});
 }
@@ -76,30 +78,32 @@ this.timelineContentTest = function(test
     info("Stop recording");
     docShell.recordProfileTimelineMarkers = false;
     finish();
   });
 }
 
 function timelineWaitForMarkers(docshell, searchFor) {
   if (typeof(searchFor) == "string") {
+    let searchForString = searchFor;
     let f = function (markers) {
-      return markers.some(m => m.name == searchFor);
+      return markers.some(m => m.name == searchForString);
     };
     searchFor = f;
   }
 
   return new Promise(function(resolve, reject) {
     let waitIterationCount = 0;
     let maxWaitIterationCount = 10; // Wait for 2sec maximum
     let markers = [];
 
-    let interval = content.setInterval(() => {
+    setTimeout(function timeoutHandler() {
       let newMarkers = docshell.popProfileTimelineMarkers();
       markers = [...markers, ...newMarkers];
       if (searchFor(markers) || waitIterationCount > maxWaitIterationCount) {
-        content.clearInterval(interval);
         resolve(markers);
+      } else {
+        setTimeout(timeoutHandler, 200);
+        waitIterationCount++;
       }
-      waitIterationCount++;
     }, 200);
   });
 }
--- a/docshell/test/browser/head.js
+++ b/docshell/test/browser/head.js
@@ -34,20 +34,29 @@ function makeTimelineTest(frameScriptNam
       finish();
       gBrowser.removeCurrentTab();
     });
   });
 }
 
 /* Open a URL for a timeline test.  */
 function timelineTestOpenUrl(url) {
-  return new Promise(function(resolve, reject) {
-    window.focus();
+  window.focus();
 
+  let tabSwitchPromise = new Promise((resolve, reject) => {
+    window.gBrowser.addEventListener("TabSwitchDone", function listener() {
+      window.gBrowser.removeEventListener("TabSwitchDone", listener);
+      resolve();
+    });
+  });
+
+  let loadPromise = new Promise(function(resolve, reject) {
     let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
     let linkedBrowser = tab.linkedBrowser;
 
     linkedBrowser.addEventListener("load", function onload() {
       linkedBrowser.removeEventListener("load", onload, true);
       resolve(tab);
     }, true);
   });
+
+  return Promise.all([tabSwitchPromise, loadPromise]).then(([_, tab]) => tab);
 }
--- a/dom/base/nsFrameLoader.cpp
+++ b/dom/base/nsFrameLoader.cpp
@@ -2774,32 +2774,40 @@ nsFrameLoader::RequestNotifyAfterRemoteP
 
 NS_IMETHODIMP
 nsFrameLoader::RequestNotifyLayerTreeReady()
 {
   if (mRemoteBrowser) {
     return mRemoteBrowser->RequestNotifyLayerTreeReady() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
   }
 
+  if (!mOwnerContent) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
   nsRefPtr<AsyncEventDispatcher> event =
     new AsyncEventDispatcher(mOwnerContent,
                              NS_LITERAL_STRING("MozLayerTreeReady"),
                              true, false);
   event->PostDOMEvent();
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsFrameLoader::RequestNotifyLayerTreeCleared()
 {
   if (mRemoteBrowser) {
     return mRemoteBrowser->RequestNotifyLayerTreeCleared() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
   }
 
+  if (!mOwnerContent) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
   nsRefPtr<AsyncEventDispatcher> event =
     new AsyncEventDispatcher(mOwnerContent,
                              NS_LITERAL_STRING("MozLayerTreeCleared"),
                              true, false);
   event->PostDOMEvent();
 
   return NS_OK;
 }
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -308,30 +308,29 @@ Tester.prototype = {
       }
     }
 
     // Make sure the window is raised before each test.
     this.SimpleTest.waitForFocus(aCallback);
   },
 
   finish: function Tester_finish(aSkipSummary) {
-    TabDestroyObserver.destroy();
-
     this.Promise.Debugging.flushUncaughtErrors();
 
     var passCount = this.tests.reduce(function(a, f) a + f.passCount, 0);
     var failCount = this.tests.reduce(function(a, f) a + f.failCount, 0);
     var todoCount = this.tests.reduce(function(a, f) a + f.todoCount, 0);
 
     if (this.repeat > 0) {
       --this.repeat;
       this.currentTestIndex = -1;
       this.nextTest();
     }
     else{
+      TabDestroyObserver.destroy();
       Services.console.unregisterListener(this);
       Services.obs.removeObserver(this, "chrome-document-global-created");
       Services.obs.removeObserver(this, "content-document-global-created");
       this.Promise.Debugging.clearUncaughtErrorObservers();
       this._treatUncaughtRejectionsAsFailures = false;
 
       // In the main process, we print the ShutdownLeaksCollector message here.
       let pid = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processID;