Merge autoland to mozilla-central. a=merge
authorGurzau Raul <rgurzau@mozilla.com>
Wed, 14 Mar 2018 00:39:00 +0200
changeset 461489 1bd1d50b1f3f53be310d20cc64e4d4e994b43940
parent 461454 00ef5405cd7e707a89568ec00120e5acac32b169 (current diff)
parent 461488 5fd7f1436e6d85bc41e91b32ac63ea63fdff3f2c (diff)
child 461520 c56ef1c14a555023949ad727c86e3c2df995edd2
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.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 autoland to mozilla-central. a=merge
testing/mozharness/mozharness/mozilla/signing.py
toolkit/components/resistfingerprinting/nsRFPService.cpp
--- a/.clang-format-ignore
+++ b/.clang-format-ignore
@@ -35,28 +35,31 @@ widget/tests/.*
 xpcom/glue/tests/.*
 xpcom/tests/.*
 
 # Generated from ./tools/rewriting/ThirdPartyPaths.txt
 # awk '{print ""$1".*"}' ./tools/rewriting/ThirdPartyPaths.txt
 browser/components/translation/cld2/.*
 browser/extensions/mortar/ppapi/.*
 db/sqlite3/src/.*
+devtools/client/sourceeditor/codemirror/.*
+devtools/client/sourceeditor/tern/.*
 extensions/spellcheck/hunspell/src/.*
 gfx/angle/.*
 gfx/cairo/.*
 gfx/graphite2/.*
 gfx/harfbuzz/.*
 gfx/ots/.*
 gfx/qcms/.*
 gfx/sfntly/.*
 gfx/skia/.*
 gfx/vr/openvr/.*
-gfx/webrender.*
-gfx/webrender_api.*
+gfx/webrender/.*
+gfx/webrender_api/.*
+gfx/wrench/.*
 gfx/ycbcr/.*
 intl/hyphenation/hyphen/.*
 intl/icu/.*
 ipc/chromium/.*
 js/src/ctypes/libffi/.*
 js/src/dtoa.c.*
 js/src/jit/arm64/vixl/.*
 media/ffvpx/.*
@@ -89,27 +92,27 @@ mobile/android/geckoview/src/thirdparty/
 mobile/android/thirdparty/.*
 modules/brotli/.*
 modules/fdlibm/.*
 modules/freetype2/.*
 modules/libbz2/.*
 modules/libmar/.*
 modules/pdfium/.*
 modules/woff2/.*
+modules/xz-embedded/.*
 modules/zlib/.*
 netwerk/sctp/src/.*
 netwerk/srtp/src/.*
 nsprpub/.*
 other-licenses/.*
 parser/expat/.*
 security/nss/.*
 security/sandbox/chromium/.*
 testing/gtest/gmock/.*
 testing/gtest/gtest/.*
-testing/talos/talos/tests/canvasmark/.*
 testing/talos/talos/tests/dromaeo/.*
 testing/talos/talos/tests/kraken/.*
 testing/talos/talos/tests/v8_7/.*
 third_party/aom/.*
 third_party/python/blessings/.*
 third_party/python/configobj/.*
 third_party/python/futures/.*
 third_party/python/jsmin/.*
@@ -129,9 +132,9 @@ third_party/python/six/.*
 third_party/python/which/.*
 third_party/rust/.*
 toolkit/components/jsoncpp/.*
 toolkit/components/lz4/.*
 toolkit/components/protobuf/.*
 toolkit/components/url-classifier/chromium/.*
 toolkit/components/url-classifier/protobuf/.*
 toolkit/crashreporter/google-breakpad/.*
-tools/fuzzing/libfuzzer.*
+tools/fuzzing/libfuzzer/.*
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -136,35 +136,16 @@ XPCOMUtils.defineLazyServiceGetters(this
 });
 
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
                                      "@mozilla.org/xre/app-info;1",
                                      "nsICrashReporter");
 }
 
-Object.defineProperty(this, "gBrowser", {
-  configurable: true,
-  enumerable: true,
-  get() {
-    delete window.gBrowser;
-
-    // The tabbed browser only exists in proper browser windows, but on Mac we
-    // load browser.js in other windows and might try to access gBrowser.
-    if (!window._gBrowser) {
-      return window.gBrowser = null;
-    }
-
-    window.gBrowser = window._gBrowser;
-    delete window._gBrowser;
-    gBrowser.init();
-    return gBrowser;
-  },
-});
-
 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) {
@@ -238,16 +219,17 @@ XPCOMUtils.defineLazyGetter(this, "Win7F
       }
     };
   }
   return null;
 });
 
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
+var gBrowser;
 var gLastValidURLStr = "";
 var gInPrintPreviewMode = false;
 var gContextMenu = null; // nsContextMenu instance
 var gMultiProcessBrowser =
   window.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIWebNavigation)
         .QueryInterface(Ci.nsILoadContext)
         .useRemoteTabs;
@@ -1203,16 +1185,20 @@ let _resolveDelayedStartup;
 var delayedStartupPromise = new Promise(resolve => {
   _resolveDelayedStartup = resolve;
 });
 
 var gBrowserInit = {
   delayedStartupFinished: false,
 
   onDOMContentLoaded() {
+    gBrowser = window._gBrowser;
+    delete window._gBrowser;
+    gBrowser.init();
+
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(nsIWebNavigation)
           .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIXULWindow)
           .XULBrowserWindow = window.XULBrowserWindow;
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
       new nsBrowserAccess();
@@ -5150,24 +5136,24 @@ var TabsProgressListener = {
           return;
         aBrowser.removeEventListener("pagehide", onPageHide, true);
         if (event.target.documentElement)
           event.target.documentElement.removeAttribute("hasBrowserHandlers");
       }, true);
     }
   },
 
-  onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI,
-                             aFlags) {
+  onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {
     // Filter out location changes caused by anchor navigation
     // or history.push/pop/replaceState.
     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
-      // Reader mode actually cares about these:
-      let mm = gBrowser.selectedBrowser.messageManager;
-      mm.sendAsyncMessage("Reader:PushState", {isArticle: gBrowser.selectedBrowser.isArticle});
+      // Reader mode cares about history.pushState and friends.
+      aBrowser.messageManager.sendAsyncMessage("Reader:PushState", {
+        isArticle: aBrowser.isArticle,
+      });
       return;
     }
 
     // Filter out location changes in sub documents.
     if (!aWebProgress.isTopLevel)
       return;
 
     // Only need to call locationChange if the PopupNotifications object
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -41,18 +41,18 @@
         }
 
         tabs.removeAttribute("overflow");
 
         if (tabs._lastTabClosedByMouse) {
           tabs._expandSpacerBy(this._scrollButtonDown.clientWidth);
         }
 
-        for (let tab of Array.from(tabs.tabbrowser._removingTabs)) {
-          tabs.tabbrowser.removeTab(tab);
+        for (let tab of Array.from(gBrowser._removingTabs)) {
+          gBrowser.removeTab(tab);
         }
 
         tabs._positionPinnedTabs();
       ]]></handler>
       <handler event="overflow"><![CDATA[
         // Ignore overflow events:
         // - from nested scrollable elements
         // - for vertical orientation
@@ -147,17 +147,17 @@
         <![CDATA[
           Services.prefs.removeObserver("privacy.userContext", this);
 
           CustomizableUI.removeListener(this);
         ]]>
       </destructor>
 
       <field name="tabbox" readonly="true">
-        this.tabbrowser.tabbox;
+        document.getElementById("tabbrowser-tabbox");
       </field>
 
       <field name="contextMenu" readonly="true">
         document.getElementById("tabContextMenu");
       </field>
 
       <field name="arrowScrollbox">
         document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox");
@@ -172,22 +172,16 @@
       <field name="restoreTabsButton">
         document.getAnonymousElementByAttribute(this, "anonid", "restore-tabs-button");
       </field>
       <field name="_restoreTabsButtonWrapperWidth">0</field>
       <field name="windowUtils">
         window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
       </field>
 
-      <property name="tabbrowser" readonly="true">
-        <getter>
-          return window.gBrowser;
-        </getter>
-      </property>
-
       <property name="_tabMinWidth">
         <setter>
           this.style.setProperty("--tab-min-width", val + "px");
           return val;
         </setter>
       </property>
 
       <property name="restoreTabsButtonWrapperWidth" readonly="true">
@@ -299,23 +293,33 @@
       </method>
 
       <property name="_isCustomizing" readonly="true">
         <getter><![CDATA[
           return document.documentElement.getAttribute("customizing") == "true";
         ]]></getter>
       </property>
 
+      <method name="_getVisibleTabs">
+        <body><![CDATA[
+          // Cannot access gBrowser before it's initialized.
+          if (!gBrowser) {
+            return [ this.firstChild ];
+          }
+
+          return gBrowser.visibleTabs;
+        ]]></body>
+      </method>
+
       <method name="_setPositionalAttributes">
         <body><![CDATA[
-          let visibleTabs = this.tabbrowser.visibleTabs;
-
-          if (!visibleTabs.length)
+          let visibleTabs = this._getVisibleTabs();
+          if (!visibleTabs.length) {
             return;
-
+          }
           let selectedIndex = visibleTabs.indexOf(this.selectedItem);
 
           if (this._beforeSelectedTab) {
             this._beforeSelectedTab.removeAttribute("beforeselected-visible");
           }
 
           if (this.selectedItem.closing || selectedIndex == 0) {
             this._beforeSelectedTab = null;
@@ -390,20 +394,21 @@
             gTabBrowserBundle.GetStringFromName(visible ? "tabs.closeTab" : "tabs.close"));
 
           TabsInTitlebar.allowedBy("tabs-visible", visible);
         ]]></body>
       </method>
 
       <method name="updateVisibility">
         <body><![CDATA[
-          if (this.childNodes.length - this.tabbrowser._removingTabs.length == 1)
+          if (this.childNodes.length - gBrowser._removingTabs.length == 1) {
             this.visible = window.toolbar.visible;
-          else
+          } else {
             this.visible = true;
+          }
         ]]></body>
       </method>
 
       <field name="_closeButtonsUpdatePending">false</field>
       <method name="_updateCloseButtons">
         <body><![CDATA[
           // If we're overflowing, tabs are at their minimum widths.
           if (this.getAttribute("overflow") == "true") {
@@ -432,17 +437,17 @@
               // Check if tab widths are below the threshold where we want to
               // remove close buttons from background tabs so that people don't
               // accidentally close tabs by selecting them.
               let rect = ele => {
                 return window.QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIDOMWindowUtils)
                              .getBoundsWithoutFlushing(ele);
               };
-              let tab = this.tabbrowser.visibleTabs[this.tabbrowser._numPinnedTabs];
+              let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
               if (tab && rect(tab).width <= this._tabClipWidth) {
                 this.setAttribute("closebuttons", "activetab");
               } else {
                 this.removeAttribute("closebuttons");
               }
             });
           });
         ]]></body>
@@ -465,94 +470,98 @@
       <field name="_tabDefaultMaxWidth">NaN</field>
       <field name="_lastTabClosedByMouse">false</field>
       <field name="_hasTabTempMaxWidth">false</field>
 
       <!-- Try to keep the active tab's close button under the mouse cursor -->
       <method name="_lockTabSizing">
         <parameter name="aTab"/>
         <body><![CDATA[
-          var tabs = this.tabbrowser.visibleTabs;
-          if (!tabs.length)
+          let tabs = this._getVisibleTabs();
+          if (!tabs.length) {
             return;
+          }
 
           var isEndTab = (aTab._tPos > tabs[tabs.length - 1]._tPos);
           var tabWidth = aTab.getBoundingClientRect().width;
 
-          if (!this._tabDefaultMaxWidth)
+          if (!this._tabDefaultMaxWidth) {
             this._tabDefaultMaxWidth =
               parseFloat(window.getComputedStyle(aTab).maxWidth);
+          }
           this._lastTabClosedByMouse = true;
 
           if (this.getAttribute("overflow") == "true") {
             // Don't need to do anything if we're in overflow mode and aren't scrolled
             // all the way to the right, or if we're closing the last tab.
-            if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled)
+            if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
               return;
-
+            }
             // If the tab has an owner that will become the active tab, the owner will
             // be to the left of it, so we actually want the left tab to slide over.
             // This can't be done as easily in non-overflow mode, so we don't bother.
-            if (aTab.owner)
+            if (aTab.owner) {
               return;
-
+            }
             this._expandSpacerBy(tabWidth);
           } else { // non-overflow mode
             // Locking is neither in effect nor needed, so let tabs expand normally.
-            if (isEndTab && !this._hasTabTempMaxWidth)
+            if (isEndTab && !this._hasTabTempMaxWidth) {
               return;
-
-            let numPinned = this.tabbrowser._numPinnedTabs;
+            }
+            let numPinned = gBrowser._numPinnedTabs;
             // Force tabs to stay the same width, unless we're closing the last tab,
             // which case we need to let them expand just enough so that the overall
             // tabbar width is the same.
             if (isEndTab) {
               let numNormalTabs = tabs.length - numPinned;
               tabWidth = tabWidth * (numNormalTabs + 1) / numNormalTabs;
-              if (tabWidth > this._tabDefaultMaxWidth)
+              if (tabWidth > this._tabDefaultMaxWidth) {
                 tabWidth = this._tabDefaultMaxWidth;
+              }
             }
             tabWidth += "px";
             for (let i = numPinned; i < tabs.length; i++) {
               let tab = tabs[i];
               tab.style.setProperty("max-width", tabWidth, "important");
               if (!isEndTab) { // keep tabs the same width
                 tab.style.transition = "none";
                 tab.clientTop; // flush styles to skip animation; see bug 649247
                 tab.style.transition = "";
               }
             }
             this._hasTabTempMaxWidth = true;
-            this.tabbrowser.addEventListener("mousemove", this);
+            gBrowser.addEventListener("mousemove", this);
             window.addEventListener("mouseout", this);
           }
         ]]></body>
       </method>
 
       <method name="_expandSpacerBy">
         <parameter name="pixels"/>
         <body><![CDATA[
           let spacer = this._closingTabsSpacer;
           spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
           this.setAttribute("using-closing-tabs-spacer", "true");
-          this.tabbrowser.addEventListener("mousemove", this);
+          gBrowser.addEventListener("mousemove", this);
           window.addEventListener("mouseout", this);
         ]]></body>
       </method>
 
       <method name="_unlockTabSizing">
         <body><![CDATA[
-          this.tabbrowser.removeEventListener("mousemove", this);
+          gBrowser.removeEventListener("mousemove", this);
           window.removeEventListener("mouseout", this);
 
           if (this._hasTabTempMaxWidth) {
             this._hasTabTempMaxWidth = false;
-            let tabs = this.tabbrowser.visibleTabs;
-            for (let i = 0; i < tabs.length; i++)
+            let tabs = this._getVisibleTabs();
+            for (let i = 0; i < tabs.length; i++) {
               tabs[i].style.maxWidth = "";
+            }
           }
 
           if (this.hasAttribute("using-closing-tabs-spacer")) {
             this.removeAttribute("using-closing-tabs-spacer");
             this._closingTabsSpacer.style.width = 0;
           }
         ]]></body>
       </method>
@@ -564,19 +573,19 @@
           this._handleTabSelect(true);
         ]]></body>
       </method>
 
       <field name="_lastNumPinned">0</field>
       <field name="_pinnedTabsLayoutCache">null</field>
       <method name="_positionPinnedTabs">
         <body><![CDATA[
-          var numPinned = this.tabbrowser._numPinnedTabs;
-          var doPosition = this.getAttribute("overflow") == "true" &&
-                           this.tabbrowser.visibleTabs.length > numPinned &&
+          let numPinned = gBrowser._numPinnedTabs;
+          let doPosition = this.getAttribute("overflow") == "true" &&
+                           this._getVisibleTabs().length > numPinned &&
                            numPinned > 0;
 
           if (doPosition) {
             this.setAttribute("positionpinnedtabs", "true");
 
             let layoutData = this._pinnedTabsLayoutCache;
             let uiDensity = document.documentElement.getAttribute("uidensity");
             if (!layoutData ||
@@ -633,33 +642,35 @@
           let screenX = event.screenX;
           if (screenX == draggedTab._dragData.animLastScreenX)
             return;
 
           draggedTab._dragData.animLastScreenX = screenX;
 
           let rtl = (window.getComputedStyle(this).direction == "rtl");
           let pinned = draggedTab.pinned;
-          let numPinned = this.tabbrowser._numPinnedTabs;
-          let tabs = this.tabbrowser.visibleTabs
-                                    .slice(pinned ? 0 : numPinned,
-                                           pinned ? numPinned : undefined);
-          if (rtl)
+          let numPinned = gBrowser._numPinnedTabs;
+          let tabs = this._getVisibleTabs()
+                         .slice(pinned ? 0 : numPinned,
+                                pinned ? numPinned : undefined);
+          if (rtl) {
             tabs.reverse();
+          }
           let tabWidth = draggedTab.getBoundingClientRect().width;
           draggedTab._dragData.tabWidth = tabWidth;
 
           // Move the dragged tab based on the mouse position.
 
           let leftTab = tabs[0];
           let rightTab = tabs[tabs.length - 1];
           let tabScreenX = draggedTab.boxObject.screenX;
           let translateX = screenX - draggedTab._dragData.screenX;
-          if (!pinned)
+          if (!pinned) {
             translateX += this.arrowScrollbox._scrollbox.scrollLeft - draggedTab._dragData.scrollX;
+          }
           let leftBound = leftTab.boxObject.screenX - tabScreenX;
           let rightBound = (rightTab.boxObject.screenX + rightTab.boxObject.width) -
                            (tabScreenX + tabWidth);
           translateX = Math.max(translateX, leftBound);
           translateX = Math.min(translateX, rightBound);
           draggedTab.style.transform = "translateX(" + translateX + "px)";
           draggedTab._dragData.translateX = translateX;
 
@@ -715,21 +726,23 @@
               return rtl ? tabWidth : -tabWidth;
             return 0;
           }
         ]]></body>
       </method>
 
       <method name="_finishAnimateTabMove">
         <body><![CDATA[
-          if (this.getAttribute("movingtab") != "true")
+          if (this.getAttribute("movingtab") != "true") {
             return;
+          }
 
-          for (let tab of this.tabbrowser.visibleTabs)
+          for (let tab of this._getVisibleTabs()) {
             tab.style.transform = "";
+          }
 
           this.removeAttribute("movingtab");
           this.parentNode.removeAttribute("movingtab");
 
           this._handleTabSelect();
         ]]></body>
       </method>
 
@@ -878,20 +891,21 @@
           }
           return "none";
         ]]></body>
       </method>
 
       <method name="_handleNewTab">
         <parameter name="tab"/>
         <body><![CDATA[
-          if (tab.parentNode != this)
+          if (tab.parentNode != this) {
             return;
+          }
           tab._fullyOpen = true;
-          this.tabbrowser.tabAnimationsInProgress--;
+          gBrowser.tabAnimationsInProgress--;
 
           this._updateCloseButtons();
 
           if (tab.getAttribute("selected") == "true") {
             this._handleTabSelect();
           } else if (!tab.hasAttribute("skipbackgroundnotify")) {
             this._notifyBackgroundTab(tab);
           }
@@ -899,39 +913,46 @@
           // XXXmano: this is a temporary workaround for bug 345399
           // We need to manually update the scroll buttons disabled state
           // if a tab was inserted to the overflow area or removed from it
           // without any scrolling and when the tabbar has already
           // overflowed.
           this.arrowScrollbox._updateScrollButtonsDisabledState();
 
           // Preload the next about:newtab if there isn't one already.
-          this.tabbrowser._createPreloadBrowser();
+          gBrowser._createPreloadBrowser();
         ]]></body>
       </method>
 
       <method name="_canAdvanceToTab">
         <parameter name="aTab"/>
         <body>
         <![CDATA[
           return !aTab.closing;
         ]]>
         </body>
       </method>
 
       <method name="getRelatedElement">
         <parameter name="aTab"/>
         <body>
         <![CDATA[
-          if (!aTab)
+          if (!aTab) {
             return null;
+          }
+
+          // Cannot access gBrowser before it's initialized.
+          if (!gBrowser) {
+            return this.tabbox.tabpanels.firstChild;
+          }
+
           // If the tab's browser is lazy, we need to `_insertBrowser` in order
           // to have a linkedPanel.  This will also serve to bind the browser
           // and make it ready to use when the tab is selected.
-          this.tabbrowser._insertBrowser(aTab);
+          gBrowser._insertBrowser(aTab);
           return document.getElementById(aTab.linkedPanel);
         ]]>
         </body>
       </method>
 
       <method name="_updateNewTabVisibility">
         <body><![CDATA[
           // Helper functions to help deal with customize mode wrapping some items
@@ -983,28 +1004,30 @@
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="TabSelect" action="this._handleTabSelect();"/>
 
       <handler event="transitionend"><![CDATA[
-        if (event.propertyName != "max-width")
+        if (event.propertyName != "max-width") {
           return;
+        }
 
         var tab = event.target;
 
         if (tab.getAttribute("fadein") == "true") {
-          if (tab._fullyOpen)
+          if (tab._fullyOpen) {
             this._updateCloseButtons();
-          else
+          } else {
             this._handleNewTab(tab);
+          }
         } else if (tab.closing) {
-          this.tabbrowser._endRemoveTab(tab);
+          gBrowser._endRemoveTab(tab);
         }
       ]]></handler>
 
       <handler event="dblclick"><![CDATA[
         // When the tabbar has an unified appearance with the titlebar
         // and menubar, a double-click in it should have the same behavior
         // as double-clicking the titlebar
         if (TabsInTitlebar.enabled || this.parentNode._dragBindingAlive)
@@ -1070,32 +1093,35 @@
             return;
           }
           delete this._clickedTabBarOnce;
           this._blockDblClick = false;
         }
       ]]></handler>
 
       <handler event="click"><![CDATA[
-        if (event.button != 1)
+        if (event.button != 1) {
           return;
+        }
 
         if (event.target.localName == "tab") {
-          this.tabbrowser.removeTab(event.target, {animate: true,
-                byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE});
+          gBrowser.removeTab(event.target, {
+            animate: true,
+            byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
+          });
         } else if (event.originalTarget.localName == "box") {
           // The user middleclicked an open space on the tabstrip. This could
           // be because they intend to open a new tab, but it could also be
           // because they just removed a tab and they now middleclicked on the
           // resulting space while that tab is closing. In that case, we don't
           // want to open a tab. So if we're removing one or more tabs, and
           // the tab click is before the end of the last visible tab, we do
           // nothing.
-          if (this.tabbrowser._removingTabs.length) {
-            let visibleTabs = this.tabbrowser.visibleTabs;
+          if (gBrowser._removingTabs.length) {
+            let visibleTabs = this._getVisibleTabs();
             let ltr = (window.getComputedStyle(this).direction == "ltr");
             let lastTab = visibleTabs[visibleTabs.length - 1];
             let endOfTab = lastTab.getBoundingClientRect()[ltr ? "right" : "left"];
             if ((ltr && event.clientX > endOfTab) ||
                 (!ltr && event.clientX < endOfTab)) {
               BrowserOpenTab();
             }
           } else {
@@ -1122,30 +1148,30 @@
         if (wrongModifiers)
           return;
 
         // Don't check if the event was already consumed because tab navigation
         // should work always for better user experience.
 
         switch (event.keyCode) {
           case KeyEvent.DOM_VK_UP:
-            this.tabbrowser.moveTabBackward();
+            gBrowser.moveTabBackward();
             break;
           case KeyEvent.DOM_VK_DOWN:
-            this.tabbrowser.moveTabForward();
+            gBrowser.moveTabForward();
             break;
           case KeyEvent.DOM_VK_RIGHT:
           case KeyEvent.DOM_VK_LEFT:
-            this.tabbrowser.moveTabOver(event);
+            gBrowser.moveTabOver(event);
             break;
           case KeyEvent.DOM_VK_HOME:
-            this.tabbrowser.moveTabToStart();
+            gBrowser.moveTabToStart();
             break;
           case KeyEvent.DOM_VK_END:
-            this.tabbrowser.moveTabToEnd();
+            gBrowser.moveTabToEnd();
             break;
           default:
             // Consume the keydown event for the above keyboard
             // shortcuts only.
             return;
         }
         event.preventDefault();
       ]]></handler>
@@ -1354,64 +1380,67 @@
             return;
         }
 
         this._tabDropIndicator.collapsed = true;
         event.stopPropagation();
         if (draggedTab && dropEffect == "copy") {
           // copy the dropped tab (wherever it's from)
           let newIndex = this._getDropIndex(event, false);
-          let newTab = this.tabbrowser.duplicateTab(draggedTab);
-          this.tabbrowser.moveTabTo(newTab, newIndex);
-          if (draggedTab.parentNode != this || event.shiftKey)
+          let newTab = gBrowser.duplicateTab(draggedTab);
+          gBrowser.moveTabTo(newTab, newIndex);
+          if (draggedTab.parentNode != this || event.shiftKey) {
             this.selectedItem = newTab;
+          }
         } else if (draggedTab && draggedTab.parentNode == this) {
           let oldTranslateX = Math.round(draggedTab._dragData.translateX);
           let tabWidth = Math.round(draggedTab._dragData.tabWidth);
           let translateOffset = oldTranslateX % tabWidth;
           let newTranslateX = oldTranslateX - translateOffset;
           if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
             newTranslateX += tabWidth;
           } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
             newTranslateX -= tabWidth;
           }
 
           let dropIndex = "animDropIndex" in draggedTab._dragData &&
                           draggedTab._dragData.animDropIndex;
           if (dropIndex && dropIndex > draggedTab._tPos)
             dropIndex--;
 
-          let animate = this.tabbrowser.animationsEnabled;
+          let animate = gBrowser.animationsEnabled;
           if (oldTranslateX && oldTranslateX != newTranslateX && animate) {
             draggedTab.setAttribute("tabdrop-samewindow", "true");
             draggedTab.style.transform = "translateX(" + newTranslateX + "px)";
             let onTransitionEnd = transitionendEvent => {
               if (transitionendEvent.propertyName != "transform" ||
                   transitionendEvent.originalTarget != draggedTab) {
                 return;
               }
               draggedTab.removeEventListener("transitionend", onTransitionEnd);
 
               draggedTab.removeAttribute("tabdrop-samewindow");
 
               this._finishAnimateTabMove();
-              if (dropIndex !== false)
-                this.tabbrowser.moveTabTo(draggedTab, dropIndex);
+              if (dropIndex !== false) {
+                gBrowser.moveTabTo(draggedTab, dropIndex);
+              }
 
-              this.tabbrowser.syncThrobberAnimations(draggedTab);
+              gBrowser.syncThrobberAnimations(draggedTab);
             };
             draggedTab.addEventListener("transitionend", onTransitionEnd);
           } else {
             this._finishAnimateTabMove();
-            if (dropIndex !== false)
-              this.tabbrowser.moveTabTo(draggedTab, dropIndex);
+            if (dropIndex !== false) {
+              gBrowser.moveTabTo(draggedTab, dropIndex);
+            }
           }
         } else if (draggedTab) {
           let newIndex = this._getDropIndex(event, false);
-          this.tabbrowser.adoptTab(draggedTab, newIndex, true);
+          gBrowser.adoptTab(draggedTab, newIndex, true);
         } else {
           // Pass true to disallow dropping javascript: or data: urls
           let links;
           try {
             links = browserDragAndDrop.dropLinks(event, true);
           } catch (ex) {}
 
           if (!links || links.length === 0)
@@ -1434,17 +1463,17 @@
               // Sync dialog cannot be used inside drop event handler.
               let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(urls.length,
                                                                           window);
               if (!answer) {
                 return;
               }
             }
 
