Merge mozilla-central to inbound. a=merge CLOSED TREE
authorBrindusan Cristian <cbrindusan@mozilla.com>
Mon, 18 Feb 2019 23:48:55 +0200
changeset 459826 10bef0ce2b5b
parent 459825 c8a0b11f729c (current diff)
parent 459806 ca3d40c83ae7 (diff)
child 459827 2c2e488c86ce
push id35575
push usercbrindusan@mozilla.com
push dateTue, 19 Feb 2019 04:40:03 +0000
treeherdermozilla-central@ee6e77950205 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone67.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 mozilla-central to inbound. a=merge CLOSED TREE
testing/web-platform/meta/css/css-text/word-break/word-break-break-all-020.html.ini
testing/web-platform/meta/css/css-text/word-break/word-break-keep-all-003.html.ini
--- a/browser/base/content/sanitize.xul
+++ b/browser/base/content/sanitize.xul
@@ -93,21 +93,21 @@
                   preference="privacy.cpd.cache"
                   onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
       </vbox>
     </hbox>
   </groupbox>
   <groupbox>
     <label><html:h2 data-l10n-id="data-section-label"/></label>
     <hbox>
-      <box data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
+      <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
         <checkbox data-l10n-id="item-site-preferences"
                   preference="privacy.cpd.siteSettings"
                   onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
-      </box>
-      <box flex="1">
+      </vbox>
+      <vbox flex="1">
         <checkbox data-l10n-id="item-offline-apps"
                   preference="privacy.cpd.offlineApps"
                   onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
-      </box>
+      </vbox>
     </hbox>
   </groupbox>
 </dialog>
