merge autoland to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Fri, 13 Oct 2017 23:35:51 +0200
changeset 386117 684b9ee0468e6560b00c69231adfe1b7e68d58a4
parent 386060 56b5c1a87dcb2c0391e7f642f99e6638dcf235c0 (current diff)
parent 386116 f013760760f227e45493de3f251949dbb23750c2 (diff)
child 386175 a31334a65a1c75638efae4452ecd271450df2ad0
push id32675
push userarchaeopteryx@coole-files.de
push dateFri, 13 Oct 2017 21:36:21 +0000
treeherdermozilla-central@684b9ee0468e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge autoland to mozilla-central. r=merge a=merge MozReview-Commit-ID: 7ph7uT1QwPS
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_No_input_and_Tab_key_pressed.js
devtools/shim/locales/en-US/aboutdevtools.dtd
netwerk/streamconv/converters/parse-ftp/3-guess.in
netwerk/streamconv/converters/parse-ftp/3-guess.out
netwerk/streamconv/converters/parse-ftp/C-VMold.in
netwerk/streamconv/converters/parse-ftp/C-VMold.out
netwerk/streamconv/converters/parse-ftp/C-zVM.in
netwerk/streamconv/converters/parse-ftp/C-zVM.out
netwerk/streamconv/converters/parse-ftp/D-WinNT.in
netwerk/streamconv/converters/parse-ftp/D-WinNT.out
netwerk/streamconv/converters/parse-ftp/E-EPLF.in
netwerk/streamconv/converters/parse-ftp/E-EPLF.out
netwerk/streamconv/converters/parse-ftp/O-guess.in
netwerk/streamconv/converters/parse-ftp/O-guess.out
netwerk/streamconv/converters/parse-ftp/R-dls.in
netwerk/streamconv/converters/parse-ftp/R-dls.out
netwerk/streamconv/converters/parse-ftp/README
netwerk/streamconv/converters/parse-ftp/U-HellSoft.in
netwerk/streamconv/converters/parse-ftp/U-HellSoft.out
netwerk/streamconv/converters/parse-ftp/U-NetPresenz.in
netwerk/streamconv/converters/parse-ftp/U-NetPresenz.out
netwerk/streamconv/converters/parse-ftp/U-NetWare.in
netwerk/streamconv/converters/parse-ftp/U-NetWare.out
netwerk/streamconv/converters/parse-ftp/U-Novonyx.in
netwerk/streamconv/converters/parse-ftp/U-Novonyx.out
netwerk/streamconv/converters/parse-ftp/U-Surge.in
netwerk/streamconv/converters/parse-ftp/U-Surge.out
netwerk/streamconv/converters/parse-ftp/U-WarFTPd.in
netwerk/streamconv/converters/parse-ftp/U-WarFTPd.out
netwerk/streamconv/converters/parse-ftp/U-WebStar.in
netwerk/streamconv/converters/parse-ftp/U-WebStar.out
netwerk/streamconv/converters/parse-ftp/U-WinNT.in
netwerk/streamconv/converters/parse-ftp/U-WinNT.out
netwerk/streamconv/converters/parse-ftp/U-hethmon.in
netwerk/streamconv/converters/parse-ftp/U-hethmon.out
netwerk/streamconv/converters/parse-ftp/U-murksw.in
netwerk/streamconv/converters/parse-ftp/U-murksw.out
netwerk/streamconv/converters/parse-ftp/U-ncFTPd.in
netwerk/streamconv/converters/parse-ftp/U-ncFTPd.out
netwerk/streamconv/converters/parse-ftp/U-no_ug.in
netwerk/streamconv/converters/parse-ftp/U-no_ug.out
netwerk/streamconv/converters/parse-ftp/U-nogid.in
netwerk/streamconv/converters/parse-ftp/U-nogid.out
netwerk/streamconv/converters/parse-ftp/U-proftpd.in
netwerk/streamconv/converters/parse-ftp/U-proftpd.out
netwerk/streamconv/converters/parse-ftp/U-wu.in
netwerk/streamconv/converters/parse-ftp/U-wu.out
netwerk/streamconv/converters/parse-ftp/V-MultiNet.in
netwerk/streamconv/converters/parse-ftp/V-MultiNet.out
netwerk/streamconv/converters/parse-ftp/V-VMS-mix.in
netwerk/streamconv/converters/parse-ftp/V-VMS-mix.out
--- a/accessible/windows/msaa/RootAccessibleWrap.cpp
+++ b/accessible/windows/msaa/RootAccessibleWrap.cpp
@@ -97,8 +97,52 @@ RootAccessibleWrap::DocumentActivated(Do
       HWND childDocHWND = static_cast<HWND>(childDoc->GetNativeWindow());
       if (childDoc != aDocument)
         nsWinUtils::HideNativeWindow(childDocHWND);
       else
         nsWinUtils::ShowNativeWindow(childDocHWND);
     }
   }
 }
+
+STDMETHODIMP
+RootAccessibleWrap::accNavigate(
+      /* [in] */ long navDir,
+      /* [optional][in] */ VARIANT varStart,
+      /* [retval][out] */ VARIANT __RPC_FAR *pvarEndUpAt)
+{
+  // Special handling for NAVRELATION_EMBEDS.
+  // When we only have a single process, this can be handled the same way as
+  // any other relation.
+  // However, for multi process, the normal relation mechanism doesn't work
+  // because it can't handle remote objects.
+  if (navDir != NAVRELATION_EMBEDS ||
+      varStart.vt != VT_I4  || varStart.lVal != CHILDID_SELF) {
+    // We only handle EMBEDS on the root here.
+    // Forward to the base implementation.
+    return DocAccessibleWrap::accNavigate(navDir, varStart, pvarEndUpAt);
+  }
+
+  if (!pvarEndUpAt) {
+    return E_INVALIDARG;
+  }
+
+  Accessible* target = nullptr;
+  // Get the document in the active tab.
+  ProxyAccessible* docProxy = GetPrimaryRemoteTopLevelContentDoc();
+  if (docProxy) {
+    target = WrapperFor(docProxy);
+  } else {
+    // The base implementation could handle this, but we may as well
+    // just handle it here.
+    Relation rel = RelationByType(RelationType::EMBEDS);
+    target = rel.Next();
+  }
+
+  if (!target) {
+    return E_FAIL;
+  }
+
+  VariantInit(pvarEndUpAt);
+  pvarEndUpAt->pdispVal = NativeAccessible(target);
+  pvarEndUpAt->vt = VT_DISPATCH;
+  return S_OK;
+}
--- a/accessible/windows/msaa/RootAccessibleWrap.h
+++ b/accessible/windows/msaa/RootAccessibleWrap.h
@@ -33,16 +33,21 @@ public:
   already_AddRefed<IUnknown> Aggregate(IUnknown* aOuter);
 
   /**
    * @return This object's own IUnknown, as opposed to its wrapper's IUnknown
    *         which is what would be returned by QueryInterface(IID_IUnknown).
    */
   already_AddRefed<IUnknown> GetInternalUnknown();
 
+  virtual /* [id] */ HRESULT STDMETHODCALLTYPE accNavigate(
+    /* [in] */ long navDir,
+    /* [optional][in] */ VARIANT varStart,
+    /* [retval][out] */ VARIANT __RPC_FAR *pvarEndUpAt) override;
+
 private:
   // DECLARE_AGGREGATABLE declares the internal IUnknown methods as well as
   // mInternalUnknown.
   DECLARE_AGGREGATABLE(RootAccessibleWrap);
   IUnknown* mOuter;
 };
 
 } // namespace a11y
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1459,16 +1459,17 @@ var gBrowserInit = {
     Services.obs.addObserver(gXPInstallObserver, "addon-install-origin-blocked");
     Services.obs.addObserver(gXPInstallObserver, "addon-install-failed");
     Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation");
     Services.obs.addObserver(gXPInstallObserver, "addon-install-complete");
     window.messageManager.addMessageListener("Browser:URIFixup", gKeywordURIFixup);
 
     BrowserOffline.init();
     IndexedDBPromptHelper.init();
+    CanvasPermissionPromptHelper.init();
 
     if (AppConstants.E10S_TESTING_ONLY)
       gRemoteTabsUI.init();
 
     // Initialize the full zoom setting.
     // We do this before the session restore service gets initialized so we can
     // apply full zoom settings to tabs restored by the session restore service.
     FullZoom.init();
@@ -1892,16 +1893,17 @@ var gBrowserInit = {
         Cu.reportError(ex);
       }
 
       if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
         MenuTouchModeObserver.uninit();
       }
       BrowserOffline.uninit();
       IndexedDBPromptHelper.uninit();
+      CanvasPermissionPromptHelper.uninit();
       PanelUI.uninit();
       AutoShowBookmarksToolbar.uninit();
     }
 
     // Final window teardown, do this last.
     window.XULBrowserWindow = null;
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIWebNavigation)
@@ -5712,27 +5714,39 @@ var gUIDensity = {
     gPrefService.setIntPref(this.uiDensityPref, mode);
   },
 
   update(mode) {
     if (mode == null) {
       mode = this.getCurrentDensity().mode;
     }
 
-    let doc = document.documentElement;
-    switch (mode) {
-    case this.MODE_COMPACT:
-      doc.setAttribute("uidensity", "compact");
-      break;
-    case this.MODE_TOUCH:
-      doc.setAttribute("uidensity", "touch");
-      break;
-    default:
-      doc.removeAttribute("uidensity");
-      break;
+    let docs = [
+      document.documentElement,
+      SidebarUI.browser.contentDocument.documentElement,
+    ];
+    for (let doc of docs) {
+      switch (mode) {
+      case this.MODE_COMPACT:
+        doc.setAttribute("uidensity", "compact");
+        break;
+      case this.MODE_TOUCH:
+        doc.setAttribute("uidensity", "touch");
+        break;
+      default:
+        doc.removeAttribute("uidensity");
+        break;
+      }
+    }
+    let tree = SidebarUI.browser.contentDocument.querySelector(".sidebar-placesTree");
+    if (tree) {
+      // Tree items don't update their styles without changing some property on the
+      // parent tree element, like background-color or border. See bug 1407399.
+      tree.style.border = "1px";
+      tree.style.border = "";
     }
 
     TabsInTitlebar.updateAppearance(true);
   },
 };
 
 var gHomeButton = {
   prefDomain: "browser.startup.homepage",
@@ -6664,16 +6678,92 @@ var IndexedDBPromptHelper = {
       browser, topic, message, this._notificationIcon, mainAction, secondaryActions,
       {
         persistent: true,
         hideClose: !Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton"),
       });
   }
 };
 
+var CanvasPermissionPromptHelper = {
+  _permissionsPrompt: "canvas-permissions-prompt",
+  _notificationIcon: "canvas-notification-icon",
+
+  init() {
+    Services.obs.addObserver(this, this._permissionsPrompt);
+  },
+
+  uninit() {
+    Services.obs.removeObserver(this, this._permissionsPrompt);
+  },
+
+  // aSubject is an nsIBrowser (e10s) or an nsIDOMWindow (non-e10s).
+  // aData is an URL string.
+  observe(aSubject, aTopic, aData) {
+    if (aTopic != this._permissionsPrompt) {
+      return;
+    }
+
+    let browser;
+    if (aSubject instanceof Ci.nsIDOMWindow) {
+      let contentWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+      browser = gBrowser.getBrowserForContentWindow(contentWindow);
+    } else {
+      browser = aSubject.QueryInterface(Ci.nsIBrowser);
+    }
+
+    let uri = Services.io.newURI(aData);
+    if (gBrowser.selectedBrowser !== browser) {
+      // Must belong to some other window.
+      return;
+    }
+
+    let message = gNavigatorBundle.getFormattedString("canvas.siteprompt", [ uri.asciiHost ]);
+
+    function setCanvasPermission(aURI, aPerm, aPersistent) {
+      Services.perms.add(aURI, "canvas/extractData", aPerm,
+                          aPersistent ? Ci.nsIPermissionManager.EXPIRE_NEVER
+                                      : Ci.nsIPermissionManager.EXPIRE_SESSION);
+    }
+
+    let mainAction = {
+      label: gNavigatorBundle.getString("canvas.allow"),
+      accessKey: gNavigatorBundle.getString("canvas.allow.accesskey"),
+      callback(state) {
+        setCanvasPermission(uri, Ci.nsIPermissionManager.ALLOW_ACTION,
+                            state && state.checkboxChecked);
+      }
+    };
+
+    let secondaryActions = [{
+      label: gNavigatorBundle.getString("canvas.notAllow"),
+      accessKey: gNavigatorBundle.getString("canvas.notAllow.accesskey"),
+      callback(state) {
+        setCanvasPermission(uri, Ci.nsIPermissionManager.DENY_ACTION,
+                            state && state.checkboxChecked);
+      }
+    }];
+
+    let checkbox = {
+      // In PB mode, we don't want the "always remember" checkbox
+      show: !PrivateBrowsingUtils.isWindowPrivate(window)
+    };
+    if (checkbox.show) {
+      checkbox.checked = true;
+      checkbox.label = gBrowserBundle.GetStringFromName("canvas.remember");
+    }
+
+    let options = {
+      checkbox
+    };
+    PopupNotifications.show(browser, aTopic, message, this._notificationIcon,
+                            mainAction, secondaryActions, options);
+  }
+};
+
 function CanCloseWindow() {
   // Avoid redundant calls to canClose from showing multiple
   // PermitUnload dialogs.
   if (Services.startup.shuttingDown || window.skipNextCanClose) {
     return true;
   }
 
   let timedOutProcesses = new WeakSet();
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -818,16 +818,18 @@
                      onmouseout="document.getElementById('identity-box').classList.remove('no-hover');"
                      align="center">
                   <image id="default-notification-icon" class="notification-anchor-icon" role="button"
                          tooltiptext="&urlbar.defaultNotificationAnchor.tooltip;"/>
                   <image id="geo-notification-icon" class="notification-anchor-icon geo-icon" role="button"
                          tooltiptext="&urlbar.geolocationNotificationAnchor.tooltip;"/>
                   <image id="addons-notification-icon" class="notification-anchor-icon install-icon" role="button"
                          tooltiptext="&urlbar.addonsNotificationAnchor.tooltip;"/>
+                  <image id="canvas-notification-icon" class="notification-anchor-icon" role="button"
+                         tooltiptext="&urlbar.canvasNotificationAnchor.tooltip;"/>
                   <image id="indexedDB-notification-icon" class="notification-anchor-icon indexedDB-icon" role="button"
                          tooltiptext="&urlbar.indexedDBNotificationAnchor.tooltip;"/>
                   <image id="password-notification-icon" class="notification-anchor-icon login-icon" role="button"
                          tooltiptext="&urlbar.passwordNotificationAnchor.tooltip;"/>
                   <stack id="plugins-notification-icon" class="notification-anchor-icon" role="button" align="center"
                          tooltiptext="&urlbar.pluginsNotificationAnchor.tooltip;">
                     <image class="plugin-icon" />
                     <image id="plugin-icon-badge" />
--- a/browser/base/content/test/permissions/browser.ini
+++ b/browser/base/content/test/permissions/browser.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 support-files=
   head.js
   permissions.html
 
+[browser_canvas_fingerprinting_resistance.js]
 [browser_permissions.js]
 [browser_temporary_permissions.js]
 support-files =
   temporary_permissions_subframe.html
   ../webrtc/get_user_media.html
 [browser_temporary_permissions_expiry.js]
 [browser_temporary_permissions_navigation.js]
 [browser_temporary_permissions_tabs.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js
@@ -0,0 +1,122 @@
+/**
+ * When "privacy.resistFingerprinting" is set to true, user permission is
+ * required for canvas data extraction.
+ * This tests whether the site permission prompt for canvas data extraction
+ * works properly.
+ */
+"use strict";
+
+const kUrl = "https://example.com/";
+const kPrincipal = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+    .getService(Ci.nsIScriptSecurityManager)
+    .createCodebasePrincipal(Services.io.newURI(kUrl), {});
+const kPermission = "canvas/extractData";
+
+function initTab() {
+  let contentWindow = content.wrappedJSObject;
+
+  let drawCanvas = (fillStyle, id) => {
+    let contentDocument = contentWindow.document;
+    let width = 64, height = 64;
+    let canvas = contentDocument.createElement("canvas");
+    if (id) {
+      canvas.setAttribute("id", id);
+    }
+    canvas.setAttribute("width", width);
+    canvas.setAttribute("height", height);
+    contentDocument.body.appendChild(canvas);
+
+    let context = canvas.getContext("2d");
+    context.fillStyle = fillStyle;
+    context.fillRect(0, 0, width, height);
+
+    return canvas;
+  };
+
+  let placeholder = drawCanvas("white");
+  contentWindow.kPlaceholderData = placeholder.toDataURL();
+  let canvas = drawCanvas("cyan", "canvas-id-canvas");
+  isnot(canvas.toDataURL(), contentWindow.kPlaceholderData,
+      "privacy.resistFingerprinting = false, canvas data != placeholder data");
+}
+
+function enableResistFingerprinting() {
+  return SpecialPowers.pushPrefEnv({
+    set: [
+      ["privacy.resistFingerprinting", true]
+    ]
+  });
+}
+
+function promisePopupShown() {
+  return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown")
+}
+
+function promisePopupHidden() {
+  return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden")
+}
+
+function extractCanvasData(grantPermission) {
+  let contentWindow = content.wrappedJSObject;
+  let canvas = contentWindow.document.getElementById("canvas-id-canvas");
+  let canvasData = canvas.toDataURL();
+  if (grantPermission) {
+    isnot(canvasData, contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true, permission granted, canvas data != placeholderdata");
+  } else if (grantPermission === false) {
+    is(canvasData, contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true, permission denied, canvas data == placeholderdata");
+  } else {
+    is(canvasData, contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true, requesting permission, canvas data == placeholderdata");
+  }
+}
+
+function triggerCommand(button) {
+  let notifications = PopupNotifications.panel.childNodes;
+  let notification = notifications[0];
+  EventUtils.synthesizeMouseAtCenter(notification[button], {});
+}
+
+function triggerMainCommand() {
+  triggerCommand("button");
+}
+
+function triggerSecondaryCommand() {
+  triggerCommand("secondaryButton");
+}
+
+function testPermission() {
+  return Services.perms.testPermissionFromPrincipal(kPrincipal, kPermission);
+}
+
+async function withNewTab(grantPermission, browser) {
+  await ContentTask.spawn(browser, null, initTab);
+  await enableResistFingerprinting();
+  let popupShown = promisePopupShown();
+  await ContentTask.spawn(browser, null, extractCanvasData);
+  await popupShown;
+  let popupHidden = promisePopupHidden();
+  if (grantPermission) {
+    triggerMainCommand();
+    await popupHidden;
+    is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted");
+  } else {
+    triggerSecondaryCommand();
+    await popupHidden;
+    is(testPermission(), Services.perms.DENY_ACTION, "permission denied");
+  }
+  await ContentTask.spawn(browser, grantPermission, extractCanvasData);
+  await SpecialPowers.popPrefEnv();
+}
+
+async function doTest(grantPermission) {
+  Services.perms.removeFromPrincipal(kPrincipal, kPermission);
+  await BrowserTestUtils.withNewTab(kUrl, withNewTab.bind(null, grantPermission));
+}
+
+// Tests clicking "Don't Allow" button of the permission prompt.
+add_task(doTest.bind(null, false));
+
+// Tests clicking "Allow" button of the permission prompt.
+add_task(doTest.bind(null, true));
--- a/browser/components/places/content/sidebarUtils.js
+++ b/browser/components/places/content/sidebarUtils.js
@@ -1,13 +1,14 @@
 /* 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/. */
 
 Components.utils.import("resource://gre/modules/AppConstants.jsm");
+window.top.gUIDensity.update();
 
 var SidebarUtils = {
   handleTreeClick: function SU_handleTreeClick(aTree, aEvent, aGutterSelect) {
     // right-clicks are not handled here
     if (aEvent.button == 2)
       return;
 
     var tbo = aTree.treeBoxObject;
--- a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_max.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_max.js
@@ -1,62 +1,11 @@
 /*
  * Bug 1330882 - A test case for opening new windows through window.open() as
  *   rounded size when fingerprinting resistance is enabled. This test is for
  *   maximum values.
  */
 
-const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
-
-const TEST_DOMAIN = "http://example.net/";
-const TEST_PATH = TEST_DOMAIN + "browser/browser/components/resistfingerprinting/test/browser/";
-
-let gMaxAvailWidth;
-let gMaxAvailHeight;
-
-// We need the chrome UI size of popup windows for testing outerWidth/Height.
-let gPopupChromeUIWidth;
-let gPopupChromeUIHeight;
-
-const TESTCASES = [
-  { settingWidth: 1025, settingHeight: 1050, targetWidth: 1000, targetHeight: 1000 },
-  { settingWidth: 9999, settingHeight: 9999, targetWidth: 1000, targetHeight: 1000 },
-  { settingWidth: 999, settingHeight: 999, targetWidth: 1000, targetHeight: 1000 },
-];
-
-add_task(async function setup() {
-  await SpecialPowers.pushPrefEnv({"set":
-    [["privacy.resistFingerprinting", true]]
-  });
-
-  // Calculate the popup window's chrome UI size for tests of outerWidth/Height.
-  let popUpChromeUISize = await calcPopUpWindowChromeUISize();
-
-  gPopupChromeUIWidth = popUpChromeUISize.chromeWidth;
-  gPopupChromeUIHeight = popUpChromeUISize.chromeHeight;
-
-  // Calculate the maximum available size.
-  let maxAvailSize = await calcMaximumAvailSize(gPopupChromeUIWidth,
-                                                gPopupChromeUIHeight);
-
-  gMaxAvailWidth = maxAvailSize.maxAvailWidth;
-  gMaxAvailHeight = maxAvailSize.maxAvailHeight;
-});
-
-add_task(async function test_window_open() {
-  // Open a tab to test window.open().
-  let tab = await BrowserTestUtils.openNewForegroundTab(
-    gBrowser, TEST_PATH + "file_dummy.html");
-
-  for (let test of TESTCASES) {
-    // Test 'width' and 'height' of window features.
-    await testWindowOpen(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                         test.targetWidth, test.targetHeight, false, gMaxAvailWidth,
-                         gMaxAvailHeight, gPopupChromeUIWidth, gPopupChromeUIHeight);
-
-    // test 'outerWidth' and 'outerHeight' of window features.
-    await testWindowOpen(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                         test.targetWidth, test.targetHeight, true, gMaxAvailWidth,
-                         gMaxAvailHeight, gPopupChromeUIWidth, gPopupChromeUIHeight);
-  }
-
-  await BrowserTestUtils.removeTab(tab);
-});
+OpenTest.run([
+  {settingWidth: 1025, settingHeight: 1050, targetWidth: 1000, targetHeight: 1000},
+  {settingWidth: 9999, settingHeight: 9999, targetWidth: 1000, targetHeight: 1000},
+  {settingWidth: 999, settingHeight: 999, targetWidth: 1000, targetHeight: 1000}
+]);
--- a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_mid.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_mid.js
@@ -1,62 +1,11 @@
 /*
  * Bug 1330882 - A test case for opening new windows through window.open() as
  *   rounded size when fingerprinting resistance is enabled. This test is for
  *   middle values.
  */
 
-const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
-
-const TEST_DOMAIN = "http://example.net/";
-const TEST_PATH = TEST_DOMAIN + "browser/browser/components/resistfingerprinting/test/browser/";
-
-let gMaxAvailWidth;
-let gMaxAvailHeight;
-
-// We need the chrome UI size of popup windows for testing outerWidth/Height.
-let gPopupChromeUIWidth;
-let gPopupChromeUIHeight;
-
-const TESTCASES = [
-  { settingWidth: 600, settingHeight: 600, targetWidth: 600, targetHeight: 600 },
-  { settingWidth: 599, settingHeight: 599, targetWidth: 600, targetHeight: 600 },
-  { settingWidth: 401, settingHeight: 501, targetWidth: 600, targetHeight: 600 },
-];
-
-add_task(async function setup() {
-  await SpecialPowers.pushPrefEnv({"set":
-    [["privacy.resistFingerprinting", true]]
-  });
-
-  // Calculate the popup window's chrome UI size for tests of outerWidth/Height.
-  let popUpChromeUISize = await calcPopUpWindowChromeUISize();
-
-  gPopupChromeUIWidth = popUpChromeUISize.chromeWidth;
-  gPopupChromeUIHeight = popUpChromeUISize.chromeHeight;
-
-  // Calculate the maximum available size.
-  let maxAvailSize = await calcMaximumAvailSize(gPopupChromeUIWidth,
-                                                gPopupChromeUIHeight);
-
-  gMaxAvailWidth = maxAvailSize.maxAvailWidth;
-  gMaxAvailHeight = maxAvailSize.maxAvailHeight;
-});
-
-add_task(async function test_window_open() {
-  // Open a tab to test window.open().
-  let tab = await BrowserTestUtils.openNewForegroundTab(
-    gBrowser, TEST_PATH + "file_dummy.html");
-
-  for (let test of TESTCASES) {
-    // Test 'width' and 'height' of window features.
-    await testWindowOpen(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                         test.targetWidth, test.targetHeight, false, gMaxAvailWidth,
-                         gMaxAvailHeight, gPopupChromeUIWidth, gPopupChromeUIHeight);
-
-    // test 'outerWidth' and 'outerHeight' of window features.
-    await testWindowOpen(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                         test.targetWidth, test.targetHeight, true, gMaxAvailWidth,
-                         gMaxAvailHeight, gPopupChromeUIWidth, gPopupChromeUIHeight);
-  }
-
-  await BrowserTestUtils.removeTab(tab);
-});
+OpenTest.run([
+  {settingWidth: 600, settingHeight: 600, targetWidth: 600, targetHeight: 600},
+  {settingWidth: 599, settingHeight: 599, targetWidth: 600, targetHeight: 600},
+  {settingWidth: 401, settingHeight: 501, targetWidth: 600, targetHeight: 600}
+]);
--- a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_min.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_min.js
@@ -1,61 +1,10 @@
 /*
  * Bug 1330882 - A test case for opening new windows through window.open() as
  *   rounded size when fingerprinting resistance is enabled. This test is for
  *   minimum values.
  */
 
-const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
-
-const TEST_DOMAIN = "http://example.net/";
-const TEST_PATH = TEST_DOMAIN + "browser/browser/components/resistfingerprinting/test/browser/";
-
-let gMaxAvailWidth;
-let gMaxAvailHeight;
-
-// We need the chrome UI size of popup windows for testing outerWidth/Height.
-let gPopupChromeUIWidth;
-let gPopupChromeUIHeight;
-
-const TESTCASES = [
-  { settingWidth: 199, settingHeight: 99, targetWidth: 200, targetHeight: 100 },
-  { settingWidth: 10, settingHeight: 10, targetWidth: 200, targetHeight: 100 },
-];
-
-add_task(async function setup() {
-  await SpecialPowers.pushPrefEnv({"set":
-    [["privacy.resistFingerprinting", true]]
-  });
-
-  // Calculate the popup window's chrome UI size for tests of outerWidth/Height.
-  let popUpChromeUISize = await calcPopUpWindowChromeUISize();
-
-  gPopupChromeUIWidth = popUpChromeUISize.chromeWidth;
-  gPopupChromeUIHeight = popUpChromeUISize.chromeHeight;
-
-  // Calculate the maximum available size.
-  let maxAvailSize = await calcMaximumAvailSize(gPopupChromeUIWidth,
-                                                gPopupChromeUIHeight);
-
-  gMaxAvailWidth = maxAvailSize.maxAvailWidth;
-  gMaxAvailHeight = maxAvailSize.maxAvailHeight;
-});
-
-add_task(async function test_window_open() {
-  // Open a tab to test window.open().
-  let tab = await BrowserTestUtils.openNewForegroundTab(
-    gBrowser, TEST_PATH + "file_dummy.html");
-
-  for (let test of TESTCASES) {
-    // Test 'width' and 'height' of window features.
-    await testWindowOpen(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                         test.targetWidth, test.targetHeight, false, gMaxAvailWidth,
-                         gMaxAvailHeight, gPopupChromeUIWidth, gPopupChromeUIHeight);
-
-    // test 'outerWidth' and 'outerHeight' of window features.
-    await testWindowOpen(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                         test.targetWidth, test.targetHeight, true, gMaxAvailWidth,
-                         gMaxAvailHeight, gPopupChromeUIWidth, gPopupChromeUIHeight);
-  }
-
-  await BrowserTestUtils.removeTab(tab);
-});
+OpenTest.run([
+  {settingWidth: 199, settingHeight: 99, targetWidth: 200, targetHeight: 100},
+  {settingWidth: 10, settingHeight: 10, targetWidth: 200, targetHeight: 100}
+]);
--- a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_windowSetting_max.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_windowSetting_max.js
@@ -1,67 +1,11 @@
 /*
  * Bug 1330882 - A test case for setting window size through window.innerWidth/Height
  *   and window.outerWidth/Height when fingerprinting resistance is enabled. This
  *   test is for maximum values.
  */
 
-const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
-
-const TEST_DOMAIN = "http://example.net/";
-const TEST_PATH = TEST_DOMAIN + "browser/browser/components/resistfingerprinting/test/browser/";
-
-let gMaxAvailWidth;
-let gMaxAvailHeight;
-
-// We need the chrome UI size of popup windows for testing outerWidth/Height.
-let gPopupChromeUIWidth;
-let gPopupChromeUIHeight;
-
-const TESTCASES = [
-  { settingWidth: 1025, settingHeight: 1050, targetWidth: 1000, targetHeight: 1000,
-    initWidth: 200, initHeight: 100 },
-  { settingWidth: 9999, settingHeight: 9999, targetWidth: 1000, targetHeight: 1000,
-    initWidth: 200, initHeight: 100  },
-  { settingWidth: 999, settingHeight: 999, targetWidth: 1000, targetHeight: 1000,
-    initWidth: 200, initHeight: 100  },
-];
-
-add_task(async function setup() {
-  await SpecialPowers.pushPrefEnv({"set":
-    [["privacy.resistFingerprinting", true]]
-  });
-
-  // Calculate the popup window's chrome UI size for tests of outerWidth/Height.
-  let popUpChromeUISize = await calcPopUpWindowChromeUISize();
-
-  gPopupChromeUIWidth = popUpChromeUISize.chromeWidth;
-  gPopupChromeUIHeight = popUpChromeUISize.chromeHeight;
-
-  // Calculate the maximum available size.
-  let maxAvailSize = await calcMaximumAvailSize(gPopupChromeUIWidth,
-                                                gPopupChromeUIHeight);
-
-  gMaxAvailWidth = maxAvailSize.maxAvailWidth;
-  gMaxAvailHeight = maxAvailSize.maxAvailHeight;
-});
-
-add_task(async function test_window_size_setting() {
-  // Open a tab to test.
-  let tab = await BrowserTestUtils.openNewForegroundTab(
-    gBrowser, TEST_PATH + "file_dummy.html");
-
-  for (let test of TESTCASES) {
-    // Test window.innerWidth and window.innerHeight.
-    await testWindowSizeSetting(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                                test.targetWidth, test.targetHeight, test.initWidth,
-                                test.initHeight, false, gMaxAvailWidth, gMaxAvailHeight,
-                                gPopupChromeUIWidth, gPopupChromeUIHeight);
-
-    // test window.outerWidth and window.outerHeight.
-    await testWindowSizeSetting(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                                test.targetWidth, test.targetHeight, test.initWidth,
-                                test.initHeight, true, gMaxAvailWidth, gMaxAvailHeight,
-                                gPopupChromeUIWidth, gPopupChromeUIHeight);
-  }
-
-  await BrowserTestUtils.removeTab(tab);
-});
+WindowSettingTest.run([
+  {settingWidth: 1025, settingHeight: 1050, targetWidth: 1000, targetHeight: 1000, initWidth: 200, initHeight: 100},
+  {settingWidth: 9999, settingHeight: 9999, targetWidth: 1000, targetHeight: 1000, initWidth: 200, initHeight: 100},
+  {settingWidth: 999, settingHeight: 999, targetWidth: 1000, targetHeight: 1000, initWidth: 200, initHeight: 100}
+]);
--- a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_windowSetting_mid.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_windowSetting_mid.js
@@ -1,67 +1,11 @@
 /*
  * Bug 1330882 - A test case for setting window size through window.innerWidth/Height
  *   and window.outerWidth/Height when fingerprinting resistance is enabled. This
  *   test is for middle values.
  */
 
-const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
-
-const TEST_DOMAIN = "http://example.net/";
-const TEST_PATH = TEST_DOMAIN + "browser/browser/components/resistfingerprinting/test/browser/";
-
-let gMaxAvailWidth;
-let gMaxAvailHeight;
-
-// We need the chrome UI size of popup windows for testing outerWidth/Height.
-let gPopupChromeUIWidth;
-let gPopupChromeUIHeight;
-
-const TESTCASES = [
-  { settingWidth: 600, settingHeight: 600, targetWidth: 600, targetHeight: 600,
-    initWidth: 200, initHeight: 100  },
-  { settingWidth: 599, settingHeight: 599, targetWidth: 600, targetHeight: 600,
-    initWidth: 200, initHeight: 100  },
-  { settingWidth: 401, settingHeight: 501, targetWidth: 600, targetHeight: 600,
-    initWidth: 200, initHeight: 100  },
-];
-
-add_task(async function setup() {
-  await SpecialPowers.pushPrefEnv({"set":
-    [["privacy.resistFingerprinting", true]]
-  });
-
-  // Calculate the popup window's chrome UI size for tests of outerWidth/Height.
-  let popUpChromeUISize = await calcPopUpWindowChromeUISize();
-
-  gPopupChromeUIWidth = popUpChromeUISize.chromeWidth;
-  gPopupChromeUIHeight = popUpChromeUISize.chromeHeight;
-
-  // Calculate the maximum available size.
-  let maxAvailSize = await calcMaximumAvailSize(gPopupChromeUIWidth,
-                                                gPopupChromeUIHeight);
-
-  gMaxAvailWidth = maxAvailSize.maxAvailWidth;
-  gMaxAvailHeight = maxAvailSize.maxAvailHeight;
-});
-
-add_task(async function test_window_size_setting() {
-  // Open a tab to test.
-  let tab = await BrowserTestUtils.openNewForegroundTab(
-    gBrowser, TEST_PATH + "file_dummy.html");
-
-  for (let test of TESTCASES) {
-    // Test window.innerWidth and window.innerHeight.
-    await testWindowSizeSetting(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                                test.targetWidth, test.targetHeight, test.initWidth,
-                                test.initHeight, false, gMaxAvailWidth, gMaxAvailHeight,
-                                gPopupChromeUIWidth, gPopupChromeUIHeight);
-
-    // test window.outerWidth and window.outerHeight.
-    await testWindowSizeSetting(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                                test.targetWidth, test.targetHeight, test.initWidth,
-                                test.initHeight, true, gMaxAvailWidth, gMaxAvailHeight,
-                                gPopupChromeUIWidth, gPopupChromeUIHeight);
-  }
-
-  await BrowserTestUtils.removeTab(tab);
-});
+WindowSettingTest.run([
+  {settingWidth: 600, settingHeight: 600, targetWidth: 600, targetHeight: 600, initWidth: 200, initHeight: 100},
+  {settingWidth: 599, settingHeight: 599, targetWidth: 600, targetHeight: 600, initWidth: 200, initHeight: 100},
+  {settingWidth: 401, settingHeight: 501, targetWidth: 600, targetHeight: 600, initWidth: 200, initHeight: 100}
+]);
--- a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_windowSetting_min.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_windowSetting_min.js
@@ -1,65 +1,10 @@
 /*
  * Bug 1330882 - A test case for setting window size through window.innerWidth/Height
  *   and window.outerWidth/Height when fingerprinting resistance is enabled. This
  *   test is for minimum values.
  */
 
-const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
-
-const TEST_DOMAIN = "http://example.net/";
-const TEST_PATH = TEST_DOMAIN + "browser/browser/components/resistfingerprinting/test/browser/";
-
-let gMaxAvailWidth;
-let gMaxAvailHeight;
-
-// We need the chrome UI size of popup windows for testing outerWidth/Height.
-let gPopupChromeUIWidth;
-let gPopupChromeUIHeight;
-
-const TESTCASES = [
-  { settingWidth: 199, settingHeight: 99, targetWidth: 200, targetHeight: 100,
-    initWidth: 1000, initHeight: 1000  },
-  { settingWidth: 10, settingHeight: 10, targetWidth: 200, targetHeight: 100,
-    initWidth: 1000, initHeight: 1000  },
-];
-
-add_task(async function setup() {
-  await SpecialPowers.pushPrefEnv({"set":
-    [["privacy.resistFingerprinting", true]]
-  });
-
-  // Calculate the popup window's chrome UI size for tests of outerWidth/Height.
-  let popUpChromeUISize = await calcPopUpWindowChromeUISize();
-
-  gPopupChromeUIWidth = popUpChromeUISize.chromeWidth;
-  gPopupChromeUIHeight = popUpChromeUISize.chromeHeight;
-
-  // Calculate the maximum available size.
-  let maxAvailSize = await calcMaximumAvailSize(gPopupChromeUIWidth,
-                                                gPopupChromeUIHeight);
-
-  gMaxAvailWidth = maxAvailSize.maxAvailWidth;
-  gMaxAvailHeight = maxAvailSize.maxAvailHeight;
-});
-
-add_task(async function test_window_size_setting() {
-  // Open a tab to test.
-  let tab = await BrowserTestUtils.openNewForegroundTab(
-    gBrowser, TEST_PATH + "file_dummy.html");
-
-  for (let test of TESTCASES) {
-    // Test window.innerWidth and window.innerHeight.
-    await testWindowSizeSetting(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                                test.targetWidth, test.targetHeight, test.initWidth,
-                                test.initHeight, false, gMaxAvailWidth, gMaxAvailHeight,
-                                gPopupChromeUIWidth, gPopupChromeUIHeight);
-
-    // test window.outerWidth and window.outerHeight.
-    await testWindowSizeSetting(tab.linkedBrowser, test.settingWidth, test.settingHeight,
-                                test.targetWidth, test.targetHeight, test.initWidth,
-                                test.initHeight, true, gMaxAvailWidth, gMaxAvailHeight,
-                                gPopupChromeUIWidth, gPopupChromeUIHeight);
-  }
-
-  await BrowserTestUtils.removeTab(tab);
-});
+WindowSettingTest.run([
+  {settingWidth: 199, settingHeight: 99, targetWidth: 200, targetHeight: 100, initWidth: 1000, initHeight: 1000},
+  {settingWidth: 10, settingHeight: 10, targetWidth: 200, targetHeight: 100, initWidth: 1000, initHeight: 1000}
+]);
--- a/browser/components/resistfingerprinting/test/browser/head.js
+++ b/browser/components/resistfingerprinting/test/browser/head.js
@@ -223,8 +223,93 @@ async function testWindowSizeSetting(aBr
           win.innerHeight = input.settingHeight;
         }
       });
 
       win.close();
     }
   );
 }
+
+class RoundedWindowTest {
+  // testOuter is optional.  run() can be invoked with only 1 parameter.
+  static run(testCases, testOuter) {
+    // "this" is the calling class itself.
+    // e.g. when invoked by RoundedWindowTest.run(), "this" is "class RoundedWindowTest".
+    let test = new this(testCases);
+    add_task(async () => test.setup());
+    add_task(async () => {
+      if (testOuter == undefined) {
+        // If testOuter is not given, do tests for both inner and outer.
+        await test.doTests(false);
+        await test.doTests(true);
+      } else {
+        await test.doTests(testOuter);
+      }
+    });
+  }
+
+  get TEST_PATH() {
+    return "http://example.net/browser/browser/components/resistfingerprinting/test/browser/";
+  }
+
+  constructor(testCases) {
+    this.testCases = testCases;
+  }
+
+  async setup() {
+    await SpecialPowers.pushPrefEnv({"set":
+      [["privacy.resistFingerprinting", true]]
+    });
+
+    // Calculate the popup window's chrome UI size for tests of outerWidth/Height.
+    let popUpChromeUISize = await calcPopUpWindowChromeUISize();
+
+    this.popupChromeUIWidth = popUpChromeUISize.chromeWidth;
+    this.popupChromeUIHeight = popUpChromeUISize.chromeHeight;
+
+    // Calculate the maximum available size.
+    let maxAvailSize = await calcMaximumAvailSize(this.popupChromeUIWidth,
+                                                  this.popupChromeUIHeight);
+
+    this.maxAvailWidth = maxAvailSize.maxAvailWidth;
+    this.maxAvailHeight = maxAvailSize.maxAvailHeight;
+  }
+
+  async doTests(testOuter) {
+    // Open a tab to test.
+    this.tab = await BrowserTestUtils.openNewForegroundTab(
+      gBrowser, this.TEST_PATH + "file_dummy.html");
+
+    for (let test of this.testCases) {
+      await this.doTest(test, testOuter);
+    }
+
+    await BrowserTestUtils.removeTab(this.tab);
+  }
+
+  async doTest() {
+    throw new Error("RoundedWindowTest.doTest must be overridden.");
+  }
+}
+
+class WindowSettingTest extends RoundedWindowTest {
+  async doTest(test, testOuter) {
+    await testWindowSizeSetting(this.tab.linkedBrowser,
+                                test.settingWidth, test.settingHeight,
+                                test.targetWidth, test.targetHeight,
+                                test.initWidth, test.initHeight,
+                                testOuter,
+                                this.maxAvailWidth, this.maxAvailHeight,
+                                this.popupChromeUIWidth, this.popupChromeUIHeight);
+  }
+}
+
+class OpenTest extends RoundedWindowTest {
+  async doTest(test, testOuter) {
+    await testWindowOpen(this.tab.linkedBrowser,
+                         test.settingWidth, test.settingHeight,
+                         test.targetWidth, test.targetHeight,
+                         testOuter,
+                         this.maxAvailWidth, this.maxAvailHeight,
+                         this.popupChromeUIWidth, this.popupChromeUIHeight);
+  }
+}
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -200,16 +200,17 @@ These should match what Safari and other
 <!ENTITY printButton.label            "Print">
 <!ENTITY printButton.tooltip          "Print this page">
 
 <!ENTITY urlbar.viewSiteInfo.label                      "View site information">
 
 <!ENTITY urlbar.defaultNotificationAnchor.tooltip         "Open message panel">
 <!ENTITY urlbar.geolocationNotificationAnchor.tooltip     "Open location request panel">
 <!ENTITY urlbar.addonsNotificationAnchor.tooltip          "Open add-on installation message panel">
+<!ENTITY urlbar.canvasNotificationAnchor.tooltip          "Manage canvas extraction permission">
 <!ENTITY urlbar.indexedDBNotificationAnchor.tooltip       "Open offline storage message panel">
 <!ENTITY urlbar.passwordNotificationAnchor.tooltip        "Open save password message panel">
 <!ENTITY urlbar.pluginsNotificationAnchor.tooltip         "Manage plug-in use">
 <!ENTITY urlbar.webNotificationAnchor.tooltip             "Change whether you can receive notifications from the site">
 <!ENTITY urlbar.persistentStorageNotificationAnchor.tooltip     "Store data in Persistent Storage">
 <!ENTITY urlbar.remoteControlNotificationAnchor.tooltip   "Browser is under remote control">
 
 <!ENTITY urlbar.webRTCShareDevicesNotificationAnchor.tooltip      "Manage sharing your camera and/or microphone with the site">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -462,16 +462,25 @@ offlineApps.allowStoring.label=Allow Sto
 offlineApps.allowStoring.accesskey=A
 offlineApps.dontAllow.label=Don’t Allow
 offlineApps.dontAllow.accesskey=n
 
 offlineApps.usage=This website (%S) is now storing more than %SMB of data on your computer for offline use.
 offlineApps.manageUsage=Show settings
 offlineApps.manageUsageAccessKey=S
 
+# Canvas permission prompt
+# LOCALIZATION NOTE (canvas.siteprompt): %S is hostname
+canvas.siteprompt=Will you allow %S to use your HTML5 canvas image data? This may be used to uniquely identify your computer.
+canvas.notAllow=Don’t Allow
+canvas.notAllow.accesskey=n
+canvas.allow=Allow Data Access
+canvas.allow.accesskey=A
+canvas.remember=Always remember my decision
+
 identity.identified.verifier=Verified by: %S
 identity.identified.verified_by_you=You have added a security exception for this site.
 identity.identified.state_and_country=%S, %S
 
 identity.icon.tooltip=Show site information
 identity.extension.label=Extension (%S)
 identity.extension.tooltip=Loaded by extension: %S
 identity.showDetails.tooltip=Show connection details
--- a/browser/themes/linux/places/places.css
+++ b/browser/themes/linux/places/places.css
@@ -46,16 +46,21 @@
   border: 0;
   background: transparent;
 }
 
 .sidebar-placesTreechildren::-moz-tree-row {
   min-height: 24px;
 }
 
+:root[uidensity=touch] #search-box,
+:root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
+  min-height: 32px;
+}
+
 .sidebar-placesTreechildren::-moz-tree-cell(leaf) ,
 .sidebar-placesTreechildren::-moz-tree-image(leaf) {
   cursor: pointer;
 }
 
 .sidebar-placesTreechildren::-moz-tree-cell-text(leaf, hover) {
   cursor: pointer;
   text-decoration: underline;
--- a/browser/themes/osx/places/places.css
+++ b/browser/themes/osx/places/places.css
@@ -14,16 +14,20 @@
 .sidebar-placesTreechildren::-moz-tree-row {
   padding-bottom: 1px;
   margin: 0;
   height: 24px;
   /* Default font size is 11px on mac, so this is 12px */
   font-size: 1.0909rem;
 }
 
+:root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
+  min-height: 32px;
+}
+
 .sidebar-placesTree {
   -moz-appearance: -moz-mac-source-list;
   -moz-font-smoothing-background-color: -moz-mac-source-list;
 }
 
 .sidebar-placesTreechildren::-moz-tree-separator {
   border-top: 1px solid #505d6d;
   margin: 0 10px;
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -56,16 +56,17 @@
   skin/classic/browser/identity-icon-notice.svg                (../shared/identity-block/identity-icon-notice.svg)
   skin/classic/browser/info.svg                                (../shared/info.svg)
   skin/classic/browser/searchReset.css                         (../shared/searchReset.css)
 
   skin/classic/browser/illustrations/error-session-restore.svg (../shared/illustrations/error-session-restore.svg)
 
   skin/classic/browser/notification-icons/camera-blocked.svg                (../shared/notification-icons/camera-blocked.svg)
   skin/classic/browser/notification-icons/camera.svg                        (../shared/notification-icons/camera.svg)
+  skin/classic/browser/notification-icons/canvas.svg                        (../shared/notification-icons/canvas.svg)
   skin/classic/browser/notification-icons/default-info.svg                  (../shared/notification-icons/default-info.svg)
   skin/classic/browser/notification-icons/desktop-notification-blocked.svg  (../shared/notification-icons/desktop-notification-blocked.svg)
   skin/classic/browser/notification-icons/desktop-notification.svg          (../shared/notification-icons/desktop-notification.svg)
   skin/classic/browser/notification-icons/focus-tab-by-prompt.svg           (../shared/notification-icons/focus-tab-by-prompt.svg)
   skin/classic/browser/notification-icons/indexedDB-blocked.svg             (../shared/notification-icons/indexedDB-blocked.svg)
   skin/classic/browser/notification-icons/indexedDB.svg                     (../shared/notification-icons/indexedDB.svg)
   skin/classic/browser/notification-icons/login-detailed.svg                (../shared/notification-icons/login-detailed.svg)
   skin/classic/browser/notification-icons/login.svg                         (../shared/notification-icons/login.svg)
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -112,16 +112,21 @@
 .screen-icon.in-use {
   list-style-image: url(chrome://browser/skin/notification-icons/screen.svg);
 }
 
 .screen-icon.blocked-permission-icon {
   list-style-image: url(chrome://browser/skin/notification-icons/screen-blocked.svg);
 }
 
+#canvas-notification-icon,
+.popup-notification-icon[popupid="canvas-permissions-prompt"] {
+  list-style-image: url(chrome://browser/skin/notification-icons/canvas.svg);
+}
+
 #webRTC-preview:not([hidden]) {
   display: -moz-stack;
   border-radius: 4px;
   border: 1px solid GrayText;
   overflow: hidden;
   min-width: 300px;
   min-height: 10em;
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/notification-icons/canvas.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 481.156 508.687" fill="context-fill" fill-opacity="context-fill-opacity">
+  <path d="M477.656 289.656c-36.133 224.615-220.16 222.188-283 218C22.22 496.163-68.584 254.586 61.98 128.066l105.857 105.872c-7.183 24.6 2.7 54.1 42.418 70.59 42.865 17.8 87.4 9.747 87.4 9.747s-14.08-22.03-15.565-68.266c-1.1-34.2-40.15-55.996-72.513-50.483L108.757 94.69c30.6-12.34 59.033-1.8 69.9 6.966 26.87 21.688 16.616 68.436 53 54 70.87-28.12-40.744-132.32 53-154 79.026-18.278 220.516 116.945 193 288zm-371-14a41 41 0 1 0 41 41 41 41 0 0 0-41-41zm149.5 92a43.5 43.5 0 1 0 43.5 43.5 43.5 43.5 0 0 0-43.5-43.5zm97.5-273a40 40 0 1 0 40 40 40 40 0 0 0-40-40zm24.5 141a45.5 45.5 0 1 0 45.5 45.5 45.5 45.5 0 0 0-45.5-45.5z"/>
+  <path d="M213.656 296.656c-36.083-15.022-42.678-42.92-33.52-64.423L35.847 87.925a17.732 17.732 0 0 1 25.076-25.078L205.45 207.393c28.076-10.037 67.206 8.853 68.206 40.263 1.24 38.716 13 58 13 58s-37.2 5.905-73-9z"/>
+</svg>
--- a/browser/themes/windows/places/places.css
+++ b/browser/themes/windows/places/places.css
@@ -16,16 +16,21 @@
 .sidebar-placesTree {
   -moz-appearance: none;
   background-color: transparent;
   color: inherit;
   border: 0;
   margin: 0;
 }
 
+:root[uidensity=touch] #search-box,
+:root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
+  min-height: 32px;
+}
+
 .sidebar-placesTreechildren::-moz-tree-cell,
 .sidebar-placesTreechildren::-moz-tree-twisty {
   padding: 0 4px;
 }
 
 .sidebar-placesTreechildren::-moz-tree-cell(leaf) ,
 .sidebar-placesTreechildren::-moz-tree-image(leaf) {
   cursor: pointer;
--- a/devtools/client/framework/browser-menus.js
+++ b/devtools/client/framework/browser-menus.js
@@ -71,17 +71,17 @@ function createToolMenuElements(toolDefi
 
   // Prevent multiple entries for the same tool.
   if (doc.getElementById(menuId)) {
     return;
   }
 
   let oncommand = function (id, event) {
     let window = event.target.ownerDocument.defaultView;
-    gDevToolsBrowser.selectToolCommand(window.gBrowser, id);
+    gDevToolsBrowser.selectToolCommand(window.gBrowser, id, window.performance.now());
   }.bind(null, id);
 
   let menuitem = createMenuItem({
     doc,
     id: "menuitem_" + id,
     label: toolDefinition.menuLabel || toolDefinition.label,
     accesskey: toolDefinition.accesskey
   });
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -68,25 +68,29 @@ var gDevToolsBrowser = exports.gDevTools
   },
 
   /**
    * This function is for the benefit of Tools:DevToolbox in
    * browser/base/content/browser-sets.inc and should not be used outside
    * of there
    */
   // used by browser-sets.inc, command
-  toggleToolboxCommand(gBrowser) {
+  toggleToolboxCommand(gBrowser, startTime) {
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     let toolbox = gDevTools.getToolbox(target);
 
     // If a toolbox exists, using toggle from the Main window :
     // - should close a docked toolbox
     // - should focus a windowed toolbox
     let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
-    isDocked ? gDevTools.closeToolbox(target) : gDevTools.showToolbox(target);
+    if (isDocked) {
+      gDevTools.closeToolbox(target);
+    } else {
+      gDevTools.showToolbox(target, null, null, null, startTime);
+    }
   },
 
   /**
    * This function ensures the right commands are enabled in a window,
    * depending on their relevant prefs. It gets run when a window is registered,
    * or when any of the devtools prefs change.
    */
   updateCommandAvailability(win) {
@@ -211,17 +215,17 @@ var gDevToolsBrowser = exports.gDevTools
    *   we select it
    * - if the toolbox is open, and the targeted tool is selected,
    *   and the host is NOT a window, we close the toolbox
    * - if the toolbox is open, and the targeted tool is selected,
    *   and the host is a window, we raise the toolbox window
    */
   // Used when: - registering a new tool
   //            - new xul window, to add menu items
-  selectToolCommand(gBrowser, toolId) {
+  selectToolCommand(gBrowser, toolId, startTime) {
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     let toolbox = gDevTools.getToolbox(target);
     let toolDefinition = gDevTools.getToolDefinition(toolId);
 
     if (toolbox &&
         (toolbox.currentToolId == toolId ||
           (toolId == "webconsole" && toolbox.splitConsole))) {
       toolbox.fireCustomKey(toolId);
@@ -229,17 +233,17 @@ var gDevToolsBrowser = exports.gDevTools
       if (toolDefinition.preventClosingOnKey ||
           toolbox.hostType == Toolbox.HostType.WINDOW) {
         toolbox.raise();
       } else {
         gDevTools.closeToolbox(target);
       }
       gDevTools.emit("select-tool-command", toolId);
     } else {
-      gDevTools.showToolbox(target, toolId).then(newToolbox => {
+      gDevTools.showToolbox(target, toolId, null, null, startTime).then(newToolbox => {
         newToolbox.fireCustomKey(toolId);
         gDevTools.emit("select-tool-command", toolId);
       });
     }
   },
 
   /**
    * Called by devtools/client/devtools-startup.js when a key shortcut is pressed
@@ -248,28 +252,31 @@ var gDevToolsBrowser = exports.gDevTools
    *         The top level browser window from which the key shortcut is pressed.
    * @param  {Object} key
    *         Key object describing the key shortcut being pressed. It comes
    *         from devtools-startup.js's KeyShortcuts array. The useful fields here
    *         are:
    *         - `toolId` used to identify a toolbox's panel like inspector or webconsole,
    *         - `id` used to identify any other key shortcuts like scratchpad or
    *         about:debugging
+   * @param {Number} startTime
+   *        Optional, indicates the time at which the key event fired. This is a
+   *        `performance.now()` timing.
    */
-  onKeyShortcut(window, key) {
+  onKeyShortcut(window, key, startTime) {
     // If this is a toolbox's panel key shortcut, delegate to selectToolCommand
     if (key.toolId) {
-      gDevToolsBrowser.selectToolCommand(window.gBrowser, key.toolId);
+      gDevToolsBrowser.selectToolCommand(window.gBrowser, key.toolId, startTime);
       return;
     }
     // Otherwise implement all other key shortcuts individually here
     switch (key.id) {
       case "toggleToolbox":
       case "toggleToolboxF12":
-        gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
+        gDevToolsBrowser.toggleToolboxCommand(window.gBrowser, startTime);
         break;
       case "toggleToolbar":
         gDevToolsBrowser.getDeveloperToolbar(window).focusToggle();
         break;
       case "webide":
         gDevToolsBrowser.openWebIDE();
         break;
       case "browserToolbox":
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -417,36 +417,45 @@ DevTools.prototype = {
     }
 
     if (browserConsole && !HUDService.getBrowserConsole()) {
       HUDService.toggleBrowserConsole();
     }
   },
 
   /**
+   * Boolean, true, if we never opened a toolbox.
+   * Used to implement the telemetry tracking toolbox opening.
+   */
+  _firstShowToolbox: true,
+
+  /**
    * Show a Toolbox for a target (either by creating a new one, or if a toolbox
    * already exists for the target, by bring to the front the existing one)
    * If |toolId| is specified then the displayed toolbox will have the
    * specified tool selected.
    * If |hostType| is specified then the toolbox will be displayed using the
    * specified HostType.
    *
    * @param {Target} target
    *         The target the toolbox will debug
    * @param {string} toolId
    *        The id of the tool to show
    * @param {Toolbox.HostType} hostType
    *        The type of host (bottom, window, side)
    * @param {object} hostOptions
    *        Options for host specifically
+   * @param {Number} startTime
+   *        Optional, indicates the time at which the user event related to this toolbox
+   *        opening started. This is a `performance.now()` timing.
    *
    * @return {Toolbox} toolbox
    *        The toolbox that was opened
    */
-  showToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
+  showToolbox: Task.async(function* (target, toolId, hostType, hostOptions, startTime) {
     let toolbox = this._toolboxes.get(target);
     if (toolbox) {
       if (hostType != null && toolbox.hostType != hostType) {
         yield toolbox.switchHost(hostType);
       }
 
       if (toolId != null && toolbox.currentToolId != toolId) {
         yield toolbox.selectTool(toolId);
@@ -460,20 +469,47 @@ DevTools.prototype = {
       let promise = this._creatingToolboxes.get(target);
       if (promise) {
         return yield promise;
       }
       let toolboxPromise = this.createToolbox(target, toolId, hostType, hostOptions);
       this._creatingToolboxes.set(target, toolboxPromise);
       toolbox = yield toolboxPromise;
       this._creatingToolboxes.delete(target);
+
+      if (startTime) {
+        this.logToolboxOpenTime(toolbox.currentToolId, startTime);
+      }
+      this._firstShowToolbox = false;
     }
     return toolbox;
   }),
 
+  /**
+   * Log telemetry related to toolbox opening.
+   * Two distinct probes are logged. One for cold startup, when we open the very first
+   * toolbox. This one includes devtools framework loading. And a second one for all
+   * subsequent toolbox opening, which should all be faster.
+   * These two probes are indexed by Tool ID.
+   *
+   * @param {String} toolId
+   *        The id of the opened tool.
+   * @param {Number} startTime
+   *        Indicates the time at which the user event related to the toolbox
+   *        opening started. This is a `performance.now()` timing.
+   */
+  logToolboxOpenTime(toolId, startTime) {
+    let { performance } = Services.appShell.hiddenDOMWindow;
+    let delay = performance.now() - startTime;
+    let telemetryKey = this._firstShowToolbox ?
+      "DEVTOOLS_COLD_TOOLBOX_OPEN_DELAY_MS" : "DEVTOOLS_WARM_TOOLBOX_OPEN_DELAY_MS";
+    let histogram = Services.telemetry.getKeyedHistogramById(telemetryKey);
+    histogram.add(toolId, delay);
+  },
+
   createToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
     let manager = new ToolboxHostManager(target, hostType, hostOptions);
 
     let toolbox = yield manager.create(toolId);
 
     this._toolboxes.set(target, toolbox);
 
     this.emit("toolbox-created", toolbox);
@@ -570,23 +606,26 @@ DevTools.prototype = {
    * Called from the DevToolsShim, used by nsContextMenu.js.
    *
    * @param {XULTab} tab
    *        The browser tab on which inspect node was used.
    * @param {Array} selectors
    *        An array of CSS selectors to find the target node. Several selectors can be
    *        needed if the element is nested in frames and not directly in the root
    *        document.
+   * @param {Number} startTime
+   *        Optional, indicates the time at which the user event related to this node
+   *        inspection started. This is a `performance.now()` timing.
    * @return {Promise} a promise that resolves when the node is selected in the inspector
    *         markup view.
    */
-  async inspectNode(tab, nodeSelectors) {
+  async inspectNode(tab, nodeSelectors, startTime) {
     let target = TargetFactory.forTab(tab);
 
-    let toolbox = await gDevTools.showToolbox(target, "inspector");
+    let toolbox = await gDevTools.showToolbox(target, "inspector", null, null, startTime);
     let inspector = toolbox.getCurrentPanel();
 
     // new-node-front tells us when the node has been selected, whether the
     // browser is remote or not.
     let onNewNode = inspector.selection.once("new-node-front");
 
     // Evaluate the cross iframes query selectors
     async function querySelectors(nodeFront) {
--- a/devtools/client/menus.js
+++ b/devtools/client/menus.js
@@ -36,17 +36,17 @@ loader.lazyRequireGetter(this, "Responsi
 loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm");
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
 exports.menuitems = [
   { id: "menu_devToolbox",
     l10nKey: "devToolboxMenuItem",
     oncommand(event) {
       let window = event.target.ownerDocument.defaultView;
-      gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
+      gDevToolsBrowser.toggleToolboxCommand(window.gBrowser, window.performance.now());
     },
     keyId: "toggleToolbox",
     checkbox: true
   },
   { id: "menu_devtools_separator",
     separator: true },
   { id: "menu_devToolbar",
     l10nKey: "devToolbarMenu",
--- a/devtools/client/webconsole/jsterm.js
+++ b/devtools/client/webconsole/jsterm.js
@@ -990,16 +990,17 @@ JSTerm.prototype = {
     this.webConsoleClient.clearNetworkRequests();
     if (clearStorage) {
       this.webConsoleClient.clearMessagesCache();
     }
     this._sidebarDestroy();
     this.focus();
     this.emit("messages-cleared");
   },
+
   /**
    * Remove all of the private messages from the Web Console output.
    *
    * This method emits the "private-messages-cleared" notification.
    */
   clearPrivateMessages: function () {
     let nodes = this.hud.outputNode.querySelectorAll(".message[private]");
     for (let node of nodes) {
@@ -1768,17 +1769,23 @@ JSTerm.prototype = {
 
   /**
    * Destroy the JSTerm object. Call this method to avoid memory leaks.
    */
   destroy: function () {
     this._sidebarDestroy();
 
     this.clearCompletion();
-    this.clearOutput();
+
+    if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+      this.webConsoleClient.clearNetworkRequests();
+      this.hud.outputNode.innerHTML = "";
+    } else {
+      this.clearOutput();
+    }
 
     this.autocompletePopup.destroy();
     this.autocompletePopup = null;
 
     if (this._onPaste) {
       this.inputNode.removeEventListener("paste", this._onPaste);
       this.inputNode.removeEventListener("drop", this._onPaste);
       this._onPaste = null;
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -201,21 +201,20 @@ skip-if = true
 subsuite = clipboard
 # old console skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_jsterm_dollar.js]
 [browser_jsterm_history_persist.js]
 skip-if = true # Bug 1401881
 [browser_jsterm_inspect.js]
 [browser_jsterm_no_autocompletion_on_defined_variables.js]
 skip-if = true # Bug 1401881
+[browser_jsterm_no_input_and_tab_key_pressed.js]
 [browser_netmonitor_shows_reqs_in_webconsole.js]
 [browser_webconsole.js]
 skip-if = true #	Bug 1404829
-[browser_webconsole_No_input_and_Tab_key_pressed.js]
-skip-if = true #	Bug 1403910
 [browser_webconsole_No_input_change_and_Tab_key_pressed.js]
 skip-if = true #	Bug 1404882
 [browser_webconsole_add_edited_input_to_history.js]
 skip-if = true # Bug 1401881
 [browser_webconsole_allow_mixedcontent_securityerrors.js]
 tags = mcb
 skip-if = true #	Bug 1403452
 # old console skip-if = (os == 'win' && bits == 64) # Bug 1390001
rename from devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_No_input_and_Tab_key_pressed.js
rename to devtools/client/webconsole/new-console-output/test/mochitest/browser_jsterm_no_input_and_tab_key_pressed.js
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_No_input_and_Tab_key_pressed.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_jsterm_no_input_and_tab_key_pressed.js
@@ -2,36 +2,33 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // See Bug 583816.
 
-const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
-                 "test/browser/test-console.html";
+const TEST_URI = "data:text/html,Testing jsterm with no input";
 
 add_task(function* () {
-  yield loadTab(TEST_URI);
-
-  let hud = yield openConsole();
+  let hud = yield openNewTabAndConsole(TEST_URI);
   testCompletion(hud);
 });
 
 function testCompletion(hud) {
   let jsterm = hud.jsterm;
   let input = jsterm.inputNode;
 
   jsterm.setInputValue("");
   EventUtils.synthesizeKey("VK_TAB", {});
   is(jsterm.completeNode.value, "<- no result", "<- no result - matched");
   is(input.value, "", "inputnode is empty - matched");
-  is(input.getAttribute("focused"), "true", "input is still focused");
+  ok(hasFocus(input), "input is still focused");
 
   // Any thing which is not in property autocompleter
   jsterm.setInputValue("window.Bug583816");
   EventUtils.synthesizeKey("VK_TAB", {});
   is(jsterm.completeNode.value, "                <- no result",
      "completenode content - matched");
   is(input.value, "window.Bug583816", "inputnode content - matched");
-  is(input.getAttribute("focused"), "true", "input is still focused");
+  ok(hasFocus(input), "input is still focused");
 }
--- a/devtools/shim/DevToolsShim.jsm
+++ b/devtools/shim/DevToolsShim.jsm
@@ -155,22 +155,28 @@ this.DevToolsShim = {
    * @param {Array} selectors
    *        An array of CSS selectors to find the target node. Several selectors can be
    *        needed if the element is nested in frames and not directly in the root
    *        document.
    * @return {Promise} a promise that resolves when the node is selected in the inspector
    *         markup view or that resolves immediately if DevTools are not installed.
    */
   inspectNode: function (tab, selectors) {
+    // Record the timing at which this event started in order to compute later in
+    // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+    // i.e. especially take `DevtoolsStartup.initDevTools` into account.
+    let { performance } = Services.appShell.hiddenDOMWindow;
+    let startTime = performance.now();
+
     let devtoolsReady = this._maybeInitializeDevTools("ContextMenu");
     if (!devtoolsReady) {
       return Promise.resolve();
     }
 
-    return this._gDevTools.inspectNode(tab, selectors);
+    return this._gDevTools.inspectNode(tab, selectors, startTime);
   },
 
   _onDevToolsRegistered: function () {
     // Register all pending event listeners on the real gDevTools object.
     for (let [event, listener] of this.listeners) {
       this._gDevTools.on(event, listener);
     }
 
--- a/devtools/shim/aboutdevtools/aboutdevtools.xhtml
+++ b/devtools/shim/aboutdevtools/aboutdevtools.xhtml
@@ -1,16 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 <!DOCTYPE html [
 <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
 <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
-<!ENTITY % aboutdevtoolsDTD SYSTEM "chrome://devtools-shim/locale/aboutdevtools.dtd"> %aboutdevtoolsDTD;
+<!ENTITY % aboutdevtoolsDTD SYSTEM "chrome://devtools-shim/content/aboutdevtools/tmp-locale/aboutdevtools.dtd"> %aboutdevtoolsDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
 <head>
   <title>&aboutDevtools.headTitle;</title>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>a
   <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://devtools-shim/content/aboutdevtools/aboutdevtools.css"  type="text/css"/>
rename from devtools/shim/locales/en-US/aboutdevtools.dtd
rename to devtools/shim/aboutdevtools/tmp-locale/aboutdevtools.dtd
--- a/devtools/shim/locales/en-US/aboutdevtools.dtd
+++ b/devtools/shim/aboutdevtools/tmp-locale/aboutdevtools.dtd
@@ -1,8 +1,12 @@
+<!-- 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/. -->
+
 <!ENTITY  aboutDevtools.headTitle "About Developer Tools">
 <!ENTITY  aboutDevtools.enable.title "Enable Firefox Developer Tools">
 <!ENTITY  aboutDevtools.enable.inspectElementTitle "Enable Firefox Developer Tools to use Inspect Element">
 
 <!ENTITY  aboutDevtools.enable.aboutDebuggingMessage
           "Develop and debug WebExtensions, web workers, service workers and more with the Firefox DevTools.">
 <!ENTITY  aboutDevtools.enable.inspectElementMessage
           "Examine and edit HTML and CSS with the DevTools Inspector.">
--- a/devtools/shim/devtools-startup.js
+++ b/devtools/shim/devtools-startup.js
@@ -444,21 +444,25 @@ DevToolsStartup.prototype = {
     // Appending a <key> element is not always enough. The <keyset> needs
     // to be detached and reattached to make sure the <key> is taken into
     // account (see bug 832984).
     let mainKeyset = doc.getElementById("mainKeyset");
     mainKeyset.parentNode.insertBefore(keyset, mainKeyset);
   },
 
   onKey(window, key) {
+    // Record the timing at which this event started in order to compute later in
+    // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+    // i.e. especially take `initDevTools` into account.
+    let startTime = window.performance.now();
     let require = this.initDevTools("KeyShortcut");
     if (require) {
       // require might be null if initDevTools was called while DevTools are disabled.
       let { gDevToolsBrowser } = require("devtools/client/framework/devtools-browser");
-      gDevToolsBrowser.onKeyShortcut(window, key);
+      gDevToolsBrowser.onKeyShortcut(window, key, startTime);
     }
   },
 
   // Create a <xul:key> DOM Element
   createKey(doc, { id, toolId, shortcut, modifiers: mod }, oncommand) {
     let k = doc.createElement("key");
     k.id = "key_" + (id || toolId);
 
--- a/devtools/shim/jar.mn
+++ b/devtools/shim/jar.mn
@@ -5,16 +5,20 @@
 devtools-shim.jar:
 %   content devtools-shim %content/
     content/aboutdevtools/aboutdevtools.xhtml  (aboutdevtools/aboutdevtools.xhtml)
     content/aboutdevtools/aboutdevtools.css (aboutdevtools/aboutdevtools.css)
     content/aboutdevtools/aboutdevtools.js (aboutdevtools/aboutdevtools.js)
 
     content/aboutdevtools/images/otter.png (aboutdevtools/images/otter.png)
 
+    # Temporary localisation file, move back to devtools/shim/locales/en-US when ready for localization
+    # See https://bugzilla.mozilla.org/show_bug.cgi?id=1408369
+    content/aboutdevtools/tmp-locale/aboutdevtools.dtd (aboutdevtools/tmp-locale/aboutdevtools.dtd)
+
     content/aboutdevtools/images/feature-inspector.svg (aboutdevtools/images/feature-inspector.svg)
     content/aboutdevtools/images/feature-console.svg (aboutdevtools/images/feature-console.svg)
     content/aboutdevtools/images/feature-debugger.svg (aboutdevtools/images/feature-debugger.svg)
     content/aboutdevtools/images/feature-network.svg (aboutdevtools/images/feature-network.svg)
     content/aboutdevtools/images/feature-memory.svg (aboutdevtools/images/feature-memory.svg)
     content/aboutdevtools/images/feature-visual-editing.svg (aboutdevtools/images/feature-visual-editing.svg)
     content/aboutdevtools/images/feature-responsive-mode.svg (aboutdevtools/images/feature-responsive-mode.svg)
     content/aboutdevtools/images/feature-storage.svg (aboutdevtools/images/feature-storage.svg)
--- a/dom/animation/AnimationEffectReadOnly.cpp
+++ b/dom/animation/AnimationEffectReadOnly.cpp
@@ -195,30 +195,28 @@ AnimationEffectReadOnly::GetComputedTimi
 
   // Convert the overall progress to a fraction of a single iteration--the
   // simply iteration progress.
   // https://w3c.github.io/web-animations/#simple-iteration-progress
   double progress = IsFinite(overallProgress)
                     ? fmod(overallProgress, 1.0)
                     : fmod(result.mIterationStart, 1.0);
 
-  // When we finish exactly at the end of an iteration we need to report
-  // the end of the final iteration and not the start of the next iteration.
-  // We *don't* want to do this when we have a zero-iteration animation or
-  // when the animation has been effectively made into a zero-duration animation
-  // using a negative end-delay, however.
-  if (result.mPhase == ComputedTiming::AnimationPhase::After &&
-      progress == 0.0 &&
-      result.mIterations != 0.0 &&
-      (result.mActiveTime != zeroDuration ||
-       result.mDuration == zeroDuration)) {
-    // The only way we can be in the after phase with a progress of zero and
-    // a current iteration of zero, is if we have a zero iteration count or
-    // were clipped using a negative end delay--both of which we should have
-    // detected above.
+  // When we are at the end of the active interval and the end of an iteration
+  // we need to report the end of the final iteration and not the start of the
+  // next iteration. We *don't* want to do this, however, when we have
+  // a zero-iteration animation.
+  if (progress == 0.0 &&
+      (result.mPhase == ComputedTiming::AnimationPhase::After ||
+       result.mPhase == ComputedTiming::AnimationPhase::Active) &&
+      result.mActiveTime == result.mActiveDuration &&
+      result.mIterations != 0.0) {
+    // The only way we can reach the end of the active interval and have
+    // a progress of zero and a current iteration of zero, is if we have a zero
+    // iteration count -- something we should have detected above.
     MOZ_ASSERT(result.mCurrentIteration != 0,
                "Should not have zero current iteration");
     progress = 1.0;
     if (result.mCurrentIteration != UINT64_MAX) {
       result.mCurrentIteration--;
     }
   }
 
--- a/dom/animation/PendingAnimationTracker.h
+++ b/dom/animation/PendingAnimationTracker.h
@@ -23,35 +23,43 @@ public:
     : mDocument(aDocument)
   { }
 
   NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PendingAnimationTracker)
   NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PendingAnimationTracker)
 
   void AddPlayPending(dom::Animation& aAnimation)
   {
-    MOZ_ASSERT(!IsWaitingToPause(aAnimation),
-               "Animation is already waiting to pause");
+    // We'd like to assert here that IsWaitingToPause(aAnimation) is false but
+    // if |aAnimation| was tracked here as a pause-pending animation when it was
+    // removed from |mDocument|, then re-attached to |mDocument|, and then
+    // played again, we could end up here with IsWaitingToPause returning true.
+    //
+    // However, that should be harmless since all it means is that we'll call
+    // Animation::TriggerOnNextTick or Animation::TriggerNow twice, both of
+    // which will handle the redundant call gracefully.
     AddPending(aAnimation, mPlayPendingSet);
     mHasPlayPendingGeometricAnimations = CheckState::Indeterminate;
   }
   void RemovePlayPending(dom::Animation& aAnimation)
   {
     RemovePending(aAnimation, mPlayPendingSet);
     mHasPlayPendingGeometricAnimations = CheckState::Indeterminate;
   }
   bool IsWaitingToPlay(const dom::Animation& aAnimation) const
   {
     return IsWaiting(aAnimation, mPlayPendingSet);
   }
 
   void AddPausePending(dom::Animation& aAnimation)
   {
-    MOZ_ASSERT(!IsWaitingToPlay(aAnimation),
-               "Animation is already waiting to play");
+    // As with AddPausePending, we'd like to assert that
+    // IsWaitingToPlay(aAnimation) is false but there are some circumstances
+    // where this can be true. Fortunately adding the animation to both pending
+    // sets should be harmless.
     AddPending(aAnimation, mPausePendingSet);
   }
   void RemovePausePending(dom::Animation& aAnimation)
   {
     RemovePending(aAnimation, mPausePendingSet);
   }
   bool IsWaitingToPause(const dom::Animation& aAnimation) const
   {
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/crashtests/1282691-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset=utf-8>
+<script>
+
+function boom() {
+  const div = document.createElement('div');
+  const anim = div.animate([{}], {});
+  document.documentElement.appendChild(div);
+  anim.pause();
+  document.documentElement.removeChild(div);
+
+  requestAnimationFrame(() => {
+    document.documentElement.appendChild(div);
+    anim.play();
+    document.documentElement.className = '';
+  });
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
--- a/dom/animation/test/crashtests/crashtests.list
+++ b/dom/animation/test/crashtests/crashtests.list
@@ -5,16 +5,17 @@ pref(dom.animations-api.core.enabled,tru
 pref(dom.animations-api.core.enabled,true) load 1216842-3.html
 pref(dom.animations-api.core.enabled,true) load 1216842-4.html
 pref(dom.animations-api.core.enabled,true) load 1216842-5.html
 pref(dom.animations-api.core.enabled,true) load 1216842-6.html
 pref(dom.animations-api.core.enabled,true) load 1272475-1.html
 pref(dom.animations-api.core.enabled,true) load 1272475-2.html
 pref(dom.animations-api.core.enabled,true) load 1278485-1.html
 pref(dom.animations-api.core.enabled,true) load 1277272-1.html
+pref(dom.animations-api.core.enabled,true) load 1282691-1.html
 pref(dom.animations-api.core.enabled,true) load 1291413-1.html
 pref(dom.animations-api.core.enabled,true) load 1291413-2.html
 pref(dom.animations-api.core.enabled,true) load 1304886-1.html
 pref(dom.animations-api.core.enabled,true) load 1322382-1.html
 pref(dom.animations-api.core.enabled,true) load 1322291-1.html
 pref(dom.animations-api.core.enabled,true) load 1322291-2.html
 pref(dom.animations-api.core.enabled,true) load 1323114-1.html
 pref(dom.animations-api.core.enabled,true) load 1323114-2.html
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -3462,37 +3462,48 @@ Element::GetTokenList(nsAtom* aAtom,
     SetProperty(aAtom, list, nsDOMTokenListPropertyDestructor);
   }
   return list;
 }
 
 Element*
 Element::Closest(const nsAString& aSelector, ErrorResult& aResult)
 {
-  nsCSSSelectorList* selectorList = ParseSelectorList(aSelector, aResult);
-  if (!selectorList) {
-    // Either we failed (and aResult already has the exception), or this
-    // is a pseudo-element-only selector that matches nothing.
-    return nullptr;
-  }
-  TreeMatchContext matchingContext(false,
-                                   nsRuleWalker::eRelevantLinkUnvisited,
-                                   OwnerDoc(),
-                                   TreeMatchContext::eNeverMatchVisited);
-  matchingContext.SetHasSpecifiedScope();
-  matchingContext.AddScopeElement(this);
-  for (nsINode* node = this; node; node = node->GetParentNode()) {
-    if (node->IsElement() &&
-        nsCSSRuleProcessor::SelectorListMatches(node->AsElement(),
-                                                matchingContext,
-                                                selectorList)) {
-      return node->AsElement();
+  return WithSelectorList<Element*>(
+    aSelector,
+    aResult,
+    [&](const RawServoSelectorList* aList) -> Element* {
+      if (!aList) {
+        return nullptr;
+      }
+      return const_cast<Element*>(Servo_SelectorList_Closest(this, aList));
+    },
+    [&](nsCSSSelectorList* aList) -> Element* {
+      if (!aList) {
+        // Either we failed (and aError already has the exception), or this
+        // is a pseudo-element-only selector that matches nothing.
+        return nullptr;
+      }
+      TreeMatchContext matchingContext(false,
+                                       nsRuleWalker::eRelevantLinkUnvisited,
+                                       OwnerDoc(),
+                                       TreeMatchContext::eNeverMatchVisited);
+      matchingContext.SetHasSpecifiedScope();
+      matchingContext.AddScopeElement(this);
+      for (nsINode* node = this; node; node = node->GetParentNode()) {
+        if (node->IsElement() &&
+            nsCSSRuleProcessor::SelectorListMatches(node->AsElement(),
+                                                    matchingContext,
+                                                    aList)) {
+          return node->AsElement();
+        }
+      }
+      return nullptr;
     }
-  }
-  return nullptr;
+  );
 }
 
 bool
 Element::Matches(const nsAString& aSelector, ErrorResult& aError)
 {
   return WithSelectorList<bool>(
     aSelector,
     aError,
--- a/dom/base/ImageEncoder.cpp
+++ b/dom/base/ImageEncoder.cpp
@@ -150,51 +150,55 @@ public:
   EncodingRunnable(const nsAString& aType,
                    const nsAString& aOptions,
                    UniquePtr<uint8_t[]> aImageBuffer,
                    layers::Image* aImage,
                    imgIEncoder* aEncoder,
                    EncodingCompleteEvent* aEncodingCompleteEvent,
                    int32_t aFormat,
                    const nsIntSize aSize,
+                   bool aUsePlaceholder,
                    bool aUsingCustomOptions)
     : Runnable("EncodingRunnable")
     , mType(aType)
     , mOptions(aOptions)
     , mImageBuffer(Move(aImageBuffer))
     , mImage(aImage)
     , mEncoder(aEncoder)
     , mEncodingCompleteEvent(aEncodingCompleteEvent)
     , mFormat(aFormat)
     , mSize(aSize)
+    , mUsePlaceholder(aUsePlaceholder)
     , mUsingCustomOptions(aUsingCustomOptions)
   {}
 
   nsresult ProcessImageData(uint64_t* aImgSize, void** aImgData)
   {
     nsCOMPtr<nsIInputStream> stream;
     nsresult rv = ImageEncoder::ExtractDataInternal(mType,
                                                     mOptions,
                                                     mImageBuffer.get(),
                                                     mFormat,
                                                     mSize,
+                                                    mUsePlaceholder,
                                                     mImage,
                                                     nullptr,
                                                     nullptr,
                                                     getter_AddRefs(stream),
                                                     mEncoder);
 
     // If there are unrecognized custom parse options, we should fall back to
     // the default values for the encoder without any options at all.
     if (rv == NS_ERROR_INVALID_ARG && mUsingCustomOptions) {
       rv = ImageEncoder::ExtractDataInternal(mType,
                                              EmptyString(),
                                              mImageBuffer.get(),
                                              mFormat,
                                              mSize,
+                                             mUsePlaceholder,
                                              mImage,
                                              nullptr,
                                              nullptr,
                                              getter_AddRefs(stream),
                                              mEncoder);
     }
     NS_ENSURE_SUCCESS(rv, rv);
 
@@ -234,47 +238,51 @@ private:
   nsAutoString mType;
   nsAutoString mOptions;
   UniquePtr<uint8_t[]> mImageBuffer;
   RefPtr<layers::Image> mImage;
   nsCOMPtr<imgIEncoder> mEncoder;
   RefPtr<EncodingCompleteEvent> mEncodingCompleteEvent;
   int32_t mFormat;
   const nsIntSize mSize;
+  bool mUsePlaceholder;
   bool mUsingCustomOptions;
 };
 
 NS_IMPL_ISUPPORTS_INHERITED0(EncodingRunnable, Runnable);
 
 StaticRefPtr<nsIThreadPool> ImageEncoder::sThreadPool;
 
 /* static */
 nsresult
 ImageEncoder::ExtractData(nsAString& aType,
                           const nsAString& aOptions,
                           const nsIntSize aSize,
+                          bool aUsePlaceholder,
                           nsICanvasRenderingContextInternal* aContext,
                           layers::AsyncCanvasRenderer* aRenderer,
                           nsIInputStream** aStream)
 {
   nsCOMPtr<imgIEncoder> encoder = ImageEncoder::GetImageEncoder(aType);
   if (!encoder) {
     return NS_IMAGELIB_ERROR_NO_ENCODER;
   }
 
-  return ExtractDataInternal(aType, aOptions, nullptr, 0, aSize, nullptr,
+  return ExtractDataInternal(aType, aOptions, nullptr, 0, aSize,
+                             aUsePlaceholder, nullptr,
                              aContext, aRenderer, aStream, encoder);
 }
 
 /* static */
 nsresult
 ImageEncoder::ExtractDataFromLayersImageAsync(nsAString& aType,
                                               const nsAString& aOptions,
                                               bool aUsingCustomOptions,
                                               layers::Image* aImage,
+                                              bool aUsePlaceholder,
                                               EncodeCompleteCallback* aEncodeCallback)
 {
   nsCOMPtr<imgIEncoder> encoder = ImageEncoder::GetImageEncoder(aType);
   if (!encoder) {
     return NS_IMAGELIB_ERROR_NO_ENCODER;
   }
 
   nsresult rv = EnsureThreadPool();
@@ -289,28 +297,30 @@ ImageEncoder::ExtractDataFromLayersImage
   nsCOMPtr<nsIRunnable> event = new EncodingRunnable(aType,
                                                      aOptions,
                                                      nullptr,
                                                      aImage,
                                                      encoder,
                                                      completeEvent,
                                                      imgIEncoder::INPUT_FORMAT_HOSTARGB,
                                                      size,
+                                                     aUsePlaceholder,
                                                      aUsingCustomOptions);
   return sThreadPool->Dispatch(event, NS_DISPATCH_NORMAL);
 }
 
 /* static */
 nsresult
 ImageEncoder::ExtractDataAsync(nsAString& aType,
                                const nsAString& aOptions,
                                bool aUsingCustomOptions,
                                UniquePtr<uint8_t[]> aImageBuffer,
                                int32_t aFormat,
                                const nsIntSize aSize,
+                               bool aUsePlaceholder,
                                EncodeCompleteCallback* aEncodeCallback)
 {
   nsCOMPtr<imgIEncoder> encoder = ImageEncoder::GetImageEncoder(aType);
   if (!encoder) {
     return NS_IMAGELIB_ERROR_NO_ENCODER;
   }
 
   nsresult rv = EnsureThreadPool();
@@ -324,16 +334,17 @@ ImageEncoder::ExtractDataAsync(nsAString
   nsCOMPtr<nsIRunnable> event = new EncodingRunnable(aType,
                                                      aOptions,
                                                      Move(aImageBuffer),
                                                      nullptr,
                                                      encoder,
                                                      completeEvent,
                                                      aFormat,
                                                      aSize,
+                                                     aUsePlaceholder,
                                                      aUsingCustomOptions);
   return sThreadPool->Dispatch(event, NS_DISPATCH_NORMAL);
 }
 
 /*static*/ nsresult
 ImageEncoder::GetInputStream(int32_t aWidth,
                              int32_t aHeight,
                              uint8_t* aImageBuffer,
@@ -354,54 +365,55 @@ ImageEncoder::GetInputStream(int32_t aWi
 
 /* static */
 nsresult
 ImageEncoder::ExtractDataInternal(const nsAString& aType,
                                   const nsAString& aOptions,
                                   uint8_t* aImageBuffer,
                                   int32_t aFormat,
                                   const nsIntSize aSize,
+                                  bool aUsePlaceholder,
                                   layers::Image* aImage,
                                   nsICanvasRenderingContextInternal* aContext,
                                   layers::AsyncCanvasRenderer* aRenderer,
                                   nsIInputStream** aStream,
                                   imgIEncoder* aEncoder)
 {
   if (aSize.IsEmpty()) {
     return NS_ERROR_INVALID_ARG;
   }
 
   nsCOMPtr<nsIInputStream> imgStream;
 
   // get image bytes
   nsresult rv;
-  if (aImageBuffer) {
+  if (aImageBuffer && !aUsePlaceholder) {
     if (BufferSizeFromDimensions(aSize.width, aSize.height, 4) == 0) {
       return NS_ERROR_INVALID_ARG;
     }
 
     rv = ImageEncoder::GetInputStream(
       aSize.width,
       aSize.height,
       aImageBuffer,
       aFormat,
       aEncoder,
       nsPromiseFlatString(aOptions).get(),
       getter_AddRefs(imgStream));
-  } else if (aContext) {
+  } else if (aContext && !aUsePlaceholder) {
     NS_ConvertUTF16toUTF8 encoderType(aType);
     rv = aContext->GetInputStream(encoderType.get(),
                                   nsPromiseFlatString(aOptions).get(),
                                   getter_AddRefs(imgStream));
-  } else if (aRenderer) {
+  } else if (aRenderer && !aUsePlaceholder) {
     NS_ConvertUTF16toUTF8 encoderType(aType);
     rv = aRenderer->GetInputStream(encoderType.get(),
                                    nsPromiseFlatString(aOptions).get(),
                                    getter_AddRefs(imgStream));
-  } else if (aImage) {
+  } else if (aImage && !aUsePlaceholder) {
     // It is safe to convert PlanarYCbCr format from YUV to RGB off-main-thread.
     // Other image formats could have problem to convert format off-main-thread.
     // So here it uses a help function GetBRGADataSourceSurfaceSync() to convert
     // format on main thread.
     if (aImage->GetFormat() == ImageFormat::PLANAR_YCBCR) {
       nsTArray<uint8_t> data;
       layers::PlanarYCbCrImage* ycbcrImage = static_cast<layers::PlanarYCbCrImage*> (aImage);
       gfxImageFormat format = SurfaceFormat::A8R8G8B8_UINT32;
@@ -467,16 +479,20 @@ ImageEncoder::ExtractDataInternal(const 
     if (NS_WARN_IF(!emptyCanvas)) {
       return NS_ERROR_INVALID_ARG;
     }
 
     DataSourceSurface::MappedSurface map;
     if (!emptyCanvas->Map(DataSourceSurface::MapType::WRITE, &map)) {
       return NS_ERROR_INVALID_ARG;
     }
+    if (aUsePlaceholder) {
+      // If placeholder data was requested, return all-white, opaque image data.
+      memset(map.mData, 0xFF, 4 * aSize.width * aSize.height);
+    }
     rv = aEncoder->InitFromData(map.mData,
                                 aSize.width * aSize.height * 4,
                                 aSize.width,
                                 aSize.height,
                                 aSize.width * 4,
                                 imgIEncoder::INPUT_FORMAT_HOSTARGB,
                                 aOptions);
     emptyCanvas->Unmap();
--- a/dom/base/ImageEncoder.h
+++ b/dom/base/ImageEncoder.h
@@ -37,16 +37,17 @@ public:
   // represented by aContext. aType may change to "image/png" if we had to fall
   // back to a PNG encoder. A return value of NS_OK implies successful data
   // extraction. If there are any unrecognized custom parse options in
   // aOptions, NS_ERROR_INVALID_ARG will be returned. When encountering this
   // error it is usual to call this function again without any options at all.
   static nsresult ExtractData(nsAString& aType,
                               const nsAString& aOptions,
                               const nsIntSize aSize,
+                              bool aUsePlaceholder,
                               nsICanvasRenderingContextInternal* aContext,
                               layers::AsyncCanvasRenderer* aRenderer,
                               nsIInputStream** aStream);
 
   // Extracts data asynchronously. aType may change to "image/png" if we had to
   // fall back to a PNG encoder. aOptions are the options to be passed to the
   // encoder and aUsingCustomOptions specifies whether custom parse options were
   // used (i.e. by using -moz-parse-options). If there are any unrecognized
@@ -58,27 +59,29 @@ public:
   // Note: The callback has to set a valid parent for content for the generated
   // Blob object.
   static nsresult ExtractDataAsync(nsAString& aType,
                                    const nsAString& aOptions,
                                    bool aUsingCustomOptions,
                                    UniquePtr<uint8_t[]> aImageBuffer,
                                    int32_t aFormat,
                                    const nsIntSize aSize,
+                                   bool aUsePlaceholder,
                                    EncodeCompleteCallback* aEncodeCallback);
 
   // Extract an Image asynchronously. Its function is same as ExtractDataAsync
   // except for the parameters. aImage is the uncompressed data. aEncodeCallback
   // will be called on main thread when encoding process is success.
   // Note: The callback has to set a valid parent for content for the generated
   // Blob object.
   static nsresult ExtractDataFromLayersImageAsync(nsAString& aType,
                                                   const nsAString& aOptions,
                                                   bool aUsingCustomOptions,
                                                   layers::Image* aImage,
+                                                  bool aUsePlaceholder,
                                                   EncodeCompleteCallback* aEncodeCallback);
 
   // Gives you a stream containing the image represented by aImageBuffer.
   // The format is given in aFormat, for example
   // imgIEncoder::INPUT_FORMAT_HOSTARGB.
   static nsresult GetInputStream(int32_t aWidth,
                                  int32_t aHeight,
                                  uint8_t* aImageBuffer,
@@ -90,16 +93,17 @@ public:
 private:
   // When called asynchronously, aContext and aRenderer are null.
   static nsresult
   ExtractDataInternal(const nsAString& aType,
                       const nsAString& aOptions,
                       uint8_t* aImageBuffer,
                       int32_t aFormat,
                       const nsIntSize aSize,
+                      bool aUsePlaceholder,
                       layers::Image* aImage,
                       nsICanvasRenderingContextInternal* aContext,
                       layers::AsyncCanvasRenderer* aRenderer,
                       nsIInputStream** aStream,
                       imgIEncoder* aEncoder);
 
   // Creates and returns an encoder instance of the type specified in aType.
   // aType may change to "image/png" if no instance of the original type could
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -119,17 +119,17 @@ DOMInterfaces = {
 
 'CacheStorage': {
     'implicitJSContext': [ 'match' ],
     'nativeType': 'mozilla::dom::cache::CacheStorage',
 },
 
 'CanvasRenderingContext2D': {
     'implicitJSContext': [
-        'createImageData', 'getImageData'
+        'createImageData', 'getImageData', 'isPointInPath', 'isPointInStroke'
     ],
     'binaryNames': {
         'mozImageSmoothingEnabled': 'imageSmoothingEnabled'
     }
 },
 
 'CaretPosition' : {
     'nativeType': 'nsDOMCaretPosition',
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -4900,57 +4900,71 @@ CanvasRenderingContext2D::SetLineDashOff
 }
 
 double
 CanvasRenderingContext2D::LineDashOffset() const {
   return CurrentState().dashOffset;
 }
 
 bool
-CanvasRenderingContext2D::IsPointInPath(double aX, double aY, const CanvasWindingRule& aWinding)
+CanvasRenderingContext2D::IsPointInPath(JSContext* aCx, double aX, double aY, const CanvasWindingRule& aWinding)
 {
   if (!FloatValidate(aX, aY)) {
     return false;
   }
 
+  // Check for site-specific permission and return false if no permission.
+  if (mCanvasElement) {
+    nsCOMPtr<nsIDocument> ownerDoc = mCanvasElement->OwnerDoc();
+    if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx))
+      return false;
+  }
+
   EnsureUserSpacePath(aWinding);
   if (!mPath) {
     return false;
   }
 
   if (mPathTransformWillUpdate) {
     return mPath->ContainsPoint(Point(aX, aY), mPathToDS);
   }
 
   return mPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform());
 }
 
-bool CanvasRenderingContext2D::IsPointInPath(const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding)
+bool CanvasRenderingContext2D::IsPointInPath(JSContext* aCx, const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding)
 {
   if (!FloatValidate(aX, aY)) {
     return false;
   }
 
   EnsureTarget();
   if (!IsTargetValid()) {
     return false;
   }
 
   RefPtr<gfx::Path> tempPath = aPath.GetPath(aWinding, mTarget);
 
   return tempPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform());
 }
 
 bool
-CanvasRenderingContext2D::IsPointInStroke(double aX, double aY)
+CanvasRenderingContext2D::IsPointInStroke(JSContext* aCx, double aX, double aY)
 {
   if (!FloatValidate(aX, aY)) {
     return false;
   }
 
+  // Check for site-specific permission and return false if no permission.
+  if (mCanvasElement) {
+    nsCOMPtr<nsIDocument> ownerDoc = mCanvasElement->OwnerDoc();
+    if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx))
+      return false;
+  }
+
   EnsureUserSpacePath();
   if (!mPath) {
     return false;
   }
 
   const ContextState &state = CurrentState();
 
   StrokeOptions strokeOptions(state.lineWidth,
@@ -4962,17 +4976,17 @@ CanvasRenderingContext2D::IsPointInStrok
                               state.dashOffset);
 
   if (mPathTransformWillUpdate) {
     return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mPathToDS);
   }
   return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mTarget->GetTransform());
 }
 
-bool CanvasRenderingContext2D::IsPointInStroke(const CanvasPath& aPath, double aX, double aY)
+bool CanvasRenderingContext2D::IsPointInStroke(JSContext* aCx, const CanvasPath& aPath, double aX, double aY)
 {
   if (!FloatValidate(aX, aY)) {
     return false;
   }
 
   EnsureTarget();
   if (!IsTargetValid()) {
     return false;
@@ -5803,36 +5817,53 @@ CanvasRenderingContext2D::GetImageDataAr
 
   if (!readback || !readback->Map(DataSourceSurface::READ, &rawData)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   IntRect dstWriteRect = srcReadRect;
   dstWriteRect.MoveBy(-aX, -aY);
 
-  {
+  // Check for site-specific permission.  This check is not needed if the
+  // canvas was created with a docshell (that is only done for special
+  // internal uses).
+  bool usePlaceholder = false;
+  if (mCanvasElement) {
+    nsCOMPtr<nsIDocument> ownerDoc = mCanvasElement->OwnerDoc();
+    usePlaceholder = !ownerDoc ||
+      !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx);
+  }
+
+  do {
     JS::AutoCheckCannotGC nogc;
     bool isShared;
     uint8_t* data = JS_GetUint8ClampedArrayData(darray, &isShared, nogc);
     MOZ_ASSERT(!isShared);        // Should not happen, data was created above
 
     uint32_t srcStride = rawData.mStride;
     uint8_t* src = rawData.mData + srcReadRect.y * srcStride + srcReadRect.x * 4;
+
+    // Return all-white, opaque pixel data if no permission.
+    if (usePlaceholder) {
+      memset(data, 0xFF, len.value());
+      break;
+    }
+
     uint8_t* dst = data + dstWriteRect.y * (aWidth * 4) + dstWriteRect.x * 4;
 
     if (mOpaque) {
       SwizzleData(src, srcStride, SurfaceFormat::X8R8G8B8_UINT32,
                   dst, aWidth * 4, SurfaceFormat::R8G8B8A8,
                   dstWriteRect.Size());
     } else {
       UnpremultiplyData(src, srcStride, SurfaceFormat::A8R8G8B8_UINT32,
                         dst, aWidth * 4, SurfaceFormat::R8G8B8A8,
                         dstWriteRect.Size());
     }
-  }
+  } while (false);
 
   readback->Unmap();
   *aRetval = darray;
   return NS_OK;
 }
 
 void
 CanvasRenderingContext2D::EnsureErrorTarget()
--- a/dom/canvas/CanvasRenderingContext2D.h
+++ b/dom/canvas/CanvasRenderingContext2D.h
@@ -199,20 +199,20 @@ public:
   void Fill(const CanvasWindingRule& aWinding);
   void Fill(const CanvasPath& aPath, const CanvasWindingRule& aWinding);
   void Stroke();
   void Stroke(const CanvasPath& aPath);
   void DrawFocusIfNeeded(mozilla::dom::Element& aElement, ErrorResult& aRv);
   bool DrawCustomFocusRing(mozilla::dom::Element& aElement);
   void Clip(const CanvasWindingRule& aWinding);
   void Clip(const CanvasPath& aPath, const CanvasWindingRule& aWinding);
-  bool IsPointInPath(double aX, double aY, const CanvasWindingRule& aWinding);
-  bool IsPointInPath(const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding);
-  bool IsPointInStroke(double aX, double aY);
-  bool IsPointInStroke(const CanvasPath& aPath, double aX, double aY);
+  bool IsPointInPath(JSContext* aCx, double aX, double aY, const CanvasWindingRule& aWinding);
+  bool IsPointInPath(JSContext* aCx, const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding);
+  bool IsPointInStroke(JSContext* aCx, double aX, double aY);
+  bool IsPointInStroke(JSContext* aCx, const CanvasPath& aPath, double aX, double aY);
   void FillText(const nsAString& aText, double aX, double aY,
                 const Optional<double>& aMaxWidth,
                 mozilla::ErrorResult& aError);
   void StrokeText(const nsAString& aText, double aX, double aY,
                   const Optional<double>& aMaxWidth,
                   mozilla::ErrorResult& aError);
   TextMetrics*
     MeasureText(const nsAString& aRawText, mozilla::ErrorResult& aError);
--- a/dom/canvas/CanvasRenderingContextHelper.cpp
+++ b/dom/canvas/CanvasRenderingContextHelper.cpp
@@ -20,16 +20,17 @@ namespace mozilla {
 namespace dom {
 
 void
 CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
                                      nsIGlobalObject* aGlobal,
                                      BlobCallback& aCallback,
                                      const nsAString& aType,
                                      JS::Handle<JS::Value> aParams,
+                                     bool aUsePlaceholder,
                                      ErrorResult& aRv)
 {
   // Encoder callback when encoding is complete.
   class EncodeCallback : public EncodeCompleteCallback
   {
   public:
     EncodeCallback(nsIGlobalObject* aGlobal, BlobCallback* aCallback)
       : mGlobal(aGlobal)
@@ -53,25 +54,26 @@ CanvasRenderingContextHelper::ToBlob(JSC
 
     nsCOMPtr<nsIGlobalObject> mGlobal;
     RefPtr<BlobCallback> mBlobCallback;
   };
 
   RefPtr<EncodeCompleteCallback> callback =
     new EncodeCallback(aGlobal, &aCallback);
 
-  ToBlob(aCx, aGlobal, callback, aType, aParams, aRv);
+  ToBlob(aCx, aGlobal, callback, aType, aParams, aUsePlaceholder, aRv);
 }
 
 void
 CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
                                      nsIGlobalObject* aGlobal,
                                      EncodeCompleteCallback* aCallback,
                                      const nsAString& aType,
                                      JS::Handle<JS::Value> aParams,
+                                     bool aUsePlaceholder,
                                      ErrorResult& aRv)
 {
   nsAutoString type;
   nsContentUtils::ASCIIToLower(aType, type);
 
   nsAutoString params;
   bool usingCustomParseOptions;
   aRv = ParseParams(aCx, type, aParams, params, &usingCustomParseOptions);
@@ -102,16 +104,17 @@ CanvasRenderingContextHelper::ToBlob(JSC
   RefPtr<EncodeCompleteCallback> callback = aCallback;
 
   aRv = ImageEncoder::ExtractDataAsync(type,
                                        params,
                                        usingCustomParseOptions,
                                        Move(imageBuffer),
                                        format,
                                        GetWidthHeight(),
+                                       aUsePlaceholder,
                                        callback);
 }
 
 already_AddRefed<nsICanvasRenderingContextInternal>
 CanvasRenderingContextHelper::CreateContext(CanvasContextType aContextType)
 {
   return CreateContextHelper(aContextType, layers::LayersBackend::LAYERS_NONE);
 }
--- a/dom/canvas/CanvasRenderingContextHelper.h
+++ b/dom/canvas/CanvasRenderingContextHelper.h
@@ -53,21 +53,21 @@ protected:
   virtual nsresult ParseParams(JSContext* aCx,
                                const nsAString& aType,
                                const JS::Value& aEncoderOptions,
                                nsAString& outParams,
                                bool* const outCustomParseOptions);
 
   void ToBlob(JSContext* aCx, nsIGlobalObject* global, BlobCallback& aCallback,
               const nsAString& aType, JS::Handle<JS::Value> aParams,
-              ErrorResult& aRv);
+              bool aUsePlaceholder, ErrorResult& aRv);
 
   void ToBlob(JSContext* aCx, nsIGlobalObject* aGlobal, EncodeCompleteCallback* aCallback,
               const nsAString& aType, JS::Handle<JS::Value> aParams,
-              ErrorResult& aRv);
+              bool aUsePlaceholder, ErrorResult& aRv);
 
   virtual already_AddRefed<nsICanvasRenderingContextInternal>
   CreateContext(CanvasContextType aContextType);
 
   already_AddRefed<nsICanvasRenderingContextInternal>
   CreateContextHelper(CanvasContextType aContextType,
                       layers::LayersBackend aCompositorBackend);
 
--- a/dom/canvas/CanvasUtils.cpp
+++ b/dom/canvas/CanvasUtils.cpp
@@ -8,31 +8,171 @@
 
 #include "nsIServiceManager.h"
 
 #include "nsIConsoleService.h"
 #include "nsIDOMCanvasRenderingContext2D.h"
 #include "nsICanvasRenderingContextInternal.h"
 #include "nsIHTMLCollection.h"
 #include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/TabChild.h"
 #include "nsIPrincipal.h"
 
 #include "nsGfxCIID.h"
 
 #include "nsTArray.h"
 
 #include "CanvasUtils.h"
 #include "mozilla/gfx/Matrix.h"
 #include "WebGL2Context.h"
 
+#include "nsIScriptObjectPrincipal.h"
+#include "nsIPermissionManager.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsContentUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsPrintfCString.h"
+#include "nsIConsoleService.h"
+#include "jsapi.h"
+
+#define TOPIC_CANVAS_PERMISSIONS_PROMPT "canvas-permissions-prompt"
+#define PERMISSION_CANVAS_EXTRACT_DATA "canvas/extractData"
+
 using namespace mozilla::gfx;
 
 namespace mozilla {
 namespace CanvasUtils {
 
+bool IsImageExtractionAllowed(nsIDocument *aDocument, JSContext *aCx)
+{
+    // Do the rest of the checks only if privacy.resistFingerprinting is on.
+    if (!nsContentUtils::ShouldResistFingerprinting()) {
+        return true;
+    }
+
+    // Don't proceed if we don't have a document or JavaScript context.
+    if (!aDocument || !aCx) {
+        return false;
+    }
+
+    // Documents with system principal can always extract canvas data.
+    nsPIDOMWindowOuter *win = aDocument->GetWindow();
+    nsCOMPtr<nsIScriptObjectPrincipal> sop(do_QueryInterface(win));
+    if (sop && nsContentUtils::IsSystemPrincipal(sop->GetPrincipal())) {
+        return true;
+    }
+
+    // Always give permission to chrome scripts (e.g. Page Inspector).
+    if (nsContentUtils::ThreadsafeIsCallerChrome()) {
+        return true;
+    }
+
+    // Get the document URI and its spec.
+    nsIURI *docURI = aDocument->GetDocumentURI();
+    nsCString docURISpec;
+    docURI->GetSpec(docURISpec);
+
+    // Allow local files to extract canvas data.
+    bool isFileURL;
+    (void) docURI->SchemeIs("file", &isFileURL);
+    if (isFileURL) {
+        return true;
+    }
+
+    // Get calling script file and line for logging.
+    JS::AutoFilename scriptFile;
+    unsigned scriptLine = 0;
+    bool isScriptKnown = false;
+    if (JS::DescribeScriptedCaller(aCx, &scriptFile, &scriptLine)) {
+        isScriptKnown = true;
+        // Don't show canvas prompt for PDF.js
+        if (scriptFile.get() &&
+                strcmp(scriptFile.get(), "resource://pdf.js/build/pdf.js") == 0) {
+            return true;
+        }
+    }
+
+    nsIDocument* topLevelDocument = aDocument->GetTopLevelContentDocument();
+    nsIURI *topLevelDocURI = topLevelDocument ? topLevelDocument->GetDocumentURI() : nullptr;
+    nsCString topLevelDocURISpec;
+    if (topLevelDocURI) {
+        topLevelDocURI->GetSpec(topLevelDocURISpec);
+    }
+
+    // Load Third Party Util service.
+    nsresult rv;
+    nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
+        do_GetService(THIRDPARTYUTIL_CONTRACTID, &rv);
+    NS_ENSURE_SUCCESS(rv, false);
+
+    // Block all third-party attempts to extract canvas.
+    bool isThirdParty = true;
+    rv = thirdPartyUtil->IsThirdPartyURI(topLevelDocURI, docURI, &isThirdParty);
+    NS_ENSURE_SUCCESS(rv, false);
+    if (isThirdParty) {
+        nsAutoCString message;
+        message.AppendPrintf("Blocked third party %s in page %s from extracting canvas data.",
+                             docURISpec.get(), topLevelDocURISpec.get());
+        if (isScriptKnown) {
+            message.AppendPrintf(" %s:%u.", scriptFile.get(), scriptLine);
+        }
+        nsContentUtils::LogMessageToConsole(message.get());
+        return false;
+    }
+
+    // Load Permission Manager service.
+    nsCOMPtr<nsIPermissionManager> permissionManager =
+        do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
+    NS_ENSURE_SUCCESS(rv, false);
+
+    // Check if the site has permission to extract canvas data.
+    // Either permit or block extraction if a stored permission setting exists.
+    uint32_t permission;
+    rv = permissionManager->TestPermission(topLevelDocURI,
+                                           PERMISSION_CANVAS_EXTRACT_DATA,
+                                           &permission);
+    NS_ENSURE_SUCCESS(rv, false);
+    switch (permission) {
+    case nsIPermissionManager::ALLOW_ACTION:
+        return true;
+    case nsIPermissionManager::DENY_ACTION:
+        return false;
+    default:
+        break;
+    }
+
+    // At this point, permission is unknown (nsIPermissionManager::UNKNOWN_ACTION).
+    nsAutoCString message;
+    message.AppendPrintf("Blocked %s in page %s from extracting canvas data.",
+                         docURISpec.get(), topLevelDocURISpec.get());
+    if (isScriptKnown) {
+        message.AppendPrintf(" %s:%u.", scriptFile.get(), scriptLine);
+    }
+    nsContentUtils::LogMessageToConsole(message.get());
+
+    // Prompt the user (asynchronous).
+    if (XRE_IsContentProcess()) {
+        TabChild* tabChild = TabChild::GetFrom(win);
+        if (tabChild) {
+            tabChild->SendShowCanvasPermissionPrompt(topLevelDocURISpec);
+        }
+    } else {
+        nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+        if (obs) {
+            obs->NotifyObservers(win, TOPIC_CANVAS_PERMISSIONS_PROMPT,
+                                 NS_ConvertUTF8toUTF16(topLevelDocURISpec).get());
+        }
+    }
+
+    // We don't extract the image for now -- user may override at prompt.
+    return false;
+}
+
 bool
 GetCanvasContextType(const nsAString& str, dom::CanvasContextType* const out_type)
 {
   if (str.EqualsLiteral("2d")) {
     *out_type = dom::CanvasContextType::Canvas2D;
     return true;
   }
 
--- a/dom/canvas/CanvasUtils.h
+++ b/dom/canvas/CanvasUtils.h
@@ -44,16 +44,19 @@ inline bool CheckSaneSubrectSize(int32_t
 void DoDrawImageSecurityCheck(dom::HTMLCanvasElement *aCanvasElement,
                               nsIPrincipal *aPrincipal,
                               bool forceWriteOnly,
                               bool CORSUsed);
 
 // Check if the context is chrome or has the permission to drawWindow
 bool HasDrawWindowPrivilege(JSContext* aCx, JSObject* aObj);
 
+// Check site-specific permission and display prompt if appropriate.
+bool IsImageExtractionAllowed(nsIDocument *aDocument, JSContext *aCx);
+
 // Make a double out of |v|, treating undefined values as 0.0 (for
 // the sake of sparse arrays).  Return true iff coercion
 // succeeded.
 bool CoerceDouble(const JS::Value& v, double* d);
 
     /* Float validation stuff */
 #define VALIDATE(_f)  if (!IsFinite(_f)) return false
 
--- a/dom/canvas/OffscreenCanvas.cpp
+++ b/dom/canvas/OffscreenCanvas.cpp
@@ -281,18 +281,23 @@ OffscreenCanvas::ToBlob(JSContext* aCx,
 
     nsCOMPtr<nsIGlobalObject> mGlobal;
     RefPtr<Promise> mPromise;
   };
 
   RefPtr<EncodeCompleteCallback> callback =
     new EncodeCallback(global, promise);
 
-  CanvasRenderingContextHelper::ToBlob(aCx, global,
-                                       callback, aType, aParams, aRv);
+  // TODO: Can we obtain the context and document here somehow
+  // so that we can decide when usePlaceholder should be true/false?
+  // See https://trac.torproject.org/18599
+  // For now, we always return a placeholder if fingerprinting resistance is on.
+  bool usePlaceholder = nsContentUtils::ShouldResistFingerprinting();
+  CanvasRenderingContextHelper::ToBlob(aCx, global, callback, aType, aParams,
+                                       usePlaceholder, aRv);
 
   return promise.forget();
 }
 
 already_AddRefed<gfx::SourceSurface>
 OffscreenCanvas::GetSurfaceSnapshot(gfxAlphaType* const aOutAlphaType)
 {
   if (!mCurrentContext) {
--- a/dom/html/HTMLCanvasElement.cpp
+++ b/dom/html/HTMLCanvasElement.cpp
@@ -39,16 +39,17 @@
 #include "nsIXPConnect.h"
 #include "nsJSUtils.h"
 #include "nsLayoutUtils.h"
 #include "nsMathUtils.h"
 #include "nsNetUtil.h"
 #include "nsRefreshDriver.h"
 #include "nsStreamUtils.h"
 #include "ActiveLayerTracker.h"
+#include "CanvasUtils.h"
 #include "VRManagerChild.h"
 #include "WebGL1Context.h"
 #include "WebGL2Context.h"
 
 using namespace mozilla::layers;
 using namespace mozilla::gfx;
 
 NS_IMPL_NS_NEW_HTML_ELEMENT(Canvas)
@@ -57,26 +58,29 @@ namespace mozilla {
 namespace dom {
 
 class RequestedFrameRefreshObserver : public nsARefreshObserver
 {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RequestedFrameRefreshObserver, override)
 
 public:
   RequestedFrameRefreshObserver(HTMLCanvasElement* const aOwningElement,
-                                nsRefreshDriver* aRefreshDriver)
+                                nsRefreshDriver* aRefreshDriver,
+                                bool aReturnPlaceholderData)
     : mRegistered(false),
+      mReturnPlaceholderData(aReturnPlaceholderData),
       mOwningElement(aOwningElement),
       mRefreshDriver(aRefreshDriver)
   {
     MOZ_ASSERT(mOwningElement);
   }
 
   static already_AddRefed<DataSourceSurface>
-  CopySurface(const RefPtr<SourceSurface>& aSurface)
+  CopySurface(const RefPtr<SourceSurface>& aSurface,
+              bool aReturnPlaceholderData)
   {
     RefPtr<DataSourceSurface> data = aSurface->GetDataSurface();
     if (!data) {
       return nullptr;
     }
 
     DataSourceSurface::ScopedMap read(data, DataSourceSurface::READ);
     if (!read.IsMapped()) {
@@ -95,22 +99,33 @@ public:
     if (!write.IsMapped()) {
       return nullptr;
     }
 
     MOZ_ASSERT(read.GetStride() == write.GetStride());
     MOZ_ASSERT(data->GetSize() == copy->GetSize());
     MOZ_ASSERT(data->GetFormat() == copy->GetFormat());
 
-    memcpy(write.GetData(), read.GetData(),
-           write.GetStride() * copy->GetSize().height);
+    if (aReturnPlaceholderData) {
+      // If returning placeholder data, fill the frame copy with white pixels.
+      memset(write.GetData(), 0xFF,
+             write.GetStride() * copy->GetSize().height);
+    } else {
+      memcpy(write.GetData(), read.GetData(),
+             write.GetStride() * copy->GetSize().height);
+    }
 
     return copy.forget();
   }
 
+  void SetReturnPlaceholderData(bool aReturnPlaceholderData)
+  {
+    mReturnPlaceholderData = aReturnPlaceholderData;
+  }
+
   void WillRefresh(TimeStamp aTime) override
   {
     MOZ_ASSERT(NS_IsMainThread());
 
     AUTO_PROFILER_LABEL("RequestedFrameRefreshObserver::WillRefresh", OTHER);
 
     if (!mOwningElement) {
       return;
@@ -139,17 +154,17 @@ public:
         return;
       }
     }
 
     RefPtr<DataSourceSurface> copy;
     {
       AUTO_PROFILER_LABEL(
         "RequestedFrameRefreshObserver::WillRefresh:CopySurface", OTHER);
-      copy = CopySurface(snapshot);
+      copy = CopySurface(snapshot, mReturnPlaceholderData);
       if (!copy) {
         return;
       }
     }
 
     {
       AUTO_PROFILER_LABEL(
         "RequestedFrameRefreshObserver::WillRefresh:SetFrame", OTHER);
@@ -196,16 +211,17 @@ public:
 private:
   virtual ~RequestedFrameRefreshObserver()
   {
     MOZ_ASSERT(!mRefreshDriver);
     MOZ_ASSERT(!mRegistered);
   }
 
   bool mRegistered;
+  bool mReturnPlaceholderData;
   HTMLCanvasElement* const mOwningElement;
   RefPtr<nsRefreshDriver> mRefreshDriver;
 };
 
 // ---------------------------------------------------------------------------
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLCanvasPrintState, mCanvas,
                                       mContext, mCallback)
@@ -749,33 +765,45 @@ HTMLCanvasElement::CaptureStream(const O
     return nullptr;
   }
 
   RefPtr<MediaStreamTrack> track =
   stream->CreateDOMTrack(videoTrackId, MediaSegment::VIDEO,
                          new CanvasCaptureTrackSource(principal, stream));
   stream->AddTrackInternal(track);
 
-  rv = RegisterFrameCaptureListener(stream->FrameCaptureListener());
+  // Check site-specific permission and display prompt if appropriate.
+  // If no permission, arrange for the frame capture listener to return
+  // all-white, opaque image data.
+  bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(
+    OwnerDoc(),
+    nsContentUtils::GetCurrentJSContext());
+
+  rv = RegisterFrameCaptureListener(stream->FrameCaptureListener(), usePlaceholder);
   if (NS_FAILED(rv)) {
     aRv.Throw(rv);
     return nullptr;
   }
 
   return stream.forget();
 }
 
 nsresult
-HTMLCanvasElement::ExtractData(nsAString& aType,
+HTMLCanvasElement::ExtractData(JSContext* aCx,
+                               nsAString& aType,
                                const nsAString& aOptions,
                                nsIInputStream** aStream)
 {
+  // Check site-specific permission and display prompt if appropriate.
+  // If no permission, return all-white, opaque image data.
+  bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(OwnerDoc(), aCx);
   return ImageEncoder::ExtractData(aType,
                                    aOptions,
                                    GetSize(),
+                                   usePlaceholder,
                                    mCurrentContext,
                                    mAsyncCanvasRenderer,
                                    aStream);
 }
 
 nsresult
 HTMLCanvasElement::ToDataURLImpl(JSContext* aCx,
                                  const nsAString& aMimeType,
@@ -795,22 +823,22 @@ HTMLCanvasElement::ToDataURLImpl(JSConte
   bool usingCustomParseOptions;
   nsresult rv =
     ParseParams(aCx, type, aEncoderOptions, params, &usingCustomParseOptions);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   nsCOMPtr<nsIInputStream> stream;
-  rv = ExtractData(type, params, getter_AddRefs(stream));
+  rv = ExtractData(aCx, type, params, getter_AddRefs(stream));
 
   // If there are unrecognized custom parse options, we should fall back to
   // the default values for the encoder without any options at all.
   if (rv == NS_ERROR_INVALID_ARG && usingCustomParseOptions) {
-    rv = ExtractData(type, EmptyString(), getter_AddRefs(stream));
+    rv = ExtractData(aCx, type, EmptyString(), getter_AddRefs(stream));
   }
 
   NS_ENSURE_SUCCESS(rv, rv);
 
   // build data URL string
   aDataURL = NS_LITERAL_STRING("data:") + type + NS_LITERAL_STRING(";base64,");
 
   uint64_t count;
@@ -850,18 +878,21 @@ HTMLCanvasElement::ToBlob(JSContext* aCx
         &aCallback,
         static_cast<void (BlobCallback::*)(Blob*, const char*)>(
           &BlobCallback::Call),
         nullptr,
         nullptr));
     return;
   }
 
+  // Check site-specific permission and display prompt if appropriate.
+  // If no permission, return all-white, opaque image data.
+  bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(OwnerDoc(), aCx);
   CanvasRenderingContextHelper::ToBlob(aCx, global, aCallback, aType,
-                                       aParams, aRv);
+                                       aParams, usePlaceholder, aRv);
 
 }
 
 OffscreenCanvas*
 HTMLCanvasElement::TransferControlToOffscreen(ErrorResult& aRv)
 {
   if (mCurrentContext) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
@@ -920,17 +951,18 @@ HTMLCanvasElement::MozGetAsFile(const ns
 
 nsresult
 HTMLCanvasElement::MozGetAsFileImpl(const nsAString& aName,
                                     const nsAString& aType,
                                     File** aResult)
 {
   nsCOMPtr<nsIInputStream> stream;
   nsAutoString type(aType);
-  nsresult rv = ExtractData(type, EmptyString(), getter_AddRefs(stream));
+  nsresult rv = ExtractData(nsContentUtils::GetCurrentJSContext(),
+                            type, EmptyString(), getter_AddRefs(stream));
   NS_ENSURE_SUCCESS(rv, rv);
 
   uint64_t imgSize;
   rv = stream->Available(&imgSize);
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_TRUE(imgSize <= UINT32_MAX, NS_ERROR_FILE_TOO_BIG);
 
   void* imgData = nullptr;
@@ -1241,17 +1273,18 @@ HTMLCanvasElement::MarkContextCleanForFr
 
 bool
 HTMLCanvasElement::IsContextCleanForFrameCapture()
 {
   return mCurrentContext && mCurrentContext->IsContextCleanForFrameCapture();
 }
 
 nsresult
-HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener)
+HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener,
+                                                bool aReturnPlaceholderData)
 {
   WeakPtr<FrameCaptureListener> listener = aListener;
 
   if (mRequestedFrameListeners.Contains(listener)) {
     return NS_OK;
   }
 
   if (!mRequestedFrameRefreshObserver) {
@@ -1280,17 +1313,19 @@ HTMLCanvasElement::RegisterFrameCaptureL
     }
 
     nsRefreshDriver* driver = context->RefreshDriver();
     if (!driver) {
       return NS_ERROR_FAILURE;
     }
 
     mRequestedFrameRefreshObserver =
-      new RequestedFrameRefreshObserver(this, driver);
+      new RequestedFrameRefreshObserver(this, driver, aReturnPlaceholderData);
+  } else {
+    mRequestedFrameRefreshObserver->SetReturnPlaceholderData(aReturnPlaceholderData);
   }
 
   mRequestedFrameListeners.AppendElement(listener);
   mRequestedFrameRefreshObserver->Register();
   return NS_OK;
 }
 
 bool
--- a/dom/html/HTMLCanvasElement.h
+++ b/dom/html/HTMLCanvasElement.h
@@ -257,17 +257,18 @@ public:
   /*
    * Register a FrameCaptureListener with this canvas.
    * The canvas hooks into the RefreshDriver while there are
    * FrameCaptureListeners registered.
    * The registered FrameCaptureListeners are stored as WeakPtrs, thus it's the
    * caller's responsibility to keep them alive. Once a registered
    * FrameCaptureListener is destroyed it will be automatically deregistered.
    */
-  nsresult RegisterFrameCaptureListener(FrameCaptureListener* aListener);
+  nsresult RegisterFrameCaptureListener(FrameCaptureListener* aListener,
+                                        bool aReturnPlaceholderData);
 
   /*
    * Returns true when there is at least one registered FrameCaptureListener
    * that has requested a frame capture.
    */
   bool IsFrameCaptureRequested() const;
 
   /*
@@ -343,17 +344,18 @@ protected:
 
   virtual JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   virtual nsIntSize GetWidthHeight() override;
 
   virtual already_AddRefed<nsICanvasRenderingContextInternal>
   CreateContext(CanvasContextType aContextType) override;
 
-  nsresult ExtractData(nsAString& aType,
+  nsresult ExtractData(JSContext* aCx,
+                       nsAString& aType,
                        const nsAString& aOptions,
                        nsIInputStream** aStream);
   nsresult ToDataURLImpl(JSContext* aCx,
                          const nsAString& aMimeType,
                          const JS::Value& aEncoderOptions,
                          nsAString& aDataURL);
   nsresult MozGetAsFileImpl(const nsAString& aName,
                             const nsAString& aType,
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -1855,16 +1855,18 @@ void HTMLMediaElement::AbortExistingLoad
   mIsRunningSelectResource = false;
 
   if (mTextTrackManager) {
     mTextTrackManager->NotifyReset();
   }
 
   mEventDeliveryPaused = false;
   mPendingEvents.Clear();
+
+  AssertReadyStateIsNothing();
 }
 
 void HTMLMediaElement::NoSupportedMediaSourceError(const nsACString& aErrorDetails)
 {
   if (mDecoder) {
     ShutdownDecoder();
   }
   mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails);
@@ -1916,16 +1918,18 @@ void HTMLMediaElement::QueueLoadFromSour
   }
 
   if (mDecoder) {
     // Reset readyState to HAVE_NOTHING since we're going to load a new decoder.
     ShutdownDecoder();
     ChangeReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING);
   }
 
+  AssertReadyStateIsNothing();
+
   ChangeDelayLoadStatus(true);
   ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_LOADING);
   RefPtr<Runnable> r = NewRunnableMethod("HTMLMediaElement::LoadFromSourceChildren",
                                          this, &HTMLMediaElement::LoadFromSourceChildren);
   RunInStableState(r);
 }
 
 void HTMLMediaElement::QueueSelectResourceTask()
@@ -2574,16 +2578,18 @@ nsresult HTMLMediaElement::LoadResource(
       decoder->Shutdown();
       LOG(LogLevel::Debug,
           ("%p Failed to load for decoder %p", this, decoder.get()));
       return rv;
     }
     return FinishDecoderSetup(decoder);
   }
 
+  AssertReadyStateIsNothing();
+
   RefPtr<ChannelLoader> loader = new ChannelLoader;
   nsresult rv = loader->Load(this);
   if (NS_SUCCEEDED(rv)) {
     mChannelLoader = loader.forget();
   }
   return rv;
 }
 
@@ -4863,22 +4869,46 @@ HTMLMediaElement::CanPlayType(const nsAS
 
   LOG(LogLevel::Debug, ("%p CanPlayType(%s) = \"%s\"", this,
                      NS_ConvertUTF16toUTF8(aType).get(),
                      NS_ConvertUTF16toUTF8(aResult).get()));
 
   return NS_OK;
 }
 
+void
+HTMLMediaElement::AssertReadyStateIsNothing()
+{
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+  if (mReadyState != nsIDOMHTMLMediaElement::HAVE_NOTHING) {
+    char buf[1024];
+    SprintfLiteral(buf,
+                   "readyState=%d networkState=%d mLoadWaitStatus=%d "
+                   "mSourceLoadCandidate=%d "
+                   "mIsLoadingFromSourceChildren=%d mPreloadAction=%d "
+                   "mSuspendedForPreloadNone=%d error=%d",
+                   int(mReadyState.Ref()),
+                   int(mNetworkState),
+                   int(mLoadWaitStatus),
+                   !!mSourceLoadCandidate,
+                   mIsLoadingFromSourceChildren,
+                   int(mPreloadAction),
+                   mSuspendedForPreloadNone,
+                   GetError() ? GetError()->Code() : 0);
+    MOZ_CRASH_UNSAFE_PRINTF("ReadyState should be HAVE_NOTHING! %s", buf);
+  }
+#endif
+}
+
 nsresult
 HTMLMediaElement::InitializeDecoderAsClone(ChannelMediaDecoder* aOriginal)
 {
   NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
   NS_ASSERTION(mDecoder == nullptr, "Shouldn't have a decoder");
-  MOZ_DIAGNOSTIC_ASSERT(mReadyState == nsIDOMHTMLMediaElement::HAVE_NOTHING);
+  AssertReadyStateIsNothing();
 
   MediaDecoderInit decoderInit(this,
                                mMuted ? 0.0 : mVolume,
                                mPreservesPitch,
                                mPlaybackRate,
                                mPreloadAction ==
                                  HTMLMediaElement::PRELOAD_METADATA,
                                mHasSuspendTaint,
@@ -4922,17 +4952,17 @@ HTMLMediaElement::SetupDecoder(DecoderTy
 
   return rv;
 }
 
 nsresult HTMLMediaElement::InitializeDecoderForChannel(nsIChannel* aChannel,
                                                        nsIStreamListener** aListener)
 {
   NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
-  MOZ_DIAGNOSTIC_ASSERT(mReadyState == nsIDOMHTMLMediaElement::HAVE_NOTHING);
+  AssertReadyStateIsNothing();
 
   DecoderDoctorDiagnostics diagnostics;
 
   nsAutoCString mimeType;
   aChannel->GetContentType(mimeType);
   NS_ASSERTION(!mimeType.IsEmpty(), "We should have the Content-Type.");
   NS_ConvertUTF8toUTF16 mimeUTF16(mimeType);
 
@@ -5036,16 +5066,20 @@ HTMLMediaElement::FinishDecoderSetup(Med
     mChannelLoader->Done();
     mChannelLoader = nullptr;
   }
 
   // We may want to suspend the new stream now.
   // This will also do an AddRemoveSelfReference.
   NotifyOwnerDocumentActivityChanged();
 
+  if (mPausedForInactiveDocumentOrChannel) {
+    mDecoder->Suspend();
+  }
+
   nsresult rv = NS_OK;
   if (!mPaused) {
     SetPlayedOrSeeked(true);
     if (!mPausedForInactiveDocumentOrChannel) {
       rv = mDecoder->Play();
     }
   }
 
@@ -6581,16 +6615,17 @@ void HTMLMediaElement::NotifyAddedSource
 {
   // If a source element is inserted as a child of a media element
   // that has no src attribute and whose networkState has the value
   // NETWORK_EMPTY, the user agent must invoke the media element's
   // resource selection algorithm.
   if (!HasAttr(kNameSpaceID_None, nsGkAtoms::src) &&
       mNetworkState == nsIDOMHTMLMediaElement::NETWORK_EMPTY)
   {
+    AssertReadyStateIsNothing();
     QueueSelectResourceTask();
   }
 
   // A load was paused in the resource selection algorithm, waiting for
   // a new source child to be added, resume the resource selection algorithm.
   if (mLoadWaitStatus == WAITING_FOR_SOURCE) {
     // Rest the flag so we don't queue multiple LoadFromSourceTask() when
     // multiple <source> are attached in an event loop.
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -1815,16 +1815,19 @@ private:
   // We keep track of these because the load algorithm resolves/rejects all
   // already-dispatched pending play promises.
   nsTArray<nsResolveOrRejectPendingPlayPromisesRunner*> mPendingPlayPromisesRunners;
 
   // A pending seek promise which is created at Seek() method call and is
   // resolved/rejected at AsyncResolveSeekDOMPromiseIfExists()/
   // AsyncRejectSeekDOMPromiseIfExists() methods.
   RefPtr<dom::Promise> mSeekDOMPromise;
+
+  // For debugging bug 1407148.
+  void AssertReadyStateIsNothing();
 };
 
 // Check if the context is chrome or has the debugger or tabs permission
 bool
 HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj);
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/indexedDB/test/unit/test_complex_keyPaths.js
+++ b/dom/indexedDB/test/unit/test_complex_keyPaths.js
@@ -80,16 +80,17 @@ function* testSteps()
     let test = " for objectStore test " + JSON.stringify(info);
     let indexName = JSON.stringify(info.keyPath);
     if (!stores[indexName]) {
       try {
         let objectStore = db.createObjectStore(indexName, { keyPath: info.keyPath });
         ok(!("exception" in info), "shouldn't throw" + test);
         is(JSON.stringify(objectStore.keyPath), JSON.stringify(info.keyPath),
            "correct keyPath property" + test);
+        // eslint-disable-next-line no-self-compare
         ok(objectStore.keyPath === objectStore.keyPath,
            "object identity should be preserved");
         stores[indexName] = objectStore;
       } catch (e) {
         ok("exception" in info, "should throw" + test);
         is(e.name, "SyntaxError", "expect a SyntaxError" + test);
         ok(e instanceof DOMException, "Got a DOM Exception" + test);
         is(e.code, DOMException.SYNTAX_ERR, "expect a syntax error" + test);
@@ -169,16 +170,17 @@ function* testSteps()
     let test = " for index test " + JSON.stringify(info);
     let indexName = JSON.stringify(info.keyPath);
     if (!indexes[indexName]) {
       try {
         let index = store.createIndex(indexName, info.keyPath);
         ok(!("exception" in info), "shouldn't throw" + test);
         is(JSON.stringify(index.keyPath), JSON.stringify(info.keyPath),
            "index has correct keyPath property" + test);
+        // eslint-disable-next-line no-self-compare
         ok(index.keyPath === index.keyPath,
            "object identity should be preserved");
         indexes[indexName] = index;
       } catch (e) {
         ok("exception" in info, "should throw" + test);
         is(e.name, "SyntaxError", "expect a SyntaxError" + test);
         ok(e instanceof DOMException, "Got a DOM Exception" + test);
         is(e.code, DOMException.SYNTAX_ERR, "expect a syntax error" + test);
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -589,16 +589,24 @@ parent:
      * When the session history is across multiple root docshells, this function
      * is used to notify parent that it needs to navigate to an entry out of
      * local index of the child.
      *
      * @param aGlobalIndex The global index of history entry to navigate to.
      */
     async RequestCrossBrowserNavigation(uint32_t aGlobalIndex);
 
+    /**
+     * This function is used to notify the parent that it should display a
+     * canvas permission prompt.
+     *
+     * @param aFirstPartyURI first party of the tab that is requesting access.
+     */
+    async ShowCanvasPermissionPrompt(nsCString aFirstPartyURI);
+
 child:
     /**
      * Notify the remote browser that it has been Show()n on this
      * side, with the given |visibleRect|.  This message is expected
      * to trigger creation of the remote browser's "widget".
      *
      * |Show()| and |Move()| take IntSizes rather than Rects because
      * content processes always render to a virtual <0, 0> top-left
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -3599,16 +3599,37 @@ TabParent::RecvRequestCrossBrowserNaviga
   nsCOMPtr<nsISupports> promise;
   if (NS_FAILED(frameLoader->RequestGroupedHistoryNavigation(aGlobalIndex,
                                                              getter_AddRefs(promise)))) {
     return IPC_FAIL_NO_REASON(this);
   }
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+TabParent::RecvShowCanvasPermissionPrompt(const nsCString& aFirstPartyURI)
+{
+  nsCOMPtr<nsIBrowser> browser = do_QueryInterface(mFrameElement);
+  if (!browser) {
+    // If the tab is being closed, the browser may not be available.
+    // In this case we can ignore the request.
+    return IPC_OK();
+  }
+  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+  if (!os) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  nsresult rv = os->NotifyObservers(browser, "canvas-permissions-prompt",
+                                    NS_ConvertUTF8toUTF16(aFirstPartyURI).get());
+  if (NS_FAILED(rv)) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  return IPC_OK();
+}
+
 void
 TabParent::LiveResizeStarted()
 {
   SuppressDisplayport(true);
 }
 
 void
 TabParent::LiveResizeStopped()
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -643,16 +643,17 @@ protected:
 
   virtual mozilla::ipc::IPCResult RecvGetTabCount(uint32_t* aValue) override;
 
   virtual mozilla::ipc::IPCResult RecvSHistoryUpdate(const uint32_t& aCount,
                                                      const uint32_t& aLocalIndex,
                                                      const bool& aTruncate) override;
 
   virtual mozilla::ipc::IPCResult RecvRequestCrossBrowserNavigation(const uint32_t& aGlobalIndex) override;
+  virtual mozilla::ipc::IPCResult RecvShowCanvasPermissionPrompt(const nsCString& aFirstPartyURI) override;
 
   ContentCacheInParent mContentCache;
 
   nsIntRect mRect;
   ScreenIntSize mDimensions;
   ScreenOrientationInternal mOrientation;
   float mDPI;
   int32_t mRounding;
--- a/dom/media/MediaInfo.h
+++ b/dom/media/MediaInfo.h
@@ -284,21 +284,28 @@ public:
   // container.
   gfx::IntRect ScaledImageRect(int64_t aWidth, int64_t aHeight) const
   {
     if ((aWidth == mImage.width && aHeight == mImage.height) ||
         !mImage.width ||
         !mImage.height) {
       return ImageRect();
     }
+
     gfx::IntRect imageRect = ImageRect();
+    int64_t w = (aWidth * imageRect.Width()) / mImage.width;
+    int64_t h = (aHeight * imageRect.Height()) / mImage.height;
+    if (!w || !h) {
+      return imageRect;
+    }
+
     imageRect.x = (imageRect.x * aWidth) / mImage.width;
     imageRect.y = (imageRect.y * aHeight) / mImage.height;
-    imageRect.SetWidth((aWidth * imageRect.Width()) / mImage.width);
-    imageRect.SetHeight((aHeight * imageRect.Height()) / mImage.height);
+    imageRect.SetWidth(w);
+    imageRect.SetHeight(h);
     return imageRect;
   }
 
   Rotation ToSupportedRotation(int32_t aDegree) const
   {
     switch (aDegree) {
       case 90:
         return kDegree_90;
--- a/dom/media/imagecapture/CaptureTask.cpp
+++ b/dom/media/imagecapture/CaptureTask.cpp
@@ -152,16 +152,17 @@ CaptureTask::SetCurrentFrames(const Vide
       nsresult rv;
       nsAutoString type(NS_LITERAL_STRING("image/jpeg"));
       nsAutoString options;
       rv = dom::ImageEncoder::ExtractDataFromLayersImageAsync(
                                 type,
                                 options,
                                 false,
                                 image,
+                                false,
                                 new EncodeComplete(this));
       if (NS_FAILED(rv)) {
         PostTrackEndEvent();
       }
       return;
     }
   }
 }
--- a/dom/media/mediasource/TrackBuffersManager.cpp
+++ b/dom/media/mediasource/TrackBuffersManager.cpp
@@ -91,28 +91,31 @@ TrackBuffersManager::TrackBuffersManager
   : mInputBuffer(new MediaByteBuffer)
   , mBufferFull(false)
   , mFirstInitializationSegmentReceived(false)
   , mNewMediaSegmentStarted(false)
   , mActiveTrack(false)
   , mType(aType)
   , mParser(ContainerParser::CreateForMIMEType(aType))
   , mProcessedInput(0)
-  , mTaskQueue(aParentDecoder->GetDemuxer()->GetTaskQueue())
-  , mParentDecoder(
-      new nsMainThreadPtrHolder<MediaSourceDecoder>(
-        "TrackBuffersManager::mParentDecoder", aParentDecoder, false /* strict */))
+  , mParentDecoder(new nsMainThreadPtrHolder<MediaSourceDecoder>(
+      "TrackBuffersManager::mParentDecoder",
+      aParentDecoder,
+      false /* strict */))
   , mAbstractMainThread(aParentDecoder->AbstractMainThread())
   , mEnded(false)
-  , mVideoEvictionThreshold(Preferences::GetUint("media.mediasource.eviction_threshold.video",
-                                                 100 * 1024 * 1024))
-  , mAudioEvictionThreshold(Preferences::GetUint("media.mediasource.eviction_threshold.audio",
-                                                 20 * 1024 * 1024))
+  , mVideoEvictionThreshold(
+      Preferences::GetUint("media.mediasource.eviction_threshold.video",
+                           100 * 1024 * 1024))
+  , mAudioEvictionThreshold(
+      Preferences::GetUint("media.mediasource.eviction_threshold.audio",
+                           20 * 1024 * 1024))
   , mEvictionState(EvictionState::NO_EVICTION_NEEDED)
-  , mMonitor("TrackBuffersManager")
+  , mMutex("TrackBuffersManager")
+  , mTaskQueue(aParentDecoder->GetDemuxer()->GetTaskQueue())
 {
   MOZ_ASSERT(NS_IsMainThread(), "Must be instanciated on the main thread");
 }
 
 TrackBuffersManager::~TrackBuffersManager()
 {
   ShutdownDemuxers();
 }
@@ -122,18 +125,22 @@ TrackBuffersManager::AppendData(already_
                                 const SourceBufferAttributes& aAttributes)
 {
   MOZ_ASSERT(NS_IsMainThread());
   RefPtr<MediaByteBuffer> data(aData);
   MSE_DEBUG("Appending %zu bytes", data->Length());
 
   mEnded = false;
 
-  return InvokeAsync(GetTaskQueue(), this, __func__,
-    &TrackBuffersManager::DoAppendData, data.forget(), aAttributes);
+  return InvokeAsync(static_cast<AbstractThread*>(GetTaskQueueSafe().get()),
+                     this,
+                     __func__,
+                     &TrackBuffersManager::DoAppendData,
+                     data.forget(),
+                     aAttributes);
 }
 
 RefPtr<TrackBuffersManager::AppendPromise>
 TrackBuffersManager::DoAppendData(already_AddRefed<MediaByteBuffer> aData,
                                   const SourceBufferAttributes& aAttributes)
 {
   RefPtr<AppendBufferTask> task = new AppendBufferTask(Move(aData), aAttributes);
   RefPtr<AppendPromise> p = task->mPromise.Ensure(__func__);
@@ -143,42 +150,50 @@ TrackBuffersManager::DoAppendData(alread
 }
 
 void
 TrackBuffersManager::QueueTask(SourceBufferTask* aTask)
 {
   // The source buffer is a wrapped native, it would be unlinked twice and so
   // the TrackBuffersManager::Detach() would also be called twice. Since the
   // detach task has been done before, we could ignore this task.
-  if (!GetTaskQueue()) {
+  RefPtr<AutoTaskQueue> taskQueue = GetTaskQueueSafe();
+  if (!taskQueue) {
     MOZ_ASSERT(aTask->GetType() == SourceBufferTask::Type::Detach,
                "only detach task could happen here!");
     MSE_DEBUG("Could not queue the task '%s' without task queue",
               aTask->GetTypeName());
     return;
   }
 
-  if (!OnTaskQueue()) {
-    GetTaskQueue()->Dispatch(NewRunnableMethod<RefPtr<SourceBufferTask>>(
+  if (!taskQueue->IsCurrentThreadIn()) {
+    taskQueue->Dispatch(NewRunnableMethod<RefPtr<SourceBufferTask>>(
       "TrackBuffersManager::QueueTask",
       this,
       &TrackBuffersManager::QueueTask,
       aTask));
     return;
   }
-  MOZ_ASSERT(OnTaskQueue());
   mQueue.Push(aTask);
   ProcessTasks();
 }
 
 void
 TrackBuffersManager::ProcessTasks()
 {
+  // ProcessTask is always called OnTaskQueue, however it is possible that it is
+  // called once again after a first Detach task has run, in which case
+  // mTaskQueue would be null.
+  // This can happen under two conditions:
+  // 1- Two Detach tasks were queued in a row due to a double cycle collection.
+  // 2- An call to ProcessTasks() had queued another run of ProcessTasks while
+  //    a Detach task is pending.
+  // We handle these two cases by aborting early.
   // A second Detach task was queued, prior the first one running, ignore it.
-  if (!GetTaskQueue()) {
+  if (!mTaskQueue) {
     RefPtr<SourceBufferTask> task = mQueue.Pop();
     if (!task) {
       return;
     }
     MOZ_RELEASE_ASSERT(task->GetType() == SourceBufferTask::Type::Detach,
                        "only detach task could happen here!");
     MSE_DEBUG("Could not process the task '%s' after detached",
               task->GetTypeName());
@@ -229,27 +244,27 @@ TrackBuffersManager::ProcessTasks()
     case Type::Abort:
       // not handled yet, and probably never.
       break;
     case Type::Reset:
       CompleteResetParserState();
       break;
     case Type::Detach:
       mCurrentInputBuffer = nullptr;
-      mTaskQueue = nullptr;
       MOZ_DIAGNOSTIC_ASSERT(mQueue.Length() == 0,
                             "Detach task must be the last");
       mVideoTracks.Reset();
       mAudioTracks.Reset();
       ShutdownDemuxers();
+      ResetTaskQueue();
       return;
     default:
       NS_WARNING("Invalid Task");
   }
-  GetTaskQueue()->Dispatch(
+  TaskQueueFromTaskQueue()->Dispatch(
     NewRunnableMethod("TrackBuffersManager::ProcessTasks",
                       this,
                       &TrackBuffersManager::ProcessTasks));
 }
 
 // The MSE spec requires that we abort the current SegmentParserLoop
 // which is then followed by a call to ResetParserState.
 // However due to our asynchronous design this causes inherent difficulties.
@@ -290,17 +305,19 @@ TrackBuffersManager::ResetParserState(So
 RefPtr<TrackBuffersManager::RangeRemovalPromise>
 TrackBuffersManager::RangeRemoval(TimeUnit aStart, TimeUnit aEnd)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MSE_DEBUG("From %.2f to %.2f", aStart.ToSeconds(), aEnd.ToSeconds());
 
   mEnded = false;
 
-  return InvokeAsync(GetTaskQueue(), this, __func__,
+  return InvokeAsync(static_cast<AbstractThread*>(GetTaskQueueSafe().get()),
+                     this,
+                     __func__,
                      &TrackBuffersManager::CodedFrameRemovalWithPromise,
                      TimeInterval(aStart, aEnd));
 }
 
 TrackBuffersManager::EvictDataResult
 TrackBuffersManager::EvictData(const TimeUnit& aPlaybackTime, int64_t aSize)
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -346,17 +363,17 @@ TrackBuffersManager::EvictData(const Tim
 
 TimeIntervals
 TrackBuffersManager::Buffered() const
 {
   MSE_DEBUG("");
 
   // http://w3c.github.io/media-source/index.html#widl-SourceBuffer-buffered
 
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
   nsTArray<const TimeIntervals*> tracks;
   if (HasVideo()) {
     tracks.AppendElement(&mVideoBufferedRanges);
   }
   if (HasAudio()) {
     tracks.AppendElement(&mAudioBufferedRanges);
   }
 
@@ -630,17 +647,17 @@ TrackBuffersManager::CodedFrameRemoval(T
   }
 
   return dataRemoved;
 }
 
 void
 TrackBuffersManager::UpdateBufferedRanges()
 {
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
 
   mVideoBufferedRanges = mVideoTracks.mSanitizedBufferedRanges;
   mAudioBufferedRanges = mAudioTracks.mSanitizedBufferedRanges;
 
 #if DEBUG
   if (HasVideo()) {
     MSE_DEBUG("after video ranges=%s",
               DumpTimeRanges(mVideoTracks.mBufferedRanges).get());
@@ -770,26 +787,27 @@ TrackBuffersManager::SegmentParserLoop()
           NeedMoreData();
           return;
         }
       }
 
       // 3. If the input buffer contains one or more complete coded frames, then run the coded frame processing algorithm.
       RefPtr<TrackBuffersManager> self = this;
       CodedFrameProcessing()
-        ->Then(GetTaskQueue(), __func__,
-               [self] (bool aNeedMoreData) {
+        ->Then(TaskQueueFromTaskQueue(),
+               __func__,
+               [self](bool aNeedMoreData) {
                  self->mProcessingRequest.Complete();
                  if (aNeedMoreData) {
                    self->NeedMoreData();
                  } else {
                    self->ScheduleSegmentParserLoop();
                  }
                },
-               [self] (const MediaResult& aRejectValue) {
+               [self](const MediaResult& aRejectValue) {
                  self->mProcessingRequest.Complete();
                  self->RejectAppend(aRejectValue, __func__);
                })
         ->Track(mProcessingRequest);
       return;
     }
   }
 }
@@ -820,17 +838,18 @@ TrackBuffersManager::RejectAppend(const 
   mSourceBufferAttributes = nullptr;
   mCurrentTask = nullptr;
   ProcessTasks();
 }
 
 void
 TrackBuffersManager::ScheduleSegmentParserLoop()
 {
-  GetTaskQueue()->Dispatch(
+  MOZ_ASSERT(OnTaskQueue());
+  TaskQueueFromTaskQueue()->Dispatch(
     NewRunnableMethod("TrackBuffersManager::SegmentParserLoop",
                       this,
                       &TrackBuffersManager::SegmentParserLoop));
 }
 
 void
 TrackBuffersManager::ShutdownDemuxers()
 {
@@ -869,30 +888,32 @@ TrackBuffersManager::CreateDemuxerforMIM
 #endif
   NS_WARNING("Not supported (yet)");
 }
 
 // We reset the demuxer by creating a new one and initializing it.
 void
 TrackBuffersManager::ResetDemuxingState()
 {
+  MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mParser && mParser->HasInitData());
   RecreateParser(true);
   mCurrentInputBuffer = new SourceBufferResource();
   // The demuxer isn't initialized yet ; we don't want to notify it
   // that data has been appended yet ; so we simply append the init segment
   // to the resource.
   mCurrentInputBuffer->AppendData(mParser->InitData());
   CreateDemuxerforMIMEType();
   if (!mInputDemuxer) {
     RejectAppend(NS_ERROR_FAILURE, __func__);
     return;
   }
   mInputDemuxer->Init()
-    ->Then(GetTaskQueue(), __func__,
+    ->Then(TaskQueueFromTaskQueue(),
+           __func__,
            this,
            &TrackBuffersManager::OnDemuxerResetDone,
            &TrackBuffersManager::OnDemuxerInitFailed)
     ->Track(mDemuxerInitRequest);
 }
 
 void
 TrackBuffersManager::OnDemuxerResetDone(const MediaResult& aResult)
@@ -953,16 +974,17 @@ TrackBuffersManager::AppendDataToCurrent
   MOZ_ASSERT(mCurrentInputBuffer);
   mCurrentInputBuffer->AppendData(aData);
   mInputDemuxer->NotifyDataArrived();
 }
 
 void
 TrackBuffersManager::InitializationSegmentReceived()
 {
+  MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mParser->HasCompleteInitData());
 
   int64_t endInit = mParser->InitSegmentRange().mEnd;
   if (mInputBuffer->Length() > mProcessedInput ||
       int64_t(mProcessedInput - mInputBuffer->Length()) > endInit) {
     // Something is not quite right with the data appended. Refuse it.
     RejectAppend(MediaResult(NS_ERROR_FAILURE,
                              "Invalid state following initialization segment"),
@@ -984,17 +1006,18 @@ TrackBuffersManager::InitializationSegme
   }
   CreateDemuxerforMIMEType();
   if (!mInputDemuxer) {
     NS_WARNING("TODO type not supported");
     RejectAppend(NS_ERROR_DOM_NOT_SUPPORTED_ERR, __func__);
     return;
   }
   mInputDemuxer->Init()
-    ->Then(GetTaskQueue(), __func__,
+    ->Then(TaskQueueFromTaskQueue(),
+           __func__,
            this,
            &TrackBuffersManager::OnDemuxerInitDone,
            &TrackBuffersManager::OnDemuxerInitFailed)
     ->Track(mDemuxerInitRequest);
 }
 
 void
 TrackBuffersManager::OnDemuxerInitDone(const MediaResult& aResult)
@@ -1172,17 +1195,17 @@ TrackBuffersManager::OnDemuxerInitDone(c
     }
     info.mCrypto = *crypto;
     // We clear our crypto init data array, so the MediaFormatReader will
     // not emit an encrypted event for the same init data again.
     info.mCrypto.mInitDatas.Clear();
   }
 
   {
-    MonitorAutoLock mon(mMonitor);
+    MutexAutoLock mut(mMutex);
     mInfo = info;
   }
 
   // We now have a valid init data ; we can store it for later use.
   mInitData = mParser->InitData();
 
   // 3. Remove the initialization segment bytes from the beginning of the input buffer.
   // This step has already been done in InitializationSegmentReceived when we
@@ -1287,17 +1310,19 @@ void
 TrackBuffersManager::DoDemuxVideo()
 {
   MOZ_ASSERT(OnTaskQueue());
   if (!HasVideo()) {
     DoDemuxAudio();
     return;
   }
   mVideoTracks.mDemuxer->GetSamples(-1)
-    ->Then(GetTaskQueue(), __func__, this,
+    ->Then(TaskQueueFromTaskQueue(),
+           __func__,
+           this,
            &TrackBuffersManager::OnVideoDemuxCompleted,
            &TrackBuffersManager::OnVideoDemuxFailed)
     ->Track(mVideoTracks.mDemuxRequest);
 }
 
 void
 TrackBuffersManager::MaybeDispatchEncryptedEvent(
   const nsTArray<RefPtr<MediaRawData>>& aSamples)
@@ -1329,17 +1354,19 @@ void
 TrackBuffersManager::DoDemuxAudio()
 {
   MOZ_ASSERT(OnTaskQueue());
   if (!HasAudio()) {
     CompleteCodedFrameProcessing();
     return;
   }
   mAudioTracks.mDemuxer->GetSamples(-1)
-    ->Then(GetTaskQueue(), __func__, this,
+    ->Then(TaskQueueFromTaskQueue(),
+           __func__,
+           this,
            &TrackBuffersManager::OnAudioDemuxCompleted,
            &TrackBuffersManager::OnAudioDemuxFailed)
     ->Track(mAudioTracks.mDemuxRequest);
 }
 
 void
 TrackBuffersManager::OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples)
 {
@@ -1866,17 +1893,17 @@ TrackBuffersManager::InsertFrames(TrackB
   }
 }
 
 void
 TrackBuffersManager::UpdateHighestTimestamp(TrackData& aTrackData,
                                             const media::TimeUnit& aHighestTime)
 {
   if (aHighestTime > aTrackData.mHighestStartTimestamp) {
-    MonitorAutoLock mon(mMonitor);
+    MutexAutoLock mut(mMutex);
     aTrackData.mHighestStartTimestamp = aHighestTime;
   }
 }
 
 uint32_t
 TrackBuffersManager::RemoveFrames(const TimeIntervals& aIntervals,
                                   TrackData& aTrackData,
                                   uint32_t aStartIndex)
@@ -1960,17 +1987,17 @@ TrackBuffersManager::RemoveFrames(const 
     } else if (aTrackData.mNextGetSampleIndex.ref() > lastRemovedIndex) {
       uint32_t samplesRemoved = lastRemovedIndex - firstRemovedIndex.ref() + 1;
       aTrackData.mNextGetSampleIndex.ref() -= samplesRemoved;
       if (aTrackData.mEvictionIndex.mLastIndex > lastRemovedIndex) {
         MOZ_DIAGNOSTIC_ASSERT(
           aTrackData.mEvictionIndex.mLastIndex >= samplesRemoved &&
           aTrackData.mEvictionIndex.mEvictable >= sizeRemoved,
           "Invalid eviction index");
-        MonitorAutoLock mon(mMonitor);
+        MutexAutoLock mut(mMutex);
         aTrackData.mEvictionIndex.mLastIndex -= samplesRemoved;
         aTrackData.mEvictionIndex.mEvictable -= sizeRemoved;
       } else {
         ResetEvictionIndex(aTrackData);
       }
     }
   }
 
@@ -1999,17 +2026,17 @@ TrackBuffersManager::RemoveFrames(const 
     // The sample with the highest presentation time got removed.
     // Rescan the trackbuffer to determine the new one.
     TimeUnit highestStartTime;
     for (const auto& sample : data) {
       if (sample->mTime > highestStartTime) {
         highestStartTime = sample->mTime;
       }
     }
-    MonitorAutoLock mon(mMonitor);
+    MutexAutoLock mut(mMutex);
     aTrackData.mHighestStartTimestamp = highestStartTime;
   }
 
   return firstRemovedIndex.ref();
 }
 
 void
 TrackBuffersManager::RecreateParser(bool aReuseInitData)
@@ -2061,17 +2088,17 @@ TrackBuffersManager::SetAppendState(Appe
   MSE_DEBUG("AppendState changed from %s to %s",
             AppendStateToStr(mSourceBufferAttributes->GetAppendState()), AppendStateToStr(aAppendState));
   mSourceBufferAttributes->SetAppendState(aAppendState);
 }
 
 MediaInfo
 TrackBuffersManager::GetMetadata() const
 {
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
   return mInfo;
 }
 
 const TimeIntervals&
 TrackBuffersManager::Buffered(TrackInfo::TrackType aTrack) const
 {
   MOZ_ASSERT(OnTaskQueue());
   return GetTracksData(aTrack).mBufferedRanges;
@@ -2082,67 +2109,67 @@ TrackBuffersManager::HighestStartTime(Tr
 {
   MOZ_ASSERT(OnTaskQueue());
   return GetTracksData(aTrack).mHighestStartTimestamp;
 }
 
 TimeIntervals
 TrackBuffersManager::SafeBuffered(TrackInfo::TrackType aTrack) const
 {
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
   return aTrack == TrackInfo::kVideoTrack
     ? mVideoBufferedRanges
     : mAudioBufferedRanges;
 }
 
 TimeUnit
 TrackBuffersManager::HighestStartTime() const
 {
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
   TimeUnit highestStartTime;
   for (auto& track : GetTracksList()) {
     highestStartTime =
       std::max(track->mHighestStartTimestamp, highestStartTime);
   }
   return highestStartTime;
 }
 
 TimeUnit
 TrackBuffersManager::HighestEndTime() const
 {
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
 
   nsTArray<const TimeIntervals*> tracks;
   if (HasVideo()) {
     tracks.AppendElement(&mVideoBufferedRanges);
   }
   if (HasAudio()) {
     tracks.AppendElement(&mAudioBufferedRanges);
   }
   return HighestEndTime(tracks);
 }
 
 TimeUnit
 TrackBuffersManager::HighestEndTime(
   nsTArray<const TimeIntervals*>& aTracks) const
 {
-  mMonitor.AssertCurrentThreadOwns();
+  mMutex.AssertCurrentThreadOwns();
 
   TimeUnit highestEndTime;
 
   for (const auto& trackRanges : aTracks) {
     highestEndTime = std::max(trackRanges->GetEnd(), highestEndTime);
   }
   return highestEndTime;
 }
 
 void
 TrackBuffersManager::ResetEvictionIndex(TrackData& aTrackData)
 {
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
   aTrackData.mEvictionIndex.Reset();
 }
 
 void
 TrackBuffersManager::UpdateEvictionIndex(TrackData& aTrackData,
                                          uint32_t currentIndex)
 {
   uint32_t evictable = 0;
@@ -2152,17 +2179,17 @@ TrackBuffersManager::UpdateEvictionIndex
   MOZ_DIAGNOSTIC_ASSERT(currentIndex == data.Length() ||
                         data[currentIndex]->mKeyframe,"Must stop at keyframe");
 
   for (uint32_t i = aTrackData.mEvictionIndex.mLastIndex; i < currentIndex;
        i++) {
     evictable += data[i]->ComputedSizeOfIncludingThis();
   }
   aTrackData.mEvictionIndex.mLastIndex = currentIndex;
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
   aTrackData.mEvictionIndex.mEvictable += evictable;
 }
 
 const TrackBuffersManager::TrackBuffer&
 TrackBuffersManager::GetTrackBuffer(TrackInfo::TrackType aTrack) const
 {
   MOZ_ASSERT(OnTaskQueue());
   return GetTracksData(aTrack).GetTrackBuffer();
@@ -2530,17 +2557,17 @@ TrackBuffersManager::FindCurrentPosition
 
   // Still not found.
   return -1;
 }
 
 uint32_t
 TrackBuffersManager::Evictable(TrackInfo::TrackType aTrack) const
 {
-  MonitorAutoLock mon(mMonitor);
+  MutexAutoLock mut(mMutex);
   return GetTracksData(aTrack).mEvictionIndex.mEvictable;
 }
 
 TimeUnit
 TrackBuffersManager::GetNextRandomAccessPoint(TrackInfo::TrackType aTrack,
                                               const TimeUnit& aFuzz)
 {
   MOZ_ASSERT(OnTaskQueue());
--- a/dom/media/mediasource/TrackBuffersManager.h
+++ b/dom/media/mediasource/TrackBuffersManager.h
@@ -4,17 +4,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef MOZILLA_TRACKBUFFERSMANAGER_H_
 #define MOZILLA_TRACKBUFFERSMANAGER_H_
 
 #include "mozilla/Atomics.h"
 #include "mozilla/Maybe.h"
-#include "mozilla/Monitor.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/NotNull.h"
 #include "AutoTaskQueue.h"
 
 #include "MediaContainerType.h"
 #include "MediaData.h"
 #include "MediaDataDemuxer.h"
 #include "MediaResult.h"
 #include "MediaSourceDecoder.h"
 #include "SourceBufferTask.h"
@@ -30,48 +31,48 @@ class MediaByteBuffer;
 class MediaRawData;
 class MediaSourceDemuxer;
 class SourceBufferResource;
 
 class SourceBufferTaskQueue
 {
 public:
   SourceBufferTaskQueue()
-  : mMonitor("SourceBufferTaskQueue")
+  : mMutex("SourceBufferTaskQueue")
   {}
   ~SourceBufferTaskQueue()
   {
     MOZ_ASSERT(mQueue.IsEmpty(), "All tasks must have been processed");
   }
 
   void Push(SourceBufferTask* aTask)
   {
-    MonitorAutoLock mon(mMonitor);
+    MutexAutoLock mut(mMutex);
     mQueue.AppendElement(aTask);
   }
 
   already_AddRefed<SourceBufferTask> Pop()
   {
-    MonitorAutoLock mon(mMonitor);
+    MutexAutoLock mut(mMutex);
     if (!mQueue.Length()) {
       return nullptr;
     }
     RefPtr<SourceBufferTask> task = Move(mQueue[0]);
     mQueue.RemoveElementAt(0);
     return task.forget();
   }
 
   nsTArray<SourceBufferTask>::size_type Length() const
   {
-    MonitorAutoLock mon(mMonitor);
+    MutexAutoLock mut(mMutex);
     return mQueue.Length();
   }
 
 private:
-  mutable Monitor mMonitor;
+  mutable Mutex mMutex;
   nsTArray<RefPtr<SourceBufferTask>> mQueue;
 };
 
 class TrackBuffersManager
 {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TrackBuffersManager);
 
@@ -450,26 +451,39 @@ private:
       default:
         return mAudioTracks;
     }
   }
   TrackData mVideoTracks;
   TrackData mAudioTracks;
 
   // TaskQueue methods and objects.
-  AbstractThread* GetTaskQueue() const
+  RefPtr<AutoTaskQueue> GetTaskQueueSafe() const
   {
+    MutexAutoLock mut(mMutex);
     return mTaskQueue;
   }
+  NotNull<AbstractThread*> TaskQueueFromTaskQueue() const
+  {
+#ifdef DEBUG
+    RefPtr<AutoTaskQueue> taskQueue = GetTaskQueueSafe();
+    MOZ_ASSERT(taskQueue && taskQueue->IsCurrentThreadIn());
+#endif
+    return WrapNotNull(mTaskQueue.get());
+  }
   bool OnTaskQueue() const
   {
-    MOZ_RELEASE_ASSERT(GetTaskQueue());
-    return GetTaskQueue()->IsCurrentThreadIn();
+    auto taskQueue = TaskQueueFromTaskQueue();
+    return taskQueue->IsCurrentThreadIn();
   }
-  RefPtr<AutoTaskQueue> mTaskQueue;
+  void ResetTaskQueue()
+  {
+    MutexAutoLock mut(mMutex);
+    mTaskQueue = nullptr;
+  }
 
   // SourceBuffer Queues and running context.
   SourceBufferTaskQueue mQueue;
   void QueueTask(SourceBufferTask* aTask);
   void ProcessTasks();
   // Set if the TrackBuffersManager is currently processing a task.
   // At this stage, this task is always a AppendBufferTask.
   RefPtr<SourceBufferTask> mCurrentTask;
@@ -501,17 +515,21 @@ private:
   {
     NO_EVICTION_NEEDED,
     EVICTION_NEEDED,
     EVICTION_COMPLETED,
   };
   Atomic<EvictionState> mEvictionState;
 
   // Monitor to protect following objects accessed across multiple threads.
-  mutable Monitor mMonitor;
+  mutable Mutex mMutex;
+  // mTaskQueue is only ever written after construction on the task queue.
+  // As such, it can be accessed while on task queue without the need for the
+  // mutex.
+  RefPtr<AutoTaskQueue> mTaskQueue;
   // Stable audio and video track time ranges.
   media::TimeIntervals mVideoBufferedRanges;
   media::TimeIntervals mAudioBufferedRanges;
   // MediaInfo of the first init segment read.
   MediaInfo mInfo;
 };
 
 } // namespace mozilla
--- a/gfx/2d/BezierUtils.cpp
+++ b/gfx/2d/BezierUtils.cpp
@@ -321,19 +321,22 @@ CalculateDistanceToEllipticArc(const Poi
   Float d = normal.y / height;
 
   Float A = b * b + d * d;
   Float B = a * b + c * d;
   Float C = a * a + c * c - 1;
 
   Float S = sqrt(B * B - A * C);
 
-  Float n1 = (- B + S) / A;
-  Float n2 = (- B - S) / A;
+  Float n1 = - B + S;
+  Float n2 = - B - S;
 
-  MOZ_ASSERT(n1 >= 0);
-  MOZ_ASSERT(n2 >= 0);
+#ifdef DEBUG
+  Float epsilon = (Float) 0.001;
+  MOZ_ASSERT(n1 >= -epsilon);
+  MOZ_ASSERT(n2 >= -epsilon);
+#endif
 
-  return n1 < n2 ? n1 : n2;
+  return std::max((n1 < n2 ? n1 : n2) / A, (Float) 0.0);
 }
 
 } // namespace gfx
 } // namespace mozilla
--- a/gfx/thebes/PrintTarget.cpp
+++ b/gfx/thebes/PrintTarget.cpp
@@ -10,16 +10,23 @@
 #include "cairo-quartz.h"
 #endif
 #ifdef CAIRO_HAS_WIN32_SURFACE
 #include "cairo-win32.h"
 #endif
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/HelpersCairo.h"
 #include "mozilla/gfx/Logging.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsUTF8Utils.h"
+
+// IPP spec disallow the job-name which is over 255 characters.
+// RFC: https://tools.ietf.org/html/rfc2911#section-4.1.2
+#define IPP_JOB_NAME_LIMIT_LENGTH 255
 
 namespace mozilla {
 namespace gfx {
 
 PrintTarget::PrintTarget(cairo_surface_t* aCairoSurface, const IntSize& aSize)
   : mCairoSurface(aCairoSurface)
   , mSize(aSize)
   , mIsFinished(false)
@@ -155,16 +162,43 @@ PrintTarget::GetReferenceDrawTarget(Draw
 #endif
 
     return do_AddRef(mRecordingRefDT);
   }
 
   return do_AddRef(mRefDT);
 }
 
+/* static */
+void
+PrintTarget::AdjustPrintJobNameForIPP(const nsAString& aJobName,
+                                      nsCString& aAdjustedJobName)
+{
+  CopyUTF16toUTF8(aJobName, aAdjustedJobName);
+
+  if (aAdjustedJobName.Length() > IPP_JOB_NAME_LIMIT_LENGTH) {
+    uint32_t length =
+      RewindToPriorUTF8Codepoint(aAdjustedJobName.get(),
+                                 (IPP_JOB_NAME_LIMIT_LENGTH - 3U));
+    aAdjustedJobName.SetLength(length);
+    aAdjustedJobName.AppendLiteral("...");
+  }
+}
+
+/* static */
+void
+PrintTarget::AdjustPrintJobNameForIPP(const nsAString& aJobName,
+                                      nsString& aAdjustedJobName)
+{
+  nsAutoCString jobName;
+  AdjustPrintJobNameForIPP(aJobName, jobName);
+
+  CopyUTF8toUTF16(jobName, aAdjustedJobName);
+}
+
 /* static */ already_AddRefed<DrawTarget>
 PrintTarget::CreateWrapAndRecordDrawTarget(DrawEventRecorder* aRecorder,
                                        DrawTarget* aDrawTarget)
 {
   MOZ_ASSERT(aRecorder);
   MOZ_ASSERT(aDrawTarget);
 
   RefPtr<DrawTarget> dt;
--- a/gfx/thebes/PrintTarget.h
+++ b/gfx/thebes/PrintTarget.h
@@ -131,16 +131,21 @@ public:
   /**
    * Returns a reference DrawTarget. Unlike MakeDrawTarget, this method is not
    * restricted to being called between BeginPage()/EndPage() calls, and the
    * returned DrawTarget it is still valid to use after EndPage() has been
    * called.
    */
   virtual already_AddRefed<DrawTarget> GetReferenceDrawTarget(DrawEventRecorder* aRecorder);
 
+  static void AdjustPrintJobNameForIPP(const nsAString& aJobName,
+                                       nsCString& aAdjustedJobName);
+  static void AdjustPrintJobNameForIPP(const nsAString& aJobName,
+                                       nsString& aAdjustedJobName);
+
 protected:
 
   // Only created via subclass's constructors
   explicit PrintTarget(cairo_surface_t* aCairoSurface, const IntSize& aSize);
 
   // Protected because we're refcounted
   virtual ~PrintTarget();
 
--- a/layout/inspector/ServoStyleRuleMap.cpp
+++ b/layout/inspector/ServoStyleRuleMap.cpp
@@ -6,16 +6,17 @@
 
 #include "mozilla/ServoStyleRuleMap.h"
 
 #include "mozilla/css/GroupRule.h"
 #include "mozilla/IntegerRange.h"
 #include "mozilla/ServoStyleRule.h"
 #include "mozilla/ServoStyleSet.h"
 #include "mozilla/ServoImportRule.h"
+#include "mozilla/StyleSheetInlines.h"
 
 #include "nsDocument.h"
 #include "nsStyleSheetService.h"
 
 namespace mozilla {
 
 ServoStyleRuleMap::ServoStyleRuleMap(ServoStyleSet* aStyleSet)
   : mStyleSet(aStyleSet)
@@ -170,13 +171,16 @@ ServoStyleRuleMap::FillTableFromRuleList
     FillTableFromRule(aRuleList->GetRule(i));
   }
 }
 
 void
 ServoStyleRuleMap::FillTableFromStyleSheet(ServoStyleSheet* aSheet)
 {
   if (aSheet->IsComplete()) {
-    FillTableFromRuleList(aSheet->GetCssRulesInternal());
+    // XBL stylesheets are not expected to ever change, so it's a waste
+    // to make its inner unique.
+    FillTableFromRuleList(aSheet->GetCssRulesInternal(
+        /* aRequireUniqueInner = */ !mStyleSet->IsForXBL()));
   }
 }
 
 } // namespace mozilla
--- a/layout/inspector/inDOMUtils.cpp
+++ b/layout/inspector/inDOMUtils.cpp
@@ -279,17 +279,18 @@ inDOMUtils::GetCSSStyleRules(nsIDOMEleme
       ServoStyleRuleMap* map = styleSet->StyleRuleMap();
       map->EnsureTable();
       maps.AppendElement(map);
     }
 
     // Collect style rule maps for bindings.
     for (nsIContent* bindingContent = element; bindingContent;
          bindingContent = bindingContent->GetBindingParent()) {
-      if (nsXBLBinding* binding = bindingContent->GetXBLBinding()) {
+      for (nsXBLBinding* binding = bindingContent->GetXBLBinding();
+           binding; binding = binding->GetBaseBinding()) {
         if (ServoStyleSet* styleSet = binding->GetServoStyleSet()) {
           ServoStyleRuleMap* map = styleSet->StyleRuleMap();
           map->EnsureTable();
           maps.AppendElement(map);
         }
       }
       // Note that we intentionally don't cut off here, unlike when we
       // do styling, because even if style rules from parent binding
--- a/layout/style/ServoBindingList.h
+++ b/layout/style/ServoBindingList.h
@@ -131,16 +131,18 @@ SERVO_BINDING_FUNC(Servo_StyleSet_Resolv
                    RawServoDeclarationBlockBorrowed declarations)
 SERVO_BINDING_FUNC(Servo_SelectorList_Drop, void,
                    RawServoSelectorListOwned selector_list)
 SERVO_BINDING_FUNC(Servo_SelectorList_Parse,
                    RawServoSelectorList*,
                    const nsACString* selector_list)
 SERVO_BINDING_FUNC(Servo_SelectorList_Matches, bool,
                    RawGeckoElementBorrowed, RawServoSelectorListBorrowed)
+SERVO_BINDING_FUNC(Servo_SelectorList_Closest, RawGeckoElementBorrowedOrNull,
+                   RawGeckoElementBorrowed, RawServoSelectorListBorrowed)
 SERVO_BINDING_FUNC(Servo_StyleSet_AddSizeOfExcludingThis, void,
                    mozilla::MallocSizeOf malloc_size_of,
                    mozilla::MallocSizeOf malloc_enclosing_size_of,
                    mozilla::ServoStyleSetSizes* sizes,
                    RawServoStyleSetBorrowed set)
 SERVO_BINDING_FUNC(Servo_UACache_AddSizeOf, void,
                    mozilla::MallocSizeOf malloc_size_of,
                    mozilla::MallocSizeOf malloc_enclosing_size_of,
--- a/layout/style/ServoStyleSet.h
+++ b/layout/style/ServoStyleSet.h
@@ -473,30 +473,30 @@ public:
    */
   already_AddRefed<ServoStyleContext>
   ReparentStyleContext(ServoStyleContext* aStyleContext,
                        ServoStyleContext* aNewParent,
                        ServoStyleContext* aNewParentIgnoringFirstLine,
                        ServoStyleContext* aNewLayoutParent,
                        Element* aElement);
 
+  bool IsMaster() const { return mKind == Kind::Master; }
+  bool IsForXBL() const { return mKind == Kind::ForXBL; }
+
 private:
   friend class AutoSetInServoTraversal;
   friend class AutoPrepareTraversal;
 
   bool ShouldTraverseInParallel() const;
 
   /**
    * Gets the pending snapshots to handle from the restyle manager.
    */
   const SnapshotTable& Snapshots();
 
-  bool IsMaster() const { return mKind == Kind::Master; }
-  bool IsForXBL() const { return mKind == Kind::ForXBL; }
-
   /**
    * Resolve all ServoDeclarationBlocks attached to mapped
    * presentation attributes cached on the document.
    *
    * Call this before jumping into Servo's style system.
    */
   void ResolveMappedAttrDeclarationBlocks();
 
--- a/layout/style/ServoStyleSheet.cpp
+++ b/layout/style/ServoStyleSheet.cpp
@@ -388,20 +388,25 @@ ServoStyleSheet::Clone(StyleSheet* aClon
     static_cast<ServoStyleSheet*>(aCloneParent),
     aCloneOwnerRule,
     aCloneDocument,
     aCloneOwningNode);
   return clone.forget();
 }
 
 ServoCSSRuleList*
-ServoStyleSheet::GetCssRulesInternal()
+ServoStyleSheet::GetCssRulesInternal(bool aRequireUniqueInner)
 {
   if (!mRuleList) {
-    EnsureUniqueInner();
+    MOZ_ASSERT(aRequireUniqueInner || !mDocument,
+               "Not requiring unique inner for stylesheet associated "
+               "with document may have undesired behavior");
+    if (aRequireUniqueInner) {
+      EnsureUniqueInner();
+    }
 
     RefPtr<ServoCssRules> rawRules =
       Servo_StyleSheet_GetRules(Inner()->mContents).Consume();
     MOZ_ASSERT(rawRules);
     mRuleList = new ServoCSSRuleList(rawRules.forget(), this);
   }
   return mRuleList;
 }
--- a/layout/style/ServoStyleSheet.h
+++ b/layout/style/ServoStyleSheet.h
@@ -115,17 +115,17 @@ public:
     nsINode* aCloneOwningNode) const final;
 
   // nsICSSLoaderObserver interface
   NS_IMETHOD StyleSheetLoaded(StyleSheet* aSheet, bool aWasAlternate,
                               nsresult aStatus) final;
 
   // Internal GetCssRules method which do not have security check and
   // completelness check.
-  ServoCSSRuleList* GetCssRulesInternal();
+  ServoCSSRuleList* GetCssRulesInternal(bool aRequireUniqueInner = true);
 
   // Returns the stylesheet's Servo origin as an OriginFlags value.
   OriginFlags GetOrigin();
 
 protected:
   virtual ~ServoStyleSheet();
 
   void LastRelease();
--- a/layout/style/StyleSheet.cpp
+++ b/layout/style/StyleSheet.cpp
@@ -445,17 +445,17 @@ StyleSheet::DropStyleSet(const StyleSetH
 
 void
 StyleSheet::EnsureUniqueInner()
 {
   MOZ_ASSERT(mInner->mSheets.Length() != 0,
              "unexpected number of outers");
   mDirty = true;
 
-  if (mInner->mSheets.Length() == 1) {
+  if (HasUniqueInner()) {
     // already unique
     return;
   }
 
   StyleSheetInfo* clone = mInner->CloneFor(this);
   MOZ_ASSERT(clone);
   mInner->RemoveSheet(this);
   mInner = clone;
--- a/layout/style/StyleSheet.h
+++ b/layout/style/StyleSheet.h
@@ -133,16 +133,17 @@ public:
 
   virtual already_AddRefed<StyleSheet> Clone(StyleSheet* aCloneParent,
                                              dom::CSSImportRule* aCloneOwnerRule,
                                              nsIDocument* aCloneDocument,
                                              nsINode* aCloneOwningNode) const = 0;
 
   bool IsModified() const { return mDirty; }
 
+  inline bool HasUniqueInner() const;
   void EnsureUniqueInner();
 
   // Append all of this sheet's child sheets to aArray.
   void AppendAllChildSheets(nsTArray<StyleSheet*>& aArray);
 
   // style sheet owner info
   enum DocumentAssociationMode : uint8_t {
     // OwnedByDocument means mDocument owns us (possibly via a chain of other
--- a/layout/style/StyleSheetInlines.h
+++ b/layout/style/StyleSheetInlines.h
@@ -124,11 +124,17 @@ StyleSheet::GetReferrerPolicy() const
 }
 
 void
 StyleSheet::GetIntegrity(dom::SRIMetadata& aResult) const
 {
   aResult = SheetInfo().mIntegrity;
 }
 
+bool
+StyleSheet::HasUniqueInner() const
+{
+  return mInner->mSheets.Length() == 1;
+}
+
 }
 
 #endif // mozilla_StyleSheetInlines_h
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -336,16 +336,17 @@ skip-if = toolkit == 'android' # bug 775
 [test_value_computation.html]
 skip-if = toolkit == 'android'
 [test_value_storage.html]
 skip-if = toolkit == 'android' && debug # bug 1397615
 [test_variable_serialization_computed.html]
 [test_variable_serialization_specified.html]
 [test_variables.html]
 support-files = support/external-variable-url.css
+[test_variables_loop.html]
 [test_variables_order.html]
 support-files = support/external-variable-url.css
 [test_video_object_fit.html]
 [test_viewport_scrollbar_causing_reflow.html]
 [test_viewport_units.html]
 [test_visited_image_loading.html]
 skip-if = toolkit == 'android' # TIMED_OUT for android
 [test_visited_image_loading_empty.html]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_variables_loop.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>CSS variables loop resolving</title>
+<script src="/MochiKit/MochiKit.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" type="text/css">
+<style id="test">
+  #outer {
+    --a: a;
+    --b: b;
+    --c: c;
+    --d: d;
+    --e: e;
+  }
+  #inner {
+    --a: var(--d, ad);
+    --b: var(--d, ad);
+    --c: var(--d, ad);
+    --d: var(--e, de);
+    --e: var(--a, ea) var(--b, eb) var(--c, ec);
+  }
+</style>
+<div id="outer">
+  <div id="inner"></div>
+</div>
+<script>
+let inner_cs = getComputedStyle(document.getElementById("inner"));
+for (let v of ['a', 'b', 'c', 'd', 'e']) {
+  is(inner_cs.getPropertyValue(`--${v}`), "",
+     `Variable --${v} should be eliminated`);
+}
+</script>
--- a/netwerk/streamconv/converters/ParseFTPList.cpp
+++ b/netwerk/streamconv/converters/ParseFTPList.cpp
@@ -1,14 +1,15 @@
 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ParseFTPList.h"
+#include <algorithm>
 #include <stdlib.h>
 #include <string.h>
 #include <ctype.h>
 #include "plstr.h"
 #include "nsDebug.h"
 #include "prprf.h"
 #include "mozilla/CheckedInt.h"
 #include "mozilla/IntegerPrintfMacros.h"
@@ -20,18 +21,32 @@ using mozilla::CheckedInt;
 
 static inline int ParsingFailed(struct list_state *state)
 {
   if (state->parsed_one || state->lstyle) /* junk if we fail to parse */
     return '?';      /* this time but had previously parsed successfully */
   return '"';        /* its part of a comment or error message */
 }
 
+void
+FixupYear(PRExplodedTime* aTime)
+{
+  /* if year has only two digits then assume that
+     00-79 is 2000-2079
+     80-99 is 1980-1999 */
+  if (aTime->tm_year < 80) {
+    aTime->tm_year += 2000;
+  } else if (aTime->tm_year < 100) {
+    aTime->tm_year += 1900;
+  }
+}
+
 int ParseFTPList(const char *line, struct list_state *state,
-                 struct list_result *result )
+                 struct list_result *result, PRTimeParamFn timeParam,
+                 NowTimeFn nowTimeFn)
 {
   unsigned int carry_buf_len; /* copy of state->carry_buf_len */
   unsigned int pos;
   const char *p;
 
   if (!line || !state || !result)
     return 0;
 
@@ -131,17 +146,17 @@ int ParseFTPList(const char *line, struc
               while (pos < linelen && isdigit(line[pos]))
                 pos++;
               if (pos < linelen && line[pos] == ',')
               {
                 PRTime t;
                 PRTime seconds;
                 PR_sscanf(p+1, "%llu", &seconds);
                 t = seconds * PR_USEC_PER_SEC;
-                PR_ExplodeTime(t, PR_LocalTimeParameters, &(result->fe_time) );
+                PR_ExplodeTime(t, timeParam, &(result->fe_time) );
               }
             }
           }
           else if (*p == 's')
           {
             if (isdigit(line[pos]))
             {
               while (pos < linelen && isdigit(line[pos]))
@@ -448,56 +463,27 @@ int ParseFTPList(const char *line, struc
              * 'used' is the size in bytes if and only if 'used'<=allocated.
              * If 'used' is size in bytes then it can be > 2^32
              * If 'used' is not size in bytes then it is size in blocks.
             */
             pos = 0;
             while (pos < toklen[1] && (tokens[1][pos]) != '/')
               pos++;
 
-/*
- * I've never seen size come back in bytes, its always in blocks, and
- * the following test fails. So, always perform the "size in blocks".
- * I'm leaving the "size in bytes" code if'd out in case we ever need
- * to re-instate it.
-*/
-#if 0
-            if (pos < toklen[1] && ( (pos<<1) > (toklen[1]-1) ||
-                 (strtoul(tokens[1], (char **)0, 10) >
-                  strtoul(tokens[1]+pos+1, (char **)0, 10))        ))
-            {                                   /* size is in bytes */
-              if (pos > (sizeof(result->fe_size)-1))
-                pos = sizeof(result->fe_size)-1;
-              memcpy( result->fe_size, tokens[1], pos );
-              result->fe_size[pos] = '\0';
-            }
-            else /* size is in blocks */
-#endif
-            {
-              /* size requires multiplication by blocksize.
-               *
-               * We could assume blocksize is 512 (like Lynx does) and
-               * shift by 9, but that might not be right. Even if it
-               * were, doing that wouldn't reflect what the file's
-               * real size was. The sanest thing to do is not use the
-               * LISTing's filesize, so we won't (like ftpmirror).
-               *
-               * ulltoa(((unsigned long long)fsz)<<9, result->fe_size, 10);
-               *
-               * A block is always 512 bytes on OpenVMS, compute size.
-               * So its rounded up to the next block, so what, its better
-               * than not showing the size at all.
-               * A block is always 512 bytes on OpenVMS, compute size.
-               * So its rounded up to the next block, so what, its better
-               * than not showing the size at all.
-              */
-              uint64_t fsz = uint64_t(strtoul(tokens[1], (char **)0, 10) * 512);
-              SprintfLiteral(result->fe_size, "%" PRId64, fsz);
-            }
-
+            /*
+             * On OpenVMS, the size is given in blocks. A block is 512
+             * bytes. This can only approximate the size of the file,
+             * but that's better than not showing a size at all.
+             * numBlocks is clamped to UINT32_MAX to make 32-bit and
+             * 64-bit builds return consistent results.
+             */
+            uint64_t numBlocks = strtoul(tokens[1], nullptr, 10);
+            numBlocks = std::min(numBlocks, (uint64_t)UINT32_MAX);
+            uint64_t fileSize = numBlocks * 512;
+            SprintfLiteral(result->fe_size, "%" PRIu64, fileSize);
           } /* if (result->fe_type != 'd') */
 
           p = tokens[2] + 2;
           if (*p == '-')
             p++;
           tbuf[0] = p[0];
           tbuf[1] = tolower(p[1]);
           tbuf[2] = tolower(p[2]);
@@ -650,28 +636,27 @@ int ParseFTPList(const char *line, struc
       if (lstyle == 'C')
       {
         state->parsed_one = 1;
         state->lstyle = lstyle;
 
         p = tokens[tokmarker+4];
         if (toklen[tokmarker+4] == 10) /* newstyle: YYYY-MM-DD format */
         {
-          result->fe_time.tm_year = atoi(p+0) - 1900;
+          result->fe_time.tm_year = atoi(p+0);
           result->fe_time.tm_month  = atoi(p+5) - 1;
           result->fe_time.tm_mday = atoi(p+8);
         }
         else /* oldstyle: [M]M/DD/YY format */
         {
           pos = toklen[tokmarker+4];
           result->fe_time.tm_month  = atoi(p) - 1;
           result->fe_time.tm_mday = atoi((p+pos)-5);
           result->fe_time.tm_year = atoi((p+pos)-2);
-          if (result->fe_time.tm_year < 70)
-            result->fe_time.tm_year += 100;
+          FixupYear(&result->fe_time);
         }
 
         p = tokens[tokmarker+5];
         pos = toklen[tokmarker+5];
         result->fe_time.tm_hour  = atoi(p);
         result->fe_time.tm_min = atoi((p+pos)-5);
         result->fe_time.tm_sec = atoi((p+pos)-2);
 
@@ -824,23 +809,17 @@ int ParseFTPList(const char *line, struc
         }
 
         result->fe_time.tm_month = atoi(tokens[0]+0);
         if (result->fe_time.tm_month != 0)
         {
           result->fe_time.tm_month--;
           result->fe_time.tm_mday = atoi(tokens[0]+3);
           result->fe_time.tm_year = atoi(tokens[0]+6);
-          /* if year has only two digits then assume that
-               00-79 is 2000-2079
-               80-99 is 1980-1999 */
-          if (result->fe_time.tm_year < 80)
-            result->fe_time.tm_year += 2000;
-          else if (result->fe_time.tm_year < 100)
-            result->fe_time.tm_year += 1900;
+          FixupYear(&result->fe_time);
         }
 
         result->fe_time.tm_hour = atoi(tokens[1]+0);
         result->fe_time.tm_min = atoi(tokens[1]+3);
         if (toklen[1] == 7)
         {
           if ((tokens[1][5]) == 'P' && result->fe_time.tm_hour < 12)
             result->fe_time.tm_hour += 12;
@@ -942,18 +921,17 @@ int ParseFTPList(const char *line, struc
             pos = (sizeof(result->fe_size)-1);
           memcpy( result->fe_size, tokens[0], pos );
           result->fe_size[pos] = '\0';
         }
 
         result->fe_time.tm_month = atoi(&p[35-18]) - 1;
         result->fe_time.tm_mday = atoi(&p[38-18]);
         result->fe_time.tm_year = atoi(&p[41-18]);
-        if (result->fe_time.tm_year < 80)
-          result->fe_time.tm_year += 100;
+        FixupYear(&result->fe_time);
         result->fe_time.tm_hour = atoi(&p[46-18]);
         result->fe_time.tm_min = atoi(&p[49-18]);
 
         /* the caller should do this (if dropping "." and ".." is desired)
         if (result->fe_type == 'd' && result->fe_fname[0] == '.' &&
             (result->fe_fnlen == 1 || (result->fe_fnlen == 2 &&
                                       result->fe_fname[1] == '.')))
           return '?';
@@ -1156,18 +1134,18 @@ int ParseFTPList(const char *line, struc
         {
           result->fe_time.tm_hour = pos;
           result->fe_time.tm_min  = atoi(p+3);
           if (p[5] == ':')
             result->fe_time.tm_sec = atoi(p+6);
 
           if (!state->now_time)
           {
-            state->now_time = PR_Now();
-            PR_ExplodeTime((state->now_time), PR_LocalTimeParameters, &(state->now_tm) );
+            state->now_time = nowTimeFn();
+            PR_ExplodeTime((state->now_time), timeParam, &(state->now_tm) );
           }
 
           result->fe_time.tm_year = state->now_tm.tm_year;
           if ( (( state->now_tm.tm_month << 5) + state->now_tm.tm_mday) <
                ((result->fe_time.tm_month << 5) + result->fe_time.tm_mday) )
             result->fe_time.tm_year--;
 
         } /* time/year */
@@ -1369,29 +1347,28 @@ int ParseFTPList(const char *line, struc
           for (pos = 0; pos < (12*3); pos+=3)
           {
             if (tbuf[0] == month_names[pos+0] &&
                 tbuf[1] == month_names[pos+1] &&
                 tbuf[2] == month_names[pos+2])
             {
               result->fe_time.tm_month = pos/3;
               result->fe_time.tm_mday = atoi(tokens[3]);
-              result->fe_time.tm_year = atoi(tokens[4]) - 1900;
+              result->fe_time.tm_year = atoi(tokens[4]);
               break;
             }
           }
           pos = 5; /* Chameleon toknum of date field */
         }
         else
         {
           result->fe_time.tm_month = atoi(p+0)-1;
           result->fe_time.tm_mday = atoi(p+3);
           result->fe_time.tm_year = atoi(p+6);
-          if (result->fe_time.tm_year < 80) /* SuperTCP */
-            result->fe_time.tm_year += 100;
+          FixupYear(&result->fe_time); /* SuperTCP */
 
           pos = 3; /* SuperTCP toknum of date field */
         }
 
         result->fe_time.tm_hour = atoi(tokens[pos]);
         result->fe_time.tm_min = atoi(&(tokens[pos][toklen[pos]-2]));
 
         /* the caller should do this (if dropping "." and ".." is desired)
@@ -1626,27 +1603,27 @@ int ParseFTPList(const char *line, struc
             }
             if (result->fe_time.tm_mday)
             {
               tokmarker += 3; /* skip mday/mon/yrtime (to find " -> ") */
               p = tokens[tokmarker];
 
               pos = atoi(p);
               if (pos > 24)
-                result->fe_time.tm_year = pos-1900;
+                result->fe_time.tm_year = pos;
               else
               {
                 if (p[1] == ':')
                   p--;
                 result->fe_time.tm_hour = pos;
                 result->fe_time.tm_min = atoi(p+3);
                 if (!state->now_time)
                 {
-                  state->now_time = PR_Now();
-                  PR_ExplodeTime((state->now_time), PR_LocalTimeParameters, &(state->now_tm) );
+                  state->now_time = nowTimeFn();
+                  PR_ExplodeTime((state->now_time), timeParam, &(state->now_tm) );
                 }
                 result->fe_time.tm_year = state->now_tm.tm_year;
                 if ( (( state->now_tm.tm_month  << 4) + state->now_tm.tm_mday) <
                      ((result->fe_time.tm_month << 4) + result->fe_time.tm_mday) )
                   result->fe_time.tm_year--;
               } /* got year or time */
             } /* got month/mday */
           } /* may have year or time */
@@ -1683,212 +1660,8 @@ int ParseFTPList(const char *line, struc
 
     /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
 
   } /* if (linelen > 0) */
 
   return ParsingFailed(state);
 }
 
-/* ==================================================================== */
-/* standalone testing                                                   */
-/* ==================================================================== */
-#if 0
-
-#include <stdio.h>
-
-static int do_it(FILE *outfile,
-                 char *line, size_t linelen, struct list_state *state,
-                 char **cmnt_buf, unsigned int *cmnt_buf_sz,
-                 char **list_buf, unsigned int *list_buf_sz )
-{
-  struct list_result result;
-  char *p;
-  int rc;
-
-  rc = ParseFTPList( line, state, &result );
-
-  if (!outfile)
-  {
-    outfile = stdout;
-    if (rc == '?')
-      fprintf(outfile, "junk: %.*s\n", (int)linelen, line );
-    else if (rc == '"')
-      fprintf(outfile, "cmnt: %.*s\n", (int)linelen, line );
-    else
-      fprintf(outfile,
-              "list: %02u-%02u-%02u  %02u:%02u%cM %20s %.*s%s%.*s\n",
-              (result.fe_time.tm_mday ? (result.fe_time.tm_month + 1) : 0),
-              result.fe_time.tm_mday,
-              (result.fe_time.tm_mday ? (result.fe_time.tm_year % 100) : 0),
-              result.fe_time.tm_hour -
-                ((result.fe_time.tm_hour > 12)?(12):(0)),
-              result.fe_time.tm_min,
-              ((result.fe_time.tm_hour >= 12) ? 'P' : 'A'),
-              (rc == 'd' ? "<DIR>         " :
-              (rc == 'l' ? "<JUNCTION>    " : result.fe_size)),
-              (int)result.fe_fnlen, result.fe_fname,
-              ((rc == 'l' && result.fe_lnlen) ? " -> " : ""),
-              (int)((rc == 'l' && result.fe_lnlen) ? result.fe_lnlen : 0),
-              ((rc == 'l' && result.fe_lnlen) ? result.fe_lname : "") );
-  }
-  else if (rc != '?') /* NOT junk */
-  {
-    char **bufp = list_buf;
-    unsigned int *bufz = list_buf_sz;
-
-    if (rc == '"') /* comment - make it a 'result' */
-    {
-      memset( &result, 0, sizeof(result));
-      result.fe_fname = line;
-      result.fe_fnlen = linelen;
-      result.fe_type = 'f';
-      if (line[linelen-1] == '/')
-      {
-        result.fe_type = 'd';
-        result.fe_fnlen--;
-      }
-      bufp = cmnt_buf;
-      bufz = cmnt_buf_sz;
-      rc = result.fe_type;
-    }
-
-    linelen = 80 + result.fe_fnlen + result.fe_lnlen;
-    p = (char *)realloc( *bufp, *bufz + linelen );
-    if (!p)
-      return -1;
-    sprintf( &p[*bufz],
-             "%02u-%02u-%04u  %02u:%02u:%02u %20s %.*s%s%.*s\n",
-              (result.fe_time.tm_mday ? (result.fe_time.tm_month + 1) : 0),
-              result.fe_time.tm_mday,
-              (result.fe_time.tm_mday ? (result.fe_time.tm_year + 1900) : 0),
-              result.fe_time.tm_hour,
-              result.fe_time.tm_min,
-              result.fe_time.tm_sec,
-              (rc == 'd' ? "<DIR>         " :
-              (rc == 'l' ? "<JUNCTION>    " : result.fe_size)),
-              (int)result.fe_fnlen, result.fe_fname,
-              ((rc == 'l' && result.fe_lnlen) ? " -> " : ""),
-              (int)((rc == 'l' && result.fe_lnlen) ? result.fe_lnlen : 0),
-              ((rc == 'l' && result.fe_lnlen) ? result.fe_lname : "") );
-    linelen = strlen(&p[*bufz]);
-    *bufp = p;
-    *bufz = *bufz + linelen;
-  }
-  return 0;
-}
-
-int main(int argc, char *argv[])
-{
-  FILE *infile = (FILE *)0;
-  FILE *outfile = (FILE *)0;
-  int need_close_in = 0;
-  int need_close_out = 0;
-
-  if (argc > 1)
-  {
-    infile = stdin;
-    if (strcmp(argv[1], "-") == 0)
-      need_close_in = 0;
-    else if ((infile = fopen(argv[1], "r")) != ((FILE *)0))
-      need_close_in = 1;
-    else
-      fprintf(stderr, "Unable to open input file '%s'\n", argv[1]);
-  }
-  if (infile && argc > 2)
-  {
-    outfile = stdout;
-    if (strcmp(argv[2], "-") == 0)
-      need_close_out = 0;
-    else if ((outfile = fopen(argv[2], "w")) != ((FILE *)0))
-      need_close_out = 1;
-    else
-    {
-      fprintf(stderr, "Unable to open output file '%s'\n", argv[2]);
-      fclose(infile);
-      infile = (FILE *)0;
-    }
-  }
-
-  if (!infile)
-  {
-    char *appname = &(argv[0][strlen(argv[0])]);
-    while (appname > argv[0])
-    {
-      appname--;
-      if (*appname == '/' || *appname == '\\' || *appname == ':')
-      {
-        appname++;
-        break;
-      }
-    }
-    fprintf(stderr,
-        "Usage: %s <inputfilename> [<outputfilename>]\n"
-        "\nIf an outout file is specified the results will be"
-        "\nbe post-processed, and only the file entries will appear"
-        "\n(or all comments if there are no file entries)."
-        "\nNot specifying an output file causes %s to run in \"debug\""
-        "\nmode, ie results are printed as lines are parsed."
-        "\nIf a filename is a single dash ('-'), stdin/stdout is used."
-        "\n", appname, appname );
-  }
-  else
-  {
-    char *cmnt_buf = (char *)0;
-    unsigned int cmnt_buf_sz = 0;
-    char *list_buf = (char *)0;
-    unsigned int list_buf_sz = 0;
-
-    struct list_state state;
-    char line[512];
-
-    memset( &state, 0, sizeof(state) );
-    while (fgets(line, sizeof(line), infile))
-    {
-      size_t linelen = strlen(line);
-      if (linelen < (sizeof(line)-1))
-      {
-        if (linelen > 0 && line[linelen-1] == '\n')
-          linelen--;
-        if (do_it( outfile, line, linelen, &state,
-                   &cmnt_buf, &cmnt_buf_sz, &list_buf, &list_buf_sz) != 0)
-        {
-          fprintf(stderr, "Insufficient memory. Listing may be incomplete.\n");
-          break;
-        }
-      }
-      else
-      {
-        /* no '\n' found. drop this and everything up to the next '\n' */
-        fprintf(stderr, "drop: %.*s", (int)linelen, line );
-        while (linelen == sizeof(line))
-        {
-          if (!fgets(line, sizeof(line), infile))
-            break;
-          linelen = 0;
-          while (linelen < sizeof(line) && line[linelen] != '\n')
-            linelen++;
-          fprintf(stderr, "%.*s", (int)linelen, line );
-        }
-        fprintf(stderr, "\n");
-      }
-    }
-    if (outfile)
-    {
-      if (list_buf)
-        fwrite( list_buf, 1, list_buf_sz, outfile );
-      else if (cmnt_buf)
-        fwrite( cmnt_buf, 1, cmnt_buf_sz, outfile );
-    }
-    if (list_buf)
-      free(list_buf);
-    if (cmnt_buf)
-      free(cmnt_buf);
-
-    if (need_close_in)
-      fclose(infile);
-    if (outfile && need_close_out)
-      fclose(outfile);
-  }
-
-  return 0;
-}
-#endif
--- a/netwerk/streamconv/converters/ParseFTPList.h
+++ b/netwerk/streamconv/converters/ParseFTPList.h
@@ -92,13 +92,17 @@ struct list_result
   const char *      fe_lname;     /* pointer to symlink name */
   uint32_t          fe_lnlen;     /* length of symlink name */
   char              fe_size[40];  /* size of file in bytes (<= (2^128 - 1)) */
   PRExplodedTime    fe_time;      /* last-modified time */
   int32_t           fe_cinfs;     /* file system is definitely case insensitive */
                                   /* (converting all-upcase names may be desirable) */
 };
 
+typedef PRTime (*NowTimeFn)();
+
 int ParseFTPList(const char *line,
                  struct list_state *state,
-                 struct list_result *result );
+                 struct list_result *result,
+                 PRTimeParamFn timeParam = PR_LocalTimeParameters,
+                 NowTimeFn nowTimeFn = PR_Now);
 
 #endif /* !ParseRTPList_h___ */
--- a/netwerk/test/gtest/moz.build
+++ b/netwerk/test/gtest/moz.build
@@ -8,11 +8,15 @@ UNIFIED_SOURCES += [
     'TestHeaders.cpp',
     'TestHttpAuthUtils.cpp',
     'TestMozURL.cpp',
     'TestPartiallySeekableInputStream.cpp',
     'TestProtocolProxyService.cpp',
     'TestStandardURL.cpp',
 ]
 
+TEST_DIRS += [
+    'parse-ftp',
+]
+
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul-gtest'
rename from netwerk/streamconv/converters/parse-ftp/3-guess.in
rename to netwerk/test/gtest/parse-ftp/3-guess.in
rename from netwerk/streamconv/converters/parse-ftp/3-guess.out
rename to netwerk/test/gtest/parse-ftp/3-guess.out
rename from netwerk/streamconv/converters/parse-ftp/C-VMold.in
rename to netwerk/test/gtest/parse-ftp/C-VMold.in
rename from netwerk/streamconv/converters/parse-ftp/C-VMold.out
rename to netwerk/test/gtest/parse-ftp/C-VMold.out
rename from netwerk/streamconv/converters/parse-ftp/C-zVM.in
rename to netwerk/test/gtest/parse-ftp/C-zVM.in
rename from netwerk/streamconv/converters/parse-ftp/C-zVM.out
rename to netwerk/test/gtest/parse-ftp/C-zVM.out
rename from netwerk/streamconv/converters/parse-ftp/D-WinNT.in
rename to netwerk/test/gtest/parse-ftp/D-WinNT.in
--- a/netwerk/streamconv/converters/parse-ftp/D-WinNT.in
+++ b/netwerk/test/gtest/parse-ftp/D-WinNT.in
@@ -41,20 +41,22 @@ 09-05-00  10:35AM       <DIR>          N
 08-02-00  06:25PM       <DIR>          NLM
 09-06-00  03:04PM       <DIR>          PageOne
 06-27-00  06:00PM       <DIR>          pccomputing
 05-09-01  01:27PM       <DIR>          pictures
 07-21-00  04:23PM       <DIR>          pranks
 08-22-00  10:36AM       <DIR>          Sean
 08-10-00  01:54PM       <DIR>          SLeong
 09-07-00  05:46PM       <DIR>          svr
-07-21-00  07:22AM       <JUNCTION>     a_junction_sample
 10-23-00  01:27PM       <JUNCTION>     b_junction_sample => foo
 06-15-00  07:37AM       <JUNCTION>     c_junction_sample -> bar too
 07-14-00  01:35PM              2094926 canprankdesk.tif
 07-21-00  01:19PM                95077 Jon Kauffman Enjoys the Good Life.jpg
 07-21-00  01:19PM                52275 Name Plate.jpg
 07-14-00  01:38PM              2250540 Valentineoffprank-HiRes.jpg
+01-11-14  12:25AM             18864566 Bug961346
+10-10-2014  10:10AM       <DIR>        Bug1061898
+01-18-12  19:00             30724571 Bug1125816
 226 Transfer complete.
 ftp> close
 221  Thank-You For Using Microsoft Products!
 ftp> 
 
rename from netwerk/streamconv/converters/parse-ftp/D-WinNT.out
rename to netwerk/test/gtest/parse-ftp/D-WinNT.out
--- a/netwerk/streamconv/converters/parse-ftp/D-WinNT.out
+++ b/netwerk/test/gtest/parse-ftp/D-WinNT.out
@@ -33,15 +33,17 @@ 09-05-2000  10:35:00       <DIR>        
 08-02-2000  18:25:00       <DIR>          NLM
 09-06-2000  15:04:00       <DIR>          PageOne
 06-27-2000  18:00:00       <DIR>          pccomputing
 05-09-2001  13:27:00       <DIR>          pictures
 07-21-2000  16:23:00       <DIR>          pranks
 08-22-2000  10:36:00       <DIR>          Sean
 08-10-2000  13:54:00       <DIR>          SLeong
 09-07-2000  17:46:00       <DIR>          svr
-07-21-2000  07:22:00       <JUNCTION>     a_junction_sample
 10-23-2000  13:27:00       <JUNCTION>     b_junction_sample -> foo
 06-15-2000  07:37:00       <JUNCTION>     c_junction_sample -> bar too
 07-14-2000  13:35:00              2094926 canprankdesk.tif
 07-21-2000  13:19:00                95077 Jon Kauffman Enjoys the Good Life.jpg
 07-21-2000  13:19:00                52275 Name Plate.jpg
 07-14-2000  13:38:00              2250540 Valentineoffprank-HiRes.jpg
+01-11-2014  00:25:00             18864566 Bug961346
+10-10-2014  10:10:00       <DIR>          Bug1061898
+01-18-2012  19:00:00             30724571 Bug1125816
rename from netwerk/streamconv/converters/parse-ftp/E-EPLF.in
rename to netwerk/test/gtest/parse-ftp/E-EPLF.in
rename from netwerk/streamconv/converters/parse-ftp/E-EPLF.out
rename to netwerk/test/gtest/parse-ftp/E-EPLF.out
--- a/netwerk/streamconv/converters/parse-ftp/E-EPLF.out
+++ b/netwerk/test/gtest/parse-ftp/E-EPLF.out
@@ -1,174 +1,178 @@
-01-11-2001  10:38:25       <DIR>          tmp
-02-29-2000  03:01:45       <DIR>          1998-100
-12-24-2000  21:44:06                 1172 mirrors.html
-02-29-2000  03:01:47       <DIR>          1999-275
-05-08-2000  07:25:16                  933 1999-275.html
-05-08-2000  07:27:06                  322 1998-401.html
-02-29-2000  03:01:47       <DIR>          1998-401
-02-29-2000  03:01:45       <DIR>          1997-275
-05-08-2000  07:28:25                  277 1998-515.html
-05-08-2000  07:36:43                  325 2000-436.html
-12-23-2000  07:47:11       <DIR>          checkpwd
-07-04-2002  19:47:46       <DIR>          bib
-02-29-2000  03:01:47       <DIR>          1999-541
-04-28-2001  05:02:35       <DIR>          2001-260
-04-17-2002  20:27:55       <DIR>          2002-501
-06-01-2001  22:56:17       <DIR>          1995-514
-02-29-2000  03:01:45       <DIR>          1999-180
-06-01-2001  04:44:23                  475 2000-515.html
-02-29-2000  03:01:47       <DIR>          1998-515
-02-29-2000  03:01:47       <DIR>          2000-515
-05-12-2000  09:46:13       <DIR>          zpfft
-10-27-2001  15:00:14       <DIR>          psibound
-02-29-2000  03:01:51                  398 anonftpd.html
-02-26-2002  03:34:22       <DIR>          nistp224
-08-10-2001  09:27:20       <DIR>          cdb
-05-01-2002  01:58:27       <DIR>          talks
-12-23-2000  22:34:35       <DIR>          ftpparse
-01-11-2000  06:50:10       <DIR>          clockspeed
-02-29-2000  03:01:52                 1350 clockspeed.html
-02-06-2002  21:52:34       <DIR>          slashdoc
-06-07-2002  18:00:40       <DIR>          export
-07-17-2001  03:48:36       <DIR>          daemontools
-07-12-2001  23:17:08                 2187 daemontools.html
-08-30-2001  22:51:16                  175 data.html
-12-28-2001  02:32:37                 1901 crypto.html
-11-11-2001  00:45:23                  739 arith.html
-11-30-2001  04:48:09                 2733 cdb.html
-11-11-2001  00:51:02                  344 floatasm.html
-12-21-2000  22:25:56       <DIR>          djbfft
-02-29-2000  03:01:52                 1319 djbfft.html
-07-09-2000  19:16:47       <DIR>          dnscache
-07-04-2000  00:57:44                 4016 dnscache.html
-07-10-2001  05:16:22       <DIR>          docs
-02-29-2000  03:01:52                  293 docs.html
-02-29-2000  03:01:52                  952 dot-forward.html
-01-11-2000  06:50:11       <DIR>          etc-mta
-02-29-2000  03:01:52                 3174 etc-mta.html
-02-29-2000  03:01:52                 2990 ezmlm.html
-12-21-2000  22:25:56       <DIR>          ftp
-02-29-2000  03:01:49       <DIR>          im
-02-29-2000  03:01:52                 1823 fastforward.html
-02-29-2000  03:01:52                 1707 ftp.html
-02-12-2001  03:17:10       <DIR>          freebugtraq
-06-02-2002  03:39:25       <DIR>          hardware
-06-01-2001  04:57:35                  610 1995-514.html
-12-21-2000  22:25:57       <DIR>          hash127
-05-19-2000  04:29:02                 4835 hash127.html
-02-29-2000  03:01:52                 1515 im.html
-02-12-2002  21:40:06       <DIR>          immhf
-04-25-2001  23:48:49                 2902 immhf.html
-01-25-2001  06:58:36       <DIR>          lib
-06-06-2001  01:45:50       <DIR>          libtai
-07-10-2000  20:54:16                 2120 libtai.html
-11-27-2001  18:52:40                14460 qmail.html
-08-30-2001  22:51:23                 2153 mail.html
-02-29-2000  03:01:52                  108 maildir.html
-02-29-2000  03:01:50       <DIR>          maildisasters
-02-29-2000  03:01:52                  873 maildisasters.html
-02-29-2000  03:01:52                 1688 mess822.html
-02-25-2002  03:47:27       <DIR>          mirror
-12-26-2001  22:02:32                  581 ntheory.html
-06-03-2002  17:36:27       <DIR>          papers
-05-21-2002  18:50:15       <DIR>          postpropter
-01-11-2000  06:50:12       <DIR>          postings
-01-11-2000  06:50:12       <DIR>          primegen
-08-30-2001  22:51:33                  415 precompiled.html
-02-29-2000  03:01:52                 1121 primegen.html
-05-05-2002  03:37:55       <DIR>          proto
-02-29-2000  03:01:52                  452 proto.html
-07-10-2001  00:17:53       <DIR>          publicfile
-11-30-2001  04:59:17                 5651 publicfile.html
-08-30-2001  22:51:37                  409 qlist.html
-04-02-2002  06:25:45       <DIR>          qmail
-02-02-2002  06:52:28                 7549 lists.html
-02-29-2000  03:01:52                 1166 qmailanalog.html
-08-30-2001  22:51:39                 1434 qmsmac.html
-03-13-2000  06:38:06                  465 rblsmtpd.html
-02-29-2000  03:01:52                 1225 rights.html
-01-11-2000  06:50:13       <DIR>          sarcasm
-09-14-2001  04:34:14                 1058 unix.html
-02-29-2000  03:01:53                 1568 serialmail.html
-10-02-2000  21:13:44                  840 sigs.html
-05-21-2002  18:50:08       <DIR>          smtp
-04-26-2001  17:58:47                 1723 smtp.html
-12-21-2000  22:26:03       <DIR>          software
-02-29-2000  03:01:53                 3671 softwarelaw.html
-01-11-2000  06:50:13       <DIR>          sortedsums
-02-29-2000  03:01:53                 2735 sortedsums.html
-02-26-2002  03:06:38       <DIR>          speed
-07-04-2000  00:40:17                  572 surveydns.html
-10-04-2001  02:59:31       <DIR>          surveys
-10-04-2001  02:46:36                 3503 surveys.html
-11-10-2000  06:58:08       <DIR>          syncookies
-02-01-2002  03:03:37                 7918 syncookies.html
-08-30-2001  22:51:43                 1115 tcpcontrol.html
-03-18-2002  13:08:27                  589 tcpip.html
-02-29-2000  03:01:53                  906 thoughts.html
-07-30-2001  16:18:09       <DIR>          threecubes
-09-14-2001  03:25:58                  525 time.html
-05-22-2002  06:21:05       <DIR>          ucspi-tcp
-01-25-2002  00:41:31                 4086 ucspi-tcp.html
-08-30-2001  22:51:50                  732 web.html
-07-04-2002  19:47:48       <DIR>          www
-09-14-2001  03:26:08                 2717 y2k.html
-02-08-2001  10:28:09       <DIR>          dnsroot
-04-23-2001  08:01:37                 2745 2001-260.html
-02-29-2000  03:01:53                  291 zeroseek.html
-02-29-2000  03:01:53       <DIR>          zmodexp
-05-19-2000  04:29:07                 1513 zmodexp.html
-08-01-2000  03:08:54       <DIR>          zmult
-08-01-2000  03:24:53                  669 zmult.html
-12-23-2000  07:19:47                 1097 checkpwd.html
-12-31-2001  19:47:16                 2077 focus.html
-07-20-2000  01:11:31                 3842 im2000.html
-07-04-2002  19:47:43       <DIR>          slashcommand
-07-04-2002  19:47:44       <DIR>          slashpackage
-07-16-2001  00:24:39                  602 slashpackage.html
-05-03-2002  02:15:33                23874 talks.html
-05-08-2000  07:24:59                  566 1997-275.html
-05-08-2000  07:31:04                  313 1998-100.html
-02-29-2000  03:01:47       <DIR>          2000-436
-05-08-2000  07:37:26                  660 1997-494.html
-05-08-2000  07:40:41                  730 1999-541.html
-09-13-2000  09:08:58                 2367 psibound.html
-06-22-2000  04:09:00                 1866 smallfactors.html
-06-09-2000  03:35:19       <DIR>          smallfactors
-07-02-2000  00:55:19       <DIR>          conferences
-05-22-2002  07:52:29       <DIR>          djbdns
-07-17-2001  06:00:58                 2712 djbdns.html
-07-17-2000  03:15:39                  216 zpfft.html
-08-20-2000  20:56:37       <DIR>          fastnewton
-08-20-2000  20:57:31                  333 fastnewton.html
-09-01-2000  20:33:51       <DIR>          patents
-12-23-2000  22:40:17                 1748 ftpparse.html
-11-11-2001  00:57:50                 3261 slash.html
-01-17-2001  18:50:10                 2663 slashdoc.html
-03-18-2002  10:04:35                 2344 dnsroot.html
-12-26-2001  22:03:00                 3238 software.html
-09-14-2001  04:34:33                 3481 compatibility.html
-05-01-2002  05:30:34                 5525 distributors.html
-02-25-2001  04:49:03                  212 freebugtraq.html
-03-31-2002  04:59:50                  806 hardware.html
-03-31-2001  08:15:18                  121 securesoftware.html
-05-22-2002  04:39:56                10950 conferences.html
-11-30-2001  18:51:50       <DIR>          2001-275
-05-29-2001  02:15:07                  459 rwb100.html
-10-03-2001  07:40:42                  863 nistp224.html
-07-30-2001  16:15:46                  778 threecubes.html
-06-03-2002  17:32:16                27125 papers.html
-11-21-2001  03:51:09                  379 bib.html
-11-30-2001  18:41:33                 3791 2001-275.html
-10-02-2001  20:08:59                  169 dh224.html
-03-16-2002  07:05:36                 1126 courses.html
-09-03-2001  00:22:28                  355 slashcommand.html
-03-27-2002  22:20:22                 1199 patents.html
-11-08-2001  20:27:15                  640 mailcopyright.html
-06-02-2002  01:27:50                 1257 djb.html
-01-07-2002  07:12:34                  301 2002-501.html
-05-01-2002  04:41:45                 1655 export.html
-03-24-2002  11:40:59                 6446 index.html
-03-16-2002  07:09:08                  570 2005-590.html
-03-16-2002  07:05:49                  800 2004-494.html
-06-02-2002  01:28:59                 4459 positions.html
-06-02-2002  00:55:31                  466 contact.html
+<!-- 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/. -->
+
+01-11-2001  09:38:25       <DIR>          tmp
+02-29-2000  02:01:45       <DIR>          1998-100
+12-24-2000  20:44:06                 1172 mirrors.html
+02-29-2000  02:01:47       <DIR>          1999-275
+05-08-2000  05:25:16                  933 1999-275.html
+05-08-2000  05:27:06                  322 1998-401.html
+02-29-2000  02:01:47       <DIR>          1998-401
+02-29-2000  02:01:45       <DIR>          1997-275
+05-08-2000  05:28:25                  277 1998-515.html
+05-08-2000  05:36:43                  325 2000-436.html
+12-23-2000  06:47:11       <DIR>          checkpwd
+07-04-2002  17:47:46       <DIR>          bib
+02-29-2000  02:01:47       <DIR>          1999-541
+04-28-2001  03:02:35       <DIR>          2001-260
+04-17-2002  18:27:55       <DIR>          2002-501
+06-01-2001  20:56:17       <DIR>          1995-514
+02-29-2000  02:01:45       <DIR>          1999-180
+06-01-2001  02:44:23                  475 2000-515.html
+02-29-2000  02:01:47       <DIR>          1998-515
+02-29-2000  02:01:47       <DIR>          2000-515
+05-12-2000  07:46:13       <DIR>          zpfft
+10-27-2001  13:00:14       <DIR>          psibound
+02-29-2000  02:01:51                  398 anonftpd.html
+02-26-2002  02:34:22       <DIR>          nistp224
+08-10-2001  07:27:20       <DIR>          cdb
+04-30-2002  23:58:27       <DIR>          talks
+12-23-2000  21:34:35       <DIR>          ftpparse
+01-11-2000  05:50:10       <DIR>          clockspeed
+02-29-2000  02:01:52                 1350 clockspeed.html
+02-06-2002  20:52:34       <DIR>          slashdoc
+06-07-2002  16:00:40       <DIR>          export
+07-17-2001  01:48:36       <DIR>          daemontools
+07-12-2001  21:17:08                 2187 daemontools.html
+08-30-2001  20:51:16                  175 data.html
+12-28-2001  01:32:37                 1901 crypto.html
+11-10-2001  23:45:23                  739 arith.html
+11-30-2001  03:48:09                 2733 cdb.html
+11-10-2001  23:51:02                  344 floatasm.html
+12-21-2000  21:25:56       <DIR>          djbfft
+02-29-2000  02:01:52                 1319 djbfft.html
+07-09-2000  17:16:47       <DIR>          dnscache
+07-03-2000  22:57:44                 4016 dnscache.html
+07-10-2001  03:16:22       <DIR>          docs
+02-29-2000  02:01:52                  293 docs.html
+02-29-2000  02:01:52                  952 dot-forward.html
+01-11-2000  05:50:11       <DIR>          etc-mta
+02-29-2000  02:01:52                 3174 etc-mta.html
+02-29-2000  02:01:52                 2990 ezmlm.html
+12-21-2000  21:25:56       <DIR>          ftp
+02-29-2000  02:01:49       <DIR>          im
+02-29-2000  02:01:52                 1823 fastforward.html
+02-29-2000  02:01:52                 1707 ftp.html
+02-12-2001  02:17:10       <DIR>          freebugtraq
+06-02-2002  01:39:25       <DIR>          hardware
+06-01-2001  02:57:35                  610 1995-514.html
+12-21-2000  21:25:57       <DIR>          hash127
+05-19-2000  02:29:02                 4835 hash127.html
+02-29-2000  02:01:52                 1515 im.html
+02-12-2002  20:40:06       <DIR>          immhf
+04-25-2001  21:48:49                 2902 immhf.html
+01-25-2001  05:58:36       <DIR>          lib
+06-05-2001  23:45:50       <DIR>          libtai
+07-10-2000  18:54:16                 2120 libtai.html
+11-27-2001  17:52:40                14460 qmail.html
+08-30-2001  20:51:23                 2153 mail.html
+02-29-2000  02:01:52                  108 maildir.html
+02-29-2000  02:01:50       <DIR>          maildisasters
+02-29-2000  02:01:52                  873 maildisasters.html
+02-29-2000  02:01:52                 1688 mess822.html
+02-25-2002  02:47:27       <DIR>          mirror
+12-26-2001  21:02:32                  581 ntheory.html
+06-03-2002  15:36:27       <DIR>          papers
+05-21-2002  16:50:15       <DIR>          postpropter
+01-11-2000  05:50:12       <DIR>          postings
+01-11-2000  05:50:12       <DIR>          primegen
+08-30-2001  20:51:33                  415 precompiled.html
+02-29-2000  02:01:52                 1121 primegen.html
+05-05-2002  01:37:55       <DIR>          proto
+02-29-2000  02:01:52                  452 proto.html
+07-09-2001  22:17:53       <DIR>          publicfile
+11-30-2001  03:59:17                 5651 publicfile.html
+08-30-2001  20:51:37                  409 qlist.html
+04-02-2002  05:25:45       <DIR>          qmail
+02-02-2002  05:52:28                 7549 lists.html
+02-29-2000  02:01:52                 1166 qmailanalog.html
+08-30-2001  20:51:39                 1434 qmsmac.html
+03-13-2000  05:38:06                  465 rblsmtpd.html
+02-29-2000  02:01:52                 1225 rights.html
+01-11-2000  05:50:13       <DIR>          sarcasm
+09-14-2001  02:34:14                 1058 unix.html
+02-29-2000  02:01:53                 1568 serialmail.html
+10-02-2000  19:13:44                  840 sigs.html
+05-21-2002  16:50:08       <DIR>          smtp
+04-26-2001  15:58:47                 1723 smtp.html
+12-21-2000  21:26:03       <DIR>          software
+02-29-2000  02:01:53                 3671 softwarelaw.html
+01-11-2000  05:50:13       <DIR>          sortedsums
+02-29-2000  02:01:53                 2735 sortedsums.html
+02-26-2002  02:06:38       <DIR>          speed
+07-03-2000  22:40:17                  572 surveydns.html
+10-04-2001  00:59:31       <DIR>          surveys
+10-04-2001  00:46:36                 3503 surveys.html
+11-10-2000  05:58:08       <DIR>          syncookies
+02-01-2002  02:03:37                 7918 syncookies.html
+08-30-2001  20:51:43                 1115 tcpcontrol.html
+03-18-2002  12:08:27                  589 tcpip.html
+02-29-2000  02:01:53                  906 thoughts.html
+07-30-2001  14:18:09       <DIR>          threecubes
+09-14-2001  01:25:58                  525 time.html
+05-22-2002  04:21:05       <DIR>          ucspi-tcp
+01-24-2002  23:41:31                 4086 ucspi-tcp.html
+08-30-2001  20:51:50                  732 web.html
+07-04-2002  17:47:48       <DIR>          www
+09-14-2001  01:26:08                 2717 y2k.html
+02-08-2001  09:28:09       <DIR>          dnsroot
+04-23-2001  06:01:37                 2745 2001-260.html
+02-29-2000  02:01:53                  291 zeroseek.html
+02-29-2000  02:01:53       <DIR>          zmodexp
+05-19-2000  02:29:07                 1513 zmodexp.html
+08-01-2000  01:08:54       <DIR>          zmult
+08-01-2000  01:24:53                  669 zmult.html
+12-23-2000  06:19:47                 1097 checkpwd.html
+12-31-2001  18:47:16                 2077 focus.html
+07-19-2000  23:11:31                 3842 im2000.html
+07-04-2002  17:47:43       <DIR>          slashcommand
+07-04-2002  17:47:44       <DIR>          slashpackage
+07-15-2001  22:24:39                  602 slashpackage.html
+05-03-2002  00:15:33                23874 talks.html
+05-08-2000  05:24:59                  566 1997-275.html
+05-08-2000  05:31:04                  313 1998-100.html
+02-29-2000  02:01:47       <DIR>          2000-436
+05-08-2000  05:37:26                  660 1997-494.html
+05-08-2000  05:40:41                  730 1999-541.html
+09-13-2000  07:08:58                 2367 psibound.html
+06-22-2000  02:09:00                 1866 smallfactors.html
+06-09-2000  01:35:19       <DIR>          smallfactors
+07-01-2000  22:55:19       <DIR>          conferences
+05-22-2002  05:52:29       <DIR>          djbdns
+07-17-2001  04:00:58                 2712 djbdns.html
+07-17-2000  01:15:39                  216 zpfft.html
+08-20-2000  18:56:37       <DIR>          fastnewton
+08-20-2000  18:57:31                  333 fastnewton.html
+09-01-2000  18:33:51       <DIR>          patents
+12-23-2000  21:40:17                 1748 ftpparse.html
+11-10-2001  23:57:50                 3261 slash.html
+01-17-2001  17:50:10                 2663 slashdoc.html
+03-18-2002  09:04:35                 2344 dnsroot.html
+12-26-2001  21:03:00                 3238 software.html
+09-14-2001  02:34:33                 3481 compatibility.html
+05-01-2002  03:30:34                 5525 distributors.html
+02-25-2001  03:49:03                  212 freebugtraq.html
+03-31-2002  03:59:50                  806 hardware.html
+03-31-2001  07:15:18                  121 securesoftware.html
+05-22-2002  02:39:56                10950 conferences.html
+11-30-2001  17:51:50       <DIR>          2001-275
+05-29-2001  00:15:07                  459 rwb100.html
+10-03-2001  05:40:42                  863 nistp224.html
+07-30-2001  14:15:46                  778 threecubes.html
+06-03-2002  15:32:16                27125 papers.html
+11-21-2001  02:51:09                  379 bib.html
+11-30-2001  17:41:33                 3791 2001-275.html
+10-02-2001  18:08:59                  169 dh224.html
+03-16-2002  06:05:36                 1126 courses.html
+09-02-2001  22:22:28                  355 slashcommand.html
+03-27-2002  21:20:22                 1199 patents.html
+11-08-2001  19:27:15                  640 mailcopyright.html
+06-01-2002  23:27:50                 1257 djb.html
+01-07-2002  06:12:34                  301 2002-501.html
+05-01-2002  02:41:45                 1655 export.html
+03-24-2002  10:40:59                 6446 index.html
+03-16-2002  06:09:08                  570 2005-590.html
+03-16-2002  06:05:49                  800 2004-494.html
+06-01-2002  23:28:59                 4459 positions.html
+06-01-2002  22:55:31                  466 contact.html
rename from netwerk/streamconv/converters/parse-ftp/O-guess.in
rename to netwerk/test/gtest/parse-ftp/O-guess.in
rename from netwerk/streamconv/converters/parse-ftp/O-guess.out
rename to netwerk/test/gtest/parse-ftp/O-guess.out
rename from netwerk/streamconv/converters/parse-ftp/R-dls.in
rename to netwerk/test/gtest/parse-ftp/R-dls.in
rename from netwerk/streamconv/converters/parse-ftp/R-dls.out
rename to netwerk/test/gtest/parse-ftp/R-dls.out
rename from netwerk/streamconv/converters/parse-ftp/README
rename to netwerk/test/gtest/parse-ftp/README
new file mode 100644
--- /dev/null
+++ b/netwerk/test/gtest/parse-ftp/TestParseFTPList.cpp
@@ -0,0 +1,169 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsPrintfCString.h"
+#include <stdio.h>
+
+#include "ParseFTPList.h"
+
+
+PRTime gTestTime = 0;
+
+// Pretend this is the current time for the purpose of running the
+// test. The day and year matter because they are used to figure out a
+// default value when no year is specified. Tests will fail if this is
+// changed too much.
+const char* kDefaultTestTime = "01-Aug-2002 00:00:00 GMT";
+
+static PRTime
+TestTime()
+{
+  return gTestTime;
+}
+
+static void
+ParseFTPLine(char* inputLine,
+             FILE* resultFile,
+             list_state* state)
+{
+  struct list_result result;
+  int rc = ParseFTPList(inputLine, state, &result, PR_GMTParameters, TestTime);
+
+  if (rc == '?' || rc == '"') {
+    // Ignore junk and comments.
+    return;
+  }
+
+  if (!resultFile) {
+    // No result file was passed in, so there's nothing to check.
+    return;
+  }
+
+  char resultLine[512];
+  ASSERT_NE(fgets(resultLine, sizeof(resultLine), resultFile), nullptr);
+
+  nsPrintfCString parsed("%02u-%02u-%04u  %02u:%02u:%02u %20s %.*s%s%.*s\n",
+                         (result.fe_time.tm_mday ? (result.fe_time.tm_month + 1) : 0),
+                         result.fe_time.tm_mday,
+                         (result.fe_time.tm_mday ? result.fe_time.tm_year : 0),
+                         result.fe_time.tm_hour,
+                         result.fe_time.tm_min,
+                         result.fe_time.tm_sec,
+                         (rc == 'd' ? "<DIR>         " :
+                          (rc == 'l' ? "<JUNCTION>    " : result.fe_size)),
+                         (int)result.fe_fnlen, result.fe_fname,
+                         ((rc == 'l' && result.fe_lnlen) ? " -> " : ""),
+                         (int)((rc == 'l' && result.fe_lnlen) ? result.fe_lnlen : 0),
+                         ((rc == 'l' && result.fe_lnlen) ? result.fe_lname : ""));
+
+  ASSERT_STREQ(parsed.get(), resultLine);
+}
+
+FILE*
+OpenResultFile(const char* resultFileName)
+{
+  if (!resultFileName) {
+    return nullptr;
+  }
+
+  FILE* resultFile = fopen(resultFileName, "r");
+  EXPECT_NE(resultFile, nullptr);
+
+  // Ignore anything in the expected result file before and including the first blank line.
+  char resultLine[512];
+  while (fgets(resultLine, sizeof(resultLine), resultFile)) {
+    size_t lineLen = strlen(resultLine);
+    EXPECT_LT(lineLen, sizeof(resultLine) - 1);
+    if (lineLen > 0 && resultLine[lineLen - 1] == '\n') {
+      lineLen--;
+    }
+    if (lineLen == 0) {
+      break;
+    }
+  }
+
+  // There must be a blank line somewhere in the result file.
+  EXPECT_EQ(strcmp(resultLine, "\n"), 0);
+
+  return resultFile;
+}
+
+void
+ParseFTPFile(const char* inputFileName,
+             const char* resultFileName)
+{
+  printf("Checking %s\n", inputFileName);
+  FILE* inFile = fopen(inputFileName, "r");
+  ASSERT_NE(inFile, nullptr);
+
+  FILE* resultFile = OpenResultFile(resultFileName);
+
+  char inputLine[512];
+  struct list_state state;
+  memset(&state, 0, sizeof(state));
+  while (fgets(inputLine, sizeof(inputLine), inFile)) {
+    size_t lineLen = strlen(inputLine);
+    EXPECT_LT(lineLen, sizeof(inputLine) - 1);
+    if (lineLen > 0 && inputLine[lineLen - 1] == '\n') {
+      lineLen--;
+    }
+
+    ParseFTPLine(inputLine, resultFile, &state);
+  }
+
+  // Make sure there are no extra lines in the result file.
+  if (resultFile) {
+    char resultLine[512];
+    EXPECT_EQ(fgets(resultLine, sizeof(resultLine), resultFile), nullptr) <<
+      "There should not be more lines in the expected results file than in the parser output.";
+    fclose(resultFile);
+  }
+
+  fclose(inFile);
+}
+
+static const char* testFiles[] = {
+  "3-guess",
+  "C-VMold",
+  "C-zVM",
+  "D-WinNT",
+  "E-EPLF",
+  "O-guess",
+  "R-dls",
+  "U-HellSoft",
+  "U-hethmon",
+  "U-murksw",
+  "U-ncFTPd",
+  "U-NetPresenz",
+  "U-NetWare",
+  "U-nogid",
+  "U-no_ug",
+  "U-Novonyx",
+  "U-proftpd",
+  "U-Surge",
+  "U-WarFTPd",
+  "U-WebStar",
+  "U-WinNT",
+  "U-wu",
+  "V-MultiNet",
+  "V-VMS-mix",
+};
+
+TEST(ParseFTPTest, Check)
+{
+  PRStatus result = PR_ParseTimeString(kDefaultTestTime, true, &gTestTime);
+  ASSERT_EQ(PR_SUCCESS, result);
+
+  char inputFileName[200];
+  char resultFileName[200];
+  for (size_t test = 0; test < mozilla::ArrayLength(testFiles); ++test) {
+    snprintf(inputFileName, mozilla::ArrayLength(inputFileName), "%s.in", testFiles[test]);
+    snprintf(resultFileName, mozilla::ArrayLength(inputFileName), "%s.out", testFiles[test]);
+    ParseFTPFile(inputFileName, resultFileName);
+  }
+}
+
rename from netwerk/streamconv/converters/parse-ftp/U-HellSoft.in
rename to netwerk/test/gtest/parse-ftp/U-HellSoft.in
rename from netwerk/streamconv/converters/parse-ftp/U-HellSoft.out
rename to netwerk/test/gtest/parse-ftp/U-HellSoft.out
rename from netwerk/streamconv/converters/parse-ftp/U-NetPresenz.in
rename to netwerk/test/gtest/parse-ftp/U-NetPresenz.in
rename from netwerk/streamconv/converters/parse-ftp/U-NetPresenz.out
rename to netwerk/test/gtest/parse-ftp/U-NetPresenz.out
rename from netwerk/streamconv/converters/parse-ftp/U-NetWare.in
rename to netwerk/test/gtest/parse-ftp/U-NetWare.in
rename from netwerk/streamconv/converters/parse-ftp/U-NetWare.out
rename to netwerk/test/gtest/parse-ftp/U-NetWare.out
rename from netwerk/streamconv/converters/parse-ftp/U-Novonyx.in
rename to netwerk/test/gtest/parse-ftp/U-Novonyx.in
rename from netwerk/streamconv/converters/parse-ftp/U-Novonyx.out
rename to netwerk/test/gtest/parse-ftp/U-Novonyx.out
rename from netwerk/streamconv/converters/parse-ftp/U-Surge.in
rename to netwerk/test/gtest/parse-ftp/U-Surge.in
rename from netwerk/streamconv/converters/parse-ftp/U-Surge.out
rename to netwerk/test/gtest/parse-ftp/U-Surge.out
rename from netwerk/streamconv/converters/parse-ftp/U-WarFTPd.in
rename to netwerk/test/gtest/parse-ftp/U-WarFTPd.in
rename from netwerk/streamconv/converters/parse-ftp/U-WarFTPd.out
rename to netwerk/test/gtest/parse-ftp/U-WarFTPd.out
rename from netwerk/streamconv/converters/parse-ftp/U-WebStar.in
rename to netwerk/test/gtest/parse-ftp/U-WebStar.in
rename from netwerk/streamconv/converters/parse-ftp/U-WebStar.out
rename to netwerk/test/gtest/parse-ftp/U-WebStar.out
rename from netwerk/streamconv/converters/parse-ftp/U-WinNT.in
rename to netwerk/test/gtest/parse-ftp/U-WinNT.in
rename from netwerk/streamconv/converters/parse-ftp/U-WinNT.out
rename to netwerk/test/gtest/parse-ftp/U-WinNT.out
--- a/netwerk/streamconv/converters/parse-ftp/U-WinNT.out
+++ b/netwerk/test/gtest/parse-ftp/U-WinNT.out
@@ -1,72 +1,54 @@
 <!-- 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/. -->
 
-cmnt: ftp> open ftp.microsoft.com
-cmnt: Microsoft FTP Service
-cmnt: Name (ftp.microsoft.com:cyp): 
-cmnt: 331 Anonymous access allowed, send identity (e-mail name) as password.
-cmnt: 230-This is FTP.Microsoft.Com
-cmnt: Anonymous user logged in.
-cmnt: Remote system type is Windows_NT.
-cmnt: ftp> quote dirstyle
-cmnt: MSDOS-like directory output is on
-cmnt: ftp> quote dirstyle
-cmnt: MSDOS-like directory output is off
-cmnt: ftp> ls
-cmnt: PORT command successful.
-cmnt: Opening ASCII mode data connection for /bin/ls.
-list: 02-25-00  00:00AM       <DIR>          channel
-list: 02-25-00  00:00AM       <DIR>          Education
-list: 02-25-00  00:00AM       <DIR>          enterprise
-list: 06-20-01  00:00AM       <DIR>          ISN
-list: 05-31-01  00:00AM       <DIR>          Museum
-list: 02-14-01  00:00AM       <DIR>          TechNet
-list: 10-24-01  00:00AM       <DIR>          whql
-list: 02-05-01  00:00AM       <DIR>          20Year
-list: 09-26-00  00:00AM       <DIR>          AccessFoxPro
-list: 12-21-00  00:00AM       <DIR>          AlbGrp
-list: 05-31-01  00:00AM       <DIR>          Alexandra
-list: 01-19-01  00:00AM       <DIR>          anna
-list: 04-06-00  00:00AM       <DIR>          anz
-list: 05-10-00  00:00AM       <DIR>          Chase Bobko2
-list: 03-29-00  00:00AM       <DIR>          cnn
-list: 11-21-00  00:00AM       <DIR>          Darin
-list: 03-09-00  00:00AM       <DIR>          digitalchicago
-list: 09-06-00  00:00AM       <DIR>          Dublin
-list: 01-30-01  00:00AM       <DIR>          eleanorf
-list: 04-26-01  00:00AM       <DIR>          girvin
-list: 04-26-00  00:00AM       <DIR>          Hires
-list: 08-15-00  00:00AM       <DIR>          HR
-list: 10-24-99  00:00AM              4368384 IMG00022.PCD
-list: 05-18-01  00:00AM       <DIR>          jacubowsky
-list: 10-12-00  00:00AM       <DIR>          JFalvey
-list: 03-28-01  00:00AM       <DIR>          johnci
-list: 07-14-00  00:00AM       <DIR>          Karin
-list: 09-07-00  00:00AM       <DIR>          Kjung
-list: 09-28-00  00:00AM       <DIR>          LarryE
-list: 08-17-00  00:00AM       <DIR>          Larson
-list: 09-12-00  00:00AM       <DIR>          marion
-list: 08-09-00  00:00AM       <DIR>          ms25
-list: 11-16-00  00:00AM       <DIR>          MS25Brochure
-list: 03-29-00  00:00AM       <DIR>          MShistory
-list: 09-05-00  00:00AM       <DIR>          Neils
-list: 08-02-00  00:00AM       <DIR>          NLM
-list: 09-06-00  00:00AM       <DIR>          PageOne
-list: 06-27-00  00:00AM       <DIR>          pccomputing
-list: 05-09-01  00:00AM       <DIR>          pictures
-list: 07-21-00  00:00AM       <DIR>          pranks
-list: 08-22-00  00:00AM       <DIR>          Sean
-list: 08-10-00  00:00AM       <DIR>          SLeong
-list: 09-07-00  00:00AM       <DIR>          svr
-list: 07-21-00  00:00AM       <DIR>          Transcontinental
-list: 10-23-00  00:00AM       <DIR>          veronist
-list: 06-15-00  00:00AM       <DIR>          zoe
-list: 07-14-00  00:00AM              2094926 canprankdesk.tif
-list: 07-21-00  00:00AM                95077 Jon Kauffman Enjoys the Good Life.jpg
-list: 07-21-00  00:00AM                52275 Name Plate.jpg
-list: 07-14-00  00:00AM              2250540 Valentineoffprank-HiRes.jpg
-junk: Transfer complete.
-junk: ftp> close
-junk:  Thank-You For Using Microsoft Products!
-junk: ftp> 
+02-25-2000  00:00:00       <DIR>          channel
+02-25-2000  00:00:00       <DIR>          Education
+02-25-2000  00:00:00       <DIR>          enterprise
+06-20-2001  00:00:00       <DIR>          ISN
+05-31-2001  00:00:00       <DIR>          Museum
+02-14-2001  00:00:00       <DIR>          TechNet
+10-24-2001  00:00:00       <DIR>          whql
+02-05-2001  00:00:00       <DIR>          20Year
+09-26-2000  00:00:00       <DIR>          AccessFoxPro
+12-21-2000  00:00:00       <DIR>          AlbGrp
+05-31-2001  00:00:00       <DIR>          Alexandra
+01-19-2001  00:00:00       <DIR>          anna
+04-06-2000  00:00:00       <DIR>          anz
+05-10-2000  00:00:00       <DIR>          Chase Bobko2
+03-29-2000  00:00:00       <DIR>          cnn
+11-21-2000  00:00:00       <DIR>          Darin
+03-09-2000  00:00:00       <DIR>          digitalchicago
+09-06-2000  00:00:00       <DIR>          Dublin
+01-30-2001  00:00:00       <DIR>          eleanorf
+04-26-2001  00:00:00       <DIR>          girvin
+04-26-2000  00:00:00       <DIR>          Hires
+08-15-2000  00:00:00       <DIR>          HR
+10-24-1999  00:00:00              4368384 IMG00022.PCD
+05-18-2001  00:00:00       <DIR>          jacubowsky
+10-12-2000  00:00:00       <DIR>          JFalvey
+03-28-2001  00:00:00       <DIR>          johnci
+07-14-2000  00:00:00       <DIR>          Karin
+09-07-2000  00:00:00       <DIR>          Kjung
+09-28-2000  00:00:00       <DIR>          LarryE
+08-17-2000  00:00:00       <DIR>          Larson
+09-12-2000  00:00:00       <DIR>          marion
+08-09-2000  00:00:00       <DIR>          ms25
+11-16-2000  00:00:00       <DIR>          MS25Brochure
+03-29-2000  00:00:00       <DIR>          MShistory
+09-05-2000  00:00:00       <DIR>          Neils
+08-02-2000  00:00:00       <DIR>          NLM
+09-06-2000  00:00:00       <DIR>          PageOne
+06-27-2000  00:00:00       <DIR>          pccomputing
+05-09-2001  00:00:00       <DIR>          pictures
+07-21-2000  00:00:00       <DIR>          pranks
+08-22-2000  00:00:00       <DIR>          Sean
+08-10-2000  00:00:00       <DIR>          SLeong
+09-07-2000  00:00:00       <DIR>          svr
+07-21-2000  00:00:00       <DIR>          Transcontinental
+10-23-2000  00:00:00       <DIR>          veronist
+06-15-2000  00:00:00       <DIR>          zoe
+07-14-2000  00:00:00              2094926 canprankdesk.tif
+07-21-2000  00:00:00                95077 Jon Kauffman Enjoys the Good Life.jpg
+07-21-2000  00:00:00                52275 Name Plate.jpg
+07-14-2000  00:00:00              2250540 Valentineoffprank-HiRes.jpg
rename from netwerk/streamconv/converters/parse-ftp/U-hethmon.in
rename to netwerk/test/gtest/parse-ftp/U-hethmon.in
rename from netwerk/streamconv/converters/parse-ftp/U-hethmon.out
rename to netwerk/test/gtest/parse-ftp/U-hethmon.out
rename from netwerk/streamconv/converters/parse-ftp/U-murksw.in
rename to netwerk/test/gtest/parse-ftp/U-murksw.in
rename from netwerk/streamconv/converters/parse-ftp/U-murksw.out
rename to netwerk/test/gtest/parse-ftp/U-murksw.out
rename from netwerk/streamconv/converters/parse-ftp/U-ncFTPd.in
rename to netwerk/test/gtest/parse-ftp/U-ncFTPd.in
rename from netwerk/streamconv/converters/parse-ftp/U-ncFTPd.out
rename to netwerk/test/gtest/parse-ftp/U-ncFTPd.out
rename from netwerk/streamconv/converters/parse-ftp/U-no_ug.in
rename to netwerk/test/gtest/parse-ftp/U-no_ug.in
rename from netwerk/streamconv/converters/parse-ftp/U-no_ug.out
rename to netwerk/test/gtest/parse-ftp/U-no_ug.out
rename from netwerk/streamconv/converters/parse-ftp/U-nogid.in
rename to netwerk/test/gtest/parse-ftp/U-nogid.in
rename from netwerk/streamconv/converters/parse-ftp/U-nogid.out
rename to netwerk/test/gtest/parse-ftp/U-nogid.out
rename from netwerk/streamconv/converters/parse-ftp/U-proftpd.in
rename to netwerk/test/gtest/parse-ftp/U-proftpd.in
rename from netwerk/streamconv/converters/parse-ftp/U-proftpd.out
rename to netwerk/test/gtest/parse-ftp/U-proftpd.out
rename from netwerk/streamconv/converters/parse-ftp/U-wu.in
rename to netwerk/test/gtest/parse-ftp/U-wu.in
rename from netwerk/streamconv/converters/parse-ftp/U-wu.out
rename to netwerk/test/gtest/parse-ftp/U-wu.out
rename from netwerk/streamconv/converters/parse-ftp/V-MultiNet.in
rename to netwerk/test/gtest/parse-ftp/V-MultiNet.in
rename from netwerk/streamconv/converters/parse-ftp/V-MultiNet.out
rename to netwerk/test/gtest/parse-ftp/V-MultiNet.out
rename from netwerk/streamconv/converters/parse-ftp/V-VMS-mix.in
rename to netwerk/test/gtest/parse-ftp/V-VMS-mix.in
rename from netwerk/streamconv/converters/parse-ftp/V-VMS-mix.out
rename to netwerk/test/gtest/parse-ftp/V-VMS-mix.out
--- a/netwerk/streamconv/converters/parse-ftp/V-VMS-mix.out
+++ b/netwerk/test/gtest/parse-ftp/V-VMS-mix.out
@@ -1,10 +1,10 @@
 <!-- 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/. -->
 
-11-04-1994  14:09:00                      LOGIN.COM
-01-29-1996  03:33:12                      CII-MANUAL.TEX
-01-29-1996  03:33:12                      THIS-IS-A-LONG-VMS-FILENAME.WITH-CR-TO-NEXT-LINE
-01-29-1996  03:33:00                      ANOTHER-LONG-VMS-FILENAME.WITH-LF-TO-NEXT-LINE
-03-05-1993  18:09:00                      CMU-VMS-IP-FTP-FILE
+11-04-1994  14:09:00                  512 LOGIN.COM
+01-29-1996  03:33:12               109056 CII-MANUAL.TEX
+01-29-1996  03:33:12               109056 THIS-IS-A-LONG-VMS-FILENAME.WITH-CR-TO-NEXT-LINE
+01-29-1996  03:33:00               109056 ANOTHER-LONG-VMS-FILENAME.WITH-LF-TO-NEXT-LINE
+03-05-1993  18:09:00                  512 CMU-VMS-IP-FTP-FILE
 03-05-1993  18:09:00        2199023255040 MAX_FILESIZE.FILE
new file mode 100644
--- /dev/null
+++ b/netwerk/test/gtest/parse-ftp/moz.build
@@ -0,0 +1,68 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+    'TestParseFTPList.cpp',
+]
+
+TEST_HARNESS_FILES.gtest += [
+    '3-guess.in',
+    '3-guess.out',
+    'C-VMold.in',
+    'C-VMold.out',
+    'C-zVM.in',
+    'C-zVM.out',
+    'D-WinNT.in',
+    'D-WinNT.out',
+    'E-EPLF.in',
+    'E-EPLF.out',
+    'O-guess.in',
+    'O-guess.out',
+    'R-dls.in',
+    'R-dls.out',
+    'U-HellSoft.in',
+    'U-HellSoft.out',
+    'U-hethmon.in',
+    'U-hethmon.out',
+    'U-murksw.in',
+    'U-murksw.out',
+    'U-ncFTPd.in',
+    'U-ncFTPd.out',
+    'U-NetPresenz.in',
+    'U-NetPresenz.out',
+    'U-NetWare.in',
+    'U-NetWare.out',
+    'U-no_ug.in',
+    'U-no_ug.out',
+    'U-nogid.in',
+    'U-nogid.out',
+    'U-Novonyx.in',
+    'U-Novonyx.out',
+    'U-proftpd.in',
+    'U-proftpd.out',
+    'U-Surge.in',
+    'U-Surge.out',
+    'U-WarFTPd.in',
+    'U-WarFTPd.out',
+    'U-WebStar.in',
+    'U-WebStar.out',
+    'U-WinNT.in',
+    'U-WinNT.out',
+    'U-wu.in',
+    'U-wu.out',
+    'V-MultiNet.in',
+    'V-MultiNet.out',
+    'V-VMS-mix.in',
+    'V-VMS-mix.out',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+LOCAL_INCLUDES += [
+    '/netwerk/streamconv/converters',
+]
+
+FINAL_LIBRARY = 'xul-gtest'
--- a/python/mach_commands.py
+++ b/python/mach_commands.py
@@ -120,29 +120,31 @@ class MachCommands(MachCommandBase):
                                                       flavor='python')
             else:
                 # Otherwise just run everything in PYTHON_UNITTEST_MANIFESTS
                 test_objects = resolver.resolve_tests(flavor='python')
 
         mp = TestManifest()
         mp.tests.extend(test_objects)
 
-        if not mp.tests:
-            message = 'TEST-UNEXPECTED-FAIL | No tests collected ' + \
-                      '(Not in PYTHON_UNITTEST_MANIFESTS?)'
-            self.log(logging.WARN, 'python-test', {}, message)
-            return 1
-
         filters = []
         if subsuite == 'default':
             filters.append(mpf.subsuite(None))
         elif subsuite:
             filters.append(mpf.subsuite(subsuite))
 
         tests = mp.active_tests(filters=filters, disabled=False, **mozinfo.info)
+
+        if not tests:
+            submsg = "for subsuite '{}' ".format(subsuite) if subsuite else ""
+            message = "TEST-UNEXPECTED-FAIL | No tests collected " + \
+                      "{}(Not in PYTHON_UNITTEST_MANIFESTS?)".format(submsg)
+            self.log(logging.WARN, 'python-test', {}, message)
+            return 1
+
         parallel = []
         sequential = []
         for test in tests:
             if test.get('sequential'):
                 sequential.append(test)
             else:
                 parallel.append(test)
 
--- a/servo/Cargo.lock
+++ b/servo/Cargo.lock
@@ -1014,17 +1014,17 @@ name = "fxhash"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "gamma-lut"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "gaol"
 version = "0.0.1"
@@ -3582,45 +3582,45 @@ dependencies = [
  "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "uuid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "webdriver 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "webrender"
 version = "0.52.1"
-source = "git+https://github.com/servo/webrender#717d29831344c753009eba8c550914484e3bc91f"
+source = "git+https://github.com/servo/webrender#cd4415aa563903f08ecd0602ec97a79879efcaf6"
 dependencies = [
  "app_units 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "bincode 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-foundation 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-graphics 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-text 7.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "dwrote 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "freetype 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "gamma-lut 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "gamma-lut 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "gleam 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "plane-split 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "thread_profiler 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "webrender_api 0.52.1 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "webrender_api"
 version = "0.52.1"
-source = "git+https://github.com/servo/webrender#717d29831344c753009eba8c550914484e3bc91f"
+source = "git+https://github.com/servo/webrender#cd4415aa563903f08ecd0602ec97a79879efcaf6"
 dependencies = [
  "app_units 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "bincode 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-foundation 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-graphics 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "dwrote 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3837,17 +3837,17 @@ dependencies = [
 "checksum flate2 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)" = "36df0166e856739905cd3d7e0b210fe818592211a008862599845e012d8d304c"
 "checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344"
 "checksum fontsan 0.3.2 (git+https://github.com/servo/fontsan)" = "<none>"
 "checksum foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e4056b9bd47f8ac5ba12be771f77a0dae796d1bbaaf5fd0b9c2d38b69b8a29d"
 "checksum freetype 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "398b8a11884898184d55aca9806f002b3cf68f0e860e0cbb4586f834ee39b0e7"
 "checksum futf 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "51f93f3de6ba1794dcd5810b3546d004600a59a98266487c8407bc4b24e398f3"
 "checksum futures 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "55f0008e13fc853f79ea8fc86e931486860d4c4c156cdffb59fa5f7fa833660a"
 "checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
-"checksum gamma-lut 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41f72af1e933f296b827361eb9e70d0267abf8ad0de9ec7fa667bbe67177b297"
+"checksum gamma-lut 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dd65074503368cef99b98844012adfed8d7f99ff3e1e6d05e9055232f2d59dc9"
 "checksum gaol 0.0.1 (git+https://github.com/servo/gaol)" = "<none>"
 "checksum gcc 0.3.47 (registry+https://github.com/rust-lang/crates.io-index)" = "5773372df827453bc38d4fd8efe425c7f28b1f54468816183fc8716cfb90bd30"
 "checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518"
 "checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685"
 "checksum gif 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8a80d6fe9e52f637df9afd4779449a7be17c39cc9c35b01589bb833f956ba596"
 "checksum gl_generator 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0940975a4ca12b088d32b5d5134826c47d2e73de4b0b459b05244c01503eccbb"
 "checksum gleam 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "bf887141f0c2a83eae026cbf3fba74f0a5cb0f01d20e5cdfcd8c4ad39295be1e"
 "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
--- a/servo/components/hashglobe/src/diagnostic.rs
+++ b/servo/components/hashglobe/src/diagnostic.rs
@@ -1,19 +1,25 @@
 use hash_map::HashMap;
 use std::borrow::Borrow;
 use std::hash::{BuildHasher, Hash};
+use std::ptr;
 
 use FailedAllocationError;
 
 #[cfg(target_pointer_width = "32")]
 const CANARY: usize = 0x42cafe99;
 #[cfg(target_pointer_width = "64")]
 const CANARY: usize = 0x42cafe9942cafe99;
 
+#[cfg(target_pointer_width = "32")]
+const POISON: usize = 0xdeadbeef;
+#[cfg(target_pointer_width = "64")]
+const POISON: usize = 0xdeadbeefdeadbeef;
+
 #[derive(Clone, Debug)]
 enum JournalEntry {
     Insert(usize),
     GOIW(usize),
     Remove(usize),
     DidClear(usize),
 }
 
@@ -49,27 +55,29 @@ impl<K: Hash + Eq, V, S: BuildHasher> Di
         self.map.verify();
         assert!(!self.readonly);
         self.readonly = true;
         self.verify();
     }
 
     fn verify(&self) {
         let mut position = 0;
-        let mut bad_canary: Option<(usize, *const usize)> = None;
+        let mut count = 0;
+        let mut bad_canary = None;
         for (_,v) in self.map.iter() {
             let canary_ref = &v.0;
+            position += 1;
             if *canary_ref == CANARY {
-                position += 1;
                 continue;
             }
-            bad_canary = Some((*canary_ref, canary_ref));
+            count += 1;
+            bad_canary = Some((*canary_ref, canary_ref, position));
         }
         if let Some(c) = bad_canary {
-            self.report_corruption(c.0, c.1, position);
+            self.report_corruption(c.0, c.1, c.2, count);
         }
     }
 
     #[inline(always)]
     pub fn with_hasher(hash_builder: S) -> Self {
         Self {
             map: HashMap::<K, (usize, V), S>::with_hasher(hash_builder),
             journal: Vec::new(),
@@ -125,16 +133,19 @@ impl<K: Hash + Eq, V, S: BuildHasher> Di
 
     #[inline(always)]
     pub fn remove<Q: ?Sized>(&mut self, k: &Q) -> Option<V>
         where K: Borrow<Q>,
               Q: Hash + Eq
     {
         assert!(!self.readonly);
         self.journal.push(JournalEntry::Remove(self.map.make_hash(k).inspect()));
+        if let Some(v) = self.map.get_mut(k) {
+            unsafe { ptr::write_volatile(&mut v.0, POISON); }
+        }
         self.map.remove(k).map(|x| x.1)
     }
 
     #[inline(always)]
     pub fn clear(&mut self) where K: 'static, V: 'static  {
         // We handle scoped mutations for the caller here, since callsites that
         // invoke clear() don't benefit from the coalescing we do around insertion.
         self.begin_mutation();
@@ -144,33 +155,36 @@ impl<K: Hash + Eq, V, S: BuildHasher> Di
         self.end_mutation();
     }
 
     #[inline(never)]
     fn report_corruption(
         &self,
         canary: usize,
         canary_addr: *const usize,
-        position: usize
+        position: usize,
+        count: usize,
     ) {
         use ::std::ffi::CString;
         let key = b"HashMapJournal\0";
         let value = CString::new(format!("{:?}", self.journal)).unwrap();
         unsafe {
             Gecko_AnnotateCrashReport(
                 key.as_ptr() as *const ::std::os::raw::c_char,
                 value.as_ptr(),
             );
         }
         panic!(
-            "HashMap Corruption (sz={}, cap={}, pairsz={}, cnry={:#x}, pos={}, base_addr={:?}, cnry_addr={:?}, jrnl_len={})",
+            concat!("HashMap Corruption (sz={}, cap={}, pairsz={}, cnry={:#x}, count={}, ",
+                    "last_pos={}, base_addr={:?}, cnry_addr={:?}, jrnl_len={})"),
             self.map.len(),
             self.map.raw_capacity(),
             ::std::mem::size_of::<(K, (usize, V))>(),
             canary,
+            count,
             position,
             self.map.raw_buffer(),
             canary_addr,
             self.journal.len(),
         );
     }
 }
 
--- a/servo/components/style/custom_properties.rs
+++ b/servo/components/style/custom_properties.rs
@@ -11,16 +11,17 @@ use cssparser::{Delimiter, Parser, Parse
 use precomputed_hash::PrecomputedHash;
 use properties::{CSSWideKeyword, DeclaredValue};
 use selector_map::{PrecomputedHashSet, PrecomputedHashMap, PrecomputedDiagnosticHashMap};
 use selectors::parser::SelectorParseErrorKind;
 use servo_arc::Arc;
 use smallvec::SmallVec;
 use std::ascii::AsciiExt;
 use std::borrow::{Borrow, Cow};
+use std::cmp;
 use std::fmt;
 use std::hash::Hash;
 use style_traits::{ToCss, StyleParseErrorKind, ParseError};
 
 /// A custom property name is just an `Atom`.
 ///
 /// Note that this does not include the `--` prefix
 pub type Name = Atom;
@@ -161,16 +162,33 @@ where
             None => return None,
         };
         self.index.remove(index);
         self.values.begin_mutation();
         let result = self.values.remove(key);
         self.values.end_mutation();
         result
     }
+
+    fn remove_set<S>(&mut self, set: &::hash::HashSet<K, S>)
+        where S: ::std::hash::BuildHasher,
+    {
+        if set.is_empty() {
+            return;
+        }
+        self.index.retain(|key| !set.contains(key));
+        self.values.begin_mutation();
+        // XXX It may be better to use retain when we back to use a
+        // normal hashmap rather than DiagnosticHashMap.
+        for key in set.iter() {
+            self.values.remove(key);
+        }
+        self.values.end_mutation();
+        debug_assert_eq!(self.values.len(), self.index.len());
+    }
 }
 
 /// An iterator for OrderedMap.
 ///
 /// The iteration order is determined by the order that the values are
 /// added to the key-value map.
 pub struct OrderedMapIterator<'a, K, V>
 where
@@ -583,189 +601,242 @@ impl<'a> CustomPropertiesBuilder<'a> {
     /// Otherwise, just use the inherited custom properties map.
     pub fn build(mut self) -> Option<Arc<CustomPropertiesMap>> {
         let mut map = match self.custom_properties.take() {
             Some(m) => m,
             None => return self.inherited.cloned(),
         };
 
         if self.may_have_cycles {
-            remove_cycles(&mut map);
             substitute_all(&mut map);
         }
         Some(Arc::new(map))
     }
 }
 
-/// https://drafts.csswg.org/css-variables/#cycles
+/// Resolve all custom properties to either substituted or invalid.
 ///
-/// The initial value of a custom property is represented by this property not
-/// being in the map.
-fn remove_cycles(map: &mut CustomPropertiesMap) {
-    let mut to_remove = PrecomputedHashSet::default();
-    {
-        type VisitedNamesStack<'a> = SmallVec<[&'a Name; 10]>;
-
-        let mut visited = PrecomputedHashSet::default();
-        let mut stack = VisitedNamesStack::new();
-        for (name, value) in map.iter() {
-            walk(map, name, value, &mut stack, &mut visited, &mut to_remove);
-
-            fn walk<'a>(
-                map: &'a CustomPropertiesMap,
-                name: &'a Name,
-                value: &'a Arc<VariableValue>,
-                stack: &mut VisitedNamesStack<'a>,
-                visited: &mut PrecomputedHashSet<&'a Name>,
-                to_remove: &mut PrecomputedHashSet<Name>,
-            ) {
-                if value.references.is_empty() {
-                    return;
-                }
-
-                let already_visited_before = !visited.insert(name);
-                if already_visited_before {
-                    return
-                }
+/// It does cycle dependencies removal at the same time as substitution.
+fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) {
+    // The cycle dependencies removal in this function is a variant
+    // of Tarjan's algorithm. It is mostly based on the pseudo-code
+    // listed in
+    // https://en.wikipedia.org/w/index.php?
+    // title=Tarjan%27s_strongly_connected_components_algorithm&oldid=801728495
+    //
+    // FIXME This function currently does at least one addref to names
+    // for each variable regardless whether it has reference. Each
+    // variable with any reference would have an additional addref.
+    // There is another addref for each reference.
+    // Strictly speaking, these addrefs are not necessary, because we
+    // don't add/remove entry from custom properties map, and thus keys
+    // should be alive in the whole process until we start removing
+    // invalids. However, there is no safe way for us to prove this to
+    // the compiler. We may be able to fix this issue at some point if
+    // the standard library can provide some kind of hashmap wrapper
+    // with frozen keys.
 
-                stack.push(name);
-                for next in value.references.iter() {
-                    if let Some(position) = stack.iter().position(|x| *x == next) {
-                        // Found a cycle
-                        for &in_cycle in &stack[position..] {
-                            to_remove.insert(in_cycle.clone());
-                        }
-                    } else {
-                        if let Some(value) = map.get(next) {
-                            walk(map, next, value, stack, visited, to_remove);
-                        }
-                    }
-                }
-                stack.pop();
-            }
-        }
-    }
-
-    for name in to_remove {
-        map.remove(&name);
+    /// Struct recording necessary information for each variable.
+    struct VarInfo {
+        /// The name of the variable. It will be taken to save addref
+        /// when the corresponding variable is popped from the stack.
+        /// This also serves as a mark for whether the variable is
+        /// currently in the stack below.
+        name: Option<Name>,
+        /// If the variable is in a dependency cycle, lowlink represents
+        /// a smaller index which corresponds to a variable in the same
+        /// strong connected component, which is known to be accessible
+        /// from this variable. It is not necessarily the root, though.
+        lowlink: usize,
     }
-}
-
-/// Replace `var()` functions for all custom properties.
-fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) {
-    // FIXME(emilio): This stash is needed because we can't prove statically to
-    // rustc that we don't try to mutate the same variable from two recursive
-    // `substitute_one` calls.
-    //
-    // If this is really really hot, we may be able to cheat using `unsafe`, I
-    // guess...
-    let mut stash = PrecomputedHashMap::default();
-    let mut invalid = PrecomputedHashSet::default();
-
-    for (name, value) in custom_properties_map.iter() {
-        if !value.references.is_empty() && !stash.contains_key(name) {
-            let _ = substitute_one(
-                name,
-                value,
-                custom_properties_map,
-                None,
-                &mut stash,
-                &mut invalid,
-            );
-        }
-    }
-
-    for (name, value) in stash.drain() {
-        custom_properties_map.insert(name, value);
+    /// Context struct for traversing the variable graph, so that we can
+    /// avoid referencing all the fields multiple times.
+    struct Context<'a> {
+        /// Number of variables visited. This is used as the order index
+        /// when we visit a new unresolved variable.
+        count: usize,
+        /// The map from custom property name to its order index.
+        index_map: PrecomputedHashMap<Name, usize>,
+        /// Information of each variable indexed by the order index.
+        var_info: SmallVec<[VarInfo; 5]>,
+        /// The stack of order index of visited variables. It contains
+        /// all unfinished strong connected components.
+        stack: SmallVec<[usize; 5]>,
+        map: &'a mut CustomPropertiesMap,
+        /// The set of invalid custom properties.
+        invalid: &'a mut PrecomputedHashSet<Name>,
     }
 
-    for name in invalid.drain() {
-        custom_properties_map.remove(&name);
-    }
+    /// This function combines the traversal for cycle removal and value
+    /// substitution. It returns either a signal None if this variable
+    /// has been fully resolved (to either having no reference or being
+    /// marked invalid), or the order index for the given name.
+    ///
+    /// When it returns, the variable corresponds to the name would be
+    /// in one of the following states:
+    /// * It is still in context.stack, which means it is part of an
+    ///   potentially incomplete dependency circle.
+    /// * It has been added into the invalid set. It can be either that
+    ///   the substitution failed, or it is inside a dependency circle.
+    ///   When this function put a variable into the invalid set because
+    ///   of dependency circle, it would put all variables in the same
+    ///   strong connected component to the set together.
+    /// * It doesn't have any reference, because either this variable
+    ///   doesn't have reference at all in specified value, or it has
+    ///   been completely resolved.
+    /// * There is no such variable at all.
+    fn traverse<'a>(name: Name, context: &mut Context<'a>) -> Option<usize> {
+        use hash::map::Entry;
 
-    debug_assert!(custom_properties_map.iter().all(|(_, v)| v.references.is_empty()));
-}
+        // Some shortcut checks.
+        let (name, value) = if let Some(value) = context.map.get(&name) {
+            // This variable has been resolved. Return the signal value.
+            if value.references.is_empty()  || context.invalid.contains(&name) {
+                return None;
+            }
+            // Whether this variable has been visited in this traversal.
+            let key;
+            match context.index_map.entry(name) {
+                Entry::Occupied(entry) => { return Some(*entry.get()); }
+                Entry::Vacant(entry) => {
+                    key = entry.key().clone();
+                    entry.insert(context.count);
+                }
+            }
+            // Hold a strong reference to the value so that we don't
+            // need to keep reference to context.map.
+            (key, value.clone())
+        } else {
+            // The variable doesn't exist at all.
+            return None;
+        };
+
+        // Add new entry to the information table.
+        let index = context.count;
+        context.count += 1;
+        debug_assert!(index == context.var_info.len());
+        context.var_info.push(VarInfo {
+            name: Some(name),
+            lowlink: index,
+        });
+        context.stack.push(index);
 
-/// Replace `var()` functions for one custom property, leaving the result in
-/// `stash`.
-///
-/// Also recursively record results for other custom properties referenced by
-/// `var()` functions.
-///
-/// Return `Err(())` for invalid at computed time.  or `Ok(last_token_type that
-/// was pushed to partial_computed_value)` otherwise.
-fn substitute_one(
-    name: &Name,
-    specified_value: &Arc<VariableValue>,
-    custom_properties: &CustomPropertiesMap,
-    partial_computed_value: Option<&mut VariableValue>,
-    stash: &mut PrecomputedHashMap<Name, Arc<VariableValue>>,
-    invalid: &mut PrecomputedHashSet<Name>,
-) -> Result<TokenSerializationType, ()> {
-    debug_assert!(!specified_value.references.is_empty());
-    debug_assert!(!stash.contains_key(name));
+        let mut self_ref = false;
+        let mut lowlink = index;
+        for next in value.references.iter() {
+            let next_index = match traverse(next.clone(), context) {
+                Some(index) => index,
+                // There is nothing to do if the next variable has been
+                // fully resolved at this point.
+                None => { continue; }
+            };
+            let next_info = &context.var_info[next_index];
+            if next_index > index {
+                // The next variable has a larger index than us, so it
+                // must be inserted in the recursive call above. We want
+                // to get its lowlink.
+                lowlink = cmp::min(lowlink, next_info.lowlink);
+            } else if next_index == index {
+                self_ref = true;
+            } else if next_info.name.is_some() {
+                // The next variable has a smaller order index and it is
+                // in the stack, so we are at the same component.
+                lowlink = cmp::min(lowlink, next_index);
+            }
+        }
+
+        context.var_info[index].lowlink = lowlink;
+        if lowlink != index {
+            // This variable is in a loop, but it is not the root of
+            // this strong connected component. We simply return for
+            // now, and the root would add it into the invalid set.
+            // This cannot be added into the invalid set here, because
+            // otherwise the shortcut check at the beginning of this
+            // function would return the wrong value.
+            return Some(index);
+        }
 
-    if invalid.contains(name) {
-        return Err(());
+        // This is the root of a strong-connected component.
+        let mut in_loop = self_ref;
+        let name;
+        loop {
+            let var_index = context.stack.pop()
+                .expect("The current variable should still be in stack");
+            let var_info = &mut context.var_info[var_index];
+            // We should never visit the variable again, so it's safe
+            // to take the name away, so that we don't do additional
+            // reference count.
+            let var_name = var_info.name.take()
+                .expect("Variable should not be poped from stack twice");
+            if var_index == index {
+                name = var_name;
+                break;
+            }
+            // Anything here is in a loop which can traverse to the
+            // variable we are handling, so we should add it into
+            // the invalid set. We should never visit the variable
+            // again so it's safe to just take the name away.
+            context.invalid.insert(var_name);
+            in_loop = true;
+        }
+        if in_loop {
+            // This variable is in loop. Resolve to invalid.
+            context.invalid.insert(name);
+            return None;
+        }
+
+        // Now we have shown that this variable is not in a loop, and
+        // all of its dependencies should have been resolved. We can
+        // start substitution now.
+        let mut computed_value = ComputedValue::empty();
+        let mut input = ParserInput::new(&value.css);
+        let mut input = Parser::new(&mut input);
+        let mut position = (input.position(), value.first_token_type);
+        let result = substitute_block(
+            &mut input,
+            &mut position,
+            &mut computed_value,
+            &mut |name, partial_computed_value| {
+                if let Some(value) = context.map.get(name) {
+                    if !context.invalid.contains(name) {
+                        partial_computed_value.push_variable(value);
+                        return Ok(value.last_token_type);
+                    }
+                }
+                Err(())
+            }
+        );
+        if let Ok(last_token_type) = result {
+            computed_value.push_from(position, &input, last_token_type);
+            context.map.insert(name, Arc::new(computed_value));
+        } else {
+            context.invalid.insert(name);
+        }
+
+        // All resolved, so return the signal value.
+        None
     }
 
-    let mut computed_value = ComputedValue::empty();
-    let mut input = ParserInput::new(&specified_value.css);
-    let mut input = Parser::new(&mut input);
-    let mut position = (input.position(), specified_value.first_token_type);
-
-    let result = substitute_block(
-        &mut input,
-        &mut position,
-        &mut computed_value,
-        &mut |name, partial_computed_value| {
-            if let Some(already_computed) = stash.get(name) {
-                partial_computed_value.push_variable(already_computed);
-                return Ok(already_computed.last_token_type);
-            }
-
-            let other_specified_value = match custom_properties.get(name) {
-                Some(v) => v,
-                None => return Err(()),
-            };
-
-            if other_specified_value.references.is_empty() {
-                partial_computed_value.push_variable(other_specified_value);
-                return Ok(other_specified_value.last_token_type);
-            }
-
-            substitute_one(
-                name,
-                other_specified_value,
-                custom_properties,
-                Some(partial_computed_value),
-                stash,
-                invalid
-            )
-        }
-    );
-
-    match result {
-        Ok(last_token_type) => {
-            computed_value.push_from(position, &input, last_token_type);
-        }
-        Err(..) => {
-            invalid.insert(name.clone());
-            return Err(())
-        }
+    // We have to clone the names so that we can mutably borrow the map
+    // in the context we create for traversal.
+    let names = custom_properties_map.index.clone();
+    let mut invalid = PrecomputedHashSet::default();
+    for name in names.into_iter() {
+        let mut context = Context {
+            count: 0,
+            index_map: PrecomputedHashMap::default(),
+            stack: SmallVec::new(),
+            var_info: SmallVec::new(),
+            map: custom_properties_map,
+            invalid: &mut invalid,
+        };
+        traverse(name, &mut context);
     }
 
-    if let Some(partial_computed_value) = partial_computed_value {
-        partial_computed_value.push_variable(&computed_value)
-    }
-
-    let last_token_type = computed_value.last_token_type;
-    stash.insert(name.clone(), Arc::new(computed_value));
-
-    Ok(last_token_type)
+    custom_properties_map.remove_set(&invalid);
 }
 
 /// Replace `var()` functions in an arbitrary bit of input.
 ///
 /// The `substitute_one` callback is called for each `var()` function in `input`.
 /// If the variable has its initial value,
 /// the callback should return `Err(())` and leave `partial_computed_value` unchanged.
 /// Otherwise, it should push the value of the variable (with its own `var()` functions replaced)
--- a/servo/components/style/data.rs
+++ b/servo/components/style/data.rs
@@ -239,41 +239,46 @@ impl ElementData {
         stack_limit_checker: Option<&StackLimitChecker>,
         nth_index_cache: Option<&mut NthIndexCache>,
     ) -> InvalidationResult {
         // In animation-only restyle we shouldn't touch snapshot at all.
         if shared_context.traversal_flags.for_animation_only() {
             return InvalidationResult::empty();
         }
 
+        use invalidation::element::collector::StateAndAttrInvalidationProcessor;
         use invalidation::element::invalidator::TreeStyleInvalidator;
 
         debug!("invalidate_style_if_needed: {:?}, flags: {:?}, has_snapshot: {}, \
                 handled_snapshot: {}, pseudo: {:?}",
                 element,
                 shared_context.traversal_flags,
                 element.has_snapshot(),
                 element.handled_snapshot(),
                 element.implemented_pseudo_element());
 
         if !element.has_snapshot() || element.handled_snapshot() {
             return InvalidationResult::empty();
         }
 
+        let mut processor = StateAndAttrInvalidationProcessor;
         let invalidator = TreeStyleInvalidator::new(
             element,
             Some(self),
             shared_context,
             stack_limit_checker,
             nth_index_cache,
+            &mut processor,
         );
 
         let result = invalidator.invalidate();
+
         unsafe { element.set_handled_snapshot() }
         debug_assert!(element.handled_snapshot());
+
         result
     }
 
     /// Returns true if this element has styles.
     #[inline]
     pub fn has_styles(&self) -> bool {
         self.styles.primary.is_some()
     }
new file mode 100644
--- /dev/null
+++ b/servo/components/style/invalidation/element/collector.rs
@@ -0,0 +1,490 @@
+/* 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/. */
+
+//! An invalidation processor for style changes due to state and attribute
+//! changes.
+
+use Atom;
+use context::{QuirksMode, SharedStyleContext};
+use data::ElementData;
+use dom::TElement;
+use element_state::{ElementState, IN_VISITED_OR_UNVISITED_STATE};
+use invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
+use invalidation::element::invalidation_map::*;
+use invalidation::element::invalidator::{InvalidationVector, Invalidation, InvalidationProcessor};
+use invalidation::element::restyle_hints::*;
+use selector_map::SelectorMap;
+use selector_parser::Snapshot;
+use selectors::NthIndexCache;
+use selectors::attr::CaseSensitivity;
+use selectors::matching::{MatchingContext, MatchingMode, VisitedHandlingMode};
+use selectors::matching::matches_selector;
+use smallvec::SmallVec;
+
+#[derive(Debug, PartialEq)]
+enum VisitedDependent {
+    Yes,
+    No,
+}
+
+/// The collector implementation.
+struct Collector<'a, 'b: 'a, E>
+where
+    E: TElement,
+{
+    element: E,
+    wrapper: ElementWrapper<'b, E>,
+    nth_index_cache: Option<&'a mut NthIndexCache>,
+    snapshot: &'a Snapshot,
+    quirks_mode: QuirksMode,
+    lookup_element: E,
+    removed_id: Option<&'a Atom>,
+    added_id: Option<&'a Atom>,
+    classes_removed: &'a SmallVec<[Atom; 8]>,
+    classes_added: &'a SmallVec<[Atom; 8]>,
+    state_changes: ElementState,
+    descendant_invalidations: &'a mut InvalidationVector,
+    sibling_invalidations: &'a mut InvalidationVector,
+    invalidates_self: bool,
+}
+
+/// An invalidation processor for style changes due to state and attribute
+/// changes.
+pub struct StateAndAttrInvalidationProcessor;
+
+impl<E> InvalidationProcessor<E> for StateAndAttrInvalidationProcessor
+where
+    E: TElement,
+{
+    /// We need to invalidate style on an eager pseudo-element, in order to
+    /// process changes that could otherwise end up in ::before or ::after
+    /// content being generated.
+    fn invalidates_on_eager_pseudo_element(&self) -> bool { true }
+
+    fn collect_invalidations(
+        &mut self,
+        element: E,
+        mut data: Option<&mut ElementData>,
+        nth_index_cache: Option<&mut NthIndexCache>,
+        shared_context: &SharedStyleContext,
+        descendant_invalidations: &mut InvalidationVector,
+        sibling_invalidations: &mut InvalidationVector,
+    ) -> bool {
+        debug_assert!(element.has_snapshot(), "Why bothering?");
+        debug_assert!(data.is_some(), "How exactly?");
+
+        let wrapper =
+            ElementWrapper::new(element, &*shared_context.snapshot_map);
+
+        let state_changes = wrapper.state_changes();
+        let snapshot = wrapper.snapshot().expect("has_snapshot lied");
+
+        if !snapshot.has_attrs() && state_changes.is_empty() {
+            return false;
+        }
+
+        // If we are sensitive to visitedness and the visited state changed, we
+        // force a restyle here. Matching doesn't depend on the actual visited
+        // state at all, so we can't look at matching results to decide what to
+        // do for this case.
+        if state_changes.intersects(IN_VISITED_OR_UNVISITED_STATE) {
+            trace!(" > visitedness change, force subtree restyle");
+            // We can't just return here because there may also be attribute
+            // changes as well that imply additional hints.
+            let data = data.as_mut().unwrap();
+            data.hint.insert(RestyleHint::restyle_subtree());
+        }
+
+        let mut classes_removed = SmallVec::<[Atom; 8]>::new();
+        let mut classes_added = SmallVec::<[Atom; 8]>::new();
+        if snapshot.class_changed() {
+            // TODO(emilio): Do this more efficiently!
+            snapshot.each_class(|c| {
+                if !element.has_class(c, CaseSensitivity::CaseSensitive) {
+                    classes_removed.push(c.clone())
+                }
+            });
+
+            element.each_class(|c| {
+                if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) {
+                    classes_added.push(c.clone())
+                }
+            })
+        }
+
+        let mut id_removed = None;
+        let mut id_added = None;
+        if snapshot.id_changed() {
+            let old_id = snapshot.id_attr();
+            let current_id = element.get_id();
+
+            if old_id != current_id {
+                id_removed = old_id;
+                id_added = current_id;
+            }
+        }
+
+        let lookup_element =
+            if element.implemented_pseudo_element().is_some() {
+                element.pseudo_element_originating_element().unwrap()
+            } else {
+                element
+            };
+
+        let invalidated_self = {
+            let mut collector = Collector {
+                wrapper,
+                lookup_element,
+                nth_index_cache,
+                state_changes,
+                element,
+                snapshot: &snapshot,
+                quirks_mode: shared_context.quirks_mode(),
+                removed_id: id_removed.as_ref(),
+                added_id: id_added.as_ref(),
+                classes_removed: &classes_removed,
+                classes_added: &classes_added,
+                descendant_invalidations,
+                sibling_invalidations,
+                invalidates_self: false,
+            };
+
+            shared_context.stylist.each_invalidation_map(|invalidation_map| {
+                collector.collect_dependencies_in_invalidation_map(invalidation_map);
+            });
+
+            // TODO(emilio): Consider storing dependencies from the UA sheet in
+            // a different map. If we do that, we can skip the stuff on the
+            // shared stylist iff cut_off_inheritance is true, and we can look
+            // just at that map.
+            let _cut_off_inheritance =
+                element.each_xbl_stylist(|stylist| {
+                    // FIXME(emilio): Replace with assert / remove when we
+                    // figure out what to do with the quirks mode mismatches
+                    // (that is, when bug 1406875 is properly fixed).
+                    collector.quirks_mode = stylist.quirks_mode();
+                    stylist.each_invalidation_map(|invalidation_map| {
+                        collector.collect_dependencies_in_invalidation_map(invalidation_map);
+                    });
+                });
+
+            collector.invalidates_self
+        };
+
+        if invalidated_self {
+            if let Some(ref mut data) = data {
+                data.hint.insert(RESTYLE_SELF);
+            }
+        }
+
+        invalidated_self
+    }
+
+    fn should_process_descendants(
+        &mut self,
+        _element: E,
+        data: Option<&mut ElementData>,
+    ) -> bool {
+        let data = match data {
+            None => return false,
+            Some(ref data) => data,
+        };
+
+        !data.styles.is_display_none() &&
+            !data.hint.contains(RESTYLE_DESCENDANTS)
+    }
+
+    fn recursion_limit_exceeded(
+        &mut self,
+        _element: E,
+        data: Option<&mut ElementData>,
+    ) {
+        if let Some(data) = data {
+            data.hint.insert(RESTYLE_DESCENDANTS);
+        }
+    }
+
+    fn invalidated_descendants(
+        &mut self,
+        element: E,
+        data: Option<&mut ElementData>,
+        child: E,
+    ) {
+        if child.get_data().is_none() {
+            return;
+        }
+
+        if data.as_ref().map_or(true, |d| d.styles.is_display_none()) {
+            return;
+        }
+
+        // The child may not be a flattened tree child of the current element,
+        // but may be arbitrarily deep.
+        //
+        // Since we keep the traversal flags in terms of the flattened tree,
+        // we need to propagate it as appropriate.
+        let mut current = child.traversal_parent();
+        while let Some(parent) = current.take() {
+            unsafe { parent.set_dirty_descendants() };
+            current = parent.traversal_parent();
+
+            if parent == element {
+                break;
+            }
+        }
+    }
+
+    fn invalidated_self(
+        &mut self,
+        _element: E,
+        data: Option<&mut ElementData>,
+    ) {
+        if let Some(data) = data {
+            data.hint.insert(RESTYLE_SELF);
+        }
+    }
+}
+
+impl<'a, 'b, E> Collector<'a, 'b, E>
+where
+    E: TElement,
+{
+    fn collect_dependencies_in_invalidation_map(
+        &mut self,
+        map: &InvalidationMap,
+    ) {
+        let quirks_mode = self.quirks_mode;
+        let removed_id = self.removed_id;
+        if let Some(ref id) = removed_id {
+            if let Some(deps) = map.id_to_selector.get(id, quirks_mode) {
+                for dep in deps {
+                    self.scan_dependency(dep, VisitedDependent::No);
+                }
+            }
+        }
+
+        let added_id = self.added_id;
+        if let Some(ref id) = added_id {
+            if let Some(deps) = map.id_to_selector.get(id, quirks_mode) {
+                for dep in deps {
+                    self.scan_dependency(dep, VisitedDependent::No);
+                }
+            }
+        }
+
+        for class in self.classes_added.iter().chain(self.classes_removed.iter()) {
+            if let Some(deps) = map.class_to_selector.get(class, quirks_mode) {
+                for dep in deps {
+                    self.scan_dependency(dep, VisitedDependent::No);
+                }
+            }
+        }
+
+        let should_examine_attribute_selector_map =
+            self.snapshot.other_attr_changed() ||
+            (self.snapshot.class_changed() && map.has_class_attribute_selectors) ||
+            (self.snapshot.id_changed() && map.has_id_attribute_selectors);
+
+        if should_examine_attribute_selector_map {
+            self.collect_dependencies_in_map(
+                &map.other_attribute_affecting_selectors
+            )
+        }
+
+        let state_changes = self.state_changes;
+        if !state_changes.is_empty() {
+            self.collect_state_dependencies(
+                &map.state_affecting_selectors,
+                state_changes,
+            )
+        }
+    }
+
+    fn collect_dependencies_in_map(
+        &mut self,
+        map: &SelectorMap<Dependency>,
+    ) {
+        map.lookup_with_additional(
+            self.lookup_element,
+            self.quirks_mode,
+            self.removed_id,
+            self.classes_removed,
+            &mut |dependency| {
+                self.scan_dependency(dependency, VisitedDependent::No);
+                true
+            },
+        );
+    }
+
+    fn collect_state_dependencies(
+        &mut self,
+        map: &SelectorMap<StateDependency>,
+        state_changes: ElementState,
+    ) {
+        map.lookup_with_additional(
+            self.lookup_element,
+            self.quirks_mode,
+            self.removed_id,
+            self.classes_removed,
+            &mut |dependency| {
+                if !dependency.state.intersects(state_changes) {
+                    return true;
+                }
+                let visited_dependent =
+                    if dependency.state.intersects(IN_VISITED_OR_UNVISITED_STATE) {
+                        VisitedDependent::Yes
+                    } else {
+                        VisitedDependent::No
+                    };
+                self.scan_dependency(&dependency.dep, visited_dependent);
+                true
+            },
+        );
+    }
+
+    /// Check whether a dependency should be taken into account, using a given
+    /// visited handling mode.
+    fn check_dependency(
+        &mut self,
+        visited_handling_mode: VisitedHandlingMode,
+        dependency: &Dependency,
+        relevant_link_found: &mut bool,
+    ) -> bool {
+        let (matches_now, relevant_link_found_now) = {
+            let mut context = MatchingContext::new_for_visited(
+                MatchingMode::Normal,
+                None,
+                self.nth_index_cache.as_mut().map(|c| &mut **c),
+                visited_handling_mode,
+                self.quirks_mode,
+            );
+
+            let matches_now = matches_selector(
+                &dependency.selector,
+                dependency.selector_offset,
+                None,
+                &self.element,
+                &mut context,
+                &mut |_, _| {},
+            );
+
+            (matches_now, context.relevant_link_found)
+        };
+
+        let (matched_then, relevant_link_found_then) = {
+            let mut context = MatchingContext::new_for_visited(
+                MatchingMode::Normal,
+                None,
+                self.nth_index_cache.as_mut().map(|c| &mut **c),
+                visited_handling_mode,
+                self.quirks_mode,
+            );
+
+            let matched_then = matches_selector(
+                &dependency.selector,
+                dependency.selector_offset,
+                None,
+                &self.wrapper,
+                &mut context,
+                &mut |_, _| {},
+            );
+
+            (matched_then, context.relevant_link_found)
+        };
+
+        *relevant_link_found = relevant_link_found_now;
+
+        // Check for mismatches in both the match result and also the status
+        // of whether a relevant link was found.
+        matched_then != matches_now ||
+            relevant_link_found_now != relevant_link_found_then
+    }
+
+    fn scan_dependency(
+        &mut self,
+        dependency: &Dependency,
+        is_visited_dependent: VisitedDependent,
+    ) {
+        debug!("TreeStyleInvalidator::scan_dependency({:?}, {:?}, {:?})",
+               self.element,
+               dependency,
+               is_visited_dependent);
+
+        if !self.dependency_may_be_relevant(dependency) {
+            return;
+        }
+
+        let mut relevant_link_found = false;
+
+        let should_account_for_dependency = self.check_dependency(
+            VisitedHandlingMode::AllLinksUnvisited,
+            dependency,
+            &mut relevant_link_found,
+        );
+
+        if should_account_for_dependency {
+            return self.note_dependency(dependency);
+        }
+
+        // If there is a relevant link, then we also matched in visited
+        // mode.
+        //
+        // Match again in this mode to ensure this also matches.
+        //
+        // Note that we never actually match directly against the element's true
+        // visited state at all, since that would expose us to timing attacks.
+        //
+        // The matching process only considers the relevant link state and
+        // visited handling mode when deciding if visited matches.  Instead, we
+        // are rematching here in case there is some :visited selector whose
+        // matching result changed for some other state or attribute change of
+        // this element (for example, for things like [foo]:visited).
+        //
+        // NOTE: This thing is actually untested because testing it is flaky,
+        // see the tests that were added and then backed out in bug 1328509.
+        if is_visited_dependent == VisitedDependent::Yes && relevant_link_found {
+            let should_account_for_dependency = self.check_dependency(
+                VisitedHandlingMode::RelevantLinkVisited,
+                dependency,
+                &mut false,
+            );
+
+            if should_account_for_dependency {
+                return self.note_dependency(dependency);
+            }
+        }
+    }
+
+    fn note_dependency(&mut self, dependency: &Dependency) {
+        if dependency.affects_self() {
+            self.invalidates_self = true;
+        }
+
+        if dependency.affects_descendants() {
+            debug_assert_ne!(dependency.selector_offset, 0);
+            debug_assert!(!dependency.affects_later_siblings());
+            self.descendant_invalidations.push(Invalidation::new(
+                dependency.selector.clone(),
+                dependency.selector_offset,
+            ));
+        } else if dependency.affects_later_siblings() {
+            debug_assert_ne!(dependency.selector_offset, 0);
+            self.sibling_invalidations.push(Invalidation::new(
+                dependency.selector.clone(),
+                dependency.selector_offset,
+            ));
+        }
+    }
+
+    /// Returns whether `dependency` may cause us to invalidate the style of
+    /// more elements than what we've already invalidated.
+    fn dependency_may_be_relevant(&self, dependency: &Dependency) -> bool {
+        if dependency.affects_descendants() || dependency.affects_later_siblings() {
+            return true;
+        }
+
+        debug_assert!(dependency.affects_self());
+        !self.invalidates_self
+    }
+}
--- a/servo/components/style/invalidation/element/invalidator.rs
+++ b/servo/components/style/invalidation/element/invalidator.rs
@@ -1,62 +1,111 @@
 /* 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/. */
 
 //! The struct that takes care of encapsulating all the logic on where and how
 //! element styles need to be invalidated.
 
-use Atom;
-use context::{QuirksMode, SharedStyleContext, StackLimitChecker};
+use context::{SharedStyleContext, StackLimitChecker};
 use data::ElementData;
 use dom::{TElement, TNode};
-use element_state::{ElementState, IN_VISITED_OR_UNVISITED_STATE};
-use invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
-use invalidation::element::invalidation_map::*;
-use invalidation::element::restyle_hints::*;
-use selector_map::SelectorMap;
-use selector_parser::{SelectorImpl, Snapshot};
+use selector_parser::SelectorImpl;
 use selectors::NthIndexCache;
-use selectors::attr::CaseSensitivity;
 use selectors::matching::{MatchingContext, MatchingMode, VisitedHandlingMode};
-use selectors::matching::{matches_selector, matches_compound_selector};
 use selectors::matching::CompoundSelectorMatchingResult;
+use selectors::matching::matches_compound_selector;
 use selectors::parser::{Combinator, Component, Selector};
 use smallvec::SmallVec;
 use std::fmt;
 
-#[derive(Debug, PartialEq)]
-enum VisitedDependent {
-    Yes,
-    No,
+/// A trait to abstract the collection of invalidations for a given pass.
+///
+/// The `data` argument is a mutable reference to the element's style data, if
+/// any.
+pub trait InvalidationProcessor<E>
+where
+    E: TElement,
+{
+    /// Whether an invalidation that contains only an eager pseudo-element
+    /// selector like ::before or ::after triggers invalidation of the element
+    /// that would originate it.
+    fn invalidates_on_eager_pseudo_element(&self) -> bool { false }
+
+    /// Collect invalidations for a given element's descendants and siblings.
+    ///
+    /// Returns whether the element itself was invalidated.
+    fn collect_invalidations(
+        &mut self,
+        element: E,
+        data: Option<&mut ElementData>,
+        nth_index_cache: Option<&mut NthIndexCache>,
+        shared_context: &SharedStyleContext,
+        descendant_invalidations: &mut InvalidationVector,
+        sibling_invalidations: &mut InvalidationVector,
+    ) -> bool;
+
+    /// Returns whether the invalidation process should process the descendants
+    /// of the given element.
+    fn should_process_descendants(
+        &mut self,
+        element: E,
+        data: Option<&mut ElementData>,
+    ) -> bool;
+
+    /// Executes an arbitrary action when the recursion limit is exceded (if
+    /// any).
+    fn recursion_limit_exceeded(
+        &mut self,
+        element: E,
+        data: Option<&mut ElementData>,
+    );
+
+    /// Executes an action when `Self` is invalidated.
+    fn invalidated_self(
+        &mut self,
+        element: E,
+        data: Option<&mut ElementData>,
+    );
+
+    /// Executes an action when any descendant of `Self` is invalidated.
+    fn invalidated_descendants(
+        &mut self,
+        element: E,
+        data: Option<&mut ElementData>,
+        child: E,
+    );
 }
 
 /// The struct that takes care of encapsulating all the logic on where and how
 /// element styles need to be invalidated.
-pub struct TreeStyleInvalidator<'a, 'b: 'a, E>
-    where E: TElement,
+pub struct TreeStyleInvalidator<'a, 'b: 'a, E, P: 'a>
+where
+    E: TElement,
+    P: InvalidationProcessor<E>
 {
     element: E,
     // TODO(emilio): It's tempting enough to just avoid running invalidation for
     // elements without data.
     //
     // But that's be wrong for sibling invalidations when a new element has been
     // inserted in the tree and still has no data (though I _think_ the slow
     // selector bits save us, it'd be nice not to depend on them).
     //
     // Seems like we could at least avoid running invalidation for the
     // descendants if an element has no data, though.
     data: Option<&'a mut ElementData>,
     shared_context: &'a SharedStyleContext<'b>,
     stack_limit_checker: Option<&'a StackLimitChecker>,
     nth_index_cache: Option<&'a mut NthIndexCache>,
+    processor: &'a mut P,
 }
 
-type InvalidationVector = SmallVec<[Invalidation; 10]>;
+/// A vector of invalidations, optimized for small invalidation sets.
+pub type InvalidationVector = SmallVec<[Invalidation; 10]>;
 
 /// The kind of invalidation we're processing.
 ///
 /// We can use this to avoid pushing invalidations of the same kind to our
 /// descendants or siblings.
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 enum InvalidationKind {
     Descendant,
@@ -66,29 +115,38 @@ enum InvalidationKind {
 /// An `Invalidation` is a complex selector that describes which elements,
 /// relative to a current element we are processing, must be restyled.
 ///
 /// When `offset` points to the right-most compound selector in `selector`,
 /// then the Invalidation `represents` the fact that the current element
 /// must be restyled if the compound selector matches.  Otherwise, if
 /// describes which descendants (or later siblings) must be restyled.
 #[derive(Clone)]
-struct Invalidation {
+pub struct Invalidation {
     selector: Selector<SelectorImpl>,
     offset: usize,
     /// Whether the invalidation was already matched by any previous sibling or
     /// ancestor.
     ///
     /// If this is the case, we can avoid pushing invalidations generated by
     /// this one if the generated invalidation is effective for all the siblings
     /// or descendants after us.
     matched_by_any_previous: bool,
 }
 
 impl Invalidation {
+    /// Create a new invalidation for a given selector and offset.
+    pub fn new(selector: Selector<SelectorImpl>, offset: usize) -> Self {
+        Self {
+            selector,
+            offset,
+            matched_by_any_previous: false,
+        }
+    }
+
     /// Whether this invalidation is effective for the next sibling or
     /// descendant after us.
     fn effective_for_next(&self) -> bool {
         // TODO(emilio): For pseudo-elements this should be mostly false, except
         // for the weird pseudos in <input type="number">.
         //
         // We should be able to do better here!
         match self.selector.combinator_at(self.offset) {
@@ -162,152 +220,60 @@ impl InvalidationResult {
     }
 
     /// Whether the invalidation has invalidate siblings.
     pub fn has_invalidated_siblings(&self) -> bool {
         self.invalidated_siblings
     }
 }
 
-impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
-    where E: TElement,
+impl<'a, 'b: 'a, E, P: 'a> TreeStyleInvalidator<'a, 'b, E, P>
+where
+    E: TElement,
+    P: InvalidationProcessor<E>,
 {
     /// Trivially constructs a new `TreeStyleInvalidator`.
     pub fn new(
         element: E,
         data: Option<&'a mut ElementData>,
         shared_context: &'a SharedStyleContext<'b>,
         stack_limit_checker: Option<&'a StackLimitChecker>,
         nth_index_cache: Option<&'a mut NthIndexCache>,
+        processor: &'a mut P,
     ) -> Self {
         Self {
             element,
             data,
             shared_context,
             stack_limit_checker,
             nth_index_cache,
+            processor,
         }
     }
 
     /// Perform the invalidation pass.
     pub fn invalidate(mut self) -> InvalidationResult {
         debug!("StyleTreeInvalidator::invalidate({:?})", self.element);
-        debug_assert!(self.element.has_snapshot(), "Why bothering?");
-        debug_assert!(self.data.is_some(), "How exactly?");
-
-        let shared_context = self.shared_context;
-
-        let wrapper =
-            ElementWrapper::new(self.element, shared_context.snapshot_map);
-        let state_changes = wrapper.state_changes();
-        let snapshot = wrapper.snapshot().expect("has_snapshot lied");
-
-        if !snapshot.has_attrs() && state_changes.is_empty() {
-            return InvalidationResult::empty();
-        }
-
-        // If we are sensitive to visitedness and the visited state changed, we
-        // force a restyle here. Matching doesn't depend on the actual visited
-        // state at all, so we can't look at matching results to decide what to
-        // do for this case.
-        if state_changes.intersects(IN_VISITED_OR_UNVISITED_STATE) {
-            trace!(" > visitedness change, force subtree restyle");
-            // We can't just return here because there may also be attribute
-            // changes as well that imply additional hints.
-            let data = self.data.as_mut().unwrap();
-            data.hint.insert(RestyleHint::restyle_subtree());
-        }
-
-        let mut classes_removed = SmallVec::<[Atom; 8]>::new();
-        let mut classes_added = SmallVec::<[Atom; 8]>::new();
-        if snapshot.class_changed() {
-            // TODO(emilio): Do this more efficiently!
-            snapshot.each_class(|c| {
-                if !self.element.has_class(c, CaseSensitivity::CaseSensitive) {
-                    classes_removed.push(c.clone())
-                }
-            });
-
-            self.element.each_class(|c| {
-                if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) {
-                    classes_added.push(c.clone())
-                }
-            })
-        }
-
-        let mut id_removed = None;
-        let mut id_added = None;
-        if snapshot.id_changed() {
-            let old_id = snapshot.id_attr();
-            let current_id = self.element.get_id();
-
-            if old_id != current_id {
-                id_removed = old_id;
-                id_added = current_id;
-            }
-        }
-
-        let lookup_element =
-            if self.element.implemented_pseudo_element().is_some() {
-                self.element.pseudo_element_originating_element().unwrap()
-            } else {
-                self.element
-            };
 
         let mut descendant_invalidations = InvalidationVector::new();
         let mut sibling_invalidations = InvalidationVector::new();
-        let invalidated_self = {
-            let mut collector = InvalidationCollector {
-                wrapper,
-                lookup_element,
-                nth_index_cache: self.nth_index_cache.as_mut().map(|c| &mut **c),
-                state_changes,
-                element: self.element,
-                snapshot: &snapshot,
-                quirks_mode: self.shared_context.quirks_mode(),
-                removed_id: id_removed.as_ref(),
-                added_id: id_added.as_ref(),
-                classes_removed: &classes_removed,
-                classes_added: &classes_added,
-                descendant_invalidations: &mut descendant_invalidations,
-                sibling_invalidations: &mut sibling_invalidations,
-                invalidates_self: false,
-            };
 
-            shared_context.stylist.each_invalidation_map(|invalidation_map| {
-                collector.collect_dependencies_in_invalidation_map(invalidation_map);
-            });
-
-            // TODO(emilio): Consider storing dependencies from the UA sheet in
-            // a different map. If we do that, we can skip the stuff on the
-            // shared stylist iff cut_off_inheritance is true, and we can look
-            // just at that map.
-            let _cut_off_inheritance =
-                self.element.each_xbl_stylist(|stylist| {
-                    // FIXME(emilio): Replace with assert / remove when we
-                    // figure out what to do with the quirks mode mismatches
-                    // (that is, when bug 1406875 is properly fixed).
-                    collector.quirks_mode = stylist.quirks_mode();
-                    stylist.each_invalidation_map(|invalidation_map| {
-                        collector.collect_dependencies_in_invalidation_map(invalidation_map);
-                    });
-                });
-
-            collector.invalidates_self
-        };
-
-        if invalidated_self {
-            if let Some(ref mut data) = self.data {
-                data.hint.insert(RESTYLE_SELF);
-            }
-        }
+        let invalidated_self = self.processor.collect_invalidations(
+            self.element,
+            self.data.as_mut().map(|d| &mut **d),
+            self.nth_index_cache.as_mut().map(|c| &mut **c),
+            self.shared_context,
+            &mut descendant_invalidations,
+            &mut sibling_invalidations,
+        );
 
         debug!("Collected invalidations (self: {}): ", invalidated_self);
         debug!(" > descendants: {:?}", descendant_invalidations);
         debug!(" > siblings: {:?}", sibling_invalidations);
+
         let invalidated_descendants = self.invalidate_descendants(&descendant_invalidations);
         let invalidated_siblings = self.invalidate_siblings(&mut sibling_invalidations);
 
         InvalidationResult { invalidated_self, invalidated_descendants, invalidated_siblings }
     }
 
     /// Go through later DOM siblings, invalidating style as needed using the
     /// `sibling_invalidations` list.
@@ -322,24 +288,24 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator
             return false;
         }
 
         let mut current = self.element.next_sibling_element();
         let mut any_invalidated = false;
 
         while let Some(sibling) = current {
             let mut sibling_data = sibling.mutate_data();
-            let sibling_data = sibling_data.as_mut().map(|d| &mut **d);
 
             let mut sibling_invalidator = TreeStyleInvalidator::new(
                 sibling,
-                sibling_data,
+                sibling_data.as_mut().map(|d| &mut **d),
                 self.shared_context,
                 self.stack_limit_checker,
                 self.nth_index_cache.as_mut().map(|c| &mut **c),
+                self.processor,
             );
 
             let mut invalidations_for_descendants = InvalidationVector::new();
             any_invalidated |=
                 sibling_invalidator.process_sibling_invalidations(
                     &mut invalidations_for_descendants,
                     sibling_invalidations,
                 );
@@ -385,64 +351,60 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator
     /// Invalidate a child and recurse down invalidating its descendants if
     /// needed.
     fn invalidate_child(
         &mut self,
         child: E,
         invalidations: &InvalidationVector,
         sibling_invalidations: &mut InvalidationVector,
     ) -> bool {
-        let mut child_data = child.mutate_data();
-        let child_data = child_data.as_mut().map(|d| &mut **d);
+        let mut invalidations_for_descendants = InvalidationVector::new();
+
+        let mut invalidated_child = false;
+        let invalidated_descendants = {
+            let mut child_data = child.mutate_data();
 
-        let mut child_invalidator = TreeStyleInvalidator::new(
-            child,
-            child_data,
-            self.shared_context,
-            self.stack_limit_checker,
-            self.nth_index_cache.as_mut().map(|c| &mut **c),
-        );
-
-        let mut invalidations_for_descendants = InvalidationVector::new();
-        let mut invalidated_child = false;
-
-        invalidated_child |=
-            child_invalidator.process_sibling_invalidations(
-                &mut invalidations_for_descendants,
-                sibling_invalidations,
+            let mut child_invalidator = TreeStyleInvalidator::new(
+                child,
+                child_data.as_mut().map(|d| &mut **d),
+                self.shared_context,
+                self.stack_limit_checker,
+                self.nth_index_cache.as_mut().map(|c| &mut **c),
+                self.processor,
             );
 
-        invalidated_child |=
-            child_invalidator.process_descendant_invalidations(
-                invalidations,
-                &mut invalidations_for_descendants,
-                sibling_invalidations,
-            );
+            invalidated_child |=
+                child_invalidator.process_sibling_invalidations(
+                    &mut invalidations_for_descendants,
+                    sibling_invalidations,
+                );
+
+            invalidated_child |=
+                child_invalidator.process_descendant_invalidations(
+                    invalidations,
+                    &mut invalidations_for_descendants,
+                    sibling_invalidations,
+                );
+
+            child_invalidator.invalidate_descendants(&invalidations_for_descendants)
+        };
 
         // The child may not be a flattened tree child of the current element,
         // but may be arbitrarily deep.
         //
         // Since we keep the traversal flags in terms of the flattened tree,
         // we need to propagate it as appropriate.
-        if invalidated_child && child.get_data().is_some() {
-            let mut current = child.traversal_parent();
-            while let Some(parent) = current.take() {
-                if parent == self.element {
-                    break;
-                }
-
-                unsafe { parent.set_dirty_descendants() };
-                current = parent.traversal_parent();
-            }
+        if invalidated_child || invalidated_descendants {
+            self.processor.invalidated_descendants(
+                self.element,
+                self.data.as_mut().map(|d| &mut **d),
+                child,
+            );
         }
 
-        let invalidated_descendants = child_invalidator.invalidate_descendants(
-            &invalidations_for_descendants
-        );
-
         invalidated_child || invalidated_descendants
     }
 
     fn invalidate_nac(
         &mut self,
         invalidations: &InvalidationVector,
     ) -> bool {
         let mut any_nac_root = false;
@@ -500,30 +462,32 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator
         if invalidations.is_empty() {
             return false;
         }
 
         debug!("StyleTreeInvalidator::invalidate_descendants({:?})",
                self.element);
         debug!(" > {:?}", invalidations);
 
-        match self.data {
-            None => return false,
-            Some(ref data) => {
-                // FIXME(emilio): Only needs to check RESTYLE_DESCENDANTS,
-                // really.
-                if data.hint.contains_subtree() {
-                    return false;
-                }
-            }
+        let should_process =
+            self.processor.should_process_descendants(
+                self.element,
+                self.data.as_mut().map(|d| &mut **d),
+            );
+
+        if !should_process {
+            return false;
         }
 
         if let Some(checker) = self.stack_limit_checker {
             if checker.limit_exceeded() {
-                self.data.as_mut().unwrap().hint.insert(RESTYLE_DESCENDANTS);
+                self.processor.recursion_limit_exceeded(
+                    self.element,
+                    self.data.as_mut().map(|d| &mut **d)
+                );
                 return true;
             }
         }
 
         let mut any_descendant = false;
 
         if let Some(anon_content) = self.element.xbl_binding_anonymous_content() {
             any_descendant |=
@@ -543,20 +507,16 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator
 
         if let Some(after) = self.element.after_pseudo_element() {
             any_descendant |=
                 self.invalidate_pseudo_element_or_nac(after, invalidations);
         }
 
         any_descendant |= self.invalidate_nac(invalidations);
 
-        if any_descendant && self.data.as_ref().map_or(false, |d| !d.styles.is_display_none()) {
-            unsafe { self.element.set_dirty_descendants() };
-        }
-
         any_descendant
     }
 
     /// Process the given sibling invalidations coming from our previous
     /// sibling.
     ///
     /// The sibling invalidations are somewhat special because they can be
     /// modified on the fly. New invalidations may be added and removed.
@@ -707,17 +667,18 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator
                     // is that we wouldn't style them in parallel, which may or
                     // may not be an issue.
                     //
                     // Also, this could be more fine grained now (perhaps a
                     // RESTYLE_PSEUDOS hint?).
                     //
                     // Note that we'll also restyle the pseudo-element because
                     // it would match this invalidation.
-                    if pseudo.is_eager() {
+                    if self.processor.invalidates_on_eager_pseudo_element() &&
+                        pseudo.is_eager() {
                         invalidated_self = true;
                     }
                 }
 
 
                 let next_invalidation = Invalidation {
                     selector: invalidation.selector.clone(),
                     offset: next_combinator_offset,
@@ -806,279 +767,18 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator
                         }
                     }
                 }
             }
             CompoundSelectorMatchingResult::NotMatched => {}
         }
 
         if invalidated_self {
-            if let Some(ref mut data) = self.data {
-                data.hint.insert(RESTYLE_SELF);
-            }
+            self.processor.invalidated_self(
+                self.element,
+                self.data.as_mut().map(|d| &mut **d),
+            );
         }
 
         SingleInvalidationResult { invalidated_self, matched, }
     }
 }
 
-struct InvalidationCollector<'a, 'b: 'a, E>
-    where E: TElement,
-{
-    element: E,
-    wrapper: ElementWrapper<'b, E>,
-    nth_index_cache: Option<&'a mut NthIndexCache>,
-    snapshot: &'a Snapshot,
-    quirks_mode: QuirksMode,
-    lookup_element: E,
-    removed_id: Option<&'a Atom>,
-    added_id: Option<&'a Atom>,
-    classes_removed: &'a SmallVec<[Atom; 8]>,
-    classes_added: &'a SmallVec<[Atom; 8]>,
-    state_changes: ElementState,
-    descendant_invalidations: &'a mut InvalidationVector,
-    sibling_invalidations: &'a mut InvalidationVector,
-    invalidates_self: bool,
-}
-
-impl<'a, 'b: 'a, E> InvalidationCollector<'a, 'b, E>
-    where E: TElement,
-{
-    fn collect_dependencies_in_invalidation_map(
-        &mut self,
-        map: &InvalidationMap,
-    ) {
-        let quirks_mode = self.quirks_mode;
-        let removed_id = self.removed_id;
-        if let Some(ref id) = removed_id {
-            if let Some(deps) = map.id_to_selector.get(id, quirks_mode) {
-                for dep in deps {
-                    self.scan_dependency(dep, VisitedDependent::No);
-                }
-            }
-        }
-
-        let added_id = self.added_id;
-        if let Some(ref id) = added_id {
-            if let Some(deps) = map.id_to_selector.get(id, quirks_mode) {
-                for dep in deps {
-                    self.scan_dependency(dep, VisitedDependent::No);
-                }
-            }
-        }
-
-        for class in self.classes_added.iter().chain(self.classes_removed.iter()) {
-            if let Some(deps) = map.class_to_selector.get(class, quirks_mode) {
-                for dep in deps {
-                    self.scan_dependency(dep, VisitedDependent::No);
-                }
-            }
-        }
-
-        let should_examine_attribute_selector_map =
-            self.snapshot.other_attr_changed() ||
-            (self.snapshot.class_changed() && map.has_class_attribute_selectors) ||
-            (self.snapshot.id_changed() && map.has_id_attribute_selectors);
-
-        if should_examine_attribute_selector_map {
-            self.collect_dependencies_in_map(
-                &map.other_attribute_affecting_selectors
-            )
-        }
-
-        let state_changes = self.state_changes;
-        if !state_changes.is_empty() {
-            self.collect_state_dependencies(
-                &map.state_affecting_selectors,
-                state_changes,
-            )
-        }
-    }
-
-    fn collect_dependencies_in_map(
-        &mut self,
-        map: &SelectorMap<Dependency>,
-    ) {
-        map.lookup_with_additional(
-            self.lookup_element,
-            self.quirks_mode,
-            self.removed_id,
-            self.classes_removed,
-            &mut |dependency| {
-                self.scan_dependency(dependency, VisitedDependent::No);
-                true
-            },
-        );
-    }
-
-    fn collect_state_dependencies(
-        &mut self,
-        map: &SelectorMap<StateDependency>,
-        state_changes: ElementState,
-    ) {
-        map.lookup_with_additional(
-            self.lookup_element,
-            self.quirks_mode,
-            self.removed_id,
-            self.classes_removed,
-            &mut |dependency| {
-                if !dependency.state.intersects(state_changes) {
-                    return true;
-                }
-                let visited_dependent =
-                    if dependency.state.intersects(IN_VISITED_OR_UNVISITED_STATE) {
-                        VisitedDependent::Yes
-                    } else {
-                        VisitedDependent::No
-                    };
-                self.scan_dependency(&dependency.dep, visited_dependent);
-                true
-            },
-        );
-    }
-
-    /// Check whether a dependency should be taken into account, using a given
-    /// visited handling mode.
-    fn check_dependency(
-        &mut self,
-        visited_handling_mode: VisitedHandlingMode,
-        dependency: &Dependency,
-        relevant_link_found: &mut bool,
-    ) -> bool {
-        let (matches_now, relevant_link_found_now) = {
-            let mut context = MatchingContext::new_for_visited(
-                MatchingMode::Normal,
-                None,
-                self.nth_index_cache.as_mut().map(|c| &mut **c),
-                visited_handling_mode,
-                self.quirks_mode,
-            );
-
-            let matches_now = matches_selector(
-                &dependency.selector,
-                dependency.selector_offset,
-                None,
-                &self.element,
-                &mut context,
-                &mut |_, _| {},
-            );
-
-            (matches_now, context.relevant_link_found)
-        };
-
-        let (matched_then, relevant_link_found_then) = {
-            let mut context = MatchingContext::new_for_visited(
-                MatchingMode::Normal,
-                None,
-                self.nth_index_cache.as_mut().map(|c| &mut **c),
-                visited_handling_mode,
-                self.quirks_mode,
-            );
-
-            let matched_then = matches_selector(
-                &dependency.selector,
-                dependency.selector_offset,
-                None,
-                &self.wrapper,
-                &mut context,
-                &mut |_, _| {},
-            );
-
-            (matched_then, context.relevant_link_found)
-        };
-
-        *relevant_link_found = relevant_link_found_now;
-
-        // Check for mismatches in both the match result and also the status
-        // of whether a relevant link was found.
-        matched_then != matches_now ||
-            relevant_link_found_now != relevant_link_found_then
-    }
-
-    fn scan_dependency(
-        &mut self,
-        dependency: &Dependency,
-        is_visited_dependent: VisitedDependent,
-    ) {
-        debug!("TreeStyleInvalidator::scan_dependency({:?}, {:?}, {:?})",
-               self.element,
-               dependency,
-               is_visited_dependent);
-
-        if !self.dependency_may_be_relevant(dependency) {
-            return;
-        }
-
-        let mut relevant_link_found = false;
-
-        let should_account_for_dependency = self.check_dependency(
-            VisitedHandlingMode::AllLinksUnvisited,
-            dependency,
-            &mut relevant_link_found,
-        );
-
-        if should_account_for_dependency {
-            return self.note_dependency(dependency);
-        }
-
-        // If there is a relevant link, then we also matched in visited
-        // mode.
-        //
-        // Match again in this mode to ensure this also matches.
-        //
-        // Note that we never actually match directly against the element's true
-        // visited state at all, since that would expose us to timing attacks.
-        //
-        // The matching process only considers the relevant link state and
-        // visited handling mode when deciding if visited matches.  Instead, we
-        // are rematching here in case there is some :visited selector whose
-        // matching result changed for some other state or attribute change of
-        // this element (for example, for things like [foo]:visited).
-        //
-        // NOTE: This thing is actually untested because testing it is flaky,
-        // see the tests that were added and then backed out in bug 1328509.
-        if is_visited_dependent == VisitedDependent::Yes && relevant_link_found {
-            let should_account_for_dependency = self.check_dependency(
-                VisitedHandlingMode::RelevantLinkVisited,
-                dependency,
-                &mut false,
-            );
-
-            if should_account_for_dependency {
-                return self.note_dependency(dependency);
-            }
-        }
-    }
-
-    fn note_dependency(&mut self, dependency: &Dependency) {
-        if dependency.affects_self() {
-            self.invalidates_self = true;
-        }
-
-        if dependency.affects_descendants() {
-            debug_assert_ne!(dependency.selector_offset, 0);
-            debug_assert!(!dependency.affects_later_siblings());
-            self.descendant_invalidations.push(Invalidation {
-                selector: dependency.selector.clone(),
-                offset: dependency.selector_offset,
-                matched_by_any_previous: false,
-            });
-        } else if dependency.affects_later_siblings() {
-            debug_assert_ne!(dependency.selector_offset, 0);
-            self.sibling_invalidations.push(Invalidation {
-                selector: dependency.selector.clone(),
-                offset: dependency.selector_offset,
-                matched_by_any_previous: false,
-            });
-        }
-    }
-
-    /// Returns whether `dependency` may cause us to invalidate the style of
-    /// more elements than what we've already invalidated.
-    fn dependency_may_be_relevant(&self, dependency: &Dependency) -> bool {
-        if dependency.affects_descendants() || dependency.affects_later_siblings() {
-            return true;
-        }
-
-        debug_assert!(dependency.affects_self());
-        !self.invalidates_self
-    }
-}
--- a/servo/components/style/invalidation/element/mod.rs
+++ b/servo/components/style/invalidation/element/mod.rs
@@ -1,10 +1,11 @@
 /* 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/. */
 
 //! Invalidation of element styles due to attribute or style changes.
 
+pub mod collector;
 pub mod element_wrapper;
 pub mod invalidation_map;
 pub mod invalidator;
 pub mod restyle_hints;
--- a/servo/ports/geckolib/glue.rs
+++ b/servo/ports/geckolib/glue.rs
@@ -1,17 +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 cssparser::{Parser, ParserInput};
 use cssparser::ToCss as ParserToCss;
 use env_logger::LogBuilder;
 use malloc_size_of::MallocSizeOfOps;
-use selectors::{self, Element};
+use selectors::{self, Element, NthIndexCache};
 use selectors::matching::{MatchingContext, MatchingMode, matches_selector};
 use servo_arc::{Arc, ArcBorrow, RawOffsetArc};
 use std::cell::RefCell;
 use std::env;
 use std::fmt::Write;
 use std::iter;
 use std::mem;
 use std::ptr;
@@ -1516,16 +1516,46 @@ pub extern "C" fn Servo_StyleRule_Select
 
         let element = GeckoElement(element);
         let mut ctx = MatchingContext::new(matching_mode, None, None, element.owner_document_quirks_mode());
         matches_selector(selector, 0, None, &element, &mut ctx, &mut |_, _| {})
     })
 }
 
 #[no_mangle]
+pub unsafe extern "C" fn Servo_SelectorList_Closest<'a>(
+    element: RawGeckoElementBorrowed<'a>,
+    selectors: RawServoSelectorListBorrowed,
+) -> RawGeckoElementBorrowedOrNull<'a> {
+    use std::borrow::Borrow;
+
+    let mut nth_index_cache = NthIndexCache::default();
+
+    let element = GeckoElement(element);
+    let mut context = MatchingContext::new(
+        MatchingMode::Normal,
+        None,
+        Some(&mut nth_index_cache),
+        element.owner_document_quirks_mode(),
+    );
+    context.scope_element = Some(element.opaque());
+
+    let selectors = ::selectors::SelectorList::from_ffi(selectors).borrow();
+    let mut current = Some(element);
+    while let Some(element) = current.take() {
+        if selectors::matching::matches_selector_list(&selectors, &element, &mut context) {
+            return Some(element.0);
+        }
+        current = element.parent_element();
+    }
+
+    return None;
+}
+
+#[no_mangle]
 pub unsafe extern "C" fn Servo_SelectorList_Matches(
     element: RawGeckoElementBorrowed,
     selectors: RawServoSelectorListBorrowed,
 ) -> bool {
     use std::borrow::Borrow;
 
     let element = GeckoElement(element);
     let mut context = MatchingContext::new(
--- a/taskcluster/ci/test/test-sets.yml
+++ b/taskcluster/ci/test/test-sets.yml
@@ -97,17 +97,17 @@ stylo-disabled-tests:
     - mochitest-gpu
     - mochitest-media
     - mochitest-webgl
 
 reftest-stylo:
     - reftest-stylo
 
 qr-talos:
-    - talos-chrome
+    # - talos-chrome # regressed, see bug 1408418
     - talos-dromaeojs
     - talos-g1
     # - talos-g2 # doesn't work with QR yet
     - talos-g3
     - talos-g4
     - talos-g5
     # - talos-other # fails with layers-free
     # - talos-svgr # fails with layers-free
--- a/taskcluster/ci/toolchain/linux.yml
+++ b/taskcluster/ci/toolchain/linux.yml
@@ -275,17 +275,17 @@ linux64-sccache:
         max-run-time: 36000
     run:
         using: toolchain-script
         script: build-sccache.sh
         resources:
             - 'taskcluster/scripts/misc/tooltool-download.sh'
         toolchain-artifact: public/build/sccache2.tar.xz
     toolchains:
-        - linux64-clang-3.9
+        - linux64-gcc-4.9
         - linux64-rust-1.19
 
 linux64-gn:
     description: "gn toolchain build"
     treeherder:
         kind: build
         platform: toolchains/opt
         symbol: TL(gn)
--- a/taskcluster/scripts/misc/build-sccache.sh
+++ b/taskcluster/scripts/misc/build-sccache.sh
@@ -4,18 +4,18 @@ set -x -e -v
 SCCACHE_REVISION=df04fa530d6b7d79fef8c848879d47dcc4d95b32
 
 # This script is for building sccache
 
 case "$(uname -s)" in
 Linux)
     WORKSPACE=$HOME/workspace
     UPLOAD_DIR=$HOME/artifacts
-    export CC=clang
-    PATH="$WORKSPACE/build/src/clang/bin:$PATH"
+    export CC=gcc
+    PATH="$WORKSPACE/build/src/gcc/bin:$PATH"
     COMPRESS_EXT=xz
     ;;
 MINGW*)
     WORKSPACE=$PWD
     UPLOAD_DIR=$WORKSPACE/public/build
     WIN_WORKSPACE="$(pwd -W)"
     COMPRESS_EXT=bz2
 
--- a/testing/cppunittest.ini
+++ b/testing/cppunittest.ini
@@ -23,16 +23,17 @@ skip-if = os != 'win'
 [TestIntegerPrintfMacros]
 [TestIntegerRange]
 [TestJSONWriter]
 [TestLinkedList]
 [TestMacroArgs]
 [TestMacroForEach]
 [TestMathAlgorithms]
 [TestMaybe]
+[TestParseFTPList]
 [TestPLDHash]
 skip-if = os == 'b2g'  #Bug 1038197
 [TestPair]
 [TestPoisonArea]
 skip-if = os == 'android' # Bug 1147630
 [TestRefPtr]
 [TestRollingMean]
 [TestScopeExit]
--- a/testing/marionette/cookie.js
+++ b/testing/marionette/cookie.js
@@ -1,15 +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 {interfaces: Ci, utils: Cu} = Components;
+const {interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.import("chrome://marionette/content/assert.js");
 const {
   InvalidCookieDomainError,
   pprint,
 } = Cu.import("chrome://marionette/content/error.js", {});
@@ -55,22 +55,17 @@ cookie.fromJSON = function(json) {
   let newCookie = {};
 
   assert.object(json, pprint`Expected cookie object, got ${json}`);
 
   newCookie.name = assert.string(json.name, "Cookie name must be string");
   newCookie.value = assert.string(json.value, "Cookie value must be string");
 
   if (typeof json.domain != "undefined") {
-    let domain = assert.string(json.domain, "Cookie domain must be string");
-    if (domain.substring(0, 1) !== ".") {
-      // make sure that this is stored as a domain cookie
-      domain = "." + domain;
-    }
-    newCookie.domain = domain;
+    newCookie.domain = assert.string(json.domain, "Cookie domain must be string");
   }
   if (typeof json.path != "undefined") {
     newCookie.path = assert.string(json.path, "Cookie path must be string");
   }
   if (typeof json.secure != "undefined") {
     newCookie.secure = assert.boolean(json.secure, "Cookie secure flag must be boolean");
   }
   if (typeof json.httpOnly != "undefined") {
@@ -99,35 +94,60 @@ cookie.fromJSON = function(json) {
  *     not present and of the correct type.
  * @throws {InvalidCookieDomainError}
  *     If <var>restrictToHost</var> is set and <var>newCookie</var>'s
  *     domain does not match.
  */
 cookie.add = function(newCookie, {restrictToHost = null} = {}) {
   assert.string(newCookie.name, "Cookie name must be string");
   assert.string(newCookie.value, "Cookie value must be string");
+
+  let hostOnly = false;
+  if (typeof newCookie.domain == "undefined") {
+    hostOnly = true;
+    newCookie.domain = restrictToHost;
+  }
   assert.string(newCookie.domain, "Cookie domain must be string");
 
   if (typeof newCookie.path == "undefined") {
     newCookie.path = "/";
   }
 
   if (typeof newCookie.expiry == "undefined") {
     // twenty years into the future
     let date = new Date();
     let now = new Date(Date.now());
     date.setYear(now.getFullYear() + 20);
     newCookie.expiry = date.getTime() / 1000;
   }
 
+  let isIpAddress = false;
+  try {
+    Services.eTLD.getPublicSuffixFromHost(newCookie.domain);
+  } catch (e) {
+    switch (e.result) {
+      case Cr.NS_ERROR_HOST_IS_IP_ADDRESS:
+        isIpAddress = true;
+        break;
+      default:
+        throw new InvalidCookieDomainError(newCookie.domain);
+    }
+  }
+
+  if (!hostOnly && !isIpAddress) {
+    // only store this as a domain cookie if the domain was specified in the
+    // request and it wasn't an IP address.
+    newCookie.domain = "." + newCookie.domain;
+  }
+
   if (restrictToHost) {
     if (!restrictToHost.endsWith(newCookie.domain) &&
-        ("." + restrictToHost) !== newCookie.domain) {
-      throw new InvalidCookieDomainError(
-          `Cookies may only be set ` +
+        ("." + restrictToHost) !== newCookie.domain &&
+        restrictToHost !== newCookie.domain) {
+      throw new InvalidCookieDomainError(`Cookies may only be set ` +
           `for the current domain (${restrictToHost})`);
     }
   }
 
   // remove port from domain, if present.
   // unfortunately this catches IPv6 addresses by mistake
   // TODO: Bug 814416
   newCookie.domain = newCookie.domain.replace(IPV4_PORT_EXPR, "");
new file mode 100644
--- /dev/null
+++ b/testing/marionette/doc/NewContributors.md
@@ -0,0 +1,162 @@
+New contributors
+================
+
+This page is aimed at people who are new to Mozilla and want to contribute
+to Mozilla source code related to Marionette Python tests, WebDriver
+spec tests and related test harnesses and tools. Mozilla has both
+git and Mercurial repositories, but this guide only describes Mercurial.
+
+If you run into issues, check out the Resources section below and
+**don't hesitate to ask questions**. :) The goal of these steps is
+to make sure you have the basics of your development environment
+working. Once you do, we can get you started with working on an
+actual bug, yay!
+
+
+Accounts, communication
+=======================
+
+  1. Set up [IRC].
+
+  2. Set up a [Bugzilla] account and a [Mozillians] profile.
+     Please include your IRC nickname in both of these accounts
+     so we can work with you more easily. For example, Eve Smith
+     would set their Bugzilla name to "Eve Smith (:esmith)", where
+     esmith is their IRC nick.
+
+  3. Join #ateam or #automation on irc.mozilla.org and introduce
+     yourself to the whole team and :maja_zf. We're nice, I promise.
+
+[IRC]: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Getting_Started_with_IRC
+[Bugzilla]: https://bugzilla.mozilla.org/
+[Mozillians]: https://mozillians.org/
+
+
+Getting the code, running tests
+===============================
+
+  1. Follow this tutorial to get a copy of Firefox source code and
+     build it: http://areweeveryoneyet.org/onramp/desktop.html
+
+     If you're asked to run a 'bootstrap' script, choose the option
+     "Firefox for Desktop Artifact Mode".  This significantly
+     reduces the time it takes to build Firefox on your machine
+     (from 30+ minutes to just 1-2 minutes).
+
+  2. Check if you can run any marionette tests: Use [mach] to run
+     [Marionette] unit tests against the Firefox binary you just
+     built in the previous step: `./mach marionette test` -- see
+     [Running Marionette tests] for details.  If you see tests
+     running, that's good enough: **you don't have to run all the
+     tests right now**, that takes a long time.
+
+     Fun fact: the set of tests that you just ran on your machine is
+     triggered automatically for every changeset in the Mozilla source
+     tree!  For example, here are the [latest results on Treeherder].
+
+  3. As an exercise, try out this different way of running the
+     Marionette unit tests, this time against Firefox Nightly
+     instead of the binary in your source tree:
+
+     * Download and install [Firefox Nightly], then find
+       the path to the executable binary that got installed.
+       For example, on a macOS it might be something like
+       `FF_NIGHTLY_PATH=/Applications/FirefoxNightly.app/Contents/MacOS/firefox-bin`.
+
+     * Create and activate a [virtualenv] environment.
+
+     * Within your checkout of the Mozilla source, cd to
+       [testing/marionette/client].
+
+     * Run `python setup.py develop` to install the marionette
+       driver package in development mode in your virtualenv.
+
+     * Next cd to [testing/marionette/harness].
+
+     * Run `cd marionette && python runtests.py tests/unit-tests.ini
+       --binary $FF_NIGHTLY_PATH`.
+
+       * These are the same tests that you ran with `./mach
+         marionette test`, but they are testing the Firefox Nightly
+         that you just installed.  (`./mach marionette tests`
+         just calls code in runtests.py).
+
+     * Configure Mercurial with helpful extensions for Mozilla
+       development by running `./mach mercurial-setup`.
+
+       * It should install extensions like firefox-trees and set
+         you up to be able to use MozReview, our code-review tool.
+
+       * If it asks you about activating the mq extension, I suggest
+         you respond with 'No'.
+
+[mach]: https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/mach
+[Marionette]: ../README.md
+[Running Marionette tests]: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Running_Tests
+[latest results on Treeherder]: https://treeherder.mozilla.org/#/jobs?repo=mozilla-inbound&filter-job_type_symbol=Mn
+[Firefox Nightly]: https://nightly.mozilla.org/
+[virtualenv]: https://www.dabapps.com/blog/introduction-to-pip-and-virtualenv-python/
+[testing/marionette/client]: https://dxr.mozilla.org/mozilla-central/source/testing/marionette/client
+[testing/marionette/harness]: https://dxr.mozilla.org/mozilla-central/source/testing/marionette/harness
+
+
+Work on bugs and get code review
+================================
+
+Once you've completed the above basics, ask :maja_zf, :whimboo,
+or :ato in #ateam for a good first bug to work on.  (Or you can
+also take a look at the simple bugs listed on Bugs Ahoy.)
+
+If you're an Outreachy applicant and you find that we're offline or
+"afk" on IRC, feel free to send Maja an email instead: [myfirstname]
+at mozilla.com -- either way, be sure to tell me which bug you are
+interested in for your application!
+
+To work on the bug that is suggested to you and push a patch up
+for review, follow the [Firefox Workflow] in hg.
+
+After testing your code locally, you will push your patch to be
+reviewed in MozReview.  To set up MozReview, see this [configuration]
+page.  (Note: the only kind of account you need for MozReview is
+Bugzilla (not LDAP) and you can only use HTTPS, not SSH.)
+
+[Firefox Workflow]: https://mozilla-version-control-tools.readthedocs.org/en/latest/hgmozilla/firefoxworkflow.html
+[configuration]: https://mozilla-version-control-tools.readthedocs.org/en/latest/mozreview/install-mercurial.html#configuring-mercurial-to-use-mozreview
+
+
+Resources
+=========
+
+  * Sometimes (often?) documentation is out-of-date.  If something looks
+    off, do ask us for help!
+
+  * Search Mozilla's hg repositories with [DXR].
+
+  * Helpful [guide for new contributors].  This is a good general
+    resource if you ever get stuck on something.  The most relevant
+    sections to you are about Bugzilla, Mercurial, Python and the
+    Development Process.
+
+  * [Mercurial for Mozillians]
+
+  * More general resources are available in this little [guide] I wrote
+    in 2015 to help a student get started with open source contributions.
+
+    * Textbook about general open source practices: [Practical
+      Open Source Software Exploration]
+
+  * This guide provides a one-track, simple flow for getting
+    started with Marionette Test Runner development.
+    The general guide to all Marionette development is at
+    https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Developer_setup.
+
+  * If you'd rather use git instead of hg, see git workflow for
+    Gecko development and/or [this blog post by :ato].
+
+[DXR]: https://dxr.mozilla.org/
+[guide for new contributors]: https://ateam-bootcamp.readthedocs.org/en/latest/guide/index.html#new-contributor-guide
+[Mercurial for Mozillians]: https://mozilla-version-control-tools.readthedocs.org/en/latest/hgmozilla/index.html
+[guide]: https://gist.github.com/mjzffr/d2adef328a416081f543
+[Practical Open Source Software Exploration]: https://quaid.fedorapeople.org/TOS/Practical_Open_Source_Software_Exploration/html/index.html
+[git workflow for Gecko development]: https://github.com/glandium/git-cinnabar/wiki/Mozilla:-A-git-workflow-for-Gecko-development
+[this blog post by :ato]: https://sny.no/2016/03/geckogit
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -2628,19 +2628,16 @@ GeckoDriver.prototype.addCookie = functi
   let {protocol, hostname} = this.currentURL;
 
   const networkSchemes = ["ftp:", "http:", "https:"];
   if (!networkSchemes.includes(protocol)) {
     throw new InvalidCookieDomainError("Document is cookie-averse");
   }
 
   let newCookie = cookie.fromJSON(cmd.parameters.cookie);
-  if (typeof newCookie.domain == "undefined") {
-    newCookie.domain = hostname;
-  }
 
   cookie.add(newCookie, {restrictToHost: hostname});
 };
 
 /**
  * Get all the cookies for the current domain.
  *
  * This is the equivalent of calling <code>document.cookie</code> and
--- a/testing/marionette/test_cookie.js
+++ b/testing/marionette/test_cookie.js
@@ -32,17 +32,18 @@ cookie.manager = {
           candidate.path === path) {
         return this.cookies.splice(i, 1);
       }
     }
     return false;
   },
 
   getCookiesFromHost(host, originAttributes = {}) {
-    let hostCookies = this.cookies.filter(cookie => cookie.host === host);
+    let hostCookies = this.cookies.filter(cookie => cookie.host === host ||
+       cookie.host === "." + host);
     let nextIndex = 0;
 
     return {
       hasMoreElements () {
         return nextIndex < hostCookies.length;
       },
 
       getNext () {
@@ -78,17 +79,17 @@ add_test(function test_fromJSON() {
     Assert.throws(() => cookie.fromJSON(test), /Cookie domain must be string/);
   }
   let test = {
     name: "foo",
     value: "bar",
     domain: "domain"
   };
   let parsedCookie = cookie.fromJSON(test);
-  equal(parsedCookie.domain, ".domain");
+  equal(parsedCookie.domain, "domain");
 
   // path
   for (let invalidType of [42, true, [], {}, null]) {
     let test = {
       name: "foo",
       value: "bar",
       path: invalidType,
     };
@@ -184,17 +185,17 @@ add_test(function test_add() {
   cookie.add({
     name: "name",
     value: "value",
     domain: "domain",
   });
   equal(1, cookie.manager.cookies.length);
   equal("name", cookie.manager.cookies[0].name);
   equal("value", cookie.manager.cookies[0].value);
-  equal("domain", cookie.manager.cookies[0].host);
+  equal(".domain", cookie.manager.cookies[0].host);
   equal("/", cookie.manager.cookies[0].path);
   ok(cookie.manager.cookies[0].expiry > new Date(Date.now()).getTime() / 1000);
 
   cookie.add({
     name: "name2",
     value: "value2",
     domain: "domain2",
   });
@@ -205,17 +206,17 @@ add_test(function test_add() {
     cookie.add(biscuit, {restrictToHost: "other domain"});
   }, /Cookies may only be set for the current domain/);
 
   cookie.add({
     name: "name4",
     value: "value4",
     domain: "my.domain:1234",
   });
-  equal("my.domain", cookie.manager.cookies[2].host);
+  equal(".my.domain", cookie.manager.cookies[2].host);
 
   cookie.add({
     name: "name5",
     value: "value5",
     domain: "domain5",
     path: "/foo/bar",
   });
   equal("/foo/bar", cookie.manager.cookies[3].path);
@@ -242,18 +243,16 @@ add_test(function test_remove() {
   equal(undefined, cookie.manager.cookies[0]);
 
   run_next_test();
 });
 
 add_test(function test_iter() {
   cookie.manager.cookies = [];
 
-  cookie.add({name: "0", value: "", domain: "example.com"});
-  cookie.add({name: "1", value: "", domain: "foo.example.com"});
-  cookie.add({name: "2", value: "", domain: "bar.example.com"});
-
+  cookie.add({name: "0", value: "", domain: "foo.example.com"});
+  cookie.add({name: "1", value: "", domain: "bar.example.com"});
   let fooCookies = [...cookie.iter("foo.example.com")];
   equal(1, fooCookies.length);
-  equal("foo.example.com", fooCookies[0].domain);
+  equal(".foo.example.com", fooCookies[0].domain);
 
   run_next_test();
 });
--- a/testing/talos/talos/tests/devtools/addon/content/damp.html
+++ b/testing/talos/talos/tests/devtools/addon/content/damp.html
@@ -14,29 +14,31 @@ var defaultConfig = {
     inspectorOpen: true,
     debuggerOpen: true,
     styleEditorOpen: true,
     performanceOpen: true,
     netmonitorOpen: true,
     saveAndReadHeapSnapshot: true,
     consoleBulkLogging: true,
     consoleStreamLogging: true,
+    consoleObjectExpansion: true,
   }
 };
 
 var testsInfo = {
   webconsoleOpen: "Measure open/close toolbox on webconsole panel",
   inspectorOpen: "Measure open/close toolbox on inspector panel",
   debuggerOpen: "Measure open/close toolbox on debugger panel",
   styleEditorOpen: "Measure open/close toolbox on style editor panel",
   performanceOpen: "Measure open/close toolbox on performance panel",
   netmonitorOpen: "Measure open/close toolbox on network monitor panel",
   saveAndReadHeapSnapshot: "Measure open/close toolbox on memory panel and save/read heap snapshot",
   consoleBulkLogging: "Measure time for a bunch of sync console.log statements to appear",
   consoleStreamLogging: "Measure rAF on page during a stream of console.log statements",
+  consoleObjectExpansion: "Measure time to expand a large object and close the console",
 };
 
 function updateConfig() {
   config = {subtests: []};
   for (var test in defaultConfig.subtests) {
     if ($("subtest-" + test).checked) { // eslint-disable-line no-undef
       config.subtests.push(test);
     }
--- a/testing/talos/talos/tests/devtools/addon/content/damp.js
+++ b/testing/talos/talos/tests/devtools/addon/content/damp.js
@@ -254,16 +254,74 @@ Damp.prototype = {
       name: "console.streamlog",
       value: avgTime
     });
 
     yield this.closeToolbox(null);
     yield this.testTeardown();
   }),
 
+  _consoleObjectExpansionTest: Task.async(function* () {
+    let tab = yield this.testSetup(SIMPLE_URL);
+    let messageManager = tab.linkedBrowser.messageManager;
+    let {toolbox} = yield this.openToolbox("webconsole");
+    let webconsole = toolbox.getPanel("webconsole");
+
+    // Resolve once the first message is received.
+    let onMessageReceived = new Promise(resolve => {
+      function receiveMessages(e, messages) {
+        for (let m of messages) {
+          resolve(m);
+        }
+      }
+      webconsole.hud.ui.once("new-messages", receiveMessages);
+    });
+
+    // Load a frame script using a data URI so we can do logs
+    // from the page.
+    messageManager.loadFrameScript("data:,(" + encodeURIComponent(
+      `function () {
+        addMessageListener("do-dir", function () {
+          content.console.dir(Array.from({length:1000}).reduce((res, _, i)=> {
+            res["item_" + i] = i;
+            return res;
+          }, {}));
+        });
+      }`
+    ) + ")()", true);
+
+    // Kick off the logging
+    messageManager.sendAsyncMessage("do-dir");
+
+    let start = performance.now();
+    yield onMessageReceived;
+    const tree = webconsole.hud.ui.outputNode.querySelector(".dir.message .tree");
+    // The tree can be collapsed since the properties are fetched asynchronously.
+    if (tree.querySelectorAll(".node").length === 1) {
+      // If this is the case, we wait for the properties to be fetched and displayed.
+      yield new Promise(resolve => {
+        const observer = new MutationObserver(mutations => {
+          resolve(mutations);
+          observer.disconnect();
+        });
+        observer.observe(tree, {
+          childList: true
+        });
+      });
+    }
+
+    this._results.push({
+      name: "console.objectexpand",
+      value: performance.now() - start,
+    });
+
+    yield this.closeToolboxAndLog("console.objectexpanded");
+    yield this.testTeardown();
+  }),
+
   takeCensus(label) {
     let start = performance.now();
 
     this._snapshot.takeCensus({
       breakdown: {
         by: "coarseType",
         objects: {
           by: "objectClass",
@@ -624,12 +682,15 @@ Damp.prototype = {
     }));
 
     if (config.subtests.indexOf("consoleBulkLogging") > -1) {
       tests = tests.concat(this._consoleBulkLoggingTest);
     }
     if (config.subtests.indexOf("consoleStreamLogging") > -1) {
       tests = tests.concat(this._consoleStreamLoggingTest);
     }
+    if (config.subtests.indexOf("consoleObjectExpansion") > -1) {
+      tests = tests.concat(this._consoleObjectExpansionTest);
+    }
 
     this._doSequence(tests, this._doneInternal);
   }
 }
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -309380,16 +309380,21 @@
      {}
     ]
    ],
    "web-animations/resources/easing-tests.js": [
     [
      {}
     ]
    ],
+   "web-animations/resources/effect-tests.js": [
+    [
+     {}
+    ]
+   ],
    "web-animations/resources/keyframe-utils.js": [
     [
      {}
     ]
    ],
    "web-animations/resources/xhr-doc.py": [
     [
      {}
@@ -409311,16 +409316,22 @@
     ]
    ],
    "webdriver/tests/contexts/resizing_and_positioning.py": [
     [
      "/webdriver/tests/contexts/resizing_and_positioning.py",
      {}
     ]
    ],
+   "webdriver/tests/cookies/add_cookie.py": [
+    [
+     "/webdriver/tests/cookies/add_cookie.py",
+     {}
+    ]
+   ],
    "webdriver/tests/cookies/delete_cookie.py": [
     [
      "/webdriver/tests/cookies/delete_cookie.py",
      {}
     ]
    ],
    "webdriver/tests/cookies/get_named_cookie.py": [
     [
@@ -631298,16 +631309,20 @@
   "web-animations/interfaces/KeyframeEffectReadOnly/copy-constructor.html": [
    "8ef986f13e7fe7ffeb7403f647b4169ac0d6a138",
    "testharness"
   ],
   "web-animations/resources/easing-tests.js": [
    "c255d606d00296b4c6957435773a20a9d8d0bd0b",
    "support"
   ],
+  "web-animations/resources/effect-tests.js": [
+   "2c52f4dd0562683459ed6b4df24c07b5401cb88e",
+   "support"
+  ],
   "web-animations/resources/keyframe-utils.js": [
    "a9c574e206087c02834e9836ea7625d843427a17",
    "support"
   ],
   "web-animations/resources/xhr-doc.py": [
    "de68c45fc1d38a49946f9046f34031e9278a1531",
    "support"
   ],
@@ -631315,29 +631330,29 @@
    "d057ad66c4561ef32f83770e4948f2019da89d48",
    "support"
   ],
   "web-animations/timing-model/animation-effects/active-time.html": [
    "42eb1a23e89ae60ccd0a3664a9a583df1eb30d49",
    "testharness"
   ],
   "web-animations/timing-model/animation-effects/current-iteration.html": [
-   "b08a35ae832ce33da7fe7fee22e589a6b85a6353",
+   "fdfc86ef16ff5fbb2df0d174f9be3e7fc6388c03",
    "testharness"
   ],
   "web-animations/timing-model/animation-effects/local-time.html": [
    "4b24cf2374a690395398f8caed9d340667dd0a9d",
    "testharness"
   ],
   "web-animations/timing-model/animation-effects/phases-and-states.html": [
    "ce3652c8a6fdd8a6019fd665bca28ed725bacd71",
    "testharness"
   ],
   "web-animations/timing-model/animation-effects/simple-iteration-progress.html": [
-   "53a4a6c6c6d07e00fecc50e5de831862e7bf4b2e",
+   "e3326f365ca153bb193166826f767149446975a3",
    "testharness"
   ],
   "web-animations/timing-model/animations/canceling-an-animation.html": [
    "079bc0e0f7ea60b94999ed1b4f92c1aa2fc2c7bb",
    "testharness"
   ],
   "web-animations/timing-model/animations/current-time.html": [
    "b1ea8e490cbfb69fd71b91a90e7e2d9ce99f42d3",
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/effect-tests.js
@@ -0,0 +1,68 @@
+// Common utility methods for testing animation effects
+
+// Tests the |property| member of |animation's| target effect's computed timing
+// at the various points indicated by |values|.
+//
+// |values| has the format:
+//
+//   {
+//     before, // value to test during before phase
+//     activeBoundary, // value to test at the very beginning of the active
+//                     // phase when playing forwards, or the very end of
+//                     // the active phase when playing backwards.
+//                     // This should be undefined if the active duration of
+//                     // the effect is zero.
+//     after,  // value to test during the after phase or undefined if the
+//             // active duration is infinite
+//   }
+//
+function assert_computed_timing_for_each_phase(animation, property, values) {
+  const effect = animation.effect;
+  const timing = effect.getComputedTiming();
+
+  // The following calculations are based on the definitions here:
+  // https://w3c.github.io/web-animations/#animation-effect-phases-and-states
+  const beforeActive = Math.max(Math.min(timing.delay, timing.endTime), 0);
+  const activeAfter =
+    Math.max(Math.min(timing.delay + timing.activeDuration, timing.endTime), 0);
+  const direction = animation.playbackRate < 0 ? 'backwards' : 'forwards';
+
+  // Before phase
+  if (direction === 'forwards') {
+    animation.currentTime = beforeActive - 1;
+  } else {
+    animation.currentTime = beforeActive;
+  }
+  assert_equals(effect.getComputedTiming()[property], values.before,
+                `Value of ${property} in the before phase`);
+
+  // Active phase
+  if (effect.getComputedTiming().activeDuration > 0) {
+    if (direction === 'forwards') {
+      animation.currentTime = beforeActive;
+    } else {
+      animation.currentTime = activeAfter;
+    }
+    assert_equals(effect.getComputedTiming()[property], values.activeBoundary,
+                  `Value of ${property} at the boundary of the active phase`);
+  } else {
+    assert_equals(values.activeBoundary, undefined,
+                  'Test specifies a value to check during the active phase but'
+                  + ' the animation has a zero duration');
+  }
+
+  // After phase
+  if (effect.getComputedTiming().activeDuration !== Infinity) {
+    if (direction === 'forwards') {
+      animation.currentTime = activeAfter;
+    } else {
+      animation.currentTime = activeAfter + 1;
+    }
+    assert_equals(effect.getComputedTiming()[property], values.after,
+                  `Value of ${property} in the after phase`);
+  } else {
+    assert_equals(values.after, undefined,
+                  'Test specifies a value to check during the after phase but'
+                  + ' the animation has an infinite duration');
+  }
+}
--- a/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html
@@ -1,41 +1,46 @@
 <!DOCTYPE html>
 <meta charset=utf-8>
 <title>Current iteration tests</title>
 <link rel="help" href="https://w3c.github.io/web-animations/#current-iteration">
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="../../testcommon.js"></script>
+<script src="../../resources/effect-tests.js"></script>
 <body>
 <div id="log"></div>
 <script>
 'use strict';
 
 function runTests(tests, description) {
-  tests.forEach(function(currentTest) {
-    var testParams = '';
-    for (var attr in currentTest.input) {
-      testParams += ' ' + attr + ':' + currentTest.input[attr];
+  for (const currentTest of tests) {
+    let testParams = Object.entries(currentTest.input)
+                           .map(([attr, value]) => `${attr}:${value}`)
+                           .join(' ');
+    if (currentTest.playbackRate !== undefined) {
+      testParams += ` playbackRate:${currentTest.playbackRate}`;
     }
-    test(function(t) {
-      var div = createDiv(t);
-      var anim = div.animate({ opacity: [ 0, 1 ] }, currentTest.input);
-      assert_equals(anim.effect.getComputedTiming().currentIteration,
-                    currentTest.before);
-      anim.currentTime = currentTest.input.delay || 0;
-      assert_equals(anim.effect.getComputedTiming().currentIteration,
-                    currentTest.active);
-      if (typeof currentTest.after !== 'undefined') {
-        anim.finish();
-        assert_equals(anim.effect.getComputedTiming().currentIteration,
-                      currentTest.after);
+
+    test(t => {
+      const div = createDiv(t);
+      const anim = div.animate({}, currentTest.input);
+      if (currentTest.playbackRate !== undefined) {
+        anim.playbackRate = currentTest.playbackRate;
       }
-    }, description + ':' + testParams);
-  });
+
+      assert_computed_timing_for_each_phase(
+        anim,
+        'currentIteration',
+        { before: currentTest.before,
+          activeBoundary: currentTest.active,
+          after: currentTest.after },
+      );
+    }, `${description}: ${testParams}`);
+  }
 }
 
 async_test(function(t) {
   var div = createDiv(t);
   var anim = div.animate({ opacity: [ 0, 1 ] }, { delay: 1 });
   assert_equals(anim.effect.getComputedTiming().currentIteration, null);
   anim.finished.then(t.step_func(function() {
     assert_equals(anim.effect.getComputedTiming().currentIteration, null);
@@ -53,105 +58,96 @@ async_test(function(t) {
 runTests([
   {
     input:    { iterations: 0,
                 iterationStart: 0,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 0,
                 duration: 100,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 0,
                 duration: Infinity,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 2.5,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 2,
-    active: 2,
     after: 2
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 2.5,
                 duration: 100,
                 delay: 1,
                 fill: 'both' },
     before: 2,
-    active: 2,
     after: 2
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 2.5,
                 duration: Infinity,
                 delay: 1,
                 fill: 'both' },
     before: 2,
-    active: 2,
     after: 2
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 3,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 3,
-    active: 3,
     after: 3
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 3,
                 duration: 100,
                 delay: 1,
                 fill: 'both' },
     before: 3,
-    active: 3,
     after: 3
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 3,
                 duration: Infinity,
                 delay: 1,
                 fill: 'both' },
     before: 3,
-    active: 3,
     after: 3
   }
 ], 'Test zero iterations');
 
 
 // --------------------------------------------------------------------
 //
 // Tests where the iteration count is an integer
@@ -161,17 +157,16 @@ runTests([
 runTests([
   {
     input:    { iterations: 3,
                 iterationStart: 0,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 2,
     after: 2
   },
 
   {
     input:    { iterations: 3,
                 iterationStart: 0,
                 duration: 100,
                 delay: 1,
@@ -193,17 +188,16 @@ runTests([
 
   {
     input:    { iterations: 3,
                 iterationStart: 2.5,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 2,
-    active: 5,
     after: 5
   },
 
   {
     input:    { iterations: 3,
                 iterationStart: 2.5,
                 duration: 100,
                 delay: 1,
@@ -225,17 +219,16 @@ runTests([
 
   {
     input:    { iterations: 3,
                 iterationStart: 3,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 3,
-    active: 5,
     after: 5
   },
 
   {
     input:    { iterations: 3,
                 iterationStart: 3,
                 duration: 100,
                 delay: 1,
@@ -266,17 +259,16 @@ runTests([
 runTests([
   {
     input:    { iterations: 3.5,
                 iterationStart: 0,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 3,
     after: 3
   },
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 0,
                 duration: 100,
                 delay: 1,
@@ -298,17 +290,16 @@ runTests([
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 2.5,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 2,
-    active: 5,
     after: 5
   },
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 2.5,
                 duration: 100,
                 delay: 1,
@@ -330,17 +321,16 @@ runTests([
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 3,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 3,
-    active: 6,
     after: 6
   },
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 3,
                 duration: 100,
                 delay: 1,
@@ -371,17 +361,16 @@ runTests([
 runTests([
   {
     input:    { iterations: Infinity,
                 iterationStart: 0,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: Infinity,
     after: Infinity
   },
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 0,
                 duration: 100,
                 delay: 1,
@@ -402,17 +391,16 @@ runTests([
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 2.5,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 2,
-    active: Infinity,
     after: Infinity
   },
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 2.5,
                 duration: 100,
                 delay: 1,
@@ -433,17 +421,16 @@ runTests([
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 3,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 3,
-    active: Infinity,
     after: Infinity
   },
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 3,
                 duration: 100,
                 delay: 1,
@@ -525,17 +512,17 @@ runTests([
   {
     input:    { iterationStart: 0.5,
                 duration: 100,
                 delay: 1,
                 fill: 'both',
                 endDelay: -50 },
     before: 0,
     active: 0,
-    after: 0
+    after: 1
   },
 
   {
     input:    { iterationStart: 0.5,
                 duration: 100,
                 delay: 1,
                 fill: 'both',
                 endDelay: -100 },
@@ -547,17 +534,17 @@ runTests([
   {
     input:    { iterations: 2,
                 duration: 100,
                 delay: 1,
                 fill: 'both',
                 endDelay: -100 },
     before: 0,
     active: 0,
-    after: 0
+    after: 1
   },
 
   {
     input:    { iterations: 1,
                 iterationStart: 2,
                 duration: 100,
                 delay: 1,
                 fill: 'both',
@@ -575,10 +562,59 @@ runTests([
                 fill: 'both',
                 endDelay: -100 },
     before: 2,
     active: 2,
     after: 2
   },
 ], 'Test end delay');
 
+
+// --------------------------------------------------------------------
+//
+// Negative playback rate tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+  {
+    input:    { duration: 1,
+                delay: 1,
+                fill: 'both' },
+    playbackRate: -1,
+    before: 0,
+    active: 0,
+    after: 0
+  },
+
+  {
+    input:    { duration: 1,
+                delay: 1,
+                iterations: 2,
+                fill: 'both' },
+    playbackRate: -1,
+    before: 0,
+    active: 1,
+    after: 1
+  },
+
+  {
+    input:    { duration: 0,
+                delay: 1,
+                fill: 'both' },
+    playbackRate: -1,
+    before: 0,
+    after: 0
+  },
+
+  {
+    input:    { duration: 0,
+                iterations: 0,
+                delay: 1,
+                fill: 'both' },
+    playbackRate: -1,
+    before: 0,
+    after: 0
+  },
+], 'Test negative playback rate');
+
 </script>
 </body>
--- a/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html
@@ -1,42 +1,47 @@
 <!DOCTYPE html>
 <meta charset=utf-8>
 <title>Simple iteration progress tests</title>
 <link rel="help"
       href="https://w3c.github.io/web-animations/#simple-iteration-progress">
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="../../testcommon.js"></script>
+<script src="../../resources/effect-tests.js"></script>
 <body>
 <div id="log"></div>
 <script>
 'use strict';
 
 function runTests(tests, description) {
-  tests.forEach(function(currentTest) {
-    var testParams = '';
-    for (var attr in currentTest.input) {
-      testParams += ' ' + attr + ':' + currentTest.input[attr];
+  for (const currentTest of tests) {
+    let testParams = Object.entries(currentTest.input)
+                           .map(([attr, value]) => `${attr}:${value}`)
+                           .join(' ');
+    if (currentTest.playbackRate !== undefined) {
+      testParams += ` playbackRate:${currentTest.playbackRate}`;
     }
-    test(function(t) {
-      var div = createDiv(t);
-      var anim = div.animate({ opacity: [ 0, 1 ] }, currentTest.input);
-      assert_equals(anim.effect.getComputedTiming().progress,
-                    currentTest.before);
-      anim.currentTime = currentTest.input.delay || 0;
-      assert_equals(anim.effect.getComputedTiming().progress,
-                    currentTest.active);
-      if (typeof currentTest.after !== 'undefined') {
-        anim.finish();
-        assert_equals(anim.effect.getComputedTiming().progress,
-                      currentTest.after);
+
+    test(t => {
+      const div = createDiv(t);
+      const anim = div.animate({}, currentTest.input);
+      if (currentTest.playbackRate !== undefined) {
+        anim.playbackRate = currentTest.playbackRate;
       }
-    }, description + ':' + testParams);
-  });
+
+      assert_computed_timing_for_each_phase(
+        anim,
+        'progress',
+        { before: currentTest.before,
+          activeBoundary: currentTest.active,
+          after: currentTest.after },
+      );
+    }, `${description}: ${testParams}`);
+  }
 }
 
 
 // --------------------------------------------------------------------
 //
 // Zero iteration duration tests
 //
 // --------------------------------------------------------------------
@@ -44,105 +49,96 @@ function runTests(tests, description) {
 runTests([
   {
     input:    { iterations: 0,
                 iterationStart: 0,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 0,
                 duration: 100,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 0,
                 duration: Infinity,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 2.5,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0.5,
-    active: 0.5,
     after: 0.5
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 2.5,
                 duration: 100,
                 delay: 1,
                 fill: 'both' },
     before: 0.5,
-    active: 0.5,
     after: 0.5
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 2.5,
                 duration: Infinity,
                 delay: 1,
                 fill: 'both' },
     before: 0.5,
-    active: 0.5,
     after: 0.5
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 3,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 3,
                 duration: 100,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   },
 
   {
     input:    { iterations: 0,
                 iterationStart: 3,
                 duration: Infinity,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0,
     after: 0
   }
 ], 'Test zero iterations');
 
 
 // --------------------------------------------------------------------
 //
 // Tests where the iteration count is an integer
@@ -152,17 +148,16 @@ runTests([
 runTests([
   {
     input:    { iterations: 3,
                 iterationStart: 0,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 1,
     after: 1
   },
 
   {
     input:    { iterations: 3,
                 iterationStart: 0,
                 duration: 100,
                 delay: 1,
@@ -184,17 +179,16 @@ runTests([
 
   {
     input:    { iterations: 3,
                 iterationStart: 2.5,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0.5,
-    active: 0.5,
     after: 0.5
   },
 
   {
     input:    { iterations: 3,
                 iterationStart: 2.5,
                 duration: 100,
                 delay: 1,
@@ -216,17 +210,16 @@ runTests([
 
   {
     input:    { iterations: 3,
                 iterationStart: 3,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 1,
     after: 1
   },
 
   {
     input:    { iterations: 3,
                 iterationStart: 3,
                 duration: 100,
                 delay: 1,
@@ -257,17 +250,16 @@ runTests([
 runTests([
   {
     input:    { iterations: 3.5,
                 iterationStart: 0,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0.5,
     after: 0.5
   },
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 0,
                 duration: 100,
                 delay: 1,
@@ -289,17 +281,16 @@ runTests([
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 2.5,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0.5,
-    active: 1,
     after: 1
   },
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 2.5,
                 duration: 100,
                 delay: 1,
@@ -321,17 +312,16 @@ runTests([
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 3,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 0.5,
     after: 0.5
   },
 
   {
     input:    { iterations: 3.5,
                 iterationStart: 3,
                 duration: 100,
                 delay: 1,
@@ -362,17 +352,16 @@ runTests([
 runTests([
   {
     input:    { iterations: Infinity,
                 iterationStart: 0,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 1,
     after: 1
   },
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 0,
                 duration: 100,
                 delay: 1,
@@ -393,17 +382,16 @@ runTests([
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 2.5,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0.5,
-    active: 0.5,
     after: 0.5
   },
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 2.5,
                 duration: 100,
                 delay: 1,
@@ -424,17 +412,16 @@ runTests([
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 3,
                 duration: 0,
                 delay: 1,
                 fill: 'both' },
     before: 0,
-    active: 1,
     after: 1
   },
 
   {
     input:    { iterations: Infinity,
                 iterationStart: 3,
                 duration: 100,
                 delay: 1,
@@ -516,17 +503,17 @@ runTests([
   {
     input:    { iterationStart: 0.5,
                 duration: 100,
                 delay: 1,
                 fill: 'both',
                 endDelay: -50 },
     before: 0.5,
     active: 0.5,
-    after: 1
+    after: 0
   },
 
   {
     input:    { iterationStart: 0.5,
                 duration: 100,
                 delay: 1,
                 fill: 'both',
                 endDelay: -100 },
@@ -538,17 +525,17 @@ runTests([
   {
     input:    { iterations: 2,
                 duration: 100,
                 delay: 1,
                 fill: 'both',
                 endDelay: -100 },
     before: 0,
     active: 0,
-    after: 1
+    after: 0
   },
 
   {
     input:    { iterations: 1,
                 iterationStart: 2,
                 duration: 100,
                 delay: 1,
                 fill: 'both',
@@ -566,10 +553,48 @@ runTests([
                 fill: 'both',
                 endDelay: -100 },
     before: 0,
     active: 0,
     after: 0
   },
 ], 'Test end delay');
 
+
+// --------------------------------------------------------------------
+//
+// Negative playback rate tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+  {
+    input:    { duration: 1,
+                delay: 1,
+                fill: 'both' },
+    playbackRate: -1,
+    before: 0,
+    active: 1,
+    after: 1
+  },
+
+  {
+    input:    { duration: 0,
+                delay: 1,
+                fill: 'both' },
+    playbackRate: -1,
+    before: 0,
+    after: 1
+  },
+
+  {
+    input:    { duration: 0,
+                iterations: 0,
+                delay: 1,
+                fill: 'both' },
+    playbackRate: -1,
+    before: 0,
+    after: 0
+  },
+], 'Test negative playback rate');
+
 </script>
 </body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/cookies/add_cookie.py
@@ -0,0 +1,76 @@
+from tests.support.fixtures import clear_all_cookies
+from tests.support.fixtures import server_config
+
+def test_add_domain_cookie(session, url):
+    session.url = url("/common/blank.html")
+    clear_all_cookies(session)
+    create_cookie_request = {
+        "cookie": {
+            "name": "hello",
+            "value": "world",
+            "domain": "web-platform.test",
+            "path": "/",
+            "httpOnly": False,
+            "secure": False
+        }
+    }
+    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
+    assert result.status == 200
+    assert "value" in result.body
+    assert isinstance(result.body["value"], dict)
+
+    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
+    assert result.status == 200
+    assert "value" in result.body
+    assert isinstance(result.body["value"], list)
+    assert len(result.body["value"]) == 1
+    assert isinstance(result.body["value"][0], dict)
+
+    cookie = result.body["value"][0]
+    assert "name" in cookie
+    assert isinstance(cookie["name"], basestring)
+    assert "value" in cookie
+    assert isinstance(cookie["value"], basestring)
+    assert "domain" in cookie
+    assert isinstance(cookie["domain"], basestring)
+
+    assert cookie["name"] == "hello"
+    assert cookie["value"] == "world"
+    assert cookie["domain"] == ".web-platform.test"
+
+def test_add_cookie_for_ip(session, url, server_config):
+    session.url = "http://127.0.0.1:%s/404" % (server_config["ports"]["http"][0])
+    clear_all_cookies(session)
+    create_cookie_request = {
+        "cookie": {
+            "name": "hello",
+            "value": "world",
+            "domain": "127.0.0.1",
+            "path": "/",
+            "httpOnly": False,
+            "secure": False
+        }
+    }
+    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
+    assert result.status == 200
+    assert "value" in result.body
+    assert isinstance(result.body["value"], dict)
+
+    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
+    assert result.status == 200
+    assert "value" in result.body
+    assert isinstance(result.body["value"], list)
+    assert len(result.body["value"]) == 1
+    assert isinstance(result.body["value"][0], dict)
+
+    cookie = result.body["value"][0]
+    assert "name" in cookie
+    assert isinstance(cookie["name"], basestring)
+    assert "value" in cookie
+    assert isinstance(cookie["value"], basestring)
+    assert "domain" in cookie
+    assert isinstance(cookie["domain"], basestring)
+
+    assert cookie["name"] == "hello"
+    assert cookie["value"] == "world"
+    assert cookie["domain"] == "127.0.0.1"
--- a/testing/web-platform/tests/webdriver/tests/cookies/get_named_cookie.py
+++ b/testing/web-platform/tests/webdriver/tests/cookies/get_named_cookie.py
@@ -60,45 +60,8 @@ def test_duplicated_cookie(session, url)
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
     assert isinstance(cookie["value"], basestring)
 
     assert cookie["name"] == "hello"
     assert cookie["value"] == "newworld"
 
-def test_add_domain_cookie(session, url):
-    session.url = url("/common/blank.html")
-    clear_all_cookies(session)
-    create_cookie_request = {
-        "cookie": {
-            "name": "hello",
-            "value": "world",
-            "domain": "web-platform.test",
-            "path": "/",
-            "httpOnly": False,
-            "secure": False
-        }
-    }
-    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], dict)
-
-    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], list)
-    assert len(result.body["value"]) == 1
-    assert isinstance(result.body["value"][0], dict)
-
-    cookie = result.body["value"][0]
-    assert "name" in cookie
-    assert isinstance(cookie["name"], basestring)
-    assert "value" in cookie
-    assert isinstance(cookie["value"], basestring)
-    assert "domain" in cookie
-    assert isinstance(cookie["domain"], basestring)
-
-    assert cookie["name"] == "hello"
-    assert cookie["value"] == "world"
-    assert cookie["domain"] == ".web-platform.test"
-
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8397,16 +8397,38 @@
      "alert_emails": ["carnold@mozilla.org"],
      "bug_numbers": [1287587],
      "expires_in_version": "60",
      "kind": "count",
      "keyed": true,
      "releaseChannelCollection": "opt-out",
      "description": "A counter incremented every time the user prints a document."
    },
+  "DEVTOOLS_COLD_TOOLBOX_OPEN_DELAY_MS": {
+    "record_in_processes": ["main"],
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"],
+    "bug_numbers": [1405584],
+    "expires_in_version": "62",
+    "kind": "exponential",
+    "high": 60000,
+    "n_buckets": 100,
+    "keyed": true,
+    "description": "Time taken (in ms) to open the first DevTools toolbox. This is keyed by tool ID being opened [inspector, webconsole, jsdebugger, styleeditor, shadereditor, canvasdebugger, performance, memory, netmonitor, storage, webaudioeditor, scratchpad, dom]."
+  },
+  "DEVTOOLS_WARM_TOOLBOX_OPEN_DELAY_MS": {
+    "record_in_processes": ["main"],
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"],
+    "bug_numbers": [1405584],
+    "expires_in_version": "62",
+    "kind": "exponential",
+    "high": 60000,
+    "n_buckets": 100,
+    "keyed": true,
+    "description": "Time taken (in ms) to open all but first DevTools toolbox. This is keyed by tool ID being opened [inspector, webconsole, jsdebugger, styleeditor, shadereditor, canvasdebugger, performance, memory, netmonitor, storage, webaudioeditor, scratchpad, dom]."
+  },
   "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 1000,
     "description": "The time (in milliseconds) that it took to display a selected source to the user."
   },
--- a/toolkit/locales/l10n.mk
+++ b/toolkit/locales/l10n.mk
@@ -161,26 +161,24 @@ endif
 	$(NSINSTALL) -D $(DIST)/$(PKG_PATH)
 	mv -f '$(DIST)/l10n-stage/$(PACKAGE)' '$(ZIP_OUT)'
 	if test -f '$(DIST)/l10n-stage/$(PACKAGE).asc'; then mv -f '$(DIST)/l10n-stage/$(PACKAGE).asc' '$(ZIP_OUT).asc'; fi
 
 repackage-zip-%: unpack
 	@$(MAKE) repackage-zip AB_CD=$* ZIP_IN='$(ZIP_IN)'
 
 
-ifdef IS_LANGUAGE_REPACK
-MERGE_TK_FILE = $(firstword \
-  $(wildcard $(REAL_LOCALE_MERGEDIR)/$(subst /locales,,toolkit/locales)/$(1)) \
-  $(wildcard $(LOCALE_SRCDIR)/$(1)) \
-  $(call EXPAND_LOCALE_SRCDIR,toolkit/locales)/$(1) )
-else
-MERGE_TK_FILE = $(call EXPAND_LOCALE_SRCDIR,toolkit/locales)/$(1)
-endif
-
-LANGPACK_DEFINES = $(call MERGE_TK_FILE,defines.inc) $(call MERGE_DIR,defines.inc)
+LANGPACK_DEFINES = \
+  $(firstword \
+    $(wildcard $(call EXPAND_LOCALE_SRCDIR,toolkit/locales)/defines.inc) \
+    $(MOZILLA_DIR)/toolkit/locales/en-US/defines.inc) \
+  $(firstword \
+    $(wildcard $(LOCALE_SRCDIR)/defines.inc) \
+    $(srcdir)/en-US/defines.inc) \
+$(NULL)
 
 # Dealing with app sub dirs: If DIST_SUBDIRS is defined it contains a
 # listing of app sub-dirs we should include in langpack xpis. If not,
 # check DIST_SUBDIR, and if that isn't present, just package the default
 # chrome directory.
 PKG_ZIP_DIRS = chrome $(or $(DIST_SUBDIRS),$(DIST_SUBDIR))
 
 # Clone a l10n repository, either via hg or git
--- a/widget/cocoa/nsDeviceContextSpecX.mm
+++ b/widget/cocoa/nsDeviceContextSpecX.mm
@@ -118,36 +118,43 @@ NS_IMETHODIMP nsDeviceContextSpecX::Init
   }
 #endif
 
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
-NS_IMETHODIMP nsDeviceContextSpecX::BeginDocument(const nsAString& aTitle, 
+NS_IMETHODIMP nsDeviceContextSpecX::BeginDocument(const nsAString& aTitle,
                                                   const nsAString& aPrintToFileName,
-                                                  int32_t          aStartPage, 
+                                                  int32_t          aStartPage,
                                                   int32_t          aEndPage)
 {
-    NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  // Print Core of Application Service sent print job with names exceeding
+  // 255 bytes. This is a workaround until fix it.
+  // (https://openradar.appspot.com/34428043)
+  nsAutoString adjustedTitle;
+  PrintTarget::AdjustPrintJobNameForIPP(aTitle, adjustedTitle);
 
-    if (!aTitle.IsEmpty()) {
-      CFStringRef cfString =
-        ::CFStringCreateWithCharacters(NULL, reinterpret_cast<const UniChar*>(aTitle.BeginReading()),
-                                             aTitle.Length());
-      if (cfString) {
-        ::PMPrintSettingsSetJobName(mPrintSettings, cfString);
-        ::CFRelease(cfString);
-      }
+  if (!adjustedTitle.IsEmpty()) {
+    CFStringRef cfString =
+      ::CFStringCreateWithCharacters(NULL,
+                                     reinterpret_cast<const UniChar*>(adjustedTitle.BeginReading()),
+                                     adjustedTitle.Length());
+    if (cfString) {
+      ::PMPrintSettingsSetJobName(mPrintSettings, cfString);
+      ::CFRelease(cfString);
     }
+  }
 
-    return NS_OK;
+  return NS_OK;
 
-    NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;