-            this.tabbrowser.loadTabs(urls, {
+            gBrowser.loadTabs(urls, {
               inBackground,
               replace,
               allowThirdPartyFixup: true,
               targetTab,
               newIndex,
               userContextId,
               triggeringPrincipal,
             });
@@ -1515,30 +1544,30 @@
         var winHeight = Math.min(window.outerHeight, availHeight.value);
         var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, availX.value),
                             availX.value + availWidth.value - winWidth);
         var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, availY.value),
                            availY.value + availHeight.value - winHeight);
 
         delete draggedTab._dragData;
 
-        if (this.tabbrowser.tabs.length == 1) {
+        if (gBrowser.tabs.length == 1) {
           // resize _before_ move to ensure the window fits the new screen.  if
           // the window is too large for its screen, the window manager may do
           // automatic repositioning.
           window.resizeTo(winWidth, winHeight);
           window.moveTo(left, top);
           window.focus();
         } else {
           let props = { screenX: left, screenY: top, suppressanimation: 1 };
           if (AppConstants.platform != "win") {
             props.outerWidth = winWidth;
             props.outerHeight = winHeight;
           }
-          this.tabbrowser.replaceTabWithWindow(draggedTab, props);
+          gBrowser.replaceTabWithWindow(draggedTab, props);
         }
         event.stopPropagation();
       ]]></handler>
 
       <handler event="dragexit"><![CDATA[
         this._dragTime = 0;
 
         // This does not work at all (see bug 458613)
@@ -1617,21 +1646,26 @@
         if (!("_lastAccessed" in this)) {
           this.updateLastAccessed();
         }
       ]]></constructor>
 
       <property name="_visuallySelected">
         <setter>
           <![CDATA[
-          if (val)
+          if (val == (this.getAttribute("visuallyselected") == "true")) {
+            return val;
+          }
+
+          if (val) {
             this.setAttribute("visuallyselected", "true");
-          else
+          } else {
             this.removeAttribute("visuallyselected");
-          this.parentNode.tabbrowser._tabAttrModified(this, ["visuallyselected"]);
+          }
+          gBrowser._tabAttrModified(this, ["visuallyselected"]);
 
           return val;
           ]]>
         </setter>
       </property>
 
       <property name="_selected">
         <setter>
@@ -1735,21 +1769,22 @@
       While it would make sense to track this in a field, the field will get nuked
       once the node is gone from the DOM, which causes us to think the tab is not
       closed, which causes us to make wrong decisions. So we use an expando instead.
       <field name="closing">false</field>
       -->
 
       <method name="_mouseenter">
         <body><![CDATA[
-          if (this.hidden || this.closing)
+          if (this.hidden || this.closing) {
             return;
+          }
 
           let tabContainer = this.parentNode;
-          let visibleTabs = tabContainer.tabbrowser.visibleTabs;
+          let visibleTabs = tabContainer._getVisibleTabs();
           let tabIndex = visibleTabs.indexOf(this);
 
           if (this.selected)
             tabContainer._handleTabSelect();
 
           if (tabIndex == 0) {
             tabContainer._beforeHoveredTab = null;
           } else {
@@ -1779,19 +1814,19 @@
             this.startUnselectedTabHoverTimer();
           }
 
           // Prepare connection to host beforehand.
           SessionStore.speculativeConnectOnTabHover(this);
 
           let tabToWarm = this;
           if (this.mOverCloseButton) {
-            tabToWarm = tabContainer.tabbrowser._findTabToBlurTo(this);
+            tabToWarm = gBrowser._findTabToBlurTo(this);
           }
-          tabContainer.tabbrowser.warmupTab(tabToWarm);
+          gBrowser.warmupTab(tabToWarm);
         ]]></body>
       </method>
 
       <method name="_mouseleave">
         <body><![CDATA[
           let tabContainer = this.parentNode;
           if (tabContainer._beforeHoveredTab) {
             tabContainer._beforeHoveredTab.removeAttribute("beforehovered");
@@ -1866,50 +1901,43 @@
           TelemetryStopwatch.finish("TAB_MEDIA_BLOCKING_TIME_MS", this);
         ]]></body>
       </method>
 
       <method name="toggleMuteAudio">
         <parameter name="aMuteReason"/>
         <body>
         <![CDATA[
-          let tabContainer = this.parentNode;
           let browser = this.linkedBrowser;
           let modifiedAttrs = [];
           let hist = Services.telemetry.getHistogramById("TAB_AUDIO_INDICATOR_USED");
 
           if (this.hasAttribute("activemedia-blocked")) {
             this.removeAttribute("activemedia-blocked");
             modifiedAttrs.push("activemedia-blocked");
 
             browser.resumeMedia();
             hist.add(3 /* unblockByClickingIcon */);
             this.finishMediaBlockTimer();
           } else {
             if (browser.audioMuted) {
-              if (this.linkedPanel) {
-                // "Lazy Browser" should not invoke its unmute method
-                browser.unmute();
-              }
+              browser.unmute();
               this.removeAttribute("muted");
               BrowserUITelemetry.countTabMutingEvent("unmute", aMuteReason);
               hist.add(1 /* unmute */);
             } else {
-              if (this.linkedPanel) {
-                // "Lazy Browser" should not invoke its mute method
-                browser.mute();
-              }
+              browser.mute();
               this.setAttribute("muted", "true");
               BrowserUITelemetry.countTabMutingEvent("mute", aMuteReason);
               hist.add(0 /* mute */);
             }
             this.muteReason = aMuteReason || null;
             modifiedAttrs.push("muted");
           }
-          tabContainer.tabbrowser._tabAttrModified(this, modifiedAttrs);
+          gBrowser._tabAttrModified(this, modifiedAttrs);
         ]]>
         </body>
       </method>
 
       <method name="setUserContextId">
         <parameter name="aUserContextId"/>
         <body>
         <![CDATA[
@@ -1974,22 +2002,23 @@
 
       <handler event="click" button="0"><![CDATA[
         if (this._overPlayingIcon) {
           this.toggleMuteAudio();
           return;
         }
 
         if (event.originalTarget.getAttribute("anonid") == "close-button") {
-          let tabContainer = this.parentNode;
-          tabContainer.tabbrowser.removeTab(this, {animate: true,
-                  byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE});
+          gBrowser.removeTab(this, {
+            animate: true,
+            byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
+          });
           // This enables double-click protection for the tab container
           // (see tabbrowser-tabs 'click' handler).
-          tabContainer._blockDblClick = true;
+          gBrowser.tabContainer._blockDblClick = true;
         }
       ]]></handler>
 
       <handler event="dblclick" button="0" phase="capturing"><![CDATA[
         // for the one-close-button case
         if (event.originalTarget.getAttribute("anonid") == "close-button") {
           event.stopPropagation();
         }
--- a/browser/base/content/test/performance/browser.ini
+++ b/browser/base/content/test/performance/browser.ini
@@ -25,12 +25,13 @@ run-if = debug || devedition || nightly_
 [browser_tabopen_squeeze_reflows.js]
 [browser_tabstrip_overflow_underflow_reflows.js]
 [browser_tabswitch_reflows.js]
 [browser_toolbariconcolor_restyles.js]
 [browser_urlbar_keyed_search_reflows.js]
 skip-if = (os == 'linux') || (os == 'win' && debug) # Disabled on Linux and Windows debug due to perma failures. Bug 1392320.
 [browser_urlbar_search_reflows.js]
 skip-if = (debug || ccov) && (os == 'linux' || os == 'win') # Disabled on Linux and Windows debug and ccov due to intermittent timeouts. Bug 1414126, bug 1426611.
+[browser_window_resize_reflows.js]
 [browser_windowclose_reflows.js]
 [browser_windowopen_flicker.js]
 skip-if = (debug && os == 'win') # Disabled on windows debug for intermittent leaks
 [browser_windowopen_reflows.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/performance/browser_window_resize_reflows.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS. This
+ * is a whitelist that should slowly go away as we improve the performance of
+ * the front-end. Instead of adding more reflows to the whitelist, you should
+ * be modifying your code to avoid the reflow.
+ *
+ * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+  {
+    stack: [
+      "onOverflow@resource:///modules/CustomizableUI.jsm",
+    ],
+    maxCount: 48,
+  },
+
+  {
+    stack: [
+      "_moveItemsBackToTheirOrigin@resource:///modules/CustomizableUI.jsm",
+      "_onLazyResize@resource:///modules/CustomizableUI.jsm",
+    ],
+    maxCount: 5,
+  },
+
+  {
+    stack: [
+      "_onLazyResize@resource:///modules/CustomizableUI.jsm",
+    ],
+    maxCount: 4,
+  },
+];
+
+const gToolbar = document.getElementById("PersonalToolbar");
+
+/**
+ * Sets the visibility state on the Bookmarks Toolbar, and
+ * waits for it to transition to fully visible.
+ *
+ * @param visible (bool)
+ *        Whether or not the bookmarks toolbar should be made visible.
+ * @returns Promise
+ */
+async function toggleBookmarksToolbar(visible) {
+  let transitionPromise =
+    BrowserTestUtils.waitForEvent(gToolbar, "transitionend",
+                                  e => e.propertyName == "max-height");
+
+  setToolbarVisibility(gToolbar, visible);
+  await transitionPromise;
+}
+
+/**
+ * Resizes a browser window to a particular width and height, and
+ * waits for it to reach a "steady state" with respect to its overflowing
+ * toolbars.
+ * @param win (browser window)
+ *        The window to resize.
+ * @param width (int)
+ *        The width to resize the window to.
+ * @param height (int)
+ *        The height to resize the window to.
+ * @returns Promise
+ */
+async function resizeWindow(win, width, height) {
+  let toolbarEvent =
+    BrowserTestUtils.waitForEvent(win, "BookmarksToolbarVisibilityUpdated");
+  let resizeEvent =
+    BrowserTestUtils.waitForEvent(win, "resize");
+  let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils);
+  dwu.ensureDirtyRootFrame();
+  win.resizeTo(width, height);
+  await resizeEvent;
+  forceImmediateToolbarOverflowHandling(win);
+  await toolbarEvent;
+}
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when resizing windows.
+ */
+add_task(async function() {
+  const BOOKMARKS_COUNT = 150;
+  const STARTING_WIDTH = 600;
+  const STARTING_HEIGHT = 400;
+  const SMALL_WIDTH = 150;
+  const SMALL_HEIGHT = 150;
+
+  await PlacesUtils.bookmarks.eraseEverything();
+
+  // Add a bunch of bookmarks to display in the Bookmarks toolbar
+  await PlacesUtils.bookmarks.insertTree({
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    children: Array(BOOKMARKS_COUNT).fill("")
+                                    .map((_, i) => ({ url: `http://test.places.${i}/`}))
+  });
+
+  let wasCollapsed = gToolbar.collapsed;
+  Assert.ok(wasCollapsed, "The toolbar is collapsed by default");
+  if (wasCollapsed) {
+    let promiseReady =
+      BrowserTestUtils.waitForEvent(gToolbar, "BookmarksToolbarVisibilityUpdated");
+    await toggleBookmarksToolbar(true);
+    await promiseReady;
+  }
+
+  registerCleanupFunction(async () => {
+    if (wasCollapsed) {
+      await toggleBookmarksToolbar(false);
+    }
+    await PlacesUtils.bookmarks.eraseEverything();
+    await PlacesUtils.history.clear();
+  });
+
+  let win = await prepareSettledWindow();
+
+  if (win.screen.availWidth < STARTING_WIDTH ||
+      win.screen.availHeight < STARTING_HEIGHT) {
+    Assert.ok(false, "This test is running on too small a display - " +
+              `(${STARTING_WIDTH}x${STARTING_HEIGHT} min)`);
+    return;
+  }
+
+  await resizeWindow(win, STARTING_WIDTH, STARTING_HEIGHT);
+
+  await withReflowObserver(async function() {
+    await resizeWindow(win, SMALL_WIDTH, SMALL_HEIGHT);
+    await resizeWindow(win, STARTING_WIDTH, STARTING_HEIGHT);
+  }, EXPECTED_REFLOWS, win);
+
+  await BrowserTestUtils.closeWindow(win);
+});
--- a/browser/base/content/test/performance/head.js
+++ b/browser/base/content/test/performance/head.js
@@ -194,28 +194,43 @@ async function ensureNoPreloadedBrowser(
                              .getService(Ci.nsIAboutNewTabService);
   aboutNewTabService.newTabURL = "about:blank";
 
   registerCleanupFunction(() => {
     aboutNewTabService.resetNewTabURL();
   });
 }
 
+/**
+ * The navigation toolbar is overflowable, meaning that some items
+ * will be moved and held within a sub-panel if the window gets too
+ * small to show their icons. The calculation for hiding those items
+ * occurs after resize events, and is debounced using a DeferredTask.
+ * This utility function allows us to fast-forward to just running
+ * that function for that DeferredTask instead of waiting for the
+ * debounce timeout to occur.
+ */
+function forceImmediateToolbarOverflowHandling(win) {
+  let overflowableToolbar = win.document.getElementById("nav-bar").overflowable;
+  if (overflowableToolbar._lazyResizeHandler && overflowableToolbar._lazyResizeHandler.isArmed) {
+    overflowableToolbar._lazyResizeHandler.disarm();
+    // Ensure the root frame is dirty before resize so that, if we're
+    // in the middle of a reflow test, we record the reflows deterministically.
+    let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                 .getInterface(Ci.nsIDOMWindowUtils);
+    dwu.ensureDirtyRootFrame();
+    overflowableToolbar._onLazyResize();
+  }
+}
+
 async function prepareSettledWindow() {
   let win = await BrowserTestUtils.openNewBrowserWindow();
 
   await ensureNoPreloadedBrowser(win);
-
-  let overflowableToolbar = win.document.getElementById("nav-bar").overflowable;
-  if (overflowableToolbar._lazyResizeHandler && overflowableToolbar._lazyResizeHandler.isArmed) {
-    info("forcing deferred overflow handling of the navigation toolbar to happen immediately");
-    overflowableToolbar._lazyResizeHandler.disarm();
-    overflowableToolbar._onLazyResize();
-  }
-
+  forceImmediateToolbarOverflowHandling(win);
   return win;
 }
 
 /**
  * Calculate and return how many additional tabs can be fit into the
  * tabstrip without causing it to overflow.
  *
  * @return int
--- a/browser/components/customizableui/test/browser_remote_tabs_button.js
+++ b/browser/components/customizableui/test/browser_remote_tabs_button.js
@@ -41,16 +41,20 @@ add_task(async function testSyncRemoteTa
   ok(remoteTabsPanel.getAttribute("visible"), "Sync Panel is in view");
 
   // Find and click the "setup" button.
   let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
   syncNowButton.click();
   info("The sync now button was clicked");
 
   await waitForCondition(() => syncWasCalled);
+
+  // We need to stop the Syncing animation manually otherwise the button
+  // will be disabled at the beginning of a next test.
+  gSync._onActivityStop();
 });
 
 add_task(async function asyncCleanup() {
   // reset the panel UI to the default state
   await resetCustomization();
   ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
 
   if (isOverflowOpen()) {
@@ -64,20 +68,20 @@ add_task(async function asyncCleanup() {
 
 function mockFunctions() {
   // mock UIState.get()
   UIState.get = () => ({
     status: UIState.STATUS_SIGNED_IN,
     email: "user@mozilla.com"
   });
 
-  service.sync = mocked_syncAndReportErrors;
+  service.sync = mocked_sync;
 }
 
-function mocked_syncAndReportErrors() {
+function mocked_sync() {
   syncWasCalled = true;
 }
 
 function restoreValues() {
   UIState.get = getState;
   service.sync = originalSync;
 }
 
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -1,14 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-browser.js */
+/* import-globals-from ../../../toolkit/components/extensions/ext-tabs-base.js */
 
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "PromiseUtils",
                                "resource://gre/modules/PromiseUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 ChromeUtils.defineModuleGetter(this, "SessionStore",
@@ -92,16 +93,218 @@ let tabListener = {
         this.initTabReady();
         this.tabReadyPromises.set(nativeTab, deferred);
       }
     }
     return deferred.promise;
   },
 };
 
+const allAttrs = new Set(["audible", "favIconUrl", "mutedInfo", "sharingState", "title"]);
+const allProperties = new Set([
+  "audible",
+  "discarded",
+  "favIconUrl",
+  "hidden",
+  "isarticle",
+  "mutedInfo",
+  "pinned",
+  "sharingState",
+  "status",
+  "title",
+]);
+const restricted = new Set(["url", "favIconUrl", "title"]);
+
+class TabsUpdateFilterEventManager extends EventManager {
+  constructor(context, eventName) {
+    let {extension} = context;
+    let {tabManager} = extension;
+
+    let register = (fire, filterProps) => {
+      let filter = {...filterProps};
+      if (filter.urls) {
+        filter.urls = new MatchPatternSet(filter.urls);
+      }
+      let needsModified = true;
+      if (filter.properties) {
+        // Default is to listen for all events.
+        needsModified = filter.properties.some(p => allAttrs.has(p));
+        filter.properties = new Set(filter.properties);
+      } else {
+        filter.properties = allProperties;
+      }
+
+      function sanitize(extension, changeInfo) {
+        let result = {};
+        let nonempty = false;
+        let hasTabs = extension.hasPermission("tabs");
+        for (let prop in changeInfo) {
+          if (hasTabs || !restricted.has(prop)) {
+            nonempty = true;
+            result[prop] = changeInfo[prop];
+          }
+        }
+        return nonempty && result;
+      }
+
+      function getWindowID(windowId) {
+        if (windowId === WINDOW_ID_CURRENT) {
+          return windowTracker.getId(windowTracker.topWindow);
+        }
+        return windowId;
+      }
+
+      function matchFilters(tab, changed) {
+        if (!filterProps) {
+          return true;
+        }
+        if (filter.tabId != null && tab.id != filter.tabId) {
+          return false;
+        }
+        if (filter.windowId != null && tab.windowId != getWindowID(filter.windowId)) {
+          return false;
+        }
+        if (filter.urls) {
+          // We check permission first because tab.uri is null if !hasTabPermission.
+          return tab.hasTabPermission && filter.urls.matches(tab.uri);
+        }
+        return true;
+      }
+
+      let fireForTab = (tab, changed) => {
+        if (!matchFilters(tab, changed)) {
+          return;
+        }
+
+        let changeInfo = sanitize(extension, changed);
+        if (changeInfo) {
+          fire.async(tab.id, changeInfo, tab.convert());
+        }
+      };
+
+      let listener = event => {
+        let needed = [];
+        if (event.type == "TabAttrModified") {
+          let changed = event.detail.changed;
+          if (changed.includes("image") && filter.properties.has("favIconUrl")) {
+            needed.push("favIconUrl");
+          }
+          if (changed.includes("muted") && filter.properties.has("mutedInfo")) {
+            needed.push("mutedInfo");
+          }
+          if (changed.includes("soundplaying") && filter.properties.has("audible")) {
+            needed.push("audible");
+          }
+          if (changed.includes("label") && filter.properties.has("title")) {
+            needed.push("title");
+          }
+          if (changed.includes("sharing") && filter.properties.has("sharingState")) {
+            needed.push("sharingState");
+          }
+        } else if (event.type == "TabPinned") {
+          needed.push("pinned");
+        } else if (event.type == "TabUnpinned") {
+          needed.push("pinned");
+        } else if (event.type == "TabBrowserInserted" &&
+                   !event.detail.insertedOnTabCreation) {
+          needed.push("discarded");
+        } else if (event.type == "TabBrowserDiscarded") {
+          needed.push("discarded");
+        } else if (event.type == "TabShow") {
+          needed.push("hidden");
+        } else if (event.type == "TabHide") {
+          needed.push("hidden");
+        }
+
+        let tab = tabManager.getWrapper(event.originalTarget);
+
+        let changeInfo = {};
+        for (let prop of needed) {
+          changeInfo[prop] = tab[prop];
+        }
+
+        fireForTab(tab, changeInfo);
+      };
+
+      let statusListener = ({browser, status, url}) => {
+        let {gBrowser} = browser.ownerGlobal;
+        let tabElem = gBrowser.getTabForBrowser(browser);
+        if (tabElem) {
+          let changed = {status};
+          if (url) {
+            changed.url = url;
+          }
+
+          fireForTab(tabManager.wrapTab(tabElem), changed);
+        }
+      };
+
+      let isArticleChangeListener = (messageName, message) => {
+        let {gBrowser} = message.target.ownerGlobal;
+        let nativeTab = gBrowser.getTabForBrowser(message.target);
+
+        if (nativeTab) {
+          let tab = tabManager.getWrapper(nativeTab);
+          fireForTab(tab, {isArticle: message.data.isArticle});
+        }
+      };
+
+      let listeners = new Map();
+      if (filter.properties.has("status")) {
+        listeners.set("status", statusListener);
+      }
+      if (needsModified) {
+        listeners.set("TabAttrModified", listener);
+      }
+      if (filter.properties.has("pinned")) {
+        listeners.set("TabPinned", listener);
+        listeners.set("TabUnpinned", listener);
+      }
+      if (filter.properties.has("discarded")) {
+        listeners.set("TabBrowserInserted", listener);
+        listeners.set("TabBrowserDiscarded", listener);
+      }
+      if (filter.properties.has("hidden")) {
+        listeners.set("TabShow", listener);
+        listeners.set("TabHide", listener);
+      }
+
+      for (let [name, listener] of listeners) {
+        windowTracker.addListener(name, listener);
+      }
+
+      if (filter.properties.has("isarticle")) {
+        tabTracker.on("tab-isarticle", isArticleChangeListener);
+      }
+
+      return () => {
+        for (let [name, listener] of listeners) {
+          windowTracker.removeListener(name, listener);
+        }
+
+        if (filter.properties.has("isarticle")) {
+          tabTracker.off("tab-isarticle", isArticleChangeListener);
+        }
+      };
+    };
+
+    super(context, eventName, register);
+  }
+
+  addListener(callback, filter) {
+    let {extension} = this.context;
+    if (filter && filter.urls &&
+        (!extension.hasPermission("tabs") && !extension.hasPermission("activeTab"))) {
+      Cu.reportError("Url filtering in tabs.onUpdated requires \"tabs\" or \"activeTab\" permission.");
+      return false;
+    }
+    return super.addListener(callback, filter);
+  }
+}
+
 this.tabs = class extends ExtensionAPI {
   static onUpdate(id, manifest) {
     if (!manifest.permissions || !manifest.permissions.includes("tabHide")) {
       showHiddenTabs(id);
     }
   }
 
   static onDisable(id) {
@@ -249,127 +452,17 @@ this.tabs = class extends ExtensionAPI {
           windowTracker.addListener("TabMove", moveListener);
           windowTracker.addListener("TabOpen", openListener);
           return () => {
             windowTracker.removeListener("TabMove", moveListener);
             windowTracker.removeListener("TabOpen", openListener);
           };
         }).api(),
 
-        onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
-          const restricted = ["url", "favIconUrl", "title"];
-
-          function sanitize(extension, changeInfo) {
-            let result = {};
-            let nonempty = false;
-            for (let prop in changeInfo) {
-              if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
-                nonempty = true;
-                result[prop] = changeInfo[prop];
-              }
-            }
-            return [nonempty, result];
-          }
-
-          let fireForTab = (tab, changed) => {
-            let [needed, changeInfo] = sanitize(extension, changed);
-            if (needed) {
-              fire.async(tab.id, changeInfo, tab.convert());
-            }
-          };
-
-          let listener = event => {
-            let needed = [];
-            if (event.type == "TabAttrModified") {
-              let changed = event.detail.changed;
-              if (changed.includes("image")) {
-                needed.push("favIconUrl");
-              }
-              if (changed.includes("muted")) {
-                needed.push("mutedInfo");
-              }
-              if (changed.includes("soundplaying")) {
-                needed.push("audible");
-              }
-              if (changed.includes("label")) {
-                needed.push("title");
-              }
-              if (changed.includes("sharing")) {
-                needed.push("sharingState");
-              }
-            } else if (event.type == "TabPinned") {
-              needed.push("pinned");
-            } else if (event.type == "TabUnpinned") {
-              needed.push("pinned");
-            } else if (event.type == "TabBrowserInserted" &&
-                       !event.detail.insertedOnTabCreation) {
-              needed.push("discarded");
-            } else if (event.type == "TabBrowserDiscarded") {
-              needed.push("discarded");
-            } else if (event.type == "TabShow") {
-              needed.push("hidden");
-            } else if (event.type == "TabHide") {
-              needed.push("hidden");
-            }
-
-            let tab = tabManager.getWrapper(event.originalTarget);
-            let changeInfo = {};
-            for (let prop of needed) {
-              changeInfo[prop] = tab[prop];
-            }
-
-            fireForTab(tab, changeInfo);
-          };
-
-          let statusListener = ({browser, status, url}) => {
-            let {gBrowser} = browser.ownerGlobal;
-            let tabElem = gBrowser.getTabForBrowser(browser);
-            if (tabElem) {
-              let changed = {status};
-              if (url) {
-                changed.url = url;
-              }
-
-              fireForTab(tabManager.wrapTab(tabElem), changed);
-            }
-          };
-
-          let isArticleChangeListener = (messageName, message) => {
-            let {gBrowser} = message.target.ownerGlobal;
-            let nativeTab = gBrowser.getTabForBrowser(message.target);
-
-            if (nativeTab) {
-              let tab = tabManager.getWrapper(nativeTab);
-              fireForTab(tab, {isArticle: message.data.isArticle});
-            }
-          };
-
-          windowTracker.addListener("status", statusListener);
-          windowTracker.addListener("TabAttrModified", listener);
-          windowTracker.addListener("TabPinned", listener);
-          windowTracker.addListener("TabUnpinned", listener);
-          windowTracker.addListener("TabBrowserInserted", listener);
-          windowTracker.addListener("TabBrowserDiscarded", listener);
-          windowTracker.addListener("TabShow", listener);
-          windowTracker.addListener("TabHide", listener);
-
-          tabTracker.on("tab-isarticle", isArticleChangeListener);
-
-          return () => {
-            windowTracker.removeListener("status", statusListener);
-            windowTracker.removeListener("TabAttrModified", listener);
-            windowTracker.removeListener("TabPinned", listener);
-            windowTracker.removeListener("TabUnpinned", listener);
-            windowTracker.removeListener("TabBrowserInserted", listener);
-            windowTracker.removeListener("TabBrowserDiscarded", listener);
-            windowTracker.removeListener("TabShow", listener);
-            windowTracker.removeListener("TabHide", listener);
-            tabTracker.off("tab-isarticle", isArticleChangeListener);
-          };
-        }).api(),
+        onUpdated: new TabsUpdateFilterEventManager(context, "tabs.onUpdated").api(),
 
         create(createProperties) {
           return new Promise((resolve, reject) => {
             let window = createProperties.windowId !== null ?
               windowTracker.getWindow(createProperties.windowId, context) :
               windowTracker.topNormalWindow;
 
             if (!window.gBrowser) {
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -283,16 +283,56 @@
         "enum": ["loading", "complete"],
         "description": "Whether the tabs have completed loading."
       },
       {
         "id": "WindowType",
         "type": "string",
         "enum": ["normal", "popup", "panel", "app", "devtools"],
         "description": "The type of window."
+      },
+      {
+        "id": "UpdatePropertyName",
+        "type": "string",
+        "enum": [
+          "audible",
+          "discarded",
+          "favIconUrl",
+          "hidden",
+          "isarticle",
+          "mutedInfo",
+          "pinned",
+          "sharingState",
+          "status",
+          "title"
+        ],
+        "description": "Event names supported in onUpdated."
+      },
+      {
+        "id": "UpdateFilter",
+        "type": "object",
+        "description": "An object describing filters to apply to tabs.onUpdated events.",
+        "properties": {
+          "urls": {
+            "type": "array",
+            "description": "A list of URLs or URL patterns. Events that cannot match any of the URLs will be filtered out.  Filtering with urls requires the <code>\"tabs\"</code> or  <code>\"activeTab\"</code> permission.",
+            "optional": true,
+            "items": { "type": "string" },
+            "minItems": 1
+          },
+          "properties": {
+            "type": "array",
+            "optional": true,
+            "description": "A list of property names. Events that do not match any of the names will be filtered out.",
+            "items": { "$ref": "UpdatePropertyName" },
+            "minItems": 1
+          },
+          "tabId": { "type": "integer", "optional": true },
+          "windowId": { "type": "integer", "optional": true }
+        }
       }
     ],
     "properties": {
       "TAB_ID_NONE": {
         "value": -1,
         "description": "An ID which represents the absence of a browser tab."
       }
     },
@@ -1395,16 +1435,24 @@
               }
             }
           },
           {
             "$ref": "Tab",
             "name": "tab",
             "description": "Gives the state of the tab that was updated."
           }
