Merge inbound to mozilla-central. a=merge
authorCosmin Sabou <csabou@mozilla.com>
Tue, 06 Mar 2018 06:27:48 +0200
changeset 461709 19838b896cd704a4cbf50d7e57b8d7dd8cafcbfd
parent 461708 7c01c3875cae3d9b8a3fab22c4b07efe20f6d48f (current diff)
parent 461646 36c84344de23aa92f96c0d40076d5262aab32ad2 (diff)
child 461710 490f87831c110acaa3eb966679ad94a1eaf51dd9
child 461714 709eae4e54ffa3f3518745516dd5d27a05255af2
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone60.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
Merge inbound to mozilla-central. a=merge
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
browser/modules/AsyncTabSwitcher.jsm
python/mozbuild/mozbuild/backend/templates/android_eclipse_empty_resource_directory/.not_an_android_resource
services/sync/tests/unit/test_errorhandler_eol.js
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -414,30 +414,30 @@ dependencies = [
  "syn 0.12.12 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "cubeb"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cubeb-core 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cubeb-core 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "cubeb-backend"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cubeb-core 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cubeb-core 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "cubeb-core"
-version = "0.4.3"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "cubeb-sys 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "cubeb-pulse"
@@ -2259,17 +2259,17 @@ dependencies = [
 "checksum crossbeam-epoch 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "59796cc6cbbdc6bb319161349db0c3250ec73ec7fcb763a51065ec4e2e158552"
 "checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9"
 "checksum cssparser 0.23.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8a807ac3ab7a217829c2a3b65732b926b2befe6a35f33b4bf8b503692430f223"
 "checksum cssparser-macros 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "079adec4af52bb5275eadd004292028c79eb3c5f5b4ee8086a36d4197032f6df"
 "checksum cstr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b6557bdb1dc9647eae1cf7f5601b14cd45fc3c7ccf2df618387416fe542da6ea"
 "checksum cstr-macros 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f9f316203d1ea36f4f18316822806f6999aa3dc5ed1adf51e35b77e3b3933d78"
 "checksum cubeb 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ed237804b9799d1c29089e6cab3f4b7160186179981a61865feff13d55a902f8"
 "checksum cubeb-backend 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4f6d8f189a1cf9cce9aec45eb0aeb1d221514d788b89a1cd6bc2b76110ee4d81"
-"checksum cubeb-core 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a6558e6b99511df1b4e7ce219b26527b0cb0f6f56df2fd5877aa9473b7c7eafb"
+"checksum cubeb-core 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7dcedc8ffadc0481f1dfbdeb315e7d7ae9afe6a70815d22efcca8e0bd2ba89e0"
 "checksum cubeb-sys 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fab28c0e152330f74bcbec1572c374397458957d5ad50605879352ec562f41aa"
 "checksum darling 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d3effd06d4057f275cb7858889f4952920bab78dd8ff0f6e7dfe0c8d2e67ed89"
 "checksum darling_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "167dd3e235c2f1da16a635c282630452cdf49191eb05711de1bcd1d3d5068c00"
 "checksum darling_macro 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c53edaba455f6073a10c27c72440860eb3f60444f8c8660a391032eeae744d82"
 "checksum debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9a032eac705ca39214d169f83e3d3da290af06d8d1d344d1baad2fd002dca4b3"
 "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
 "checksum dtoa-short 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "068d4026697c1a18f0b0bb8cfcad1b0c151b90d8edb9bf4c235ad68128920d1d"
 "checksum dwrote 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a207eb7b40e25d1d28dc679f451d321fb6954b73ceaa47986702575865469461"
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,16 +17,20 @@ members = [
 exclude = [
   # Exclude third-party code vendored into mozilla-central.
   "gfx/webrender",
   "gfx/webrender_api",
   "gfx/webrender_bindings",
   "servo",
   "third_party/rust",
 
+  # Excluded because this is a standalone tool for developers and not intended
+  # to be built as part of mozilla-central and is not shipped to users.
+  "gfx/wrench",
+
   # Excluded because these crates have their own Cargo workspaces so they can't
   # be included in the top-level one.
   "media/audioipc",
   "media/cubeb-rs",
 
   # Excluded because they are used only as dependencies, not top-level targets,
   # so we don't need to vendor their dev-dependencies.
   "dom/webauthn/u2f-hid-rs",
--- a/accessible/base/XULMap.h
+++ b/accessible/base/XULMap.h
@@ -1,15 +1,16 @@
 /* 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/. */
 
 XULMAP_TYPE(browser, OuterDocAccessible)
 XULMAP_TYPE(button, XULButtonAccessible)
 XULMAP_TYPE(checkbox, XULCheckboxAccessible)
+XULMAP_TYPE(description, XULLabelAccessible)
 XULMAP_TYPE(dropMarker, XULDropmarkerAccessible)
 XULMAP_TYPE(editor, OuterDocAccessible)
 XULMAP_TYPE(findbar, XULToolbarAccessible)
 XULMAP_TYPE(groupbox, XULGroupboxAccessible)
 XULMAP_TYPE(iframe, OuterDocAccessible)
 XULMAP_TYPE(listbox, XULListboxAccessibleWrap)
 XULMAP_TYPE(listhead, XULColumAccessible)
 XULMAP_TYPE(listheader, XULColumnItemAccessible)
@@ -37,26 +38,55 @@ XULMAP_TYPE(toolbarspring, XULToolbarSep
 XULMAP_TYPE(treecol, XULColumnItemAccessible)
 XULMAP_TYPE(treecolpicker, XULButtonAccessible)
 XULMAP_TYPE(treecols, XULTreeColumAccessible)
 XULMAP_TYPE(toolbar, XULToolbarAccessible)
 XULMAP_TYPE(toolbarbutton, XULToolbarButtonAccessible)
 XULMAP_TYPE(tooltip, XULTooltipAccessible)
 
 XULMAP(
+  colorpicker,
+  [](nsIContent* aContent, Accessible* aContext) -> Accessible* {
+    if (aContent->IsElement() &&
+        aContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                                           nsGkAtoms::button, eIgnoreCase)) {
+      return new XULColorPickerAccessible(aContent, aContext->Document());
+    }
+    return nullptr;
+  }
+)
+
+XULMAP(
+  label,
+  [](nsIContent* aContent, Accessible* aContext) -> Accessible* {
+    if (aContent->IsElement() &&
+        aContent->AsElement()->ClassList()->Contains(NS_LITERAL_STRING("text-link"))) {
+      return new XULLinkAccessible(aContent, aContext->Document());
+    }
+    return new XULLabelAccessible(aContent, aContext->Document());
+  }
+)
+
+XULMAP(
   image,
   [](nsIContent* aContent, Accessible* aContext) -> Accessible* {
-    if (aContent->IsElement() &&
-        aContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::onclick)) {
+    if (!aContent->IsElement()) {
+      return nullptr;
+    }
+
+    if (aContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::onclick)) {
       return new XULToolbarButtonAccessible(aContent, aContext->Document());
     }
 
+    if (aContent->AsElement()->ClassList()->Contains(NS_LITERAL_STRING("colorpickertile"))) {
+      return new XULColorPickerTileAccessible(aContent, aContext->Document());
+    }
+
     // Don't include nameless images in accessible tree.
-    if (!aContent->IsElement() ||
-        !aContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext)) {
+    if (!aContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext)) {
       return nullptr;
     }
 
     return new ImageAccessibleWrap(aContent, aContext->Document());
   }
 )
 
 XULMAP(
--- a/accessible/base/nsAccessibilityService.cpp
+++ b/accessible/base/nsAccessibilityService.cpp
@@ -1240,22 +1240,16 @@ nsAccessibilityService::CreateAccessible
     // Prefer to use XUL to decide if and what kind of accessible to create.
     const XULMarkupMapInfo* xulMap =
       mXULMarkupMap.Get(content->NodeInfo()->NameAtom());
     if (xulMap && xulMap->new_func) {
       newAcc = xulMap->new_func(content, aContext);
     }
 #endif
 
-    // XBL bindings may use @role attribute to point the accessible type
-    // they belong to.
-    if (!newAcc) {
-      newAcc = CreateAccessibleByType(content, document);
-    }
-
     // Any XUL box can be used as tabpanel, make sure we create a proper
     // accessible for it.
     if (!newAcc && aContext->IsXULTabpanels() &&
         content->GetParent() == aContext->GetContent()) {
       LayoutFrameType frameType = frame->Type();
       if (frameType == LayoutFrameType::Box ||
           frameType == LayoutFrameType::Scroll) {
         newAcc = new XULTabpanelAccessible(content, document);
@@ -1462,46 +1456,16 @@ nsAccessibilityService::Shutdown()
 
   if (observerService) {
     static const char16_t kShutdownIndicator[] = { '0', 0 };
     observerService->NotifyObservers(nullptr, "a11y-init-or-shutdown", kShutdownIndicator);
   }
 }
 
 already_AddRefed<Accessible>
-nsAccessibilityService::CreateAccessibleByType(nsIContent* aContent,
-                                               DocAccessible* aDoc)
-{
-  nsAutoString role;
-  nsCoreUtils::XBLBindingRole(aContent, role);
-  if (role.IsEmpty())
-    return nullptr;
-
-  RefPtr<Accessible> accessible;
-#ifdef MOZ_XUL
-  // XUL controls
-  if (role.EqualsLiteral("xul:colorpicker")) {
-    accessible = new XULColorPickerAccessible(aContent, aDoc);
-
-  } else if (role.EqualsLiteral("xul:colorpickertile")) {
-    accessible = new XULColorPickerTileAccessible(aContent, aDoc);
-
-  } else if (role.EqualsLiteral("xul:link")) {
-    accessible = new XULLinkAccessible(aContent, aDoc);
-
-  } else if (role.EqualsLiteral("xul:text")) {
-    accessible = new XULLabelAccessible(aContent, aDoc);
-
-  }
-#endif // MOZ_XUL
-
-  return accessible.forget();
-}
-
-already_AddRefed<Accessible>
 nsAccessibilityService::CreateAccessibleByFrameType(nsIFrame* aFrame,
                                                     nsIContent* aContent,
                                                     Accessible* aContext)
 {
   DocAccessible* document = aContext->Document();
 
   RefPtr<Accessible> newAcc;
   switch (aFrame->AccessibleType()) {
--- a/accessible/base/nsAccessibilityService.h
+++ b/accessible/base/nsAccessibilityService.h
@@ -287,22 +287,16 @@ private:
   bool Init();
 
   /**
    * Shutdowns accessibility service.
    */
   void Shutdown();
 
   /**
-   * Create accessible for the element having XBL bindings.
-   */
-  already_AddRefed<Accessible>
-    CreateAccessibleByType(nsIContent* aContent, DocAccessible* aDoc);
-
-  /**
    * Create an accessible whose type depends on the given frame.
    */
   already_AddRefed<Accessible>
     CreateAccessibleByFrameType(nsIFrame* aFrame, nsIContent* aContent,
                                 Accessible* aContext);
 
   /**
    * Notify observers about change of the accessibility service's consumers.
--- a/accessible/base/nsCoreUtils.cpp
+++ b/accessible/base/nsCoreUtils.cpp
@@ -663,20 +663,8 @@ nsCoreUtils::AccEventObserversExist()
 void
 nsCoreUtils::DispatchAccEvent(RefPtr<nsIAccessibleEvent> event)
 {
   nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
   NS_ENSURE_TRUE_VOID(obsService);
 
   obsService->NotifyObservers(event, NS_ACCESSIBLE_EVENT_TOPIC, nullptr);
 }
-
-void
-nsCoreUtils::XBLBindingRole(const nsIContent* aEl, nsAString& aRole)
-{
-  for (const nsXBLBinding* binding = aEl->GetXBLBinding(); binding;
-       binding = binding->GetBaseBinding()) {
-    Element* bindingElm = binding->PrototypeBinding()->GetBindingElement();
-    bindingElm->GetAttr(kNameSpaceID_None, nsGkAtoms::role, aRole);
-    if (!aRole.IsEmpty())
-      break;
-  }
-}
--- a/accessible/base/nsCoreUtils.h
+++ b/accessible/base/nsCoreUtils.h
@@ -316,16 +316,11 @@ public:
    * Return true if there are any observers of accessible events.
    */
   static bool AccEventObserversExist();
 
   /**
    * Notify accessible event observers of an event.
    */
   static void DispatchAccEvent(RefPtr<nsIAccessibleEvent> aEvent);
-
-  /**
-   * Return a role attribute on XBL bindings of the element.
-   */
-  static void XBLBindingRole(const nsIContent* aEl, nsAString& aRole);
 };
 
 #endif
--- a/accessible/generic/HyperTextAccessible.cpp
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -1149,16 +1149,23 @@ HyperTextAccessible::LandmarkRole() cons
 
   // Only return xml-roles "region" if the section has an accessible name.
   if (mContent->IsHTMLElement(nsGkAtoms::section)) {
     nsAutoString name;
     const_cast<HyperTextAccessible*>(this)->Name(name);
     return name.IsEmpty() ? nullptr : nsGkAtoms::region;
   }
 
+  // Only return xml-roles "form" if the form has an accessible name.
+  if (mContent->IsHTMLElement(nsGkAtoms::form)) {
+    nsAutoString name;
+    const_cast<HyperTextAccessible*>(this)->Name(name);
+    return name.IsEmpty() ? nullptr : nsGkAtoms::form;
+  }
+
   return nullptr;
 }
 
 int32_t
 HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType)
 {
   nsIFrame* hyperFrame = GetFrame();
   if (!hyperFrame)
--- a/accessible/tests/mochitest/elm/test_HTMLSpec.html
+++ b/accessible/tests/mochitest/elm/test_HTMLSpec.html
@@ -538,20 +538,29 @@
       testElm("footer_in_fieldset", obj);
       testElm("footer_in_figure", obj);
       testElm("footer_in_td", obj);
 
       // ////////////////////////////////////////////////////////////////////////
       // HTML:form
 
       obj = {
-        role: ROLE_FORM
+        role: ROLE_FORM,
+        absentAttributes: { "xml-roles": "form" }
       };
       testElm("form", obj);
 
+      // HTML:form with an accessible name
+
+      obj = {
+        role: ROLE_FORM,
+        attributes: { "xml-roles": "form" }
+      };
+      testElm("named_form", obj);
+
       // ////////////////////////////////////////////////////////////////////////
       // // HTML:frameset, HTML:frame and HTML:iframe
 
       obj = {
         INTERNAL_FRAME: [ { // HTML:iframe
           DOCUMENT: [ {
             INTERNAL_FRAME: [ { // HTML:frame
               DOCUMENT: [ { role: ROLE_TEXT_LEAF} ]
@@ -1544,16 +1553,17 @@
   <figure>
     <footer id="footer_in_figure">Some copyright info</footer>
   </figure>
   <table><tr><td>
     <footer id="footer_in_td">Some copyright info</footer>
   </td></tr></table>
 
   <form id="form"></form>
+  <form id="named_form" aria-label="New form"></form>
 
   <iframe id="frameset_container"
           src="data:text/html,<html><frameset><frame src='data:text/html,hi'></frame></frameset></html>">
   </iframe>
 
   <h1 id="h1">heading1</h1>
   <h2 id="h2">heading2</h2>
   <h3 id="h3">heading3</h3>
--- a/accessible/xul/XULFormControlAccessible.cpp
+++ b/accessible/xul/XULFormControlAccessible.cpp
@@ -3,17 +3,16 @@
  * 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/. */
 
 #include "XULFormControlAccessible.h"
 
 #include "Accessible-inl.h"
 #include "HTMLFormControlAccessible.h"
 #include "nsAccUtils.h"
-#include "nsCoreUtils.h"
 #include "DocAccessible.h"
 #include "nsIAccessibleRelation.h"
 #include "Relation.h"
 #include "Role.h"
 #include "States.h"
 #include "TreeWalker.h"
 #include "XULMenuAccessible.h"
 
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -227,17 +227,17 @@ pref("general.smoothScroll", true);
 pref("general.autoScroll", false);
 #else
 pref("general.autoScroll", true);
 #endif
 
 pref("browser.stopReloadAnimation.enabled", true);
 pref("browser.schedulePressure.enabled", true);
 pref("browser.schedulePressure.defaultCount", 3);
-pref("browser.schedulePressure.timeoutMs", 1000);
+pref("browser.schedulePressure.timeoutMs", 300);
 
 // UI density of the browser chrome. This mostly affects toolbarbutton
 // and urlbar spacing. The possible values are 0=normal, 1=compact, 2=touch.
 pref("browser.uidensity", 0);
 // Whether Firefox will automatically override the uidensity to "touch"
 // while the user is in a touch environment (such as Windows tablet mode).
 pref("browser.touchmode.auto", true);
 
--- a/browser/base/content/browser-fullScreenAndPointerLock.js
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -365,24 +365,23 @@ var FullScreen = {
         this.willToggle(false);
         break;
       case "fullscreen":
         this.toggle();
         break;
       case "MozDOMFullscreen:Entered": {
         // The event target is the element which requested the DOM
         // fullscreen. If we were entering DOM fullscreen for a remote
-        // browser, the target would be `gBrowser` and the original
-        // target would be the browser which was the parameter of
+        // browser, the target would be the browser which was the parameter of
         // `remoteFrameFullscreenChanged` call. If the fullscreen
         // request was initiated from an in-process browser, we need
         // to get its corresponding browser here.
         let browser;
-        if (event.target == gBrowser.container) {
-          browser = event.originalTarget;
+        if (event.target.ownerGlobal == window) {
+          browser = event.target;
         } else {
           let topWin = event.target.ownerGlobal.top;
           browser = gBrowser.getBrowserForContentWindow(topWin);
         }
         TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
         this.enterDomFullscreen(browser);
         break;
       }
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -111,24 +111,16 @@ panelview[mainview] > .panel-header {
   position: absolute;
 }
 
 .panel-viewstack {
   overflow: visible;
   transition: height var(--panelui-subview-transition-duration);
 }
 
-#navigator-toolbox {
-  -moz-binding: url("chrome://browser/content/tabbrowser.xml#empty");
-}
-
-tabbrowser {
-  -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser");
-}
-
 #tabbrowser-tabs {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabs");
 }
 
 @supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") {
   #tabbrowser-tabs {
     /* Without this, the tabs container width extends beyond the window width */
     width: 0;
@@ -286,17 +278,16 @@ window:not([chromehidden~="toolbar"]) #n
 #main-window:not([chromemargin]) > #titlebar,
 #main-window[inFullscreen] > #titlebar,
 #main-window[inFullscreen] .titlebar-placeholder,
 #main-window:not([tabsintitlebar]) .titlebar-placeholder {
   display: none;
 }
 
 #titlebar {
-  -moz-binding: url("chrome://global/content/bindings/general.xml#windowdragbox");
   -moz-window-dragging: drag;
 }
 
 #titlebar-spacer {
   pointer-events: none;
 }
 
 #main-window[tabsintitlebar] #titlebar-buttonbox {
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -135,16 +135,24 @@ XPCOMUtils.defineLazyServiceGetters(this
 });
 
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
                                      "@mozilla.org/xre/app-info;1",
                                      "nsICrashReporter");
 }
 
+XPCOMUtils.defineLazyGetter(this, "gBrowser", function() {
+  // The TabBrowser class only exists in proper browser windows, whereas
+  // browser.js may be loaded in other windows where a non-tabbrowser
+  // browser might try to access the gBrowser property.
+  // eslint-disable-next-line no-undef
+  return (typeof TabBrowser == "function") ? new TabBrowser() : null;
+});
+
 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
   return Services.strings.createBundle("chrome://browser/locale/browser.properties");
 });
 XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
   // This is a stringbundle-like interface to gBrowserBundle, formerly a getter for
   // the "bundle_browser" element.
   return {
     getString(key) {
@@ -218,17 +226,16 @@ XPCOMUtils.defineLazyGetter(this, "Win7F
       }
     };
   }
   return null;
 });
 
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
-var gBrowser = null; // Will be instantiated by the <tabbbrowser> constructor.
 var gLastValidURLStr = "";
 var gInPrintPreviewMode = false;
 var gContextMenu = null; // nsContextMenu instance
 var gMultiProcessBrowser =
   window.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIWebNavigation)
         .QueryInterface(Ci.nsILoadContext)
         .useRemoteTabs;