--- a/browser/base/content/test/performance/browser_appmenu.js
+++ b/browser/base/content/test/performance/browser_appmenu.js
@@ -37,17 +37,17 @@ const EXPECTED_APPMENU_OPEN_REFLOWS = [
 
     maxCount: 7, // This number should only ever go down - never up.
   },
 ];
 
 add_task(async function() {
   await ensureNoPreloadedBrowser();
 
-  let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
+  let textBoxRect = document.getAnonymousElementByAttribute(gURLBar.textbox,
     "anonid", "moz-input-box").getBoundingClientRect();
   let menuButtonRect =
     document.getElementById("PanelUI-menu-button").getBoundingClientRect();
   let frameExpectations = {
     filter: rects => rects.filter(r => !(
       // We expect the menu button to get into the active state.
       r.y1 >= menuButtonRect.top && r.y2 <= menuButtonRect.bottom &&
       r.x1 >= menuButtonRect.left && r.x2 <= menuButtonRect.right
--- a/browser/base/content/test/performance/browser_tabopen.js
+++ b/browser/base/content/test/performance/browser_tabopen.js
@@ -29,17 +29,17 @@ add_task(async function() {
   // tab opening operation.
   await ensureFocusedUrlbar();
 
   let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
   let firstTabRect = gBrowser.selectedTab.getBoundingClientRect();
   let firstTabLabelRect =
     document.getAnonymousElementByAttribute(gBrowser.selectedTab, "anonid", "tab-label")
             .getBoundingClientRect();
-  let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
+  let textBoxRect = document.getAnonymousElementByAttribute(gURLBar.textbox,
     "anonid", "moz-input-box").getBoundingClientRect();
   let inRange = (val, min, max) => min <= val && val <= max;
 
   // Add a reflow observer and open a new tab.
   await withPerfObserver(async function() {
     let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
     BrowserOpenTab();
     await BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "TabAnimationEnd");
--- a/browser/base/content/test/performance/browser_tabopen_squeeze.js
+++ b/browser/base/content/test/performance/browser_tabopen_squeeze.js
@@ -29,17 +29,17 @@ add_task(async function() {
   // cause the tab to squeeze to a smaller size rather than overflow.
   const TAB_COUNT_FOR_SQUEEZE = computeMaxTabCount() - 1;
 
   await createTabs(TAB_COUNT_FOR_SQUEEZE);
 
   await ensureFocusedUrlbar();
 
   let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
-  let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
+  let textBoxRect = document.getAnonymousElementByAttribute(gURLBar.textbox,
     "anonid", "moz-input-box").getBoundingClientRect();
 
   await withPerfObserver(async function() {
     let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
     BrowserOpenTab();
     await BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "TabAnimationEnd");
     await switchDone;
   }, {expectedReflows: EXPECTED_REFLOWS,
--- a/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
+++ b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
@@ -33,19 +33,19 @@ add_task(async function() {
 
   const TAB_COUNT_FOR_OVERFLOW = computeMaxTabCount();
 
   await createTabs(TAB_COUNT_FOR_OVERFLOW);
 
   await ensureFocusedUrlbar();
 
   let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
-  let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
+  let textBoxRect = document.getAnonymousElementByAttribute(gURLBar.textbox,
     "anonid", "moz-input-box").getBoundingClientRect();
-  let urlbarDropmarkerRect = document.getAnonymousElementByAttribute(gURLBar,
+  let urlbarDropmarkerRect = document.getAnonymousElementByAttribute(gURLBar.textbox,
     "anonid", "historydropmarker").getBoundingClientRect();
 
   let ignoreTabstripRects = {
     filter: rects => rects.filter(r => !(
       // We expect plenty of changed rects within the tab strip.
       r.y1 >= tabStripRect.top && r.y2 <= tabStripRect.bottom &&
       r.x1 >= tabStripRect.left && r.x2 <= tabStripRect.right
     )),
--- a/browser/base/content/test/performance/browser_windowclose.js
+++ b/browser/base/content/test/performance/browser_windowclose.js
@@ -35,17 +35,17 @@ add_task(async function() {
   });
 
   // At the time of writing, there are no reflows on window closing.
   // Mochitest will fail if we have no assertions, so we add one here
   // to make sure nobody adds any new ones.
   Assert.equal(EXPECTED_REFLOWS.length, 0,
     "We shouldn't have added any new expected reflows for window close.");
 
-  let dropmarkerRect = document.getAnonymousElementByAttribute(gURLBar,
+  let dropmarkerRect = document.getAnonymousElementByAttribute(gURLBar.textbox,
     "anonid", "historydropmarker").getBoundingClientRect();
 
   await withPerfObserver(async function() {
     let promiseOrigBrowserFocused = BrowserTestUtils.waitForCondition(() => {
       return Services.focus.activeWindow == window;
     });
     await BrowserTestUtils.closeWindow(win);
     await promiseOrigBrowserFocused;
--- a/browser/base/content/test/performance/head.js
+++ b/browser/base/content/test/performance/head.js
@@ -258,17 +258,17 @@ async function prepareSettledWindow() {
 // urlbar and changed rects for its dropmarker when opening new tabs.
 async function ensureFocusedUrlbar() {
   // The switchingtabs attribute prevents the historydropmarker opacity
   // transition, so if we expect a transitionend event when this attribute
   // is set, we wait forever. (it's removed off a MozAfterPaint event listener)
   await BrowserTestUtils.waitForCondition(() =>
     !gURLBar.hasAttribute("switchingtabs"));
 
-  let dropmarker = document.getAnonymousElementByAttribute(gURLBar, "anonid",
+  let dropmarker = document.getAnonymousElementByAttribute(gURLBar.textbox, "anonid",
                                                            "historydropmarker");
   let opacityPromise = BrowserTestUtils.waitForEvent(dropmarker, "transitionend",
                                                      false, e => e.propertyName === "opacity");
   gURLBar.focus();
   await opacityPromise;
 }
 
 /**
--- a/browser/base/content/test/popups/browser_popupUI.js
+++ b/browser/base/content/test/popups/browser_popupUI.js
@@ -15,17 +15,17 @@ add_task(async function toolbar_ui_visib
   const doc = win.document;
 
   ok(win.gURLBar, "location bar exists in the popup");
   isnot(win.gURLBar.clientWidth, 0, "location bar is visible in the popup");
   ok(win.gURLBar.readOnly, "location bar is read-only in the popup");
   isnot(doc.getElementById("Browser:OpenLocation").getAttribute("disabled"), "true",
      "'open location' command is not disabled in the popup");
 
-  let historyButton = doc.getAnonymousElementByAttribute(win.gURLBar, "anonid",
+  let historyButton = doc.getAnonymousElementByAttribute(win.gURLBar.textbox, "anonid",
                                                          "historydropmarker");
   is(historyButton.clientWidth, 0, "history dropdown button is hidden in the popup");
 
   EventUtils.synthesizeKey("t", { accelKey: true }, win);
   is(win.gBrowser.browsers.length, 1, "Accel+T doesn't open a new tab in the popup");
   is(gBrowser.browsers.length, 3, "Accel+T opened a new tab in the parent window");
   gBrowser.removeCurrentTab();
 
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -312,16 +312,20 @@ async function toggleBookmarksToolbar(vi
 
   setToolbarVisibility(bookmarksToolbar, visible);
   await transitionPromise;
 }
 
 async function openContextMenuInPopup(extension, selector = "body") {
   let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
   let browser = await awaitExtensionPanel(extension);
+
+  // Ensure that the document layout has been flushed before triggering the mouse event
+  // (See Bug 1519808 for a rationale).
+  await browser.ownerGlobal.promiseDocumentFlushed(() => {});
   let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
   await BrowserTestUtils.synthesizeMouseAtCenter(selector, {type: "mousedown", button: 2}, browser);
   await BrowserTestUtils.synthesizeMouseAtCenter(selector, {type: "contextmenu"}, browser);
   await popupShownPromise;
   return contentAreaContextMenu;
 }
 
 async function openContextMenuInSidebar(selector = "body") {
--- a/browser/components/places/content/bookmarkProperties.js
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -192,19 +192,20 @@ var BookmarkPropertiesPanel = {
           break;
 
         case "folder":
           this._itemType = BOOKMARK_FOLDER;
           if (!this._title) {
             if ("URIList" in dialogInfo) {
               this._title = this._strings.getString("bookmarkAllTabsDefault");
               this._URIs = dialogInfo.URIList;
-            } else
+            } else {
               this._title = this._strings.getString("newFolderDefault");
               this._dummyItem = true;
+            }
           }
           break;
       }
     } else { // edit
       this._node = dialogInfo.node;
       this._title = this._node.title;
       if (PlacesUtils.nodeIsFolder(this._node))
         this._itemType = BOOKMARK_FOLDER;
--- a/browser/components/preferences/sanitize.xul
+++ b/browser/components/preferences/sanitize.xul
@@ -54,19 +54,19 @@
         <checkbox data-l10n-id="item-cache"
                   preference="privacy.clearOnShutdown.cache"/>
       </vbox>
     </hbox>
   </groupbox>
   <groupbox>
     <label><html:h2 data-l10n-id="data-section-label"/></label>
     <hbox>
-      <box data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
+      <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
         <checkbox data-l10n-id="item-site-preferences"
                   preference="privacy.clearOnShutdown.siteSettings"/>
-      </box>
-      <box>
+      </vbox>
+      <vbox>
         <checkbox data-l10n-id="item-offline-apps"
                   preference="privacy.clearOnShutdown.offlineApps"/>
-      </box>
+      </vbox>
     </hbox>
   </groupbox>
 </dialog>
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js
@@ -99,21 +99,23 @@ add_task(async function test_myths_link(
     elementId: "private-browsing-myths",
     expectedUrl: "https://example.com/private-browsing-myths",
   });
 
   await BrowserTestUtils.closeWindow(win);
 });
 
 function urlBarHasHiddenFocus(win) {
-  return win.gURLBar.hasAttribute("focused") && win.gURLBar.classList.contains("hidden-focus");
+  return win.gURLBar.hasAttribute("focused") &&
+    win.gURLBar.textbox.classList.contains("hidden-focus");
 }
 
 function urlBarHasNormalFocus(win) {
-  return win.gURLBar.hasAttribute("focused") && !win.gURLBar.classList.contains("hidden-focus");
+  return win.gURLBar.hasAttribute("focused") &&
+    !win.gURLBar.textbox.classList.contains("hidden-focus");
 }
 
 /**
  * Tests the search hand-off on character keydown in "about:privatebrowsing".
  */
 add_task(async function test_search_handoff_on_keydown() {
   enableSearchUI();
   let { win, tab } = await openAboutPrivateBrowsing();
--- a/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
+++ b/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
@@ -1,21 +1,23 @@
 "use strict";
 
+const {UrlbarTestUtils} = ChromeUtils.import("resource://testing-common/UrlbarTestUtils.jsm");
+
 const TEST_ENGINE_NAME = "Foo";
 const TEST_ENGINE_BASENAME = "testEngine.xml";
 const SEARCHBAR_BASE_ID = "searchbar-engine-one-off-item-";
 const URLBAR_BASE_ID = "urlbar-engine-one-off-item-";
 const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
 
 const urlbar = document.getElementById("urlbar");
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
 const urlbarPopup = document.getElementById("PopupAutoCompleteRichResult");
 const searchOneOff = searchPopup.oneOffButtons;
-const urlBarOneOff = urlbarPopup.oneOffSearchButtons;
+const urlBarOneOff = UrlbarTestUtils.getOneOffSearchButtons(window);
 
 var originalEngine;
 
 async function resetEngine() {
   await Services.search.setDefault(originalEngine);
 }
 
 registerCleanupFunction(resetEngine);
@@ -90,17 +92,20 @@ add_task(async function test_urlBarChang
   // For the urlbar, we should keep the new engine's icon.
   Assert.equal(oneOffButton.id, URLBAR_BASE_ID + defaultEngine.name,
                "Should now have the original engine's id for the button");
   Assert.equal(oneOffButton.getAttribute("tooltiptext"), defaultEngine.name,
                "Should now have the original engine's name for the tooltip");
   Assert.equal(oneOffButton.image, defaultEngine.iconURI.spec,
                "Should now have the original engine's uri for the image");
 
-  await promiseClosePopup(urlbarPopup);
+  await UrlbarTestUtils.promisePopupClose(window);
+
+  // Move the cursor out of the panel area to avoid messing with other tests.
+  await EventUtils.synthesizeNativeMouseMove(urlbarPopup);
 });
 
 /**
  * Promises that an engine change has happened for the current engine, which
  * has resulted in the test engine now being the current engine.
  *
  * @returns {Promise} Resolved once the test engine is set as the current engine.
  */
@@ -127,30 +132,28 @@ function promisedefaultEngineChanged() {
  * @param {object} popup The expected popup.
  * @param {object} oneOffInstance The expected one-off instance for the popup.
  * @param {string} baseId The expected string for the id of the current
  *                        engine button, without the engine name.
  * @returns {object} Returns an object that represents the one off button for the
  *                          test engine.
  */
 async function openPopupAndGetEngineButton(isSearch, popup, oneOffInstance, baseId) {
-  // Open the popup.
-  let promise = promiseEvent(popup, "popupshown");
   info("Opening panel");
 
   // We have to open the popups in differnt ways.
   if (isSearch) {
+    // Open the popup.
+    let promise = promiseEvent(popup, "popupshown");
     // Use the search icon to avoid hitting the network.
     EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+    await promise;
   } else {
-    // There's no history at this stage, so we need to press a key.
-    urlbar.focus();
-    EventUtils.sendString("a");
+    await UrlbarTestUtils.promiseAutocompleteResultPopup(window, "a", waitForFocus);
   }
-  await promise;
 
   const contextMenu = oneOffInstance.contextMenuPopup;
   const oneOffButtons = oneOffInstance.buttons;
 
   // Get the one-off button for the test engine.
   let oneOffButton;
   for (let node of oneOffButtons.children) {
     if (node.engine && node.engine.name == TEST_ENGINE_NAME) {
@@ -161,17 +164,17 @@ async function openPopupAndGetEngineButt
   Assert.notEqual(oneOffButton, undefined,
                   "One-off for test engine should exist");
   Assert.equal(oneOffButton.getAttribute("tooltiptext"), TEST_ENGINE_NAME,
                "One-off should have the tooltip set to the engine name");
   Assert.equal(oneOffButton.id, baseId + TEST_ENGINE_NAME,
                "Should have the correct id");
 
   // Open the context menu on the one-off.
-  promise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+  let promise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
   EventUtils.synthesizeMouseAtCenter(oneOffButton, {
     type: "contextmenu",
     button: 2,
   });
   await promise;
 
   return oneOffButton;
 }
--- a/browser/components/search/test/browser/browser_searchbar_openpopup.js
+++ b/browser/components/search/test/browser/browser_searchbar_openpopup.js
@@ -1,16 +1,11 @@
 // Tests that the suggestion popup appears at the right times in response to
 // focus and user events (mouse, keyboard, drop).
 
-// Instead of loading EventUtils.js into the test scope in browser-test.js for all tests,
-// we only need EventUtils.js for a few files which is why we are using loadSubScript.
-var EventUtils = {};
-Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
-
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
 const kValues = ["long text", "long text 2", "long text 3"];
 
 const isWindows = Services.appinfo.OS == "WINNT";
 const mouseDown = isWindows ? 2 : 1;
 const mouseUp = isWindows ? 4 : 2;
 const utils = window.windowUtils;
 const scale = utils.screenPixelsPerCSSPixel;
@@ -441,17 +436,17 @@ add_task(async function dont_consume_cli
   await promise;
   isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
 
   is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
-  await synthesizeNativeMouseClick(gURLBar);
+  await synthesizeNativeMouseClick(gURLBar.inputField);
   await promise;
 
   is(Services.focus.focusedElement, gURLBar.inputField, "Should have focused the URL bar");
 
   textbox.value = "";
 });
 
 // Dropping text to the searchbar should open the popup
--- a/browser/components/shell/content/setDesktopBackground.js
+++ b/browser/components/shell/content/setDesktopBackground.js
@@ -1,14 +1,13 @@
 /* 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/. */
 
-const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+/* import-globals-from ../../../base/content/utilityOverlay.js */
 
 var gSetBackground = {
   _position: AppConstants.platform == "macosx" ? "STRETCH" : "",
   _backgroundColor: AppConstants.platform != "macosx" ? 0 : undefined,
   _screenWidth: 0,
   _screenHeight: 0,
   _image: null,
   _canvas: null,
--- a/browser/modules/BrowserWindowTracker.jsm
+++ b/browser/modules/BrowserWindowTracker.jsm
@@ -77,18 +77,25 @@ function _handleMessage(message) {
   let browser = message.target;
   if (message.name === "Browser:Init" &&
       browser === browser.ownerGlobal.gBrowser.selectedBrowser) {
     _updateCurrentContentOuterWindowID(browser);
   }
 }
 
 function _trackWindowOrder(window) {
-  _trackedWindows.splice(window.windowState == window.STATE_MINIMIZED ?
-    _trackedWindows.length - 1 : 0, 0, window);
+  if (window.windowState == window.STATE_MINIMIZED) {
+    let firstMinimizedWindow = _trackedWindows.findIndex(w => w.windowState == w.STATE_MINIMIZED);
+    if (firstMinimizedWindow == -1) {
+      firstMinimizedWindow = _trackedWindows.length;
+    }
+    _trackedWindows.splice(firstMinimizedWindow, 0, window);
+  } else {
+    _trackedWindows.unshift(window);
+  }
 }
 
 function _untrackWindowOrder(window) {
   let idx = _trackedWindows.indexOf(window);
   if (idx >= 0)
     _trackedWindows.splice(idx, 1);
 }
 
@@ -135,19 +142,19 @@ var WindowHelper = {
     _untrackWindowOrder(window);
     _trackWindowOrder(window);
 
     _updateCurrentContentOuterWindowID(window.gBrowser.selectedBrowser);
   },
 
   onSizemodeChange(window) {
     if (window.windowState == window.STATE_MINIMIZED) {
-      // Make sure to have the minimized window at the end of the list.
+      // Make sure to have the minimized window behind unminimized windows.
       _untrackWindowOrder(window);
-      _trackedWindows.push(window);
+      _trackWindowOrder(window);
     }
   },
 };
 
 this.BrowserWindowTracker = {
   /**
    * Get the most recent browser window.
    *
--- a/devtools/client/aboutdebugging-new/src/actions/runtimes.js
+++ b/devtools/client/aboutdebugging-new/src/actions/runtimes.js
@@ -39,33 +39,20 @@ const {
   UPDATE_RUNTIME_MULTIE10S_FAILURE,
   UPDATE_RUNTIME_MULTIE10S_START,
   UPDATE_RUNTIME_MULTIE10S_SUCCESS,
   WATCH_RUNTIME_FAILURE,
   WATCH_RUNTIME_START,
   WATCH_RUNTIME_SUCCESS,
 } = require("../constants");
 
-async function getRuntimeInfo(runtime, clientWrapper) {
-  const { type } = runtime;
-  const { name, channel, deviceName, isMultiE10s, version } =
-    await clientWrapper.getDeviceDescription();
-  const icon =
-    (channel === "release" || channel === "beta" || channel === "aurora")
-      ? `chrome://devtools/skin/images/aboutdebugging-firefox-${ channel }.svg`
-      : "chrome://devtools/skin/images/aboutdebugging-firefox-nightly.svg";
-
-  return {
-    deviceName,
-    icon,
-    isMultiE10s,
-    name,
-    type,
-    version,
-  };
+async function getRuntimeIcon(channel) {
+  return (channel === "release" || channel === "beta" || channel === "aurora")
+    ? `chrome://devtools/skin/images/aboutdebugging-firefox-${ channel }.svg`
+    : "chrome://devtools/skin/images/aboutdebugging-firefox-nightly.svg";
 }
 
 function onRemoteDebuggerClientClosed() {
   window.AboutDebugging.onNetworkLocationsUpdated();
   window.AboutDebugging.onUSBRuntimesUpdated();
 }
 
 function onMultiE10sUpdated() {
@@ -73,40 +60,46 @@ function onMultiE10sUpdated() {
 }
 
 function connectRuntime(id) {
   return async (dispatch, getState) => {
     dispatch({ type: CONNECT_RUNTIME_START });
     try {
       const runtime = findRuntimeById(id, getState().runtimes);
       const clientWrapper = await createClientForRuntime(runtime);
-      const info = await getRuntimeInfo(runtime, clientWrapper);
-      const { isMultiE10s } = info;
-      delete info.isMultiE10s;
+
+      const deviceDescription = await clientWrapper.getDeviceDescription();
+      const compatibilityReport = await clientWrapper.checkVersionCompatibility();
+      const icon = await getRuntimeIcon(deviceDescription.channel);
 
       const {
         CONNECTION_PROMPT,
         PERMANENT_PRIVATE_BROWSING,
         SERVICE_WORKERS_ENABLED,
       } = RUNTIME_PREFERENCE;
-
       const connectionPromptEnabled =
         await clientWrapper.getPreference(CONNECTION_PROMPT, false);
-
       const privateBrowsing =
         await clientWrapper.getPreference(PERMANENT_PRIVATE_BROWSING, false);
       const serviceWorkersEnabled =
         await clientWrapper.getPreference(SERVICE_WORKERS_ENABLED, true);
       const serviceWorkersAvailable = serviceWorkersEnabled && !privateBrowsing;
 
       const runtimeDetails = {
         clientWrapper,
+        compatibilityReport,
         connectionPromptEnabled,
-        info,
-        isMultiE10s,
+        info: {
+          deviceName: deviceDescription.deviceName,
+          icon,
+          name: deviceDescription.name,
+          type: runtime.type,
+          version: deviceDescription.version,
+        },
+        isMultiE10s: deviceDescription.isMultiE10s,
         serviceWorkersAvailable,
       };
 
       const deviceFront = await clientWrapper.getFront("device");
       if (deviceFront) {
         deviceFront.on("multi-e10s-updated", onMultiE10sUpdated);
       }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/CompatibilityWarning.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+"use strict";
+
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const FluentReact = require("devtools/client/shared/vendor/fluent-react");
+const Localized = createFactory(FluentReact.Localized);
+
+const Message = createFactory(require("./shared/Message"));
+
+const { MESSAGE_LEVEL } = require("../constants");
+const { COMPATIBILITY_STATUS } = require("devtools/client/shared/remote-debugging/version-checker");
+
+const TROUBLESHOOTING_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
+
+const Types = require("../types/index");
+
+class CompatibilityWarning extends PureComponent {
+  static get propTypes() {
+    return {
+      compatibilityReport: Types.compatibilityReport.isRequired,
+    };
+  }
+
+  render() {
+    const { localID, localVersion, minVersion, runtimeID, runtimeVersion, status } =
+      this.props.compatibilityReport;
+
+    if (status === COMPATIBILITY_STATUS.COMPATIBLE) {
+      return null;
+    }
+
+    const isTooOld = status === COMPATIBILITY_STATUS.TOO_OLD;
+    const localizationId = isTooOld ?
+      "about-debugging-runtime-version-too-old" :
+      "about-debugging-runtime-version-too-recent";
+
+    const statusClassName = isTooOld ?
+      "js-compatibility-warning-too-old" :
+      "js-compatibility-warning-too-recent";
+
+    return Message(
+      {
+        level: MESSAGE_LEVEL.WARNING,
+      },
+      Localized(
+        {
+          id: localizationId,
+          a: dom.a({
+            href: TROUBLESHOOTING_URL,
+            target: "_blank",
+          }),
+          $localID: localID,
+          $localVersion: localVersion,
+          $minVersion: minVersion,
+          $runtimeID: runtimeID,
+          $runtimeVersion: runtimeVersion,
+        },
+        dom.p(
+          {
+            className: `js-compatibility-warning ${statusClassName}`,
+          },
+          localizationId,
+        ),
+      )
+    );
+  }
+}
+
+module.exports = CompatibilityWarning;
--- a/devtools/client/aboutdebugging-new/src/components/RuntimePage.js
+++ b/devtools/client/aboutdebugging-new/src/components/RuntimePage.js
@@ -7,16 +7,17 @@
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
 
+const CompatibilityWarning = createFactory(require("./CompatibilityWarning"));
 const ConnectionPromptSetting = createFactory(require("./ConnectionPromptSetting"));
 const DebugTargetPane = createFactory(require("./debugtarget/DebugTargetPane"));
 const ExtensionDetail = createFactory(require("./debugtarget/ExtensionDetail"));
 const InspectAction = createFactory(require("./debugtarget/InspectAction"));
 const ProfilerDialog = createFactory(require("./ProfilerDialog"));
 const RuntimeInfo = createFactory(require("./RuntimeInfo"));
 const ServiceWorkerAction = createFactory(require("./debugtarget/ServiceWorkerAction"));
 const ServiceWorkersWarning = createFactory(require("./ServiceWorkersWarning"));
@@ -140,23 +141,25 @@ class RuntimePage extends PureComponent 
 
     if (!runtimeDetails) {
       // runtimeInfo can be null when the selectPage action navigates from a runtime A
       // to a runtime B (between unwatchRuntime and watchRuntime).
       return null;
     }
 
     const { type } = runtimeDetails.info;
+    const { compatibilityReport } = runtimeDetails;
     return dom.article(
       {
         className: "page js-runtime-page",
       },
       RuntimeInfo(runtimeDetails.info),
       this.renderRemoteRuntimeActions(),
       runtimeDetails.serviceWorkersAvailable ? null : ServiceWorkersWarning(),
+      CompatibilityWarning({ compatibilityReport }),
       isSupportedDebugTargetPane(type, DEBUG_TARGET_PANE.TEMPORARY_EXTENSION)
         ? TemporaryExtensionInstaller({
             dispatch,
             temporaryInstallError,
         }) : null,
       this.renderDebugTargetPane("Temporary Extensions",
                                  temporaryExtensions,
                                  TemporaryExtensionAction,
--- a/devtools/client/aboutdebugging-new/src/components/moz.build
+++ b/devtools/client/aboutdebugging-new/src/components/moz.build
@@ -7,16 +7,17 @@ DIRS += [
     'debugtarget',
     'shared',
     'sidebar',
 ]
 
 DevToolsModules(
     'App.css',
     'App.js',
+    'CompatibilityWarning.js',
     'ConnectionPromptSetting.js',
     'ProfilerDialog.css',
     'ProfilerDialog.js',
     'RuntimeInfo.js',
     'RuntimePage.css',
     'RuntimePage.js',
     'ServiceWorkersWarning.js',
 )
--- a/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
+++ b/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
@@ -1,14 +1,17 @@
 /* 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/. */
 
 "use strict";
 
+const { checkVersionCompatibility } =
+  require("devtools/client/shared/remote-debugging/version-checker");
+
 const { RUNTIME_PREFERENCE } = require("../constants");
 const { WorkersListener } = require("./workers-listener");
 
 const PREF_TYPES = {
   BOOL: "BOOL",
 };
 
 // Map of preference to preference type.
@@ -67,28 +70,31 @@ class ClientWrapper {
   }
 
   onFront(typeName, listener) {
     this.client.mainRoot.onFront(typeName, listener);
   }
 
   async getDeviceDescription() {
     const deviceFront = await this.getFront("device");
-    const { brandName, channel, deviceName, isMultiE10s, version } =
-      await deviceFront.getDescription();
+    const description = await deviceFront.getDescription();
     // Only expose a specific set of properties.
     return {
-      channel,
-      deviceName,
-      isMultiE10s,
-      name: brandName,
-      version,
+      channel: description.channel,
+      deviceName: description.deviceName,
+      isMultiE10s: description.isMultiE10s,
+      name: description.brandName,
+      version: description.version,
     };
   }
 
+  async checkVersionCompatibility() {
+    return checkVersionCompatibility(this.client);
+  }
+
   async setPreference(prefName, value) {
     const prefType = PREF_TO_TYPE[prefName];
     const preferenceFront = await this.client.mainRoot.getFront("preference");
     switch (prefType) {
       case PREF_TYPES.BOOL:
         return preferenceFront.setBoolPref(prefName, value);
       default:
         throw new Error("Unsupported preference" + prefName);
--- a/devtools/client/aboutdebugging-new/src/types/index.js
+++ b/devtools/client/aboutdebugging-new/src/types/index.js
@@ -1,16 +1,15 @@
 /* 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/. */
 
 "use strict";
 
-const { debugTarget } = require("./debug-target");
-const { runtime, runtimeDetails } = require("./runtime");
-const ui = require("./ui");
+const debugTargetTypes = require("./debug-target");
+const runtimeTypes = require("./runtime");
+const uiTypes = require("./ui");
 
 module.exports = Object.assign({}, {
-  debugTarget,
-  runtime,
-  runtimeDetails,
-  ...ui,
+  ...debugTargetTypes,
+  ...runtimeTypes,
+  ...uiTypes,
 });
--- a/devtools/client/aboutdebugging-new/src/types/runtime.js
+++ b/devtools/client/aboutdebugging-new/src/types/runtime.js
@@ -1,36 +1,62 @@
 /* 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/. */
 
 "use strict";
 
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { ClientWrapper } = require("../modules/client-wrapper");
+const { COMPATIBILITY_STATUS } = require("devtools/client/shared/remote-debugging/version-checker");
 
 const runtimeInfo = {
   // device name which is running the runtime,
   // unavailable on this-firefox runtime
   deviceName: PropTypes.string,
 
   // icon which represents the kind of runtime
   icon: PropTypes.string.isRequired,
 
   // name of runtime such as "Firefox Nightly"
   name: PropTypes.string.isRequired,
 
   // version of runtime
   version: PropTypes.string.isRequired,
 };
 
+const compatibilityReport = {
+  // build ID for the current runtime (date formatted as yyyyMMdd eg "20193101")
+  localID: PropTypes.string.isRequired,
+
+  // "platform" version for the current runtime (eg "67.0a1")
+  localVersion: PropTypes.string.isRequired,
+
+  // minimum "platform" version supported for remote debugging by the current runtime
+  minVersion: PropTypes.string.isRequired,
+
+  // build ID for the target runtime (date formatted as yyyyMMdd eg "20193101")
+  runtimeID: PropTypes.string.isRequired,
+
+  // "platform" version for the target runtime (eg "67.0a1")
+  runtimeVersion: PropTypes.string.isRequired,
+
+  // report result, either COMPATIBLE, TOO_OLD or TOO_RECENT
+  status: PropTypes.oneOf(Object.values(COMPATIBILITY_STATUS)).isRequired,
+};
+exports.compatibilityReport = PropTypes.shape(compatibilityReport);
+
 const runtimeDetails = {
   // ClientWrapper built using a DebuggerClient for the runtime
   clientWrapper: PropTypes.instanceOf(ClientWrapper).isRequired,
 
+  // compatibility report to check if the target runtime is in range of the backward
+  // compatibility policy for DevTools remote debugging.
+  compatibilityReport: PropTypes.shape(compatibilityReport).isRequired,
+
   // reflect devtools.debugger.prompt-connection preference of this runtime
   connectionPromptEnabled: PropTypes.bool.isRequired,
 
   // runtime information
   info: PropTypes.shape(runtimeInfo).isRequired,
 
   // True if this runtime supports multiple content processes
   // This might be undefined when connecting to runtimes older than Fx 66
--- a/devtools/client/aboutdebugging-new/test/browser/browser.ini
+++ b/devtools/client/aboutdebugging-new/test/browser/browser.ini
@@ -56,16 +56,17 @@ skip-if = (os == 'linux' && bits == 32) 
 [browser_aboutdebugging_devtoolstoolbox_reload.js]
 [browser_aboutdebugging_devtoolstoolbox_shortcuts.js]
 skip-if = (os == "win" && ccov) # Bug 1521349
 [browser_aboutdebugging_devtoolstoolbox_tooltip_markupview.js]
 [browser_aboutdebugging_navigate.js]
 [browser_aboutdebugging_persist_connection.js]
 [browser_aboutdebugging_profiler_dialog.js]
 [browser_aboutdebugging_routes.js]
+[browser_aboutdebugging_runtime_compatibility_warning.js]
 [browser_aboutdebugging_runtime_remote_runtime_buttons.js]
 [browser_aboutdebugging_runtime_usbclient_closed.js]
 [browser_aboutdebugging_select_network_runtime.js]
 [browser_aboutdebugging_select_page_with_serviceworker.js]
 [browser_aboutdebugging_serviceworker_fetch_flag.js]
 [browser_aboutdebugging_serviceworker_multie10s.js]
 [browser_aboutdebugging_serviceworker_not_compatible.js]
 [browser_aboutdebugging_serviceworker_push.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_runtime_compatibility_warning.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const COMPATIBLE_RUNTIME = "Compatible Runtime";
+const COMPATIBLE_DEVICE = "Compatible Device";
+const OLD_RUNTIME = "Old Runtime";
+const OLD_DEVICE = "Old Device";
+const RECENT_RUNTIME = "Recent Runtime";
+const RECENT_DEVICE = "Recent Device";
+
+add_task(async function() {
+  const { COMPATIBILITY_STATUS } =
+        require("devtools/client/shared/remote-debugging/version-checker");
+  const { COMPATIBLE, TOO_OLD, TOO_RECENT } = COMPATIBILITY_STATUS;
+
+  info("Create three mocked runtimes, with different compatibility reports");
+  const mocks = new Mocks();
+  createRuntimeWithReport(mocks, COMPATIBLE_RUNTIME, COMPATIBLE_DEVICE, COMPATIBLE);
+  createRuntimeWithReport(mocks, OLD_RUNTIME, OLD_DEVICE, TOO_OLD);
+  createRuntimeWithReport(mocks, RECENT_RUNTIME, RECENT_DEVICE, TOO_RECENT);
+
+  const { document, tab } = await openAboutDebugging();
+  mocks.emitUSBUpdate();
+
+  info("Connect to all runtimes");
+  await connectToRuntime(COMPATIBLE_DEVICE, document);
+  await connectToRuntime(OLD_DEVICE, document);
+  await connectToRuntime(RECENT_DEVICE, document);
+
+  info("Select the compatible runtime and check that no warning is displayed");
+  await selectRuntime(COMPATIBLE_DEVICE, COMPATIBLE_RUNTIME, document);
+  ok(!document.querySelector(".js-compatibility-warning"),
+    "Compatibility warning is not displayed");
+
+  info("Select the old runtime and check that the too-old warning is displayed");
+  await selectRuntime(OLD_DEVICE, OLD_RUNTIME, document);
+  ok(document.querySelector(".js-compatibility-warning-too-old"),
+    "Expected compatibility warning is displayed (too-old)");
+
+  info("Select the recent runtime and check that the too-recent warning is displayed");
+  await selectRuntime(RECENT_DEVICE, RECENT_RUNTIME, document);
+  ok(document.querySelector(".js-compatibility-warning-too-recent"),
+    "Expected compatibility warning is displayed (too-recent)");
+
+  await removeTab(tab);
+});
+
+function createRuntimeWithReport(mocks, name, deviceName, status) {
+  const runtimeId = [name, deviceName].join("-");
+  const compatibleUsbClient = mocks.createUSBRuntime(runtimeId, { deviceName, name });
+  const report = { status };
+  compatibleUsbClient.checkVersionCompatibility = () => report;
+}
--- a/devtools/client/aboutdebugging-new/test/browser/mocks/helper-client-wrapper-mock.js
+++ b/devtools/client/aboutdebugging-new/test/browser/mocks/helper-client-wrapper-mock.js
@@ -83,16 +83,22 @@ function createClientMock() {
     // no-op
     onFront: () => {},
     // stores the preference locally (doesn't update about:config)
     setPreference: function(prefName, value) {
       this._preferences[prefName] = value;
     },
     getPerformancePanelUrl: () => "data:text/html;charset=UTF-8,fake_profiler_page",
     loadPerformanceProfiler: () => {},
+    // Valid compatibility report
+    checkVersionCompatibility: () => {
+      const { COMPATIBILITY_STATUS } =
+        require("devtools/client/shared/remote-debugging/version-checker");
+      return { status: COMPATIBILITY_STATUS.COMPATIBLE };
+    },
   };
 }
 
 // Create a ClientWrapper mock that can be used to replace the this-firefox runtime.
 function createThisFirefoxClientMock() {
   const mockThisFirefoxDescription = {
     name: "Firefox",
     channel: "nightly",
--- a/devtools/client/aboutdebugging-new/tmp-locale/en-US/aboutdebugging.notftl
+++ b/devtools/client/aboutdebugging-new/tmp-locale/en-US/aboutdebugging.notftl
@@ -125,21 +125,35 @@ about-debugging-runtime-service-workers 
 about-debugging-runtime-shared-workers = Shared Workers
 # Title of the other workers category.
 about-debugging-runtime-other-workers = Other Workers
 
 # Label of the button opening the performance profiler panel in runtime pages for remote
 # runtimes.
 about-debugging-runtime-profile-button = Profile Runtime
 
-# This string is displayed in about:debugging#workers if the current configuration of the
-# browser is incompatible with service workers. Learn more points to MDN.
+# This string is displayed in the runtime page if the current configuration of the
+# target runtime is incompatible with service workers. "Learn more" points to MDN.
 # https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging#Service_workers_not_compatible
 about-debugging-runtime-service-workers-not-compatible = Your browser configuration is not compatible with Service Workers. <a>Learn more</a>
 
+# This string is displayed in the runtime page if the remote runtime version is too old.
+# "Troubleshooting" link points to https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting
+# { $runtimeVersion } is the version of the remote runtime (for instance "67.0a1")
+# { $minVersion } is the minimum version that is compatible with the current Firefox instance (same format)
+about-debugging-runtime-version-too-old = The connected runtime has an old version ({ $runtimeVersion }). The minimum supported version is ({ $minVersion }). This is an unsupported setup and may cause DevTools to fail. Please update the connected runtime. <a>Troubleshooting</a>
+
+# This string is displayed in the runtime page if the remote runtime version is too recent.
+# "Troubleshooting" link points to https://developer.mozilla.org/en-US/docs/Tools/WebIDE/Troubleshooting
+# { $runtimeID } is the build ID of the remote runtime (for instance "20181231", format is yyyyMMdd)
+# { $localID } is the build ID of the current Firefox instance (same format)
+# { $runtimeVersion } is the version of the remote runtime (for instance "67.0a1")
+# { $localVersion } is the version of your current runtime (same format)
+about-debugging-runtime-version-too-recent = The connected runtime is more recent ({ $runtimeVersion }, buildID { $runtimeID }) than your desktop Firefox ({ $localVersion }, buildID { $localID }). This is an unsupported setup and may cause DevTools to fail. Please update Firefox. <a>Troubleshooting</a>
+
 # Displayed in the categories of "runtime" pages that don't have any debug target to
 # show. Debug targets depend on the category (extensions, tabs, workers...).
 about-debugging-debug-target-list-empty = Nothing yet.
 
 # Text of a button displayed next to debug targets of "runtime" pages. Clicking on this
 # button will open a DevTools toolbox that will allow inspecting the target.
 # A target can be an addon, a tab, a worker...
 about-debugging-debug-target-inspect-button = Inspect
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -74,17 +74,18 @@ Tools.inspector = {
   panelLabel: l10n("inspector.panelLabel"),
   get tooltip() {
     if (osString == "Darwin") {
       const cmdShiftC = "Cmd+Shift+" + l10n("inspector.commandkey");
       const cmdOptC = "Cmd+Opt+" + l10n("inspector.commandkey");
       return l10n("inspector.mac.tooltip", cmdShiftC, cmdOptC);
     }
 
-    return l10n("inspector.tooltip2", "Ctrl+Shift+") + l10n("inspector.commandkey");
+    const ctrlShiftC = "Ctrl+Shift+" + l10n("inspector.commandkey");
+    return l10n("inspector.tooltip2", ctrlShiftC);
   },
   inMenu: true,
 
   preventClosingOnKey: true,
   onkey: function(panel, toolbox) {
     toolbox.inspector.nodePicker.togglePicker();
   },
 
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -1368,28 +1368,49 @@ Toolbox.prototype = {
 
   /**
    * The element picker button enables the ability to select a DOM node by clicking
    * it on the page.
    */
   _buildPickerButton() {
     this.pickerButton = this._createButtonState({
       id: "command-button-pick",
-      description: L10N.getStr("pickButton.tooltip"),
+      description: this._getPickerTooltip(),
       onClick: this._onPickerClick,
       isInStartContainer: true,
       isTargetSupported: target => {
         return target.traits.frames;
       },
     });
 
     return this.pickerButton;
   },
 
   /**
+   * Get the tooltip for the element picker button.
+   * It has multiple possible keyboard shortcuts for macOS.
+   *
+   * @return {String}
+   */
+  _getPickerTooltip() {
+    let shortcut = L10N.getStr("toolbox.elementPicker.key");
+    shortcut = KeyShortcuts.parseElectronKey(this.win, shortcut);
+    shortcut = KeyShortcuts.stringify(shortcut);
+    const shortcutMac = L10N.getStr("toolbox.elementPicker.mac.key");
+    const isMac = Services.appinfo.OS === "Darwin";
+    const label = isMac
+      ? "toolbox.elementPicker.mac.tooltip"
+      : "toolbox.elementPicker.tooltip";
+
+    return isMac
+      ? L10N.getFormatStr(label, shortcut, shortcutMac)
+      : L10N.getFormatStr(label, shortcut);
+  },
+
+  /**
    * Apply the current cache setting from devtools.cache.disabled to this
    * toolbox's tab.
    */
   _applyCacheSettings: async function() {
     const pref = "devtools.cache.disabled";
     const cacheDisabled = Services.prefs.getBoolPref(pref);
 
     await this.target.reconfigure({
@@ -1478,17 +1499,17 @@ Toolbox.prototype = {
     const button = this.pickerButton;
     const currentPanel = this.getCurrentPanel();
 
     if (currentPanel && currentPanel.updatePickerButton) {
       currentPanel.updatePickerButton();
     } else {
       // If the current panel doesn't define a custom updatePickerButton,
       // revert the button to its default state
-      button.description = L10N.getStr("pickButton.tooltip");
+      button.description = this._getPickerTooltip();
       button.className = null;
       button.disabled = null;
     }
   },
 
   /**
    * Update the visual state of the Frame picker button.
    */
--- a/devtools/client/inspector/changes/reducers/changes.js
+++ b/devtools/client/inspector/changes/reducers/changes.js
@@ -40,16 +40,18 @@ function cloneState(state = {}) {
 /**
  * Given information about a CSS rule and its ancestor rules (@media, @supports, etc),
  * create entries in the given rules collection for each rule and assign parent/child
  * dependencies.
  *
  * @param {Object} ruleData
  *        Information about a CSS rule:
  *        {
+ *          id:        {String}
+ *                     Unique rule id.
  *          selector:  {String}
  *                     CSS selector text
  *          ancestors: {Array}
  *                     Flattened CSS rule tree of the rule's ancestors with the root rule
  *                     at the beginning of the array and the leaf rule at the end.
  *          ruleIndex: {Array}
  *                     Indexes of each ancestor rule within its parent rule.
  *        }
@@ -61,28 +63,29 @@ function cloneState(state = {}) {
  * @return {Object}
  *         Entry for the CSS rule created the given collection of rules.
  */
 function createRule(ruleData, rules) {
   // Append the rule data to the flattened CSS rule tree with its ancestors.
   const ruleAncestry = [...ruleData.ancestors, { ...ruleData }];
 
   return ruleAncestry
-    // First, generate a unique identifier for each rule.
     .map((rule, index) => {
       // Ensure each rule has ancestors excluding itself (expand the flattened rule tree).
       rule.ancestors = ruleAncestry.slice(0, index);
       // Ensure each rule has a selector text.
       // For the purpose of displaying in the UI, we treat at-rules as selectors.
       if (!rule.selector) {
         rule.selector =
           `${rule.typeName} ${(rule.conditionText || rule.name || rule.keyText)}`;
       }
 
-      return getRuleHash(rule);
+      // Bug 1525326: Remove getRuleHash() in Firefox 70. Until then, we fallback
+      // to the custom hashing method if the server did not provide a rule with an id.
+      return rule.id || getRuleHash(rule);
     })
     // Then, create new entries in the rules collection and assign dependencies.
     .map((ruleId, index, array) => {
       const { selector } = ruleAncestry[index];
       const prevRuleId = array[index - 1];
       const nextRuleId = array[index + 1];
 
       // Copy or create an entry for this rule.
@@ -189,29 +192,30 @@ const reducers = {
       ancestors: [],
       add: [],
       remove: [],
     };
 
     change = { ...defaults, ...change };
     state = cloneState(state);
 
-    const { type, href, index, isFramed } = change.source;
     const { selector, ancestors, ruleIndex, type: changeType } = change;
-    const sourceId = getSourceHash(change.source);
-    const ruleId = getRuleHash({ selector, ancestors, ruleIndex });
+    // Bug 1525326: remove getSourceHash() and getRuleHash() in Firefox 70 after we no
+    // longer support old servers which do not implement the id for the rule and source.
+    const sourceId = change.source.id || getSourceHash(change.source);
+    const ruleId = change.id || getRuleHash({ selector, ancestors, ruleIndex });
 
     // Copy or create object identifying the source (styelsheet/element) for this change.
-    const source = Object.assign({}, state[sourceId], { type, href, index, isFramed });
+    const source = Object.assign({}, state[sourceId], change.source);
     // Copy or create collection of all rules ever changed in this source.
     const rules = Object.assign({}, source.rules);
-    // Refrence or create object identifying the rule for this change.
+    // Reference or create object identifying the rule for this change.
     let rule = rules[ruleId];
     if (!rule) {
-      rule = createRule({ selector, ancestors, ruleIndex }, rules);
+      rule = createRule(change, rules);
       if (changeType.startsWith("rule-")) {
         rule.changeType = changeType;
       }
     }
 
     if (change.remove && change.remove.length) {
       for (const decl of change.remove) {
         // Find the position of any added declaration which matches the incoming
--- a/devtools/client/inspector/changes/test/browser.ini
+++ b/devtools/client/inspector/changes/test/browser.ini
@@ -16,8 +16,9 @@ support-files =
 [browser_changes_declaration_duplicate.js]
 [browser_changes_declaration_edit_value.js]
 [browser_changes_declaration_identical_rules.js]
 [browser_changes_declaration_remove_ahead.js]
 [browser_changes_declaration_remove_disabled.js]
 [browser_changes_declaration_remove.js]
 [browser_changes_declaration_rename.js]
 [browser_changes_rule_selector.js]
+skip-if=true # Enable with Bug 1527924
--- a/devtools/client/locales/en-US/toolbox.properties
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -27,19 +27,33 @@ toolbox.label=Developer Tools
 # The name of the tool: %1$S.
 options.toolNotSupportedMarker=%1$S *
 
 # LOCALIZATION NOTE (scratchpad.keycode)
 # Used for opening scratchpad from the detached toolbox window
 # Needs to match scratchpad.keycode from browser.dtd
 scratchpad.keycode=VK_F4
 
-# LOCALIZATION NOTE (pickButton.tooltip)
-# This is the tooltip of the pick button in the toolbox toolbar
-pickButton.tooltip=Pick an element from the page
+# LOCALIZATION NOTE (toolbox.pickButton.tooltip)
+# This is the tooltip of the element picker button in the toolbox toolbar.
+# %S is the keyboard shortcut that toggles the element picker.
+toolbox.elementPicker.tooltip=Pick an element from the page (%S)
+
+# LOCALIZATION NOTE (toolbox.pickButton.mac.tooltip)
+# Like toolbox.pickButton.tooltip, but for macOS there are two possible keyboard
+# shortcuts: Cmd+Shift+C or Cmd+Opt+C
+toolbox.elementPicker.mac.tooltip=Pick an element from the page (%1$S or %2$S)
+
+# LOCALIZATION NOTE (toolbox.elementPicker.key)
+# Key shortcut used to toggle the element picker.
+toolbox.elementPicker.key=CmdOrCtrl+Shift+C
+
+# LOCALIZATION NOTE (toolbox.elementPicker.mac.key)
+# Key shortcut used to toggle the element picker for macOS.
+toolbox.elementPicker.mac.key=Cmd+Opt+C
 
 # LOCALIZATION NOTE (sidebar.showAllTabs.tooltip)
 # This is the tooltip shown when hover over the '…' button in the tabbed side
 # bar, when there's no enough space to show all tabs at once
 sidebar.showAllTabs.tooltip=All tabs
 
 # LOCALIZATION NOTE (toolbox.noContentProcessForTab.message)
 # Used as a message in the alert displayed when trying to open a browser
--- a/devtools/client/netmonitor/src/assets/styles/StatusCode.css
+++ b/devtools/client/netmonitor/src/assets/styles/StatusCode.css
@@ -22,15 +22,16 @@
 .status-code[data-code^="3"] {
   background-color: var(--status-code-color-3xx);
 }
 
 .status-code[data-code^="4"] {
   background-color: var(--status-code-color-4xx);
 }
 
-.status-code[data-code^="5"] {
+.status-code[data-code^="5"], .status-code[data-code^="6"], .status-code[data-code^="7"],
+.status-code[data-code^="8"], .status-code[data-code^="9"] {
   background-color: var(--status-code-color-5xx);
 }
 
 .status-code:not([data-code^="3"]) {
   color: var(--theme-body-background);
 }
--- a/devtools/client/shared/components/NotificationBox.js
+++ b/devtools/client/shared/components/NotificationBox.js
@@ -2,17 +2,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Component } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const Immutable = require("devtools/client/shared/vendor/immutable");
 const { LocalizationHelper } = require("devtools/shared/l10n");
 
 const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
 const { div, span, button } = dom;
 
 // Priority Levels
 const PriorityLevels = {
   PRIORITY_INFO_LOW: 1,
@@ -38,75 +37,60 @@ const PriorityLevels = {
  * as well as consume list of notifications provided as a prop
  * (coming e.g. from Redux store).
  */
 class NotificationBox extends Component {
   static get propTypes() {
     return {
       // Optional box ID (used for mounted node ID attribute)
       id: PropTypes.string,
-
-      // List of notifications appended into the box.
-      // Use `PropTypes.arrayOf` validation (see below) as soon as
-      // ImmutableJS is removed. See bug 1461678 for more details.
-      notifications: PropTypes.object,
-      /* notifications: PropTypes.arrayOf(PropTypes.shape({
-        // label to appear on the notification.
-        label: PropTypes.string.isRequired,
-
-        // Value used to identify the notification
-        value: PropTypes.string.isRequired,
-
-        // URL of image to appear on the notification. If "" then an icon
-        // appropriate for the priority level is used.
-        image: PropTypes.string.isRequired,
-
-        // Notification priority; see Priority Levels.
-        priority: PropTypes.number.isRequired,
-
-        // Array of button descriptions to appear on the notification.
-        buttons: PropTypes.arrayOf(PropTypes.shape({
-          // Function to be called when the button is activated.
-          // This function is passed three arguments:
-          // 1) the NotificationBox component the button is associated with
-          // 2) the button description as passed to appendNotification.
-          // 3) the element which was the target of the button press event.
-          // If the return value from this function is not True, then the
-          // notification is closed. The notification is also not closed
-          // if an error is thrown.
-          callback: PropTypes.func.isRequired,
-
-          // The label to appear on the button.
-          label: PropTypes.string.isRequired,
-
-          // The accesskey attribute set on the <button> element.
-          accesskey: PropTypes.string,
-        })),
-
-        // A function to call to notify you of interesting things that happen
-        // with the notification box.
-        eventCallback: PropTypes.func,
-      })),*/
-
+      /**
+       * List of notifications appended into the box. Each item of the map is an object
+       * of the following shape:
+       *   - {String} label: Label to appear on the notification.
+       *   - {String} value: Value used to identify the notification.
+       *   - {String} image: URL of image to appear on the notification. If "" then an
+       *                     appropriate icon for the priority level is used.
+       *   - {Number} priority: Notification priority; see Priority Levels.
+       *   - {Function} eventCallback: A function to call to notify you of interesting
+                                       things that happen with the notification box.
+       *   - {Array<Object>} buttons: Array of button descriptions to appear on the
+       *                              notification. Should be of the following shape:
+       *                     - {Function} callback: This function is passed 3 arguments:
+                                                    1) the NotificationBox component
+                                                       the button is associated with.
+                                                    2) the button description as passed
+                                                       to appendNotification.
+                                                    3) the element which was the target
+                                                       of the button press event.
+                                                    If the return value from this function
+                                                    is not true, then the notification is
+                                                    closed. The notification is also not
+                                                    closed if an error is thrown.
+                             - {String} label: The label to appear on the button.
+                             - {String} accesskey: The accesskey attribute set on the
+                                                   <button> element.
+      */
+      notifications: PropTypes.instanceOf(Map),
       // Message that should be shown when hovering over the close button
       closeButtonTooltip: PropTypes.string,
     };
   }
 
   static get defaultProps() {
     return {
       closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip"),
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
-      notifications: new Immutable.OrderedMap(),
+      notifications: new Map(),
     };
 
     this.appendNotification = this.appendNotification.bind(this);
     this.removeNotification = this.removeNotification.bind(this);
     this.getNotificationWithValue = this.getNotificationWithValue.bind(this);
     this.getCurrentNotification = this.getCurrentNotification.bind(this);
     this.close = this.close.bind(this);
     this.renderButton = this.renderButton.bind(this);
@@ -155,17 +139,17 @@ class NotificationBox extends Component 
     return Object.assign({}, notification, {
       close: () => {
         this.close(notification);
       },
     });
   }
 
   getCurrentNotification() {
-    return this.state.notifications.first();
+    return getHighestPriorityNotification(this.state.notifications);
   }
 
   /**
    * Close specified notification.
    */
   close(notification) {
     if (!notification) {
       return;
@@ -174,18 +158,20 @@ class NotificationBox extends Component 
     if (notification.eventCallback) {
       notification.eventCallback("removed");
     }
 
     if (!this.state.notifications.get(notification.value)) {
       return;
     }
 
+    const newNotifications = new Map(this.state.notifications);
+    newNotifications.delete(notification.value);
     this.setState({
-      notifications: this.state.notifications.remove(notification.value),
+      notifications: newNotifications,
     });
   }
 
   /**
    * Render a button. A notification can have a set of custom buttons.
    * These are used to execute custom callback.
    */
   renderButton(props, notification) {
@@ -246,26 +232,25 @@ class NotificationBox extends Component 
   }
 
   /**
    * Render the top (highest priority) notification. Only one
    * notification is rendered at a time.
    */
   render() {
     const notifications = this.props.notifications || this.state.notifications;
-    const notification = notifications ? notifications.first() : null;
-    const content = notification ?
-      this.renderNotification(notification) :
-      null;
+    const notification = getHighestPriorityNotification(notifications);
+    const content = notification
+      ? this.renderNotification(notification)
+      : null;
 
     return div({
       className: "notificationbox",
-      id: this.props.id},
-      content
-    );
+      id: this.props.id,
+    }, content);
   }
 }
 
 // Helpers
 
 /**
  * Create a new notification. If another notification is already present with
  * a higher priority, the new notification will be added behind it.
@@ -296,46 +281,61 @@ function appendNotification(state, props
   let type = "warning";
   if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
     type = "critical";
   } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
     type = "info";
   }
 
   if (!state.notifications) {
-    state.notifications = new Immutable.OrderedMap();
+    state.notifications = new Map();
   }
 
-  let notifications = state.notifications.set(value, {
-    label: label,
-    value: value,
-    image: image,
-    priority: priority,
-    type: type,
+  const notifications = new Map(state.notifications);
+  notifications.set(value, {
+    label,
+    value,
+    image,
+    priority,
+    type,
     buttons: Array.isArray(buttons) ? buttons : [],
-    eventCallback: eventCallback,
-  });
-
-  // High priorities must be on top.
-  notifications = notifications.sortBy((val, key) => {
-    return -val.priority;
+    eventCallback,
   });
 
   return {
-    notifications: notifications,
+    notifications,
   };
 }
 
 function getNotificationWithValue(notifications, value) {
   return notifications ? notifications.get(value) : null;
 }
 
 function removeNotificationWithValue(notifications, value) {
+  const newNotifications = new Map(notifications);
+  newNotifications.delete(value);
+
   return {
-    notifications: notifications.remove(value),
+    notifications: newNotifications,
   };
 }
 
+function getHighestPriorityNotification(notifications) {
+  if (!notifications) {
+    return null;
+  }
+
+  let currentNotification = null;
+  // High priorities must be on top.
+  for (const [, notification] of notifications) {
+    if (!currentNotification || notification.priority > currentNotification.priority) {
+      currentNotification = notification;
+    }
+  }
+
+  return currentNotification;
+}
+
 module.exports.NotificationBox = NotificationBox;
 module.exports.PriorityLevels = PriorityLevels;
 module.exports.appendNotification = appendNotification;
 module.exports.getNotificationWithValue = getNotificationWithValue;
 module.exports.removeNotificationWithValue = removeNotificationWithValue;
--- a/devtools/client/shared/remote-debugging/version-checker.js
+++ b/devtools/client/shared/remote-debugging/version-checker.js
@@ -132,16 +132,17 @@ function _compareVersionCompatibility(lo
     // device builds, since their devices will almost always be newer than the client.
     status = COMPATIBILITY_STATUS.TOO_RECENT;
   } else {
     status = COMPATIBILITY_STATUS.COMPATIBLE;
   }
 
   return {
     localID,
+    localVersion,
     minVersion,
     runtimeID,
     runtimeVersion,
     status,
   };
 }
 // Exported for tests.
 exports._compareVersionCompatibility = _compareVersionCompatibility;
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -1080,20 +1080,22 @@ var StyleRuleActor = protocol.ActorClass
    * - the rule's host stylesheet (or element for inline styles)
    * - the rule's ancestor rules (@media, @supports, @keyframes), if any
    * - the rule's position within its ancestor tree, if any
    *
    * @return {Object}
    */
   get metadata() {
     const data = {};
+    data.id = this.actorID;
     // Collect information about the rule's ancestors (@media, @supports, @keyframes).
     // Used to show context for this change in the UI and to match the rule for undo/redo.
     data.ancestors = this.ancestorRules.map(rule => {
       return {
+        id: rule.actorID,
         // Rule type as number defined by CSSRule.type (ex: 4, 7, 12)
         // @see https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
         type: rule.rawRule.type,
         // Rule type as human-readable string (ex: "@media", "@supports", "@keyframes")
         typeName: CSSRuleTypeName[rule.rawRule.type],
         // Conditions of @media and @supports rules (ex: "min-width: 1em")
         conditionText: rule.rawRule.conditionText,
         // Name of @keyframes rule; refrenced by the animation-name CSS property.
@@ -1101,41 +1103,48 @@ var StyleRuleActor = protocol.ActorClass
         // Selector of individual @keyframe rule within a @keyframes rule (ex: 0%, 100%).
         keyText: rule.rawRule.keyText,
         // Array with the indexes of this rule and its ancestors within the CSS rule tree.
         ruleIndex: rule._ruleIndex,
       };
     });
 
     // For changes in element style attributes, generate a unique selector.
-    if (this.type === ELEMENT_STYLE) {
+    if (this.type === ELEMENT_STYLE && this.rawNode) {
       // findCssSelector() fails on XUL documents. Catch and silently ignore that error.
       try {
         data.selector = findCssSelector(this.rawNode);
       } catch (err) {}
 
       data.source = {
         type: "element",
         // Used to differentiate between elements which match the same generated selector
         // but live in different documents (ex: host document and iframe).
         href: this.rawNode.baseURI,
         // Element style attributes don't have a rule index; use the generated selector.
         index: data.selector,
         // Whether the element lives in a different frame than the host document.
         isFramed: this.rawNode.ownerGlobal !== this.pageStyle.ownerWindow,
       };
+
+      const nodeActor = this.pageStyle.walker.getNode(this.rawNode);
+      if (nodeActor) {
+        data.source.id = nodeActor.actorID;
+      }
+
       data.ruleIndex = 0;
     } else {
       data.selector = (this.type === CSSRule.KEYFRAME_RULE)
         ? this.rawRule.keyText
         : this.rawRule.selectorText;
       data.source = {
         // Inline stylesheets have a null href; Use window URL instead.
         type: this.sheetActor.href ? "stylesheet" : "inline",
         href: this.sheetActor.href || this.sheetActor.window.location.toString(),
+        id: this.sheetActor.actorID,
         index: this.sheetActor.styleSheetIndex,
         // Whether the stylesheet lives in a different frame than the host document.
         isFramed: this.sheetActor.ownerWindow !== this.sheetActor.window,
       };
       // Used to differentiate between changes to rules with identical selectors.
       data.ruleIndex = this._ruleIndex;
     }
 
--- a/dom/push/PushServiceAndroidGCM.jsm
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -108,29 +108,36 @@ var PushServiceAndroidGCM = {
       });
   },
 
   _messageAndHeaders(data) {
     // Default is no data (and no encryption).
     let message = null;
     let headers = null;
 
-    if (data.message && data.enc && (data.enckey || data.cryptokey)) {
-      headers = {
-        encryption_key: data.enckey,
-        crypto_key: data.cryptokey,
-        encryption: data.enc,
-        encoding: data.con,
-      };
+    if (data.message) {
+      if (data.enc && (data.enckey || data.cryptokey)) {
+        headers = {
+          encryption_key: data.enckey,
+          crypto_key: data.cryptokey,
+          encryption: data.enc,
+          encoding: data.con,
+        };
+      } else if (data.con == 'aes128gcm') {
+        headers = {
+          encoding: data.con,
+        };
+      }
       // Ciphertext is (urlsafe) Base 64 encoded.
       message = ChromeUtils.base64URLDecode(data.message, {
         // The Push server may append padding.
         padding: "ignore",
       });
     }
+
     return { headers, message };
   },
 
   _configure: function(serverURL, debug) {
     return EventDispatcher.instance.sendRequestForResult({
       type: "PushServiceAndroidGCM:Configure",
       endpoint: serverURL.spec,
       debug: debug,
--- a/gfx/layers/wr/AsyncImagePipelineManager.cpp
+++ b/gfx/layers/wr/AsyncImagePipelineManager.cpp
@@ -229,18 +229,17 @@ Maybe<TextureHost::ResourceUpdateOp> Asy
   WebRenderTextureHost* wrTexture = texture->AsWebRenderTextureHost();
 
   bool useExternalImage = !gfxEnv::EnableWebRenderRecording() && wrTexture;
   aPipeline->mUseExternalImage = useExternalImage;
 
   // Use WebRenderTextureHostWrapper only for video.
   // And WebRenderTextureHostWrapper could be used only with
   // WebRenderTextureHost that supports NativeTexture
-  bool useWrTextureWrapper = aPipeline->mImageHost->GetAsyncRef() &&
-                             useExternalImage && wrTexture &&
+  bool useWrTextureWrapper = useExternalImage && wrTexture &&
                              wrTexture->SupportsWrNativeTexture();
 
   // The non-external image code path falls back to converting the texture into
   // an rgb image.
   auto numKeys = useExternalImage ? texture->NumSubTextures() : 1;
 
   // If we already had a texture and the format hasn't changed, better to reuse
   // the image keys than create new ones.
@@ -275,23 +274,23 @@ Maybe<TextureHost::ResourceUpdateOp> Asy
   if (!useExternalImage) {
     return UpdateWithoutExternalImage(texture, aKeys[0], op, aMaybeFastTxn);
   }
 
   if (useWrTextureWrapper && aPipeline->mWrTextureWrapper) {
     MOZ_ASSERT(canUpdate);
     // Reuse WebRenderTextureHostWrapper. With it, rendered frame could be
     // updated without batch re-creation.
-    aPipeline->mWrTextureWrapper->UpdateWebRenderTextureHost(wrTexture);
+    aPipeline->mWrTextureWrapper->UpdateWebRenderTextureHost(aMaybeFastTxn, wrTexture);
     // Ensure frame generation.
     SetWillGenerateFrame();
   } else {
     if (useWrTextureWrapper) {
       aPipeline->mWrTextureWrapper = new WebRenderTextureHostWrapper(this);
-      aPipeline->mWrTextureWrapper->UpdateWebRenderTextureHost(wrTexture);
+      aPipeline->mWrTextureWrapper->UpdateWebRenderTextureHost(aMaybeFastTxn, wrTexture);
     }
     Range<wr::ImageKey> keys(&aKeys[0], aKeys.Length());
     auto externalImageKey =
         aPipeline->mWrTextureWrapper
             ? aPipeline->mWrTextureWrapper->GetExternalImageKey()
             : wrTexture->GetExternalImageKey();
     wrTexture->PushResourceUpdates(aMaybeFastTxn, op, keys, externalImageKey);
   }
--- a/gfx/layers/wr/WebRenderTextureHostWrapper.cpp
+++ b/gfx/layers/wr/WebRenderTextureHostWrapper.cpp
@@ -5,20 +5,45 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "WebRenderTextureHostWrapper.h"
 
 #include "mozilla/layers/AsyncImagePipelineManager.h"
 #include "mozilla/layers/WebRenderTextureHost.h"
 #include "mozilla/webrender/RenderTextureHostWrapper.h"
 #include "mozilla/webrender/RenderThread.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/webrender/RenderThread.h"
 
 namespace mozilla {
 namespace layers {
 
+class ScheduleUpdateRenderTextureHost : public wr::NotificationHandler {
+ public:
+  ScheduleUpdateRenderTextureHost(uint64_t aSrcExternalImageId,
+                                  uint64_t aWrappedExternalImageId)
+      : mSrcExternalImageId(aSrcExternalImageId),
+        mWrappedExternalImageId(aWrappedExternalImageId) {}
+
+  virtual void Notify(wr::Checkpoint aCheckpoint) override {
+    if (aCheckpoint == wr::Checkpoint::FrameTexturesUpdated) {
+      MOZ_ASSERT(wr::RenderThread::IsInRenderThread());
+      wr::RenderThread::Get()->UpdateRenderTextureHost(
+          mSrcExternalImageId,
+          mWrappedExternalImageId);
+    } else {
+      MOZ_ASSERT(aCheckpoint == wr::Checkpoint::TransactionDropped);
+    }
+  }
+
+ protected:
+  uint64_t mSrcExternalImageId;
+  uint64_t mWrappedExternalImageId;
+};
+
 WebRenderTextureHostWrapper::WebRenderTextureHostWrapper(
     AsyncImagePipelineManager* aManager)
     : mExternalImageId(aManager->GetNextExternalImageId()) {
   MOZ_ASSERT(aManager);
   MOZ_COUNT_CTOR(WebRenderTextureHostWrapper);
 
   RefPtr<wr::RenderTextureHost> texture = new wr::RenderTextureHostWrapper();
   wr::RenderThread::Get()->RegisterExternalImage(wr::AsUint64(mExternalImageId),
@@ -27,18 +52,26 @@ WebRenderTextureHostWrapper::WebRenderTe
 
 WebRenderTextureHostWrapper::~WebRenderTextureHostWrapper() {
   MOZ_COUNT_DTOR(WebRenderTextureHostWrapper);
   wr::RenderThread::Get()->UnregisterExternalImage(
       wr::AsUint64(mExternalImageId));
 }
 
 void WebRenderTextureHostWrapper::UpdateWebRenderTextureHost(
+    wr::TransactionBuilder& aTxn,
     WebRenderTextureHost* aTextureHost) {
   MOZ_ASSERT(aTextureHost);
+
+  // AsyncImagePipelineManager is responsible of holding compositable ref of
+  // wrapped WebRenderTextureHost by using ForwardingTextureHost.
+  // ScheduleUpdateRenderTextureHost does not need to handle it.
+
+  aTxn.Notify(wr::Checkpoint::FrameTexturesUpdated,
+      MakeUnique<ScheduleUpdateRenderTextureHost>(
+        wr::AsUint64(mExternalImageId),
+        wr::AsUint64(aTextureHost->GetExternalImageKey())));
+
   mWrTextureHost = aTextureHost;
-  wr::RenderThread::Get()->UpdateRenderTextureHost(
-      wr::AsUint64(mExternalImageId),
-      wr::AsUint64(aTextureHost->GetExternalImageKey()));
 }
 
 }  // namespace layers
 }  // namespace mozilla
--- a/gfx/layers/wr/WebRenderTextureHostWrapper.h
+++ b/gfx/layers/wr/WebRenderTextureHostWrapper.h
@@ -16,17 +16,18 @@ class WebRenderTextureHost;
 class AsyncImagePipelineManager;
 
 class WebRenderTextureHostWrapper {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebRenderTextureHostWrapper)
 
  public:
   explicit WebRenderTextureHostWrapper(AsyncImagePipelineManager* aManager);
 
-  void UpdateWebRenderTextureHost(WebRenderTextureHost* aTextureHost);
+  void UpdateWebRenderTextureHost(wr::TransactionBuilder& aTxn,
+                                  WebRenderTextureHost* aTextureHost);
 
   wr::ExternalImageId GetExternalImageKey() { return mExternalImageId; }
 
  protected:
   virtual ~WebRenderTextureHostWrapper();
 
   RefPtr<WebRenderTextureHost> mWrTextureHost;
   wr::ExternalImageId mExternalImageId;
--- a/gfx/webrender_bindings/RenderTextureHostWrapper.cpp
+++ b/gfx/webrender_bindings/RenderTextureHostWrapper.cpp
@@ -44,17 +44,17 @@ void RenderTextureHostWrapper::Unlock() 
 void RenderTextureHostWrapper::ClearCachedResources() {
   if (mTextureHost) {
     mTextureHost->ClearCachedResources();
   }
 }
 
 void RenderTextureHostWrapper::UpdateRenderTextureHost(
     RenderTextureHost* aTextureHost) {
-  MOZ_ASSERT(!mInited || RenderThread::IsInRenderThread());
+  MOZ_ASSERT(RenderThread::IsInRenderThread());
   MOZ_RELEASE_ASSERT(!mLocked);
 
   mInited = true;
   mTextureHost = aTextureHost;
 }
 
 }  // namespace wr
 }  // namespace mozilla
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -589,16 +589,17 @@ void RenderThread::UnregisterExternalIma
   } else {
     mRenderTextures.erase(it);
   }
 }
 
 void RenderThread::UpdateRenderTextureHost(uint64_t aSrcExternalImageId,
                                            uint64_t aWrappedExternalImageId) {
   MOZ_ASSERT(aSrcExternalImageId != aWrappedExternalImageId);
+  MOZ_ASSERT(RenderThread::IsInRenderThread());
 
   MutexAutoLock lock(mRenderTextureMapLock);
   if (mHasShutdown) {
     return;
   }
   auto src = mRenderTextures.find(aSrcExternalImageId);
   auto wrapped = mRenderTextures.find(aWrappedExternalImageId);
   if (src == mRenderTextures.end() || wrapped == mRenderTextures.end()) {
@@ -606,24 +607,17 @@ void RenderThread::UpdateRenderTextureHo
   }
   MOZ_ASSERT(src->second->AsRenderTextureHostWrapper());
   MOZ_ASSERT(!wrapped->second->AsRenderTextureHostWrapper());
   RenderTextureHostWrapper* wrapper = src->second->AsRenderTextureHostWrapper();
   if (!wrapper) {
     MOZ_ASSERT_UNREACHABLE("unexpected to happen");
     return;
   }
-  if (!wrapper->IsInited()) {
-    wrapper->UpdateRenderTextureHost(wrapped->second);
-    MOZ_ASSERT(wrapper->IsInited());
-  } else {
-    Loop()->PostTask(NewRunnableMethod<RenderTextureHost*>(
-        "RenderTextureHostWrapper::UpdateRenderTextureHost", wrapper,
-        &RenderTextureHostWrapper::UpdateRenderTextureHost, wrapped->second));
-  }
+  wrapper->UpdateRenderTextureHost(wrapped->second);
 }
 
 void RenderThread::UnregisterExternalImageDuringShutdown(
     uint64_t aExternalImageId) {
   MOZ_ASSERT(IsInRenderThread());
   MutexAutoLock lock(mRenderTextureMapLock);
   MOZ_ASSERT(mHasShutdown);
   MOZ_ASSERT(mRenderTextures.find(aExternalImageId) != mRenderTextures.end());
--- a/gfx/webrender_bindings/RenderThread.h
+++ b/gfx/webrender_bindings/RenderThread.h
@@ -180,17 +180,17 @@ class RenderThread final {
 
   /// Can be called from any thread.
   void RegisterExternalImage(uint64_t aExternalImageId,
                              already_AddRefed<RenderTextureHost> aTexture);
 
   /// Can be called from any thread.
   void UnregisterExternalImage(uint64_t aExternalImageId);
 
-  /// Can be called from any thread.
+  /// Can only be called from the render thread.
   void UpdateRenderTextureHost(uint64_t aSrcExternalImageId,
                                uint64_t aWrappedExternalImageId);
 
   /// Can only be called from the render thread.
   void UnregisterExternalImageDuringShutdown(uint64_t aExternalImageId);
 
   /// Can only be called from the render thread.
   RenderTextureHost* GetRenderTexture(WrExternalImageId aExternalImageId);
--- a/gfx/wr/webrender/src/scene_builder.rs
+++ b/gfx/wr/webrender/src/scene_builder.rs
@@ -575,16 +575,17 @@ impl SceneBuilder {
 
                 (Some(info), Some(tx), Some(rx))
             }
             _ => (None, None, None),
         };
 
         let scene_swap_start_time = precise_time_ns();
         let has_resources_updates = !txn.resource_updates.is_empty();
+        let invalidate_rendered_frame = txn.invalidate_rendered_frame;
 
         self.tx.send(SceneBuilderResult::Transaction(txn, result_tx)).unwrap();
 
         let _ = self.api_tx.send(ApiMsg::WakeUp);
 
         if let Some(pipeline_info) = pipeline_info {
             // Block until the swap is done, then invoke the hook.
             let swap_result = result_rx.unwrap().recv();
@@ -592,17 +593,17 @@ impl SceneBuilder {
             self.hooks.as_ref().unwrap().post_scene_swap(pipeline_info, scene_swap_time);
             // Once the hook is done, allow the RB thread to resume
             match swap_result {
                 Ok(SceneSwapResult::Complete(resume_tx)) => {
                     resume_tx.send(()).ok();
                 },
                 _ => (),
             };
-        } else if has_resources_updates {
+        } else if has_resources_updates || invalidate_rendered_frame {
             if let &Some(ref hooks) = &self.hooks {
                 hooks.post_resource_update();
             }
         } else {
             if let &Some(ref hooks) = &self.hooks {
                 hooks.post_empty_scene_build();
             }
         }
--- a/intl/lwbrk/LineBreaker.cpp
+++ b/intl/lwbrk/LineBreaker.cpp
@@ -521,17 +521,17 @@ static int8_t GetClass(uint32_t u) {
       /* REGIONAL_INDICATOR = 39,           [RI] */ CLASS_CHARACTER,
       /* E_BASE = 40,                       [EB] */ CLASS_BREAKABLE,
       /* E_MODIFIER = 41,                   [EM] */ CLASS_CHARACTER,
       /* ZWJ = 42,                          [ZWJ]*/ CLASS_CHARACTER};
 
   static_assert(U_LB_COUNT == mozilla::ArrayLength(sUnicodeLineBreakToClass),
                 "Gecko vs ICU LineBreak class mismatch");
 
-  auto cls = mozilla::unicode::GetLineBreakClass(u);
+  auto cls = GetLineBreakClass(u);
   MOZ_ASSERT(cls < mozilla::ArrayLength(sUnicodeLineBreakToClass));
   return sUnicodeLineBreakToClass[cls];
 }
 
 static bool GetPair(int8_t c1, int8_t c2) {
   NS_ASSERTION(c1 < MAX_CLASSES, "illegal classes 1");
   NS_ASSERTION(c2 < MAX_CLASSES, "illegal classes 2");
 
@@ -885,26 +885,45 @@ void LineBreaker::GetJISx4051Breaks(cons
       }
       cl = ContextualAnalysis(prev, ch, next, state);
     } else {
       if (ch == U_EQUAL) state.NotifySeenEqualsSign();
       state.NotifyNonHyphenCharacter(ch);
       cl = GetClass(ch);
     }
 
+    // To implement word-break:break-all, we overwrite the line-break class of
+    // alphanumeric characters so they are treated the same as ideographic.
+    // The relevant characters will have been assigned CLASS_CHARACTER or
+    // CLASS_CLOSE by GetClass(), but those classes also include others that
+    // we don't want to touch here, so we re-check the Unicode line-break class
+    // to determine which ones to modify.
+    if (aWordBreak == LineBreaker::kWordBreak_BreakAll &&
+        (cl == CLASS_CHARACTER || cl == CLASS_CLOSE)) {
+      auto cls = GetLineBreakClass(ch);
+      if (cls == U_LB_ALPHABETIC || cls == U_LB_NUMERIC ||
+          cls == U_LB_AMBIGUOUS || cls == U_LB_COMPLEX_CONTEXT ||
+          /* Additional Japanese and Korean LB classes; CSS Text spec doesn't
+             explicitly mention these, but this appears to give expected
+             behavior (spec issue?) */
+          cls == U_LB_CONDITIONAL_JAPANESE_STARTER ||
+          (cls >= U_LB_H2 && cls <= U_LB_JV)) {
+        cl = CLASS_BREAKABLE;
+      }
+    }
+
     bool allowBreak = false;
     if (cur > 0) {
       NS_ASSERTION(CLASS_COMPLEX != lastClass || CLASS_COMPLEX != cl,
                    "Loop should have prevented adjacent complex chars here");
-      if (aWordBreak == LineBreaker::kWordBreak_Normal) {
+      if (aWordBreak == LineBreaker::kWordBreak_Normal ||
+          aWordBreak == LineBreaker::kWordBreak_BreakAll) {
         allowBreak = (state.UseConservativeBreaking())
                          ? GetPairConservative(lastClass, cl)
                          : GetPair(lastClass, cl);
-      } else if (aWordBreak == LineBreaker::kWordBreak_BreakAll) {
-        allowBreak = true;
       }
     }
     aBreakBefore[cur] = allowBreak;
     if (allowBreak) state.NotifyBreakBefore();
     lastClass = cl;
     if (CLASS_COMPLEX == cl) {
       uint32_t end = cur + chLen;
 
@@ -914,29 +933,31 @@ void LineBreaker::GetJISx4051Breaks(cons
           break;
         }
         ++end;
         if (c > 0xFFFFU) {  // it was a surrogate pair
           ++end;
         }
       }
 
-      NS_GetComplexLineBreaks(aChars + cur, end - cur, aBreakBefore + cur);
-
-      // We have to consider word-break value again for complex characters
-      if (aWordBreak != LineBreaker::kWordBreak_Normal) {
-        // Respect word-break property
-        for (uint32_t i = cur; i < end; i++)
-          aBreakBefore[i] = (aWordBreak == LineBreaker::kWordBreak_BreakAll);
+      if (aWordBreak == LineBreaker::kWordBreak_BreakAll) {
+        // For break-all, we don't need to run a dictionary-based breaking
+        // algorithm, we just allow breaks between all grapheme clusters.
+        ClusterIterator ci(aChars + cur, end - cur);
+        while (!ci.AtEnd()) {
+          ci.Next();
+          aBreakBefore[ci - aChars] = true;
+        }
+      } else {
+        NS_GetComplexLineBreaks(aChars + cur, end - cur, aBreakBefore + cur);
+        // restore breakability at chunk begin, which was always set to false
+        // by the complex line breaker
+        aBreakBefore[cur] = allowBreak;
       }
 
-      // restore breakability at chunk begin, which was always set to false
-      // by the complex line breaker
-      aBreakBefore[cur] = allowBreak;
-
       cur = end - 1;
     }
 
     if (chLen == 2) {
       // Supplementary-plane character: mark that we cannot break before the
       // trailing low surrogate, and advance past it.
       ++cur;
       aBreakBefore[cur] = false;
@@ -959,24 +980,32 @@ void LineBreaker::GetJISx4051Breaks(cons
       cl = ContextualAnalysis(cur > 0 ? aChars[cur - 1] : U_NULL, ch,
                               cur + 1 < aLength ? aChars[cur + 1] : U_NULL,
                               state);
     } else {
       if (ch == U_EQUAL) state.NotifySeenEqualsSign();
       state.NotifyNonHyphenCharacter(ch);
       cl = GetClass(ch);
     }
+    if (aWordBreak == LineBreaker::kWordBreak_BreakAll &&
+        (cl == CLASS_CHARACTER || cl == CLASS_CLOSE)) {
+      auto cls = GetLineBreakClass(ch);
+      // Don't need to check additional Japanese/Korean classes in 8-bit
+      if (cls == U_LB_ALPHABETIC || cls == U_LB_NUMERIC ||
+          cls == U_LB_COMPLEX_CONTEXT) {
+        cl = CLASS_BREAKABLE;
+      }
+    }
 
     bool allowBreak = false;
     if (cur > 0) {
-      if (aWordBreak == LineBreaker::kWordBreak_Normal) {
+      if (aWordBreak == LineBreaker::kWordBreak_Normal ||
+          aWordBreak == LineBreaker::kWordBreak_BreakAll) {
         allowBreak = (state.UseConservativeBreaking())
                          ? GetPairConservative(lastClass, cl)
                          : GetPair(lastClass, cl);
-      } else if (aWordBreak == LineBreaker::kWordBreak_BreakAll) {
-        allowBreak = true;
       }
     }
     aBreakBefore[cur] = allowBreak;
     if (allowBreak) state.NotifyBreakBefore();
     lastClass = cl;
   }
 }
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -3248,32 +3248,32 @@ void nsLayoutUtils::AddExtraBackgroundIt
 
   // For the viewport frame in print preview/page layout we want to paint
   // the grey background behind the page, not the canvas color.
   if (frameType == LayoutFrameType::Viewport &&
       nsLayoutUtils::NeedsPrintPreviewBackground(presContext)) {
     nsRect bounds =
         nsRect(aBuilder.ToReferenceFrame(aFrame), aFrame->GetSize());
     nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
-        &aBuilder, aFrame, bounds, bounds, false);
+        &aBuilder, aFrame, bounds, bounds);
     presShell->AddPrintPreviewBackgroundItem(aBuilder, aList, aFrame, bounds);
   } else if (frameType != LayoutFrameType::Page) {
     // For printing, this function is first called on an nsPageFrame, which
     // creates a display list with a PageContent item. The PageContent item's
     // paint function calls this function on the nsPageFrame's child which is
     // an nsPageContentFrame. We only want to add the canvas background color
     // item once, for the nsPageContentFrame.
 
     // Add the canvas background color to the bottom of the list. This
     // happens after we've built the list so that AddCanvasBackgroundColorItem
     // can monkey with the contents if necessary.
     nsRect canvasArea = aVisibleRegion.GetBounds();
     canvasArea.IntersectRect(aCanvasArea, canvasArea);
     nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
-        &aBuilder, aFrame, canvasArea, canvasArea, false);
+        &aBuilder, aFrame, canvasArea, canvasArea);
     presShell->AddCanvasBackgroundColorItem(aBuilder, aList, aFrame, canvasArea,
                                             aBackstop);
   }
 }
 
 /**
  * Returns a retained display list builder for frame |aFrame|. If there is no
  * retained display list builder property set for the frame, and if the flag
--- a/layout/generic/ViewportFrame.cpp
+++ b/layout/generic/ViewportFrame.cpp
@@ -113,18 +113,17 @@ static void BuildDisplayListForTopLayerF
     clipState.SetClipChainForContainingBlockDescendants(
         savedOutOfFlowData->mCombinedClipChain);
     clipState.ClipContainingBlockDescendantsExtra(
         visible + aBuilder->ToReferenceFrame(aFrame), nullptr);
     asrSetter.SetCurrentActiveScrolledRoot(
         savedOutOfFlowData->mContainingBlockActiveScrolledRoot);
   }
   nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
-      aBuilder, aFrame, visible, dirty,
-      aBuilder->IsAtRootOfPseudoStackingContext());
+      aBuilder, aFrame, visible, dirty);
 
   nsDisplayList list;
   aFrame->BuildDisplayListForStackingContext(aBuilder, &list);
   aList->AppendToTop(&list);
 }
 
 void ViewportFrame::BuildDisplayListForTopLayer(nsDisplayListBuilder* aBuilder,
                                                 nsDisplayList* aList) {
--- a/layout/generic/nsCanvasFrame.cpp
+++ b/layout/generic/nsCanvasFrame.cpp
@@ -524,17 +524,17 @@ void nsCanvasFrame::BuildDisplayList(nsD
       nsDisplayList thisItemList;
       nsDisplayBackgroundImage::InitData bgData =
           nsDisplayBackgroundImage::GetInitData(aBuilder, this, i, bgRect, bg);
 
       if (bgData.shouldFixToViewport) {
         auto* displayData = aBuilder->GetCurrentFixedBackgroundDisplayData();
         nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
             aBuilder, this, aBuilder->GetVisibleRect(),
-            aBuilder->GetDirtyRect(), false);
+            aBuilder->GetDirtyRect());
 
         DisplayListClipState::AutoSaveRestore clipState(aBuilder);
         nsDisplayListBuilder::AutoCurrentActiveScrolledRootSetter asrSetter(
             aBuilder);
         if (displayData) {
           nsPoint offset =
               GetOffsetTo(PresContext()->GetPresShell()->GetRootFrame());
           aBuilder->SetVisibleRect(displayData->mVisibleRect + offset);
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -2908,17 +2908,17 @@ void nsIFrame::BuildDisplayListForStacki
           nsLayoutUtils::GetNearestScrollableFrame(
               GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC |
                                nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN));
   bool useFixedPosition = disp->mPosition == NS_STYLE_POSITION_FIXED &&
                           (nsLayoutUtils::IsFixedPosFrameInDisplayPort(this) ||
                            BuilderHasScrolledClip(aBuilder));
 
   nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
-      aBuilder, this, visibleRect, dirtyRect, true);
+      aBuilder, this, visibleRect, dirtyRect, isTransformed);
 
   // Depending on the effects that are applied to this frame, we can create
   // multiple container display items and wrap them around our contents.
   // This enum lists all the potential container display items, in the order
   // outside to inside.
   enum class ContainerItemType : uint8_t {
     eNone = 0,
     eOwnLayerIfNeeded,
@@ -3447,16 +3447,17 @@ void nsIFrame::BuildDisplayListForSimple
   const nsPoint offset = aChild->GetOffsetTo(this);
   const nsRect visible = aBuilder->GetVisibleRect() - offset;
   const nsRect dirty = aBuilder->GetDirtyRect() - offset;
 
   if (!DescendIntoChild(aBuilder, aChild, visible, dirty)) {
     return;
   }
 
+  // Child cannot be transformed since it is not a stacking context.
   nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
       aBuilder, aChild, visible, dirty, false);
 
   CheckForApzAwareEventHandlers(aBuilder, aChild);
 
   aBuilder->BuildCompositorHitTestInfoIfNeeded(
       aChild, aLists.BorderBackground(),
       buildingForChild.IsAnimatedGeometryRoot());
@@ -3650,17 +3651,17 @@ void nsIFrame::BuildDisplayListForChild(
     pseudoStackingContext = true;
     awayFromCommonPath = true;
   }
 
   NS_ASSERTION(!isStackingContext || pseudoStackingContext,
                "Stacking contexts must also be pseudo-stacking-contexts");
 
   nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
-      aBuilder, child, visible, dirty, pseudoStackingContext);
+      aBuilder, child, visible, dirty);
   DisplayListClipState::AutoClipMultiple clipState(aBuilder);
   nsDisplayListBuilder::AutoCurrentActiveScrolledRootSetter asrSetter(aBuilder);
   CheckForApzAwareEventHandlers(aBuilder, child);
 
   if (savedOutOfFlowData) {
     aBuilder->SetBuildingInvisibleItems(false);
 
     clipState.SetClipChainForContainingBlockDescendants(
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -3103,17 +3103,17 @@ void ScrollFrameHelper::AppendScrollPart
     const bool isOverlayScrollbar =
         scrollDirection.isSome() && overlayScrollbars;
     const bool createLayer = aCreateLayer || isOverlayScrollbar ||
                              gfxPrefs::AlwaysLayerizeScrollbarTrackTestOnly();
 
     nsDisplayListCollection partList(aBuilder);
     {
       nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
-          aBuilder, mOuter, visible, dirty, true);
+          aBuilder, mOuter, visible, dirty);
 
       nsDisplayListBuilder::AutoCurrentScrollbarInfoSetter infoSetter(
           aBuilder, scrollTargetId, scrollDirection, createLayer);
       mOuter->BuildDisplayListForChild(
           aBuilder, scrollParts[i], partList,
           nsIFrame::DISPLAY_CHILD_FORCE_STACKING_CONTEXT);
     }
 
@@ -3138,17 +3138,17 @@ void ScrollFrameHelper::AppendScrollPart
         aBuilder->SetDisablePartialUpdates(true);
       }
     }
 
     {
       nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
           aBuilder, scrollParts[i],
           visible + mOuter->GetOffsetTo(scrollParts[i]),
-          dirty + mOuter->GetOffsetTo(scrollParts[i]), true);
+          dirty + mOuter->GetOffsetTo(scrollParts[i]));
       nsDisplayListBuilder::AutoCurrentScrollbarInfoSetter infoSetter(
           aBuilder, scrollTargetId, scrollDirection, createLayer);
 
       ::AppendToTop(aBuilder, aLists, partList.PositionedDescendants(),
                     scrollParts[i], appendToTopFlags);
     }
   }
 }
@@ -3355,18 +3355,17 @@ void ScrollFrameHelper::BuildDisplayList
         asrSetter.EnterScrollFrame(sf);
         if (isRcdRsf) {
           aBuilder->SetActiveScrolledRootForRootScrollframe(
               aBuilder->CurrentActiveScrolledRoot());
         }
       }
 
       nsDisplayListBuilder::AutoBuildingDisplayList building(
-          aBuilder, mOuter, visibleRect, dirtyRect,
-          aBuilder->IsAtRootOfPseudoStackingContext());
+          aBuilder, mOuter, visibleRect, dirtyRect);
 
       // Don't clip the scrolled child, and don't paint scrollbars/scrollcorner.
       // The scrolled frame shouldn't have its own background/border, so we
       // can just pass aLists directly.
       mOuter->BuildDisplayListForChild(aBuilder, mScrolledFrame, aLists);
     }
 
     if (addScrollBars) {
@@ -3591,18 +3590,17 @@ void ScrollFrameHelper::BuildDisplayList
         // If there is no display port, we don't need this because the clip
         // from the scroll port is still applied.
         scrolledRectClip = scrolledRectClip.Intersect(visibleRect);
       }
       scrolledRectClipState.ClipContainingBlockDescendants(
           scrolledRectClip + aBuilder->ToReferenceFrame(mOuter));
 
       nsDisplayListBuilder::AutoBuildingDisplayList building(
-          aBuilder, mOuter, visibleRect, dirtyRect,
-          aBuilder->IsAtRootOfPseudoStackingContext());
+          aBuilder, mOuter, visibleRect, dirtyRect);
 
       mOuter->BuildDisplayListForChild(aBuilder, mScrolledFrame, set);
 
       if (dirtyRectHasBeenOverriden && gfxPrefs::LayoutDisplayListShowArea()) {
         nsDisplaySolidColor* color = MakeDisplayItem<nsDisplaySolidColor>(
             aBuilder, mOuter,
             dirtyRect + aBuilder->GetCurrentFrameOffsetToReferenceFrame(),
             NS_RGBA(0, 0, 255, 64), false);
--- a/layout/generic/nsPageFrame.cpp
+++ b/layout/generic/nsPageFrame.cpp
@@ -505,42 +505,40 @@ void nsPageFrame::BuildDisplayList(nsDis
 
     // Overwrite current clip, since we're going to wrap in a transform
     // and the current clip is no longer meaningful.
     clipState.Clear();
     clipState.ClipContainingBlockDescendants(clipRect, nullptr);
 
     nsRect visibleRect = child->GetVisualOverflowRectRelativeToSelf();
     nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
-        aBuilder, child, visibleRect, visibleRect,
-        aBuilder->IsAtRootOfPseudoStackingContext());
+        aBuilder, child, visibleRect, visibleRect);
     child->BuildDisplayListForStackingContext(aBuilder, &content);
 
     // We may need to paint out-of-flow frames whose placeholders are
     // on other pages. Add those pages to our display list. Note that
     // out-of-flow frames can't be placed after their placeholders so
     // we don't have to process earlier pages. The display lists for
     // these extra pages are pruned so that only display items for the
     // page we currently care about (which we would have reached by
     // following placeholders to their out-of-flows) end up on the list.
     nsIFrame* page = child;
     while ((page = GetNextPage(page)) != nullptr) {
       nsRect childVisible = visibleRect + child->GetOffsetTo(page);
 
       nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
-          aBuilder, page, childVisible, childVisible,
-          aBuilder->IsAtRootOfPseudoStackingContext());
+          aBuilder, page, childVisible, childVisible);
       BuildDisplayListForExtraPage(aBuilder, this, page, &content);
     }
 
     // Invoke AutoBuildingDisplayList to ensure that the correct visibleRect
     // is used to compute the visible rect if AddCanvasBackgroundColorItem
     // creates a display item.
     nsDisplayListBuilder::AutoBuildingDisplayList building(
-        aBuilder, child, visibleRect, visibleRect, true);
+        aBuilder, child, visibleRect, visibleRect);
 
     // Add the canvas background color to the bottom of the list. This
     // happens after we've built the list so that AddCanvasBackgroundColorItem
     // can monkey with the contents if necessary.
     nsRect backgroundRect =
         nsRect(aBuilder->ToReferenceFrame(child), child->GetSize());
 
     PresContext()->GetPresShell()->AddCanvasBackgroundColorItem(
--- a/layout/generic/nsSimplePageSequenceFrame.cpp
+++ b/layout/generic/nsSimplePageSequenceFrame.cpp
@@ -711,18 +711,17 @@ void nsSimplePageSequenceFrame::BuildDis
     nsIFrame* child = PrincipalChildList().FirstChild();
     nsRect visible = aBuilder->GetVisibleRect();
     visible.ScaleInverseRoundOut(PresContext()->GetPrintPreviewScale());
 
     while (child) {
       if (child->GetVisualOverflowRectRelativeToParent().Intersects(visible)) {
         nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
             aBuilder, child, visible - child->GetPosition(),
-            visible - child->GetPosition(),
-            aBuilder->IsAtRootOfPseudoStackingContext());
+            visible - child->GetPosition());
         child->BuildDisplayListForStackingContext(aBuilder, &content);
         aBuilder->ResetMarkedFramesForDisplayList(this);
       }
       child = child->GetNextSibling();
     }
   }
 
   content.AppendToTop(MakeDisplayItem<nsDisplayTransform>(
--- a/layout/generic/nsSubDocumentFrame.cpp
+++ b/layout/generic/nsSubDocumentFrame.cpp
@@ -463,18 +463,18 @@ void nsSubDocumentFrame::BuildDisplayLis
       // nsDisplayZoom changes the meaning of appunits.
       nestedClipState.Clear();
     }
 
     // Invoke AutoBuildingDisplayList to ensure that the correct dirty rect
     // is used to compute the visible rect if AddCanvasBackgroundColorItem
     // creates a display item.
     nsIFrame* frame = subdocRootFrame ? subdocRootFrame : this;
-    nsDisplayListBuilder::AutoBuildingDisplayList building(
-        aBuilder, frame, visible, dirty, true);
+    nsDisplayListBuilder::AutoBuildingDisplayList building(aBuilder, frame,
+                                                           visible, dirty);
 
     if (subdocRootFrame) {
       nsIFrame* rootScrollFrame = presShell->GetRootScrollFrame();
       nsDisplayListBuilder::AutoCurrentScrollParentIdSetter idSetter(
           aBuilder,
           ignoreViewportScrolling && rootScrollFrame &&
                   rootScrollFrame->GetContent()
               ? nsLayoutUtils::FindOrCreateIDFor(rootScrollFrame->GetContent())
@@ -578,18 +578,18 @@ void nsSubDocumentFrame::BuildDisplayLis
       gfxPrefs::LayoutUseContainersForRootFrames() &&
       !nsLayoutUtils::NeedsPrintPreviewBackground(presContext)) {
     nsRect bounds =
         GetContentRectRelativeToSelf() + aBuilder->ToReferenceFrame(this);
 
     // Invoke AutoBuildingDisplayList to ensure that the correct dirty rect
     // is used to compute the visible rect if AddCanvasBackgroundColorItem
     // creates a display item.
-    nsDisplayListBuilder::AutoBuildingDisplayList building(
-        aBuilder, this, visible, dirty, true);
+    nsDisplayListBuilder::AutoBuildingDisplayList building(aBuilder, this,
+                                                           visible, dirty);
     // Add the canvas background color to the bottom of the list. This
     // happens after we've built the list so that AddCanvasBackgroundColorItem
     // can monkey with the contents if necessary.
     uint32_t flags =
         nsIPresShell::FORCE_DRAW | nsIPresShell::APPEND_UNSCROLLED_ONLY;
     presShell->AddCanvasBackgroundColorItem(*aBuilder, childItems, this, bounds,
                                             NS_RGBA(0, 0, 0, 0), flags);
   }
--- a/layout/generic/nsVideoFrame.cpp
+++ b/layout/generic/nsVideoFrame.cpp
@@ -542,18 +542,17 @@ void nsVideoFrame::BuildDisplayList(nsDi
   // but only want to draw mPosterImage conditionally. Others we
   // always add to the display list.
   for (nsIFrame* child : mFrames) {
     if (child->GetContent() != mPosterImage || shouldDisplayPoster ||
         child->IsBoxFrame()) {
       nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
           aBuilder, child,
           aBuilder->GetVisibleRect() - child->GetOffsetTo(this),
-          aBuilder->GetDirtyRect() - child->GetOffsetTo(this),
-          aBuilder->IsAtRootOfPseudoStackingContext());
+          aBuilder->GetDirtyRect() - child->GetOffsetTo(this));
 
       child->BuildDisplayListForStackingContext(aBuilder, aLists.Content());
     }
   }
 }
 
 #ifdef ACCESSIBILITY
 a11y::AccType nsVideoFrame::AccessibleType() { return a11y::eHTMLMediaType; }
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -1013,17 +1013,16 @@ nsDisplayListBuilder::nsDisplayListBuild
       mFilterASR(nullptr),
       mContainsBlendMode(false),
       mIsBuildingScrollbar(false),
       mCurrentScrollbarWillHaveLayer(false),
       mBuildCaret(aBuildCaret),
       mRetainingDisplayList(aRetainingDisplayList),
       mPartialUpdate(false),
       mIgnoreSuppression(false),
-      mIsAtRootOfPseudoStackingContext(false),
       mIncludeAllOutOfFlows(false),
       mDescendIntoSubdocuments(true),
       mSelectedFramesOnly(false),
       mAllowMergingAndFlattening(true),
       mWillComputePluginGeometry(false),
       mInTransform(false),
       mInFilter(false),
       mInPageSequence(false),
@@ -1770,121 +1769,101 @@ static bool IsStickyFrameActive(nsDispla
   }
 
   nsIScrollableFrame* sf = do_QueryFrame(parent);
   return sf->IsScrollingActive(aBuilder) && sf->GetScrolledFrame() == cursor;
 }
 
 nsDisplayListBuilder::AGRState nsDisplayListBuilder::IsAnimatedGeometryRoot(
     nsIFrame* aFrame, bool& aIsAsync, nsIFrame** aParent) {
+  // We can return once we know that this frame is an AGR, and we're either
+  // async, or sure that none of the later conditions might make us async.
+  // The exception to this is when IsPaintingToWindow() == false.
   aIsAsync = false;
   if (aFrame == mReferenceFrame) {
     aIsAsync = true;
     return AGR_YES;
   }
+
   if (!IsPaintingToWindow()) {
     if (aParent) {
       *aParent = nsLayoutUtils::GetCrossDocParentFrame(aFrame);
     }
     return AGR_NO;
   }
 
   nsIFrame* parent = nsLayoutUtils::GetCrossDocParentFrame(aFrame);
   if (!parent) {
     aIsAsync = true;
     return AGR_YES;
   }
 
-  AGRState result = AGR_NO;  // Possible to transition from not being an AGR
-                             // to being an AGR without a style change.
-
-  LayoutFrameType parentType = parent->Type();
-
   if (aFrame->IsTransformed()) {
     aIsAsync = EffectCompositor::HasAnimationsForCompositor(
         aFrame, eCSSProperty_transform);
-    result = AGR_YES;
-  }
-
+    return AGR_YES;
+  }
+
+  LayoutFrameType parentType = parent->Type();
   if (parentType == LayoutFrameType::Scroll ||
       parentType == LayoutFrameType::ListControl) {
     nsIScrollableFrame* sf = do_QueryFrame(parent);
-    if (sf->GetScrolledFrame() == aFrame) {
-      if (sf->IsScrollingActive(this)) {
-        aIsAsync = aIsAsync || sf->IsMaybeAsynchronouslyScrolled();
-        result = AGR_YES;
-      } else {
-        result = AGR_MAYBE;
-      }
-    }
-  }
-
-  // Finished checking all conditions that might set aIsAsync, so we can
-  // early return now.
-  if (result == AGR_YES) {
-    return result;
-  }
-
-  if (nsLayoutUtils::IsPopup(aFrame)) return AGR_YES;
-  if (ActiveLayerTracker::IsOffsetStyleAnimated(aFrame)) {
-    const bool inBudget = AddToAGRBudget(aFrame);
-    if (inBudget) {
+    if (sf->GetScrolledFrame() == aFrame && sf->IsScrollingActive(this)) {
+      MOZ_ASSERT(!aFrame->IsTransformed());
+      aIsAsync = sf->IsMaybeAsynchronouslyScrolled();
       return AGR_YES;
     }
   }
-  if (!aFrame->GetParent() &&
-      nsLayoutUtils::ViewportHasDisplayPort(aFrame->PresContext())) {
-    // Viewport frames in a display port need to be animated geometry roots
-    // for background-attachment:fixed elements.
-    return AGR_YES;
-  }
 
   // Treat the slider thumb as being as an active scrolled root when it wants
   // its own layer so that it can move without repainting.
   if (parentType == LayoutFrameType::Slider) {
-    nsIScrollableFrame* sf =
-        static_cast<nsSliderFrame*>(parent)->GetScrollFrame();
+    auto* sf = static_cast<nsSliderFrame*>(parent)->GetScrollFrame();
     // The word "Maybe" in IsMaybeScrollingActive might be confusing but we do
     // indeed need to always consider scroll thumbs as AGRs if
     // IsMaybeScrollingActive is true because that is the same condition we use
     // in ScrollFrameHelper::AppendScrollPartsTo to layerize scroll thumbs.
     if (sf && sf->IsMaybeScrollingActive()) {
       return AGR_YES;
     }
-    result = AGR_MAYBE;
-  }
-
-  if (aFrame->StyleDisplay()->mPosition == NS_STYLE_POSITION_STICKY) {
-    if (IsStickyFrameActive(this, aFrame, parent)) {
+  }
+
+  if (nsLayoutUtils::IsPopup(aFrame)) {
+    return AGR_YES;
+  }
+
+  if (ActiveLayerTracker::IsOffsetStyleAnimated(aFrame)) {
+    const bool inBudget = AddToAGRBudget(aFrame);
+    if (inBudget) {
       return AGR_YES;
     }
-    result = AGR_MAYBE;
+  }
+
+  if (!aFrame->GetParent() &&
+      nsLayoutUtils::ViewportHasDisplayPort(aFrame->PresContext())) {
+    // Viewport frames in a display port need to be animated geometry roots
+    // for background-attachment:fixed elements.
+    return AGR_YES;
+  }
+
+  if (aFrame->StyleDisplay()->mPosition == NS_STYLE_POSITION_STICKY &&
+      IsStickyFrameActive(this, aFrame, parent)) {
+    return AGR_YES;
   }
 
   // Fixed-pos frames are parented by the viewport frame, which has no parent.
   if (nsLayoutUtils::IsFixedPosFrameInDisplayPort(aFrame)) {
     return AGR_YES;
   }
 
-  if ((aFrame->GetStateBits() & NS_FRAME_MAY_BE_TRANSFORMED) &&
-      aFrame->IsFrameOfType(nsIFrame::eSVG)) {
-    // For SVG containers, they always have
-    // NS_FRAME_MAY_BE_TRANSFORMED bit.  However, they would be
-    // affected by the fragement identifiers in the svgView form at
-    // runtime without a new ComputedStyle.
-    // For example, layout/reftests/svg/fragmentIdentifier-01.xhtml
-    //
-    // see https://www.w3.org/TR/SVG/linking.html#SVGFragmentIdentifiers
-    result = AGR_MAYBE;
-  }
-
   if (aParent) {
     *aParent = parent;
   }
-  return result;
+
+  return AGR_NO;
 }
 
 nsIFrame* nsDisplayListBuilder::FindAnimatedGeometryRootFrameFor(
     nsIFrame* aFrame, bool& aIsAsync) {
   MOZ_ASSERT(
       nsLayoutUtils::IsAncestorFrameCrossDoc(RootReferenceFrame(), aFrame));
   nsIFrame* cursor = aFrame;
   while (cursor != RootReferenceFrame()) {
@@ -3691,17 +3670,17 @@ static bool SpecialCutoutRegionCase(nsDi
     nsDisplayBackgroundImage::InitData bgData =
         nsDisplayBackgroundImage::GetInitData(aBuilder, aFrame, i, bgOriginRect,
                                               bgSC);
 
     if (bgData.shouldFixToViewport) {
       auto* displayData = aBuilder->GetCurrentFixedBackgroundDisplayData();
       nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
           aBuilder, aFrame, aBuilder->GetVisibleRect(),
-          aBuilder->GetDirtyRect(), false);
+          aBuilder->GetDirtyRect());
 
       nsDisplayListBuilder::AutoCurrentActiveScrolledRootSetter asrSetter(
           aBuilder);
       if (displayData) {
         asrSetter.SetCurrentActiveScrolledRoot(
             displayData->mContainingBlockActiveScrolledRoot);
         if (nsLayoutUtils::UsesAsyncScrolling(aFrame)) {
           // Override the dirty rect on the builder to be the dirty rect of
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -412,20 +412,18 @@ class nsDisplayListBuilder {
     int mAccumulatedRectLevels;
     nsRect mVisibleRect;
   };
 
   /**
    * A frame can be in one of three states of AGR.
    * AGR_NO     means the frame is not an AGR for now.
    * AGR_YES    means the frame is an AGR for now.
-   * AGR_MAYBE  means the frame is not an AGR for now, but a transition
-   *            to AGR_YES without restyling is possible.
-   */
-  enum AGRState { AGR_NO, AGR_YES, AGR_MAYBE };
+   */
+  enum AGRState { AGR_NO, AGR_YES };
 
  public:
   typedef mozilla::FrameLayerBuilder FrameLayerBuilder;
   typedef mozilla::DisplayItemClip DisplayItemClip;
   typedef mozilla::DisplayItemClipChain DisplayItemClipChain;
   typedef mozilla::DisplayItemClipChainHasher DisplayItemClipChainHasher;
   typedef mozilla::DisplayItemClipChainEqualer DisplayItemClipChainEqualer;
   typedef mozilla::DisplayListClipState DisplayListClipState;
@@ -531,26 +529,16 @@ class nsDisplayListBuilder {
    * @return true if "painting is suppressed" during page load and we
    * should paint only the background of the document.
    */
   bool IsBackgroundOnly() {
     NS_ASSERTION(mPresShellStates.Length() > 0,
                  "don't call this if we're not in a presshell");
     return CurrentPresShellState()->mIsBackgroundOnly;
   }
-  /**
-   * @return true if the currently active BuildDisplayList call is being
-   * applied to a frame at the root of a pseudo stacking context. A pseudo
-   * stacking context is either a real stacking context or basically what
-   * CSS2.1 appendix E refers to with "treat the element as if it created
-   * a new stacking context
-   */
-  bool IsAtRootOfPseudoStackingContext() const {
-    return mIsAtRootOfPseudoStackingContext;
-  }
 
   /**
    * @return the root of given frame's (sub)tree, whose origin
    * establishes the coordinate system for the child display items.
    */
   const nsIFrame* FindReferenceFrameFor(const nsIFrame* aFrame,
                                         nsPoint* aOffset = nullptr) const;
 
@@ -1081,99 +1069,98 @@ class nsDisplayListBuilder {
   /**
    * Merges the display items in |aMergedItems| and returns a new temporary
    * display item.
    * The display items in |aMergedItems| have to be mergeable with each other.
    */
   nsDisplayItem* MergeItems(nsTArray<nsDisplayItem*>& aMergedItems);
 
   /**
-   * A helper class to temporarily set the value of
-   * mIsAtRootOfPseudoStackingContext, and temporarily
-   * set mCurrentFrame and related state. Also temporarily sets mDirtyRect.
-   * aDirtyRect is relative to aForChild.
+   * A helper class used to temporarily set nsDisplayListBuilder properties for
+   * building display items.
+   * aVisibleRect and aDirtyRect are relative to aForChild.
    */
   class AutoBuildingDisplayList {
    public:
     AutoBuildingDisplayList(nsDisplayListBuilder* aBuilder, nsIFrame* aForChild,
                             const nsRect& aVisibleRect,
-                            const nsRect& aDirtyRect, bool aIsRoot)
+                            const nsRect& aDirtyRect)
+        : AutoBuildingDisplayList(aBuilder, aForChild, aVisibleRect, aDirtyRect,
+                                  aForChild->IsTransformed()) {}
+
+    AutoBuildingDisplayList(nsDisplayListBuilder* aBuilder, nsIFrame* aForChild,
+                            const nsRect& aVisibleRect,
+                            const nsRect& aDirtyRect, const bool aIsTransformed)
         : mBuilder(aBuilder),
           mPrevFrame(aBuilder->mCurrentFrame),
           mPrevReferenceFrame(aBuilder->mCurrentReferenceFrame),
           mPrevHitTestArea(aBuilder->mHitTestArea),
           mPrevHitTestInfo(aBuilder->mHitTestInfo),
           mPrevOffset(aBuilder->mCurrentOffsetToReferenceFrame),
           mPrevVisibleRect(aBuilder->mVisibleRect),
           mPrevDirtyRect(aBuilder->mDirtyRect),
           mPrevAGR(aBuilder->mCurrentAGR),
-          mPrevIsAtRootOfPseudoStackingContext(
-              aBuilder->mIsAtRootOfPseudoStackingContext),
           mPrevAncestorHasApzAwareEventHandler(
               aBuilder->mAncestorHasApzAwareEventHandler),
           mPrevBuildingInvisibleItems(aBuilder->mBuildingInvisibleItems),
           mPrevInInvalidSubtree(aBuilder->mInInvalidSubtree) {
-      if (aForChild->IsTransformed()) {
+      if (aIsTransformed) {
         aBuilder->mCurrentOffsetToReferenceFrame = nsPoint();
         aBuilder->mCurrentReferenceFrame = aForChild;
       } else if (aBuilder->mCurrentFrame == aForChild->GetParent()) {
         aBuilder->mCurrentOffsetToReferenceFrame += aForChild->GetPosition();
       } else {
         aBuilder->mCurrentReferenceFrame = aBuilder->FindReferenceFrameFor(
             aForChild, &aBuilder->mCurrentOffsetToReferenceFrame);
       }
+
       bool isAsync;
       mCurrentAGRState = aBuilder->IsAnimatedGeometryRoot(aForChild, isAsync);
+
       if (aBuilder->mCurrentFrame == aForChild->GetParent()) {
         if (mCurrentAGRState == AGR_YES) {
           aBuilder->mCurrentAGR = aBuilder->WrapAGRForFrame(
               aForChild, isAsync, aBuilder->mCurrentAGR);
         }
-      } else if (aForChild != aBuilder->mCurrentFrame) {
+      } else if (aBuilder->mCurrentFrame != aForChild) {
         aBuilder->mCurrentAGR =
             aBuilder->FindAnimatedGeometryRootFor(aForChild);
       }
+
       MOZ_ASSERT(nsLayoutUtils::IsAncestorFrameCrossDoc(
           aBuilder->RootReferenceFrame(), *aBuilder->mCurrentAGR));
       aBuilder->mInInvalidSubtree =
           aBuilder->mInInvalidSubtree || aForChild->IsFrameModified();
       aBuilder->mCurrentFrame = aForChild;
       aBuilder->mVisibleRect = aVisibleRect;
       aBuilder->mDirtyRect =
           aBuilder->mInInvalidSubtree ? aVisibleRect : aDirtyRect;
-      aBuilder->mIsAtRootOfPseudoStackingContext = aIsRoot;
     }
 
     void SetReferenceFrameAndCurrentOffset(const nsIFrame* aFrame,
                                            const nsPoint& aOffset) {
       mBuilder->mCurrentReferenceFrame = aFrame;
       mBuilder->mCurrentOffsetToReferenceFrame = aOffset;
     }
 
     bool IsAnimatedGeometryRoot() const { return mCurrentAGRState == AGR_YES; }
 
-    bool MaybeAnimatedGeometryRoot() const {
-      return mCurrentAGRState == AGR_MAYBE;
-    }
-
     void RestoreBuildingInvisibleItemsValue() {
       mBuilder->mBuildingInvisibleItems = mPrevBuildingInvisibleItems;
     }
 
     ~AutoBuildingDisplayList() {
       mBuilder->mCurrentFrame = mPrevFrame;
       mBuilder->mCurrentReferenceFrame = mPrevReferenceFrame;
       mBuilder->mHitTestArea = mPrevHitTestArea;
       mBuilder->mHitTestInfo = mPrevHitTestInfo;
       mBuilder->mCurrentOffsetToReferenceFrame = mPrevOffset;
       mBuilder->mVisibleRect = mPrevVisibleRect;
       mBuilder->mDirtyRect = mPrevDirtyRect;
       mBuilder->mCurrentAGR = mPrevAGR;
-      mBuilder->mIsAtRootOfPseudoStackingContext =
-          mPrevIsAtRootOfPseudoStackingContext;
       mBuilder->mAncestorHasApzAwareEventHandler =
           mPrevAncestorHasApzAwareEventHandler;
       mBuilder->mBuildingInvisibleItems = mPrevBuildingInvisibleItems;
       mBuilder->mInInvalidSubtree = mPrevInInvalidSubtree;
     }
 
    private:
     nsDisplayListBuilder* mBuilder;
@@ -1181,17 +1168,16 @@ class nsDisplayListBuilder {
     const nsIFrame* mPrevFrame;
     const nsIFrame* mPrevReferenceFrame;
     nsRect mPrevHitTestArea;
     CompositorHitTestInfo mPrevHitTestInfo;
     nsPoint mPrevOffset;
     nsRect mPrevVisibleRect;
     nsRect mPrevDirtyRect;
     RefPtr<AnimatedGeometryRoot> mPrevAGR;
-    bool mPrevIsAtRootOfPseudoStackingContext;
     bool mPrevAncestorHasApzAwareEventHandler;
     bool mPrevBuildingInvisibleItems;
     bool mPrevInInvalidSubtree;
   };
 
   /**
    * A helper class to temporarily set the value of mInTransform.
    */
@@ -1954,17 +1940,16 @@ class nsDisplayListBuilder {
   const ActiveScrolledRoot* mFilterASR;
   bool mContainsBlendMode;
   bool mIsBuildingScrollbar;
   bool mCurrentScrollbarWillHaveLayer;
   bool mBuildCaret;
   bool mRetainingDisplayList;
   bool mPartialUpdate;
   bool mIgnoreSuppression;
-  bool mIsAtRootOfPseudoStackingContext;
   bool mIncludeAllOutOfFlows;
   bool mDescendIntoSubdocuments;
   bool mSelectedFramesOnly;
   bool mAllowMergingAndFlattening;
   bool mWillComputePluginGeometry;
   // True when we're building a display list that's directly or indirectly
   // under an nsDisplayTransform
   bool mInTransform;
--- a/layout/xul/nsSliderFrame.cpp
+++ b/layout/xul/nsSliderFrame.cpp
@@ -320,17 +320,17 @@ void nsSliderFrame::BuildDisplayListForC
         refSize.width /= scale.width;
         refSize.height /= scale.height;
       }
       nsRect dirty = aBuilder->GetVisibleRect().Intersect(thumbRect);
       dirty = nsLayoutUtils::ComputePartialPrerenderArea(
           aBuilder->GetVisibleRect(), overflow, refSize);
 
       nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
-          aBuilder, this, dirty, dirty, false);
+          aBuilder, this, dirty, dirty);
 
       // Clip the thumb layer to the slider track. This is necessary to ensure
       // FrameLayerBuilder is able to merge content before and after the
       // scrollframe into the same layer (otherwise it thinks the thumb could
       // potentially move anywhere within the existing clip).
       DisplayListClipState::AutoSaveRestore thumbClipState(aBuilder);
       thumbClipState.ClipContainingBlockDescendants(
           GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(this));
--- a/mobile/android/app/src/main/res/layout/activity_stream_webpage_item_row.xml
+++ b/mobile/android/app/src/main/res/layout/activity_stream_webpage_item_row.xml
@@ -82,25 +82,24 @@
         android:textAlignment="viewStart"
         android:gravity="start"
         android:textColor="#ff000000"
         android:textSize="14sp"
         android:textStyle="bold"
         tools:text="Descriptive title of a page that is veeeeeeery long - maybe even too long?" />
 
     <LinearLayout
+        android:id="@+id/page_source_layout"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_alignParentBottom="true"
         android:layout_below="@id/page_title"
         android:layout_toEndOf="@id/icon_wrapper"
         android:layout_toRightOf="@id/icon_wrapper"
         android:gravity="center_vertical"
         android:orientation="horizontal"
-        android:paddingBottom="@dimen/activity_stream_base_margin"
         android:paddingEnd="@dimen/activity_stream_base_margin"
         android:paddingLeft="@dimen/activity_stream_base_margin"
         android:paddingRight="@dimen/activity_stream_base_margin"
         android:paddingStart="@dimen/activity_stream_base_margin"
         android:paddingTop="4dp"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -121,9 +120,39 @@
             android:layout_weight="1"
             android:textAlignment="viewStart"
             android:gravity="start"
             android:textColor="@color/activity_stream_subtitle"
             android:textSize="12sp"
             tools:text="Bookmarked" />
 
     </LinearLayout>
+
+    <TextView
+        android:id="@+id/switch_tab_hint"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/page_source_layout"
+        android:layout_toEndOf="@id/icon_wrapper"
+        android:layout_toRightOf="@id/icon_wrapper"
+        android:drawableStart="@drawable/ic_url_bar_tab"
+        android:drawableLeft="@drawable/ic_url_bar_tab"
+        android:paddingStart="6dp"
+        android:paddingLeft="6dp"
+        android:paddingEnd="0dp"
+        android:paddingRight="0dp"
+        android:text="@string/switch_to_tab"
+        android:textColor="@color/activity_stream_subtitle"
+        android:textSize="12sp"
+        tools:drawableLeft="@drawable/ic_url_bar_tab"
+        tools:drawableStart="@drawable/ic_url_bar_tab"
+        tools:text="@string/switch_to_tab" />
+
+    <!-- Empty bottom space added irrespective of "switch_tab_hint" or "page_source_layout"
+         being the bottom view. ("switch_tab_hint" can have visibility="gone") -->
+    <View
+        android:id="@+id/layout_bottom_margin"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/activity_stream_base_margin"
+        android:layout_alignParentBottom="true"
+        android:layout_below="@id/switch_tab_hint" />
+
 </RelativeLayout>
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -54,16 +54,17 @@ import org.mozilla.gecko.preferences.Gec
 import org.mozilla.gecko.pwa.PwaUtils;
 import org.mozilla.gecko.telemetry.TelemetryBackgroundReceiver;
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.BitmapUtils;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.PRNGFixes;
 import org.mozilla.gecko.util.ShortcutUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -687,24 +688,32 @@ public class GeckoApplication extends Ap
 
         if (manifestUrl != null) {
             // If a page has associated manifest, lets install it (PWA A2HS)
             // At this time, this page must be a secure page.
             // Please hide PWA badge UI in front end side.
             final boolean safeForPwa = PwaUtils.shouldAddPwaShortcut(selectedTab);
             if (!safeForPwa) {
                 final String message = "This page is not safe for PWA";
+
                 // For release and beta, we record an error message
                 if (AppConstants.RELEASE_OR_BETA) {
                     Log.e(LOG_TAG, message);
                 } else {
-                    // For nightly and local build, we'll throw an exception here.
-                    throw new IllegalStateException(message);
+                    final Activity currentActivity =
+                            GeckoActivityMonitor.getInstance().getCurrentActivity();
+                    final SafeIntent safeIntent = new SafeIntent(currentActivity.getIntent());
+                    final boolean isInAutomation = IntentUtils.getIsInAutomationFromEnvironment(safeIntent);
+
+                    if (isInAutomation) {
+                        // For nightly automated tests, we'll throw an exception here
+                        // in order to fast fail.
+                        throw new IllegalStateException(message);
+                    }
                 }
-
             }
 
             final GeckoBundle message = new GeckoBundle();
             message.putInt("iconSize", GeckoAppShell.getPreferredIconSize());
             message.putString("manifestUrl", manifestUrl);
             message.putString("originalUrl", url);
             message.putString("originalTitle", title);
             EventDispatcher.getInstance().dispatch("Browser:LoadManifest", message);
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/StreamRecyclerAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/StreamRecyclerAdapter.java
@@ -143,22 +143,30 @@ public class StreamRecyclerAdapter exten
                     openContextMenuForTopSite(topSite, absolutePosition, tabletContextMenuAnchor, parent, faviconWidth, faviconHeight);
                     return true;
                 }
             });
         } else if (type == RowItemType.TOP_STORIES_TITLE.getViewType()) {
             return new StreamTitleRow(inflater.inflate(StreamTitleRow.LAYOUT_ID, parent, false), R.string.activity_stream_topstories, R.string.activity_stream_link_more, LINK_MORE_POCKET, onUrlOpenListener);
         } else if (type == RowItemType.TOP_STORIES_ITEM.getViewType() ||
                 type == RowItemType.HIGHLIGHT_ITEM.getViewType()) {
-            return new WebpageItemRow(inflater.inflate(WebpageItemRow.LAYOUT_ID, parent, false), new WebpageItemRow.OnMenuButtonClickListener() {
-                @Override
-                public void onMenuButtonClicked(final WebpageItemRow row, final int position) {
-                    openContextMenuForWebpageItemRow(row, position, parent, ActivityStreamTelemetry.Contract.INTERACTION_MENU_BUTTON);
-                }
-            });
+            return new WebpageItemRow(inflater.inflate(WebpageItemRow.LAYOUT_ID, parent, false),
+                    new WebpageItemRow.OnMenuButtonClickListener() {
+                        @Override
+                        public void onMenuButtonClicked(final WebpageItemRow row, final int position) {
+                            openContextMenuForWebpageItemRow(
+                                    row, position, parent, ActivityStreamTelemetry.Contract.INTERACTION_MENU_BUTTON);
+                        }
+                    }, new WebpageItemRow.OnContentChangedListener() {
+                        @Override
+                        public void onContentChanged(int itemPosition) {
+                            notifyItemChanged(itemPosition);
+                        }
+                    }
+            );
         } else if (type == RowItemType.HIGHLIGHTS_TITLE.getViewType()) {
             return new StreamTitleRow(inflater.inflate(StreamTitleRow.LAYOUT_ID, parent, false), R.string.activity_stream_highlights);
         } else if (type == RowItemType.HIGHLIGHTS_EMPTY_STATE.getViewType()) {
             return new HighlightsEmptyStateRow(inflater.inflate(HighlightsEmptyStateRow.LAYOUT_ID, parent, false));
         } else if (type == RowItemType.LEARN_MORE_LINK.getViewType()) {
             return new LearnMoreRow(inflater.inflate(LearnMoreRow.LAYOUT_ID, parent, false));
         } else {
             throw new IllegalStateException("Missing inflation for ViewType " + type);
@@ -211,16 +219,34 @@ public class StreamRecyclerAdapter exten
             setViewVisible(bookmarksEnabled || visitedEnabled, holder.itemView);
         } else if (type == RowItemType.TOP_STORIES_TITLE.getViewType()) {
             final Context context = holder.itemView.getContext();
             final boolean pocketEnabled = ActivityStreamConfiguration.isPocketRecommendingTopSites(context);
             setViewVisible(pocketEnabled, holder.itemView);
         }
     }
 
+    @Override
+    public void onViewAttachedToWindow(StreamViewHolder holder) {
+        super.onViewAttachedToWindow(holder);
+
+        if (holder instanceof WebpageItemRow) {
+            ((WebpageItemRow) holder).initResources();
+        }
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(StreamViewHolder holder) {
+        super.onViewDetachedFromWindow(holder);
+
+        if (holder instanceof WebpageItemRow) {
+            ((WebpageItemRow) holder).doCleanup();
+        }
+    }
+
     /**
      * This sets a child view of the adapter visible or hidden.
      *
      * This only applies to children whose height and width are WRAP_CONTENT and MATCH_PARENT
      * respectively.
      *
      * NB: This is a hack for the views that are included in the RecyclerView adapter even if
      * they shouldn't be shown, such as the section title views or the empty view for highlights.
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/stream/WebpageItemRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/stream/WebpageItemRow.java
@@ -2,60 +2,71 @@
  * 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/. */
 
 package org.mozilla.gecko.activitystream.homepanel.stream;
 
 import android.content.Context;
 import android.graphics.Color;
+import android.support.annotation.NonNull;
 import android.support.annotation.UiThread;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.activitystream.Utils;
 import org.mozilla.gecko.activitystream.homepanel.model.WebpageRowModel;
+import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.util.DrawableUtil;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.TouchTargetUtil;
 import org.mozilla.gecko.util.URIUtils;
 import org.mozilla.gecko.util.ViewUtil;
 
 import java.lang.ref.WeakReference;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.UUID;
 
-public class WebpageItemRow extends StreamViewHolder {
+public class WebpageItemRow extends StreamViewHolder implements Tabs.OnTabsChangedListener {
     private static final String LOGTAG = "GeckoWebpageItemRow";
 
     public static final int LAYOUT_ID = R.layout.activity_stream_webpage_item_row;
     private static final double SIZE_RATIO = 0.75;
 
     private WebpageRowModel webpageModel;
+    private OnContentChangedListener contentChangedListener;
     private int position;
 
     private final StreamOverridablePageIconLayout pageIconLayout;
     private final TextView pageDomainView;
     private final TextView pageTitleView;
     private final ImageView pageSourceIconView;
     private final TextView pageSourceView;
     private final ImageView menuButton;
+    private final TextView switchToTabHint;
 
-    public WebpageItemRow(final View itemView, final OnMenuButtonClickListener onMenuButtonClickListener) {
+    public WebpageItemRow(final View itemView,
+                          final OnMenuButtonClickListener onMenuButtonClickListener,
+                          final OnContentChangedListener contentChangedListener) {
         super(itemView);
 
+        this.contentChangedListener = contentChangedListener;
+
         pageTitleView = (TextView) itemView.findViewById(R.id.page_title);
         pageIconLayout = (StreamOverridablePageIconLayout) itemView.findViewById(R.id.page_icon);
         pageSourceView = (TextView) itemView.findViewById(R.id.page_source);
         pageDomainView = (TextView) itemView.findViewById(R.id.page_domain);
         pageSourceIconView = (ImageView) itemView.findViewById(R.id.page_source_icon);
+        switchToTabHint = (TextView) itemView.findViewById(R.id.switch_tab_hint);
 
         menuButton = (ImageView) itemView.findViewById(R.id.menu);
         menuButton.setImageDrawable(
                 DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, Color.LTGRAY));
         TouchTargetUtil.ensureTargetHitArea(menuButton, itemView);
         ViewUtil.enableTouchRipple(menuButton);
         menuButton.setOnClickListener(new View.OnClickListener() {
             @Override
@@ -78,16 +89,27 @@ public class WebpageItemRow extends Stre
         ViewGroup.LayoutParams layoutParams = pageIconLayout.getLayoutParams();
         layoutParams.width = tilesWidth;
         layoutParams.height = (int) Math.floor(tilesWidth * SIZE_RATIO);
         pageIconLayout.setLayoutParams(layoutParams);
 
         updateUiForSource(model.getSource());
         updatePageDomain();
         pageIconLayout.updateIcon(model.getUrl(), model.getImageUrl());
+
+        final boolean isTabOpenedForItem = isTabOpenedForItem();
+        switchToTabHint.setVisibility(isTabOpenedForItem ? View.VISIBLE : View.GONE);
+    }
+
+    public void initResources() {
+        Tabs.registerOnTabsChangedListener(this);
+    }
+
+    public void doCleanup() {
+        Tabs.unregisterOnTabsChangedListener(this);
     }
 
     public void updateUiForSource(Utils.HighlightSource source) {
         switch (source) {
             case BOOKMARKED:
                 pageSourceView.setText(R.string.activity_stream_highlight_label_bookmarked);
                 pageSourceView.setVisibility(View.VISIBLE);
                 pageSourceIconView.setImageResource(R.drawable.ic_as_bookmarked);
@@ -167,16 +189,76 @@ public class WebpageItemRow extends Stre
          * the View we're making this request for hasn't been re-used by the time this request completes.
          */
         @UiThread
         private boolean isTagSameAsStartTag(final View viewToCheck) {
             return viewTagAtStart.equals(viewToCheck.getTag(VIEW_TAG_ID));
         }
     }
 
+    @Override
+    public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+        final String itemUrl = webpageModel.getUrl();
+        if (itemUrl == null || tab == null) {
+            return;
+        }
+
+        // Only interested if a matching tab has been opened/closed/switched to a different page
+        final boolean validEvent = msg.equals(Tabs.TabEvents.ADDED) ||
+                msg.equals(Tabs.TabEvents.CLOSED) ||
+                msg.equals(Tabs.TabEvents.LOCATION_CHANGE);
+        if (!validEvent) {
+            return;
+        }
+
+        // Actually check for any tab change regarding any of the currently displayed stream URLs
+        //
+        // "data" is an empty String for ADDED/CLOSED, and contains the previous/old URL during
+        // LOCATION_CHANGE (the new URL is retrieved using tab.getURL()).
+        //
+        // Normalized tabUrl and data using "ReaderModeUtils.stripAboutReaderUrl"
+        // because they can be about:reader URLs if the current or old tab page was a reader view
+        final String tabUrl = ReaderModeUtils.stripAboutReaderUrl(tab.getURL());
+        final String previousTabUrl = ReaderModeUtils.stripAboutReaderUrl(data);
+        if (itemUrl.equals(tabUrl) || itemUrl.equals(previousTabUrl)) {
+            notifyListenerAboutContentChange();
+        }
+    }
+
     public View getTabletContextMenuAnchor() {
         return menuButton;
     }
 
+    /**
+     * @return true if there is an opened tab for this item's {@link #webpageModel} Url.
+     */
+    private boolean isTabOpenedForItem() {
+        Tab tab = Tabs.getInstance().getFirstTabForUrl(webpageModel.getUrl(), isCurrentTabInPrivateMode());
+
+        return tab != null;
+    }
+
+    /**
+     * @return true if this view is shown inside a private tab, independent of whether
+     * a private mode theme is applied via <code>setPrivateMode(true)</code>.
+     */
+    private boolean isCurrentTabInPrivateMode() {
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        return tab != null && tab.isPrivate();
+    }
+
+    private void notifyListenerAboutContentChange() {
+        if (contentChangedListener != null) {
+            contentChangedListener.onContentChanged(getAdapterPosition());
+        }
+    }
+
     public interface OnMenuButtonClickListener {
         void onMenuButtonClicked(WebpageItemRow row, int position);
     }
+
+    /**
+     * Informs upstream about changes regarding this row's content.
+     */
+    public interface OnContentChangedListener {
+        void onContentChanged(int itemPosition);
+    }
 }
--- a/services/common/tests/unit/head_helpers.js
+++ b/services/common/tests/unit/head_helpers.js
@@ -149,28 +149,52 @@ function installFakePAC() {
                                       PACSystemSettings);
 }
 
 function uninstallFakePAC() {
   _("Uninstalling fake PAC.");
   MockRegistrar.unregister(fakePACCID);
 }
 
+function _eventsTelemetrySnapshot(component, source) {
+  const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+  const TELEMETRY_CATEGORY_ID = "uptake.remotecontent.result";
+  const snapshot = Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT, true);
+  const parentEvents = snapshot.parent || [];
+  return parentEvents
+    // Transform raw event data to objects.
+    .map(([i, category, method, object, value, extras]) => { return { category, method, object, value, extras }; })
+    // Keep only for the specified component and source.
+    .filter((e) => e.category == TELEMETRY_CATEGORY_ID && e.object == component && e.extras.source == source)
+    // Return total number of events received by status, to mimic histograms snapshots.
+    .reduce((acc, e) => {
+      acc[e.value] = (acc[e.value] || 0) + 1;
+      return acc;
+    }, {});
+}
 
 function getUptakeTelemetrySnapshot(key) {
-  const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+  const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
   const TELEMETRY_HISTOGRAM_ID = "UPTAKE_REMOTE_CONTENT_RESULT_1";
-  return Services.telemetry
-           .getKeyedHistogramById(TELEMETRY_HISTOGRAM_ID)
-           .snapshot()[key];
+  const TELEMETRY_COMPONENT = "remotesettings";
+  const histogram = Services.telemetry.getKeyedHistogramById(TELEMETRY_HISTOGRAM_ID).snapshot()[key];
+  const events = _eventsTelemetrySnapshot(TELEMETRY_COMPONENT, key);
+  return { histogram, events };
 }
 
 function checkUptakeTelemetry(snapshot1, snapshot2, expectedIncrements) {
-  const LABELS = ["up_to_date", "success", "backoff", "pref_disabled", "parse_error", "content_error", "sign_error", "sign_retry_error", "conflict_error", "sync_error", "apply_error", "server_error", "certificate_error", "download_error", "timeout_error", "network_error", "offline_error", "cleanup_error", "unknown_error", "custom_1_error", "custom_2_error", "custom_3_error", "custom_4_error", "custom_5_error"];
-  for (const label of LABELS) {
-    const key = LABELS.indexOf(label);
-    const expected = expectedIncrements[label] || 0;
-    let value1 = (snapshot1 && snapshot1.values[key]) || 0;
-    let value2 = (snapshot2 && snapshot2.values[key]) || 0;
-    const actual = value2 - value1;
-    equal(expected, actual, `check histogram values for ${label}`);
+  const STATUSES = ["up_to_date", "success", "backoff", "pref_disabled", "parse_error", "content_error", "sign_error", "sign_retry_error", "conflict_error", "sync_error", "apply_error", "server_error", "certificate_error", "download_error", "timeout_error", "network_error", "offline_error", "cleanup_error", "unknown_error", "custom_1_error", "custom_2_error", "custom_3_error", "custom_4_error", "custom_5_error"];
+
+  for (const status of STATUSES) {
+    const key = STATUSES.indexOf(status);
+    const expected = expectedIncrements[status] || 0;
+    // Check histogram increments.
+    let value1 = (snapshot1 && snapshot1.histogram && snapshot1.histogram.values[key]) || 0;
+    let value2 = (snapshot2 && snapshot2.histogram && snapshot2.histogram.values[key]) || 0;
+    let actual = value2 - value1;
+    equal(expected, actual, `check histogram values for ${status}`);
+    // Check events increments.
+    value1 = (snapshot1 && snapshot1.histogram && snapshot1.histogram.values[key]) || 0;
+    value2 = (snapshot2 && snapshot2.histogram && snapshot2.histogram.values[key]) || 0;
+    actual = value2 - value1;
+    equal(expected, actual, `check events for ${status}`);
   }
 }
--- a/services/common/tests/unit/test_uptake_telemetry.js
+++ b/services/common/tests/unit/test_uptake_telemetry.js
@@ -1,27 +1,31 @@
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js");
 
+const COMPONENT = "remotesettings";
+
+
 add_task(async function test_unknown_status_is_not_reported() {
   const source = "update-source";
   const startHistogram = getUptakeTelemetrySnapshot(source);
 
-  UptakeTelemetry.report(source, "unknown-status");
+  UptakeTelemetry.report(COMPONENT, "unknown-status", { source });
 
   const endHistogram = getUptakeTelemetrySnapshot(source);
   const expectedIncrements = {};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
 
 add_task(async function test_each_status_can_be_caught_in_snapshot() {
   const source = "some-source";
   const startHistogram = getUptakeTelemetrySnapshot(source);
 
   const expectedIncrements = {};
   for (const label of Object.keys(UptakeTelemetry.STATUS)) {
     const status = UptakeTelemetry.STATUS[label];
-    UptakeTelemetry.report(source, status);
+    UptakeTelemetry.report(COMPONENT, status, { source });
     expectedIncrements[status] = 1;
   }
 
   const endHistogram = getUptakeTelemetrySnapshot(source);
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
--- a/services/common/uptake-telemetry.js
+++ b/services/common/uptake-telemetry.js
@@ -5,19 +5,23 @@
 "use strict";
 
 
 var EXPORTED_SYMBOLS = ["UptakeTelemetry"];
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 
-// Telemetry report results.
+// Telemetry histogram id (see Histograms.json).
 const TELEMETRY_HISTOGRAM_ID = "UPTAKE_REMOTE_CONTENT_RESULT_1";
 
+// Telemetry events id (see Events.yaml).
+const TELEMETRY_EVENTS_ID = "uptake.remotecontent.result";
+
+
 /**
  * A Telemetry helper to report uptake of remote content.
  */
 class UptakeTelemetry {
   /**
    * Supported uptake statuses:
    *
    * - `UP_TO_DATE`: Local content was already up-to-date with remote content.
@@ -74,19 +78,39 @@ class UptakeTelemetry {
       CUSTOM_4_ERROR:        "custom_4_error",
       CUSTOM_5_ERROR:        "custom_5_error",
     };
   }
 
   /**
    * Reports the uptake status for the specified source.
    *
-   * @param {string} source  the identifier of the update source.
-   * @param {string} status  the uptake status.
+   * @param {string} component     the component reporting the uptake (eg. "normandy").
+   * @param {string} status        the uptake status (eg. "network_error")
+   * @param {Object} extra         extra values to report
+   * @param {string} extra.source  the update source (eg. "recipe-42").
    */
-  static report(source, status) {
+  static report(component, status, extra = {}) {
+    const { source } = extra;
+
+    if (!source) {
+      throw new Error("`source` value is mandatory.");
+    }
+
+    // Report event for real-time monitoring. See Events.yaml for registration.
+    // Contrary to histograms, Telemetry Events are not enabled by default.
+    // Enable them on first call to `report()`.
+    if (!this._eventsEnabled) {
+      Services.telemetry.setEventRecordingEnabled(TELEMETRY_EVENTS_ID, true);
+      this._eventsEnabled = true;
+    }
+    Services.telemetry
+      .recordEvent(TELEMETRY_EVENTS_ID, "uptake", component, status, extra);
+
+    // Report via histogram in main ping.
+    // Note: this is the legacy equivalent of the above event. We keep it for continuity.
     Services.telemetry
       .getKeyedHistogramById(TELEMETRY_HISTOGRAM_ID)
       .add(source, status);
   }
 }
 
 this.UptakeTelemetry = UptakeTelemetry;
--- a/services/settings/RemoteSettingsClient.jsm
+++ b/services/settings/RemoteSettingsClient.jsm
@@ -24,16 +24,18 @@ ChromeUtils.defineModuleGetter(this, "Re
 ChromeUtils.defineModuleGetter(this, "Utils",
                                "resource://services-settings/Utils.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 
 // IndexedDB name.
 const DB_NAME = "remote-settings";
 
+const TELEMETRY_COMPONENT = "remotesettings";
+
 const INVALID_SIGNATURE = "Invalid content signature";
 const MISSING_SIGNATURE = "Missing signature";
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "gServerURL",
                                       "services.settings.server");
 XPCOMUtils.defineLazyPreferenceGetter(this, "gChangesPath",
                                       "services.settings.changes.path");
 XPCOMUtils.defineLazyPreferenceGetter(this, "gVerifySignature",
@@ -378,17 +380,17 @@ class RemoteSettingsClient extends Event
       }
       throw e;
     } finally {
       // No error was reported, this is a success!
       if (reportStatus === null) {
         reportStatus = UptakeTelemetry.STATUS.SUCCESS;
       }
       // Report success/error status to Telemetry.
-      UptakeTelemetry.report(this.identifier, reportStatus);
+      UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, { source: this.identifier });
     }
   }
 
   /**
    * Fetch the signature info from the collection metadata and verifies that the
    * local set of records has the same.
    *
    * @param {Array<Object>} remoteRecords   The list of changes to apply to the local database.
--- a/services/settings/remote-settings.js
+++ b/services/settings/remote-settings.js
@@ -35,18 +35,20 @@ const PREF_SETTINGS_DEFAULT_SIGNER     =
 const PREF_SETTINGS_SERVER_BACKOFF     = "server.backoff";
 const PREF_SETTINGS_CHANGES_PATH       = "changes.path";
 const PREF_SETTINGS_LAST_UPDATE        = "last_update_seconds";
 const PREF_SETTINGS_LAST_ETAG          = "last_etag";
 const PREF_SETTINGS_CLOCK_SKEW_SECONDS = "clock_skew_seconds";
 const PREF_SETTINGS_LOAD_DUMP          = "load_dump";
 
 
-// Telemetry update source identifier.
-const TELEMETRY_HISTOGRAM_KEY = "settings-changes-monitoring";
+// Telemetry identifiers.
+const TELEMETRY_COMPONENT = "remotesettings";
+const TELEMETRY_SOURCE = "settings-changes-monitoring";
+
 // Push broadcast id.
 const BROADCAST_ID = "remote-settings/monitor_changes";
 
 XPCOMUtils.defineLazyGetter(this, "gPrefs", () => {
   return Services.prefs.getBranch(PREF_SETTINGS_BRANCH);
 });
 XPCOMUtils.defineLazyPreferenceGetter(this, "gServerURL", PREF_SETTINGS_BRANCH + PREF_SETTINGS_SERVER);
 XPCOMUtils.defineLazyPreferenceGetter(this, "gChangesPath", PREF_SETTINGS_BRANCH + PREF_SETTINGS_CHANGES_PATH);
@@ -155,52 +157,53 @@ function remoteSettingsFunction() {
    */
   remoteSettings.pollChanges = async ({ expectedTimestamp } = {}) => {
     // Check if the server backoff time is elapsed.
     if (gPrefs.prefHasUserValue(PREF_SETTINGS_SERVER_BACKOFF)) {
       const backoffReleaseTime = gPrefs.getCharPref(PREF_SETTINGS_SERVER_BACKOFF);
       const remainingMilliseconds = parseInt(backoffReleaseTime, 10) - Date.now();
       if (remainingMilliseconds > 0) {
         // Backoff time has not elapsed yet.
-        UptakeTelemetry.report(TELEMETRY_HISTOGRAM_KEY,
-                               UptakeTelemetry.STATUS.BACKOFF);
+        UptakeTelemetry.report(TELEMETRY_COMPONENT,
+                               UptakeTelemetry.STATUS.BACKOFF,
+                               { source: TELEMETRY_SOURCE });
         throw new Error(`Server is asking clients to back off; retry in ${Math.ceil(remainingMilliseconds / 1000)}s.`);
       } else {
         gPrefs.clearUserPref(PREF_SETTINGS_SERVER_BACKOFF);
       }
     }
 
     Services.obs.notifyObservers(null, "remote-settings:changes-poll-start", JSON.stringify({ expectedTimestamp }));
 
     const lastEtag = gPrefs.getCharPref(PREF_SETTINGS_LAST_ETAG, "");
 
     let pollResult;
     try {
       pollResult = await Utils.fetchLatestChanges(remoteSettings.pollingEndpoint, { expectedTimestamp, lastEtag });
     } catch (e) {
       // Report polling error to Uptake Telemetry.
-      let report;
+      let reportStatus;
       if (/Server/.test(e.message)) {
-        report = UptakeTelemetry.STATUS.SERVER_ERROR;
+        reportStatus = UptakeTelemetry.STATUS.SERVER_ERROR;
       } else if (/NetworkError/.test(e.message)) {
-        report = UptakeTelemetry.STATUS.NETWORK_ERROR;
+        reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
       } else {
-        report = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
+        reportStatus = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
       }
-      UptakeTelemetry.report(TELEMETRY_HISTOGRAM_KEY, report);
+      UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, { source: TELEMETRY_SOURCE });
       // No need to go further.
       throw new Error(`Polling for changes failed: ${e.message}.`);
     }
 
     const {serverTimeMillis, changes, currentEtag, backoffSeconds} = pollResult;
 
     // Report polling success to Uptake Telemetry.
-    const report = changes.length == 0 ? UptakeTelemetry.STATUS.UP_TO_DATE
-                                       : UptakeTelemetry.STATUS.SUCCESS;
-    UptakeTelemetry.report(TELEMETRY_HISTOGRAM_KEY, report);
+    const reportStatus = changes.length === 0 ? UptakeTelemetry.STATUS.UP_TO_DATE
+                                              : UptakeTelemetry.STATUS.SUCCESS;
+    UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, { source: TELEMETRY_SOURCE });
 
     // Check if the server asked the clients to back off (for next poll).
     if (backoffSeconds) {
       const backoffReleaseTime = Date.now() + backoffSeconds * 1000;
       gPrefs.setCharPref(PREF_SETTINGS_SERVER_BACKOFF, backoffReleaseTime);
     }
 
     // Record new update time and the difference between local and server time.
--- a/taskcluster/docker/funsize-update-generator/Pipfile.lock
+++ b/taskcluster/docker/funsize-update-generator/Pipfile.lock
@@ -41,19 +41,20 @@
                 "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
                 "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
             ],
             "index": "pypi",
             "version": "==3.5.4"
         },
         "arrow": {
             "hashes": [
-                "sha256:9cb4a910256ed536751cd5728673bfb53e6f0026e240466f90c2a92c0b79c895"
+                "sha256:3397e5448952e18e1295bf047014659effa5ae8da6a5371d37ff0ddc46fa6872",
+                "sha256:6f54d9f016c0b7811fac9fb8c2c7fa7421d80c54dbdd75ffb12913c55db60b8a"
             ],
-            "version": "==0.13.0"
+            "version": "==0.13.1"
         },
         "asn1crypto": {
             "hashes": [
                 "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
                 "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
             ],
             "version": "==0.24.0"
         },
@@ -81,50 +82,46 @@
             "hashes": [
                 "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
                 "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
             ],
             "version": "==2018.11.29"
         },
         "cffi": {
             "hashes": [
-                "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
-                "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
-                "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
-                "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
-                "sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
-                "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
-                "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
-                "sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
-                "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
-                "sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
-                "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
-                "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
-                "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
-                "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
-                "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
-                "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
-                "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
-                "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
-                "sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
-                "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
-                "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
-                "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
-                "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
-                "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
-                "sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
-                "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
-                "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
-                "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
-                "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
-                "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
-                "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
-                "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
+                "sha256:0b5f895714a7a9905148fc51978c62e8a6cbcace30904d39dcd0d9e2265bb2f6",
+                "sha256:27cdc7ba35ee6aa443271d11583b50815c4bb52be89a909d0028e86c21961709",
+                "sha256:2d4a38049ea93d5ce3c7659210393524c1efc3efafa151bd85d196fa98fce50a",
+                "sha256:3262573d0d60fc6b9d0e0e6e666db0e5045cbe8a531779aa0deb3b425ec5a282",
+                "sha256:358e96cfffc185ab8f6e7e425c7bb028931ed08d65402fbcf3f4e1bff6e66556",
+                "sha256:37c7db824b5687fbd7ea5519acfd054c905951acc53503547c86be3db0580134",
+                "sha256:39b9554dfe60f878e0c6ff8a460708db6e1b1c9cc6da2c74df2955adf83e355d",
+                "sha256:42b96a77acf8b2d06821600fa87c208046decc13bd22a4a0e65c5c973443e0da",
+                "sha256:5b37dde5035d3c219324cac0e69d96495970977f310b306fa2df5910e1f329a1",
+                "sha256:5d35819f5566d0dd254f273d60cf4a2dcdd3ae3003dfd412d40b3fe8ffd87509",
+                "sha256:5df73aa465e53549bd03c819c1bc69fb85529a5e1a693b7b6cb64408dd3970d1",
+                "sha256:7075b361f7a4d0d4165439992d0b8a3cdfad1f302bf246ed9308a2e33b046bd3",
+                "sha256:7678b5a667b0381c173abe530d7bdb0e6e3b98e062490618f04b80ca62686d96",
+                "sha256:7dfd996192ff8a535458c17f22ff5eb78b83504c34d10eefac0c77b1322609e2",
+                "sha256:8a3be5d31d02c60f84c4fd4c98c5e3a97b49f32e16861367f67c49425f955b28",
+                "sha256:9812e53369c469506b123aee9dcb56d50c82fad60c5df87feb5ff59af5b5f55c",
+                "sha256:9b6f7ba4e78c52c1a291d0c0c0bd745d19adde1a9e1c03cb899f0c6efd6f8033",
+                "sha256:a85bc1d7c3bba89b3d8c892bc0458de504f8b3bcca18892e6ed15b5f7a52ad9d",
+                "sha256:aa6b9c843ad645ebb12616de848cc4e25a40f633ccc293c3c9fe34107c02c2ea",
+                "sha256:bae1aa56ee00746798beafe486daa7cfb586cd395c6ce822ba3068e48d761bc0",
+                "sha256:bae96e26510e4825d5910a196bf6b5a11a18b87d9278db6d08413be8ea799469",
+                "sha256:bd78df3b594013b227bf31d0301566dc50ba6f40df38a70ded731d5a8f2cb071",
+                "sha256:c2711197154f46d06f73542c539a0ff5411f1951fab391e0a4ac8359badef719",
+                "sha256:d998c20e3deed234fca993fd6c8314cb7cbfda05fd170f1bd75bb5d7421c3c5a",
+                "sha256:df4f840d77d9e37136f8e6b432fecc9d6b8730f18f896e90628712c793466ce6",
+                "sha256:f5653c2581acb038319e6705d4e3593677676df14b112f13e0b5b44b6a18df1a",
+                "sha256:f7c7aa485a2e2250d455148470ffd0195eecc3d845122635202d7467d6f7b4cf",
+                "sha256:f9e2c66a6493147de835f207f198540a56b26745ce4f272fbc7c2f2cfebeb729"
             ],
-            "version": "==1.11.5"
+            "version": "==1.12.1"
         },
         "chardet": {
             "hashes": [
                 "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
                 "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
             ],
             "version": "==3.0.4"
         },
@@ -338,21 +335,21 @@
                 "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
                 "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
             ],
             "index": "pypi",
             "version": "==2.21.0"
         },
         "scriptworker": {
             "hashes": [
-                "sha256:238fb4c4c53feb4844040ad70920243e35bdbf89d426496cc3536a79b9b52491",
-                "sha256:cd7376a464692316e402abf3565692917a4d13f6d2a57827165c583fab9613db"
+                "sha256:0545f12e0f546966f8033544958a74ee0c6dfe234db5ea7bb70b91dccc9a8f20",
+                "sha256:7d92ec8de7e125cd8927ac724761e3ef25840c6c8931214357405bb3e873b820"
             ],
             "index": "pypi",
-            "version": "==18.1.0"
+            "version": "==19.0.0"
         },
         "sh": {
             "hashes": [
                 "sha256:ae3258c5249493cebe73cb4e18253a41ed69262484bad36fdb3efcb8ad8870bb",
                 "sha256:b52bf5833ed01c7b5c5fb73a7f71b3d98d48e9b9b8764236237bdc7ecae850fc"
             ],
             "index": "pypi",
             "version": "==1.12.14"
--- a/testing/geckodriver/CHANGES.md
+++ b/testing/geckodriver/CHANGES.md
@@ -1,14 +1,37 @@
 Change log
 ==========
 
 All notable changes to this program is documented in this file.
 
 
+Unreleased
+----------
+
+### Removed
+
+- Removed `--webdriver-port` command-line option
+
+  `--webdriver-port <PORT>` was an undocumented alias for `--port`,
+  initially used for backwards compatibility with clients
+  prior to Selenium 3.0.0.
+
+### Changed
+
+- Improved error messages for incorrect command-line usage
+
+### Fixed
+
+- Errors related to incorrect command-line usage no longer hidden
+
+  By mistake, earlier versions of geckodriver failed to print incorrect
+  flag use.  With this release problems are again written to stderr.
+
+
 0.24.0 (2019-01-28, `917474f3473e`)
 -----------------------------------
 
 ### Added
 
 - Introduces `strictFileInteractability` capability
 
   The new capabilitiy indicates if strict interactability checks
--- a/testing/geckodriver/src/main.rs
+++ b/testing/geckodriver/src/main.rs
@@ -15,19 +15,22 @@ extern crate serde_derive;
 extern crate serde_json;
 extern crate uuid;
 extern crate webdriver;
 extern crate zip;
 
 #[macro_use]
 extern crate log;
 
-use std::io::Write;
+use std::env;
+use std::fmt;
+use std::io;
 use std::net::{IpAddr, SocketAddr};
 use std::path::PathBuf;
+use std::result;
 use std::str::FromStr;
 
 use clap::{App, Arg};
 
 macro_rules! try_opt {
     ($expr:expr, $err_type:expr, $err_msg:expr) => {{
         match $expr {
             Some(x) => x,
@@ -41,183 +44,291 @@ mod capabilities;
 mod command;
 mod logging;
 mod marionette;
 mod prefs;
 
 #[cfg(test)]
 pub mod test;
 
-use crate::build::BuildInfo;
 use crate::command::extension_routes;
+use crate::logging::Level;
 use crate::marionette::{MarionetteHandler, MarionetteSettings};
 
-type ProgramResult = std::result::Result<(), (ExitCode, String)>;
+const EXIT_SUCCESS: i32 = 0;
+const EXIT_USAGE: i32 = 64;
+const EXIT_UNAVAILABLE: i32 = 69;
+
+enum FatalError {
+    Parsing(clap::Error),
+    Usage(String),
+    Server(io::Error),
+}
+
+impl FatalError {
+    fn exit_code(&self) -> i32 {
+        use FatalError::*;
+        match *self {
+            Parsing(_) | Usage(_) => EXIT_USAGE,
+            Server(_) => EXIT_UNAVAILABLE,
+        }
+    }
+
+    fn help_included(&self) -> bool {
+        match *self {
+            FatalError::Parsing(_) => true,
+            _ => false,
+        }
+    }
+}
+
+impl From<clap::Error> for FatalError {
+    fn from(err: clap::Error) -> FatalError {
+        FatalError::Parsing(err)
+    }
+}
 
-enum ExitCode {
-    Ok = 0,
-    Usage = 64,
-    Unavailable = 69,
+impl From<io::Error> for FatalError {
+    fn from(err: io::Error) -> FatalError {
+        FatalError::Server(err)
+    }
+}
+
+// harmonise error message from clap to avoid duplicate "error:" prefix
+impl fmt::Display for FatalError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use FatalError::*;
+        let s = match *self {
+            Parsing(ref err) => err.to_string(),
+            Usage(ref s) => format!("error: {}", s),
+            Server(ref err) => format!("error: {}", err.to_string()),
+        };
+        write!(f, "{}", s)
+    }
+}
+
+macro_rules! usage {
+    ($msg:expr) => {
+        return Err(FatalError::Usage($msg.to_string()));
+    };
+
+    ($fmt:expr, $($arg:tt)+) => {
+        return Err(FatalError::Usage(format!($fmt, $($arg)+)));
+    };
+}
+
+type ProgramResult<T> = result::Result<T, FatalError>;
+
+enum Operation {
+    Help,
+    Version,
+    Server {
+        log_level: Option<Level>,
+        address: SocketAddr,
+        settings: MarionetteSettings,
+    },
 }
 
-fn print_version() {
-    println!("geckodriver {}", BuildInfo);
-    println!("");
-    println!("The source code of this program is available from");
-    println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central.");
-    println!("");
-    println!("This program is subject to the terms of the Mozilla Public License 2.0.");
-    println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.");
+fn parse_args(app: &mut App) -> ProgramResult<Operation> {
+    let matches = app.get_matches_from_safe_borrow(env::args())?;
+
+    let log_level = if matches.is_present("log_level") {
+        Level::from_str(matches.value_of("log_level").unwrap()).ok()
+    } else {
+        Some(match matches.occurrences_of("verbosity") {
+            0 => Level::Info,
+            1 => Level::Debug,
+            _ => Level::Trace,
+        })
+    };
+
+    let host = matches.value_of("webdriver_host").unwrap();
+    let port = {
+        let s = matches.value_of("webdriver_port").unwrap();
+        match u16::from_str(s) {
+            Ok(n) => n,
+            Err(e) => usage!("invalid --port: {}: {}", e, s),
+        }
+    };
+    let address = match IpAddr::from_str(host) {
+        Ok(addr) => SocketAddr::new(addr, port),
+        Err(e) => usage!("{}: {}:{}", e, host, port),
+    };
+
+    let binary = matches.value_of("binary").map(PathBuf::from);
+
+    let marionette_host = matches.value_of("marionette_host").unwrap();
+    let marionette_port = match matches.value_of("marionette_port") {
+        Some(s) => match u16::from_str(s) {
+            Ok(n) => Some(n),
+            Err(e) => usage!("invalid --marionette-port: {}", e),
+        },
+        None => None,
+    };
+
+    let op = if matches.is_present("help") {
+        Operation::Help
+    } else if matches.is_present("version") {
+        Operation::Version
+    } else {
+        let settings = MarionetteSettings {
+            host: marionette_host.to_string(),
+            port: marionette_port,
+            binary,
+            connect_existing: matches.is_present("connect_existing"),
+            jsdebugger: matches.is_present("jsdebugger"),
+        };
+        Operation::Server {
+            log_level,
+            address,
+            settings,
+        }
+    };
+
+    Ok(op)
 }
 
-fn app<'a, 'b>() -> App<'a, 'b> {
-    App::new(format!("geckodriver {}", crate_version!()))
-        .about("WebDriver implementation for Firefox.")
+fn inner_main(app: &mut App) -> ProgramResult<()> {
+    match parse_args(app)? {
+        Operation::Help => print_help(app),
+        Operation::Version => print_version(),
+
+        Operation::Server {
+            log_level,
+            address,
+            settings,
+        } => {
+            if let Some(ref level) = log_level {
+                logging::init_with_level(*level).unwrap();
+            } else {
+                logging::init().unwrap();
+            }
+
+            let handler = MarionetteHandler::new(settings);
+            let listening = webdriver::server::start(address, handler, &extension_routes()[..])?;
+            debug!("Listening on {}", listening.socket);
+        }
+    }
+
+    Ok(())
+}
+
+fn main() {
+    use std::process::exit;
+
+    let mut app = make_app();
+
+    // use std::process:Termination when it graduates
+    exit(match inner_main(&mut app) {
+        Ok(_) => EXIT_SUCCESS,
+
+        Err(e) => {
+            eprintln!("{}: {}", get_program_name(), e);
+            if !e.help_included() {
+                print_help(&mut app);
+            }
+
+            e.exit_code()
+        }
+    });
+}
+
+fn make_app<'a, 'b>() -> App<'a, 'b> {
+    App::new(format!("geckodriver {}", build::BuildInfo))
+        .about("WebDriver implementation for Firefox")
         .arg(
             Arg::with_name("webdriver_host")
                 .long("host")
+                .takes_value(true)
                 .value_name("HOST")
-                .help("Host ip to use for WebDriver server (default: 127.0.0.1)")
-                .takes_value(true),
+                .default_value("127.0.0.1")
+                .help("Host IP to use for WebDriver server"),
         )
         .arg(
             Arg::with_name("webdriver_port")
                 .short("p")
                 .long("port")
+                .takes_value(true)
                 .value_name("PORT")
-                .help("Port to use for WebDriver server (default: 4444)")
-                .takes_value(true)
-                .alias("webdriver-port"),
+                .default_value("4444")
+                .help("Port to use for WebDriver server"),
         )
         .arg(
             Arg::with_name("binary")
                 .short("b")
                 .long("binary")
+                .takes_value(true)
                 .value_name("BINARY")
-                .help("Path to the Firefox binary")
-                .takes_value(true),
+                .help("Path to the Firefox binary"),
         )
         .arg(
             Arg::with_name("marionette_host")
                 .long("marionette-host")
+                .takes_value(true)
                 .value_name("HOST")
-                .help("Host to use to connect to Gecko (default: 127.0.0.1)")
-                .takes_value(true)
+                .default_value("127.0.0.1")
+                .help("Host to use to connect to Gecko"),
         )
         .arg(
             Arg::with_name("marionette_port")
                 .long("marionette-port")
+                .takes_value(true)
                 .value_name("PORT")
-                .help("Port to use to connect to Gecko (default: system-allocated port)")
-                .takes_value(true),
+                .help("Port to use to connect to Gecko [default: system-allocated port]"),
         )
         .arg(
             Arg::with_name("connect_existing")
                 .long("connect-existing")
                 .requires("marionette_port")
                 .help("Connect to an existing Firefox instance"),
         )
         .arg(
             Arg::with_name("jsdebugger")
                 .long("jsdebugger")
-                .takes_value(false)
                 .help("Attach browser toolbox debugger for Firefox"),
         )
         .arg(
             Arg::with_name("verbosity")
-                .short("v")
                 .multiple(true)
                 .conflicts_with("log_level")
+                .short("v")
                 .help("Log level verbosity (-v for debug and -vv for trace level)"),
         )
         .arg(
             Arg::with_name("log_level")
                 .long("log")
                 .takes_value(true)
                 .value_name("LEVEL")
                 .possible_values(&["fatal", "error", "warn", "info", "config", "debug", "trace"])
                 .help("Set Gecko log level"),
         )
         .arg(
+            Arg::with_name("help")
+                .short("h")
+                .long("help")
+                .help("Prints this message"),
+        )
+        .arg(
             Arg::with_name("version")
                 .short("V")
                 .long("version")
                 .help("Prints version and copying information"),
         )
 }
 
-fn run() -> ProgramResult {
-    let matches = app().get_matches();
-
-    if matches.is_present("version") {
-        print_version();
-        return Ok(());
-    }
-
-    let host = matches.value_of("webdriver_host").unwrap_or("127.0.0.1");
-    let port = match u16::from_str(
-        matches
-            .value_of("webdriver_port")
-            .or(matches.value_of("webdriver_port_alias"))
-            .unwrap_or("4444"),
-    ) {
-        Ok(x) => x,
-        Err(_) => return Err((ExitCode::Usage, "invalid WebDriver port".into())),
-    };
-    let addr = match IpAddr::from_str(host) {
-        Ok(addr) => SocketAddr::new(addr, port),
-        Err(_) => return Err((ExitCode::Usage, "invalid host address".into())),
-    };
-
-    let binary = matches.value_of("binary").map(PathBuf::from);
+fn get_program_name() -> String {
+    env::args().next().unwrap()
+}
 
-    let marionette_host = matches.value_of("marionette_host")
-        .unwrap_or("127.0.0.1").to_string();
-    let marionette_port = match matches.value_of("marionette_port") {
-        Some(x) => match u16::from_str(x) {
-            Ok(x) => Some(x),
-            Err(_) => return Err((ExitCode::Usage, "invalid Marionette port".into())),
-        },
-        None => None,
-    };
-
-    let log_level = if matches.is_present("log_level") {
-        logging::Level::from_str(matches.value_of("log_level").unwrap()).ok()
-    } else {
-        match matches.occurrences_of("verbosity") {
-            0 => Some(logging::Level::Info),
-            1 => Some(logging::Level::Debug),
-            _ => Some(logging::Level::Trace),
-        }
-    };
-    if let Some(ref level) = log_level {
-        logging::init_with_level(*level).unwrap();
-    } else {
-        logging::init().unwrap();
-    }
-
-    let settings = MarionetteSettings {
-        host: marionette_host,
-        port: marionette_port,
-        binary,
-        connect_existing: matches.is_present("connect_existing"),
-        jsdebugger: matches.is_present("jsdebugger"),
-    };
-    let handler = MarionetteHandler::new(settings);
-    let listening = webdriver::server::start(addr, handler, &extension_routes()[..])
-        .map_err(|err| (ExitCode::Unavailable, err.to_string()))?;
-    debug!("Listening on {}", listening.socket);
-
-    Ok(())
+fn print_help(app: &mut App) {
+    app.print_help().ok();
+    println!();
 }
 
-fn main() {
-    let exit_code = match run() {
-        Ok(_) => ExitCode::Ok,
-        Err((exit_code, reason)) => {
-            error!("{}", reason);
-            exit_code
-        }
-    };
-
-    std::io::stdout().flush().unwrap();
-    std::process::exit(exit_code as i32);
+fn print_version() {
+    println!("geckodriver {}", build::BuildInfo);
+    println!();
+    println!("The source code of this program is available from");
+    println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central.");
+    println!();
+    println!("This program is subject to the terms of the Mozilla Public License 2.0.");
+    println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.");
 }
--- a/testing/mozharness/mozfile/mozfile.py
+++ b/testing/mozharness/mozfile/mozfile.py
@@ -32,22 +32,22 @@ except NameError:
     WindowsError = None  # so we can unconditionally catch it later...
 
 
 # utilities for extracting archives
 
 def extract_tarball(src, dest):
     """extract a .tar file"""
 
-    bundle = tarfile.open(src)
-    namelist = bundle.getnames()
+    with tarfile.open(src) as bundle:
+        namelist = bundle.getnames()
 
-    for name in namelist:
-        bundle.extract(name, path=dest)
-    bundle.close()
+        for name in namelist:
+            bundle.extract(name, path=dest)
+
     return namelist
 
 
 def extract_zip(src, dest):
     """extract a zip file"""
 
     if isinstance(src, zipfile.ZipFile):
         bundle = src
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-text/word-break/word-break-break-all-020.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[word-break-break-all-020.html]
-  expected: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-text/word-break/word-break-keep-all-003.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[word-break-keep-all-003.html]
-  expected: FAIL
--- a/testing/web-platform/meta/service-workers/service-worker/navigation-redirect.https.html.ini
+++ b/testing/web-platform/meta/service-workers/service-worker/navigation-redirect.https.html.ini
@@ -14,16 +14,19 @@
 
   [Redirect to same-origin out-scope with opaque redirect response which is passed through Cache.]
     expected: FAIL
 
   [SW-generated redirect to same-origin out-scope.]
     expected: FAIL
 
   [SW-fallbacked redirect to same-origin out-scope.]
+    disabled:
+     if os == "linux": https://bugzilla.mozilla.org/show_bug.cgi?id=1522439
+     if debug and os == "win": https://bugzilla.mozilla.org/show_bug.cgi?id=1522439
     expected: FAIL
 
   [SW-fetched redirect to same-origin out-scope.]
     expected: FAIL
 
   [SW-generated redirect to same-origin out-scope with a hash fragment.]
     expected: FAIL
 
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
@@ -284,17 +284,17 @@ def run_tests(config, test_paths, produc
                         unexpected_count += manager_group.unexpected_count()
 
                 test_total += test_count
                 unexpected_total += unexpected_count
                 logger.info("Got %i unexpected results" % unexpected_count)
                 logger.suite_end()
                 if repeat_until_unexpected and unexpected_total > 0:
                     break
-                if len(test_loader.test_ids) == skipped_tests:
+                if repeat_count == 1 and len(test_loader.test_ids) == skipped_tests:
                     break
 
     if test_total == 0:
         if skipped_tests > 0:
             logger.warning("All requested tests were skipped")
         else:
             if kwargs["default_exclude"]:
                 logger.info("No tests ran")
--- a/toolkit/components/aboutperformance/content/aboutPerformance.js
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.js
@@ -246,23 +246,29 @@ var State = {
   isTracker(host) {
     if (!this._trackingState.has(host)) {
       // Temporarily set to false to avoid doing several lookups if a site has
       // several subframes on the same domain.
       this._trackingState.set(host, false);
       if (host.startsWith("about:") || host.startsWith("moz-nullprincipal"))
         return false;
 
-      let principal =
-        Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("http://" + host);
+      let uri = Services.io.newURI("http://" + host);
       let classifier =
         Cc["@mozilla.org/url-classifier/dbservice;1"].getService(Ci.nsIURIClassifier);
-      classifier.classify(principal, null, true,
-                          (aErrorCode, aList, aProvider, aFullHash) => {
-        this._trackingState.set(host, ChromeUtils.IsClassifierBlockingErrorCode(aErrorCode));
+      let feature = classifier.getFeatureByName("tracking-protection");
+      if (!feature) {
+        return false;
+      }
+
+      classifier.asyncClassifyLocalWithFeatures(uri, [feature],
+        Ci.nsIUrlClassifierFeature.blacklist, list => {
+          if (list.length) {
+            this._trackingState.set(host, true);
+          }
       });
     }
     return this._trackingState.get(host);
   },
 
   getCounters() {
     tabFinder.update();
     // We rebuild the maps during each iteration to make sure that
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -378,17 +378,17 @@ class InnerWindowReference {
     }
     return null;
   }
 
   invalidate() {
     // If invalidate() is called while the inner window is in the bfcache, then
     // we are unable to remove the event listener, and handleEvent will be
     // called once more if the page is revived from the bfcache.
-    if (this.contentWindow) {
+    if (this.contentWindow && !Cu.isDeadWrapper(this.contentWindow)) {
       this.contentWindow.removeEventListener("pagehide", this, {mozSystemGroup: true});
       this.contentWindow.removeEventListener("pageshow", this, {mozSystemGroup: true});
     }
     this.contentWindow = null;
     this.needWindowIDCheck = false;
   }
 
   handleEvent(event) {
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -351,17 +351,17 @@ ProxyMessenger = {
 
     const noHandlerError = {
       result: MessageChannel.RESULT_NO_HANDLER,
       message: "No matching message handler for the given recipient.",
     };
 
     let extension = GlobalManager.extensionMap.get(sender.extensionId);
 
-    if (extension.wakeupBackground) {
+    if (extension && extension.wakeupBackground) {
       await extension.wakeupBackground();
     }
 
     let {
       messageManager: receiverMM,
       xulBrowser: receiverBrowser,
     } = this.getMessageManagerForRecipient(recipient);
     if (!extension || !receiverMM) {
--- a/toolkit/components/normandy/lib/Uptake.jsm
+++ b/toolkit/components/normandy/lib/Uptake.jsm
@@ -4,17 +4,17 @@
 
 "use strict";
 
 ChromeUtils.defineModuleGetter(
   this, "UptakeTelemetry", "resource://services-common/uptake-telemetry.js");
 
 var EXPORTED_SYMBOLS = ["Uptake"];
 
-const SOURCE_PREFIX = "normandy";
+const COMPONENT = "normandy";
 
 var Uptake = {
   // Action uptake
   ACTION_NETWORK_ERROR: UptakeTelemetry.STATUS.NETWORK_ERROR,
   ACTION_PRE_EXECUTION_ERROR: UptakeTelemetry.STATUS.CUSTOM_1_ERROR,
   ACTION_POST_EXECUTION_ERROR: UptakeTelemetry.STATUS.CUSTOM_2_ERROR,
   ACTION_SERVER_ERROR: UptakeTelemetry.STATUS.SERVER_ERROR,
   ACTION_SUCCESS: UptakeTelemetry.STATUS.SUCCESS,
@@ -27,19 +27,19 @@ var Uptake = {
 
   // Uptake for the runner as a whole
   RUNNER_INVALID_SIGNATURE: UptakeTelemetry.STATUS.SIGNATURE_ERROR,
   RUNNER_NETWORK_ERROR: UptakeTelemetry.STATUS.NETWORK_ERROR,
   RUNNER_SERVER_ERROR: UptakeTelemetry.STATUS.SERVER_ERROR,
   RUNNER_SUCCESS: UptakeTelemetry.STATUS.SUCCESS,
 
   reportRunner(status) {
-    UptakeTelemetry.report(`${SOURCE_PREFIX}/runner`, status);
+    UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/runner` });
   },
 
   reportRecipe(recipeId, status) {
-    UptakeTelemetry.report(`${SOURCE_PREFIX}/recipe/${recipeId}`, status);
+    UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/recipe/${recipeId}` });
   },
 
   reportAction(actionName, status) {
-    UptakeTelemetry.report(`${SOURCE_PREFIX}/action/${actionName}`, status);
+    UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/action/${actionName}` });
   },
 };
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -853,16 +853,41 @@ security.ui.identitypopup:
     release_channel_collection: opt-in
     record_in_processes:
       - main
     extra_keys:
       tp: Whether Tracking Protection was active while the user interacted with the UI
       cr: Whether Cookie Restrictions was active while the user interacted with the UI
     products:
       - firefox
+uptake.remotecontent.result:
+  uptake:
+    description: >
+      Was the remote content successfully pulled?
+      This uptake telemetry allows to monitor the behaviour of our clients when it comes
+      to fetching data from remote servers. This helps defect-detection and allow observation of
+      the proportion of success among clients and sources, the distribution of error causes, and
+      its evolution over time.
+    methods:
+      - uptake
+    objects:
+      - remotesettings
+      - normandy
+    extra_keys:
+      source: >
+        A label to distinguish what is being pulled or updated in the component (eg. recipe id,
+        settings collection name, ...).
+    bug_numbers:
+      - 1517469
+    record_in_processes: ["main"]
+    release_channel_collection: opt-out
+    expiry_version: never
+    notification_emails:
+      - mleplatre@mozilla.com
+      - bens-directs@mozilla.com
 
 intl.ui.browserLanguage:
   action:
     description: >
       User interactions for the browser language within about-preferences in the main pane and in
       the browser language dialog. Each dialog event (on the dialog object, and the manage and
       search methods of the main object) has a value which is a monotonically increasing number
       that links it with other events related to the same dialog instance.
--- a/toolkit/components/telemetry/docs/collection/events.rst
+++ b/toolkit/components/telemetry/docs/collection/events.rst
@@ -69,16 +69,18 @@ For the Firefox Telemetry implementation
 
   - Each extra key name: Max. string length is ``15``.
   - Each extra value: Max. byte length is ``80``.
 
 Only ``value`` and the values of ``extra`` will be truncated if over the specified length.
 Any other ``String`` going over its limit will be reported as an error and the operation
 aborted.
 
+.. _eventdefinition:
+
 The YAML definition file
 ========================
 
 Any event recorded into Firefox Telemetry must be registered before it can be recorded.
 For any code that ships as part of Firefox that happens in `Events.yaml <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Events.yaml>`_.
 
 The probes in the definition file are represented in a fixed-depth, three-level structure. The first level contains *category* names (grouping multiple events together), the second level contains *event* names, under which the events properties are listed. E.g.:
 
--- a/toolkit/components/telemetry/docs/collection/uptake.rst
+++ b/toolkit/components/telemetry/docs/collection/uptake.rst
@@ -21,19 +21,20 @@ The helper — described below — reports predefined update status, which eventually gives a unified way to obtain:
 
 Usage
 -----
 
 .. code-block:: js
 
    const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js", {});
 
-   UptakeTelemetry.report(source, status);
+   UptakeTelemetry.report(component, status, { source });
 
-- ``source``, a ``string`` that is an identifier for the update source (eg. ``addons-blocklist``)
+- ``component``, a ``string`` that identifies the calling component (eg. ``"remotesettings"``, ``"normandy"``). Arbitrary components have to be previously declared in the :ref:`Telemetry Events definition file <eventdefinition>`.
+- ``source``, a ``string`` to distinguish what is being pulled or updated in the component (eg. ``"blocklists/addons"``, ``"recipes/33"``)
 - ``status``, one of the following status constants:
 
   - ``UptakeTelemetry.STATUS.UP_TO_DATE``: Local content was already up-to-date with remote content.
   - ``UptakeTelemetry.STATUS.SUCCESS``: Local content was updated successfully.
   - ``UptakeTelemetry.STATUS.BACKOFF``: Remote server asked clients to backoff.
   - ``UptakeTelemetry.STATUS.PREF_DISABLED``: Update is disabled in user preferences.
   - ``UptakeTelemetry.STATUS.PARSE_ERROR``: Parsing server response has failed.
   - ``UptakeTelemetry.STATUS.CONTENT_ERROR``: Server response has unexpected content.
@@ -52,45 +53,46 @@ Usage
   - ``UptakeTelemetry.STATUS.UNKNOWN_ERROR``: Uncategorized error.
   - ``UptakeTelemetry.STATUS.CUSTOM_1_ERROR``: Error #1 specific to this update source.
   - ``UptakeTelemetry.STATUS.CUSTOM_2_ERROR``: Error #2 specific to this update source.
   - ``UptakeTelemetry.STATUS.CUSTOM_3_ERROR``: Error #3 specific to this update source.
   - ``UptakeTelemetry.STATUS.CUSTOM_4_ERROR``: Error #4 specific to this update source.
   - ``UptakeTelemetry.STATUS.CUSTOM_5_ERROR``: Error #5 specific to this update source.
 
 
-The data is submitted to a single :ref:`keyed histogram <histogram-type-keyed>` whose id is ``UPTAKE_REMOTE_CONTENT_RESULT_1`` and the specified update ``source`` as the key.
+The data is submitted as a :ref:`Telemetry Event <eventtelemetry>`, and to a single :ref:`keyed histogram <histogram-type-keyed>` whose id is ``UPTAKE_REMOTE_CONTENT_RESULT_1`` and the specified update ``source`` as the key.
 
 Example:
 
 .. code-block:: js
 
+   const COMPONENT = "normandy";
    const UPDATE_SOURCE = "update-monitoring";
 
    let status;
    try {
      const data = await fetch(uri);
      status = UptakeTelemetry.STATUS.SUCCESS;
    } catch (e) {
      status = /NetworkError/.test(e) ?
                  UptakeTelemetry.STATUS.NETWORK_ERROR :
                  UptakeTelemetry.STATUS.SERVER_ERROR ;
    }
-   UptakeTelemetry.report(UPDATE_SOURCE, status);
+   UptakeTelemetry.report(COMPONENT, status, { source: UPDATE_SOURCE });
 
 
 Use-cases
 ---------
 
 The following remote data sources are already using this unified histogram.
 
 * remote settings changes monitoring
 * add-ons blocklist
 * gfx blocklist
 * plugins blocklist
 * certificate revocation
 * certificate pinning
-* :ref:`Shield Recipe client <components/normandy>`
+* :ref:`Normandy Recipe client <components/normandy>`
 
 Obviously, the goal is to eventually converge and avoid ad-hoc Telemetry probes for measuring uptake of remote content. Some notable potential use-cases are:
 
 * nsUpdateService
 * mozapps extensions update
--- a/widget/tests/test_imestate.html
+++ b/widget/tests/test_imestate.html
@@ -1288,17 +1288,17 @@ function runTestPasswordFieldOnDialog() 
   };
 
   var observer = new WindowObserver();
   var arg1 = {}, arg2 = {};
   Services.prompt.promptPassword(window, "title", "text", arg1, "msg", arg2);
 
   ok(true, "password dialog was closed");
 
-  Services.obs.addObserver(observer, "domwindowopened");
+  Services.obs.removeObserver(observer, "domwindowopened");
 
   var passwordField;
 
   function onPasswordDialogLoad() {
     ok(true, "onPasswordDialogLoad is called");
     dialog.removeEventListener("load", onPasswordDialogLoad);
     passwordField = dialog.document.getElementById("password1Textbox");
     passwordField.addEventListener("focus", onPasswordFieldFocus);