+        ],
+        "extraParameters": [
+          {
+            "$ref": "UpdateFilter",
+            "name": "filter",
+            "optional": true,
+            "description": "A set of filters that restricts the events that will be sent to this listener."
+          }
         ]
       },
       {
         "name": "onMoved",
         "type": "function",
         "description": "Fired when a tab is moved within a window. Only one move event is fired, representing the tab the user directly moved. Move events are not fired for the other tabs that must move in response. This event is not fired when a tab is moved between windows. For that, see $(ref:tabs.onDetached).",
         "parameters": [
           {"type": "integer", "name": "tabId", "minimum": 0},
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -173,16 +173,17 @@ skip-if = !e10s
 [browser_ext_tabs_lazy.js]
 [browser_ext_tabs_removeCSS.js]
 [browser_ext_tabs_move_array.js]
 [browser_ext_tabs_move_window.js]
 [browser_ext_tabs_move_window_multiple.js]
 [browser_ext_tabs_move_window_pinned.js]
 [browser_ext_tabs_onHighlighted.js]
 [browser_ext_tabs_onUpdated.js]
+[browser_ext_tabs_onUpdated_filter.js]
 [browser_ext_tabs_opener.js]
 [browser_ext_tabs_printPreview.js]
 [browser_ext_tabs_query.js]
 [browser_ext_tabs_readerMode.js]
 [browser_ext_tabs_reload.js]
 [browser_ext_tabs_reload_bypass_cache.js]
 [browser_ext_tabs_saveAsPDF.js]
 skip-if = os == 'mac' # Save as PDF not supported on Mac OS X
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated_filter.js
@@ -0,0 +1,241 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_filter_url() {
+  let ext_fail = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        browser.test.fail(`received unexpected onUpdated event ${JSON.stringify(changeInfo)}`);
+      }, {urls: ["*://*.mozilla.org/*"]});
+    },
+  });
+  await ext_fail.startup();
+
+  let ext_perm = ExtensionTestUtils.loadExtension({
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        browser.test.fail(`received unexpected onUpdated event without tabs permission`);
+      }, {urls: ["*://mochi.test/*"]});
+    },
+  });
+  await ext_perm.startup();
+
+  let ext_ok = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        browser.test.log(`got onUpdated ${JSON.stringify(changeInfo)}`);
+        if (changeInfo.status === "complete") {
+          browser.test.notifyPass("onUpdated");
+        }
+      }, {urls: ["*://mochi.test/*"]});
+    },
+  });
+  await ext_ok.startup();
+  let ok1 = ext_ok.awaitFinish("onUpdated");
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+  await ok1;
+
+  await ext_ok.unload();
+  await ext_fail.unload();
+  await ext_perm.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_url_activeTab() {
+  let ext = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["activeTab"],
+    },
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        browser.test.fail("should only have notification for activeTab, selectedTab is not activeTab");
+      }, {urls: ["*://mochi.test/*"]});
+    },
+  });
+  await ext.startup();
+
+  let ext2 = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        if (changeInfo.status === "complete") {
+          browser.test.notifyPass("onUpdated");
+        }
+      }, {urls: ["*://mochi.test/*"]});
+    },
+  });
+  await ext2.startup();
+  let ok = ext2.awaitFinish("onUpdated");
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/#foreground");
+  await Promise.all([ok]);
+
+  await ext.unload();
+  await ext2.unload();
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_tabId() {
+  let ext_fail = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        browser.test.fail(`received unexpected onUpdated event ${JSON.stringify(changeInfo)}`);
+      }, {tabId: 12345});
+    },
+  });
+  await ext_fail.startup();
+
+  let ext_ok = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        if (changeInfo.status === "complete") {
+          browser.test.notifyPass("onUpdated");
+        }
+      });
+    },
+  });
+  await ext_ok.startup();
+  let ok = ext_ok.awaitFinish("onUpdated");
+
+  let ext_ok2 = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      browser.tabs.onCreated.addListener(tab => {
+        browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+          if (changeInfo.status === "complete") {
+            browser.test.notifyPass("onUpdated");
+          }
+        }, {tabId: tab.id});
+        browser.test.log(`Tab specific tab listener on tab ${tab.id}`);
+      });
+    },
+  });
+  await ext_ok2.startup();
+  let ok2 = ext_ok2.awaitFinish("onUpdated");
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+  await Promise.all([ok, ok2]);
+
+  await ext_ok.unload();
+  await ext_ok2.unload();
+  await ext_fail.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_windowId() {
+  let ext_fail = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        browser.test.fail(`received unexpected onUpdated event ${JSON.stringify(changeInfo)}`);
+      }, {windowId: 12345});
+    },
+  });
+  await ext_fail.startup();
+
+  let ext_ok = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        if (changeInfo.status === "complete") {
+          browser.test.notifyPass("onUpdated");
+        }
+      }, {windowId: browser.windows.WINDOW_ID_CURRENT});
+    },
+  });
+  await ext_ok.startup();
+  let ok = ext_ok.awaitFinish("onUpdated");
+
+  let ext_ok2 = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    async background() {
+      let window = await browser.windows.getCurrent();
+      browser.test.log(`Window specific tab listener on window ${window.id}`);
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        if (changeInfo.status === "complete") {
+          browser.test.notifyPass("onUpdated");
+        }
+      }, {windowId: window.id});
+      browser.test.sendMessage("ready");
+    },
+  });
+  await ext_ok2.startup();
+  await ext_ok2.awaitMessage("ready");
+  let ok2 = ext_ok2.awaitFinish("onUpdated");
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+  await Promise.all([ok, ok2]);
+
+  await ext_ok.unload();
+  await ext_ok2.unload();
+  await ext_fail.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_property() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      // We expect only status updates, anything else is a failure.
+      let properties = new Set([
+        "audible",
+        "discarded",
+        "favIconUrl",
+        "hidden",
+        "isarticle",
+        "mutedInfo",
+        "pinned",
+        "sharingState",
+        "title",
+      ]);
+      browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+        browser.test.log(`got onUpdated ${JSON.stringify(changeInfo)}`);
+        browser.test.assertTrue(!!changeInfo.status, "changeInfo has status");
+        if (Object.keys(changeInfo).some(p => properties.has(p))) {
+          browser.test.fail(`received unexpected onUpdated event ${JSON.stringify(changeInfo)}`);
+        }
+        if (changeInfo.status === "complete") {
+          browser.test.notifyPass("onUpdated");
+        }
+      }, {properties: ["status"]});
+    },
+  });
+  await extension.startup();
+  let ok = extension.awaitFinish("onUpdated");
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+  await ok;
+
+  await extension.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/browser/components/extensions/test/mochitest/test_ext_all_apis.html
+++ b/browser/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -14,16 +14,17 @@
 /* exported expectedContentApisTargetSpecific, expectedBackgroundApisTargetSpecific */
 let expectedContentApisTargetSpecific = [
 ];
 
 let expectedBackgroundApisTargetSpecific = [
   "tabs.MutedInfoReason",
   "tabs.TAB_ID_NONE",
   "tabs.TabStatus",
+  "tabs.UpdatePropertyName",
   "tabs.WindowType",
   "tabs.ZoomSettingsMode",
   "tabs.ZoomSettingsScope",
   "tabs.connect",
   "tabs.create",
   "tabs.detectLanguage",
   "tabs.duplicate",
   "tabs.discard",
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -995,16 +995,18 @@ function PlacesToolbar(aPlace) {
   // If personal-bookmarks has been dragged to the tabs toolbar,
   // we have to track addition and removals of tabs, to properly
   // recalculate the available space for bookmarks.
   // TODO (bug 734730): Use a performant mutation listener when available.
   if (this._viewElt.parentNode.parentNode == document.getElementById("TabsToolbar")) {
     this._addEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
   }
 
+  this._updatingNodesVisibility = false;
+
   PlacesViewBase.call(this, aPlace);
 
   Services.telemetry.getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS")
                     .add(Date.now() - startTime);
 }
 
 PlacesToolbar.prototype = {
   __proto__: PlacesViewBase.prototype,
@@ -1248,44 +1250,65 @@ PlacesToolbar.prototype = {
     // Update the chevron on a timer.  This will avoid repeated work when
     // lot of changes happen in a small timeframe.
     if (this._updateNodesVisibilityTimer)
       this._updateNodesVisibilityTimer.cancel();
 
     this._updateNodesVisibilityTimer = this._setTimer(100);
   },
 
-  _updateNodesVisibilityTimerCallback: function PT__updateNodesVisibilityTimerCallback() {
-    let scrollRect = this._rootElt.getBoundingClientRect();
+  async _updateNodesVisibilityTimerCallback() {
+    if (this._updatingNodesVisibility || window.closed) {
+      return;
+    }
+    this._updatingNodesVisibility = true;
+
+    let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindowUtils);
+
+    let scrollRect =
+      await window.promiseDocumentFlushed(() => dwu.getBoundsWithoutFlushing(this._rootElt));
+
     let childOverflowed = false;
-    for (let child of this._rootElt.childNodes) {
-      // Once a child overflows, all the next ones will.
-      if (!childOverflowed) {
-        let childRect = child.getBoundingClientRect();
-        childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
-                                     : (childRect.right > scrollRect.right);
+
+    // We're about to potentially update a bunch of nodes, so we do it
+    // in a requestAnimationFrame so that other JS that's might execute
+    // in the same tick can avoid flushing styles and layout for these
+    // changes.
+    window.requestAnimationFrame(() => {
+      for (let child of this._rootElt.childNodes) {
+        // Once a child overflows, all the next ones will.
+        if (!childOverflowed) {
+          let childRect = dwu.getBoundsWithoutFlushing(child);
+          childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
+                                       : (childRect.right > scrollRect.right);
+        }
+
+        if (childOverflowed) {
+          child.removeAttribute("image");
+          child.style.visibility = "hidden";
+        } else {
+          let icon = child._placesNode.icon;
+          if (icon)
+            child.setAttribute("image", icon);
+          child.style.visibility = "visible";
+        }
       }
 
-      if (childOverflowed) {
-        child.removeAttribute("image");
-        child.style.visibility = "hidden";
-      } else {
-        let icon = child._placesNode.icon;
-        if (icon)
-          child.setAttribute("image", icon);
-        child.style.visibility = "visible";
+      // We rebuild the chevron on popupShowing, so if it is open
+      // we must update it.
+      if (!this._chevron.collapsed && this._chevron.open) {
+        this._updateChevronPopupNodesVisibility();
       }
-    }
 
-    // We rebuild the chevron on popupShowing, so if it is open
-    // we must update it.
-    if (!this._chevron.collapsed && this._chevron.open)
-      this._updateChevronPopupNodesVisibility();
-    let event = new CustomEvent("BookmarksToolbarVisibilityUpdated", {bubbles: true});
-    this._viewElt.dispatchEvent(event);
+      let event = new CustomEvent("BookmarksToolbarVisibilityUpdated", {bubbles: true});
+      this._viewElt.dispatchEvent(event);
+    });
+
+    this._updatingNodesVisibility = false;
   },
 
   nodeInserted:
   function PT_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
     let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
     if (parentElt == this._rootElt) { // Node is on the toolbar.
       let children = this._rootElt.childNodes;
       // Nothing to do if it's a never-visible node, but note it's possible
@@ -1603,19 +1626,17 @@ PlacesToolbar.prototype = {
     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
     return timer;
   },
 
   notify: function PT_notify(aTimer) {
     if (aTimer == this._updateNodesVisibilityTimer) {
       this._updateNodesVisibilityTimer = null;
-      // Bug 1440070: This should use promiseDocumentFlushed, so that
-      // _updateNodesVisibilityTimerCallback can use getBoundsWithoutFlush.
-      window.requestAnimationFrame(this._updateNodesVisibilityTimerCallback.bind(this));
+      this._updateNodesVisibilityTimerCallback();
     } else if (aTimer == this._ibTimer) {
       // * Timer to turn off indicator bar.
       this._dropIndicator.collapsed = true;
       this._ibTimer = null;
     } else if (aTimer == this._overFolder.openTimer) {
       // * Timer to open a menubutton that's being dragged over.
       // Set the autoopen attribute on the folder's menupopup so that
       // the menu will automatically close when the mouse drags off of it.
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -462,25 +462,22 @@ var gMainPane = {
 
       setEventListener("separateProfileMode", "command", gMainPane.separateProfileModeChange);
       let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
       setEventListener("getStarted", "click", gMainPane.onGetStarted);
 
       OS.File.stat(ignoreSeparateProfile).then(() => separateProfileModeCheckbox.checked = false,
         () => separateProfileModeCheckbox.checked = true);
 
-      if (!Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
-        document.getElementById("sync-dev-edition-root").hidden = true;
-        return;
+      if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+        document.getElementById("sync-dev-edition-root").hidden = false;
+        fxAccounts.getSignedInUser().then(data => {
+          document.getElementById("getStarted").selectedIndex = data ? 1 : 0;
+        }).catch(Cu.reportError);
       }
-
-      fxAccounts.getSignedInUser().then(data => {
-        document.getElementById("getStarted").selectedIndex = data ? 1 : 0;
-      })
-        .catch(Cu.reportError);
     }
 
     // Initialize the Firefox Updates section.
     let version = AppConstants.MOZ_APP_VERSION_DISPLAY;
 
     // Include the build ID if this is an "a#" (nightly) build
     if (/a\d+$/.test(version)) {
       let buildID = Services.appinfo.appBuildID;
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -28,17 +28,17 @@
           data-category="paneGeneral"
           hidden="true">
   <caption><label data-l10n-id="startup-header"/></caption>
 
 #ifdef MOZ_DEV_EDITION
   <vbox id="separateProfileBox">
     <checkbox id="separateProfileMode"
               data-l10n-id="separate-profile-mode"/>
-    <hbox id="sync-dev-edition-root" lign="center" class="indent">
+    <hbox id="sync-dev-edition-root" lign="center" class="indent" hidden="true">
       <label id="useFirefoxSync" data-l10n-id="use-firefox-sync"/>
       <deck id="getStarted">
         <label class="text-link" data-l10n-id="get-started-not-logged-in"/>
         <label class="text-link" data-l10n-id="get-started-configured"/>
       </deck>
     </hbox>
   </vbox>
 #endif
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -655,25 +655,33 @@ notification[value="translation"] menuli
   }
   :root[tabsintitlebar][sizemode="maximized"] > #titlebar {
     -moz-appearance: -moz-window-titlebar-maximized;
   }
 
   /* Add extra space to titlebar for dragging */
   :root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar,
   :root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar {
-    margin-top: var(--space-above-tabbar);
+    padding-top: var(--space-above-tabbar);
   }
 
   /* Private browsing and accessibility indicators */
   :root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar > .private-browsing-indicator,
   :root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar > .accessibility-indicator,
   :root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar > .private-browsing-indicator,
   :root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar > .accessibility-indicator {
-    margin-top: calc(-1 * var(--space-above-tabbar));
+    padding-top: calc(-1 * var(--space-above-tabbar));
+  }
+
+  /* Make #TabsToolbar transparent as we style underlying #titlebar with
+   * -moz-window-titlebar (Gtk+ theme).
+   */
+  :root[tabsintitlebar] #TabsToolbar,
+  :root[tabsintitlebar] #toolbar-menubar {
+    -moz-appearance: none;
   }
 
   /* The button box must appear on top of the navigator-toolbox in order for
    * click and hover mouse events to work properly for the button in the restored
    * window state. Otherwise, elements in the navigator-toolbox, like the menubar,
    * can swallow those events.
    */
   #titlebar-buttonbox {
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -662,17 +662,17 @@ function getAvailableIds() {
 
   return out;
 }
 
 /**
  * Show available ids.
  */
 function showAvailableIds() {
-  info(getAvailableIds);
+  info(getAvailableIds());
 }
 
 /**
  * Get a cell value.
  *
  * @param {String} id
  *        The uniqueId of the row.
  * @param {String} column
--- a/dom/base/nsObjectLoadingContent.cpp
+++ b/dom/base/nsObjectLoadingContent.cpp
@@ -802,32 +802,28 @@ nsObjectLoadingContent::InstantiatePlugi
 
   RefPtr<nsNPAPIPluginInstance> pluginInstance;
   GetPluginInstance(getter_AddRefs(pluginInstance));
   if (pluginInstance) {
     nsCOMPtr<nsIPluginTag> pluginTag;
     pluginHost->GetPluginTagForInstance(pluginInstance,
                                         getter_AddRefs(pluginTag));
 
-    nsCOMPtr<nsIBlocklistService> blocklist =
-      do_GetService("@mozilla.org/extensions/blocklist;1");
-    if (blocklist) {
-      uint32_t blockState = nsIBlocklistService::STATE_NOT_BLOCKED;
-      blocklist->GetPluginBlocklistState(pluginTag, EmptyString(),
-                                         EmptyString(), &blockState);
-      if (blockState == nsIBlocklistService::STATE_OUTDATED) {
-        // Fire plugin outdated event if necessary
-        LOG(("OBJLC [%p]: Dispatching plugin outdated event for content\n",
-             this));
-        nsCOMPtr<nsIRunnable> ev = new nsSimplePluginEvent(thisContent,
-                                                     NS_LITERAL_STRING("PluginOutdated"));
-        nsresult rv = NS_DispatchToCurrentThread(ev);
-        if (NS_FAILED(rv)) {
-          NS_WARNING("failed to dispatch nsSimplePluginEvent");
-        }
+
+    uint32_t blockState = nsIBlocklistService::STATE_NOT_BLOCKED;
+    pluginTag->GetBlocklistState(&blockState);
+    if (blockState == nsIBlocklistService::STATE_OUTDATED) {
+      // Fire plugin outdated event if necessary
+      LOG(("OBJLC [%p]: Dispatching plugin outdated event for content\n",
+           this));
+      nsCOMPtr<nsIRunnable> ev = new nsSimplePluginEvent(thisContent,
+                                                   NS_LITERAL_STRING("PluginOutdated"));
+      nsresult rv = NS_DispatchToCurrentThread(ev);
+      if (NS_FAILED(rv)) {
+        NS_WARNING("failed to dispatch nsSimplePluginEvent");
       }
     }
 
     // If we have a URI but didn't open a channel yet (eAllowPluginSkipChannel)
     // or we did load with a channel but are re-instantiating, re-open the
     // channel. OpenChannel() performs security checks, and this plugin has
     // already passed content policy in LoadObject.
     if ((mURI && !mChannelLoaded) || (mChannelLoaded && !aIsLoading)) {
--- a/dom/base/nsPluginArray.cpp
+++ b/dom/base/nsPluginArray.cpp
@@ -392,17 +392,18 @@ nsPluginArray::EnsurePlugins()
       } else {
         mCTPPlugins.AppendElement(new nsPluginElement(mWindow, pluginTags[i]));
       }
     }
   }
 
   if (mPlugins.Length() == 0 && mCTPPlugins.Length() != 0) {
     nsCOMPtr<nsPluginTag> hiddenTag = new nsPluginTag("Hidden Plugin", nullptr, "dummy.plugin", nullptr, nullptr,
-                                                      nullptr, nullptr, nullptr, 0, 0, false);
+                                                      nullptr, nullptr, nullptr, 0, 0, false,
+                                                      nsIBlocklistService::STATE_NOT_BLOCKED);
     mPlugins.AppendElement(new nsPluginElement(mWindow, hiddenTag));
   }
 
   // Alphabetize the enumeration order of non-hidden plugins to reduce
   // fingerprintable entropy based on plugins' installation file times.
   mPlugins.Sort();
 }
 // nsPluginElement implementation.
--- a/dom/plugins/base/nsPluginHost.cpp
+++ b/dom/plugins/base/nsPluginHost.cpp
@@ -129,17 +129,17 @@ static const char *kPrefWhitelist = "plu
 static const char *kPrefLoadInParentPrefix = "plugin.load_in_parent_process.";
 static const char *kPrefDisableFullPage = "plugin.disable_full_page_plugin_for_types";
 
 // How long we wait before unloading an idle plugin process.
 // Defaults to 30 seconds.
 static const char *kPrefUnloadPluginTimeoutSecs = "dom.ipc.plugins.unloadTimeoutSecs";
 static const uint32_t kDefaultPluginUnloadingTimeout = 30;
 
-static const char *kPluginRegistryVersion = "0.18";
+static const char *kPluginRegistryVersion = "0.19";
 
 static const char kDirectoryServiceContractID[] = "@mozilla.org/file/directory_service;1";
 
 #define kPluginRegistryFilename NS_LITERAL_CSTRING("pluginreg.dat")
 
 LazyLogModule nsPluginLogging::gNPNLog(NPN_LOG_NAME);
 LazyLogModule nsPluginLogging::gNPPLog(NPP_LOG_NAME);
 LazyLogModule nsPluginLogging::gPluginLog(PLUGIN_LOG_NAME);
@@ -261,17 +261,20 @@ nsPluginHost::nsPluginHost()
   mPluginsDisabled = Preferences::GetBool("plugin.disable", false);
 
   Preferences::AddStrongObserver(this, "plugin.disable");
 
   nsCOMPtr<nsIObserverService> obsService =
     mozilla::services::GetObserverService();
   if (obsService) {
     obsService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
-    obsService->AddObserver(this, "blocklist-updated", false);
+    if (XRE_IsParentProcess()) {
+      obsService->AddObserver(this, "blocklist-updated", false);
+      obsService->AddObserver(this, "blocklist-loaded", false);
+    }
   }
 
 #ifdef PLUGIN_LOGGING
   MOZ_LOG(nsPluginLogging::gNPNLog, PLUGIN_LOG_ALWAYS,("NPN Logging Active!\n"));
   MOZ_LOG(nsPluginLogging::gPluginLog, PLUGIN_LOG_ALWAYS,("General Plugin Logging Active! (nsPluginHost::ctor)\n"));
   MOZ_LOG(nsPluginLogging::gNPPLog, PLUGIN_LOG_ALWAYS,("NPP Logging Active!\n"));
 
   PLUGIN_LOG(PLUGIN_LOG_ALWAYS,("nsPluginHost::ctor\n"));
@@ -1994,16 +1997,23 @@ nsresult nsPluginHost::ScanPluginsDirect
     }
   }
 
   pluginFiles.Sort(CompareFilesByTime());
 
   nsCOMArray<nsIFile> extensionDirs;
   GetExtensionDirectories(extensionDirs);
 
+  nsCOMPtr<nsIBlocklistService> blocklist =
+    do_GetService("@mozilla.org/extensions/blocklist;1");
+
+  bool isBlocklistLoaded = false;
+  if (blocklist && NS_FAILED(blocklist->GetIsLoaded(&isBlocklistLoaded))) {
+    isBlocklistLoaded = false;
+  }
   for (int32_t i = (pluginFiles.Length() - 1); i >= 0; i--) {
     nsCOMPtr<nsIFile>& localfile = pluginFiles[i];
 
     nsString utf16FilePath;
     rv = localfile->GetPath(utf16FilePath);
     if (NS_FAILED(rv))
       continue;
 
@@ -2085,22 +2095,27 @@ nsresult nsPluginHost::ScanPluginsDirect
         }
         mInvalidPlugins = invalidTag;
 
         // Mark aPluginsChanged so pluginreg is rewritten
         *aPluginsChanged = true;
         continue;
       }
 
-      pluginTag = new nsPluginTag(&info, fileModTime, fromExtension);
-      pluginFile.FreePluginInfo(info);
+      uint32_t state = nsIBlocklistService::STATE_NOT_BLOCKED;
+      pluginTag = new nsPluginTag(&info, fileModTime, fromExtension, state);
       pluginTag->mLibrary = library;
-      uint32_t state;
-      rv = pluginTag->GetBlocklistState(&state);
-      NS_ENSURE_SUCCESS(rv, rv);
+      // If the blocklist is loaded, get the blocklist state now.
+      // If it isn't loaded yet, we'll update it once it loads.
+      if (isBlocklistLoaded &&
+          NS_SUCCEEDED(blocklist->GetPluginBlocklistState(pluginTag, EmptyString(),
+                                                          EmptyString(), &state))) {
+        pluginTag->SetBlocklistState(state);
+      }
+      pluginFile.FreePluginInfo(info);
 
       // If the blocklist says it is risky and we have never seen this
       // plugin before, then disable it.
       if (state == nsIBlocklistService::STATE_SOFTBLOCKED && !seenBefore) {
         pluginTag->SetEnabledState(nsIPluginTag::STATE_DISABLED);
       }
 
       // Plugin unloading is tag-based. If we created a new tag and loaded
@@ -2739,26 +2754,28 @@ nsPluginHost::WritePluginInfo()
       PLUGIN_REGISTRY_END_OF_LINE_MARKER,
       (tag->mFullPath.get()),
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       PLUGIN_REGISTRY_END_OF_LINE_MARKER,
       (tag->Version().get()),
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       PLUGIN_REGISTRY_END_OF_LINE_MARKER);
 
-    // lastModifiedTimeStamp|canUnload|tag->mFlags|fromExtension
-    PR_fprintf(fd, "%lld%c%d%c%lu%c%d%c%c\n",
+    // lastModifiedTimeStamp|canUnload|tag->mFlags|fromExtension|blocklistState
+    PR_fprintf(fd, "%lld%c%d%c%lu%c%d%c%d%c%c\n",
       tag->mLastModifiedTime,
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       false, // did store whether or not to unload in-process plugins
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       0, // legacy field for flags
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       tag->IsFromExtension(),
       PLUGIN_REGISTRY_FIELD_DELIMITER,
+      tag->BlocklistState(),
+      PLUGIN_REGISTRY_FIELD_DELIMITER,
       PLUGIN_REGISTRY_END_OF_LINE_MARKER);
 
     //description, name & mtypecount are on separate line
     PR_fprintf(fd, "%s%c%c\n%s%c%c\n%d\n",
       (tag->Description().get()),
       PLUGIN_REGISTRY_FIELD_DELIMITER,
       PLUGIN_REGISTRY_END_OF_LINE_MARKER,
       (tag->Name().get()),
@@ -2969,22 +2986,23 @@ nsPluginHost::ReadPluginInfo()
     if (!reader.NextLine())
       return rv;
 
     const char *version;
     version = reader.LinePtr();
     if (!reader.NextLine())
       return rv;
 
-    // lastModifiedTimeStamp|canUnload|tag.mFlag|fromExtension
-    if (4 != reader.ParseLine(values, 4))
+    // lastModifiedTimeStamp|canUnload|tag.mFlag|fromExtension|blocklistState
+    if (5 != reader.ParseLine(values, 5))
       return rv;
 
     int64_t lastmod = nsCRT::atoll(values[0]);
     bool fromExtension = atoi(values[3]);
+    uint16_t blocklistState = atoi(values[4]);
     if (!reader.NextLine())
       return rv;
 
     char *description = reader.LinePtr();
     if (!reader.NextLine())
       return rv;
 
     const char *name = reader.LinePtr();
@@ -3035,17 +3053,17 @@ nsPluginHost::ReadPluginInfo()
     RefPtr<nsPluginTag> tag = new nsPluginTag(name,
       description,
       filename,
       fullpath,
       version,
       (const char* const*)mimetypes,
       (const char* const*)mimedescriptions,
       (const char* const*)extensions,
-      mimetypecount, lastmod, fromExtension, true);
+      mimetypecount, lastmod, fromExtension, blocklistState, true);
 
     delete [] heapalloced;
 
     // Import flags from registry into prefs for old registry versions
     MOZ_LOG(nsPluginLogging::gPluginLog, PLUGIN_LOG_BASIC,
       ("LoadCachedPluginsInfo : Loading Cached plugininfo for %s\n", tag->FileName().get()));
 
     tag->mNext = mCachedPlugins;
@@ -3384,25 +3402,42 @@ NS_IMETHODIMP nsPluginHost::Observe(nsIS
     mPluginsDisabled = Preferences::GetBool("plugin.disable", false);
     // Unload or load plugins as needed
     if (mPluginsDisabled) {
       UnloadPlugins();
     } else {
       LoadPlugins();
     }
   }
-  if (!strcmp("blocklist-updated", aTopic)) {
+  if (XRE_IsParentProcess() &&
+      (!strcmp("blocklist-updated", aTopic) || !strcmp("blocklist-loaded", aTopic))) {
+    nsCOMPtr<nsIBlocklistService> blocklist =
+      do_GetService("@mozilla.org/extensions/blocklist;1");
+    if (!blocklist) {
+      return NS_OK;
+    }
     nsPluginTag* plugin = mPlugins;
+    bool blocklistAlteredPlugins = false;
     while (plugin) {
-      plugin->InvalidateBlocklistState();
+      uint32_t blocklistState = nsIBlocklistService::STATE_NOT_BLOCKED;
+      nsresult rv = blocklist->GetPluginBlocklistState(plugin, EmptyString(),
+                                                       EmptyString(), &blocklistState);
+      NS_ENSURE_SUCCESS(rv, rv);
+      uint32_t oldBlocklistState;
+      plugin->GetBlocklistState(&oldBlocklistState);
+      plugin->SetBlocklistState(blocklistState);
+      blocklistAlteredPlugins |= (oldBlocklistState != blocklistState);
       plugin = plugin->mNext;
     }
-    // We update blocklists asynchronously by just sending a new plugin list to
-    // content.
-    if (XRE_IsParentProcess()) {
+    if (blocklistAlteredPlugins) {
+      // Write the changed list to disk:
+      WritePluginInfo();
+
+      // We update blocklists asynchronously by just sending a new plugin list to
+      // content.
       // We'll need to repack our tags and send them to content again.
       IncrementChromeEpoch();
       SendPluginsToContent();
     }
   }
   return NS_OK;
 }
 
--- a/dom/plugins/base/nsPluginTags.cpp
+++ b/dom/plugins/base/nsPluginTags.cpp
@@ -218,32 +218,32 @@ nsIInternalPluginTag::HasMimeType(const 
 }
 
 /* nsPluginTag */
 
 uint32_t nsPluginTag::sNextId;
 
 nsPluginTag::nsPluginTag(nsPluginInfo* aPluginInfo,
                          int64_t aLastModifiedTime,
-                         bool fromExtension)
+                         bool fromExtension,
+                         uint32_t aBlocklistState)
   : nsIInternalPluginTag(aPluginInfo->fName, aPluginInfo->fDescription,
                          aPluginInfo->fFileName, aPluginInfo->fVersion),
     mId(sNextId++),
     mContentProcessRunningCount(0),
     mHadLocalInstance(false),
     mLibrary(nullptr),
     mIsFlashPlugin(false),
     mSupportsAsyncRender(false),
     mFullPath(aPluginInfo->fFullPath),
     mLastModifiedTime(aLastModifiedTime),
     mSandboxLevel(0),
     mIsSandboxLoggingEnabled(false),
-    mCachedBlocklistState(nsIBlocklistService::STATE_NOT_BLOCKED),
-    mCachedBlocklistStateValid(false),
-    mIsFromExtension(fromExtension)
+    mIsFromExtension(fromExtension),
+    mBlocklistState(aBlocklistState)
 {
   InitMime(aPluginInfo->fMimeTypeArray,
            aPluginInfo->fMimeDescriptionArray,
            aPluginInfo->fExtensionArray,
            aPluginInfo->fVariantCount);
   InitSandboxLevel();
   EnsureMembersAreUTF8();
   FixupVersion();
@@ -255,31 +255,31 @@ nsPluginTag::nsPluginTag(const char* aNa
                          const char* aFullPath,
                          const char* aVersion,
                          const char* const* aMimeTypes,
                          const char* const* aMimeDescriptions,
                          const char* const* aExtensions,
                          int32_t aVariants,
                          int64_t aLastModifiedTime,
                          bool fromExtension,
+                         uint32_t aBlocklistState,
                          bool aArgsAreUTF8)
   : nsIInternalPluginTag(aName, aDescription, aFileName, aVersion),
     mId(sNextId++),
     mContentProcessRunningCount(0),
     mHadLocalInstance(false),
     mLibrary(nullptr),
     mIsFlashPlugin(false),
     mSupportsAsyncRender(false),
     mFullPath(aFullPath),
     mLastModifiedTime(aLastModifiedTime),
     mSandboxLevel(0),
     mIsSandboxLoggingEnabled(false),
-    mCachedBlocklistState(nsIBlocklistService::STATE_NOT_BLOCKED),
-    mCachedBlocklistStateValid(false),
-    mIsFromExtension(fromExtension)
+    mIsFromExtension(fromExtension),
+    mBlocklistState(aBlocklistState)
 {
   InitMime(aMimeTypes, aMimeDescriptions, aExtensions,
            static_cast<uint32_t>(aVariants));
   InitSandboxLevel();
   if (!aArgsAreUTF8)
     EnsureMembersAreUTF8();
   FixupVersion();
 }
@@ -293,31 +293,30 @@ nsPluginTag::nsPluginTag(uint32_t aId,
                          nsTArray<nsCString> aMimeTypes,
                          nsTArray<nsCString> aMimeDescriptions,
                          nsTArray<nsCString> aExtensions,
                          bool aIsFlashPlugin,
                          bool aSupportsAsyncRender,
                          int64_t aLastModifiedTime,
                          bool aFromExtension,
                          int32_t aSandboxLevel,
-                         uint16_t aBlocklistState)
+                         uint32_t aBlocklistState)
   : nsIInternalPluginTag(aName, aDescription, aFileName, aVersion, aMimeTypes,
                          aMimeDescriptions, aExtensions),
     mId(aId),
     mContentProcessRunningCount(0),
     mLibrary(nullptr),
     mIsFlashPlugin(aIsFlashPlugin),
     mSupportsAsyncRender(aSupportsAsyncRender),
     mLastModifiedTime(aLastModifiedTime),
     mSandboxLevel(aSandboxLevel),
     mIsSandboxLoggingEnabled(false),
     mNiceFileName(),
-    mCachedBlocklistState(aBlocklistState),
-    mCachedBlocklistStateValid(true),
-    mIsFromExtension(aFromExtension)
+    mIsFromExtension(aFromExtension),
+    mBlocklistState(aBlocklistState)
 {
 }
 
 nsPluginTag::~nsPluginTag()
 {
   NS_ASSERTION(!mNext, "Risk of exhausting the stack space, bug 486349");
 }
 
@@ -543,19 +542,17 @@ nsPluginTag::GetDisabled(bool* aDisabled
 {
   *aDisabled = !IsEnabled();
   return NS_OK;
 }
 
 bool
 nsPluginTag::IsBlocklisted()
 {
-  uint32_t blocklistState;
-  nsresult rv = GetBlocklistState(&blocklistState);
-  return NS_FAILED(rv) || blocklistState == nsIBlocklistService::STATE_BLOCKED;
+  return mBlocklistState == nsIBlocklistService::STATE_BLOCKED;
 }
 
 NS_IMETHODIMP
 nsPluginTag::GetBlocklisted(bool* aBlocklisted)
 {
   *aBlocklisted = IsBlocklisted();
   return NS_OK;
 }
@@ -717,58 +714,30 @@ nsPluginTag::GetNiceName(nsACString & aR
 {
   aResult = GetNiceFileName();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsPluginTag::GetBlocklistState(uint32_t *aResult)
 {
-  // If we're in the content process, assume our cache state to always be valid,
-  // as the only way it can be updated is via a plugin list push from the
-  // parent process.
-  if (!XRE_IsParentProcess()) {
-    *aResult = mCachedBlocklistState;
-    return NS_OK;
-  }
-
-  nsCOMPtr<nsIBlocklistService> blocklist =
-    do_GetService("@mozilla.org/extensions/blocklist;1");
-
-  if (!blocklist) {
-    *aResult = nsIBlocklistService::STATE_NOT_BLOCKED;
-  }
-  // The EmptyString()s are so we use the currently running application
-  // and toolkit versions
-  else if (NS_FAILED(blocklist->GetPluginBlocklistState(this, EmptyString(),
-                                                   EmptyString(), aResult))) {
-    *aResult = nsIBlocklistService::STATE_NOT_BLOCKED;
-  }
-
-  MOZ_ASSERT(*aResult <= UINT16_MAX);
-  mCachedBlocklistState = (uint16_t) *aResult;
-  mCachedBlocklistStateValid = true;
+  *aResult = mBlocklistState;
   return NS_OK;
 }
 
 void
-nsPluginTag::SetBlocklistState(uint16_t aBlocklistState)
+nsPluginTag::SetBlocklistState(uint32_t aBlocklistState)
 {
-  // We should only ever call this on content processes. Any calls in the parent
-  // process should route through GetBlocklistState since we'll have the
-  // blocklist service there.
-  MOZ_ASSERT(!XRE_IsParentProcess());
-  mCachedBlocklistState = aBlocklistState;
-  mCachedBlocklistStateValid = true;
+  mBlocklistState = aBlocklistState;
 }
 
-void
-nsPluginTag::InvalidateBlocklistState()
+uint32_t
+nsPluginTag::BlocklistState()
 {
-  mCachedBlocklistStateValid = false;
+  return mBlocklistState;
 }
 
 NS_IMETHODIMP
 nsPluginTag::GetLastModifiedTime(PRTime* aLastModifiedTime)
 {
   MOZ_ASSERT(aLastModifiedTime);
   *aLastModifiedTime = mLastModifiedTime;
   return NS_OK;
--- a/dom/plugins/base/nsPluginTags.h
+++ b/dom/plugins/base/nsPluginTags.h
@@ -5,16 +5,17 @@
 
 #ifndef nsPluginTags_h_
 #define nsPluginTags_h_
 
 #include "mozilla/Attributes.h"
 #include "nscore.h"
 #include "nsCOMPtr.h"
 #include "nsCOMArray.h"
+#include "nsIBlocklistService.h"
 #include "nsIPluginTag.h"
 #include "nsITimer.h"
 #include "nsString.h"
 
 class nsIURI;
 struct PRLibrary;
 struct nsPluginInfo;
 class nsNPAPIPlugin;
@@ -103,58 +104,61 @@ public:
     ePluginState_Disabled = 0,
     ePluginState_Clicktoplay = 1,
     ePluginState_Enabled = 2,
     ePluginState_MaxValue = 3,
   };
 
   nsPluginTag(nsPluginInfo* aPluginInfo,
               int64_t aLastModifiedTime,
-              bool fromExtension);
+              bool fromExtension,
+              uint32_t aBlocklistState);
   nsPluginTag(const char* aName,
               const char* aDescription,
               const char* aFileName,
               const char* aFullPath,
               const char* aVersion,
               const char* const* aMimeTypes,
               const char* const* aMimeDescriptions,
               const char* const* aExtensions,
               int32_t aVariants,
               int64_t aLastModifiedTime,
               bool fromExtension,
+              uint32_t aBlocklistState,
               bool aArgsAreUTF8 = false);
   nsPluginTag(uint32_t aId,
               const char* aName,
               const char* aDescription,
               const char* aFileName,
               const char* aFullPath,
               const char* aVersion,
               nsTArray<nsCString> aMimeTypes,
               nsTArray<nsCString> aMimeDescriptions,
               nsTArray<nsCString> aExtensions,
               bool aIsFlashPlugin,
               bool aSupportsAsyncRender,
               int64_t aLastModifiedTime,
               bool aFromExtension,
               int32_t aSandboxLevel,
-              uint16_t aBlocklistState);
+              uint32_t aBlocklistState);
 
   void TryUnloadPlugin(bool inShutdown);
 
   // plugin is enabled and not blocklisted
   bool IsActive();
 
   bool IsEnabled() override;
   void SetEnabled(bool enabled);
   bool IsClicktoplay();
   bool IsBlocklisted();
+  uint32_t BlocklistState();
 
   PluginState GetPluginState();
   void SetPluginState(PluginState state);
-  void SetBlocklistState(uint16_t aBlocklistState);
+  void SetBlocklistState(uint32_t aBlocklistState);
 
   bool HasSameNameAndMimes(const nsPluginTag *aPluginTag) const;
   const nsCString& GetNiceFileName() override;
 
   bool IsFromExtension() const;
 
   RefPtr<nsPluginTag> mNext;
   uint32_t      mId;
@@ -170,25 +174,22 @@ public:
   bool          mIsFlashPlugin;
   bool          mSupportsAsyncRender;
   nsCString     mFullPath; // UTF-8
   int64_t       mLastModifiedTime;
   nsCOMPtr<nsITimer> mUnloadTimer;
   int32_t       mSandboxLevel;
   bool          mIsSandboxLoggingEnabled;
 
-  void          InvalidateBlocklistState();
-
 private:
   virtual ~nsPluginTag();
 
   nsCString     mNiceFileName; // UTF-8
-  uint16_t      mCachedBlocklistState;
-  bool          mCachedBlocklistStateValid;
   bool          mIsFromExtension;
+  uint32_t      mBlocklistState;
 
   void InitMime(const char* const* aMimeTypes,
                 const char* const* aMimeDescriptions,
                 const char* const* aExtensions,
                 uint32_t aVariantCount);
   void InitSandboxLevel();
   nsresult EnsureMembersAreUTF8();
   void FixupVersion();
--- a/gfx/layers/Layers.cpp
+++ b/gfx/layers/Layers.cpp
@@ -224,16 +224,18 @@ Layer::StartPendingAnimations(const Time
         }
       });
 }
 
 void
 Layer::SetAsyncPanZoomController(uint32_t aIndex, AsyncPanZoomController *controller)
 {
   MOZ_ASSERT(aIndex < GetScrollMetadataCount());
+  // We should never be setting an APZC on a non-scrollable layer
+  MOZ_ASSERT(!controller || GetFrameMetrics(aIndex).IsScrollable());
   mApzcs[aIndex] = controller;
 }
 
 AsyncPanZoomController*
 Layer::GetAsyncPanZoomController(uint32_t aIndex) const
 {
   MOZ_ASSERT(aIndex < GetScrollMetadataCount());
 #ifdef DEBUG
--- a/gfx/layers/Layers.h
+++ b/gfx/layers/Layers.h
@@ -1793,16 +1793,20 @@ public:
    * composited.
    */
   virtual void ClearInvalidRegion() { mInvalidRegion.SetEmpty(); }
 
   // These functions allow attaching an AsyncPanZoomController to this layer,
   // and can be used anytime.
   // A layer has an APZC at index aIndex only-if GetFrameMetrics(aIndex).IsScrollable();
   // attempting to get an APZC for a non-scrollable metrics will return null.
+  // The reverse is also true (that if GetFrameMetrics(aIndex).IsScrollable()
+  // is true, then the layer will have an APZC), although that only holds on
+  // the compositor-side layer tree, and only after the APZ code has had a chance
+  // to rebuild its internal hit-testing tree using the layer tree.
   // The aIndex for these functions must be less than GetScrollMetadataCount().
   void SetAsyncPanZoomController(uint32_t aIndex, AsyncPanZoomController *controller);
   AsyncPanZoomController* GetAsyncPanZoomController(uint32_t aIndex) const;
   // The ScrollMetadataChanged function is used internally to ensure the APZC array length
   // matches the frame metrics array length.
 
   virtual void ClearCachedResources() {}
 
--- a/gfx/layers/apz/public/APZSampler.h
+++ b/gfx/layers/apz/public/APZSampler.h
@@ -2,34 +2,39 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #ifndef mozilla_layers_APZSampler_h
 #define mozilla_layers_APZSampler_h
 
+#include "LayersTypes.h"
 #include "mozilla/layers/APZTestData.h"
+#include "mozilla/layers/AsyncCompositionManager.h" // for AsyncTransform
 #include "mozilla/Maybe.h"
 #include "nsTArray.h"
+#include "Units.h"
 
 namespace mozilla {
 
 class TimeStamp;
 
 namespace wr {
 class TransactionBuilder;
 struct WrTransformProperty;
 } // namespace wr
 
 namespace layers {
 
 class APZCTreeManager;
 class FocusTarget;
 class Layer;
+class LayerMetricsWrapper;
+struct ScrollThumbData;
 class WebRenderScrollData;
 
 /**
  * This interface is used to interact with the APZ code from the compositor
  * thread. It internally redispatches the functions to the sampler thread
  * in the case where the two threads are not the same.
  */
 class APZSampler {
@@ -65,16 +70,45 @@ public:
 
   void SetTestAsyncScrollOffset(uint64_t aLayersId,
                                 const FrameMetrics::ViewID& aScrollId,
                                 const CSSPoint& aOffset);
   void SetTestAsyncZoom(uint64_t aLayersId,
                         const FrameMetrics::ViewID& aScrollId,
                         const LayerToParentLayerScale& aZoom);
 
+  bool SampleAnimations(const LayerMetricsWrapper& aLayer,
+                        const TimeStamp& aSampleTime);
+
+  /**
+   * Compute the updated shadow transform for a scroll thumb layer that
+   * reflects async scrolling of the associated scroll frame.
+   *
+   * Refer to APZCTreeManager::ComputeTransformForScrollThumb for the
+   * description of parameters. The only difference is that this function takes
+   * |aContent| instead of |aApzc| and |aMetrics|; aContent is the
+   * LayerMetricsWrapper corresponding to the scroll frame that is scrolled by
+   * the scroll thumb, and so the APZC and metrics can be obtained from
+   * |aContent|.
+   */
+  LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
+      const LayerToParentLayerMatrix4x4& aCurrentTransform,
+      const LayerMetricsWrapper& aContent,
+      const ScrollThumbData& aThumbData,
+      bool aScrollbarIsDescendant,
+      AsyncTransformComponentMatrix* aOutClipTransform);
+
+  ParentLayerPoint GetCurrentAsyncScrollOffset(const LayerMetricsWrapper& aLayer);
+  AsyncTransform GetCurrentAsyncTransform(const LayerMetricsWrapper& aLayer);
+  AsyncTransformComponentMatrix GetOverscrollTransform(const LayerMetricsWrapper& aLayer);
+  AsyncTransformComponentMatrix GetCurrentAsyncTransformWithOverscroll(const LayerMetricsWrapper& aLayer);
+
+  void MarkAsyncTransformAppliedToContent(const LayerMetricsWrapper& aLayer);
+  bool HasUnusedAsyncTransform(const LayerMetricsWrapper& aLayer);
+
 protected:
   virtual ~APZSampler();
 
 private:
   RefPtr<APZCTreeManager> mApz;
 };
 
 } // namespace layers
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -604,17 +604,17 @@ APZCTreeManager::PushStateToWR(wr::Trans
           return;
         }
 
         HitTestingTreeNode* scrollTargetNode = it->second;
         AsyncPanZoomController* scrollTargetApzc = scrollTargetNode->GetApzc();
         MOZ_ASSERT(scrollTargetApzc);
         LayerToParentLayerMatrix4x4 transform = scrollTargetApzc->CallWithLastContentPaintMetrics(
             [&](const FrameMetrics& aMetrics) {
-                return AsyncCompositionManager::ComputeTransformForScrollThumb(
+                return ComputeTransformForScrollThumb(
                     aNode->GetTransform() * AsyncTransformMatrix(),
                     scrollTargetNode->GetTransform().ToUnknownMatrix(),
                     scrollTargetApzc,
                     aMetrics,
                     aNode->GetScrollThumbData(),
                     scrollTargetNode->IsAncestorOf(aNode),
                     nullptr);
             });
@@ -2931,17 +2931,17 @@ APZCTreeManager::ComputeTransformForNode
     // transformation that will be applied to the thumb in
     // AsyncCompositionManager.
     ScrollableLayerGuid guid{aNode->GetLayersId(), 0, aNode->GetScrollTargetId()};
     if (RefPtr<HitTestingTreeNode> scrollTargetNode = GetTargetNode(guid, &GuidComparatorIgnoringPresShell)) {
       AsyncPanZoomController* scrollTargetApzc = scrollTargetNode->GetApzc();
       MOZ_ASSERT(scrollTargetApzc);
       return scrollTargetApzc->CallWithLastContentPaintMetrics(
         [&](const FrameMetrics& aMetrics) {
-          return AsyncCompositionManager::ComputeTransformForScrollThumb(
+          return ComputeTransformForScrollThumb(
               aNode->GetTransform() * AsyncTransformMatrix(),
               scrollTargetNode->GetTransform().ToUnknownMatrix(),
               scrollTargetApzc,
               aMetrics,
               aNode->GetScrollThumbData(),
               scrollTargetNode->IsAncestorOf(aNode),
               nullptr);
         });
@@ -2984,16 +2984,178 @@ APZCTreeManager::GetAPZTestData(uint64_t
   auto it = mTestData.find(aLayersId);
   if (it == mTestData.end()) {
     return false;
   }
   *aOutData = *(it->second);
   return true;
 }
 
+/*static*/ LayerToParentLayerMatrix4x4
+APZCTreeManager::ComputeTransformForScrollThumb(
+    const LayerToParentLayerMatrix4x4& aCurrentTransform,
+    const Matrix4x4& aScrollableContentTransform,
+    AsyncPanZoomController* aApzc,
+    const FrameMetrics& aMetrics,
+    const ScrollThumbData& aThumbData,
+    bool aScrollbarIsDescendant,
+    AsyncTransformComponentMatrix* aOutClipTransform)
+{
+  // We only apply the transform if the scroll-target layer has non-container
+  // children (i.e. when it has some possibly-visible content). This is to
+  // avoid moving scroll-bars in the situation that only a scroll information
+  // layer has been built for a scroll frame, as this would result in a
+  // disparity between scrollbars and visible content.
+  if (aMetrics.IsScrollInfoLayer()) {
+    return LayerToParentLayerMatrix4x4{};
+  }
+
+  MOZ_RELEASE_ASSERT(aApzc);
+
+  AsyncTransformComponentMatrix asyncTransform =
+    aApzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);
+
+  // |asyncTransform| represents the amount by which we have scrolled and
+  // zoomed since the last paint. Because the scrollbar was sized and positioned based
+  // on the painted content, we need to adjust it based on asyncTransform so that
+  // it reflects what the user is actually seeing now.
+  AsyncTransformComponentMatrix scrollbarTransform;
+  if (*aThumbData.mDirection == ScrollDirection::eVertical) {
+    const ParentLayerCoord asyncScrollY = asyncTransform._42;
+    const float asyncZoomY = asyncTransform._22;
+
+    // The scroll thumb needs to be scaled in the direction of scrolling by the
+    // inverse of the async zoom. This is because zooming in decreases the
+    // fraction of the whole srollable rect that is in view.
+    const float yScale = 1.f / asyncZoomY;
+
+    // Note: |metrics.GetZoom()| doesn't yet include the async zoom.
+    const CSSToParentLayerScale effectiveZoom(aMetrics.GetZoom().yScale * asyncZoomY);
+
+    // Here we convert the scrollbar thumb ratio into a true unitless ratio by
+    // dividing out the conversion factor from the scrollframe's parent's space
+    // to the scrollframe's space.
+    const float ratio = aThumbData.mThumbRatio /
+        (aMetrics.GetPresShellResolution() * asyncZoomY);
+    // The scroll thumb needs to be translated in opposite direction of the
+    // async scroll. This is because scrolling down, which translates the layer
+    // content up, should result in moving the scroll thumb down.
+    ParentLayerCoord yTranslation = -asyncScrollY * ratio;
+
+    // The scroll thumb additionally needs to be translated to compensate for
+    // the scale applied above. The origin with respect to which the scale is
+    // applied is the origin of the entire scrollbar, rather than the origin of
+    // the scroll thumb (meaning, for a vertical scrollbar it's at the top of
+    // the composition bounds). This means that empty space above the thumb
+    // is scaled too, effectively translating the thumb. We undo that
+    // translation here.
+    // (One can think of the adjustment being done to the translation here as
+    // a change of basis. We have a method to help with that,
+    // Matrix4x4::ChangeBasis(), but it wouldn't necessarily make the code
+    // cleaner in this case).
+    const CSSCoord thumbOrigin = (aMetrics.GetScrollOffset().y * ratio);
+    const CSSCoord thumbOriginScaled = thumbOrigin * yScale;
+    const CSSCoord thumbOriginDelta = thumbOriginScaled - thumbOrigin;
+    const ParentLayerCoord thumbOriginDeltaPL = thumbOriginDelta * effectiveZoom;
+    yTranslation -= thumbOriginDeltaPL;
+
+    if (aMetrics.IsRootContent()) {
+      // Scrollbar for the root are painted at the same resolution as the
+      // content. Since the coordinate space we apply this transform in includes
+      // the resolution, we need to adjust for it as well here. Note that in
+      // another metrics.IsRootContent() hunk below we apply a
+      // resolution-cancelling transform which ensures the scroll thumb isn't
+      // actually rendered at a larger scale.
+      yTranslation *= aMetrics.GetPresShellResolution();
+    }
+
+    scrollbarTransform.PostScale(1.f, yScale, 1.f);
+    scrollbarTransform.PostTranslate(0, yTranslation, 0);
+  }
+  if (*aThumbData.mDirection == ScrollDirection::eHorizontal) {
+    // See detailed comments under the VERTICAL case.
+
+    const ParentLayerCoord asyncScrollX = asyncTransform._41;
+    const float asyncZoomX = asyncTransform._11;
+
+    const float xScale = 1.f / asyncZoomX;
+
+    const CSSToParentLayerScale effectiveZoom(aMetrics.GetZoom().xScale * asyncZoomX);
+
+    const float ratio = aThumbData.mThumbRatio /
+        (aMetrics.GetPresShellResolution() * asyncZoomX);
+    ParentLayerCoord xTranslation = -asyncScrollX * ratio;
+
+    const CSSCoord thumbOrigin = (aMetrics.GetScrollOffset().x * ratio);
+    const CSSCoord thumbOriginScaled = thumbOrigin * xScale;
+    const CSSCoord thumbOriginDelta = thumbOriginScaled - thumbOrigin;
+    const ParentLayerCoord thumbOriginDeltaPL = thumbOriginDelta * effectiveZoom;
+    xTranslation -= thumbOriginDeltaPL;
+
+    if (aMetrics.IsRootContent()) {
+      xTranslation *= aMetrics.GetPresShellResolution();
+    }
+
+    scrollbarTransform.PostScale(xScale, 1.f, 1.f);
+    scrollbarTransform.PostTranslate(xTranslation, 0, 0);
+  }
+
+  LayerToParentLayerMatrix4x4 transform =
+      aCurrentTransform * scrollbarTransform;
+
+  AsyncTransformComponentMatrix compensation;
+  // If the scrollbar layer is for the root then the content's resolution
+  // applies to the scrollbar as well. Since we don't actually want the scroll
+  // thumb's size to vary with the zoom (other than its length reflecting the
+  // fraction of the scrollable length that's in view, which is taken care of
+  // above), we apply a transform to cancel out this resolution.
+  if (aMetrics.IsRootContent()) {
+    compensation =
+        AsyncTransformComponentMatrix::Scaling(
+            aMetrics.GetPresShellResolution(),
+            aMetrics.GetPresShellResolution(),
+            1.0f).Inverse();
+  }
+  // If the scrollbar layer is a child of the content it is a scrollbar for,
+  // then we need to adjust for any async transform (including an overscroll
+  // transform) on the content. This needs to be cancelled out because layout
+  // positions and sizes the scrollbar on the assumption that there is no async
+  // transform, and without this adjustment the scrollbar will end up in the
+  // wrong place.
+  //
+  // Note that since the async transform is applied on top of the content's
+  // regular transform, we need to make sure to unapply the async transform in
+  // the same coordinate space. This requires applying the content transform
+  // and then unapplying it after unapplying the async transform.
+  if (aScrollbarIsDescendant) {
+    AsyncTransformComponentMatrix overscroll =
+        aApzc->GetOverscrollTransform(AsyncPanZoomController::eForCompositing);
+    Matrix4x4 asyncUntransform = (asyncTransform * overscroll).Inverse().ToUnknownMatrix();
+    Matrix4x4 contentTransform = aScrollableContentTransform;
+    Matrix4x4 contentUntransform = contentTransform.Inverse();
+
+    AsyncTransformComponentMatrix asyncCompensation =
+        ViewAs<AsyncTransformComponentMatrix>(
+            contentTransform
+          * asyncUntransform
+          * contentUntransform);
+
+    compensation = compensation * asyncCompensation;
+
+    // Pass the async compensation out to the caller so that it can use it
+    // to transform clip transforms as needed.
+    if (aOutClipTransform) {
+      *aOutClipTransform = asyncCompensation;
+    }
+  }
+  transform = transform * compensation;
+
+  return transform;
+}
+
 #if defined(MOZ_WIDGET_ANDROID)
 AndroidDynamicToolbarAnimator*
 APZCTreeManager::GetAndroidDynamicToolbarAnimator()
 {
   return mToolbarAnimator;
 }
 #endif // defined(MOZ_WIDGET_ANDROID)
 
--- a/gfx/layers/apz/src/APZCTreeManager.h
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -50,16 +50,17 @@ class FocusTarget;
 struct FlingHandoffState;
 struct ScrollableLayerGuidHash;
 class LayerMetricsWrapper;
 class InputQueue;
 class GeckoContentController;
 class HitTestingTreeNode;
 class WebRenderScrollData;
 struct AncestorTransform;
+struct ScrollThumbData;
 
 /**
  * ****************** NOTE ON LOCK ORDERING IN APZ **************************
  *
  * There are two main kinds of locks used by APZ: APZCTreeManager::mTreeLock
  * ("the tree lock") and AsyncPanZoomController::mRecursiveMutex ("APZC locks").
  * There is also the APZCTreeManager::mTestDataLock ("test lock").
  *
@@ -479,16 +480,48 @@ public:
       uint64_t*             aOutFocusSequenceNumber) override;
 
   void UpdateWheelTransaction(
       LayoutDeviceIntPoint aRefPoint,
       EventMessage aEventMessage) override;
 
   bool GetAPZTestData(uint64_t aLayersId, APZTestData* aOutData);
 
+  /**
+   * Compute the updated shadow transform for a scroll thumb layer that
+   * reflects async scrolling of the associated scroll frame.
+   *
+   * @param aCurrentTransform The current shadow transform on the scroll thumb
+   *    layer, as returned by Layer::GetLocalTransform() or similar.
+   * @param aScrollableContentTransform The current content transform on the
+   *    scrollable content, as returned by Layer::GetTransform().
+   * @param aApzc The APZC that scrolls the scroll frame.
+   * @param aMetrics The metrics associated with the scroll frame, reflecting
+   *    the last paint of the associated content. Note: this metrics should
+   *    NOT reflect async scrolling, i.e. they should be the layer tree's
+   *    copy of the metrics, or APZC's last-content-paint metrics.
+   * @param aThumbData The scroll thumb data for the the scroll thumb layer.
+   * @param aScrollbarIsDescendant True iff. the scroll thumb layer is a
+   *    descendant of the layer bearing the scroll frame's metrics.
+   * @param aOutClipTransform If not null, and |aScrollbarIsDescendant| is true,
+   *    this will be populated with a transform that should be applied to the
+   *    clip rects of all layers between the scroll thumb layer and the ancestor
+   *    layer for the scrollable content.
+   * @return The new shadow transform for the scroll thumb layer, including
+   *    any pre- or post-scales.
+   */
+  static LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
+      const LayerToParentLayerMatrix4x4& aCurrentTransform,
+      const gfx::Matrix4x4& aScrollableContentTransform,
+      AsyncPanZoomController* aApzc,
+      const FrameMetrics& aMetrics,
+      const ScrollThumbData& aThumbData,
+      bool aScrollbarIsDescendant,
+      AsyncTransformComponentMatrix* aOutClipTransform);
+
 protected:
   // Protected destructor, to discourage deletion outside of Release():
   virtual ~APZCTreeManager();
 
   // Protected hooks for gtests subclass
   virtual AsyncPanZoomController* NewAPZCInstance(uint64_t aLayersId,
                                                   GeckoContentController* aController);
 public:
--- a/gfx/layers/apz/src/APZSampler.cpp
+++ b/gfx/layers/apz/src/APZSampler.cpp
@@ -2,17 +2,20 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "mozilla/layers/APZSampler.h"
 
 #include "APZCTreeManager.h"
+#include "AsyncPanZoomController.h"
 #include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/LayerMetricsWrapper.h"
+#include "TreeTraversal.h"
 
 namespace mozilla {
 namespace layers {
 
 APZSampler::APZSampler(const RefPtr<APZCTreeManager>& aApz)
   : mApz(aApz)
 {
 }
@@ -117,10 +120,95 @@ APZSampler::SetTestAsyncZoom(uint64_t aL
   RefPtr<AsyncPanZoomController> apzc = mApz->GetTargetAPZC(aLayersId, aScrollId);
   if (apzc) {
     apzc->SetTestAsyncZoom(aZoom);
   } else {
     NS_WARNING("Unable to find APZC in SetTestAsyncZoom");
   }
 }
 
+bool
+APZSampler::SampleAnimations(const LayerMetricsWrapper& aLayer,
+                             const TimeStamp& aSampleTime)
+{
+  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+  // TODO: eventually we can drop the aLayer argument and just walk the APZ
+  // tree directly in mApz.
+
+  bool activeAnimations = false;
+
+  ForEachNodePostOrder<ForwardIterator>(aLayer,
+      [&activeAnimations, &aSampleTime](LayerMetricsWrapper aLayerMetrics)
+      {
+        if (AsyncPanZoomController* apzc = aLayerMetrics.GetApzc()) {
+          apzc->ReportCheckerboard(aSampleTime);
+          activeAnimations |= apzc->AdvanceAnimations(aSampleTime);
+        }
+      }
+  );
+
+  return activeAnimations;
+}
+
+LayerToParentLayerMatrix4x4
+APZSampler::ComputeTransformForScrollThumb(const LayerToParentLayerMatrix4x4& aCurrentTransform,
+                                           const LayerMetricsWrapper& aContent,
+                                           const ScrollThumbData& aThumbData,
+                                           bool aScrollbarIsDescendant,
+                                           AsyncTransformComponentMatrix* aOutClipTransform)
+{
+  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+  return mApz->ComputeTransformForScrollThumb(aCurrentTransform,
+                                              aContent.GetTransform(),
+                                              aContent.GetApzc(),
+                                              aContent.Metrics(),
+                                              aThumbData,
+                                              aScrollbarIsDescendant,
+                                              aOutClipTransform);
+}
+
+ParentLayerPoint
+APZSampler::GetCurrentAsyncScrollOffset(const LayerMetricsWrapper& aLayer)
+{
+  MOZ_ASSERT(aLayer.GetApzc());
+  return aLayer.GetApzc()->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing);
+}
+
+AsyncTransform
+APZSampler::GetCurrentAsyncTransform(const LayerMetricsWrapper& aLayer)
+{
+  MOZ_ASSERT(aLayer.GetApzc());
+  return aLayer.GetApzc()->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);
+}
+
+AsyncTransformComponentMatrix
+APZSampler::GetOverscrollTransform(const LayerMetricsWrapper& aLayer)
+{
+  MOZ_ASSERT(aLayer.GetApzc());
+  return aLayer.GetApzc()->GetOverscrollTransform(AsyncPanZoomController::eForCompositing);
+}
+
+AsyncTransformComponentMatrix
+APZSampler::GetCurrentAsyncTransformWithOverscroll(const LayerMetricsWrapper& aLayer)
+{
+  MOZ_ASSERT(aLayer.GetApzc());
+  return aLayer.GetApzc()->GetCurrentAsyncTransformWithOverscroll(AsyncPanZoomController::eForCompositing);
+}
+
+void
+APZSampler::MarkAsyncTransformAppliedToContent(const LayerMetricsWrapper& aLayer)
+{
+  MOZ_ASSERT(aLayer.GetApzc());
+  aLayer.GetApzc()->MarkAsyncTransformAppliedToContent();
+}
+
+bool
+APZSampler::HasUnusedAsyncTransform(const LayerMetricsWrapper& aLayer)
+{
+  AsyncPanZoomController* apzc = aLayer.GetApzc();
+  return apzc
+      && !apzc->GetAsyncTransformAppliedToContent()
+      && !AsyncTransformComponentMatrix(apzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForHitTesting)).IsIdentity();
+}
+
 } // namespace layers
 } // namespace mozilla
--- a/gfx/layers/composite/AsyncCompositionManager.cpp
+++ b/gfx/layers/composite/AsyncCompositionManager.cpp
@@ -1,29 +1,29 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "mozilla/layers/AsyncCompositionManager.h"
 #include <stdint.h>                     // for uint32_t
-#include "apz/src/AsyncPanZoomController.h"
 #include "FrameMetrics.h"               // for FrameMetrics
 #include "LayerManagerComposite.h"      // for LayerManagerComposite, etc
 #include "Layers.h"                     // for Layer, ContainerLayer, etc
 #include "gfxPoint.h"                   // for gfxPoint, gfxSize
 #include "gfxPrefs.h"                   // for gfxPrefs
 #include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc
 #include "mozilla/WidgetUtils.h"        // for ComputeTransformForRotation
 #include "mozilla/gfx/BaseRect.h"       // for BaseRect
 #include "mozilla/gfx/Point.h"          // for RoundedToInt, PointTyped
 #include "mozilla/gfx/Rect.h"           // for RoundedToInt, RectTyped
 #include "mozilla/gfx/ScaleFactor.h"    // for ScaleFactor
 #include "mozilla/layers/AnimationHelper.h"
+#include "mozilla/layers/APZSampler.h"  // for APZSampler
 #include "mozilla/layers/APZUtils.h"    // for CompleteAsyncTransform
 #include "mozilla/layers/Compositor.h"  // for Compositor
 #include "mozilla/layers/CompositorBridgeParent.h" // for CompositorBridgeParent, etc
 #include "mozilla/layers/CompositorThread.h"
 #include "mozilla/layers/LayerAnimationUtils.h" // for TimingFunctionToComputedTimingFunction
 #include "mozilla/layers/LayerMetricsWrapper.h" // for LayerMetricsWrapper
 #include "nsCoord.h"                    // for NSAppUnitsToFloatPixels, etc
 #include "nsDebug.h"                    // for NS_ASSERTION, etc
@@ -63,24 +63,25 @@ IsSameDimension(dom::ScreenOrientationIn
 }
 
 static bool
 ContentMightReflowOnOrientationChange(const IntRect& rect)
 {
   return rect.Width() != rect.Height();
 }
 
-  AsyncCompositionManager::AsyncCompositionManager(CompositorBridgeParent* aParent,
-                                                   HostLayerManager* aManager)
+AsyncCompositionManager::AsyncCompositionManager(CompositorBridgeParent* aParent,
+                                                 HostLayerManager* aManager)
   : mLayerManager(aManager)
   , mIsFirstPaint(true)
   , mLayersUpdated(false)
   , mReadyForCompose(true)
   , mCompositorBridge(aParent)
 {
+  MOZ_ASSERT(mCompositorBridge);
 }
 
 AsyncCompositionManager::~AsyncCompositionManager()
 {
 }
 
 void
 AsyncCompositionManager::ResolveRefLayers(CompositorBridgeParent* aCompositor,
@@ -690,47 +691,28 @@ SampleAnimations(Layer* aLayer,
         if (ancestorRefLayer && aLayer->AsRefLayer() == ancestorRefLayer) {
           ancestorRefLayer = nullptr;
         }
       });
 
   return animProcess;
 }
 