@@ -1297,17 +1304,18 @@ var gBrowserInit = {
     mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
     mm.loadFrameScript("chrome://global/content/content-HybridContentTelemetry.js", true);
     mm.loadFrameScript("chrome://global/content/manifestMessages.js", true);
 
     window.messageManager.addMessageListener("Browser:LoadURI", RedirectLoad);
 
     if (!gMultiProcessBrowser) {
       // There is a Content:Click message manually sent from content.
-      Services.els.addSystemEventListener(gBrowser.container, "click", contentAreaClick, true);
+      Services.els.addSystemEventListener(gBrowser.mPanelContainer, "click",
+        contentAreaClick, true);
     }
 
     // hook up UI through progress listener
     gBrowser.addProgressListener(window.XULBrowserWindow);
     gBrowser.addTabsProgressListener(window.TabsProgressListener);
 
     SidebarUI.init();
 
@@ -1382,19 +1390,18 @@ var gBrowserInit = {
   },
 
   _cancelDelayedStartup() {
     window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
     this._boundDelayedStartup = null;
   },
 
   _delayedStartup() {
-    let tmp = {};
-    ChromeUtils.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
-    let TelemetryTimestamps = tmp.TelemetryTimestamps;
+    let { TelemetryTimestamps } =
+      ChromeUtils.import("resource://gre/modules/TelemetryTimestamps.jsm", {});
     TelemetryTimestamps.add("delayedStartupStarted");
 
     this._cancelDelayedStartup();
 
     // We need to set the OfflineApps message listeners up before we
     // load homepages, which might need them.
     OfflineApps.init();
 
@@ -1415,21 +1422,21 @@ var gBrowserInit = {
 
       let browser = gBrowser.getBrowserForDocument(event.target);
       // Reset the zoom for the tabcrashed page.
       ZoomManager.setZoomForBrowser(browser, 1);
     }, false, true);
 
     gBrowser.addEventListener("InsecureLoginFormsStateChange", function() {
       gIdentityHandler.refreshForInsecureLoginForms();
-    });
+    }, true);
 
     gBrowser.addEventListener("PermissionStateChange", function() {
       gIdentityHandler.refreshIdentityBlock();
-    });
+    }, true);
 
     // Get the service so that it initializes and registers listeners for new
     // tab pages in order to be ready for any early-loading about:newtab pages,
     // e.g., start/home page, command line / startup uris to load, sessionstore
     gAboutNewTabService.QueryInterface(Ci.nsISupports);
 
     this._handleURIToLoad();
 
@@ -1539,16 +1546,22 @@ var gBrowserInit = {
     });
 
     window.addEventListener("mousemove", MousePosTracker);
     window.addEventListener("dragover", MousePosTracker);
 
     gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
     gNavToolbox.addEventListener("customizationending", CustomizationHandler);
 
+    if (AppConstants.platform == "linux") {
+      let { WindowDraggingElement } =
+        ChromeUtils.import("resource://gre/modules/WindowDraggingUtils.jsm", {});
+      new WindowDraggingElement(document.getElementById("titlebar"));
+    }
+
     SessionStore.promiseInitialized.then(() => {
       // Bail out if the window has been closed in the meantime.
       if (window.closed) {
         return;
       }
 
       // Enable the Restore Last Session command if needed
       RestoreLastSessionObserver.init();
@@ -1911,16 +1924,17 @@ var gBrowserInit = {
       BrowserOffline.uninit();
       IndexedDBPromptHelper.uninit();
       CanvasPermissionPromptHelper.uninit();
       PanelUI.uninit();
       AutoShowBookmarksToolbar.uninit();
     }
 
     // Final window teardown, do this last.
+    gBrowser.destroy();
     window.XULBrowserWindow = null;
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIWebNavigation)
           .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIXULWindow)
           .XULBrowserWindow = null;
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = null;
@@ -2586,17 +2600,17 @@ function readFromClipboard() {
  *        outerWindowID (optional):
  *          The outerWindowID of the content window containing the document that
  *          we want to view the source of. You only need to provide this if you
  *          want to attempt to retrieve the document source from the network
  *          cache.
  *        lineNumber (optional):
  *          The line number to focus on once the source is loaded.
  */
-function BrowserViewSourceOfDocument(aArgsOrDocument) {
+async function BrowserViewSourceOfDocument(aArgsOrDocument) {
   let args;
 
   if (aArgsOrDocument instanceof Document) {
     let doc = aArgsOrDocument;
     // Deprecated API - callers should pass args object instead.
     if (Cu.isCrossProcessWrapper(doc)) {
       throw new Error("BrowserViewSourceOfDocument cannot accept a CPOW " +
                       "as a document.");
@@ -2610,72 +2624,65 @@ function BrowserViewSourceOfDocument(aAr
     let outerWindowID = requestor.getInterface(Ci.nsIDOMWindowUtils)
                                  .outerWindowID;
     let URL = browser.currentURI.spec;
     args = { browser, outerWindowID, URL };
   } else {
     args = aArgsOrDocument;
   }
 
-  let viewInternal = () => {
-    let tabBrowser = gBrowser;
-    let preferredRemoteType;
-    if (args.browser) {
-      preferredRemoteType = args.browser.remoteType;
-    } else {
-      if (!tabBrowser) {
-        throw new Error("BrowserViewSourceOfDocument should be passed the " +
-                        "subject browser if called from a window without " +
-                        "gBrowser defined.");
-      }
-      // Some internal URLs (such as specific chrome: and about: URLs that are
-      // not yet remote ready) cannot be loaded in a remote browser.  View
-      // source in tab expects the new view source browser's remoteness to match
-      // that of the original URL, so disable remoteness if necessary for this
-      // URL.
-      preferredRemoteType =
-        E10SUtils.getRemoteTypeForURI(args.URL, gMultiProcessBrowser);
-    }
-
-    // In the case of popups, we need to find a non-popup browser window.
-    if (!tabBrowser || !window.toolbar.visible) {
-      // This returns only non-popup browser windows by default.
-      let browserWindow = RecentWindow.getMostRecentBrowserWindow();
-      tabBrowser = browserWindow.gBrowser;
-    }
-
-    // `viewSourceInBrowser` will load the source content from the page
-    // descriptor for the tab (when possible) or fallback to the network if
-    // that fails.  Either way, the view source module will manage the tab's
-    // location, so use "about:blank" here to avoid unnecessary redundant
-    // requests.
-    let tab = tabBrowser.loadOneTab("about:blank", {
-      relatedToCurrent: true,
-      inBackground: false,
-      preferredRemoteType,
-      sameProcessAsFrameLoader: args.browser ? args.browser.frameLoader : null,
-      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
-    });
-    args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
-    top.gViewSourceUtils.viewSourceInBrowser(args);
-  };
-
   // Check if external view source is enabled.  If so, try it.  If it fails,
   // fallback to internal view source.
   if (Services.prefs.getBoolPref("view_source.editor.external")) {
-    top.gViewSourceUtils
-       .openInExternalEditor(args, result => {
-      if (!result) {
-        viewInternal();
-      }
-    });
+    try {
+      await top.gViewSourceUtils.openInExternalEditor(args);
+      return;
+    } catch (data) {}
+  }
+
+  let tabBrowser = gBrowser;
+  let preferredRemoteType;
+  if (args.browser) {
+    preferredRemoteType = args.browser.remoteType;
   } else {
-    // Display using internal view source
-    viewInternal();
-  }
+    if (!tabBrowser) {
+      throw new Error("BrowserViewSourceOfDocument should be passed the " +
+                      "subject browser if called from a window without " +
+                      "gBrowser defined.");
+    }
+    // Some internal URLs (such as specific chrome: and about: URLs that are
+    // not yet remote ready) cannot be loaded in a remote browser.  View
+    // source in tab expects the new view source browser's remoteness to match
+    // that of the original URL, so disable remoteness if necessary for this
+    // URL.
+    preferredRemoteType =
+      E10SUtils.getRemoteTypeForURI(args.URL, gMultiProcessBrowser);
+  }
+
+  // In the case of popups, we need to find a non-popup browser window.
+  if (!tabBrowser || !window.toolbar.visible) {
+    // This returns only non-popup browser windows by default.
+    let browserWindow = RecentWindow.getMostRecentBrowserWindow();
+    tabBrowser = browserWindow.gBrowser;
+  }
+
+  // `viewSourceInBrowser` will load the source content from the page
+  // descriptor for the tab (when possible) or fallback to the network if
+  // that fails.  Either way, the view source module will manage the tab's
+  // location, so use "about:blank" here to avoid unnecessary redundant
+  // requests.
+  let tab = tabBrowser.loadOneTab("about:blank", {
+    relatedToCurrent: true,
+    inBackground: false,
+    preferredRemoteType,
+    sameProcessAsFrameLoader: args.browser ? args.browser.frameLoader : null,
+    triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+  });
+  args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
+  top.gViewSourceUtils.viewSourceInBrowser(args);
 }
 
 /**
  * Opens the View Source dialog for the source loaded in the root
  * top-level document of the browser. This is really just a
  * convenience wrapper around BrowserViewSourceOfDocument.
  *
  * @param browser
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -62,16 +62,17 @@
 
 # All JS files which are not content (only) dependent that browser.xul
 # wishes to include *must* go into the global-scripts.inc file
 # so that they can be shared by macBrowserOverlay.xul.
 #include global-scripts.inc
 
 <script type="application/javascript">
   Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", this);
+  Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser.js", this);
 </script>
 
 # All sets except for popupsets (commands, keys, stringbundles and broadcasters) *must* go into the
 # browser-sets.inc file for sharing with hiddenWindow.xul.
 #define FULL_BROWSER_WINDOW
 #include browser-sets.inc
 #undef FULL_BROWSER_WINDOW
 
@@ -1206,22 +1207,38 @@
         </sidebarheader>
         <browser id="sidebar" flex="1" autoscroll="false" disablehistory="true" disablefullscreen="true"
                   style="min-width: 14em; width: 18em; max-width: 36em;" tooltip="aHTMLTooltip"/>
       </vbox>
 
       <splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/>
       <vbox id="appcontent" flex="1">
         <notificationbox id="high-priority-global-notificationbox" notificationside="top"/>
-        <tabbrowser flex="1" contenttooltip="aHTMLTooltip"
-                    tabcontainer="tabbrowser-tabs"
-                    contentcontextmenu="contentAreaContextMenu"
-                    autocompletepopup="PopupAutoComplete"
-                    selectmenulist="ContentSelectDropdown"
-                    datetimepicker="DateTimePickerPanel"/>
+        <tabbox id="tabbrowser-tabbox"
+                    flex="1" eventnode="document" tabcontainer="tabbrowser-tabs"
+                    onselect="if (event.target.localName == 'tabpanels') gBrowser.updateCurrentBrowser();">
+          <tabpanels flex="1" class="plain" selectedIndex="0" id="tabbrowser-tabpanels">
+            <notificationbox flex="1" notificationside="top">
+              <hbox flex="1" class="browserSidebarContainer">
+                <vbox flex="1" class="browserContainer">
+                  <stack flex="1" class="browserStack">
+                    <browser id="tabbrowser-initialBrowser" type="content"
+                             message="true" messagemanagergroup="browsers"
+                             primary="true" blank="true"
+                             tooltip="aHTMLTooltip"
+                             contextmenu="contentAreaContextMenu"
+                             autocompletepopup="PopupAutoComplete"
+                             selectmenulist="ContentSelectDropdown"
+                             datetimepicker="DateTimePickerPanel"/>
+                  </stack>
+                </vbox>
+              </hbox>
+            </notificationbox>
+          </tabpanels>
+        </tabbox>
       </vbox>
       <vbox id="browser-border-end" hidden="true" layer="true"/>
     </hbox>
 #include ../../components/customizableui/content/customizeMode.inc.xul
   </deck>
 
   <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true">
     <html:div class="pointerlockfswarning-domain-text">
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -6,17 +6,16 @@
 # If you update this list, you may need to add a mapping within the following
 # file so that ESLint works correctly:
 # tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
 
 <script type="application/javascript">
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 for (let script of [
-  "chrome://browser/content/tabbrowser.js",
   "chrome://browser/content/browser.js",
 
   "chrome://browser/content/browser-captivePortal.js",
   "chrome://browser/content/browser-compacttheme.js",
   "chrome://browser/content/browser-feeds.js",
   "chrome://browser/content/browser-media.js",
   "chrome://browser/content/browser-pageActions.js",
   "chrome://browser/content/browser-places.js",
--- a/browser/base/content/pageinfo/pageInfo.xul
+++ b/browser/base/content/pageinfo/pageInfo.xul
@@ -64,30 +64,30 @@
     <key key="&selectall.key;"   modifiers="alt"   command="cmd_selectall"/>
   </keyset>
 
   <menupopup id="picontext">
     <menuitem id="menu_selectall" label="&selectall.label;" command="cmd_selectall" accesskey="&selectall.accesskey;"/>
     <menuitem id="menu_copy"      label="&copy.label;"      command="cmd_copy"      accesskey="&copy.accesskey;"/>
   </menupopup>
 
-  <windowdragbox id="topBar" class="viewGroupWrapper">
+  <vbox id="topBar">
     <radiogroup id="viewGroup" class="chromeclass-toolbar" orient="horizontal">
       <radio id="generalTab"  label="&generalTab;"  accesskey="&generalTab.accesskey;"
            oncommand="showTab('general');"/>
       <radio id="mediaTab"    label="&mediaTab;"    accesskey="&mediaTab.accesskey;"
            oncommand="showTab('media');" hidden="true"/>
       <radio id="feedTab"     label="&feedTab;"     accesskey="&feedTab.accesskey;"
            oncommand="showTab('feed');" hidden="true"/>
       <radio id="permTab"     label="&permTab;"     accesskey="&permTab.accesskey;"
            oncommand="showTab('perm');"/>
       <radio id="securityTab" label="&securityTab;" accesskey="&securityTab.accesskey;"
            oncommand="showTab('security');"/>
     </radiogroup>
-  </windowdragbox>
+  </vbox>
 
   <deck id="mainDeck" flex="1">
     <!-- General page information -->
     <vbox id="generalPanel">
       <grid id="generalGrid">
         <columns>
           <column/>
           <column class="gridSeparator"/>
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -84,15 +84,15 @@ tabpanels {
   transition: width .15s ease-out;
 }
 
 browser[blank],
 browser[pendingpaint] {
   opacity: 0;
 }
 
-tabbrowser[pendingpaint] {
+#tabbrowser-tabpanels[pendingpaint] {
   background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
   background-repeat: no-repeat;
   background-position: center center;
   background-color: #f9f9f9 !important;
   background-size: 30px;
 }
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -1,56 +1,43 @@
 /* -*- 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;
+  constructor() {
     this.requiresAddonInterpositions = true;
 
-    // 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;
+    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");
-    });
-    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.ownerGlobal = window;
+    this.ownerDocument = document;
+
+    this.mPanelContainer = document.getElementById("tabbrowser-tabpanels");
+    this.addEventListener = this.mPanelContainer.addEventListener.bind(this.mPanelContainer);
+    this.removeEventListener = this.mPanelContainer.removeEventListener.bind(this.mPanelContainer);
+    this.dispatchEvent = this.mPanelContainer.dispatchEvent.bind(this.mPanelContainer);
+
+    this.initialBrowser = document.getElementById("tabbrowser-initialBrowser");
+
+    this.tabContainer = document.getElementById("tabbrowser-tabs");
+
+    this.tabs = this.tabContainer.childNodes;
+
+    this.tabbox = document.getElementById("tabbrowser-tabbox");
 
     this.closingTabsEnum = { ALL: 0, OTHER: 1, TO_END: 2 };
 
     this._visibleTabs = null;
 
     this.mCurrentTab = null;
 
     this._lastRelatedTabMap = new WeakMap();
@@ -76,16 +63,18 @@ class TabBrowser {
     this._previewMode = false;
 
     this._lastFindValue = "";
 
     this._contentWaitingCount = 0;
 
     this.tabAnimationsInProgress = 0;
 
+    this._XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
     /**
      * 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
@@ -159,17 +148,17 @@ class TabBrowser {
     this._switcher = null;
 
     this._tabMinWidthLimit = 50;
 
     this._soundPlayingAttrRemovalTimer = 0;
 
     this._hoverTabTimer = null;
 
-    this.mCurrentBrowser = document.getAnonymousElementByAttribute(this.container, "anonid", "initialBrowser");
+    this.mCurrentBrowser = this.initialBrowser;
     this.mCurrentBrowser.permanentKey = {};
 
     CustomizableUI.addListener(this);
     this._updateNewTabVisibility();
 
     Services.obs.addObserver(this, "contextual-identity-updated");
 
     this.mCurrentTab = this.tabContainer.firstChild;
@@ -191,34 +180,36 @@ class TabBrowser {
     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);
+    document.getElementById("mainPopupSet").appendChild(this._autoScrollPopup);
     this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
     this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
 
     // 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");
+    if (Services.prefs.getBoolPref("browser.display.use_system_colors")) {
+      this.mPanelContainer.style.backgroundColor = "-moz-default-background-color";
+    } else if (Services.prefs.getIntPref("browser.display.document_color_use") == 2) {
+      this.mPanelContainer.style.backgroundColor =
+        Services.prefs.getCharPref("browser.display.background_color");
+    }
 
     let messageManager = window.getGroupMessageManager("browsers");
 
     let remote = window.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIWebNavigation)
                        .QueryInterface(Ci.nsILoadContext)
                        .useRemoteTabs;
     if (remote) {
@@ -285,18 +276,17 @@ class TabBrowser {
   }
 
   get popupAnchor() {
     if (this.mCurrentTab._popupAnchor) {
       return this.mCurrentTab._popupAnchor;
     }
     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");
+    let popupAnchor = document.createElementNS(this._XUL_NS, "hbox");
     popupAnchor.className = "popup-anchor";
     popupAnchor.hidden = true;
     stack.appendChild(popupAnchor);
     return this.mCurrentTab._popupAnchor = popupAnchor;
   }
 
   set selectedTab(val) {
     if (gNavToolbox.collapsed && !this._allowTabChange) {
@@ -482,17 +472,17 @@ class TabBrowser {
 
   getFindBar(aTab) {
     if (!aTab)
       aTab = this.selectedTab;
 
     if (aTab._findBar)
       return aTab._findBar;
 
-    let findBar = document.createElementNS(this.namespaceURI, "findbar");
+    let findBar = document.createElementNS(this._XUL_NS, "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;
@@ -504,17 +494,17 @@ class TabBrowser {
     event.initEvent("TabFindInitialized", true, false);
     aTab.dispatchEvent(event);
 
     return findBar;
   }
 
   getStatusPanel() {
     if (!this._statusPanel) {
-      this._statusPanel = document.createElementNS(this.namespaceURI, "statuspanel");
+      this._statusPanel = document.createElementNS(this._XUL_NS, "statuspanel");
       this._statusPanel.setAttribute("inactive", "true");
       this._statusPanel.setAttribute("layer", "true");
       this._appendStatusPanel();
     }
     return this._statusPanel;
   }
 
   _appendStatusPanel() {
@@ -857,17 +847,17 @@ class TabBrowser {
   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 docElement = document.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.
@@ -899,17 +889,17 @@ class TabBrowser {
           newTitle = uri.prePath + sep + newTitle;
       }
     } catch (e) {}
 
     return newTitle;
   }
 
   updateTitlebar() {
-    this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
+    document.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
   }
 
   updateCurrentBrowser(aForceUpdate) {
     var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
     if (this.mCurrentBrowser == newBrowser && !aForceUpdate)
       return;
 
     if (!aForceUpdate) {
@@ -1172,18 +1162,17 @@ class TabBrowser {
     // 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 prompts = newBrowser.parentNode.getElementsByTagNameNS(this._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.
@@ -1779,19 +1768,17 @@ class TabBrowser {
     // 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"));
-      }
+      browser.setAttribute("autocompletepopup", "PopupAutoComplete");
     }
 
     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
@@ -1831,25 +1818,23 @@ class TabBrowser {
     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");
+    let b = document.createElementNS(this._XUL_NS, "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"));
+    b.setAttribute("contextmenu", "contentAreaContextMenu");
+    b.setAttribute("tooltip", "aHTMLTooltip");
 
     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;
@@ -1862,18 +1847,18 @@ class TabBrowser {
 
     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"));
+    if (!aParams.isPreloadBrowser) {
+      b.setAttribute("autocompletepopup", "PopupAutoComplete");
     }
 
     /*
      * 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".
@@ -1885,22 +1870,19 @@ class TabBrowser {
      *      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("selectmenulist", "ContentSelectDropdown");
+
+    b.setAttribute("datetimepicker", "DateTimePickerPanel");
 
     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.
@@ -1915,37 +1897,35 @@ class TabBrowser {
     // 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);
     }
 
     // Create the browserStack container
-    var stack = document.createElementNS(NS_XUL, "stack");
+    let stack = document.createElementNS(this._XUL_NS, "stack");
     stack.className = "browserStack";
     stack.appendChild(b);
     stack.setAttribute("flex", "1");
 
     // Create the browserContainer
-    var browserContainer = document.createElementNS(NS_XUL, "vbox");
+    let browserContainer = document.createElementNS(this._XUL_NS, "vbox");
     browserContainer.className = "browserContainer";
     browserContainer.appendChild(stack);
     browserContainer.setAttribute("flex", "1");
 
     // Create the sidebar container
-    var browserSidebarContainer = document.createElementNS(NS_XUL,
-      "hbox");
+    let browserSidebarContainer = document.createElementNS(this._XUL_NS, "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");
+    let notificationbox = document.createElementNS(this._XUL_NS, "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");
@@ -2170,17 +2150,16 @@ class TabBrowser {
     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;
@@ -2248,17 +2227,17 @@ class TabBrowser {
     // 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");
+    var t = document.createElementNS(this._XUL_NS, "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 */ }
@@ -3549,22 +3528,23 @@ class TabBrowser {
 
   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;
+    let direction = window.getComputedStyle(document.documentElement).direction;
     if ((direction == "ltr" && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
-      (direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT))
+        (direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT)) {
       this.moveTabForward();
-    else
+    } else {
       this.moveTabBackward();
+    }
   }
 
   /**
    * @param   aTab
    *          Can be from a different window as well
    * @param   aRestoreTabImmediately
    *          Can defer loading of the tab contents
    */
@@ -3596,1095 +3576,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);
     }
   }
 