-static bool
-SampleAPZAnimations(const LayerMetricsWrapper& aLayer, TimeStamp aSampleTime)
-{
-  bool activeAnimations = false;
-
-  ForEachNodePostOrder<ForwardIterator>(aLayer,
-      [&activeAnimations, &aSampleTime](LayerMetricsWrapper aLayerMetrics)
-      {
-        if (AsyncPanZoomController* apzc = aLayerMetrics.GetApzc()) {
-          apzc->ReportCheckerboard(aSampleTime);
-          activeAnimations |= apzc->AdvanceAnimations(aSampleTime);
-        }
-      }
-  );
-
-  return activeAnimations;
-}
-
 void
 AsyncCompositionManager::RecordShadowTransforms(Layer* aLayer)
 {
   MOZ_ASSERT(gfxPrefs::CollectScrollTransforms());
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
 
   ForEachNodePostOrder<ForwardIterator>(
       aLayer,
       [this] (Layer* layer)
       {
         for (uint32_t i = 0; i < layer->GetScrollMetadataCount(); i++) {
-          AsyncPanZoomController* apzc = layer->GetAsyncPanZoomController(i);
-          if (!apzc) {
+          if (!layer->GetFrameMetrics(i).IsScrollable()) {
             continue;
           }
           gfx::Matrix4x4 shadowTransform = layer->AsHostLayer()->GetShadowBaseTransform();
           if (!shadowTransform.Is2D()) {
             continue;
           }
 
           Matrix transform = shadowTransform.As2D();
@@ -896,154 +878,156 @@ AsyncCompositionManager::ApplyAsyncConte
         // which is moved by all async scrolls on this layer.
         if (const Maybe<LayerClip>& scrolledClip = layer->GetScrolledClip()) {
           if (scrolledClip->GetMaskLayerIndex()) {
             ancestorMaskLayers.AppendElement(
                 layer->GetAncestorMaskLayerAt(*scrolledClip->GetMaskLayerIndex()));
           }
         }
 
-        for (uint32_t i = 0; i < layer->GetScrollMetadataCount(); i++) {
-          AsyncPanZoomController* controller = layer->GetAsyncPanZoomController(i);
-          if (!controller) {
-            continue;
-          }
+        if (RefPtr<APZSampler> sampler = mCompositorBridge->GetAPZSampler()) {
+          for (uint32_t i = 0; i < layer->GetScrollMetadataCount(); i++) {
+            LayerMetricsWrapper wrapper(layer, i);
+            const FrameMetrics& metrics = wrapper.Metrics();
+            if (!metrics.IsScrollable()) {
+              continue;
+            }
 
-          hasAsyncTransform = true;
+            hasAsyncTransform = true;
 
-          AsyncTransform asyncTransformWithoutOverscroll =
-              controller->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);
-          AsyncTransformComponentMatrix overscrollTransform =
-              controller->GetOverscrollTransform(AsyncPanZoomController::eForCompositing);
-          AsyncTransformComponentMatrix asyncTransform =
-              AsyncTransformComponentMatrix(asyncTransformWithoutOverscroll)
-            * overscrollTransform;
+            AsyncTransform asyncTransformWithoutOverscroll =
+                sampler->GetCurrentAsyncTransform(wrapper);
+            AsyncTransformComponentMatrix overscrollTransform =
+                sampler->GetOverscrollTransform(wrapper);
+            AsyncTransformComponentMatrix asyncTransform =
+                AsyncTransformComponentMatrix(asyncTransformWithoutOverscroll)
+              * overscrollTransform;
 
-          if (!layer->IsScrollableWithoutContent()) {
-            controller->MarkAsyncTransformAppliedToContent();
-          }
+            if (!layer->IsScrollableWithoutContent()) {
+              sampler->MarkAsyncTransformAppliedToContent(wrapper);
+            }
 
-          const ScrollMetadata& scrollMetadata = layer->GetScrollMetadata(i);
-          const FrameMetrics& metrics = scrollMetadata.GetMetrics();
+            const ScrollMetadata& scrollMetadata = wrapper.Metadata();
 
 #if defined(MOZ_WIDGET_ANDROID)
-          // If we find a metrics which is the root content doc, use that. If not, use
-          // the root layer. Since this function recurses on children first we should
-          // only end up using the root layer if the entire tree was devoid of a
-          // root content metrics. This is a temporary solution; in the long term we
-          // should not need the root content metrics at all. See bug 1201529 comment
-          // 6 for details.
-          if (!(*aOutFoundRoot)) {
-            *aOutFoundRoot = metrics.IsRootContent() ||       /* RCD */
-                  (layer->GetParent() == nullptr &&          /* rootmost metrics */
-                   i + 1 >= layer->GetScrollMetadataCount());
-            if (*aOutFoundRoot) {
-              mRootScrollableId = metrics.GetScrollId();
-              Compositor* compositor = mLayerManager->GetCompositor();
-              if (CompositorBridgeParent* bridge = compositor->GetCompositorBridgeParent()) {
-                AndroidDynamicToolbarAnimator* animator = bridge->GetAndroidDynamicToolbarAnimator();
-                MOZ_ASSERT(animator);
-                if (mIsFirstPaint) {
-                  animator->UpdateRootFrameMetrics(metrics);
-                  animator->FirstPaint();
-                  mIsFirstPaint = false;
+            // If we find a metrics which is the root content doc, use that. If not, use
+            // the root layer. Since this function recurses on children first we should
+            // only end up using the root layer if the entire tree was devoid of a
+            // root content metrics. This is a temporary solution; in the long term we
+            // should not need the root content metrics at all. See bug 1201529 comment
+            // 6 for details.
+            if (!(*aOutFoundRoot)) {
+              *aOutFoundRoot = metrics.IsRootContent() ||       /* RCD */
+                    (layer->GetParent() == nullptr &&          /* rootmost metrics */
+                     i + 1 >= layer->GetScrollMetadataCount());
+              if (*aOutFoundRoot) {
+                mRootScrollableId = metrics.GetScrollId();
+                Compositor* compositor = mLayerManager->GetCompositor();
+                if (CompositorBridgeParent* bridge = compositor->GetCompositorBridgeParent()) {
+                  AndroidDynamicToolbarAnimator* animator = bridge->GetAndroidDynamicToolbarAnimator();
+                  MOZ_ASSERT(animator);
+                  if (mIsFirstPaint) {
+                    animator->UpdateRootFrameMetrics(metrics);
+                    animator->FirstPaint();
+                    mIsFirstPaint = false;
+                  }
+                  if (mLayersUpdated) {
+                    animator->NotifyLayersUpdated();
+                    mLayersUpdated = false;
+                  }
+                  // If this is not actually the root content then the animator is not getting updated in AsyncPanZoomController::NotifyLayersUpdated
+                  // because the root content document is not scrollable. So update it here so it knows if the root composition size has changed.
+                  if (!metrics.IsRootContent()) {
+                    animator->MaybeUpdateCompositionSizeAndRootFrameMetrics(metrics);
+                  }
                 }
-                if (mLayersUpdated) {
-                  animator->NotifyLayersUpdated();
-                  mLayersUpdated = false;
-                }
-                // If this is not actually the root content then the animator is not getting updated in AsyncPanZoomController::NotifyLayersUpdated
-                // because the root content document is not scrollable. So update it here so it knows if the root composition size has changed.
-                if (!metrics.IsRootContent()) {
-                  animator->MaybeUpdateCompositionSizeAndRootFrameMetrics(metrics);
-                }
+                fixedLayerMargins = mFixedLayerMargins;
               }
-              fixedLayerMargins = mFixedLayerMargins;
             }
-          }
 #else
-          *aOutFoundRoot = false;
-          // Non-Android platforms still care about this flag being cleared after
-          // the first call to TransformShadowTree().
-          mIsFirstPaint = false;
+            *aOutFoundRoot = false;
+            // Non-Android platforms still care about this flag being cleared after
+            // the first call to TransformShadowTree().
+            mIsFirstPaint = false;
 #endif
 
-          // Transform the current local clips by this APZC's async transform. If we're
-          // using containerful scrolling, then the clip is not part of the scrolled
-          // frame and should not be transformed.
-          if (!scrollMetadata.UsesContainerScrolling()) {
-            MOZ_ASSERT(asyncTransform.Is2D());
-            if (clipParts.mFixedClip) {
-              *clipParts.mFixedClip = TransformBy(asyncTransform, *clipParts.mFixedClip);
+            // Transform the current local clips by this APZC's async transform. If we're
+            // using containerful scrolling, then the clip is not part of the scrolled
+            // frame and should not be transformed.
+            if (!scrollMetadata.UsesContainerScrolling()) {
+              MOZ_ASSERT(asyncTransform.Is2D());
+              if (clipParts.mFixedClip) {
+                *clipParts.mFixedClip = TransformBy(asyncTransform, *clipParts.mFixedClip);
+              }
+              if (clipParts.mScrolledClip) {
+                *clipParts.mScrolledClip = TransformBy(asyncTransform, *clipParts.mScrolledClip);
+              }
             }
-            if (clipParts.mScrolledClip) {
-              *clipParts.mScrolledClip = TransformBy(asyncTransform, *clipParts.mScrolledClip);
-            }
-          }
-          // Note: we don't set the layer's shadow clip rect property yet;
-          // AlignFixedAndStickyLayers will use the clip parts from the clip parts
-          // cache.
+            // Note: we don't set the layer's shadow clip rect property yet;
+            // AlignFixedAndStickyLayers will use the clip parts from the clip parts
+            // cache.
 
-          combinedAsyncTransform *= asyncTransform;
+            combinedAsyncTransform *= asyncTransform;
 
-          // For the purpose of aligning fixed and sticky layers, we disregard
-          // the overscroll transform as well as any OMTA transform when computing the
-          // 'aCurrentTransformForRoot' parameter. This ensures that the overscroll
-          // and OMTA transforms are not unapplied, and therefore that the visual
-          // effects apply to fixed and sticky layers. We do this by using
-          // GetTransform() as the base transform rather than GetLocalTransform(),
-          // which would include those factors.
-          LayerToParentLayerMatrix4x4 transformWithoutOverscrollOrOmta =
-              layer->GetTransformTyped()
-            * CompleteAsyncTransform(
-                AdjustForClip(asyncTransformWithoutOverscroll, layer));
+            // For the purpose of aligning fixed and sticky layers, we disregard
+            // the overscroll transform as well as any OMTA transform when computing the
+            // 'aCurrentTransformForRoot' parameter. This ensures that the overscroll
+            // and OMTA transforms are not unapplied, and therefore that the visual
+            // effects apply to fixed and sticky layers. We do this by using
+            // GetTransform() as the base transform rather than GetLocalTransform(),
+            // which would include those factors.
+            LayerToParentLayerMatrix4x4 transformWithoutOverscrollOrOmta =
+                layer->GetTransformTyped()
+              * CompleteAsyncTransform(
+                  AdjustForClip(asyncTransformWithoutOverscroll, layer));
 
-          AlignFixedAndStickyLayers(layer, layer, metrics.GetScrollId(), oldTransform,
-                                    transformWithoutOverscrollOrOmta, fixedLayerMargins,
-                                    &clipPartsCache);
+            AlignFixedAndStickyLayers(layer, layer, metrics.GetScrollId(), oldTransform,
+                                      transformWithoutOverscrollOrOmta, fixedLayerMargins,
+                                      &clipPartsCache);
 
-          // Combine the local clip with the ancestor scrollframe clip. This is not
-          // included in the async transform above, since the ancestor clip should not
-          // move with this APZC.
-          if (scrollMetadata.HasScrollClip()) {
-            ParentLayerIntRect clip = scrollMetadata.ScrollClip().GetClipRect();
-            if (layer->GetParent() && layer->GetParent()->GetTransformIsPerspective()) {
-              // If our parent layer has a perspective transform, we want to apply
-              // our scroll clip to it instead of to this layer (see bug 1168263).
-              // A layer with a perspective transform shouldn't have multiple
-              // children with FrameMetrics, nor a child with multiple FrameMetrics.
-              // (A child with multiple FrameMetrics would mean that there's *another*
-              // scrollable element between the one with the CSS perspective and the
-              // transformed element. But you'd have to use preserve-3d on the inner
-              // scrollable element in order to have the perspective apply to the
-              // transformed child, and preserve-3d is not supported on scrollable
-              // elements, so this case can't occur.)
-              MOZ_ASSERT(!stackDeferredClips.top());
-              stackDeferredClips.top().emplace(clip);
-            } else {
-              clipParts.mScrolledClip = IntersectMaybeRects(Some(clip),
-                  clipParts.mScrolledClip);
+            // Combine the local clip with the ancestor scrollframe clip. This is not
+            // included in the async transform above, since the ancestor clip should not
+            // move with this APZC.
+            if (scrollMetadata.HasScrollClip()) {
+              ParentLayerIntRect clip = scrollMetadata.ScrollClip().GetClipRect();
+              if (layer->GetParent() && layer->GetParent()->GetTransformIsPerspective()) {
+                // If our parent layer has a perspective transform, we want to apply
+                // our scroll clip to it instead of to this layer (see bug 1168263).
+                // A layer with a perspective transform shouldn't have multiple
+                // children with FrameMetrics, nor a child with multiple FrameMetrics.
+                // (A child with multiple FrameMetrics would mean that there's *another*
+                // scrollable element between the one with the CSS perspective and the
+                // transformed element. But you'd have to use preserve-3d on the inner
+                // scrollable element in order to have the perspective apply to the
+                // transformed child, and preserve-3d is not supported on scrollable
+                // elements, so this case can't occur.)
+                MOZ_ASSERT(!stackDeferredClips.top());
+                stackDeferredClips.top().emplace(clip);
+              } else {
+                clipParts.mScrolledClip = IntersectMaybeRects(Some(clip),
+                    clipParts.mScrolledClip);
+              }
             }
-          }
+
+            // Do the same for the ancestor mask layers: ancestorMaskLayers contains
+            // the ancestor mask layers for scroll frames *inside* the current scroll
+            // frame, so these are the ones we need to shift by our async transform.
+            for (Layer* ancestorMaskLayer : ancestorMaskLayers) {
+              SetShadowTransform(ancestorMaskLayer,
+                  ancestorMaskLayer->GetLocalTransformTyped() * asyncTransform);
+            }
 
-          // Do the same for the ancestor mask layers: ancestorMaskLayers contains
-          // the ancestor mask layers for scroll frames *inside* the current scroll
-          // frame, so these are the ones we need to shift by our async transform.
-          for (Layer* ancestorMaskLayer : ancestorMaskLayers) {
-            SetShadowTransform(ancestorMaskLayer,
-                ancestorMaskLayer->GetLocalTransformTyped() * asyncTransform);
-          }
-
-          // Append the ancestor mask layer for this scroll frame to ancestorMaskLayers.
-          if (scrollMetadata.HasScrollClip()) {
-            const LayerClip& scrollClip = scrollMetadata.ScrollClip();
-            if (scrollClip.GetMaskLayerIndex()) {
-              size_t maskLayerIndex = scrollClip.GetMaskLayerIndex().value();
-              Layer* ancestorMaskLayer = layer->GetAncestorMaskLayerAt(maskLayerIndex);
-              ancestorMaskLayers.AppendElement(ancestorMaskLayer);
+            // Append the ancestor mask layer for this scroll frame to ancestorMaskLayers.
+            if (scrollMetadata.HasScrollClip()) {
+              const LayerClip& scrollClip = scrollMetadata.ScrollClip();
+              if (scrollClip.GetMaskLayerIndex()) {
+                size_t maskLayerIndex = scrollClip.GetMaskLayerIndex().value();
+                Layer* ancestorMaskLayer = layer->GetAncestorMaskLayerAt(maskLayerIndex);
+                ancestorMaskLayers.AppendElement(ancestorMaskLayer);
+              }
             }
           }
         }
 
         bool clipChanged = (hasAsyncTransform || clipDeferredFromChildren ||
                             layer->GetScrolledClipRect());
         if (clipChanged) {
           // Intersect the two clip parts and apply them to the layer.
@@ -1080,40 +1064,39 @@ AsyncCompositionManager::ApplyAsyncConte
       });
 
   return appliedTransform;
 }
 
 static bool
 LayerIsScrollbarTarget(const LayerMetricsWrapper& aTarget, Layer* aScrollbar)
 {
-  AsyncPanZoomController* apzc = aTarget.GetApzc();
-  if (!apzc) {
+  const FrameMetrics& metrics = aTarget.Metrics();
+  if (!metrics.IsScrollable()) {
     return false;
   }
-  const FrameMetrics& metrics = aTarget.Metrics();
   if (metrics.GetScrollId() != aScrollbar->GetScrollbarTargetContainerId()) {
     return false;
   }
   return !metrics.IsScrollInfoLayer();
 }
 
 static void
-ApplyAsyncTransformToScrollbarForContent(Layer* aScrollbar,
+ApplyAsyncTransformToScrollbarForContent(const RefPtr<APZSampler>& aSampler,
+                                         Layer* aScrollbar,
                                          const LayerMetricsWrapper& aContent,
                                          bool aScrollbarIsDescendant)
 {
   AsyncTransformComponentMatrix clipTransform;
 
+  MOZ_ASSERT(aSampler);
   LayerToParentLayerMatrix4x4 transform =
-      AsyncCompositionManager::ComputeTransformForScrollThumb(
+      aSampler->ComputeTransformForScrollThumb(
           aScrollbar->GetLocalTransformTyped(),
-          aContent.GetTransform(),
-          aContent.GetApzc(),
-          aContent.Metrics(),
+          aContent,
           aScrollbar->GetScrollThumbData(),
           aScrollbarIsDescendant,
           &clipTransform);
 
   if (aScrollbarIsDescendant) {
     // We also need to make a corresponding change on the clip rect of all the
     // layers on the ancestor chain from the scrollbar layer up to but not
     // including the layer with the async transform. Otherwise the scrollbar
@@ -1121,178 +1104,16 @@ ApplyAsyncTransformToScrollbarForContent
     for (Layer* ancestor = aScrollbar; ancestor != aContent.GetLayer(); ancestor = ancestor->GetParent()) {
       TransformClipRect(ancestor, clipTransform);
     }
   }
 
   SetShadowTransform(aScrollbar, transform);
 }
 
-/* static */ LayerToParentLayerMatrix4x4
-AsyncCompositionManager::ComputeTransformForScrollThumb(
-    const LayerToParentLayerMatrix4x4& aCurrentTransform,
-    const Matrix4x4& aScrollableContentTransform,
-    AsyncPanZoomController* aApzc,
-    const FrameMetrics& aMetrics,
-    const ScrollThumbData& aThumbData,
-    bool aScrollbarIsDescendant,
-    AsyncTransformComponentMatrix* aOutClipTransform)
-{
-  // We only apply the transform if the scroll-target layer has non-container
-  // children (i.e. when it has some possibly-visible content). This is to
-  // avoid moving scroll-bars in the situation that only a scroll information
-  // layer has been built for a scroll frame, as this would result in a
-  // disparity between scrollbars and visible content.
-  if (aMetrics.IsScrollInfoLayer()) {
-    return LayerToParentLayerMatrix4x4{};
-  }
-
-  MOZ_RELEASE_ASSERT(aApzc);
-
-  AsyncTransformComponentMatrix asyncTransform =
-    aApzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);
-
-  // |asyncTransform| represents the amount by which we have scrolled and
-  // zoomed since the last paint. Because the scrollbar was sized and positioned based
-  // on the painted content, we need to adjust it based on asyncTransform so that
-  // it reflects what the user is actually seeing now.
-  AsyncTransformComponentMatrix scrollbarTransform;
-  if (*aThumbData.mDirection == ScrollDirection::eVertical) {
-    const ParentLayerCoord asyncScrollY = asyncTransform._42;
-    const float asyncZoomY = asyncTransform._22;
-
-    // The scroll thumb needs to be scaled in the direction of scrolling by the
-    // inverse of the async zoom. This is because zooming in decreases the
-    // fraction of the whole srollable rect that is in view.
-    const float yScale = 1.f / asyncZoomY;
-
-    // Note: |metrics.GetZoom()| doesn't yet include the async zoom.
-    const CSSToParentLayerScale effectiveZoom(aMetrics.GetZoom().yScale * asyncZoomY);
-
-    // Here we convert the scrollbar thumb ratio into a true unitless ratio by
-    // dividing out the conversion factor from the scrollframe's parent's space
-    // to the scrollframe's space.
-    const float ratio = aThumbData.mThumbRatio /
-        (aMetrics.GetPresShellResolution() * asyncZoomY);
-    // The scroll thumb needs to be translated in opposite direction of the
-    // async scroll. This is because scrolling down, which translates the layer
-    // content up, should result in moving the scroll thumb down.
-    ParentLayerCoord yTranslation = -asyncScrollY * ratio;
-
-    // The scroll thumb additionally needs to be translated to compensate for
-    // the scale applied above. The origin with respect to which the scale is
-    // applied is the origin of the entire scrollbar, rather than the origin of
-    // the scroll thumb (meaning, for a vertical scrollbar it's at the top of
-    // the composition bounds). This means that empty space above the thumb
-    // is scaled too, effectively translating the thumb. We undo that
-    // translation here.
-    // (One can think of the adjustment being done to the translation here as
-    // a change of basis. We have a method to help with that,
-    // Matrix4x4::ChangeBasis(), but it wouldn't necessarily make the code
-    // cleaner in this case).
-    const CSSCoord thumbOrigin = (aMetrics.GetScrollOffset().y * ratio);
-    const CSSCoord thumbOriginScaled = thumbOrigin * yScale;
-    const CSSCoord thumbOriginDelta = thumbOriginScaled - thumbOrigin;
-    const ParentLayerCoord thumbOriginDeltaPL = thumbOriginDelta * effectiveZoom;
-    yTranslation -= thumbOriginDeltaPL;
-
-    if (aMetrics.IsRootContent()) {
-      // Scrollbar for the root are painted at the same resolution as the
-      // content. Since the coordinate space we apply this transform in includes
-      // the resolution, we need to adjust for it as well here. Note that in
-      // another metrics.IsRootContent() hunk below we apply a
-      // resolution-cancelling transform which ensures the scroll thumb isn't
-      // actually rendered at a larger scale.
-      yTranslation *= aMetrics.GetPresShellResolution();
-    }
-
-    scrollbarTransform.PostScale(1.f, yScale, 1.f);
-    scrollbarTransform.PostTranslate(0, yTranslation, 0);
-  }
-  if (*aThumbData.mDirection == ScrollDirection::eHorizontal) {
-    // See detailed comments under the VERTICAL case.
-
-    const ParentLayerCoord asyncScrollX = asyncTransform._41;
-    const float asyncZoomX = asyncTransform._11;
-
-    const float xScale = 1.f / asyncZoomX;
-
-    const CSSToParentLayerScale effectiveZoom(aMetrics.GetZoom().xScale * asyncZoomX);
-
-    const float ratio = aThumbData.mThumbRatio /
-        (aMetrics.GetPresShellResolution() * asyncZoomX);
-    ParentLayerCoord xTranslation = -asyncScrollX * ratio;
-
-    const CSSCoord thumbOrigin = (aMetrics.GetScrollOffset().x * ratio);
-    const CSSCoord thumbOriginScaled = thumbOrigin * xScale;
-    const CSSCoord thumbOriginDelta = thumbOriginScaled - thumbOrigin;
-    const ParentLayerCoord thumbOriginDeltaPL = thumbOriginDelta * effectiveZoom;
-    xTranslation -= thumbOriginDeltaPL;
-
-    if (aMetrics.IsRootContent()) {
-      xTranslation *= aMetrics.GetPresShellResolution();
-    }
-
-    scrollbarTransform.PostScale(xScale, 1.f, 1.f);
-    scrollbarTransform.PostTranslate(xTranslation, 0, 0);
-  }
-
-  LayerToParentLayerMatrix4x4 transform =
-      aCurrentTransform * scrollbarTransform;
-
-  AsyncTransformComponentMatrix compensation;
-  // If the scrollbar layer is for the root then the content's resolution
-  // applies to the scrollbar as well. Since we don't actually want the scroll
-  // thumb's size to vary with the zoom (other than its length reflecting the
-  // fraction of the scrollable length that's in view, which is taken care of
-  // above), we apply a transform to cancel out this resolution.
-  if (aMetrics.IsRootContent()) {
-    compensation =
-        AsyncTransformComponentMatrix::Scaling(
-            aMetrics.GetPresShellResolution(),
-            aMetrics.GetPresShellResolution(),
-            1.0f).Inverse();
-  }
-  // If the scrollbar layer is a child of the content it is a scrollbar for,
-  // then we need to adjust for any async transform (including an overscroll
-  // transform) on the content. This needs to be cancelled out because layout
-  // positions and sizes the scrollbar on the assumption that there is no async
-  // transform, and without this adjustment the scrollbar will end up in the
-  // wrong place.
-  //
-  // Note that since the async transform is applied on top of the content's
-  // regular transform, we need to make sure to unapply the async transform in
-  // the same coordinate space. This requires applying the content transform
-  // and then unapplying it after unapplying the async transform.
-  if (aScrollbarIsDescendant) {
-    AsyncTransformComponentMatrix overscroll =
-        aApzc->GetOverscrollTransform(AsyncPanZoomController::eForCompositing);
-    Matrix4x4 asyncUntransform = (asyncTransform * overscroll).Inverse().ToUnknownMatrix();
-    Matrix4x4 contentTransform = aScrollableContentTransform;
-    Matrix4x4 contentUntransform = contentTransform.Inverse();
-
-    AsyncTransformComponentMatrix asyncCompensation =
-        ViewAs<AsyncTransformComponentMatrix>(
-            contentTransform
-          * asyncUntransform
-          * contentUntransform);
-
-    compensation = compensation * asyncCompensation;
-
-    // Pass the async compensation out to the caller so that it can use it
-    // to transform clip transforms as needed.
-    if (aOutClipTransform) {
-      *aOutClipTransform = asyncCompensation;
-    }
-  }
-  transform = transform * compensation;
-
-  return transform;
-}
-
 static LayerMetricsWrapper
 FindScrolledLayerForScrollbar(Layer* aScrollbar, bool* aOutIsAncestor)
 {
   // First check if the scrolled layer is an ancestor of the scrollbar layer.
   LayerMetricsWrapper root(aScrollbar->Manager()->GetRoot());
   LayerMetricsWrapper prevAncestor(aScrollbar);
   LayerMetricsWrapper scrolledLayer;
 
@@ -1339,17 +1160,18 @@ AsyncCompositionManager::ApplyAsyncTrans
   // That is the content that this scrollbar is for. We pick up the transient
   // async transform from that layer and use it to update the scrollbar position.
   // Note that it is possible that the content layer is no longer there; in
   // this case we don't need to do anything because there can't be an async
   // transform on the content.
   bool isAncestor = false;
   const LayerMetricsWrapper& scrollTarget = FindScrolledLayerForScrollbar(aLayer, &isAncestor);
   if (scrollTarget) {
-    ApplyAsyncTransformToScrollbarForContent(aLayer, scrollTarget, isAncestor);
+    ApplyAsyncTransformToScrollbarForContent(mCompositorBridge->GetAPZSampler(),
+        aLayer, scrollTarget, isAncestor);
   }
 }
 
 void
 AsyncCompositionManager::GetFrameUniformity(FrameUniformityData* aOutData)
 {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   mLayerTransformRecorder.EndTest(aOutData);
@@ -1434,17 +1256,20 @@ AsyncCompositionManager::TransformShadow
 #if defined(MOZ_WIDGET_ANDROID)
       MOZ_ASSERT(foundRoot);
       if (foundRoot && mFixedLayerMargins != ScreenMargin()) {
         MoveScrollbarForLayerMargin(root, mRootScrollableId, mFixedLayerMargins);
       }
 #endif
     }
 
-    bool apzAnimating = SampleAPZAnimations(LayerMetricsWrapper(root), nextFrame);
+    bool apzAnimating = false;
+    if (RefPtr<APZSampler> apz = mCompositorBridge->GetAPZSampler()) {
+      apzAnimating = apz->SampleAnimations(LayerMetricsWrapper(root), nextFrame);
+    }
     mAnimationMetricsTracker.UpdateApzAnimationInProgress(apzAnimating, aVsyncRate);
     wantNextFrame |= apzAnimating;
   }
 
   HostLayer* rootComposite = root->AsHostLayer();
 
   gfx::Matrix4x4 trans = rootComposite->GetShadowBaseTransform();
   trans *= gfx::Matrix4x4::From2D(mWorldTransform);
--- a/gfx/layers/composite/AsyncCompositionManager.h
+++ b/gfx/layers/composite/AsyncCompositionManager.h
@@ -19,17 +19,16 @@
 #include "mozilla/layers/FrameUniformityData.h" // For FrameUniformityData
 #include "mozilla/layers/LayersMessages.h"  // for TargetConfig
 #include "mozilla/RefPtr.h"                   // for nsRefPtr
 #include "nsISupportsImpl.h"            // for LayerManager::AddRef, etc
 
 namespace mozilla {
 namespace layers {
 
-class AsyncPanZoomController;
 class Layer;
 class LayerManagerComposite;
 class AutoResolveRefLayers;
 class CompositorBridgeParent;
 
 // Represents async transforms consisting of a scale and a translation.
 struct AsyncTransform {
   explicit AsyncTransform(LayerToParentLayerScale aScale = LayerToParentLayerScale(),
@@ -128,47 +127,16 @@ public:
 
     Maybe<ParentLayerIntRect> Intersect() const {
       return IntersectMaybeRects(mFixedClip, mScrolledClip);
     }
   };
 
   typedef std::map<Layer*, ClipParts> ClipPartsCache;
 
-  /**
-   * Compute the updated shadow transform for a scroll thumb layer that
-   * reflects async scrolling of the associated scroll frame.
-   *
-   * @param aCurrentTransform The current shadow transform on the scroll thumb
-   *    layer, as returned by Layer::GetLocalTransform() or similar.
-   * @param aScrollableContentTransform The current content transform on the
-   *    scrollable content, as returned by Layer::GetTransform().
-   * @param aApzc The APZC that scrolls the scroll frame.
-   * @param aMetrics The metrics associated with the scroll frame, reflecting
-   *    the last paint of the associated content. Note: this metrics should
-   *    NOT reflect async scrolling, i.e. they should be the layer tree's
-   *    copy of the metrics, or APZC's last-content-paint metrics.
-   * @param aThumbData The scroll thumb data for the the scroll thumb layer.
-   * @param aScrollbarIsDescendant True iff. the scroll thumb layer is a
-   *    descendant of the layer bearing the scroll frame's metrics.
-   * @param aOutClipTransform If not null, and |aScrollbarIsDescendant| is true,
-   *    this will be populated with a transform that should be applied to the
-   *    clip rects of all layers between the scroll thumb layer and the ancestor
-   *    layer for the scrollable content.
-   * @return The new shadow transform for the scroll thumb layer, including
-   *    any pre- or post-scales.
-   */
-  static LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
-      const LayerToParentLayerMatrix4x4& aCurrentTransform,
-      const gfx::Matrix4x4& aScrollableContentTransform,
-      AsyncPanZoomController* aApzc,
-      const FrameMetrics& aMetrics,
-      const ScrollThumbData& aThumbData,
-      bool aScrollbarIsDescendant,
-      AsyncTransformComponentMatrix* aOutClipTransform);
 private:
   // Return true if an AsyncPanZoomController content transform was
   // applied for |aLayer|. |*aOutFoundRoot| is set to true on Android only, if
   // one of the metrics on one of the layers was determined to be the "root"
   // and its state was synced to the Java front-end. |aOutFoundRoot| must be
   // non-null.
   bool ApplyAsyncContentTransformToTree(Layer* aLayer,
                                         bool* aOutFoundRoot);
@@ -251,17 +219,17 @@ private:
   bool mReadyForCompose;
 
   gfx::Matrix mWorldTransform;
   LayerTransformRecorder mLayerTransformRecorder;
 
   TimeStamp mPreviousFrameTimeStamp;
   AnimationMetricsTracker mAnimationMetricsTracker;
 
-  CompositorBridgeParent* mCompositorBridge;
+  MOZ_NON_OWNING_REF CompositorBridgeParent* mCompositorBridge;
 
 #ifdef MOZ_WIDGET_ANDROID
 public:
   void SetFixedLayerMargins(ScreenIntCoord aTop, ScreenIntCoord aBottom);
 private:
   // The following two fields are only needed on Fennec with C++ APZ, because
   // then we need to reposition the gecko scrollbar to deal with the
   // dynamic toolbar shifting content around.
--- a/gfx/layers/composite/ContainerLayerComposite.cpp
+++ b/gfx/layers/composite/ContainerLayerComposite.cpp
@@ -1,29 +1,29 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "ContainerLayerComposite.h"
 #include <algorithm>                    // for min
-#include "apz/src/AsyncPanZoomController.h"  // for AsyncPanZoomController
 #include "FrameMetrics.h"               // for FrameMetrics
 #include "Units.h"                      // for LayerRect, LayerPixel, etc
 #include "CompositableHost.h"           // for CompositableHost
 #include "gfxEnv.h"                     // for gfxEnv
 #include "gfxPrefs.h"                   // for gfxPrefs
 #include "mozilla/Assertions.h"         // for MOZ_ASSERT, etc
 #include "mozilla/RefPtr.h"             // for RefPtr
 #include "mozilla/UniquePtr.h"          // for UniquePtr
 #include "mozilla/gfx/BaseRect.h"       // for BaseRect
 #include "mozilla/gfx/Matrix.h"         // for Matrix4x4
 #include "mozilla/gfx/Point.h"          // for Point, IntPoint
 #include "mozilla/gfx/Rect.h"           // for IntRect, Rect
+#include "mozilla/layers/APZSampler.h"  // for APZSampler
 #include "mozilla/layers/Compositor.h"  // for Compositor, etc
 #include "mozilla/layers/CompositorTypes.h"  // for DiagnosticFlags::CONTAINER
 #include "mozilla/layers/Effects.h"     // for Effect, EffectChain, etc
 #include "mozilla/layers/TextureHost.h"  // for CompositingRenderTarget
 #include "mozilla/layers/AsyncCompositionManager.h" // for ViewTransform
 #include "mozilla/layers/LayerMetricsWrapper.h" // for LayerMetricsWrapper
 #include "mozilla/mozalloc.h"           // for operator delete, etc
 #include "mozilla/RefPtr.h"                   // for nsRefPtr
@@ -281,45 +281,48 @@ ContainerPrepare(ContainerT* aContainer,
       aContainer->mLastIntermediateSurface = nullptr;
     }
   } else {
     aContainer->mLastIntermediateSurface = nullptr;
   }
 }
 
 template<class ContainerT> void
-RenderMinimap(ContainerT* aContainer, LayerManagerComposite* aManager,
-                   const RenderTargetIntRect& aClipRect, Layer* aLayer)
+RenderMinimap(ContainerT* aContainer,
+              const RefPtr<APZSampler>& aSampler,
+              LayerManagerComposite* aManager,
+              const RenderTargetIntRect& aClipRect, Layer* aLayer)
 {
   Compositor* compositor = aManager->GetCompositor();
+  MOZ_ASSERT(aSampler);
 
   if (aLayer->GetScrollMetadataCount() < 1) {
     return;
   }
 
-  AsyncPanZoomController* controller = aLayer->GetAsyncPanZoomController(0);
-  if (!controller) {
+  LayerMetricsWrapper wrapper(aLayer, 0);
+  const FrameMetrics& fm = wrapper.Metrics();
+  if (!fm.IsScrollable()) {
     return;
   }
 
-  ParentLayerPoint scrollOffset = controller->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing);
+  ParentLayerPoint scrollOffset = aSampler->GetCurrentAsyncScrollOffset(wrapper);
 
   // Options
   const int verticalPadding = 10;
   const int horizontalPadding = 5;
   gfx::Color backgroundColor(0.3f, 0.3f, 0.3f, 0.3f);
   gfx::Color tileActiveColor(1, 1, 1, 0.4f);
   gfx::Color tileBorderColor(0, 0, 0, 0.1f);
   gfx::Color pageBorderColor(0, 0, 0);
   gfx::Color criticalDisplayPortColor(1.f, 1.f, 0);
   gfx::Color displayPortColor(0, 1.f, 0);
   gfx::Color viewPortColor(0, 0, 1.f, 0.3f);
 
   // Rects
-  const FrameMetrics& fm = aLayer->GetFrameMetrics(0);
   ParentLayerRect compositionBounds = fm.GetCompositionBounds();
   LayerRect scrollRect = fm.GetScrollableRect() * fm.LayersPixelsPerCSSPixel();
   LayerRect viewRect = ParentLayerRect(scrollOffset, compositionBounds.Size()) / LayerToParentLayerScale(1);
   LayerRect dp = (fm.GetDisplayPort() + fm.GetScrollOffset()) * fm.LayersPixelsPerCSSPixel();
   Maybe<LayerRect> cdp;
   if (!fm.GetCriticalDisplayPort().IsEmpty()) {
     cdp = Some((fm.GetCriticalDisplayPort() + fm.GetScrollOffset()) * fm.LayersPixelsPerCSSPixel());
   }
@@ -377,16 +380,21 @@ RenderMinimap(ContainerT* aContainer, La
 
 template<class ContainerT> void
 RenderLayers(ContainerT* aContainer, LayerManagerComposite* aManager,
              const RenderTargetIntRect& aClipRect,
              const Maybe<gfx::Polygon>& aGeometry)
 {
   Compositor* compositor = aManager->GetCompositor();
 
+  RefPtr<APZSampler> sampler;
+  if (CompositorBridgeParent* cbp = compositor->GetCompositorBridgeParent()) {
+    sampler = cbp->GetAPZSampler();
+  }
+
   for (size_t i = 0u; i < aContainer->mPrepared->mLayers.Length(); i++) {
     PreparedLayer& preparedData = aContainer->mPrepared->mLayers[i];
 
     const gfx::IntRect clipRect = preparedData.mClipRect.ToUnknownRect();
     LayerComposite* layerToRender = static_cast<LayerComposite*>(preparedData.mLayer->ImplData());
     const Maybe<gfx::Polygon>& childGeometry = preparedData.mGeometry;
 
     Layer* layer = layerToRender->GetLayer();
@@ -445,35 +453,36 @@ RenderLayers(ContainerT* aContainer, Lay
     // Draw a border around scrollable layers.
     // A layer can be scrolled by multiple scroll frames. Draw a border
     // for each.
     // Within the list of scroll frames for a layer, the layer border for a
     // scroll frame lower down is affected by the async transforms on scroll
     // frames higher up, so loop from the top down, and accumulate an async
     // transform as we go along.
     Matrix4x4 asyncTransform;
-    for (uint32_t i = layer->GetScrollMetadataCount(); i > 0; --i) {
-      if (layer->GetFrameMetrics(i - 1).IsScrollable()) {
-        // Since the composition bounds are in the parent layer's coordinates,
-        // use the parent's effective transform rather than the layer's own.
-        ParentLayerRect compositionBounds = layer->GetFrameMetrics(i - 1).GetCompositionBounds();
-        aManager->GetCompositor()->DrawDiagnostics(DiagnosticFlags::CONTAINER,
-                                                   compositionBounds.ToUnknownRect(),
-                                                   aClipRect.ToUnknownRect(),
-                                                   asyncTransform * aContainer->GetEffectiveTransform());
-        if (AsyncPanZoomController* apzc = layer->GetAsyncPanZoomController(i - 1)) {
+    if (sampler) {
+      for (uint32_t i = layer->GetScrollMetadataCount(); i > 0; --i) {
+        LayerMetricsWrapper wrapper(layer, i - 1);
+        if (wrapper.Metrics().IsScrollable()) {
+          // Since the composition bounds are in the parent layer's coordinates,
+          // use the parent's effective transform rather than the layer's own.
+          ParentLayerRect compositionBounds = wrapper.Metrics().GetCompositionBounds();
+          aManager->GetCompositor()->DrawDiagnostics(DiagnosticFlags::CONTAINER,
+                                                     compositionBounds.ToUnknownRect(),
+                                                     aClipRect.ToUnknownRect(),
+                                                     asyncTransform * aContainer->GetEffectiveTransform());
           asyncTransform =
-              apzc->GetCurrentAsyncTransformWithOverscroll(AsyncPanZoomController::eForCompositing).ToUnknownMatrix()
-            * asyncTransform;
+              sampler->GetCurrentAsyncTransformWithOverscroll(wrapper).ToUnknownMatrix()
+              * asyncTransform;
         }
       }
-    }
 
-    if (gfxPrefs::APZMinimap()) {
-      RenderMinimap(aContainer, aManager, aClipRect, layer);
+      if (gfxPrefs::APZMinimap()) {
+        RenderMinimap(aContainer, sampler, aManager, aClipRect, layer);
+      }
     }
 
     // invariant: our GL context should be current here, I don't think we can
     // assert it though
   }
 }
 
 template<class ContainerT> RefPtr<CompositingRenderTarget>
@@ -600,26 +609,24 @@ ContainerRender(ContainerT* aContainer,
                  aGeometry);
   }
 
   // If it is a scrollable container layer with no child layers, and one of the APZCs
   // attached to it has a nonempty async transform, then that transform is not applied
   // to any visible content. Display a warning box (conditioned on the FPS display being
   // enabled).
   if (gfxPrefs::LayersDrawFPS() && aContainer->IsScrollableWithoutContent()) {
+    RefPtr<APZSampler> sampler = aManager->GetCompositor()->GetCompositorBridgeParent()->GetAPZSampler();
     // Since aContainer doesn't have any children we can just iterate from the top metrics
     // on it down to the bottom using GetFirstChild and not worry about walking onto another
     // underlying layer.
     for (LayerMetricsWrapper i(aContainer); i; i = i.GetFirstChild()) {
-      if (AsyncPanZoomController* apzc = i.GetApzc()) {
-        if (!apzc->GetAsyncTransformAppliedToContent()
-            && !AsyncTransformComponentMatrix(apzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForHitTesting)).IsIdentity()) {
-          aManager->UnusedApzTransformWarning();
-          break;
-        }
+      if (sampler->HasUnusedAsyncTransform(i)) {
+        aManager->UnusedApzTransformWarning();
+        break;
       }
     }
   }
 }
 
 ContainerLayerComposite::ContainerLayerComposite(LayerManagerComposite *aManager)
   : ContainerLayer(aManager, nullptr)
   , LayerComposite(aManager)
--- a/gfx/layers/composite/ContainerLayerComposite.h
+++ b/gfx/layers/composite/ContainerLayerComposite.h
@@ -11,16 +11,17 @@
 #include "mozilla/Attributes.h"         // for override
 #include "mozilla/UniquePtr.h"          // for UniquePtr
 #include "mozilla/layers/LayerManagerComposite.h"
 #include "mozilla/gfx/Rect.h"
 
 namespace mozilla {
 namespace layers {
 
+class APZSampler;
 class CompositableHost;
 class CompositingRenderTarget;
 struct PreparedData;
 
 class ContainerLayerComposite : public ContainerLayer,
                                 public LayerComposite
 {
   template<class ContainerT>
@@ -49,17 +50,19 @@ class ContainerLayerComposite : public C
                                              const RenderTargetIntRect& aClipRect);
   template<class ContainerT>
   friend RefPtr<CompositingRenderTarget>
   CreateOrRecycleTarget(ContainerT* aContainer,
                         LayerManagerComposite* aManager,
                         const RenderTargetIntRect& aClipRect);
 
   template<class ContainerT>
-  void RenderMinimap(ContainerT* aContainer, LayerManagerComposite* aManager,
+  void RenderMinimap(ContainerT* aContainer,
+                     const RefPtr<APZSampler>& aSampler,
+                     LayerManagerComposite* aManager,
                      const RenderTargetIntRect& aClipRect, Layer* aLayer);
 public:
   explicit ContainerLayerComposite(LayerManagerComposite *aManager);
 
 protected:
   ~ContainerLayerComposite();
 
 public:
--- a/taskcluster/scripts/misc/build-sccache.sh
+++ b/taskcluster/scripts/misc/build-sccache.sh
@@ -1,13 +1,13 @@
 #!/bin/bash
 set -x -e -v
 
-# 0.2.2 + a few fixes
-SCCACHE_REVISION=8871ae7bd8d7f844228fbcfecb6f471b22a01e1d
+# 0.2.6
+SCCACHE_REVISION=dfdfce28e0cf6c16eb821e8aa3c3f63f78b25497
 
 # This script is for building sccache
 
 case "$(uname -s)" in
 Linux)
     WORKSPACE=$HOME/workspace
     UPLOAD_DIR=$HOME/artifacts
     COMPRESS_EXT=xz
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
@@ -17,19 +17,25 @@ from marionette_harness import (
     skip,
     skip_if_mobile,
     WindowManagerMixin,
 )
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
+BLACK_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' # noqa
+RED_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=' # noqa
+
 def inline(doc):
     return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
 
+def inline_image(data):
+    return 'data:image/png;base64,%s' % data
+
 
 class BaseNavigationTestCase(WindowManagerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(BaseNavigationTestCase, self).setUp()
 
         file_path = os.path.join(here, 'data', 'test.html').replace("\\", "/")
 
@@ -482,16 +488,19 @@ class TestBackForwardNavigation(BaseNavi
             {"url": self.marionette.absolute_url("white.png")},
         ]
         self.run_bfcache_test(test_pages)
 
     def test_image_to_image(self):
         test_pages = [
             {"url": self.marionette.absolute_url("black.png")},
             {"url": self.marionette.absolute_url("white.png")},
+            {"url": inline_image(RED_PIXEL)},
+            {"url": inline_image(BLACK_PIXEL)},
+            {"url": self.marionette.absolute_url("black.png")},
         ]
         self.run_bfcache_test(test_pages)
 
     @run_if_e10s("Requires e10s mode enabled")
     def test_remoteness_change(self):
         test_pages = [
             {"url": "about:robots", "is_remote": False},
             {"url": self.test_page_remote, "is_remote": True},
--- a/testing/mozharness/mozharness/base/script.py
+++ b/testing/mozharness/mozharness/base/script.py
@@ -145,16 +145,33 @@ class ScriptMixin(PlatformMixin):
     Attributes:
         env (dict): a mapping object representing the string environment.
         script_obj (ScriptMixin): reference to a ScriptMixin instance.
     """
 
     env = None
     script_obj = None
 
+    def query_filesize(self, file_path):
+        self.info("Determining filesize for %s" % file_path)
+        length = os.path.getsize(file_path)
+        self.info(" %s" % str(length))
+        return length
+
+    # TODO this should be parallelized with the to-be-written BaseHelper!
+    def query_sha512sum(self, file_path):
+        self.info("Determining sha512sum for %s" % file_path)
+        m = hashlib.sha512()
+        contents = self.read_from_file(file_path, verbose=False,
+                                       open_mode='rb')
+        m.update(contents)
+        sha512 = m.hexdigest()
+        self.info(" %s" % sha512)
+        return sha512
+
     def platform_name(self):
         """ Return the platform name on which the script is running on.
         Returns:
             None: for failure to determine the platform.
             str: The name of the platform (e.g. linux64)
         """
         return platform_name()
 
--- a/testing/mozharness/mozharness/mozilla/building/buildbase.py
+++ b/testing/mozharness/mozharness/mozilla/building/buildbase.py
@@ -36,17 +36,16 @@ from mozharness.mozilla.buildbot import 
     TBPL_FAILURE,
     TBPL_RETRY,
     TBPL_WARNING,
     TBPL_SUCCESS,
     TBPL_WORST_LEVEL_TUPLE,
 )
 from mozharness.mozilla.purge import PurgeMixin
 from mozharness.mozilla.secrets import SecretsMixin
-from mozharness.mozilla.signing import SigningMixin
 from mozharness.mozilla.testing.errors import TinderBoxPrintRe
 from mozharness.mozilla.testing.unittest import tbox_print_summary
 from mozharness.mozilla.updates.balrog import BalrogMixin
 from mozharness.base.python import (
     PerfherderResourceOptionsMixin,
     VirtualenvMixin,
 )
 
@@ -664,17 +663,17 @@ def generate_build_ID():
     return time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))
 
 
 def generate_build_UID():
     return uuid.uuid4().hex
 
 
 class BuildScript(BuildbotMixin, PurgeMixin, BalrogMixin,
-                  SigningMixin, VirtualenvMixin, MercurialScript,
+                  VirtualenvMixin, MercurialScript,
                   SecretsMixin, PerfherderResourceOptionsMixin):
     def __init__(self, **kwargs):
         # objdir is referenced in _query_abs_dirs() so let's make sure we
         # have that attribute before calling BaseScript.__init__
         self.objdir = None
         super(BuildScript, self).__init__(**kwargs)
         # epoch is only here to represent the start of the buildbot build
         # that this mozharn script came from. until I can grab bbot's
@@ -922,42 +921,24 @@ or run without that action (ie: --no-{ac
             if c.get('update_channel'):
                 env["MOZ_UPDATE_CHANNEL"] = c['update_channel']
             else:  # let's just give the generic channel based on branch
                 env["MOZ_UPDATE_CHANNEL"] = "nightly-%s" % (self.branch,)
 
         if self.config.get('pgo_build') or self._compile_against_pgo():
             env['MOZ_PGO'] = '1'
 
-        if c.get('enable_signing'):
-            if os.environ.get('MOZ_SIGNING_SERVERS'):
-                moz_sign_cmd = subprocess.list2cmdline(
-                    self.query_moz_sign_cmd(formats=None)
-                )
-                # windows fix. This is passed to mach build env and we call that
-                # with python, not with bash so we need to fix the slashes here
-                env['MOZ_SIGN_CMD'] = moz_sign_cmd.replace('\\', '\\\\\\\\')
-            else:
-                self.warning("signing disabled because MOZ_SIGNING_SERVERS is not set")
-        elif 'MOZ_SIGN_CMD' in env:
-            # Ensure that signing is truly disabled
-            # MOZ_SIGN_CMD may be defined by default in buildbot (see MozillaBuildFactory)
-            self.warning("Clearing MOZ_SIGN_CMD because we don't have config['enable_signing']")
-            del env['MOZ_SIGN_CMD']
-
         # to activate the right behaviour in mozonfigs while we transition
         if c.get('enable_release_promotion'):
             env['ENABLE_RELEASE_PROMOTION'] = "1"
             update_channel = c.get('update_channel', self.branch)
             self.info("Release promotion update channel: %s"
                       % (update_channel,))
             env["MOZ_UPDATE_CHANNEL"] = update_channel
 
-        # we can't make env an attribute of self because env can change on
-        # every call for reasons like MOZ_SIGN_CMD
         return env
 
     def query_mach_build_env(self, multiLocale=None):
         c = self.config
         if multiLocale is None and self.query_is_nightly():
             multiLocale = c.get('multi_locale', False)
         mach_env = {}
         if c.get('upload_env'):
deleted file mode 100755
--- a/testing/mozharness/mozharness/mozilla/signing.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python
-# ***** BEGIN LICENSE BLOCK *****
-# 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/.
-# ***** END LICENSE BLOCK *****
-"""Mozilla-specific signing methods.
-"""
-
-import os
-import re
-import json
-import sys
-
-from mozharness.base.errors import BaseErrorList
-from mozharness.base.log import ERROR, FATAL
-from mozharness.base.signing import AndroidSigningMixin, BaseSigningMixin
-
-AndroidSignatureVerificationErrorList = BaseErrorList + [{
-    "regex": re.compile(r'''^Invalid$'''),
-    "level": FATAL,
-    "explanation": "Signature is invalid!"
-}, {
-    "substr": "filename not matched",
-    "level": ERROR,
-}, {
-    "substr": "ERROR: Could not unzip",
-    "level": ERROR,
-}, {
-    "regex": re.compile(r'''Are you sure this is a (nightly|release) package'''),
-    "level": FATAL,
-    "explanation": "Not signed!"
-}]
-
-
-# SigningMixin {{{1
-
-class SigningMixin(BaseSigningMixin):
-    """Generic signing helper methods."""
-    def query_moz_sign_cmd(self, formats=['gpg']):
-        if 'MOZ_SIGNING_SERVERS' not in os.environ:
-            self.fatal("MOZ_SIGNING_SERVERS not in env; no MOZ_SIGN_CMD for you!")
-        dirs = self.query_abs_dirs()
-        signing_dir = os.path.join(dirs['abs_work_dir'], 'tools', 'release', 'signing')
-        cache_dir = os.path.join(dirs['abs_work_dir'], 'signing_cache')
-        token = os.path.join(dirs['base_work_dir'], 'token')
-        nonce = os.path.join(dirs['base_work_dir'], 'nonce')
-        host_cert = os.path.join(signing_dir, 'host.cert')
-        python = sys.executable
-        # A mock environment is a special case, the system python isn't
-        # available there
-        if 'mock_target' in self.config:
-            python = 'python2.7'
-        cmd = [
-            python,
-            os.path.join(signing_dir, 'signtool.py'),
-            '--cachedir', cache_dir,
-            '-t', token,
-            '-n', nonce,
-            '-c', host_cert,
-        ]
-        if formats:
-            for f in formats:
-                cmd += ['-f', f]
-        for h in os.environ['MOZ_SIGNING_SERVERS'].split(","):
-            cmd += ['-H', h]
-        return cmd
-
-    def generate_signing_manifest(self, files):
-        """Generate signing manifest for signingworkers
-
-        Every entry in the manifest requires a dictionary of
-        "file_to_sign" (basename) and "hash" (SHA512) of every file to be
-        signed. Signing format is defined in the signing task.
-        """
-        manifest_content = [
-            {
-                "file_to_sign": os.path.basename(f),
-                "hash": self.query_sha512sum(f)
-            }
-            for f in files
-        ]
-        return json.dumps(manifest_content)
-
-
-# MobileSigningMixin {{{1
-class MobileSigningMixin(AndroidSigningMixin, SigningMixin):
-    def verify_android_signature(self, apk, script=None, key_alias="nightly",
-                                 tools_dir="tools/", env=None):
-        """Runs mjessome's android signature verification script.
-        This currently doesn't check to see if the apk exists; you may want
-        to do that before calling the method.
-        """
-        c = self.config
-        dirs = self.query_abs_dirs()
-        if script is None:
-            script = c.get('signature_verification_script')
-        if env is None:
-            env = self.query_env()
-        return self.run_command(
-            [script, "--tools-dir=%s" % tools_dir, "--%s" % key_alias,
-             "--apk=%s" % apk],
-            cwd=dirs['abs_work_dir'],
-            env=env,
-            error_list=AndroidSignatureVerificationErrorList
-        )
--- a/testing/mozharness/scripts/desktop_l10n.py
+++ b/testing/mozharness/scripts/desktop_l10n.py
@@ -8,17 +8,16 @@
 
 This script manages Desktop repacks for nightly builds.
 """
 import os
 import glob
 import re
 import sys
 import shlex
-import subprocess
 
 # load modules from parent dir
 sys.path.insert(1, os.path.dirname(sys.path[0]))
 
 from mozharness.base.errors import MakefileErrorList
 from mozharness.base.script import BaseScript
 from mozharness.base.transfer import TransferMixin
 from mozharness.base.vcs.vcsbase import VCSMixin
@@ -26,17 +25,16 @@ from mozharness.mozilla.buildbot import 
 from mozharness.mozilla.purge import PurgeMixin
 from mozharness.mozilla.building.buildbase import (
     MakeUploadOutputParser,
     get_mozconfig_path,
 )
 from mozharness.mozilla.l10n.locales import LocalesMixin
 from mozharness.mozilla.mar import MarMixin
 from mozharness.mozilla.release import ReleaseMixin
-from mozharness.mozilla.signing import SigningMixin
 from mozharness.mozilla.updates.balrog import BalrogMixin
 from mozharness.base.python import VirtualenvMixin
 
 try:
     import simplejson as json
     assert json
 except ImportError:
     import json
@@ -63,17 +61,17 @@ runtime_config_tokens = ('buildid', 'ver
                          'abs_objdir', 'revision',
                          'to_buildid', 'en_us_binary_url',
                          'en_us_installer_binary_url', 'mar_tools_url',
                          'who')
 
 
 # DesktopSingleLocale {{{1
 class DesktopSingleLocale(LocalesMixin, ReleaseMixin, BuildbotMixin,
-                          VCSMixin, SigningMixin, PurgeMixin, BaseScript,
+                          VCSMixin, PurgeMixin, BaseScript,
                           BalrogMixin, MarMixin, VirtualenvMixin, TransferMixin):
     """Manages desktop repacks"""
     config_options = [[
         ['--balrog-config', ],
         {"action": "extend",
          "dest": "config_files",
          "type": "string",
          "help": "Specify the balrog configuration file"}
@@ -368,21 +366,16 @@ class DesktopSingleLocale(LocalesMixin, 
         bootstrap_env = self.query_env(partial_env=config.get("bootstrap_env"),
                                        replace_dict=replace_dict)
         # Override en_us_installer_binary_url if passed as a buildbot property
         if self.buildbot_config["properties"].get("en_us_installer_binary_url"):
             self.info("Overriding en_us_binary_url with %s" %
                       self.buildbot_config["properties"]["en_us_installer_binary_url"])
             bootstrap_env['EN_US_INSTALLER_BINARY_URL'] = str(
                 self.buildbot_config["properties"]["en_us_installer_binary_url"])
-        if 'MOZ_SIGNING_SERVERS' in os.environ:
-            sign_cmd = self.query_moz_sign_cmd(formats=None)
-            sign_cmd = subprocess.list2cmdline(sign_cmd)
-            # windows fix
-            bootstrap_env['MOZ_SIGN_CMD'] = sign_cmd.replace('\\', '\\\\\\\\')
         for binary in self._mar_binaries():
             # "mar -> MAR" and 'mar.exe -> MAR' (windows)
             name = binary.replace('.exe', '')
             name = name.upper()
             binary_path = os.path.join(self._mar_tool_dir(), binary)
             # windows fix...
             if binary.endswith('.exe'):
                 binary_path = binary_path.replace('\\', '\\\\\\\\')
@@ -406,21 +399,16 @@ class DesktopSingleLocale(LocalesMixin, 
             for extra in config['upload_env_extra']:
                 upload_env[extra] = config['upload_env_extra'][extra]
 
         self.upload_env = upload_env
         return self.upload_env
 
     def query_l10n_env(self):
         l10n_env = self._query_upload_env().copy()
-        # both upload_env and bootstrap_env define MOZ_SIGN_CMD
-        # the one from upload_env is taken from os.environ, the one from
-        # bootstrap_env is set with query_moz_sign_cmd()
-        # we need to use the value provided my query_moz_sign_cmd or make upload
-        # will fail (signtool.py path is wrong)
         l10n_env.update(self.query_bootstrap_env())
         return l10n_env
 
     def _query_make_ident_output(self):
         """Get |make ident| output from the objdir.
         Only valid after setup is run.
        """
         if self.make_ident_output:
--- a/testing/mozharness/scripts/mobile_l10n.py
+++ b/testing/mozharness/scripts/mobile_l10n.py
@@ -8,17 +8,16 @@
 
 This currently supports nightly and release single locale repacks for
 Android.  This also creates nightly updates.
 """
 
 import glob
 import os
 import re
-import subprocess
 import sys
 
 try:
     import simplejson as json
     assert json
 except ImportError:
     import json
 
@@ -26,30 +25,29 @@ except ImportError:
 sys.path.insert(1, os.path.dirname(sys.path[0]))
 
 from mozharness.base.errors import MakefileErrorList
 from mozharness.base.log import OutputParser
 from mozharness.base.transfer import TransferMixin
 from mozharness.mozilla.buildbot import BuildbotMixin
 from mozharness.mozilla.purge import PurgeMixin
 from mozharness.mozilla.release import ReleaseMixin
-from mozharness.mozilla.signing import MobileSigningMixin
 from mozharness.mozilla.tooltool import TooltoolMixin
 from mozharness.base.vcs.vcsbase import MercurialScript
 from mozharness.mozilla.l10n.locales import LocalesMixin
 from mozharness.mozilla.mock import MockMixin
 from mozharness.mozilla.secrets import SecretsMixin
 from mozharness.mozilla.updates.balrog import BalrogMixin
 from mozharness.base.python import VirtualenvMixin
 
 
 # MobileSingleLocale {{{1
 class MobileSingleLocale(MockMixin, LocalesMixin, ReleaseMixin,
-                         MobileSigningMixin, TransferMixin, TooltoolMixin,
-                         BuildbotMixin, PurgeMixin, MercurialScript, BalrogMixin,
+                         TransferMixin, TooltoolMixin, BuildbotMixin,
+                         PurgeMixin, MercurialScript, BalrogMixin,
                          VirtualenvMixin, SecretsMixin):
     config_options = [[
         ['--locale', ],
         {"action": "extend",
          "dest": "locales",
          "type": "string",
          "help": "Specify the locale(s) to sign and update"
          }
@@ -180,19 +178,16 @@ class MobileSingleLocale(MockMixin, Loca
                 'version': rc['version'],
                 'buildnum': rc['buildnum']
             }
         repack_env = self.query_env(partial_env=c.get("repack_env"),
                                     replace_dict=replace_dict)
         if c.get('base_en_us_binary_url') and c.get('release_config_file'):
             rc = self.query_release_config()
             repack_env['EN_US_BINARY_URL'] = c['base_en_us_binary_url'] % replace_dict
-        if 'MOZ_SIGNING_SERVERS' in os.environ:
-            repack_env['MOZ_SIGN_CMD'] = \
-                subprocess.list2cmdline(self.query_moz_sign_cmd(formats=['jar']))
 
         if self.query_is_nightly() or self.query_is_nightly_promotion():
             if self.query_is_nightly():
                 # Nightly promotion needs to set update_channel but not do all
                 # the 'IS_NIGHTLY' automation parts, like uploading symbols
                 # (for now).
                 repack_env["IS_NIGHTLY"] = "yes"
             # In branch_specifics.py we might set update_channel explicitly.
@@ -222,18 +217,16 @@ class MobileSingleLocale(MockMixin, Loca
         # builds go to 'mozilla-central-l10n', while android builds add part of
         # the platform name as well, like 'mozilla-central-android-api-16-l10n'.
         # So we override the branch with something that contains the platform
         # name.
         replace_dict['branch'] = c['upload_branch']
 
         upload_env = self.query_env(partial_env=c.get("upload_env"),
                                     replace_dict=replace_dict)
-        if 'MOZ_SIGNING_SERVERS' in os.environ:
-            upload_env['MOZ_SIGN_CMD'] = subprocess.list2cmdline(self.query_moz_sign_cmd())
         if self.query_is_release_or_beta():
             upload_env['MOZ_PKG_VERSION'] = '%(version)s' % replace_dict
         self.upload_env = upload_env
         return self.upload_env
 
     def _query_make_ident_output(self):
         """Get |make ident| output from the objdir.
         Only valid after setup is run.
old mode 100644
new mode 100755
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -139,24 +139,26 @@ nsRFPService::IsTimerPrecisionReductionE
  * The below is a simple time-based Least Recently Used cache used to store the
  * result of a cryptographic hash function. It has LRU_CACHE_SIZE slots, and will
  * be used from multiple threads. It is thread-safe.
  */
 #define LRU_CACHE_SIZE         (45)
 #define HASH_DIGEST_SIZE_BITS  (256)
 #define HASH_DIGEST_SIZE_BYTES (HASH_DIGEST_SIZE_BITS / 8)
 
-class LRUCache
+class LRUCache final
 {
 public:
   LRUCache()
     : mLock("mozilla.resistFingerprinting.LRUCache") {
     this->cache.SetLength(LRU_CACHE_SIZE);
   }
 
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(LRUCache)
+
   nsCString Get(long long aKey) {
     for (auto & cacheEntry : this->cache) {
       // Read optimistically befor locking
       if (cacheEntry.key == aKey) {
         MutexAutoLock lock(mLock);
 
         // Double check after we have a lock
         if (MOZ_UNLIKELY(cacheEntry.key != aKey)) {
@@ -197,16 +199,18 @@ public:
     lowestKey->key = aKey;
     lowestKey->data = aValue;
     lowestKey->accessTime = PR_Now();
     MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose, ("LRU Cache STORE with %lli", aKey));
   }
 
 
 private:
+  ~LRUCache() = default;
+
   struct CacheEntry {
     Atomic<long long, Relaxed> key;
     PRTime accessTime = 0;
     nsCString data;
 
     CacheEntry() {
       this->key = 0xFFFFFFFFFFFFFFFF;
       this->accessTime = 0;
@@ -219,17 +223,17 @@ private:
     }
   };
 
   AutoTArray<CacheEntry, LRU_CACHE_SIZE> cache;
   mozilla::Mutex mLock;
 };
 
 // We make a single LRUCache
-static StaticAutoPtr<LRUCache> sCache;
+static StaticRefPtr<LRUCache> sCache;
 
 /**
  * The purpose of this function is to deterministicly generate a random midpoint
  * between a lower clamped value and an upper clamped value. Assuming a clamping
  * resolution of 100, here is an example:
  *
  * |---------------------------------------|--------------------------|
  * lower clamped value (e.g. 300)          |           upper clamped value (400)
@@ -288,26 +292,28 @@ nsRFPService::RandomMidpoint(long long a
                              long long* aMidpointOut,
                              uint8_t * aSecretSeed /* = nullptr */)
 {
   nsresult rv;
   const int kSeedSize = 16;
   const int kClampTimesPerDigest = HASH_DIGEST_SIZE_BITS / 32;
   static uint8_t * sSecretMidpointSeed = nullptr;
 
-  if(MOZ_UNLIKELY(!sCache)) {
-    StaticMutexAutoLock lock(sLock);
-    if(MOZ_LIKELY(!sCache)) {
-      sCache = new LRUCache();
-      ClearOnShutdown(&sCache);
-    }
+  if(MOZ_UNLIKELY(!aMidpointOut)) {
+    return NS_ERROR_INVALID_ARG;
   }
 
-  if(MOZ_UNLIKELY(!aMidpointOut)) {
-    return NS_ERROR_INVALID_ARG;
+  RefPtr<LRUCache> cache;
+  {
+    StaticMutexAutoLock lock(sLock);
+    cache = sCache;
+  }
+
+  if(!cache) {
+    return NS_ERROR_FAILURE;
   }
 
   /*
    * Below, we will call a cryptographic hash function. That's expensive. We look for ways to
    * make it more efficient.
    *
    * We only need as much output from the hash function as the maximum resolution we will
    * ever support, because we will reduce the output modulo that value. The maximum resolution
@@ -323,17 +329,17 @@ nsRFPService::RandomMidpoint(long long a
    * kClampTimesPerDigest (just like we reduced the real time value to aClampedTime!)
    *
    * Then we hash _that_ value (assuming it's not in the cache) and index into the digest result
    * the appropriate bit offset.
    */
   long long reducedResolution = aResolutionUSec * kClampTimesPerDigest;
   long long extraClampedTime = (aClampedTimeUSec / reducedResolution) * reducedResolution;
 
-  nsCString hashResult = sCache->Get(extraClampedTime);
+  nsCString hashResult = cache->Get(extraClampedTime);
 
   if(hashResult.Length() != HASH_DIGEST_SIZE_BYTES) { // Cache Miss =(
     // If someone has pased in the testing-only parameter, replace our seed with it
     if (aSecretSeed != nullptr) {
       StaticMutexAutoLock lock(sLock);
       if (sSecretMidpointSeed) {
         delete[] sSecretMidpointSeed;
       }
@@ -390,17 +396,17 @@ nsRFPService::RandomMidpoint(long long a
      rv = hasher->Update((const uint8_t *)&extraClampedTime, sizeof(extraClampedTime));
      NS_ENSURE_SUCCESS(rv, rv);
 
      nsAutoCStringN<HASH_DIGEST_SIZE_BYTES> derivedSecret;
      rv = hasher->Finish(false, derivedSecret);
      NS_ENSURE_SUCCESS(rv, rv);
 
      // Finally, store it in the cache
-     sCache->Store(extraClampedTime, derivedSecret);
+     cache->Store(extraClampedTime, derivedSecret);
      hashResult = derivedSecret;
   }
 
   // Offset the appropriate index into the hash output, and then turn it into a random midpoint
   // between 0 and aResolutionUSec. Sometimes out input time is negative, we ride the negative
   // out to the end until we start doing pointer math. (We also triple check we're in bounds.)
   int byteOffset = abs(((aClampedTimeUSec - extraClampedTime) / aResolutionUSec) * 4);
   if (MOZ_UNLIKELY(byteOffset > (HASH_DIGEST_SIZE_BYTES - 4))) {
@@ -690,16 +696,22 @@ nsRFPService::Init()
   const char* tzValue = PR_GetEnv("TZ");
   if (tzValue) {
     mInitialTZValue = nsCString(tzValue);
   }
 
   // Call Update here to cache the values of the prefs and set the timezone.
   UpdateRFPPref();
 
+  // Create the LRU Cache when we initialize, to avoid accidently trying to
+  // create it (and call ClearOnShutdown) on a non-main-thread
+  if(!sCache) {
+    sCache = new LRUCache();
+  }
+
   return rv;
 }
 
 // This function updates only timing-related fingerprinting items
 void
 nsRFPService::UpdateTimers() {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -759,16 +771,21 @@ nsRFPService::UpdateRFPPref()
 
 void
 nsRFPService::StartShutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
 
+  StaticMutexAutoLock lock(sLock);
+  {
+    sCache = nullptr;
+  }
+
   if (obs) {
     obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
 
     nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
 
     if (prefs) {
       prefs->RemoveObserver(RESIST_FINGERPRINTING_PREF, this);
       prefs->RemoveObserver(RFP_TIMER_PREF, this);
--- a/toolkit/mozapps/extensions/internal/PluginProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
@@ -372,17 +372,17 @@ PluginWrapper.prototype = {
       AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]);
     }
 
     return val;
   },
 
   get blocklistState() {
     let { tags: [tag] } = pluginFor(this);
-    return Services.blocklist.getPluginBlocklistState(tag);
+    return tag.blocklistState;
   },
 
   get blocklistURL() {
     let { tags: [tag] } = pluginFor(this);
     return Services.blocklist.getPluginBlocklistURL(tag);
   },
 
   get size() {
--- a/toolkit/mozapps/extensions/nsBlocklistService.js
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -225,17 +225,16 @@ function Blocklist() {
   gLoggingEnabled = Services.prefs.getBoolPref(PREF_EM_LOGGING_ENABLED, false);
   gBlocklistEnabled = Services.prefs.getBoolPref(PREF_BLOCKLIST_ENABLED, true);
   gBlocklistLevel = Math.min(Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
                              MAX_BLOCK_LEVEL);
   Services.prefs.addObserver("extensions.blocklist.", this);
   Services.prefs.addObserver(PREF_EM_LOGGING_ENABLED, this);
   this.wrappedJSObject = this;
   // requests from child processes come in here, see receiveMessage.
-  Services.ppmm.addMessageListener("Blocklist:getPluginBlocklistState", this);
   Services.ppmm.addMessageListener("Blocklist:content-blocklist-updated", this);
 }
 
 Blocklist.prototype = {
   /**
    * Extension ID -> array of Version Ranges
    * Each value in the version range array is a JS Object that has the
    * following properties:
@@ -251,17 +250,16 @@ Blocklist.prototype = {
    *                                 (default = *)
    */
   _addonEntries: null,
   _gfxEntries: null,
   _pluginEntries: null,
 
   shutdown() {
     Services.obs.removeObserver(this, "xpcom-shutdown");
-    Services.ppmm.removeMessageListener("Blocklist:getPluginBlocklistState", this);
     Services.ppmm.removeMessageListener("Blocklist:content-blocklist-updated", this);
     Services.prefs.removeObserver("extensions.blocklist.", this);
     Services.prefs.removeObserver(PREF_EM_LOGGING_ENABLED, this);
   },
 
   observe(aSubject, aTopic, aData) {
     switch (aTopic) {
     case "xpcom-shutdown":
@@ -289,20 +287,16 @@ Blocklist.prototype = {
       this._preloadBlocklist();
       break;
     }
   },
 
   // Message manager message handlers
   receiveMessage(aMsg) {
     switch (aMsg.name) {
-      case "Blocklist:getPluginBlocklistState":
-        return this.getPluginBlocklistState(aMsg.data.addonData,
-                                            aMsg.data.appVersion,
-                                            aMsg.data.toolkitVersion);
       case "Blocklist:content-blocklist-updated":
         Services.obs.notifyObservers(null, "content-blocklist-updated");
         break;
       default:
         throw new Error("Unknown blocklist message received from content: " + aMsg.name);
     }
     return undefined;
   },
@@ -623,19 +617,16 @@ Blocklist.prototype = {
     }
 
     // Save current blocklist timestamp to pref.
     const lastModified = request.getResponseHeader("Last-Modified") || "";
     Services.prefs.setCharPref(PREF_BLOCKLIST_LAST_MODIFIED, lastModified);
 
     var oldAddonEntries = this._addonEntries;
     var oldPluginEntries = this._pluginEntries;
-    this._addonEntries = [];
-    this._gfxEntries = [];
-    this._pluginEntries = [];
 
     this._loadBlocklistFromXML(responseXML);
     // We don't inform the users when the graphics blocklist changed at runtime.
     // However addons and plugins blocking status is refreshed.
     this._blocklistUpdated(oldAddonEntries, oldPluginEntries);
 
     try {
       let path = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
@@ -670,23 +661,16 @@ Blocklist.prototype = {
    * Finds the newest blocklist file from the application and the profile and
    * load it or does nothing if neither exist.
    */
   _loadBlocklist() {
     this._addonEntries = [];
     this._gfxEntries = [];
     this._pluginEntries = [];
 
-    if (this._isBlocklistPreloaded()) {
-      Services.telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false);
-      this._loadBlocklistFromString(this._preloadedBlocklistContent);
-      delete this._preloadedBlocklistContent;
-      return;
-    }
-
     Services.telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(true);
 
     var profFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
     try {
       this._loadBlocklistFromFile(profFile);
     } catch (ex) {
       LOG("Blocklist::_loadBlocklist: couldn't load file from profile, trying app dir");
       try {
@@ -801,26 +785,21 @@ Blocklist.prototype = {
     if (text)
       this._loadBlocklistFromString(text);
   },
 
   get isLoaded() {
     return this._addonEntries != null && this._gfxEntries != null && this._pluginEntries != null;
   },
 
-  _isBlocklistPreloaded() {
-    return this._preloadedBlocklistContent != null;
-  },
-
   /* Used for testing */
   _clear() {
     this._addonEntries = null;
     this._gfxEntries = null;
     this._pluginEntries = null;
-    this._preloadedBlocklistContent = null;
   },
 
   async _preloadBlocklist() {
     let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
     try {
       await this._preloadBlocklistFile(profPath);
       return;
     } catch (e) {
@@ -846,20 +825,25 @@ Blocklist.prototype = {
 
     if (!gBlocklistEnabled) {
       LOG("Blocklist::_preloadBlocklistFile: blocklist is disabled");
       return;
     }
 
     let text = await OS.File.read(path, { encoding: "utf-8" });
 
-    if (!this._addonEntries) {
-      // Store the content only if a sync load has not been performed in the meantime.
-      this._preloadedBlocklistContent = text;
-    }
+    await new Promise(resolve => {
+      Services.tm.idleDispatchToMainThread(() => {
+        if (!this.isLoaded) {
+          Services.telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false);
+          this._loadBlocklistFromString(text);
+        }
+        resolve();
+      });
+    });
   },
 
   _loadBlocklistFromString(text) {
     try {
       var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
                    createInstance(Ci.nsIDOMParser);
       var doc = parser.parseFromString(text, "text/xml");
       if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
@@ -871,16 +855,19 @@ Blocklist.prototype = {
     } catch (e) {
       LOG("Blocklist::_loadBlocklistFromString: Error constructing blocklist " + e);
       return;
     }
     this._loadBlocklistFromXML(doc);
   },
 
   _loadBlocklistFromXML(doc) {
+    this._addonEntries = [];
+    this._gfxEntries = [];
+    this._pluginEntries = [];
     try {
       var childNodes = doc.documentElement.childNodes;
       for (let element of childNodes) {
         if (!(element instanceof Ci.nsIDOMElement))
           continue;
         switch (element.localName) {
         case "emItems":
           this._addonEntries = this._processItemNodes(element.childNodes, "emItem",
--- a/toolkit/mozapps/extensions/nsBlocklistServiceContent.js
+++ b/toolkit/mozapps/extensions/nsBlocklistServiceContent.js
@@ -84,23 +84,18 @@ Blocklist.prototype = {
     return Ci.nsIBlocklistService.STATE_BLOCKED;
   },
 
   get isLoaded() {
     // Lie until we fix bug 1443870.
     return true;
   },
 
-  // There are a few callers in layout that rely on this.
   getPluginBlocklistState(aPluginTag, aAppVersion, aToolkitVersion) {
-    return Services.cpmm.sendSyncMessage("Blocklist:getPluginBlocklistState", {
-      addonData: this.flattenObject(aPluginTag),
-      appVersion: aAppVersion,
-      toolkitVersion: aToolkitVersion
-    })[0];
+    throw new Error(kMissingAPIMessage);
   },
 
   getAddonBlocklistURL(aAddon, aAppVersion, aToolkitVersion) {
     throw new Error(kMissingAPIMessage);
   },
 
   getPluginBlocklistURL(aPluginTag) {
     throw new Error(kMissingAPIMessage);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js
@@ -2,39 +2,46 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 add_task(async function() {
   let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
                   getService().wrappedJSObject;
   let scope = ChromeUtils.import("resource://gre/modules/osfile.jsm", {});
 
-  // sync -> async
+  // sync -> async. Check that async code doesn't try to read the file
+  // once it's already been read synchronously.
+  let read = scope.OS.File.read;
+  let triedToRead = false;
+  scope.OS.File.read = () => triedToRead = true;
   blocklist._loadBlocklist();
   Assert.ok(blocklist.isLoaded);
   await blocklist._preloadBlocklist();
-  Assert.ok(!blocklist._isBlocklistPreloaded());
+  Assert.ok(!triedToRead);
+  scope.OS.File.read = read;
   blocklist._clear();
 
-  // async -> sync
+  info("sync -> async complete");
+
+  // async first. Check that once we preload the content, that is sufficient.
   await blocklist._preloadBlocklist();
-  Assert.ok(!blocklist.isLoaded);
-  Assert.ok(blocklist._isBlocklistPreloaded());
-  blocklist._loadBlocklist();
   Assert.ok(blocklist.isLoaded);
-  Assert.ok(!blocklist._isBlocklistPreloaded());
+  // Calling _loadBlocklist now would just re-load the list sync.
+
+  info("async test complete");
   blocklist._clear();
 
   // async -> sync -> async
-  let read = scope.OS.File.read;
   scope.OS.File.read = function(...args) {
     return new Promise((resolve, reject) => {
       executeSoon(() => {
         blocklist._loadBlocklist();
+        // Now do the async bit after all:
         resolve(read(...args));
       });
     });
   };
 
   await blocklist._preloadBlocklist();
+  // We're mostly just checking this doesn't error out.
   Assert.ok(blocklist.isLoaded);
-  Assert.ok(!blocklist._isBlocklistPreloaded());
+  info("mixed async/sync test complete");
 });
--- a/tools/lint/codespell.yml
+++ b/tools/lint/codespell.yml
@@ -1,14 +1,23 @@
 ---
 codespell:
     description: Check code for common misspellings
     include:
+        - browser/base/content/docs/
+        - browser/experiments/docs
+        - build/docs
+        - mobile/android/docs
+        - python/mozlint
+        - taskcluster/docs
+        - testing/mozbase/docs
+        - toolkit/components/extensions/docs
+        - toolkit/components/telemetry/docs/
+        - toolkit/crashreporter/docs
         - tools/lint
-        - python/mozlint
     exclude:
         - third_party
     # List of extensions coming from:
     # tools/lint/{flake8,eslint}.yml
     # tools/mach_commands.py (clang-format)
     # + documentation
     # + localization files
     extensions:
--- a/tools/rewriting/ThirdPartyPaths.txt
+++ b/tools/rewriting/ThirdPartyPaths.txt
@@ -1,23 +1,25 @@
 browser/components/translation/cld2/
 browser/extensions/mortar/ppapi/
 db/sqlite3/src/
+devtools/client/sourceeditor/codemirror/
+devtools/client/sourceeditor/tern/
 extensions/spellcheck/hunspell/src/
 gfx/angle/
 gfx/cairo/
 gfx/graphite2/
 gfx/harfbuzz/
 gfx/ots/
 gfx/qcms/
 gfx/sfntly/
 gfx/skia/
 gfx/vr/openvr/
-gfx/webrender
-gfx/webrender_api
+gfx/webrender/
+gfx/webrender_api/
 gfx/wrench/
 gfx/ycbcr/
 intl/hyphenation/hyphen/
 intl/icu/
 ipc/chromium/
 js/src/ctypes/libffi/
 js/src/dtoa.c
 js/src/jit/arm64/vixl/
@@ -51,16 +53,17 @@ mobile/android/geckoview/src/thirdparty/
 mobile/android/thirdparty/
 modules/brotli/
 modules/fdlibm/
 modules/freetype2/
 modules/libbz2/
 modules/libmar/
 modules/pdfium/
 modules/woff2/
+modules/xz-embedded/
 modules/zlib/
 netwerk/sctp/src/
 netwerk/srtp/src/
 nsprpub/
 other-licenses/
 parser/expat/
 security/nss/
 security/sandbox/chromium/
@@ -90,9 +93,9 @@ third_party/python/six/
 third_party/python/which/
 third_party/rust/
 toolkit/components/jsoncpp/
 toolkit/components/lz4/
 toolkit/components/protobuf/
 toolkit/components/url-classifier/chromium/
 toolkit/components/url-classifier/protobuf/
 toolkit/crashreporter/google-breakpad/
-tools/fuzzing/libfuzzer
+tools/fuzzing/libfuzzer/
--- a/widget/gtk/gtk3drawing.cpp
+++ b/widget/gtk/gtk3drawing.cpp
@@ -2280,19 +2280,25 @@ moz_gtk_info_bar_paint(cairo_t *cr, GdkR
 static gint
 moz_gtk_header_bar_paint(WidgetNodeType widgetType,
                          cairo_t *cr, GdkRectangle* rect, GtkWidgetState* state)
 {
     GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
     GtkStyleContext *style = GetStyleContext(widgetType, GTK_TEXT_DIR_LTR,
                                              state_flags);
     InsetByMargin(rect, style);
-    gtk_render_background(style, cr, rect->x, rect->y, rect->width,
-                          rect->height);
-    gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+
+    // Some themes (Adwaita for instance) draws bold dark line at
+    // titlebar bottom. It does not fit well with Firefox tabs so
+    // draw with some extent to make the titlebar bottom part invisible.
+    #define TITLEBAR_EXTENT 4
+    gtk_render_background(style, cr, rect->x, rect->y,
+                          rect->width, rect->height + TITLEBAR_EXTENT);
+    gtk_render_frame(style, cr, rect->x, rect->y,
+                     rect->width, rect->height + TITLEBAR_EXTENT);
 
     return MOZ_GTK_SUCCESS;
 }
 
 
 
 static GtkBorder
 GetMarginBorderPadding(GtkStyleContext* aStyle)