@@ -4736,17 +3642,17 @@ class TabBrowser {
       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")
+          if (window.getComputedStyle(document.documentElement).direction == "ltr")
             offset *= -1;
           this.tabContainer.advanceSelectedTab(offset, true);
           aEvent.preventDefault();
       }
     }
   }
 
   createTooltip(event) {
@@ -5039,17 +3945,17 @@ class TabBrowser {
                         .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() {
+  destroy() {
     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,
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -4,58 +4,16 @@
    - 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/. -->
 
 <bindings id="tabBrowserBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
-  <!--
-  This binding is bound to <toolbox id="navigator-toolbox"> so that
-  the #tabbrowser binding is initialized before the #tabs binding.
-  Remove after bug 1392352.
-  -->
-  <binding id="empty"/>
-
-  <binding id="tabbrowser">
-    <resources>
-      <stylesheet src="chrome://browser/content/tabbrowser.css"/>
-    </resources>
-
-    <content>
-      <xul:tabbox anonid="tabbox" class="tabbrowser-tabbox"
-                  flex="1" eventnode="document" xbl:inherits="tabcontainer"
-                  onselect="if (event.target.localName == 'tabpanels') gBrowser.updateCurrentBrowser();">
-        <xul:tabpanels flex="1" class="plain" selectedIndex="0" anonid="panelcontainer">
-          <xul:notificationbox flex="1" notificationside="top">
-            <xul:hbox flex="1" class="browserSidebarContainer">
-              <xul:vbox flex="1" class="browserContainer">
-                <xul:stack flex="1" class="browserStack" anonid="browserStack">
-                  <xul:browser anonid="initialBrowser" type="content" message="true" messagemanagergroup="browsers"
-                               primary="true" blank="true"
-                               xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,selectmenulist,datetimepicker"/>
-                </xul:stack>
-              </xul:vbox>
-            </xul:hbox>
-          </xul:notificationbox>
-        </xul:tabpanels>
-      </xul:tabbox>
-      <children/>
-    </content>
-    <implementation>
-      <constructor>
-        gBrowser = new TabBrowser(this);
-      </constructor>
-      <destructor>
-        gBrowser.disconnectedCallback();
-      </destructor>
-    </implementation>
-  </binding>
-
   <binding id="tabbrowser-arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll">
     <implementation>
       <!-- Override scrollbox.xml method, since our scrollbox's children are
            inherited from the binding parent -->
       <method name="_getScrollableElements">
         <body><![CDATA[
           return Array.filter(document.getBindingParent(this).childNodes,
                               this._canScrollToElement, this);
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -2,16 +2,17 @@
 prefs =
   browser.policies.enabled=true
 support-files =
   head.js
   config_popups_cookies_addons_flash.json
   config_broken_json.json
 
 [browser_policies_broken_json.js]
+[browser_policies_notice_in_aboutpreferences.js]
 [browser_policies_popups_cookies_addons_flash.js]
 [browser_policies_runOnce_helper.js]
 [browser_policies_setAndLockPref_API.js]
 [browser_policies_simple_policies.js]
 [browser_policies_sorted_alphabetically.js]
 [browser_policies_validate_and_parse_API.js]
 [browser_policy_app_update.js]
 [browser_policy_block_about_addons.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_notice_in_aboutpreferences.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_notice_in_aboutprefences() {
+  await setupPolicyEngineWithJson({
+    "policies": { }
+  });
+
+  await BrowserTestUtils.withNewTab("about:preferences", async browser => {
+  // eslint-disable-next-line mozilla/no-cpows-in-tests
+  ok(!browser.contentDocument.getElementById("policies-container").hidden,
+     "The Policies notice was made visible in about:preferences");
+  });
+});
--- a/browser/components/migration/BrowserProfileMigrators.manifest
+++ b/browser/components/migration/BrowserProfileMigrators.manifest
@@ -1,14 +1,19 @@
 component {6F8BB968-C14F-4D6F-9733-6C6737B35DCE} ProfileMigrator.js
 contract @mozilla.org/toolkit/profile-migrator;1 {6F8BB968-C14F-4D6F-9733-6C6737B35DCE}
 
 #if defined(XP_WIN) || defined(XP_MACOSX)
 component {4bf85aa5-4e21-46ca-825f-f9c51a5e8c76} ChromeProfileMigrator.js
 contract @mozilla.org/profile/migrator;1?app=browser&type=canary {4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}
+#else
+component {47f75963-840b-4950-a1f0-d9c1864f8b8e} ChromeProfileMigrator.js
+contract @mozilla.org/profile/migrator;1?app=browser&type=chrome-beta {47f75963-840b-4950-a1f0-d9c1864f8b8e}
+component {7370a02a-4886-42c3-a4ec-d48c726ec30a} ChromeProfileMigrator.js
+contract @mozilla.org/profile/migrator;1?app=browser&type=chrome-dev {7370a02a-4886-42c3-a4ec-d48c726ec30a}
 #endif
 component {4cec1de4-1671-4fc3-a53e-6c539dc77a26} ChromeProfileMigrator.js
 contract @mozilla.org/profile/migrator;1?app=browser&type=chrome {4cec1de4-1671-4fc3-a53e-6c539dc77a26}
 component {8cece922-9720-42de-b7db-7cef88cb07ca} ChromeProfileMigrator.js
 contract @mozilla.org/profile/migrator;1?app=browser&type=chromium {8cece922-9720-42de-b7db-7cef88cb07ca}
 
 component {91185366-ba97-4438-acba-48deaca63386} FirefoxProfileMigrator.js
 contract @mozilla.org/profile/migrator;1?app=browser&type=firefox {91185366-ba97-4438-acba-48deaca63386}
--- a/browser/components/migration/ChromeMigrationUtils.jsm
+++ b/browser/components/migration/ChromeMigrationUtils.jsm
@@ -7,18 +7,16 @@ var EXPORTED_SYMBOLS = ["ChromeMigration
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 ChromeUtils.import("resource://gre/modules/osfile.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var ChromeMigrationUtils = {
-  _chromeUserDataPath: null,
-
   _extensionVersionDirectoryNames: {},
 
   // The cache for the locale strings.
   // For example, the data could be:
   // {
   //   "profile-id-1": {
   //     "extension-id-1": {
   //       "name": {
@@ -168,70 +166,63 @@ var ChromeMigrationUtils = {
    */
   async getLastUsedProfileId() {
     let localState = await this.getLocalState();
     return localState ? localState.profile.last_used : "Default";
   },
 
   /**
    * Get the local state file content.
+   * @param {String} dataPath the type of Chrome data we're looking for (Chromium, Canary, etc.)
    * @returns {Object} The JSON-based content.
    */
-  async getLocalState() {
+  async getLocalState(dataPath = "Chrome") {
     let localState = null;
     try {
-      let localStatePath = OS.Path.join(this.getChromeUserDataPath(), "Local State");
+      let localStatePath = OS.Path.join(this.getDataPath(dataPath), "Local State");
       let localStateJson = await OS.File.read(localStatePath, { encoding: "utf-8" });
       localState = JSON.parse(localStateJson);
     } catch (ex) {
       Cu.reportError(ex);
       throw ex;
     }
     return localState;
   },
 
   /**
    * Get the path of Chrome extension directory.
    * @param {String} profileId - The user profile's ID.
    * @returns {String} The path of Chrome extension directory.
    */
   getExtensionPath(profileId) {
-    return OS.Path.join(this.getChromeUserDataPath(), profileId, "Extensions");
-  },
-
-  /**
-   * Get the path of the Chrome user data directory.
-   * @returns {String} The path of the Chrome user data directory.
-   */
-  getChromeUserDataPath() {
-    if (!this._chromeUserDataPath) {
-      this._chromeUserDataPath = this.getDataPath("Chrome");
-    }
-    return this._chromeUserDataPath;
+    return OS.Path.join(this.getDataPath(), profileId, "Extensions");
   },
 
   /**
    * Get the path of an application data directory.
-   * @param {String} chromeProjectName - The Chrome project name, e.g. "Chrome", "Chromium" or "Canary".
+   * @param {String} chromeProjectName - The Chrome project name, e.g. "Chrome", "Canary", etc.
+   *                                     Defaults to "Chrome".
    * @returns {String} The path of application data directory.
    */
-  getDataPath(chromeProjectName) {
+  getDataPath(chromeProjectName = "Chrome") {
     const SUB_DIRECTORIES = {
       win: {
         Chrome: ["Google", "Chrome"],
         Chromium: ["Chromium"],
         Canary: ["Google", "Chrome SxS"],
       },
       macosx: {
         Chrome: ["Google", "Chrome"],
         Chromium: ["Chromium"],
         Canary: ["Google", "Chrome Canary"],
       },
       linux: {
         Chrome: ["google-chrome"],
+        "Chrome Beta": ["google-chrome-beta"],
+        "Chrome Dev": ["google-chrome-unstable"],
         Chromium: ["chromium"],
         // Canary is not available on Linux.
       },
     };
     let dirKey, subfolders;
     subfolders = SUB_DIRECTORIES[AppConstants.platform][chromeProjectName];
     if (!subfolders) {
       return null;
--- a/browser/components/migration/ChromeProfileMigrator.js
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -155,17 +155,17 @@ ChromeProfileMigrator.prototype.getSourc
       return this.__sourceProfiles;
 
     let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
     if (!chromeUserDataPath)
       return [];
 
     let profiles = [];
     try {
-      let localState = await ChromeMigrationUtils.getLocalState();
+      let localState = await ChromeMigrationUtils.getLocalState(this._chromeUserDataPathSuffix);
       let info_cache = localState.profile.info_cache;
       for (let profileFolderName in info_cache) {
         profiles.push({
           id: profileFolderName,
           name: info_cache[profileFolderName].name || profileFolderName,
         });
       }
     } catch (e) {
@@ -483,9 +483,32 @@ CanaryProfileMigrator.prototype = Object
 CanaryProfileMigrator.prototype.classDescription = "Chrome Canary Profile Migrator";
 CanaryProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=canary";
 CanaryProfileMigrator.prototype.classID = Components.ID("{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}");
 
 if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
   componentsArray.push(CanaryProfileMigrator);
 }
 
+/**
+ * Chrome Dev / Unstable and Beta. Only separate from `regular` chrome on Linux
+ */
+if (AppConstants.platform != "win" && AppConstants.platform != "macosx") {
+  function ChromeDevMigrator() {
+    this._chromeUserDataPathSuffix = "Chrome Dev";
+  }
+  ChromeDevMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
+  ChromeDevMigrator.prototype.classDescription = "Chrome Dev Profile Migrator";
+  ChromeDevMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome-dev";
+  ChromeDevMigrator.prototype.classID = Components.ID("{7370a02a-4886-42c3-a4ec-d48c726ec30a}");
+
+  function ChromeBetaMigrator() {
+    this._chromeUserDataPathSuffix = "Chrome Beta";
+  }
+  ChromeBetaMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
+  ChromeBetaMigrator.prototype.classDescription = "Chrome Beta Profile Migrator";
+  ChromeBetaMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome-beta";
+  ChromeBetaMigrator.prototype.classID = Components.ID("{47f75963-840b-4950-a1f0-d9c1864f8b8e}");
+
+  componentsArray.push(ChromeDevMigrator, ChromeBetaMigrator);
+}
+
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(componentsArray);
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -52,17 +52,17 @@ XPCOMUtils.defineLazyGetter(this, "gAvai
       "firefox", "edge", "ie", "chrome", "chromium", "360se",
       "canary",
     ];
   }
   if (AppConstants.platform == "macosx") {
     return ["firefox", "safari", "chrome", "chromium", "canary"];
   }
   if (AppConstants.XP_UNIX) {
-    return ["firefox", "chrome", "chromium"];
+    return ["firefox", "chrome", "chrome-beta", "chrome-dev", "chromium"];
   }
   return [];
 });
 
 function getMigrationBundle() {
   if (!gMigrationBundle) {
     gMigrationBundle = Services.strings.createBundle(
      "chrome://browser/locale/migration/migration.properties");
@@ -513,17 +513,17 @@ var MigrationUtils = Object.freeze({
    *        The key of the string to retrieve.
    * @param aReplacements
    *        [optioanl] Array of replacements to run on the retrieved string.
    * @return the retrieved string.
    *
    * @see nsIStringBundle
    */
   getLocalizedString: function MU_getLocalizedString(aKey, aReplacements) {
-    aKey = aKey.replace(/_(canary|chromium)$/, "_chrome");
+    aKey = aKey.replace(/_(canary|chromium|chrome-beta|chrome-dev)$/, "_chrome");
 
     const OVERRIDES = {
       "4_firefox": "4_firefox_history_and_bookmarks",
       "64_firefox": "64_firefox_other",
     };
     aKey = OVERRIDES[aKey] || aKey;
 
     if (aReplacements === undefined)
@@ -539,16 +539,20 @@ var MigrationUtils = Object.freeze({
       case "ie":
         return "sourceNameIE";
       case "safari":
         return "sourceNameSafari";
       case "canary":
         return "sourceNameCanary";
       case "chrome":
         return "sourceNameChrome";
+      case "chrome-beta":
+        return "sourceNameChromeBeta";
+      case "chrome-dev":
+        return "sourceNameChromeDev";
       case "chromium":
         return "sourceNameChromium";
       case "firefox":
         return "sourceNameFirefox";
       case "360se":
         return "sourceName360se";
     }
     return null;
--- a/browser/components/migration/content/migration.xul
+++ b/browser/components/migration/content/migration.xul
@@ -42,16 +42,18 @@
       <radio id="360se"     label="&importFrom360se.label;"     accesskey="&importFrom360se.accesskey;"/>
 #elifdef XP_MACOSX
       <radio id="safari"    label="&importFromSafari.label;"    accesskey="&importFromSafari.accesskey;"/>
       <radio id="chrome"    label="&importFromChrome.label;"    accesskey="&importFromChrome.accesskey;"/>
       <radio id="chromium"  label="&importFromChromium.label;"  accesskey="&importFromChromium.accesskey;"/>
       <radio id="canary"    label="&importFromCanary.label;"    accesskey="&importFromCanary.accesskey;"/>
 #elifdef XP_UNIX
       <radio id="chrome"    label="&importFromChrome.label;"    accesskey="&importFromChrome.accesskey;"/>
+      <radio id="chrome-beta" label="&importFromChromeBeta.label;" accesskey="&importFromChromeBeta.accesskey;"/>
+      <radio id="chrome-dev"  label="&importFromChromeDev.label;"  accesskey="&importFromChromeDev.accesskey;"/>
       <radio id="chromium"  label="&importFromChromium.label;"  accesskey="&importFromChromium.accesskey;"/>
 #endif
       <radio id="nothing"   label="&importFromNothing.label;"   accesskey="&importFromNothing.accesskey;" hidden="true"/>
     </radiogroup>
     <label id="noSources" hidden="true">&noMigrationSources.label;</label>
     <spacer flex="1"/>
     <description class="header" id="closeSourceBrowser" style="visibility:hidden">&closeSourceBrowser.label;</description>
   </wizardpage>
--- a/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
@@ -1,14 +1,14 @@
 "use strict";
 
 ChromeUtils.import("resource:///modules/ChromeMigrationUtils.jsm");
 
 // Setup chrome user data path for all platforms.
-ChromeMigrationUtils.getChromeUserDataPath = () => {
+ChromeMigrationUtils.getDataPath = () => {
   return do_get_file("Library/Application Support/Google/Chrome/").path;
 };
 
 add_task(async function test_getExtensionList_function() {
   let extensionList = await ChromeMigrationUtils.getExtensionList("Default");
   Assert.equal(extensionList.length, 2, "There should be 2 extensions installed.");
   Assert.deepEqual(extensionList.find(extension => extension.id == "fake-extension-1"), {
     id: "fake-extension-1",
--- a/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
@@ -47,29 +47,16 @@ add_task(async function test_getDataPath
     Assert.equal(chromiumUserDataPath,
       OS.Path.join(getRootPath(), ".config", "chromium"),
       "Should get the path of Chromium data directory.");
     Assert.equal(canaryUserDataPath, null,
       "Should get null for Canary.");
   }
 });
 
-add_task(async function test_getChromeUserDataPath_function() {
-  let chromeUserDataPath = ChromeMigrationUtils.getChromeUserDataPath();
-  let expectedPath;
-  if (AppConstants.platform == "win") {
-    expectedPath = OS.Path.join(getRootPath(), "Google", "Chrome", "User Data");
-  } else if (AppConstants.platform == "macosx") {
-    expectedPath = OS.Path.join(getRootPath(), "Application Support", "Google", "Chrome");
-  } else {
-    expectedPath = OS.Path.join(getRootPath(), ".config", "google-chrome");
-  }
-  Assert.equal(chromeUserDataPath, expectedPath, "Should get the path of Chrome user data directory.");
-});
-
 add_task(async function test_getExtensionPath_function() {
   let extensionPath = ChromeMigrationUtils.getExtensionPath("Default");
   let expectedPath;
   if (AppConstants.platform == "win") {
     expectedPath = OS.Path.join(getRootPath(), "Google", "Chrome", "User Data", "Default", "Extensions");
   } else if (AppConstants.platform == "macosx") {
     expectedPath = OS.Path.join(getRootPath(), "Application Support", "Google", "Chrome", "Default", "Extensions");
   } else {
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -70,16 +70,18 @@ function init_all() {
     if (event.keyCode == KeyEvent.DOM_VK_TAB) {
       categories.setAttribute("keyboard-navigation", "true");
     }
   });
   categories.addEventListener("mousedown", function() {
     this.removeAttribute("keyboard-navigation");
   });
 
+  maybeDisplayPoliciesNotice();
+
   window.addEventListener("hashchange", onHashChange);
   gotoPref();
 
   let helpButton = document.querySelector(".help-button > .text-link");
   let helpUrl = Services.urlFormatter.formatURLPref("app.support.baseURL") + "preferences";
   helpButton.setAttribute("href", helpUrl);
 
   document.dispatchEvent(new CustomEvent("Initialized", {
@@ -298,19 +300,19 @@ function getClosestDisplayedHeader(eleme
   if (searchHeader && searchHeader.hidden &&
       header.previousSibling.classList.contains("subcategory")) {
     header = header.previousSibling;
   }
   return header;
 }
 
 function scrollContentTo(element) {
-  const SEARCH_CONTAINER_HEIGHT = document.querySelector(".search-container").clientHeight;
+  const STICKY_CONTAINER_HEIGHT = document.querySelector(".sticky-container").clientHeight;
   let mainContent = document.querySelector(".main-content");
-  let top = element.getBoundingClientRect().top - SEARCH_CONTAINER_HEIGHT;
+  let top = element.getBoundingClientRect().top - STICKY_CONTAINER_HEIGHT;
   mainContent.scroll({
     top,
     behavior: "smooth",
   });
 }
 
 function friendlyPrefCategoryNameToInternalName(aName) {
   if (aName.startsWith("pane"))
@@ -406,8 +408,14 @@ async function confirmRestartPrompt(aRes
 function appendSearchKeywords(aId, keywords) {
   let element = document.getElementById(aId);
   let searchKeywords = element.getAttribute("searchkeywords");
   if (searchKeywords) {
     keywords.push(searchKeywords);
   }
   element.setAttribute("searchkeywords", keywords.join(" "));
 }
+
+function maybeDisplayPoliciesNotice() {
+  if (Services.policies.status == Services.policies.ACTIVE) {
+    document.getElementById("policies-container").removeAttribute("hidden");
+  }
+}
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -194,17 +194,25 @@
     </vbox>
 
     <keyset>
       <key data-l10n-id="focus-search" modifiers="accel" id="focusSearch1" oncommand="gSearchResultsPane.searchInput.focus();"/>
     </keyset>
 
     <vbox class="main-content" flex="1" align="start">
       <vbox class="pane-container">
-        <hbox class="search-container" pack="end">
+        <hbox class="sticky-container" pack="end" align="top">
+          <hbox id="policies-container" align="stretch" flex="1" hidden="true">
+            <hbox align="top">
+              <image class="info-icon"></image>
+            </hbox>
+            <hbox align="center" flex="1">
+              <label class="policies-label" flex="1" data-l10n-id="policies-notice"></label>
+            </hbox>
+          </hbox>
           <textbox
             type="search" id="searchInput"
             data-l10n-id="search-input"
             data-l10n-attrs="style"
             hidden="true" clickSelectsAll="true"/>
         </hbox>
         <vbox id="mainPrefPane" class="prefpane prefwindow">
 #include searchResults.xul
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -59,16 +59,21 @@ var gSearchPane = {
 
     this._initAutocomplete();
 
     let suggestsPref = Preferences.get("browser.search.suggest.enabled");
     let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches");
     let updateSuggestionCheckboxes = this._updateSuggestionCheckboxes.bind(this);
     suggestsPref.on("change", updateSuggestionCheckboxes);
     urlbarSuggestsPref.on("change", updateSuggestionCheckboxes);
+    let urlbarSuggests = document.getElementById("urlBarSuggestion");
+    urlbarSuggests.addEventListener("command", () => {
+      urlbarSuggestsPref.value = !urlbarSuggests.checked;
+    });
+
     this._initShowSearchSuggestionsFirst();
     this._updateSuggestionCheckboxes();
   },
 
   _initShowSearchSuggestionsFirst() {
     this._urlbarSuggestionsPosPref = Preferences.get("browser.urlbar.matchBuckets");
     let checkbox =
       document.getElementById("showSearchSuggestionsFirstCheckbox");
--- a/browser/components/preferences/in-content/tests/browser_searchsuggestions.js
+++ b/browser/components/preferences/in-content/tests/browser_searchsuggestions.js
@@ -1,43 +1,56 @@
-var original = Services.prefs.getBoolPref("browser.search.suggest.enabled");
+const SUGGEST_PREF_NAME = "browser.search.suggest.enabled";
+const URLBAR_SUGGEST_PREF_NAME = "browser.urlbar.suggest.searches";
+var original = Services.prefs.getBoolPref(SUGGEST_PREF_NAME);
 
 registerCleanupFunction(() => {
-  Services.prefs.setBoolPref("browser.search.suggest.enabled", original);
+  Services.prefs.setBoolPref(SUGGEST_PREF_NAME, original);
 });
 
 // Open with suggestions enabled
 add_task(async function() {
-  Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+  Services.prefs.setBoolPref(SUGGEST_PREF_NAME, true);
+  const INITIAL_URLBAR_SUGGEST_VALUE = Services.prefs.getBoolPref(URLBAR_SUGGEST_PREF_NAME);
 
   await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
 
   let doc = gBrowser.selectedBrowser.contentDocument;
   let urlbarBox = doc.getElementById("urlBarSuggestion");
   ok(!urlbarBox.disabled, "Checkbox should be enabled");
+  is(urlbarBox.checked, INITIAL_URLBAR_SUGGEST_VALUE,
+     "Checkbox should match initial pref value: " + INITIAL_URLBAR_SUGGEST_VALUE);
 
-  Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+  urlbarBox.doCommand();
+  is(urlbarBox.checked, !INITIAL_URLBAR_SUGGEST_VALUE,
+     "Checkbox should be flipped after clicking it");
+  let prefValue = Services.prefs.getBoolPref(URLBAR_SUGGEST_PREF_NAME);
+  is(prefValue, urlbarBox.checked, "Pref should match checkbox. Pref: " + prefValue);
 
+  urlbarBox.doCommand();
+  is(urlbarBox.checked, INITIAL_URLBAR_SUGGEST_VALUE,
+     "Checkbox should be back to initial value after clicking it");
+  prefValue = Services.prefs.getBoolPref(URLBAR_SUGGEST_PREF_NAME);
+  is(prefValue, urlbarBox.checked, "Pref should match checkbox. Pref: " + prefValue);
+
+  Services.prefs.setBoolPref(SUGGEST_PREF_NAME, false);
+  ok(!urlbarBox.checked, "Checkbox should now be unchecked");
   ok(urlbarBox.disabled, "Checkbox should be disabled");
 
   gBrowser.removeCurrentTab();
 });
 
 // Open with suggestions disabled
 add_task(async function() {
-  Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+  Services.prefs.setBoolPref(SUGGEST_PREF_NAME, false);
 
   await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
 
   let doc = gBrowser.selectedBrowser.contentDocument;
   let urlbarBox = doc.getElementById("urlBarSuggestion");
   ok(urlbarBox.disabled, "Checkbox should be disabled");
 
-  Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+  Services.prefs.setBoolPref(SUGGEST_PREF_NAME, true);
 
   ok(!urlbarBox.disabled, "Checkbox should be enabled");
 
   gBrowser.removeCurrentTab();
 });
-
-add_task(async function() {
-  Services.prefs.setBoolPref("browser.search.suggest.enabled", original);
-});
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -20,16 +20,22 @@ pref-page =
 # in order to make the entire placeholder string visible
 #
 # Notice: The value of the `.style` attribute is a CSS string, and the `width`
 # is the name of the CSS property. It is intended only to adjust the element's width.
 # Do not translate.
 search-input =
     .style = width: 15.4em
 
+policies-notice =
+    { PLATFORM() ->
+        [windows] Your organization has disabled the ability to change some options.
+       *[other] Your organization has disabled the ability to change some preferences.
+    }
+
 pane-general-title = General
 category-general =
     .tooltiptext = { pane-general-title }
 
 pane-search-title = Search
 category-search =
     .tooltiptext = { pane-search-title }
 
--- a/browser/locales/en-US/chrome/browser/migration/migration.dtd
+++ b/browser/locales/en-US/chrome/browser/migration/migration.dtd
@@ -16,16 +16,20 @@
 <!ENTITY importFromNothing.label        "Don’t import anything">
 <!ENTITY importFromNothing.accesskey    "D">
 <!ENTITY importFromSafari.label         "Safari">
 <!ENTITY importFromSafari.accesskey     "S">
 <!ENTITY importFromCanary.label         "Chrome Canary">
 <!ENTITY importFromCanary.accesskey     "n">
 <!ENTITY importFromChrome.label         "Chrome">
 <!ENTITY importFromChrome.accesskey     "C">
+<!ENTITY importFromChromeBeta.label     "Chrome Beta">
+<!ENTITY importFromChromeBeta.accesskey "B">
+<!ENTITY importFromChromeDev.label      "Chrome Dev">
+<!ENTITY importFromChromeDev.accesskey  "D">
 <!ENTITY importFromChromium.label       "Chromium">
 <!ENTITY importFromChromium.accesskey   "u">
 <!ENTITY importFromFirefox.label        "Firefox">
 <!ENTITY importFromFirefox.accesskey    "X">
 <!ENTITY importFrom360se.label          "360 Secure Browser">
 <!ENTITY importFrom360se.accesskey      "3">
 
 <!ENTITY noMigrationSources.label       "No programs that contain bookmarks, history or password data could be found.">
--- a/browser/locales/en-US/chrome/browser/migration/migration.properties
+++ b/browser/locales/en-US/chrome/browser/migration/migration.properties
@@ -5,16 +5,18 @@
 profileName_format=%S %S
 
 # Browser Specific
 sourceNameIE=Internet Explorer
 sourceNameEdge=Microsoft Edge
 sourceNameSafari=Safari
 sourceNameCanary=Google Chrome Canary
 sourceNameChrome=Google Chrome
+sourceNameChromeBeta=Google Chrome Beta
+sourceNameChromeDev=Google Chrome Dev
 sourceNameChromium=Chromium
 sourceNameFirefox=Mozilla Firefox
 sourceName360se=360 Secure Browser
 
 importedBookmarksFolder=From %S
 
 importedSafariReadingList=Reading List (From Safari)
 importedEdgeReadingList=Reading List (From Edge)
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.mPanelContainer.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.mPanelContainer.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") {