Merge inbound to mozilla-central. a=merge
authorGurzau Raul <rgurzau@mozilla.com>
Thu, 19 Jul 2018 12:54:18 +0300
changeset 427261 183ee39bf309cd8463d8db5b5c8eb232cd0dac53
parent 427230 25c83a66f294a9790f2a91691837e94d098bd6b0 (current diff)
parent 427260 afc31a0d40fc4d0d215570891781211066fa8c5e (diff)
child 427276 a388b8b396009d9204eb74d5daf1246e5b18020f
child 427288 a1e60967e3c4b105dcff38d048626749430f9776
push id34297
push userrgurzau@mozilla.com
push dateThu, 19 Jul 2018 09:54:46 +0000
treeherdermozilla-central@183ee39bf309 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.0a1
first release with
nightly linux32
183ee39bf309 / 63.0a1 / 20180719100142 / files
nightly linux64
183ee39bf309 / 63.0a1 / 20180719100142 / files
nightly mac
183ee39bf309 / 63.0a1 / 20180719100142 / files
nightly win32
183ee39bf309 / 63.0a1 / 20180719100142 / files
nightly win64
183ee39bf309 / 63.0a1 / 20180719100142 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
browser/base/content/test/general/browser_registerProtocolHandler_notification.html
browser/base/content/test/general/browser_registerProtocolHandler_notification.js
testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-001.html.ini
testing/web-platform/meta/css/cssom/cssstyledeclaration-setter-order.html.ini
testing/web-platform/tests/css/cssom/cssstyledeclaration-setter-order.html
xpcom/base/moz.build
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -10,17 +10,16 @@ prefs = browser.cache.offline.insecure.e
 support-files =
   POSTSearchEngine.xml
   alltabslistener.html
   app_bug575561.html
   app_subframe_bug575561.html
   audio.ogg
   browser_bug479408_sample.html
   browser_bug970746.xhtml
-  browser_registerProtocolHandler_notification.html
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   browser_tab_dragdrop_embed.html
   browser_web_channel.html
   browser_web_channel_iframe.html
   bug592338.html
   bug792517-2.html
   bug792517.html
@@ -489,18 +488,16 @@ skip-if = true # Bug 1409184 disabled be
 [browser_visibleTabs_tabPreview.js]
 skip-if = (os == "win" && !debug)
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_web_channel.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_zbug569342.js]
 skip-if = e10s || debug # Bug 1094240 - has findbar-related failures
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_registerProtocolHandler_notification.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_addCertException.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_e10s_about_page_triggeringprincipal.js]
 skip-if = verify
 support-files =
   file_about_child.html
   file_about_parent.html
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -60,17 +60,16 @@ static const RedirEntry kRedirMap[] = {
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
   { "feeds", "chrome://browser/content/feeds/subscribe.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
   { "policies", "chrome://browser/content/policies/aboutPolicies.xhtml",
-    nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT },
   { "privatebrowsing", "chrome://browser/content/aboutPrivateBrowsing.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
     nsIAboutModule::ALLOW_SCRIPT },
   { "rights",
     "chrome://global/content/aboutRights.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -56,16 +56,22 @@ var EXPORTED_SYMBOLS = ["Policies"];
  *   It will be different for each policy. It could be a boolean,
  *   a string, an array or a complex object. All parameters have
  *   been validated according to the schema, and no unknown
  *   properties will be present on them.
  *
  * The callbacks will be bound to their parent policy object.
  */
 var Policies = {
+  "AppUpdateURL": {
+    onBeforeAddons(manager, param) {
+      setDefaultPref("app.update.url", param.href);
+    }
+  },
+
   "Authentication": {
     onBeforeAddons(manager, param) {
       if ("SPNEGO" in param) {
         setAndLockPref("network.negotiate-auth.trusted-uris", param.SPNEGO.join(", "));
       }
       if ("Delegated" in param) {
         setAndLockPref("network.negotiate-auth.delegation-uris", param.Delegated.join(", "));
       }
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -1,12 +1,18 @@
 {
   "$schema": "http://json-schema.org/draft-04/schema#",
   "type": "object",
   "properties": {
+    "AppUpdateURL": {
+      "description": "Sets custom app update server URL.",
+      "machine_only": true,
+      "type": "URL"
+    },
+
     "Authentication": {
       "description": "Sites that support integrated authentication. See https://developer.mozilla.org/en-US/docs/Mozilla/Integrated_authentication",
 
       "type": "object",
       "properties": {
         "SPNEGO" : {
           "type": "array",
           "items": {
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -15,16 +15,17 @@ support-files =
 [browser_policies_getActivePolicies.js]
 [browser_policies_notice_in_aboutpreferences.js]
 [browser_policies_popups_cookies_addons_flash.js]
 [browser_policies_runOnce_helper.js]
 [browser_policies_setAndLockPref_API.js]
 [browser_policies_simple_pref_policies.js]
 [browser_policies_sorted_alphabetically.js]
 [browser_policy_app_update.js]
+[browser_policy_app_update_URL.js]
 [browser_policy_block_about_addons.js]
 [browser_policy_block_about_config.js]
 [browser_policy_block_about_profiles.js]
 [browser_policy_block_about_support.js]
 [browser_policy_block_set_desktop_background.js]
 [browser_policy_bookmarks.js]
 [browser_policy_clear_blocked_cookies.js]
 [browser_policy_cookie_settings.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_app_update_URL.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_app_update_URL() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "AppUpdateURL": "https://www.example.com/"
+    }
+  });
+
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.ACTIVE, "Engine is active");
+
+  let expected = Services.prefs.getStringPref("app.update.url", undefined);
+
+  is("https://www.example.com/", expected, "Correct app update URL");
+});
--- a/browser/components/feeds/WebContentConverter.js
+++ b/browser/components/feeds/WebContentConverter.js
@@ -146,18 +146,55 @@ const Utils = {
 
     // If the uri doesn't contain '%s', it won't be a good handler
     if (!uri.spec.includes("%s"))
       throw NS_ERROR_DOM_SYNTAX_ERR;
 
     return uri;
   },
 
+  // This list should be kept up-to-date with the spec:
+  // https://html.spec.whatwg.org/multipage/system-state.html#custom-handlers
+  _safeProtocols: new Set([
+    "bitcoin",
+    "geo",
+    "im",
+    "irc",
+    "ircs",
+    "magnet",
+    "mailto",
+    "mms",
+    "news",
+    "nntp",
+    "openpgp4fpr",
+    "sip",
+    "sms",
+    "smsto",
+    "ssh",
+    "tel",
+    "urn",
+    "webcal",
+    "wtai",
+    "xmpp",
+  ]),
+
   // NB: Throws if aProtocol is not allowed.
   checkProtocolHandlerAllowed(aProtocol, aURIString, aWindowOrNull) {
+    if (aProtocol.startsWith("web+")) {
+      if (!/[a-z]+/.test(aProtocol.substring(4 /* web+ */))) {
+        throw this.getSecurityError(
+          `Permission denied to add a protocol handler for ${aProtocol}`,
+          aWindowOrNull);
+      }
+    } else if (!this._safeProtocols.has(aProtocol)) {
+      throw this.getSecurityError(
+        `Permission denied to add a protocol handler for ${aProtocol}`,
+        aWindowOrNull);
+    }
+
     // First, check to make sure this isn't already handled internally (we don't
     // want to let them take over, say "chrome").
     let handler = Services.io.getProtocolHandler(aProtocol);
     if (!(handler instanceof Ci.nsIExternalProtocolHandler)) {
       // This is handled internally, so we don't want them to register
       throw this.getSecurityError(
         `Permission denied to add ${aURIString} as a protocol handler`,
         aWindowOrNull);
@@ -359,16 +396,17 @@ WebContentConverterRegistrar.prototype =
     }
     return false;
   },
 
   /**
    * See nsIWebContentHandlerRegistrar
    */
   registerProtocolHandler(aProtocol, aURIString, aTitle, aBrowserOrWindow) {
+    aProtocol = (aProtocol || "").toLowerCase();
     LOG("registerProtocolHandler(" + aProtocol + "," + aURIString + "," + aTitle + ")");
     let haveWindow = (aBrowserOrWindow instanceof Ci.nsIDOMWindow);
     let uri;
     if (haveWindow) {
       uri = Utils.checkAndGetURI(aURIString, aBrowserOrWindow);
     } else {
       // aURIString must not be a relative URI.
       uri = Utils.makeURI(aURIString, null);
@@ -993,16 +1031,17 @@ WebContentConverterRegistrarContent.prot
 
     messageManager.sendAsyncMessage("WCCR:registerContentHandler",
                                     { contentType: aContentType,
                                       uri: uri.spec,
                                       title: aTitle });
   },
 
   registerProtocolHandler(aProtocol, aURIString, aTitle, aBrowserOrWindow) {
+    aProtocol = (aProtocol || "").toLowerCase();
     // aBrowserOrWindow must be a window.
     let messageManager = aBrowserOrWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                                          .getInterface(Ci.nsIWebNavigation)
                                          .QueryInterface(Ci.nsIDocShell)
                                          .QueryInterface(Ci.nsIInterfaceRequestor)
                                          .getInterface(Ci.nsITabChild)
                                          .messageManager;
 
--- a/browser/components/feeds/test/browser/browser.ini
+++ b/browser/components/feeds/test/browser/browser.ini
@@ -1,3 +1,6 @@
+[browser_registerProtocolHandler_notification.js]
+support-files =
+  browser_registerProtocolHandler_notification.html
 [browser_telemetry_checks.js]
 support-files =
   valid-feed.xml
rename from browser/base/content/test/general/browser_registerProtocolHandler_notification.html
rename to browser/components/feeds/test/browser/browser_registerProtocolHandler_notification.html
--- a/browser/base/content/test/general/browser_registerProtocolHandler_notification.html
+++ b/browser/components/feeds/test/browser/browser_registerProtocolHandler_notification.html
@@ -2,14 +2,14 @@
 <html>
   <head>
     <title>Protocol registrar page</title>
     <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
     <meta content="utf-8" http-equiv="encoding">
   </head>
   <body>
     <script type="text/javascript">
-      navigator.registerProtocolHandler("testprotocol",
+      navigator.registerProtocolHandler("web+testprotocol",
           "https://example.com/foobar?uri=%s",
           "Test Protocol");
     </script>
   </body>
 </html>
rename from browser/base/content/test/general/browser_registerProtocolHandler_notification.js
rename to browser/components/feeds/test/browser/browser_registerProtocolHandler_notification.js
--- a/browser/base/content/test/general/browser_registerProtocolHandler_notification.js
+++ b/browser/components/feeds/test/browser/browser_registerProtocolHandler_notification.js
@@ -1,43 +1,37 @@
 /* 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/. */
 
-function test() {
-  waitForExplicitFinish();
-  let notificationValue = "Protocol Registration: testprotocol";
-  let testURI = "https://example.com/browser/" +
-    "browser/base/content/test/general/browser_registerProtocolHandler_notification.html";
+const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com");
+add_task(async function() {
+  let notificationValue = "Protocol Registration: web+testprotocol";
+  let testURI = TEST_PATH + "browser_registerProtocolHandler_notification.html";
 
-    waitForCondition(function() {
-        // Do not start until the notification is up
-        let notificationBox = window.gBrowser.getNotificationBox();
-        let notification = notificationBox.getNotificationWithValue(notificationValue);
-        return notification;
-    },
-    function() {
+  window.gBrowser.selectedBrowser.loadURI(testURI);
+  await TestUtils.waitForCondition(function() {
+    // Do not start until the notification is up
+    let notificationBox = window.gBrowser.getNotificationBox();
+    let notification = notificationBox.getNotificationWithValue(notificationValue);
+    return notification;
+  }, "Still can not get notification after retrying 100 times.", 100, 100);
 
-        let notificationBox = window.gBrowser.getNotificationBox();
-        let notification = notificationBox.getNotificationWithValue(notificationValue);
-        ok(notification, "Notification box should be displayed");
-        if (notification == null) {
-            finish();
-            return;
-        }
-        is(notification.type, "info", "We expect this notification to have the type of 'info'.");
-        isnot(notification.image, null, "We expect this notification to have an icon.");
-
-        let buttons = notification.getElementsByClassName("notification-button-default");
-        is(buttons.length, 1, "We expect see one default button.");
+  let notificationBox = window.gBrowser.getNotificationBox();
+  let notification = notificationBox.getNotificationWithValue(notificationValue);
+  ok(notification, "Notification box should be displayed");
+  if (notification == null) {
+    finish();
+    return;
+  }
+  is(notification.type, "info", "We expect this notification to have the type of 'info'.");
+  isnot(notification.image, null, "We expect this notification to have an icon.");
 
-        buttons = notification.getElementsByClassName("notification-button");
-        is(buttons.length, 1, "We expect see one button.");
+  let buttons = notification.getElementsByClassName("notification-button-default");
+  is(buttons.length, 1, "We expect see one default button.");
 
-        let button = buttons[0];
-        isnot(button.label, null, "We expect the add button to have a label.");
-        todo_isnot(button.accesskey, null, "We expect the add button to have a accesskey.");
+  buttons = notification.getElementsByClassName("notification-button");
+  is(buttons.length, 1, "We expect see one button.");
 
-        finish();
-    }, "Still can not get notification after retry 100 times.", 100);
-
-    window.gBrowser.selectedBrowser.loadURI(testURI);
-}
+  let button = buttons[0];
+  isnot(button.label, null, "We expect the add button to have a label.");
+  todo_isnot(button.accesskey, null, "We expect the add button to have a accesskey.");
+});
--- a/browser/components/feeds/test/test_registerHandler.html
+++ b/browser/components/feeds/test/test_registerHandler.html
@@ -39,51 +39,57 @@ https://bugzilla.mozilla.org/show_bug.cg
       set: [
         ["dom.registerContentHandler.enabled", true],
         ["dom.registerProtocolHandler.insecure.enabled", true],
       ]
     });
     ok(navigator.registerContentHandler, "navigator.registerContentHandler should be defined");
 
     // testing a generic case
-    is(testRegisterHandler(true, "foo", "http://mochi.test:8888/%s", "Foo handler"), true, "registering a foo protocol handler should work");
+    is(testRegisterHandler(true, "web+foo", "http://mochi.test:8888/%s", "Foo handler"), true, "registering a web+foo protocol handler should work");
     is(testRegisterHandler(false, "application/rss+xml", "http://mochi.test:8888/%s", "Foo handler"), true, "registering a foo content handler should work");
 
     // testing with wrong uris
-    is(testRegisterHandler(true, "foo", "http://mochi.test:8888/", "Foo handler"), false, "a protocol handler uri should contain %s");
+    is(testRegisterHandler(true, "web+foo", "http://mochi.test:8888/", "Foo handler"), false, "a protocol handler uri should contain %s");
     is(testRegisterHandler(false, "application/rss+xml", "http://mochi.test:8888/", "Foo handler"), false, "a content handler uri should contain %s");
 
     // the spec explicitly allows relative urls to be passed
-    is(testRegisterHandler(true, "foo", "foo/%s", "Foo handler"), true, "a protocol handler uri should be valid");
+    is(testRegisterHandler(true, "web+foo", "foo/%s", "Foo handler"), true, "a protocol handler uri should be valid");
     is(testRegisterHandler(false, "application/rss+xml", "foo/%s", "Foo handler"), true, "a content handler uri should be valid");
 
     // we should only accept to register when the handler has the same host as the current page (bug 402287)
-    is(testRegisterHandler(true, "foo", "http://remotehost:8888/%s", "Foo handler"), false, "registering a foo protocol handler with a different host should not work");
+    is(testRegisterHandler(true, "fweb+oo", "http://remotehost:8888/%s", "Foo handler"), false, "registering a web+foo protocol handler with a different host should not work");
     is(testRegisterHandler(false, "application/rss+xml", "http://remotehost:8888/%s", "Foo handler"), false, "registering a foo content handler with a different host should not work");
 
     // restriction to http(s) for the uri of the handler (bug 401343)
     // https should work (http already tested in the generic case)
-    is(testRegisterHandler(true, "foo", "https://mochi.test:8888/%s", "Foo handler"), true, "registering a foo protocol handler with https scheme should work");
+    is(testRegisterHandler(true, "web+foo", "https://mochi.test:8888/%s", "Foo handler"), true, "registering a web+foo protocol handler with https scheme should work");
     is(testRegisterHandler(false, "application/rss+xml", "https://mochi.test:8888/%s", "Foo handler"), true, "registering a foo content handler with https scheme should work");
     // ftp should not work
-    is(testRegisterHandler(true, "foo", "ftp://mochi.test:8888/%s", "Foo handler"), false, "registering a foo protocol handler with ftp scheme should not work");
+    is(testRegisterHandler(true, "web+foo", "ftp://mochi.test:8888/%s", "Foo handler"), false, "registering a web+foo protocol handler with ftp scheme should not work");
     is(testRegisterHandler(false, "application/rss+xml", "ftp://mochi.test:8888/%s", "Foo handler"), false, "registering a foo content handler with ftp scheme should not work");
     // chrome should not work
-    is(testRegisterHandler(true, "foo", "chrome://mochi.test:8888/%s", "Foo handler"), false, "registering a foo protocol handler with chrome scheme should not work");
+    is(testRegisterHandler(true, "web+foo", "chrome://mochi.test:8888/%s", "Foo handler"), false, "registering a web+foo protocol handler with chrome scheme should not work");
     is(testRegisterHandler(false, "application/rss+xml", "chrome://mochi.test:8888/%s", "Foo handler"), false, "registering a foo content handler with chrome scheme should not work");
     // foo should not work
-    is(testRegisterHandler(true, "foo", "foo://mochi.test:8888/%s", "Foo handler"), false, "registering a foo protocol handler with foo scheme should not work");
+    is(testRegisterHandler(true, "web+foo", "foo://mochi.test:8888/%s", "Foo handler"), false, "registering a web+foo protocol handler with foo scheme should not work");
     is(testRegisterHandler(false, "application/rss+xml", "foo://mochi.test:8888/%s", "Foo handler"), false, "registering a foo content handler with foo scheme should not work");
 
     // for security reasons, protocol handlers should never be registered for some schemes (chrome, vbscript, ...) (bug 402788)
     is(testRegisterHandler(true, "chrome", "http://mochi.test:8888/%s", "chrome handler"), false, "registering a chrome protocol handler should not work");
     is(testRegisterHandler(true, "vbscript", "http://mochi.test:8888/%s", "vbscript handler"), false, "registering a vbscript protocol handler should not work");
     is(testRegisterHandler(true, "javascript", "http://mochi.test:8888/%s", "javascript handler"), false, "registering a javascript protocol handler should not work");
     is(testRegisterHandler(true, "moz-icon", "http://mochi.test:8888/%s", "moz-icon handler"), false, "registering a moz-icon protocol handler should not work");
 
+    // registering anything not on the list of safe schemes and unprefixed by web+ shouldn't work
+    is(testRegisterHandler(true, "foo", "http://mochi.test:8888/%s", "chrome handler"), false, "registering a foo protocol handler should not work");
+    is(testRegisterHandler(true, "web+", "http://mochi.test:8888/%s", "chrome handler"), false, "registering a 'web+' protocol handler should not work");
+    is(testRegisterHandler(true, "web+1", "http://mochi.test:8888/%s", "chrome handler"), false, "registering a 'web+1' protocol handler should not work");
+
+
     // for security reasons, content handlers should never be registered for some types (html, ...)
     is(testRegisterHandler(false, "application/rss+xml", "http://mochi.test:8888/%s", "Foo handler"), true, "registering rss content handlers should work");
     is(testRegisterHandler(false, "application/atom+xml", "http://mochi.test:8888/%s", "Foo handler"), true, "registering atom content handlers should work");
     todo_is(testRegisterHandler(false, "text/html", "http://mochi.test:8888/%s", "Foo handler"), false, "registering html content handlers should not work"); // bug 403798
     SimpleTest.finish();
   }
 
   tests();
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 
 // This test makes sure that the web pages can't register protocol handlers
 // inside the private browsing mode.
 
 add_task(async function test() {
-  let notificationValue = "Protocol Registration: testprotocol";
+  let notificationValue = "Protocol Registration: web+testprotocol";
   let testURI = "https://example.com/browser/" +
     "browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html";
 
   let doTest = async function(aIsPrivateMode, aWindow) {
     let tab = aWindow.gBrowser.selectedTab = aWindow.gBrowser.addTab(testURI);
     await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
     let promiseFinished = PromiseUtils.defer();
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
 <html>
   <head>
     <title>Protocol registrar page</title>
   </head>
   <body>
     <script type="text/javascript">
-      navigator.registerProtocolHandler("testprotocol",
+      navigator.registerProtocolHandler("web+testprotocol",
           "https://example.com/foobar?uri=%s",
           "Test Protocol");
     </script>
   </body>
 </html>
--- a/dom/base/nsNodeInfoManager.h
+++ b/dom/base/nsNodeInfoManager.h
@@ -18,18 +18,16 @@
 #include "nsDataHashtable.h"
 #include "nsStringFwd.h"
 
 class nsBindingManager;
 class nsAtom;
 class nsIDocument;
 class nsIPrincipal;
 class nsWindowSizes;
-struct PLHashEntry;
-struct PLHashTable;
 template<class T> struct already_AddRefed;
 
 #define RECENTLY_USED_NODEINFOS_SIZE 31
 
 class nsNodeInfoManager final
 {
 private:
   ~nsNodeInfoManager();
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -16765,30 +16765,35 @@ class CGIterableMethodGenerator(CGGeneri
                 """
                 if (!JS::IsCallable(arg0)) {
                   ThrowErrorMessage(cx, MSG_NOT_CALLABLE, "Argument 1 of ${ifaceName}.forEach");
                   return false;
                 }
                 JS::AutoValueArray<3> callArgs(cx);
                 callArgs[2].setObject(*obj);
                 JS::Rooted<JS::Value> ignoredReturnVal(cx);
+                auto GetKeyAtIndex = &${selfType}::GetKeyAtIndex;
+                auto GetValueAtIndex = &${selfType}::GetValueAtIndex;
                 for (size_t i = 0; i < self->GetIterableLength(); ++i) {
-                  if (!ToJSValue(cx, self->GetValueAtIndex(i), callArgs[0])) {
+                  if (!CallIterableGetter(cx, GetValueAtIndex, self, i,
+                                          callArgs[0])) {
                     return false;
                   }
-                  if (!ToJSValue(cx, self->GetKeyAtIndex(i), callArgs[1])) {
+                  if (!CallIterableGetter(cx, GetKeyAtIndex, self, i,
+                                          callArgs[1])) {
                     return false;
                   }
                   if (!JS::Call(cx, arg1, arg0, JS::HandleValueArray(callArgs),
                                 &ignoredReturnVal)) {
                     return false;
                   }
                 }
                 """,
-                ifaceName=descriptor.interface.identifier.name))
+                ifaceName=descriptor.interface.identifier.name,
+                selfType=descriptor.nativeType))
             return
         CGGeneric.__init__(self, fill(
             """
             typedef ${iterClass} itrType;
             RefPtr<itrType> result(new itrType(self,
                                                  itrType::IterableIteratorType::${itrMethod},
                                                  &${ifaceName}Iterator_Binding::Wrap));
             """,
--- a/dom/bindings/IterableIterator.h
+++ b/dom/bindings/IterableIterator.h
@@ -52,16 +52,63 @@ public:
   IterableIteratorBase() {}
 
 protected:
   virtual ~IterableIteratorBase() {}
   virtual void UnlinkHelper() = 0;
   virtual void TraverseHelper(nsCycleCollectionTraversalCallback& cb) = 0;
 };
 
+// Helpers to call iterator getter methods with the correct arguments, depending
+// on the types they return, and convert the result to JS::Values.
+
+// Helper for Get[Key,Value]AtIndex(uint32_t) methods, which accept an index and
+// return a type supported by ToJSValue.
+template <typename T, typename U>
+bool
+CallIterableGetter(JSContext* aCx, U (T::* aMethod)(uint32_t),
+                   T* aInst, uint32_t aIndex,
+                   JS::MutableHandle<JS::Value> aResult)
+{
+  return ToJSValue(aCx, (aInst->*aMethod)(aIndex), aResult);
+}
+
+template <typename T, typename U>
+bool
+CallIterableGetter(JSContext* aCx, U (T::* aMethod)(uint32_t) const,
+                   const T* aInst, uint32_t aIndex,
+                   JS::MutableHandle<JS::Value> aResult)
+{
+  return ToJSValue(aCx, (aInst->*aMethod)(aIndex), aResult);
+}
+
+
+// Helper for Get[Key,Value]AtIndex(JSContext*, uint32_t, MutableHandleValue)
+// methods, which accept a JS context, index, and mutable result value handle,
+// and return true on success or false on failure.
+template <typename T>
+bool
+CallIterableGetter(JSContext* aCx,
+                   bool (T::* aMethod)(JSContext*, uint32_t, JS::MutableHandle<JS::Value>),
+                   T* aInst, uint32_t aIndex,
+                   JS::MutableHandle<JS::Value> aResult)
+{
+  return (aInst->*aMethod)(aCx, aIndex, aResult);
+}
+
+template <typename T>
+bool
+CallIterableGetter(JSContext* aCx,
+                   bool (T::* aMethod)(JSContext*, uint32_t, JS::MutableHandle<JS::Value>) const,
+                   const T* aInst, uint32_t aIndex,
+                   JS::MutableHandle<JS::Value> aResult)
+{
+  return (aInst->*aMethod)(aCx, aIndex, aResult);
+}
+
 template <typename T>
 class IterableIterator final : public IterableIteratorBase
 {
 public:
   typedef bool (*WrapFunc)(JSContext* aCx,
                            IterableIterator<T>* aObject,
                            JS::Handle<JSObject*> aGivenProto,
                            JS::MutableHandle<JSObject*> aReflector);
@@ -73,51 +120,69 @@ public:
     , mIteratorType(aIteratorType)
     , mWrapFunc(aWrapFunc)
     , mIndex(0)
   {
     MOZ_ASSERT(mIterableObj);
     MOZ_ASSERT(mWrapFunc);
   }
 
+  bool
+  GetKeyAtIndex(JSContext* aCx, uint32_t aIndex,
+                JS::MutableHandle<JS::Value> aResult)
+  {
+    return CallIterableGetter(aCx, &T::GetKeyAtIndex,
+                              mIterableObj.get(),
+                              aIndex, aResult);
+  }
+
+  bool
+  GetValueAtIndex(JSContext* aCx, uint32_t aIndex,
+                  JS::MutableHandle<JS::Value> aResult)
+  {
+    return CallIterableGetter(aCx, &T::GetValueAtIndex,
+                              mIterableObj.get(),
+                              aIndex, aResult);
+  }
+
   void
   Next(JSContext* aCx, JS::MutableHandle<JSObject*> aResult, ErrorResult& aRv)
   {
     JS::Rooted<JS::Value> value(aCx, JS::UndefinedValue());
     if (mIndex >= this->mIterableObj->GetIterableLength()) {
       DictReturn(aCx, aResult, true, value, aRv);
       return;
     }
     switch (mIteratorType) {
     case IterableIteratorType::Keys:
     {
-      if (!ToJSValue(aCx, this->mIterableObj->GetKeyAtIndex(mIndex), &value)) {
+      if (!GetKeyAtIndex(aCx, mIndex, &value)) {
         aRv.Throw(NS_ERROR_FAILURE);
         return;
       }
       DictReturn(aCx, aResult, false, value, aRv);
       break;
     }
     case IterableIteratorType::Values:
     {
-      if (!ToJSValue(aCx, this->mIterableObj->GetValueAtIndex(mIndex), &value)) {
+      if (!GetValueAtIndex(aCx, mIndex, &value)) {
         aRv.Throw(NS_ERROR_FAILURE);
         return;
       }
       DictReturn(aCx, aResult, false, value, aRv);
       break;
     }
     case IterableIteratorType::Entries:
     {
       JS::Rooted<JS::Value> key(aCx);
-      if (!ToJSValue(aCx, this->mIterableObj->GetKeyAtIndex(mIndex), &key)) {
+      if (!GetKeyAtIndex(aCx, mIndex, &key)) {
         aRv.Throw(NS_ERROR_FAILURE);
         return;
       }
-      if (!ToJSValue(aCx, this->mIterableObj->GetValueAtIndex(mIndex), &value)) {
+      if (!GetValueAtIndex(aCx, mIndex, &value)) {
         aRv.Throw(NS_ERROR_FAILURE);
         return;
       }
       KeyAndValueReturn(aCx, key, value, aResult, aRv);
       break;
     }
     default:
       MOZ_CRASH("Invalid iterator type!");
--- a/dom/ipc/SharedMap.cpp
+++ b/dom/ipc/SharedMap.cpp
@@ -173,35 +173,26 @@ SharedMap::EntryArray() const
 }
 
 const nsString
 SharedMap::GetKeyAtIndex(uint32_t aIndex) const
 {
   return NS_ConvertUTF8toUTF16(EntryArray()[aIndex]->Name());
 }
 
-JS::Value
-SharedMap::GetValueAtIndex(uint32_t aIndex) const
+bool
+SharedMap::GetValueAtIndex(JSContext* aCx, uint32_t aIndex,
+                           JS::MutableHandle<JS::Value> aResult) const
 {
-  JSObject* wrapper = GetWrapper();
-  MOZ_ASSERT(wrapper,
-             "Should never see GetValueAtIndex on a SharedMap without a live "
-             "wrapper");
-  if (!wrapper) {
-    return JS::NullValue();
+  ErrorResult rv;
+  EntryArray()[aIndex]->Read(aCx, aResult, rv);
+  if (rv.MaybeSetPendingException(aCx)) {
+    return false;
   }
-
-  AutoJSContext cx;
-
-  JSAutoRealm ar(cx, wrapper);
-
-  JS::RootedValue val(cx);
-  EntryArray()[aIndex]->Read(cx, &val, IgnoreErrors());
-
-  return val;
+  return true;
 }
 
 void
 SharedMap::Entry::TakeData(StructuredCloneData&& aHolder)
 {
   mData = AsVariant(std::move(aHolder));
 
   mSize = Holder().Data().Size();
--- a/dom/ipc/SharedMap.h
+++ b/dom/ipc/SharedMap.h
@@ -91,21 +91,18 @@ public:
   }
 
   /**
    * These functions return the key or value, respectively, at the given index.
    * The index *must* be less than the value returned by GetIterableLength(), or
    * the program will crash.
    */
   const nsString GetKeyAtIndex(uint32_t aIndex) const;
-  // Note: This function should only be called if the instance has a live,
-  // cached wrapper. If it does not, this function will return null, and assert
-  // in debug builds.
-  // The returned value will always be in the same Realm as that wrapper.
-  JS::Value GetValueAtIndex(uint32_t aIndex) const;
+  bool GetValueAtIndex(JSContext* aCx, uint32_t aIndex,
+                       JS::MutableHandle<JS::Value> aResult) const;
 
 
   /**
    * Returns a copy of the read-only file descriptor which backs the shared
    * memory region for this map. The file descriptor may be passed between
    * processes, and used to update corresponding instances in child processes.
    */
   FileDescriptor CloneMapFile();
--- a/gfx/2d/ScaledFontDWrite.cpp
+++ b/gfx/2d/ScaledFontDWrite.cpp
@@ -470,17 +470,23 @@ ScaledFontDWrite::GetWRFontInstanceOptio
   if (UseEmbeddedBitmaps()) {
     options.flags |= wr::FontInstanceFlags::EMBEDDED_BITMAPS;
   }
   if (ForceGDIMode()) {
     options.flags |= wr::FontInstanceFlags::FORCE_GDI;
   }
   options.bg_color = wr::ToColorU(Color());
   options.synthetic_italics = wr::DegreesToSyntheticItalics(GetSyntheticObliqueAngle());
+
+  wr::FontInstancePlatformOptions platformOptions;
+  platformOptions.gamma = uint16_t(std::round(mGamma * 100.0f));
+  platformOptions.contrast = uint16_t(std::round(std::min(mContrast, 1.0f) * 100.0f));
+
   *aOutOptions = Some(options);
+  *aOutPlatformOptions = Some(platformOptions);
   return true;
 }
 
 // Helper for UnscaledFontDWrite::CreateScaledFont: create a clone of the
 // given IDWriteFontFace, with specified variation-axis values applied.
 // Returns nullptr in case of failure.
 static already_AddRefed<IDWriteFontFace5>
 CreateFaceWithVariations(IDWriteFontFace* aFace,
--- a/gfx/layers/ipc/WebRenderMessages.ipdlh
+++ b/gfx/layers/ipc/WebRenderMessages.ipdlh
@@ -84,22 +84,27 @@ struct OpUpdateAsyncImagePipeline {
   PipelineId pipelineId;
   LayoutDeviceRect scBounds;
   Matrix4x4 scTransform;
   MaybeIntSize scaleToSize;
   ImageRendering filter;
   MixBlendMode mixBlendMode;
 };
 
+struct OpUpdatedAsyncImagePipeline {
+  PipelineId pipelineId;
+};
+
 union WebRenderParentCommand {
   OpAddPipelineIdForCompositable;
   OpRemovePipelineIdForCompositable;
   OpRemoveExternalImageId;
   OpReleaseTextureOfImage;
   OpUpdateAsyncImagePipeline;
+  OpUpdatedAsyncImagePipeline;
   CompositableOperation;
   OpAddCompositorAnimations;
 };
 
 struct OffsetRange {
   uint32_t source;
   uint32_t start;
   uint32_t length;
--- a/gfx/layers/wr/AsyncImagePipelineManager.cpp
+++ b/gfx/layers/wr/AsyncImagePipelineManager.cpp
@@ -17,17 +17,16 @@
 #include "mozilla/webrender/WebRenderAPI.h"
 #include "mozilla/webrender/WebRenderTypes.h"
 
 namespace mozilla {
 namespace layers {
 
 AsyncImagePipelineManager::AsyncImagePipeline::AsyncImagePipeline()
  : mInitialised(false)
- , mSentDL(false)
  , mIsChanged(false)
  , mUseExternalImage(false)
  , mFilter(wr::ImageRendering::Auto)
  , mMixBlendMode(wr::MixBlendMode::Normal)
 {}
 
 AsyncImagePipelineManager::AsyncImagePipelineManager(already_AddRefed<wr::WebRenderAPI>&& aApi)
  : mApi(aApi)
@@ -264,115 +263,153 @@ AsyncImagePipelineManager::UpdateWithout
   }
 
   dSurf->Unmap();
 
   return Some(aOp);
 }
 
 void
-AsyncImagePipelineManager::ApplyAsyncImages(wr::TransactionBuilder& aTxn)
+AsyncImagePipelineManager::ApplyAsyncImagesOfImageBridge(wr::TransactionBuilder& aTxn)
 {
   if (mDestroyed || mAsyncImagePipelines.Count() == 0) {
     return;
   }
 
   wr::Epoch epoch = GetNextImageEpoch();
 
   // We use a pipeline with a very small display list for each video element.
   // Update each of them if needed.
   for (auto iter = mAsyncImagePipelines.Iter(); !iter.Done(); iter.Next()) {
     wr::PipelineId pipelineId = wr::AsPipelineId(iter.Key());
     AsyncImagePipeline* pipeline = iter.Data();
-
-    nsTArray<wr::ImageKey> keys;
-    auto op = UpdateImageKeys(aTxn, pipeline, keys);
-
-    bool updateDisplayList = pipeline->mInitialised &&
-                             (pipeline->mIsChanged || op == Some(TextureHost::ADD_IMAGE)) &&
-                             !!pipeline->mCurrentTexture;
-
-    if (!pipeline->mSentDL) {
-      // If we haven't sent a display list yet, do it anyway, even if it's just
-      // an empty DL with a stacking context and no actual image. Otherwise WR
-      // will assert about missing the pipeline
-      updateDisplayList = true;
+    // If aync image pipeline does not use ImageBridge, do not need to apply.
+    if (!pipeline->mImageHost->GetAsyncRef()) {
+      continue;
     }
+    ApplyAsyncImageForPipeline(epoch, pipelineId, pipeline, aTxn);
+  }
+}
 
-    // We will schedule generating a frame after the scene
-    // build is done or resource update is done, so we don't need to do it here.
+void
+AsyncImagePipelineManager::ApplyAsyncImageForPipeline(const wr::Epoch& aEpoch,
+                                                      const wr::PipelineId& aPipelineId,
+                                                      AsyncImagePipeline* aPipeline,
+                                                      wr::TransactionBuilder& aTxn)
+{
+  nsTArray<wr::ImageKey> keys;
+  auto op = UpdateImageKeys(aTxn, aPipeline, keys);
+
+  bool updateDisplayList = aPipeline->mInitialised &&
+                           (aPipeline->mIsChanged || op == Some(TextureHost::ADD_IMAGE)) &&
+                           !!aPipeline->mCurrentTexture;
+
+  // We will schedule generating a frame after the scene
+  // build is done or resource update is done, so we don't need to do it here.
 
-    if (!updateDisplayList) {
-      // We don't need to update the display list, either because we can't or because
-      // the previous one is still up to date.
-      // We may, however, have updated some resources.
-      aTxn.UpdateEpoch(pipelineId, epoch);
-      if (pipeline->mCurrentTexture) {
-        HoldExternalImage(pipelineId, epoch, pipeline->mCurrentTexture->AsWebRenderTextureHost());
-      }
-      continue;
+  if (!updateDisplayList) {
+    // We don't need to update the display list, either because we can't or because
+    // the previous one is still up to date.
+    // We may, however, have updated some resources.
+    aTxn.UpdateEpoch(aPipelineId, aEpoch);
+    if (aPipeline->mCurrentTexture) {
+      HoldExternalImage(aPipelineId, aEpoch, aPipeline->mCurrentTexture->AsWebRenderTextureHost());
+    }
+    return;
+  }
+
+  aPipeline->mIsChanged = false;
+
+  wr::LayoutSize contentSize { aPipeline->mScBounds.Width(), aPipeline->mScBounds.Height() };
+  wr::DisplayListBuilder builder(aPipelineId, contentSize);
+
+  float opacity = 1.0f;
+  Maybe<wr::WrClipId> referenceFrameId = builder.PushStackingContext(
+    wr::ToLayoutRect(aPipeline->mScBounds),
+    nullptr,
+    nullptr,
+    &opacity,
+    aPipeline->mScTransform.IsIdentity() ? nullptr : &aPipeline->mScTransform,
+    wr::TransformStyle::Flat,
+    nullptr,
+    aPipeline->mMixBlendMode,
+    nsTArray<wr::WrFilterOp>(),
+    true,
+    // This is fine to do unconditionally because we only push images here.
+    wr::GlyphRasterSpace::Screen());
+
+  if (aPipeline->mCurrentTexture && !keys.IsEmpty()) {
+    LayoutDeviceRect rect(0, 0, aPipeline->mCurrentTexture->GetSize().width, aPipeline->mCurrentTexture->GetSize().height);
+    if (aPipeline->mScaleToSize.isSome()) {
+      rect = LayoutDeviceRect(0, 0, aPipeline->mScaleToSize.value().width, aPipeline->mScaleToSize.value().height);
     }
 
-    pipeline->mSentDL = true;
-    pipeline->mIsChanged = false;
-
-    wr::LayoutSize contentSize { pipeline->mScBounds.Width(), pipeline->mScBounds.Height() };
-    wr::DisplayListBuilder builder(pipelineId, contentSize);
+    if (aPipeline->mUseExternalImage) {
+      MOZ_ASSERT(aPipeline->mCurrentTexture->AsWebRenderTextureHost());
+      Range<wr::ImageKey> range_keys(&keys[0], keys.Length());
+      aPipeline->mCurrentTexture->PushDisplayItems(builder,
+                                                  wr::ToLayoutRect(rect),
+                                                  wr::ToLayoutRect(rect),
+                                                  aPipeline->mFilter,
+                                                  range_keys);
+      HoldExternalImage(aPipelineId, aEpoch, aPipeline->mCurrentTexture->AsWebRenderTextureHost());
+    } else {
+      MOZ_ASSERT(keys.Length() == 1);
+      builder.PushImage(wr::ToLayoutRect(rect),
+                        wr::ToLayoutRect(rect),
+                        true,
+                        aPipeline->mFilter,
+                        keys[0]);
+    }
+  }
 
-    float opacity = 1.0f;
-    Maybe<wr::WrClipId> referenceFrameId = builder.PushStackingContext(
-        wr::ToLayoutRect(pipeline->mScBounds),
-        nullptr,
-        nullptr,
-        &opacity,
-        pipeline->mScTransform.IsIdentity() ? nullptr : &pipeline->mScTransform,
-        wr::TransformStyle::Flat,
-        nullptr,
-        pipeline->mMixBlendMode,
-        nsTArray<wr::WrFilterOp>(),
-        true,
-        // This is fine to do unconditionally because we only push images here.
-        wr::GlyphRasterSpace::Screen());
+  builder.PopStackingContext(referenceFrameId.isSome());
 
-    if (pipeline->mCurrentTexture && !keys.IsEmpty()) {
-      LayoutDeviceRect rect(0, 0, pipeline->mCurrentTexture->GetSize().width, pipeline->mCurrentTexture->GetSize().height);
-      if (pipeline->mScaleToSize.isSome()) {
-        rect = LayoutDeviceRect(0, 0, pipeline->mScaleToSize.value().width, pipeline->mScaleToSize.value().height);
-      }
+  wr::BuiltDisplayList dl;
+  wr::LayoutSize builderContentSize;
+  builder.Finalize(builderContentSize, dl);
+  aTxn.SetDisplayList(gfx::Color(0.f, 0.f, 0.f, 0.f),
+                      aEpoch,
+                      LayerSize(aPipeline->mScBounds.Width(), aPipeline->mScBounds.Height()),
+                      aPipelineId, builderContentSize,
+                      dl.dl_desc, dl.dl);
+}
 
-      if (pipeline->mUseExternalImage) {
-        MOZ_ASSERT(pipeline->mCurrentTexture->AsWebRenderTextureHost());
-        Range<wr::ImageKey> range_keys(&keys[0], keys.Length());
-        pipeline->mCurrentTexture->PushDisplayItems(builder,
-                                                    wr::ToLayoutRect(rect),
-                                                    wr::ToLayoutRect(rect),
-                                                    pipeline->mFilter,
-                                                    range_keys);
-        HoldExternalImage(pipelineId, epoch, pipeline->mCurrentTexture->AsWebRenderTextureHost());
-      } else {
-        MOZ_ASSERT(keys.Length() == 1);
-        builder.PushImage(wr::ToLayoutRect(rect),
-                          wr::ToLayoutRect(rect),
-                          true,
-                          pipeline->mFilter,
-                          keys[0]);
-      }
-    }
+void
+AsyncImagePipelineManager::ApplyAsyncImageForPipeline(const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn)
+{
+  AsyncImagePipeline* pipeline = mAsyncImagePipelines.Get(wr::AsUint64(aPipelineId));
+  if (!pipeline) {
+    return;
+  }
+
+  wr::Epoch epoch = GetNextImageEpoch();
+  ApplyAsyncImageForPipeline(epoch, aPipelineId, pipeline, aTxn);
+}
 
-    builder.PopStackingContext(referenceFrameId.isSome());
+void
+AsyncImagePipelineManager::SetEmptyDisplayList(const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn)
+{
+  AsyncImagePipeline* pipeline = mAsyncImagePipelines.Get(wr::AsUint64(aPipelineId));
+  if (!pipeline) {
+    return;
+  }
 
-    wr::BuiltDisplayList dl;
-    wr::LayoutSize builderContentSize;
-    builder.Finalize(builderContentSize, dl);
-    aTxn.SetDisplayList(gfx::Color(0.f, 0.f, 0.f, 0.f),
-                        epoch,
-                        LayerSize(pipeline->mScBounds.Width(), pipeline->mScBounds.Height()),
-                        pipelineId, builderContentSize,
-                        dl.dl_desc, dl.dl);
-  }
+  wr::Epoch epoch = GetNextImageEpoch();
+  wr::LayoutSize contentSize { pipeline->mScBounds.Width(), pipeline->mScBounds.Height() };
+  wr::DisplayListBuilder builder(aPipelineId, contentSize);
+
+  wr::BuiltDisplayList dl;
+  wr::LayoutSize builderContentSize;
+  builder.Finalize(builderContentSize, dl);
+  aTxn.SetDisplayList(gfx::Color(0.f, 0.f, 0.f, 0.f),
+                      epoch,
+                      LayerSize(pipeline->mScBounds.Width(), pipeline->mScBounds.Height()),
+                      aPipelineId, builderContentSize,
+                      dl.dl_desc, dl.dl);
 }
 
 void
 AsyncImagePipelineManager::HoldExternalImage(const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch, WebRenderTextureHost* aTexture)
 {
   if (mDestroyed) {
     return;
   }
--- a/gfx/layers/wr/AsyncImagePipelineManager.h
+++ b/gfx/layers/wr/AsyncImagePipelineManager.h
@@ -83,17 +83,20 @@ public:
   void RemoveAsyncImagePipeline(const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn);
 
   void UpdateAsyncImagePipeline(const wr::PipelineId& aPipelineId,
                                 const LayoutDeviceRect& aScBounds,
                                 const gfx::Matrix4x4& aScTransform,
                                 const gfx::MaybeIntSize& aScaleToSize,
                                 const wr::ImageRendering& aFilter,
                                 const wr::MixBlendMode& aMixBlendMode);
-  void ApplyAsyncImages(wr::TransactionBuilder& aTxn);
+  void ApplyAsyncImagesOfImageBridge(wr::TransactionBuilder& aTxn);
+  void ApplyAsyncImageForPipeline(const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn);
+
+  void SetEmptyDisplayList(const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn);
 
   void AppendImageCompositeNotification(const ImageCompositeNotificationInfo& aNotification)
   {
     mImageCompositeNotifications.AppendElement(aNotification);
   }
 
   void FlushImageNotifications(nsTArray<ImageCompositeNotificationInfo>* aNotifications)
   {
@@ -159,29 +162,32 @@ private:
       mScBounds = aScBounds;
       mScTransform = aScTransform;
       mScaleToSize = aScaleToSize;
       mFilter = aFilter;
       mMixBlendMode = aMixBlendMode;
     }
 
     bool mInitialised;
-    bool mSentDL;
     bool mIsChanged;
     bool mUseExternalImage;
     LayoutDeviceRect mScBounds;
     gfx::Matrix4x4 mScTransform;
     gfx::MaybeIntSize mScaleToSize;
     wr::ImageRendering mFilter;
     wr::MixBlendMode mMixBlendMode;
     RefPtr<WebRenderImageHost> mImageHost;
     CompositableTextureHostRef mCurrentTexture;
     nsTArray<wr::ImageKey> mKeys;
   };
 
+  void ApplyAsyncImageForPipeline(const wr::Epoch& aEpoch,
+                                  const wr::PipelineId& aPipelineId,
+                                  AsyncImagePipeline* aPipeline,
+                                  wr::TransactionBuilder& aTxn);
   Maybe<TextureHost::ResourceUpdateOp>
   UpdateImageKeys(wr::TransactionBuilder& aResourceUpdates,
                   AsyncImagePipeline* aPipeline,
                   nsTArray<wr::ImageKey>& aKeys);
   Maybe<TextureHost::ResourceUpdateOp>
   UpdateWithoutExternalImage(wr::TransactionBuilder& aResources,
                              TextureHost* aTexture,
                              wr::ImageKey aKey,
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -795,23 +795,16 @@ WebRenderBridgeParent::RecvSetDisplayLis
       LayoutDeviceIntSize widgetSize = mWidget->GetClientSize();
       LayoutDeviceIntRect docRect(LayoutDeviceIntPoint(), widgetSize);
       txn.SetWindowParameters(widgetSize, docRect);
     }
     gfx::Color clearColor(0.f, 0.f, 0.f, 0.f);
     txn.SetDisplayList(clearColor, wrEpoch, LayerSize(aSize.width, aSize.height),
                        mPipelineId, aContentSize,
                        dlDesc, dlData);
-    // The display list that we're sending to WR might contain references to
-    // other pipelines that we just added to the async image manager in the
-    // ProcessWebRenderParentCommands call above. If we send the display list
-    // alone then WR will not yet have the content for those other pipelines
-    // and so it will emit errors; the ApplyAsyncImages call below ensure that
-    // we provide the pipeline content to WR as part of the same transaction.
-    mAsyncImageManager->ApplyAsyncImages(txn);
 
     mApi->SendTransaction(txn);
 
     // We will schedule generating a frame after the scene
     // build is done, so we don't need to do it here.
   }
 
   HoldPendingTransactionId(wrEpoch, aTransactionId, aRefreshStartTime, aTxnStartTime, aFwdTime);
@@ -934,17 +927,18 @@ WebRenderBridgeParent::ProcessWebRenderP
 {
   for (InfallibleTArray<WebRenderParentCommand>::index_type i = 0; i < aCommands.Length(); ++i) {
     const WebRenderParentCommand& cmd = aCommands[i];
     switch (cmd.type()) {
       case WebRenderParentCommand::TOpAddPipelineIdForCompositable: {
         const OpAddPipelineIdForCompositable& op = cmd.get_OpAddPipelineIdForCompositable();
         AddPipelineIdForCompositable(op.pipelineId(),
                                      op.handle(),
-                                     op.isAsync());
+                                     op.isAsync(),
+                                     aTxn);
         break;
       }
       case WebRenderParentCommand::TOpRemovePipelineIdForCompositable: {
         const OpRemovePipelineIdForCompositable& op = cmd.get_OpRemovePipelineIdForCompositable();
         RemovePipelineIdForCompositable(op.pipelineId(), aTxn);
         break;
       }
       case WebRenderParentCommand::TOpRemoveExternalImageId: {
@@ -960,16 +954,22 @@ WebRenderBridgeParent::ProcessWebRenderP
       case WebRenderParentCommand::TOpUpdateAsyncImagePipeline: {
         const OpUpdateAsyncImagePipeline& op = cmd.get_OpUpdateAsyncImagePipeline();
         mAsyncImageManager->UpdateAsyncImagePipeline(op.pipelineId(),
                                                      op.scBounds(),
                                                      op.scTransform(),
                                                      op.scaleToSize(),
                                                      op.filter(),
                                                      op.mixBlendMode());
+        mAsyncImageManager->ApplyAsyncImageForPipeline(op.pipelineId(), aTxn);
+        break;
+      }
+      case WebRenderParentCommand::TOpUpdatedAsyncImagePipeline: {
+        const OpUpdatedAsyncImagePipeline& op = cmd.get_OpUpdatedAsyncImagePipeline();
+        mAsyncImageManager->ApplyAsyncImageForPipeline(op.pipelineId(), aTxn);
         break;
       }
       case WebRenderParentCommand::TCompositableOperation: {
         if (!ReceiveCompositableUpdate(cmd.get_CompositableOperation())) {
           NS_ERROR("ReceiveCompositableUpdate failed");
         }
         break;
       }
@@ -1094,17 +1094,18 @@ WebRenderBridgeParent::RecvGetSnapshot(P
   mApi->Readback(start, size, buffer, buffer_size);
 
   return IPC_OK();
 }
 
 void
 WebRenderBridgeParent::AddPipelineIdForCompositable(const wr::PipelineId& aPipelineId,
                                                     const CompositableHandle& aHandle,
-                                                    const bool& aAsync)
+                                                    const bool& aAsync,
+                                                    wr::TransactionBuilder& aTxn)
 {
   if (mDestroyed) {
     return;
   }
 
   MOZ_ASSERT(mAsyncCompositables.find(wr::AsUint64(aPipelineId)) == mAsyncCompositables.end());
 
   RefPtr<CompositableHost> host;
@@ -1131,16 +1132,23 @@ WebRenderBridgeParent::AddPipelineIdForC
     return;
   }
 
   wrHost->SetWrBridge(this);
   wrHost->EnableUseAsyncImagePipeline();
   mAsyncCompositables.emplace(wr::AsUint64(aPipelineId), wrHost);
   mAsyncImageManager->AddAsyncImagePipeline(aPipelineId, wrHost);
 
+  // If this is being called from WebRenderBridgeParent::RecvSetDisplayList,
+  // then aTxn might contain a display list that references pipelines that
+  // we just added to the async image manager.
+  // If we send the display list alone then WR will not yet have the content for
+  // the pipelines and so it will emit errors; the SetEmptyDisplayList call
+  // below ensure that we provide its content to WR as part of the same transaction.
+  mAsyncImageManager->SetEmptyDisplayList(aPipelineId, aTxn);
   return;
 }
 
 void
 WebRenderBridgeParent::RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId,
                                                        wr::TransactionBuilder& aTxn)
 {
   if (mDestroyed) {
@@ -1516,17 +1524,17 @@ WebRenderBridgeParent::CompositeToTarget
   {
     // TODO: We can improve upon this by using two transactions: one for everything that
     // doesn't change the display list (in other words does not cause the scene to be
     // re-built), and one for the rest. This way, if an async pipeline needs to re-build
     // its display list, other async pipelines can still be rendered while the scene is
     // building. Those other async pipelines can go in the other transaction that
     // we create below.
     wr::TransactionBuilder txn;
-    mAsyncImageManager->ApplyAsyncImages(txn);
+    mAsyncImageManager->ApplyAsyncImagesOfImageBridge(txn);
     mApi->SendTransaction(txn);
   }
 
   if (!mAsyncImageManager->GetCompositeUntilTime().IsNull()) {
     // Trigger another CompositeToTarget() call because there might be another
     // frame that we want to generate after this one.
     // It will check if we actually want to generate the frame or not.
     mCompositorScheduler->ScheduleComposition();
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -216,17 +216,18 @@ private:
   bool PushExternalImageForTexture(wr::ExternalImageId aExtId,
                                    wr::ImageKey aKey,
                                    TextureHost* aTexture,
                                    bool aIsUpdate,
                                    wr::TransactionBuilder& aResources);
 
   void AddPipelineIdForCompositable(const wr::PipelineId& aPipelineIds,
                                     const CompositableHandle& aHandle,
-                                    const bool& aAsync);
+                                    const bool& aAsync,
+                                    wr::TransactionBuilder& aTxn);
   void RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId,
                                        wr::TransactionBuilder& aTxn);
 
   void RemoveExternalImageId(const ExternalImageId& aImageId);
   void ReleaseTextureOfImage(const wr::ImageKey& aKey);
 
   LayersId GetLayersId() const;
   void ProcessWebRenderParentCommands(const InfallibleTArray<WebRenderParentCommand>& aCommands,
--- a/gfx/layers/wr/WebRenderCanvasRenderer.cpp
+++ b/gfx/layers/wr/WebRenderCanvasRenderer.cpp
@@ -90,10 +90,23 @@ void
 WebRenderCanvasRendererAsync::Destroy()
 {
   if (mPipelineId.isSome()) {
     mManager->WrBridge()->RemovePipelineIdForCompositable(mPipelineId.ref());
     mPipelineId.reset();
   }
 }
 
+void
+WebRenderCanvasRendererAsync::UpdateCompositableClientForEmptyTransaction()
+{
+  UpdateCompositableClient();
+  if (mPipelineId.isSome()) {
+    // Notify an update of async image pipeline during empty transaction.
+    // During non empty transaction, WebRenderBridgeParent receives OpUpdateAsyncImagePipeline message,
+    // but during empty transaction, the message is not sent to WebRenderBridgeParent.
+    // Then OpUpdatedAsyncImagePipeline is used to notify the update.
+    mManager->WrBridge()->AddWebRenderParentCommand(OpUpdatedAsyncImagePipeline(mPipelineId.ref()));
+  }
+}
+
 } // namespace layers
 } // namespace mozilla
--- a/gfx/layers/wr/WebRenderCanvasRenderer.h
+++ b/gfx/layers/wr/WebRenderCanvasRenderer.h
@@ -40,16 +40,18 @@ public:
   WebRenderCanvasRendererAsync* AsWebRenderCanvasRendererAsync() override { return this; }
 
   void Initialize(const CanvasInitializeData& aData) override;
   bool CreateCompositable() override;
 
   void ClearCachedResources() override;
   void Destroy() override;
 
+  void UpdateCompositableClientForEmptyTransaction();
+
   Maybe<wr::PipelineId> GetPipelineId() { return mPipelineId; }
 protected:
   Maybe<wr::PipelineId> mPipelineId;
 };
 
 } // namespace layers
 } // namespace mozilla
 
--- a/gfx/layers/wr/WebRenderCommandBuilder.cpp
+++ b/gfx/layers/wr/WebRenderCommandBuilder.cpp
@@ -1160,17 +1160,17 @@ WebRenderCommandBuilder::Destroy()
 void
 WebRenderCommandBuilder::EmptyTransaction()
 {
   // We need to update canvases that might have changed.
   for (auto iter = mLastCanvasDatas.Iter(); !iter.Done(); iter.Next()) {
     RefPtr<WebRenderCanvasData> canvasData = iter.Get()->GetKey();
     WebRenderCanvasRendererAsync* canvas = canvasData->GetCanvasRenderer();
     if (canvas) {
-      canvas->UpdateCompositableClient();
+      canvas->UpdateCompositableClientForEmptyTransaction();
     }
   }
 }
 
 bool
 WebRenderCommandBuilder::NeedsEmptyTransaction()
 {
   return !mLastCanvasDatas.IsEmpty();
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-visited/logical-box-border-color-visited-link-003.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Logical Properties: Flow-Relative Border Colors in Visited Links</title>
+<style>
+a {
+  display: inline-block;
+  vertical-align: top;
+  border: 25px solid green;
+  border-top-color: red;
+  border-left-color: red;
+}
+#link1:visited,
+#link2,
+#link3:visited,
+#link4 {
+  direction: rtl;
+}
+#link1:visited,
+#link2:visited,
+#link3,
+#link4 {
+  writing-mode: vertical-rl;
+}
+#link1:visited {
+  border-block-start-color: green;
+  border-inline-start-color: green;
+}
+#link2:visited {
+  border-block-start-color: green;
+  border-inline-end-color: green;
+}
+#link3:visited {
+  border-inline-start-color: green;
+  border-block-end-color: green;
+}
+#link4:visited {
+  border-inline-end-color: green;
+  border-block-end-color: green;
+}
+</style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<a id="link1" href="visited-page.html"></a><a id="link2" href="visited-page.html"></a><br>
+<a id="link3" href="visited-page.html"></a><a id="link4" href="visited-page.html"></a>
--- a/layout/style/test/moz.build
+++ b/layout/style/test/moz.build
@@ -76,16 +76,17 @@ TEST_HARNESS_FILES.testing.mochitest.tes
     '/layout/reftests/css-visited/first-line-1-ref.html',
     '/layout/reftests/css-visited/first-line-1.html',
     '/layout/reftests/css-visited/inherit-keyword-1-ref.html',
     '/layout/reftests/css-visited/inherit-keyword-1.xhtml',
     '/layout/reftests/css-visited/link-root-1-ref.xhtml',
     '/layout/reftests/css-visited/link-root-1.xhtml',
     '/layout/reftests/css-visited/logical-box-border-color-visited-link-001.html',
     '/layout/reftests/css-visited/logical-box-border-color-visited-link-002.html',
+    '/layout/reftests/css-visited/logical-box-border-color-visited-link-003.html',
     '/layout/reftests/css-visited/logical-box-border-color-visited-link-ref.html',
     '/layout/reftests/css-visited/mathml-links-ref.html',
     '/layout/reftests/css-visited/mathml-links.html',
     '/layout/reftests/css-visited/outline-1-ref.html',
     '/layout/reftests/css-visited/outline-1.html',
     '/layout/reftests/css-visited/placeholder-1-ref.html',
     '/layout/reftests/css-visited/placeholder-1.html',
     '/layout/reftests/css-visited/selector-adj-sibling-1-ref.html',
--- a/layout/style/test/test_visited_reftests.html
+++ b/layout/style/test/test_visited_reftests.html
@@ -89,16 +89,17 @@ var gTests = [
   "== link-root-1.xhtml link-root-1-ref.xhtml",
   "== mathml-links.html mathml-links-ref.html",
   "== placeholder-1.html placeholder-1-ref.html",
   "== visited-inherit-1.html visited-inherit-1-ref.html",
   "== transition-on-visited.html transition-on-visited-ref.html",
   "== logical-box-border-color-visited-link-001.html logical-box-border-color-visited-link-ref.html",
   // TODO: test should equal the reference after implementing logical border shorthands.
   "!= logical-box-border-color-visited-link-002.html logical-box-border-color-visited-link-ref.html",
+  "== logical-box-border-color-visited-link-003.html logical-box-border-color-visited-link-ref.html",
 ];
 
 // We record the maximum number of times we had to look at a test before
 // it switched to the passing state (though we assume it's 10 to start
 // rather than 0 so that we have a reasonable default).  Then we make a
 // test "time out" if it takes more than gTimeoutFactor times that
 // amount of time.  This allows us to report a test failure rather than
 // making a test failure just show up as a timeout.
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -328,34 +328,16 @@ VARCACHE_PREF(
 #endif
 VARCACHE_PREF(
   "layout.css.frames-timing.enabled",
    layout_css_frames_timing_enabled,
   bool, PREF_VALUE
 )
 #undef PREF_VALUE
 
-// When the pref is true, CSSStyleDeclaration.setProperty always appends
-// new declarations (and discards old ones if they exist), otherwise, it
-// will update in-place when given property exists in the block, and
-// avoid updating at all when the existing property declaration is
-// identical to the new one.
-// See bug 1415330, bug 1460295, and bug 1461285 for some background.
-#ifdef RELEASE_OR_BETA
-# define PREF_VALUE false
-#else
-# define PREF_VALUE true
-#endif
-VARCACHE_PREF(
-  "layout.css.property-append-only",
-   layout_css_property_append_only,
-   bool, PREF_VALUE
-)
-#undef PREF_VALUE
-
 // Should the :visited selector ever match (otherwise :link matches instead)?
 VARCACHE_PREF(
   "layout.css.visited_links_enabled",
    layout_css_visited_links_enabled,
   bool, true
 )
 
 // Pref to control whether @-moz-document rules are enabled in content pages.
--- a/servo/components/style/animation.rs
+++ b/servo/components/style/animation.rs
@@ -4,17 +4,17 @@
 
 //! CSS transitions and animations.
 
 use Atom;
 use bezier::Bezier;
 use context::SharedStyleContext;
 use dom::{OpaqueNode, TElement};
 use font_metrics::FontMetricsProvider;
-use properties::{self, CascadeFlags, ComputedValues, LonghandId};
+use properties::{self, CascadeMode, ComputedValues, LonghandId};
 use properties::animated_properties::AnimatedProperty;
 use properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection;
 use properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState;
 use rule_tree::CascadeLevel;
 use servo_arc::Arc;
 use std::fmt;
 use std::sync::mpsc::Sender;
 use stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue};
@@ -499,19 +499,18 @@ where
                 context.stylist.device(),
                 /* pseudo = */ None,
                 previous_style.rules(),
                 &context.guards,
                 iter,
                 Some(previous_style),
                 Some(previous_style),
                 Some(previous_style),
-                /* visited_style = */ None,
                 font_metrics_provider,
-                CascadeFlags::empty(),
+                CascadeMode::Unvisited { visited_rules: None },
                 context.quirks_mode(),
                 /* rule_cache = */ None,
                 &mut Default::default(),
                 /* element = */ None,
             );
             computed
         },
     }
--- a/servo/components/style/gecko/pseudo_element.rs
+++ b/servo/components/style/gecko/pseudo_element.rs
@@ -5,17 +5,17 @@
 //! Gecko's definition of a pseudo-element.
 //!
 //! Note that a few autogenerated bits of this live in
 //! `pseudo_element_definition.mako.rs`. If you touch that file, you probably
 //! need to update the checked-in files for Servo.
 
 use cssparser::ToCss;
 use gecko_bindings::structs::{self, CSSPseudoElementType};
-use properties::{CascadeFlags, ComputedValues, PropertyFlags};
+use properties::{ComputedValues, PropertyFlags};
 use properties::longhands::display::computed_value::T as Display;
 use selector_parser::{NonTSPseudoClass, PseudoElementCascadeType, SelectorImpl};
 use std::fmt;
 use string_cache::Atom;
 use values::serialize_atom_identifier;
 
 include!(concat!(
     env!("OUT_DIR"),
@@ -50,24 +50,22 @@ impl PseudoElement {
 
         if self.is_precomputed() {
             return PseudoElementCascadeType::Precomputed;
         }
 
         PseudoElementCascadeType::Lazy
     }
 
-    /// The CascadeFlags needed to cascade this pseudo-element.
+    /// Whether cascading this pseudo-element makes it inherit all properties,
+    /// even reset ones.
     ///
-    /// This is only needed to support the broken INHERIT_ALL pseudo mode for
-    /// Servo.
+    /// This is used in Servo for anonymous boxes, though it's likely broken.
     #[inline]
-    pub fn cascade_flags(&self) -> CascadeFlags {
-        CascadeFlags::empty()
-    }
+    pub fn inherits_all(&self) -> bool { false }
 
     /// Whether the pseudo-element should inherit from the default computed
     /// values instead of from the parent element.
     ///
     /// This is not the common thing, but there are some pseudos (namely:
     /// ::backdrop), that shouldn't inherit from the parent element.
     pub fn inherits_from_default_values(&self) -> bool {
         matches!(*self, PseudoElement::Backdrop)
--- a/servo/components/style/properties/computed_value_flags.rs
+++ b/servo/components/style/properties/computed_value_flags.rs
@@ -51,31 +51,27 @@ bitflags! {
         /// Whether this style inherits the `content` property.
         ///
         /// Important because of the same reason.
         const INHERITS_CONTENT = 1 << 7;
 
         /// Whether the child explicitly inherits any reset property.
         const INHERITS_RESET_STYLE = 1 << 8;
 
-        /// A flag to mark a style which is a visited style.
-        const IS_STYLE_IF_VISITED = 1 << 9;
-
         /// Whether the style or any of the ancestors has a multicol style.
         ///
         /// Only used in Servo.
-        const CAN_BE_FRAGMENTED = 1 << 10;
+        const CAN_BE_FRAGMENTED = 1 << 9;
     }
 }
 
 impl ComputedValueFlags {
     /// Flags that are unconditionally propagated to descendants.
     #[inline]
     fn inherited_flags() -> Self {
-        ComputedValueFlags::IS_STYLE_IF_VISITED |
         ComputedValueFlags::IS_RELEVANT_LINK_VISITED |
         ComputedValueFlags::CAN_BE_FRAGMENTED |
         ComputedValueFlags::IS_IN_PSEUDO_ELEMENT_SUBTREE |
         ComputedValueFlags::HAS_TEXT_DECORATION_LINES
     }
 
     /// Flags that may be propagated to descendants.
     #[inline]
--- a/servo/components/style/properties/data.py
+++ b/servo/components/style/properties/data.py
@@ -163,17 +163,17 @@ def parse_property_aliases(alias_list):
 
 
 class Longhand(object):
     def __init__(self, style_struct, name, spec=None, animation_value_type=None, keyword=None,
                  predefined_type=None, servo_pref=None, gecko_pref=None,
                  enabled_in="content", need_index=False,
                  gecko_ffi_name=None,
                  allowed_in_keyframe_block=True, cast_type='u8',
-                 logical=False, alias=None, extra_prefixes=None, boxed=False,
+                 logical=False, logical_group=None, alias=None, extra_prefixes=None, boxed=False,
                  flags=None, allowed_in_page_rule=False, allow_quirks=False,
                  ignored_when_colors_disabled=False,
                  vector=False, servo_restyle_damage="repaint"):
         self.name = name
         if not spec:
             raise TypeError("Spec should be specified for %s" % name)
         self.spec = spec
         self.keyword = keyword
@@ -191,16 +191,19 @@ class Longhand(object):
         #  * "content" implies the property is accessible unconditionally,
         #    modulo a pref, set via servo_pref / gecko_pref.
         assert enabled_in in ["", "ua", "chrome", "content"]
         self.enabled_in = enabled_in
         self.need_index = need_index
         self.gecko_ffi_name = gecko_ffi_name or "m" + self.camel_case
         self.cast_type = cast_type
         self.logical = arg_to_bool(logical)
+        self.logical_group = logical_group
+        if self.logical:
+            assert logical_group, "Property " + name + " must have a logical group"
         self.alias = parse_property_aliases(alias)
         self.extra_prefixes = parse_property_aliases(extra_prefixes)
         self.boxed = arg_to_bool(boxed)
         self.flags = flags.split() if flags else []
         self.allowed_in_page_rule = arg_to_bool(allowed_in_page_rule)
         self.allow_quirks = allow_quirks
         self.ignored_when_colors_disabled = ignored_when_colors_disabled
         self.is_vector = vector
--- a/servo/components/style/properties/declaration_block.rs
+++ b/servo/components/style/properties/declaration_block.rs
@@ -6,16 +6,17 @@
 
 #![deny(missing_docs)]
 
 use context::QuirksMode;
 use cssparser::{DeclarationListParser, parse_important, ParserInput, CowRcStr};
 use cssparser::{Parser, AtRuleParser, DeclarationParser, Delimiter, ParseErrorKind};
 use custom_properties::CustomPropertiesBuilder;
 use error_reporting::{ParseErrorReporter, ContextualParseError};
+use itertools::Itertools;
 use parser::ParserContext;
 use properties::animated_properties::{AnimationValue, AnimationValueMap};
 use shared_lock::Locked;
 use smallbitvec::{self, SmallBitVec};
 use smallvec::SmallVec;
 use std::fmt::{self, Write};
 use std::iter::{DoubleEndedIterator, Zip};
 use std::slice::Iter;
@@ -34,31 +35,38 @@ pub struct AnimationRules(pub Option<Arc
 
 impl AnimationRules {
     /// Returns whether these animation rules represents an actual rule or not.
     pub fn is_empty(&self) -> bool {
         self.0.is_none() && self.1.is_none()
     }
 }
 
-/// Enum for how a given declaration should be pushed into a declaration block.
-#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
-pub enum DeclarationPushMode {
-    /// Mode used when declarations were obtained from CSS parsing.
-    /// If there is an existing declaration of the same property with a higher
-    /// importance, the new declaration will be discarded. Otherwise, it will
-    /// be appended to the end of the declaration block.
-    Parsing,
-    /// In this mode, if there is an existing declaration of the same property,
-    /// the value is updated in-place. Otherwise it's appended. This is one
-    /// possible behavior of CSSOM.
-    Update,
-    /// In this mode, the new declaration is always pushed to the end of the
-    /// declaration block. This is another possible behavior of CSSOM.
+/// An enum describes how a declaration should update
+/// the declaration block.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum DeclarationUpdate {
+    /// The given declaration doesn't update anything.
+    None,
+    /// The given declaration is new, and should be append directly.
     Append,
+    /// The given declaration can be updated in-place at the given position.
+    UpdateInPlace { pos: usize },
+    /// The given declaration cannot be updated in-place, and an existing
+    /// one needs to be removed at the given position.
+    AppendAndRemove { pos: usize },
+}
+
+/// A struct describes how a declaration block should be updated by
+/// a `SourcePropertyDeclaration`.
+#[derive(Default)]
+pub struct SourcePropertyDeclarationUpdate {
+    updates: SubpropertiesVec<DeclarationUpdate>,
+    new_count: usize,
+    any_removal: bool,
 }
 
 /// A declaration [importance][importance].
 ///
 /// [importance]: https://drafts.csswg.org/css-cascade/#importance
 #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
 pub enum Importance {
     /// Indicates a declaration without `!important`.
@@ -435,150 +443,88 @@ impl PropertyDeclarationBlock {
         }
     }
 
     /// Returns whether the property is definitely new for this declaration
     /// block. It returns true when the declaration is a non-custom longhand
     /// and it doesn't exist in the block, and returns false otherwise.
     #[inline]
     fn is_definitely_new(&self, decl: &PropertyDeclaration) -> bool {
-        match decl.id() {
-            PropertyDeclarationId::Longhand(id) => !self.longhands.contains(id),
-            PropertyDeclarationId::Custom(..) => false,
-        }
-    }
-
-    /// Returns whether calling extend with `DeclarationPushMode::Update`
-    /// will cause this declaration block to change.
-    pub fn will_change_in_update_mode(
-        &self,
-        source_declarations: &SourcePropertyDeclaration,
-        importance: Importance,
-    ) -> bool {
-        // XXX The type of parameter seems to be necessary because otherwise
-        // the compiler complains about `decl` not living long enough in the
-        // all_shorthand expression. Why?
-        let needs_update = |decl: &_| {
-            if self.is_definitely_new(decl) {
-                return true;
-            }
-            self.declarations.iter().enumerate()
-                .find(|&(_, ref slot)| slot.id() == decl.id())
-                .map_or(true, |(i, slot)| {
-                    let important = self.declarations_importance[i];
-                    *slot != *decl || important != importance.important()
-                })
-        };
-        source_declarations.declarations.iter().any(&needs_update) ||
-            source_declarations.all_shorthand.declarations().any(|decl| needs_update(&decl))
+        decl.id().as_longhand().map_or(false, |id| !self.longhands.contains(id))
     }
 
     /// Adds or overrides the declaration for a given property in this block.
     ///
     /// See the documentation of `push` to see what impact `source` has when the
     /// property is already there.
-    ///
-    /// When calling with `DeclarationPushMode::Update`, this should not change
-    /// anything if `will_change_in_update_mode` returns false.
     pub fn extend(
         &mut self,
         mut drain: SourcePropertyDeclarationDrain,
         importance: Importance,
-        mode: DeclarationPushMode,
     ) -> bool {
-        match mode {
-            DeclarationPushMode::Parsing => {
-                let all_shorthand_len = match drain.all_shorthand {
-                    AllShorthand::NotSet => 0,
-                    AllShorthand::CSSWideKeyword(_) |
-                    AllShorthand::WithVariables(_) => shorthands::ALL_SHORTHAND_MAX_LEN,
-                };
-                let push_calls_count =
-                    drain.declarations.len() + all_shorthand_len;
+        let all_shorthand_len = match drain.all_shorthand {
+            AllShorthand::NotSet => 0,
+            AllShorthand::CSSWideKeyword(_) |
+            AllShorthand::WithVariables(_) => shorthands::ALL_SHORTHAND_MAX_LEN,
+        };
+        let push_calls_count =
+            drain.declarations.len() + all_shorthand_len;
 
-                // With deduplication the actual length increase may be less than this.
-                self.declarations.reserve(push_calls_count);
-            }
-            _ => {
-                // Don't bother reserving space, since it's usually the case
-                // that CSSOM just overrides properties and we don't need to use
-                // more memory. See bug 1424346 for an example where this
-                // matters.
-                //
-                // TODO: Would it be worth to call reserve() just if it's empty?
-            }
-        }
+        // With deduplication the actual length increase may be less than this.
+        self.declarations.reserve(push_calls_count);
 
         let mut changed = false;
         for decl in &mut drain.declarations {
-            changed |= self.push(
-                decl,
-                importance,
-                mode,
-            );
+            changed |= self.push(decl, importance);
         }
         drain.all_shorthand.declarations().fold(changed, |changed, decl| {
-            changed | self.push(decl, importance, mode)
+            changed | self.push(decl, importance)
         })
     }
 
     /// Adds or overrides the declaration for a given property in this block.
     ///
-    /// Depending on the value of `mode`, this has a different behavior in the
-    /// presence of another declaration with the same ID in the declaration
-    /// block.
+    /// Returns whether the declaration has changed.
     ///
-    /// Returns whether the declaration has changed.
+    /// This is only used for parsing and internal use.
     pub fn push(
         &mut self,
         declaration: PropertyDeclaration,
         importance: Importance,
-        mode: DeclarationPushMode,
     ) -> bool {
         if !self.is_definitely_new(&declaration) {
             let mut index_to_remove = None;
             for (i, slot) in self.declarations.iter_mut().enumerate() {
                 if slot.id() != declaration.id() {
                     continue;
                 }
 
-                if matches!(mode, DeclarationPushMode::Parsing) {
-                    let important = self.declarations_importance[i];
+                let important = self.declarations_importance[i];
 
-                    // For declarations from parsing, non-important declarations
-                    // shouldn't override existing important one.
-                    if important && !importance.important() {
-                        return false;
-                    }
+                // For declarations from parsing, non-important declarations
+                // shouldn't override existing important one.
+                if important && !importance.important() {
+                    return false;
+                }
 
-                    // As a compatibility hack, specially on Android,
-                    // don't allow to override a prefixed webkit display
-                    // value with an unprefixed version from parsing
-                    // code.
-                    //
-                    // TODO(emilio): Unship.
-                    if let PropertyDeclaration::Display(old_display) = *slot {
-                        use properties::longhands::display::computed_value::T as display;
+                // As a compatibility hack, specially on Android,
+                // don't allow to override a prefixed webkit display
+                // value with an unprefixed version from parsing
+                // code.
+                //
+                // TODO(emilio): Unship.
+                if let PropertyDeclaration::Display(old_display) = *slot {
+                    use properties::longhands::display::computed_value::T as display;
 
-                        if let PropertyDeclaration::Display(new_display) = declaration {
-                            if display::should_ignore_parsed_value(old_display, new_display) {
-                                return false;
-                            }
+                    if let PropertyDeclaration::Display(new_display) = declaration {
+                        if display::should_ignore_parsed_value(old_display, new_display) {
+                            return false;
                         }
                     }
                 }
-                if matches!(mode, DeclarationPushMode::Update) {
-                    let important = self.declarations_importance[i];
-                    if *slot == declaration && important == importance.important() {
-                        return false;
-                    }
-                    *slot = declaration;
-                    self.declarations_importance.set(i, importance.important());
-                    return true;
-                }
 
                 index_to_remove = Some(i);
                 break;
             }
 
             if let Some(index) = index_to_remove {
                 self.declarations.remove(index);
                 self.declarations_importance.remove(index);
@@ -591,16 +537,193 @@ impl PropertyDeclarationBlock {
         if let PropertyDeclarationId::Longhand(id) = declaration.id() {
             self.longhands.insert(id);
         }
         self.declarations.push(declaration);
         self.declarations_importance.push(importance.important());
         true
     }
 
+    /// Prepares updating this declaration block with the given
+    /// `SourcePropertyDeclaration` and importance, and returns whether
+    /// there is something to update.
+    pub fn prepare_for_update(
+        &self,
+        source_declarations: &SourcePropertyDeclaration,
+        importance: Importance,
+        updates: &mut SourcePropertyDeclarationUpdate,
+    ) -> bool {
+        debug_assert!(updates.updates.is_empty());
+        // Check whether we are updating for an all shorthand change.
+        if !matches!(source_declarations.all_shorthand, AllShorthand::NotSet) {
+            debug_assert!(source_declarations.declarations.is_empty());
+            return source_declarations.all_shorthand.declarations().any(|decl| {
+                self.is_definitely_new(&decl) ||
+                self.declarations.iter().enumerate()
+                    .find(|&(_, ref d)| d.id() == decl.id())
+                    .map_or(true, |(i, d)| {
+                        let important = self.declarations_importance[i];
+                        *d != decl || important != importance.important()
+                    })
+            });
+        }
+        // Fill `updates` with update information.
+        let mut any_update = false;
+        let new_count = &mut updates.new_count;
+        let any_removal = &mut updates.any_removal;
+        let updates = &mut updates.updates;
+        updates.extend(source_declarations.declarations.iter().map(|declaration| {
+            if self.is_definitely_new(declaration) {
+                return DeclarationUpdate::Append;
+            }
+            let longhand_id = declaration.id().as_longhand();
+            if let Some(longhand_id) = longhand_id {
+                if let Some(logical_group) = longhand_id.logical_group() {
+                    let mut needs_append = false;
+                    for (pos, decl) in self.declarations.iter().enumerate().rev() {
+                        let id = match decl.id().as_longhand() {
+                            Some(id) => id,
+                            None => continue,
+                        };
+                        if id == longhand_id {
+                            if needs_append {
+                                return DeclarationUpdate::AppendAndRemove { pos };
+                            }
+                            let important = self.declarations_importance[pos];
+                            if decl == declaration && important == importance.important() {
+                                return DeclarationUpdate::None;
+                            }
+                            return DeclarationUpdate::UpdateInPlace { pos };
+                        }
+                        if !needs_append &&
+                           id.logical_group() == Some(logical_group) &&
+                           id.is_logical() != longhand_id.is_logical() {
+                            needs_append = true;
+                        }
+                    }
+                    unreachable!("Longhand should be found in loop above");
+                }
+            }
+            self.declarations.iter().enumerate()
+                .find(|&(_, ref decl)| decl.id() == declaration.id())
+                .map_or(DeclarationUpdate::Append, |(pos, decl)| {
+                    let important = self.declarations_importance[pos];
+                    if decl == declaration && important == importance.important() {
+                        DeclarationUpdate::None
+                    } else {
+                        DeclarationUpdate::UpdateInPlace { pos }
+                    }
+                })
+        }).inspect(|update| {
+            if matches!(update, DeclarationUpdate::None) {
+                return;
+            }
+            any_update = true;
+            match update {
+                DeclarationUpdate::Append => {
+                    *new_count += 1;
+                }
+                DeclarationUpdate::AppendAndRemove { .. } => {
+                    *any_removal = true;
+                }
+                _ => {}
+            }
+        }));
+        any_update
+    }
+
+    /// Update this declaration block with the given data.
+    pub fn update(
+        &mut self,
+        drain: SourcePropertyDeclarationDrain,
+        importance: Importance,
+        updates: &mut SourcePropertyDeclarationUpdate,
+    ) {
+        let important = importance.important();
+        if !matches!(drain.all_shorthand, AllShorthand::NotSet) {
+            debug_assert!(updates.updates.is_empty());
+            for decl in drain.all_shorthand.declarations() {
+                if self.is_definitely_new(&decl) {
+                    let longhand_id = decl.id().as_longhand().unwrap();
+                    self.declarations.push(decl);
+                    self.declarations_importance.push(important);
+                    self.longhands.insert(longhand_id);
+                } else {
+                    let (idx, slot) = self.declarations.iter_mut()
+                        .enumerate().find(|&(_, ref d)| d.id() == decl.id()).unwrap();
+                    *slot = decl;
+                    self.declarations_importance.set(idx, important);
+                }
+            }
+            return;
+        }
+
+        self.declarations.reserve(updates.new_count);
+        if updates.any_removal {
+            // Prepare for removal and fixing update positions.
+            struct UpdateOrRemoval<'a> {
+                item: &'a mut DeclarationUpdate,
+                pos: usize,
+                remove: bool,
+            }
+            let mut updates_and_removals: SubpropertiesVec<UpdateOrRemoval> =
+                updates.updates.iter_mut().filter_map(|item| {
+                    let (pos, remove) = match *item {
+                        DeclarationUpdate::UpdateInPlace { pos } => (pos, false),
+                        DeclarationUpdate::AppendAndRemove { pos } => (pos, true),
+                        _ => return None,
+                    };
+                    Some(UpdateOrRemoval { item, pos, remove })
+                }).collect();
+            // Execute removals. It's important to do it in reverse index order,
+            // so that removing doesn't invalidate following positions.
+            updates_and_removals.sort_unstable_by_key(|update| update.pos);
+            updates_and_removals.iter().rev()
+                .filter(|update| update.remove)
+                .for_each(|update| {
+                    self.declarations.remove(update.pos);
+                    self.declarations_importance.remove(update.pos);
+                });
+            // Fixup pos field for updates.
+            let mut removed_count = 0;
+            for update in updates_and_removals.iter_mut() {
+                if update.remove {
+                    removed_count += 1;
+                    continue;
+                }
+                debug_assert_eq!(
+                    *update.item,
+                    DeclarationUpdate::UpdateInPlace { pos: update.pos }
+                );
+                *update.item = DeclarationUpdate::UpdateInPlace {
+                    pos: update.pos - removed_count
+                };
+            }
+        }
+        // Execute updates and appends.
+        for (decl, update) in drain.declarations.zip_eq(updates.updates.iter()) {
+            match *update {
+                DeclarationUpdate::None => {},
+                DeclarationUpdate::Append |
+                DeclarationUpdate::AppendAndRemove { .. } => {
+                    if let Some(id) = decl.id().as_longhand() {
+                        self.longhands.insert(id);
+                    }
+                    self.declarations.push(decl);
+                    self.declarations_importance.push(important);
+                }
+                DeclarationUpdate::UpdateInPlace { pos } => {
+                    self.declarations[pos] = decl;
+                    self.declarations_importance.set(pos, important);
+                }
+            }
+        }
+        updates.updates.clear();
+    }
+
     /// Returns the first declaration that would be removed by removing
     /// `property`.
     #[inline]
     pub fn first_declaration_to_remove(
         &self,
         property: &PropertyId,
     ) -> Option<usize> {
         if let Some(id) = property.longhand_id() {
@@ -1237,17 +1360,16 @@ pub fn parse_property_declaration_list(
     };
     let mut iter = DeclarationListParser::new(input, parser);
     while let Some(declaration) = iter.next() {
         match declaration {
             Ok(importance) => {
                 block.extend(
                     iter.parser.declarations.drain(),
                     importance,
-                    DeclarationPushMode::Parsing,
                 );
             }
             Err((error, slice)) => {
                 iter.parser.declarations.clear();
 
                 // If the unrecognized property looks like a vendor-specific property,
                 // silently ignore it instead of polluting the error output.
                 if let ParseErrorKind::Custom(StyleParseErrorKind::UnknownVendorProperty) = error.kind {
--- a/servo/components/style/properties/longhands/border.mako.rs
+++ b/servo/components/style/properties/longhands/border.mako.rs
@@ -22,41 +22,44 @@
     %>
     ${helpers.predefined_type(
         "border-%s-color" % side_name, "Color",
         "computed_value::T::currentcolor()",
         alias=maybe_moz_logical_alias(product, side, "-moz-border-%s-color"),
         spec=maybe_logical_spec(side, "color"),
         animation_value_type="AnimatedColor",
         logical=is_logical,
+        logical_group="border-color",
         allow_quirks=not is_logical,
         flags="APPLIES_TO_FIRST_LETTER",
         ignored_when_colors_disabled=True,
     )}
 
     ${helpers.predefined_type(
         "border-%s-style" % side_name, "BorderStyle",
         "specified::BorderStyle::None",
         alias=maybe_moz_logical_alias(product, side, "-moz-border-%s-style"),
         spec=maybe_logical_spec(side, "style"),
         flags="APPLIES_TO_FIRST_LETTER",
         animation_value_type="discrete" if not is_logical else "none",
         logical=is_logical,
+        logical_group="border-style",
         needs_context=False,
     )}
 
     ${helpers.predefined_type(
         "border-%s-width" % side_name,
         "BorderSideWidth",
         "::values::computed::NonNegativeLength::new(3.)",
         computed_type="::values::computed::NonNegativeLength",
         alias=maybe_moz_logical_alias(product, side, "-moz-border-%s-width"),
         spec=maybe_logical_spec(side, "width"),
         animation_value_type="NonNegativeLength",
         logical=is_logical,
+        logical_group="border-width",
         flags="APPLIES_TO_FIRST_LETTER GETCS_NEEDS_LAYOUT_FLUSH",
         allow_quirks=not is_logical,
         servo_restyle_damage="reflow rebuild_and_reflow_inline"
     )}
 % endfor
 
 ${helpers.gecko_keyword_conversion(Keyword('border-style',
                                    "none solid double dotted dashed hidden groove ridge inset outset"),
--- a/servo/components/style/properties/longhands/margin.mako.rs
+++ b/servo/components/style/properties/longhands/margin.mako.rs
@@ -15,14 +15,15 @@
     ${helpers.predefined_type(
         "margin-%s" % side[0],
         "LengthOrPercentageOrAuto",
         "computed::LengthOrPercentageOrAuto::Length(computed::Length::new(0.))",
         alias=maybe_moz_logical_alias(product, side, "-moz-margin-%s"),
         allow_quirks=not side[1],
         animation_value_type="ComputedValue",
         logical=side[1],
+        logical_group="margin",
         spec=spec,
         flags="APPLIES_TO_FIRST_LETTER GETCS_NEEDS_LAYOUT_FLUSH",
         allowed_in_page_rule=True,
         servo_restyle_damage="reflow"
     )}
 % endfor
--- a/servo/components/style/properties/longhands/padding.mako.rs
+++ b/servo/components/style/properties/longhands/padding.mako.rs
@@ -16,14 +16,15 @@
     %>
     ${helpers.predefined_type(
         "padding-%s" % side[0],
         "NonNegativeLengthOrPercentage",
         "computed::NonNegativeLengthOrPercentage::zero()",
         alias=maybe_moz_logical_alias(product, side, "-moz-padding-%s"),
         animation_value_type="NonNegativeLengthOrPercentage",
         logical=side[1],
+        logical_group="padding",
         spec=spec,
         flags="APPLIES_TO_FIRST_LETTER APPLIES_TO_PLACEHOLDER GETCS_NEEDS_LAYOUT_FLUSH",
         allow_quirks=not side[1],
         servo_restyle_damage="reflow rebuild_and_reflow_inline"
     )}
 % endfor
--- a/servo/components/style/properties/longhands/position.mako.rs
+++ b/servo/components/style/properties/longhands/position.mako.rs
@@ -13,30 +13,32 @@
     ${helpers.predefined_type(
         side,
         "LengthOrPercentageOrAuto",
         "computed::LengthOrPercentageOrAuto::Auto",
         spec="https://www.w3.org/TR/CSS2/visuren.html#propdef-%s" % side,
         flags="GETCS_NEEDS_LAYOUT_FLUSH",
         animation_value_type="ComputedValue",
         allow_quirks=True,
-        servo_restyle_damage="reflow_out_of_flow"
+        servo_restyle_damage="reflow_out_of_flow",
+        logical_group="inset",
     )}
 % endfor
 // inset-* logical properties, map to "top" / "left" / "bottom" / "right"
 % for side in LOGICAL_SIDES:
     ${helpers.predefined_type(
         "inset-%s" % side,
         "LengthOrPercentageOrAuto",
         "computed::LengthOrPercentageOrAuto::Auto",
         spec="https://drafts.csswg.org/css-logical-props/#propdef-inset-%s" % side,
         flags="GETCS_NEEDS_LAYOUT_FLUSH",
         alias="offset-%s:layout.css.offset-logical-properties.enabled" % side,
         animation_value_type="ComputedValue",
         logical=True,
+        logical_group="inset",
     )}
 % endfor
 
 #[cfg(feature = "gecko")]
 macro_rules! impl_align_conversions {
     ($name: path) => {
         impl From<u8> for $name {
             fn from(bits: u8) -> $name {
@@ -216,40 +218,43 @@ macro_rules! impl_align_conversions {
         %>
         // width, height, block-size, inline-size
         ${helpers.predefined_type(
             size,
             "MozLength",
             "computed::MozLength::auto()",
             parse_function,
             logical=logical,
+            logical_group="size",
             allow_quirks=not logical,
             spec=spec % size,
             animation_value_type="MozLength",
             flags="GETCS_NEEDS_LAYOUT_FLUSH",
             servo_restyle_damage="reflow"
         )}
         // min-width, min-height, min-block-size, min-inline-size,
         ${helpers.predefined_type(
             "min-%s" % size,
             "MozLength",
             "computed::MozLength::auto()",
             parse_function,
             logical=logical,
+            logical_group="min-size",
             allow_quirks=not logical,
             spec=spec % size,
             animation_value_type="MozLength",
             servo_restyle_damage = "reflow"
         )}
         ${helpers.predefined_type(
             "max-%s" % size,
             "MaxLength",
             "computed::MaxLength::none()",
             parse_function,
             logical=logical,
+            logical_group="max-size",
             allow_quirks=not logical,
             spec=spec % size,
             animation_value_type="MaxLength",
             servo_restyle_damage = "reflow"
         )}
     % else:
         // servo versions (no keyword support)
         ${helpers.predefined_type(size,
--- a/servo/components/style/properties/properties.mako.rs
+++ b/servo/components/style/properties/properties.mako.rs
@@ -7,16 +7,17 @@
 // Please note that valid Rust syntax may be mangled by the Mako parser.
 // For example, Vec<&Foo> will be mangled as Vec&Foo>. To work around these issues, the code
 // can be escaped. In the above example, Vec<<&Foo> or Vec< &Foo> achieves the desired result of Vec<&Foo>.
 
 <%namespace name="helpers" file="/helpers.mako.rs" />
 
 #[cfg(feature = "servo")]
 use app_units::Au;
+use arrayvec::{ArrayVec, Drain as ArrayVecDrain};
 use dom::TElement;
 use custom_properties::CustomPropertiesBuilder;
 use servo_arc::{Arc, UniqueArc};
 use smallbitvec::SmallBitVec;
 use std::borrow::Cow;
 use std::{ops, ptr};
 use std::cell::RefCell;
 use std::fmt::{self, Write};
@@ -51,16 +52,17 @@ use values::serialize_atom_name;
 use rule_tree::{CascadeLevel, StrongRuleNode};
 use self::computed_value_flags::*;
 use str::{CssString, CssStringBorrow, CssStringWriter};
 use style_adjuster::StyleAdjuster;
 
 pub use self::declaration_block::*;
 
 <%!
+    from collections import defaultdict
     from data import Method, Keyword, to_rust_ident, to_camel_case, SYSTEM_FONT_LONGHANDS
     import os.path
 %>
 
 #[path="${repr(os.path.join(os.path.dirname(__file__), 'computed_value_flags.rs'))[1:-1]}"]
 pub mod computed_value_flags;
 #[path="${repr(os.path.join(os.path.dirname(__file__), 'declaration_block.rs'))[1:-1]}"]
 pub mod declaration_block;
@@ -836,16 +838,39 @@ bitflags! {
          * they can be checked in the C++ side via ServoCSSPropList.h. */
         /// This property can be animated on the compositor.
         const CAN_ANIMATE_ON_COMPOSITOR = 0;
         /// This shorthand property is accessible from getComputedStyle.
         const SHORTHAND_IN_GETCS = 0;
     }
 }
 
+<%
+    logical_groups = defaultdict(list)
+    for prop in data.longhands:
+        if prop.logical_group:
+            logical_groups[prop.logical_group].append(prop)
+
+    for group, props in logical_groups.iteritems():
+        logical_count = sum(1 for p in props if p.logical)
+        if logical_count * 2 != len(props):
+            raise RuntimeError("Logical group {} has ".format(group) +
+                               "unbalanced logical / physical properties")
+%>
+
+/// A group for properties which may override each other
+/// via logical resolution.
+#[derive(Clone, Copy, Eq, Hash, PartialEq)]
+pub enum LogicalGroup {
+    % for group in logical_groups.iterkeys():
+    /// ${group}
+    ${to_camel_case(group)},
+    % endfor
+}
+
 /// An identifier for a given longhand property.
 #[derive(Clone, Copy, Eq, Hash, MallocSizeOf, PartialEq)]
 #[repr(u16)]
 pub enum LonghandId {
     % for i, property in enumerate(data.longhands):
         /// ${property.name}
         ${property.camel_case} = ${i},
     % endfor
@@ -988,29 +1013,49 @@ impl LonghandId {
     /// the given writing mode.
     ///
     /// Otherwise, return unchanged.
     #[inline]
     pub fn to_physical(&self, wm: WritingMode) -> Self {
         match *self {
             % for property in data.longhands:
             % if property.logical:
+                <% logical_group = property.logical_group %>
                 LonghandId::${property.camel_case} => {
                     <%helpers:logical_setter_helper name="${property.name}">
                     <%def name="inner(physical_ident)">
+                        <%
+                            physical_name = physical_ident.replace("_", "-")
+                            physical_property = data.longhands_by_name[physical_name]
+                            assert logical_group == physical_property.logical_group
+                        %>
                         LonghandId::${to_camel_case(physical_ident)}
                     </%def>
                     </%helpers:logical_setter_helper>
                 }
             % endif
             % endfor
             _ => *self
         }
     }
 
+    /// Return the logical group of this longhand property.
+    pub fn logical_group(&self) -> Option<LogicalGroup> {
+        const LOGICAL_GROUPS: [Option<LogicalGroup>; ${len(data.longhands)}] = [
+            % for prop in data.longhands:
+            % if prop.logical_group:
+            Some(LogicalGroup::${to_camel_case(prop.logical_group)}),
+            % else:
+            None,
+            % endif
+            % endfor
+        ];
+        LOGICAL_GROUPS[*self as usize]
+    }
+
     /// Returns PropertyFlags for given longhand property.
     pub fn flags(&self) -> PropertyFlags {
         match *self {
             % for property in data.longhands:
                 LonghandId::${property.camel_case} =>
                     % for flag in property.flags:
                         PropertyFlags::${flag} |
                     % endfor
@@ -1556,16 +1601,25 @@ impl<'a> PropertyDeclarationId<'a> {
             PropertyDeclarationId::Longhand(id) => id.name().into(),
             PropertyDeclarationId::Custom(name) => {
                 let mut s = String::new();
                 write!(&mut s, "--{}", name).unwrap();
                 s.into()
             }
         }
     }
+
+    /// Returns longhand id if it is, None otherwise.
+    #[inline]
+    pub fn as_longhand(&self) -> Option<LonghandId> {
+        match *self {
+            PropertyDeclarationId::Longhand(id) => Some(id),
+            _ => None,
+        }
+    }
 }
 
 /// Servo's representation of a CSS property, that is, either a longhand, a
 /// shorthand, or a custom property.
 #[derive(Clone, Eq, PartialEq)]
 pub enum PropertyId {
     /// A longhand property.
     Longhand(LonghandId),
@@ -2217,29 +2271,28 @@ impl PropertyDeclaration {
                         }
                     })
                 }
             }
         }
     }
 }
 
-const MAX_SUB_PROPERTIES_PER_SHORTHAND_EXCEPT_ALL: usize =
-    ${max(len(s.sub_properties) for s in data.shorthands_except_all())};
-
-type SourcePropertyDeclarationArray =
-    [PropertyDeclaration; MAX_SUB_PROPERTIES_PER_SHORTHAND_EXCEPT_ALL];
+type SubpropertiesArray<T> =
+    [T; ${max(len(s.sub_properties) for s in data.shorthands_except_all())}];
+
+type SubpropertiesVec<T> = ArrayVec<SubpropertiesArray<T>>;
 
 /// A stack-allocated vector of `PropertyDeclaration`
 /// large enough to parse one CSS `key: value` declaration.
 /// (Shorthands expand to multiple `PropertyDeclaration`s.)
 pub struct SourcePropertyDeclaration {
-    declarations: ::arrayvec::ArrayVec<SourcePropertyDeclarationArray>,
-
-    /// Stored separately to keep MAX_SUB_PROPERTIES_PER_SHORTHAND_EXCEPT_ALL smaller.
+    declarations: SubpropertiesVec<PropertyDeclaration>,
+
+    /// Stored separately to keep SubpropertiesVec smaller.
     all_shorthand: AllShorthand,
 }
 
 impl SourcePropertyDeclaration {
     /// Create one. It’s big, try not to move it around.
     #[inline]
     pub fn new() -> Self {
         SourcePropertyDeclaration {
@@ -2269,17 +2322,17 @@ impl SourcePropertyDeclaration {
     fn push(&mut self, declaration: PropertyDeclaration) {
         let _result = self.declarations.try_push(declaration);
         debug_assert!(_result.is_ok());
     }
 }
 
 /// Return type of SourcePropertyDeclaration::drain
 pub struct SourcePropertyDeclarationDrain<'a> {
-    declarations: ::arrayvec::Drain<'a, SourcePropertyDeclarationArray>,
+    declarations: ArrayVecDrain<'a, SubpropertiesArray<PropertyDeclaration>>,
     all_shorthand: AllShorthand,
 }
 
 enum AllShorthand {
     NotSet,
     CSSWideKeyword(CSSWideKeyword),
     WithVariables(Arc<UnparsedValue>)
 }
@@ -2687,21 +2740,16 @@ pub struct ComputedValues {
 }
 
 impl ComputedValues {
     /// Returns whether this style's display value is equal to contents.
     pub fn is_display_contents(&self) -> bool {
         self.get_box().clone_display().is_contents()
     }
 
-    /// Whether we're a visited style.
-    pub fn is_style_if_visited(&self) -> bool {
-        self.flags.contains(ComputedValueFlags::IS_STYLE_IF_VISITED)
-    }
-
     /// Gets a reference to the rule node. Panic if no rule node exists.
     pub fn rules(&self) -> &StrongRuleNode {
         self.rules.as_ref().unwrap()
     }
 
     /// Returns the visited style, if any.
     pub fn visited_style(&self) -> Option<<&ComputedValues> {
         self.visited_style.as_ref().map(|s| &**s)
@@ -3220,72 +3268,62 @@ pub struct StyleBuilder<'a> {
 
 impl<'a> StyleBuilder<'a> {
     /// Trivially construct a `StyleBuilder`.
     fn new(
         device: &'a Device,
         parent_style: Option<<&'a ComputedValues>,
         parent_style_ignoring_first_line: Option<<&'a ComputedValues>,
         pseudo: Option<<&'a PseudoElement>,
-        cascade_flags: CascadeFlags,
         rules: Option<StrongRuleNode>,
         custom_properties: Option<Arc<::custom_properties::CustomPropertiesMap>>,
-        visited_style: Option<Arc<ComputedValues>>,
     ) -> Self {
         debug_assert_eq!(parent_style.is_some(), parent_style_ignoring_first_line.is_some());
         #[cfg(feature = "gecko")]
         debug_assert!(parent_style.is_none() ||
                       ::std::ptr::eq(parent_style.unwrap(),
                                      parent_style_ignoring_first_line.unwrap()) ||
                       parent_style.unwrap().pseudo() == Some(PseudoElement::FirstLine));
         let reset_style = device.default_computed_values();
         let inherited_style = parent_style.unwrap_or(reset_style);
         let inherited_style_ignoring_first_line = parent_style_ignoring_first_line.unwrap_or(reset_style);
-        // FIXME(bz): INHERIT_ALL seems like a fundamentally broken idea.  I'm
+        // FIXME(bz): inherits_all seems like a fundamentally broken idea.  I'm
         // 99% sure it should give incorrect behavior for table anonymous box
         // backgrounds, for example.  This code doesn't attempt to make it play
         // nice with inherited_style_ignoring_first_line.
-        let reset_style = if cascade_flags.contains(CascadeFlags::INHERIT_ALL) {
+        let reset_style = if pseudo.map_or(false, |p| p.inherits_all()) {
             inherited_style
         } else {
             reset_style
         };
 
-        let mut flags = inherited_style.flags.inherited();
-        if cascade_flags.contains(CascadeFlags::VISITED_DEPENDENT_ONLY) {
-            flags.insert(ComputedValueFlags::IS_STYLE_IF_VISITED);
-        }
+        let flags = inherited_style.flags.inherited();
 
         StyleBuilder {
             device,
             inherited_style,
             inherited_style_ignoring_first_line,
             reset_style,
             pseudo,
             rules,
             modified_reset: false,
             custom_properties,
             writing_mode: inherited_style.writing_mode,
             flags,
-            visited_style,
+            visited_style: None,
             % for style_struct in data.active_style_structs():
             % if style_struct.inherited:
             ${style_struct.ident}: StyleStructRef::Borrowed(inherited_style.${style_struct.name_lower}_arc()),
             % else:
             ${style_struct.ident}: StyleStructRef::Borrowed(reset_style.${style_struct.name_lower}_arc()),
             % endif
             % endfor
         }
     }
 
-    /// Whether we're a visited style.
-    pub fn is_style_if_visited(&self) -> bool {
-        self.flags.contains(ComputedValueFlags::IS_STYLE_IF_VISITED)
-    }
-
     /// NOTE(emilio): This is done so we can compute relative units with respect
     /// to the parent style, but all the early properties / writing-mode / etc
     /// are already set to the right ones on the kid.
     ///
     /// Do _not_ actually call this to construct a style, this should mostly be
     /// used for animations.
     pub fn for_animation(
         device: &'a Device,
@@ -3425,26 +3463,26 @@ impl<'a> StyleBuilder<'a> {
             parent.visited_style().map(|style| {
                 Self::for_inheritance(
                     device,
                     Some(style),
                     pseudo,
                 ).build()
             })
         });
-        Self::new(
+        let mut ret = Self::new(
             device,
             parent,
             parent,
             pseudo,
-            CascadeFlags::empty(),
             /* rules = */ None,
             parent.and_then(|p| p.custom_properties().cloned()),
-            visited_style,
-        )
+        );
+        ret.visited_style = visited_style;
+        ret
     }
 
     /// Returns whether we have a visited style.
     pub fn has_visited_style(&self) -> bool {
         self.visited_style.is_some()
     }
 
     /// Returns whether we're a pseudo-elements style.
@@ -3645,28 +3683,16 @@ pub type CascadePropertyFn =
 /// A per-longhand array of functions to perform the CSS cascade on each of
 /// them, effectively doing virtual dispatch.
 static CASCADE_PROPERTY: [CascadePropertyFn; ${len(data.longhands)}] = [
     % for property in data.longhands:
         longhands::${property.ident}::cascade_property,
     % endfor
 ];
 
-bitflags! {
-    /// A set of flags to tweak the behavior of the `cascade` function.
-    pub struct CascadeFlags: u8 {
-        /// Whether to inherit all styles from the parent. If this flag is not
-        /// present, non-inherited styles are reset to their initial values.
-        const INHERIT_ALL = 1;
-
-        /// Whether to only cascade properties that are visited dependent.
-        const VISITED_DEPENDENT_ONLY = 1 << 1;
-    }
-}
-
 /// Performs the CSS cascade, computing new styles for an element from its parent style.
 ///
 /// The arguments are:
 ///
 ///   * `device`: Used to get the initial viewport and other external state.
 ///
 ///   * `rule_node`: The rule node in the tree that represent the CSS rules that
 ///   matched.
@@ -3679,56 +3705,86 @@ bitflags! {
 pub fn cascade<E>(
     device: &Device,
     pseudo: Option<<&PseudoElement>,
     rule_node: &StrongRuleNode,
     guards: &StylesheetGuards,
     parent_style: Option<<&ComputedValues>,
     parent_style_ignoring_first_line: Option<<&ComputedValues>,
     layout_parent_style: Option<<&ComputedValues>,
-    visited_style: Option<Arc<ComputedValues>>,
+    visited_rules: Option<<&StrongRuleNode>,
     font_metrics_provider: &FontMetricsProvider,
-    flags: CascadeFlags,
+    quirks_mode: QuirksMode,
+    rule_cache: Option<<&RuleCache>,
+    rule_cache_conditions: &mut RuleCacheConditions,
+    element: Option<E>,
+) -> Arc<ComputedValues>
+where
+    E: TElement,
+{
+    cascade_rules(
+        device,
+        pseudo,
+        rule_node,
+        guards,
+        parent_style,
+        parent_style_ignoring_first_line,
+        layout_parent_style,
+        font_metrics_provider,
+        CascadeMode::Unvisited { visited_rules },
+        quirks_mode,
+        rule_cache,
+        rule_cache_conditions,
+        element,
+    )
+}
+
+fn cascade_rules<E>(
+    device: &Device,
+    pseudo: Option<<&PseudoElement>,
+    rule_node: &StrongRuleNode,
+    guards: &StylesheetGuards,
+    parent_style: Option<<&ComputedValues>,
+    parent_style_ignoring_first_line: Option<<&ComputedValues>,
+    layout_parent_style: Option<<&ComputedValues>,
+    font_metrics_provider: &FontMetricsProvider,
+    cascade_mode: CascadeMode,
     quirks_mode: QuirksMode,
     rule_cache: Option<<&RuleCache>,
     rule_cache_conditions: &mut RuleCacheConditions,
     element: Option<E>,
 ) -> Arc<ComputedValues>
 where
     E: TElement,
 {
     debug_assert_eq!(parent_style.is_some(), parent_style_ignoring_first_line.is_some());
     let empty = SmallBitVec::new();
-
-    let property_restriction = pseudo.and_then(|p| p.property_restriction());
-
+    let restriction = pseudo.and_then(|p| p.property_restriction());
     let iter_declarations = || {
         rule_node.self_and_ancestors().flat_map(|node| {
             let cascade_level = node.cascade_level();
-            let source = node.style_source();
-
-            let declarations = if source.is_some() {
-                source.as_ref().unwrap().read(cascade_level.guard(guards)).declaration_importance_iter()
-            } else {
-                // The root node has no style source.
-                DeclarationImportanceIterator::new(&[], &empty)
+            let node_importance = node.importance();
+            let declarations = match node.style_source() {
+                Some(source) => {
+                    source.read(cascade_level.guard(guards)).declaration_importance_iter()
+                }
+                None => DeclarationImportanceIterator::new(&[], &empty),
             };
-            let node_importance = node.importance();
 
             declarations
                 // Yield declarations later in source order (with more precedence) first.
                 .rev()
                 .filter_map(move |(declaration, declaration_importance)| {
-                    if let Some(property_restriction) = property_restriction {
+                    if let Some(restriction) = restriction {
                         // declaration.id() is either a longhand or a custom
                         // property.  Custom properties are always allowed, but
                         // longhands are only allowed if they have our
-                        // property_restriction flag set.
+                        // restriction flag set.
                         if let PropertyDeclarationId::Longhand(id) = declaration.id() {
-                            if !id.flags().contains(property_restriction) {
+                            if !id.flags().contains(restriction) {
                                 return None
                             }
                         }
                     }
 
                     if declaration_importance == node_importance {
                         Some((declaration, cascade_level))
                     } else {
@@ -3742,40 +3798,54 @@ where
         device,
         pseudo,
         rule_node,
         guards,
         iter_declarations,
         parent_style,
         parent_style_ignoring_first_line,
         layout_parent_style,
-        visited_style,
         font_metrics_provider,
-        flags,
+        cascade_mode,
         quirks_mode,
         rule_cache,
         rule_cache_conditions,
         element,
     )
 }
 
+/// Whether we're cascading for visited or unvisited styles.
+#[derive(Clone, Copy)]
+pub enum CascadeMode<'a> {
+    /// We're cascading for unvisited styles.
+    Unvisited {
+        /// The visited rules that should match the visited style.
+        visited_rules: Option<<&'a StrongRuleNode>,
+    },
+    /// We're cascading for visited styles.
+    Visited {
+        /// The writing mode of our unvisited style, needed to correctly resolve
+        /// logical properties..
+        writing_mode: WritingMode,
+    },
+}
+
 /// NOTE: This function expects the declaration with more priority to appear
 /// first.
 pub fn apply_declarations<'a, E, F, I>(
     device: &Device,
     pseudo: Option<<&PseudoElement>,
     rules: &StrongRuleNode,
     guards: &StylesheetGuards,
     iter_declarations: F,
     parent_style: Option<<&ComputedValues>,
     parent_style_ignoring_first_line: Option<<&ComputedValues>,
     layout_parent_style: Option<<&ComputedValues>,
-    visited_style: Option<Arc<ComputedValues>>,
     font_metrics_provider: &FontMetricsProvider,
-    flags: CascadeFlags,
+    cascade_mode: CascadeMode,
     quirks_mode: QuirksMode,
     rule_cache: Option<<&RuleCache>,
     rule_cache_conditions: &mut RuleCacheConditions,
     element: Option<E>,
 ) -> Arc<ComputedValues>
 where
     E: TElement,
     F: Fn() -> I,
@@ -3783,26 +3853,18 @@ where
 {
     debug_assert!(layout_parent_style.is_none() || parent_style.is_some());
     debug_assert_eq!(parent_style.is_some(), parent_style_ignoring_first_line.is_some());
     #[cfg(feature = "gecko")]
     debug_assert!(parent_style.is_none() ||
                   ::std::ptr::eq(parent_style.unwrap(),
                                  parent_style_ignoring_first_line.unwrap()) ||
                   parent_style.unwrap().pseudo() == Some(PseudoElement::FirstLine));
-    let (inherited_style, layout_parent_style) = match parent_style {
-        Some(parent_style) => {
-            (parent_style,
-             layout_parent_style.unwrap_or(parent_style))
-        },
-        None => {
-            (device.default_computed_values(),
-             device.default_computed_values())
-        }
-    };
+    let inherited_style =
+        parent_style.unwrap_or(device.default_computed_values());
 
     let custom_properties = {
         let mut builder =
             CustomPropertiesBuilder::new(inherited_style.custom_properties());
 
         for (declaration, _cascade_level) in iter_declarations() {
             if let PropertyDeclaration::Custom(ref declaration) = *declaration {
                 builder.cascade(&declaration.name, declaration.value.borrow());
@@ -3817,20 +3879,18 @@ where
         // We'd really like to own the rules here to avoid refcount traffic, but
         // animation's usage of `apply_declarations` make this tricky. See bug
         // 1375525.
         builder: StyleBuilder::new(
             device,
             parent_style,
             parent_style_ignoring_first_line,
             pseudo,
-            flags,
             Some(rules.clone()),
             custom_properties,
-            visited_style,
         ),
         cached_system_font: None,
         in_media_query: false,
         for_smil_animation: false,
         for_non_inherited_property: None,
         font_metrics_provider,
         quirks_mode,
         rule_cache_conditions: RefCell::new(rule_cache_conditions),
@@ -3882,17 +3942,17 @@ where
             let physical_longhand_id = longhand_id ${maybe_to_physical};
             if seen.contains(physical_longhand_id) {
                 continue
             }
 
             // Only a few properties are allowed to depend on the visited state
             // of links.  When cascading visited styles, we can save time by
             // only processing these properties.
-            if flags.contains(CascadeFlags::VISITED_DEPENDENT_ONLY) &&
+            if matches!(cascade_mode, CascadeMode::Visited { .. }) &&
                !physical_longhand_id.is_visited_dependent() {
                 continue
             }
 
             let mut declaration = match *declaration {
                 PropertyDeclaration::WithVariables(ref declaration) => {
                     if !declaration.id.inherited() {
                         context.rule_cache_conditions.borrow_mut()
@@ -3947,19 +4007,57 @@ where
                     continue;
                 }
             % endif
 
             let discriminant = longhand_id as usize;
             (CASCADE_PROPERTY[discriminant])(&*declaration, &mut context);
         }
         % if category_to_cascade_now == "early":
-            let writing_mode =
-                WritingMode::new(context.builder.get_inherited_box());
+            let writing_mode = match cascade_mode {
+                CascadeMode::Unvisited { .. } => {
+                    WritingMode::new(context.builder.get_inherited_box())
+                }
+                CascadeMode::Visited { writing_mode } => writing_mode,
+            };
+
             context.builder.writing_mode = writing_mode;
+            if let CascadeMode::Unvisited { visited_rules: Some(visited_rules) } = cascade_mode {
+                let is_link = pseudo.is_none() && element.unwrap().is_link();
+                macro_rules! visited_parent {
+                    ($parent:expr) => {
+                        if is_link {
+                            $parent
+                        } else {
+                            $parent.map(|p| p.visited_style().unwrap_or(p))
+                        }
+                    }
+                }
+                // We could call apply_declarations directly, but that'd cause
+                // another instantiation of this function which is not great.
+                context.builder.visited_style = Some(cascade_rules(
+                    device,
+                    pseudo,
+                    visited_rules,
+                    guards,
+                    visited_parent!(parent_style),
+                    visited_parent!(parent_style_ignoring_first_line),
+                    visited_parent!(layout_parent_style),
+                    font_metrics_provider,
+                    CascadeMode::Visited { writing_mode },
+                    quirks_mode,
+                    // The rule cache doesn't care about caching :visited
+                    // styles, we cache the unvisited style instead. We still do
+                    // need to set the caching dependencies properly if present
+                    // though, so the cache conditions need to match.
+                    /* rule_cache = */ None,
+                    &mut *context.rule_cache_conditions.borrow_mut(),
+                    element,
+                ));
+            }
 
             let mut _skip_font_family = false;
 
             % if product == "gecko":
 
                 // <svg:text> is not affected by text zoom, and it uses a preshint to
                 // disable it. We fix up the struct when this happens by unzooming
                 // its contained font values, which will have been zoomed in the parent
@@ -4092,31 +4190,31 @@ where
            seen.contains(LonghandId::FontStretch) ||
            seen.contains(LonghandId::FontFamily) {
             builder.mutate_font().compute_font_hash();
         }
     % endif
 
     builder.clear_modified_reset();
 
-    StyleAdjuster::new(&mut builder).adjust(
-        layout_parent_style,
-        element,
-        flags,
-    );
+    if matches!(cascade_mode, CascadeMode::Unvisited { .. }) {
+        StyleAdjuster::new(&mut builder).adjust(
+            layout_parent_style.unwrap_or(inherited_style),
+            element,
+        );
+    }
 
     if builder.modified_reset() || !apply_reset {
         // If we adjusted any reset structs, we can't cache this ComputedValues.
         //
         // Also, if we re-used existing reset structs, don't bother caching it
         // back again. (Aside from being wasted effort, it will be wrong, since
         // context.rule_cache_conditions won't be set appropriately if we
         // didn't compute those reset properties.)
-        context.rule_cache_conditions.borrow_mut()
-            .set_uncacheable();
+        context.rule_cache_conditions.borrow_mut().set_uncacheable();
     }
 
     builder.build()
 }
 
 /// See StyleAdjuster::adjust_for_border_width.
 pub fn adjust_border_width(style: &mut StyleBuilder) {
     % for side in ["top", "right", "bottom", "left"]:
--- a/servo/components/style/rule_cache.rs
+++ b/servo/components/style/rule_cache.rs
@@ -119,21 +119,16 @@ impl RuleCache {
     ///
     /// This needs to receive a `StyleBuilder` with the `early` properties
     /// already applied.
     pub fn find(
         &self,
         guards: &StylesheetGuards,
         builder_with_early_props: &StyleBuilder,
     ) -> Option<&ComputedValues> {
-        if builder_with_early_props.is_style_if_visited() {
-            // FIXME(emilio): We can probably do better, does it matter much?
-            return None;
-        }
-
         // A pseudo-element with property restrictions can result in different
         // computed values if it's also used for a non-pseudo.
         if builder_with_early_props
             .pseudo
             .and_then(|p| p.property_restriction())
             .is_some()
         {
             return None;
@@ -161,21 +156,16 @@ impl RuleCache {
         style: &Arc<ComputedValues>,
         pseudo: Option<&PseudoElement>,
         conditions: &RuleCacheConditions,
     ) -> bool {
         if !conditions.cacheable() {
             return false;
         }
 
-        if style.is_style_if_visited() {
-            // FIXME(emilio): We can probably do better, does it matter much?
-            return false;
-        }
-
         // A pseudo-element with property restrictions can result in different
         // computed values if it's also used for a non-pseudo.
         if pseudo.and_then(|p| p.property_restriction()).is_some() {
             return false;
         }
 
         let rules = style.rules.as_ref();
         let rules = match Self::get_rule_node_for_cache(guards, rules) {
--- a/servo/components/style/servo/selector_parser.rs
+++ b/servo/components/style/servo/selector_parser.rs
@@ -218,42 +218,42 @@ impl PseudoElement {
     /// For most (but not all) anon-boxes, we inherit all values from the
     /// parent, this is the hook in the style system to allow this.
     ///
     /// FIXME(emilio): It's likely that this is broken in a variety of
     /// situations, and what it really wants is just inherit some reset
     /// properties...  Also, I guess it just could do all: inherit on the
     /// stylesheet, though chances are that'd be kinda slow if we don't cache
     /// them...
-    pub fn cascade_flags(&self) -> CascadeFlags {
+    pub fn inherits_all(&self) -> bool {
         match *self {
             PseudoElement::After |
             PseudoElement::Before |
             PseudoElement::Selection |
             PseudoElement::DetailsContent |
-            PseudoElement::DetailsSummary => CascadeFlags::empty(),
+            PseudoElement::DetailsSummary |
             // Anonymous table flows shouldn't inherit their parents properties in order
             // to avoid doubling up styles such as transformations.
             PseudoElement::ServoAnonymousTableCell |
             PseudoElement::ServoAnonymousTableRow |
             PseudoElement::ServoText |
-            PseudoElement::ServoInputText => CascadeFlags::empty(),
+            PseudoElement::ServoInputText => false,
 
             // For tables, we do want style to inherit, because TableWrapper is
             // responsible for handling clipping and scrolling, while Table is
             // responsible for creating stacking contexts.
             //
             // StackingContextCollectionFlags makes sure this is processed
             // properly.
             PseudoElement::ServoAnonymousTable |
             PseudoElement::ServoAnonymousTableWrapper |
             PseudoElement::ServoTableWrapper |
             PseudoElement::ServoAnonymousBlock |
             PseudoElement::ServoInlineBlockWrapper |
-            PseudoElement::ServoInlineAbsolute => CascadeFlags::INHERIT_ALL,
+            PseudoElement::ServoInlineAbsolute => true,
         }
     }
 
     /// Covert non-canonical pseudo-element to canonical one, and keep a
     /// canonical one as it is.
     pub fn canonical(&self) -> PseudoElement {
         self.clone()
     }
--- a/servo/components/style/style_adjuster.rs
+++ b/servo/components/style/style_adjuster.rs
@@ -2,17 +2,17 @@
  * 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/. */
 
 //! A struct to encapsulate all the style fixups and flags propagations
 //! a computed style needs in order for it to adhere to the CSS spec.
 
 use app_units::Au;
 use dom::TElement;
-use properties::{self, CascadeFlags, ComputedValues, StyleBuilder};
+use properties::{self, ComputedValues, StyleBuilder};
 use properties::computed_value_flags::ComputedValueFlags;
 use properties::longhands::display::computed_value::T as Display;
 use properties::longhands::float::computed_value::T as Float;
 use properties::longhands::overflow_x::computed_value::T as Overflow;
 use properties::longhands::position::computed_value::T as Position;
 
 /// A struct that implements all the adjustment methods.
 ///
@@ -676,17 +676,16 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
     ///
     /// When comparing to Gecko, this is similar to the work done by
     /// `ComputedStyle::ApplyStyleFixups`, plus some parts of
     /// `nsStyleSet::GetContext`.
     pub fn adjust<E>(
         &mut self,
         layout_parent_style: &ComputedValues,
         element: Option<E>,
-        flags: CascadeFlags,
     ) where
         E: TElement,
     {
         if cfg!(debug_assertions) {
             if element
                 .and_then(|e| e.implemented_pseudo_element())
                 .is_some()
             {
@@ -700,25 +699,16 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
         // animation, and the font stuff for Gecko
         // (Stylist::compute_for_declarations) should pass an element to
         // cascade(), then we can make this assertion hold everywhere.
         // debug_assert!(
         //     element.is_some() || self.style.pseudo.is_some(),
         //     "Should always have an element around for non-pseudo styles"
         // );
 
-        // Don't adjust visited styles, visited-dependent properties aren't
-        // affected by these adjustments and it'd be just wasted work anyway.
-        //
-        // It also doesn't make much sense to adjust them, since we don't
-        // cascade most properties anyway, and they wouldn't be looked up.
-        if flags.contains(CascadeFlags::VISITED_DEPENDENT_ONLY) {
-            return;
-        }
-
         self.adjust_for_visited(element);
         #[cfg(feature = "gecko")]
         {
             self.adjust_for_prohibited_display_contents(element);
             self.adjust_for_fieldset_content(layout_parent_style);
         }
         self.adjust_for_top_layer();
         self.blockify_if_necessary(layout_parent_style, element);
--- a/servo/components/style/stylesheets/keyframes_rule.rs
+++ b/servo/components/style/stylesheets/keyframes_rule.rs
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Keyframes: https://drafts.csswg.org/css-animations/#keyframes
 
 use cssparser::{AtRuleParser, CowRcStr, Parser, ParserInput, QualifiedRuleParser, RuleListParser};
 use cssparser::{parse_one_rule, DeclarationListParser, DeclarationParser, SourceLocation, Token};
 use error_reporting::ContextualParseError;
 use parser::ParserContext;
-use properties::{DeclarationPushMode, Importance, PropertyDeclaration};
+use properties::{Importance, PropertyDeclaration};
 use properties::{LonghandId, PropertyDeclarationBlock, PropertyId};
 use properties::{PropertyDeclarationId, SourcePropertyDeclaration};
 use properties::LonghandIdSet;
 use properties::longhands::transition_timing_function::single_value::SpecifiedValue as SpecifiedTimingFunction;
 use servo_arc::Arc;
 use shared_lock::{DeepCloneParams, DeepCloneWithLock, SharedRwLock, SharedRwLockReadGuard};
 use shared_lock::{Locked, ToCssWithGuard};
 use std::fmt::{self, Write};
@@ -549,17 +549,16 @@ impl<'a, 'i> QualifiedRuleParser<'i> for
         let mut iter = DeclarationListParser::new(input, parser);
         let mut block = PropertyDeclarationBlock::new();
         while let Some(declaration) = iter.next() {
             match declaration {
                 Ok(()) => {
                     block.extend(
                         iter.parser.declarations.drain(),
                         Importance::Normal,
-                        DeclarationPushMode::Parsing,
                     );
                 },
                 Err((error, slice)) => {
                     iter.parser.declarations.clear();
                     let location = error.location;
                     let error =
                         ContextualParseError::UnsupportedKeyframePropertyDeclaration(slice, error);
                     context.log_css_error(location, error);
--- a/servo/components/style/stylist.rs
+++ b/servo/components/style/stylist.rs
@@ -15,17 +15,17 @@ use gecko_bindings::structs::{ServoStyle
 use hashglobe::FailedAllocationError;
 use invalidation::element::invalidation_map::InvalidationMap;
 use invalidation::media_queries::{EffectiveMediaQueryResults, ToMediaListKey};
 #[cfg(feature = "gecko")]
 use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps};
 #[cfg(feature = "gecko")]
 use malloc_size_of::MallocUnconditionalShallowSizeOf;
 use media_queries::Device;
-use properties::{self, CascadeFlags, ComputedValues};
+use properties::{self, CascadeMode, ComputedValues};
 use properties::{AnimationRules, PropertyDeclarationBlock};
 use rule_cache::{RuleCache, RuleCacheConditions};
 use rule_tree::{CascadeLevel, RuleTree, ShadowCascadeOrder, StrongRuleNode, StyleSource};
 use selector_map::{PrecomputedHashMap, SelectorMap, SelectorMapEntry};
 use selector_parser::{PerPseudoElementMap, PseudoElement, SelectorImpl, SnapshotMap};
 use selectors::NthIndexCache;
 use selectors::attr::{CaseSensitivity, NamespaceConstraint};
 use selectors::bloom::{BloomFilter, NonCountingBloomFilter};
@@ -840,83 +840,45 @@ impl Stylist {
         rule_cache: Option<&RuleCache>,
         rule_cache_conditions: &mut RuleCacheConditions,
     ) -> Arc<ComputedValues>
     where
         E: TElement,
     {
         debug_assert!(pseudo.is_some() || element.is_some(), "Huh?");
 
-        let cascade_flags = pseudo.map_or(CascadeFlags::empty(), |p| p.cascade_flags());
-
         // We need to compute visited values if we have visited rules or if our
         // parent has visited values.
-        let mut visited_values = None;
-        if inputs.visited_rules.is_some() || parent_style.and_then(|s| s.visited_style()).is_some()
-        {
-            // At this point inputs may have visited rules, or rules.
-            let rule_node = match inputs.visited_rules.as_ref() {
-                Some(rules) => rules,
-                None => inputs.rules.as_ref().unwrap_or(self.rule_tree.root()),
-            };
-
-            let inherited_style;
-            let inherited_style_ignoring_first_line;
-            let layout_parent_style_for_visited;
-            if pseudo.is_none() && element.unwrap().is_link() {
-                // We just want to use our parent style as our parent.
-                inherited_style = parent_style;
-                inherited_style_ignoring_first_line = parent_style_ignoring_first_line;
-                layout_parent_style_for_visited = layout_parent_style;
-            } else {
-                // We want to use the visited bits (if any) from our parent
-                // style as our parent.
-                inherited_style = parent_style
-                    .map(|parent_style| parent_style.visited_style().unwrap_or(parent_style));
-                inherited_style_ignoring_first_line = parent_style_ignoring_first_line
-                    .map(|parent_style| parent_style.visited_style().unwrap_or(parent_style));
-                layout_parent_style_for_visited = layout_parent_style
-                    .map(|parent_style| parent_style.visited_style().unwrap_or(parent_style));
+        let visited_rules = match inputs.visited_rules.as_ref() {
+            Some(rules) => Some(rules),
+            None => {
+                if parent_style.and_then(|s| s.visited_style()).is_some() {
+                    Some(inputs.rules.as_ref().unwrap_or(self.rule_tree.root()))
+                } else {
+                    None
+                }
             }
-
-            visited_values = Some(properties::cascade::<E>(
-                &self.device,
-                pseudo,
-                rule_node,
-                guards,
-                inherited_style,
-                inherited_style_ignoring_first_line,
-                layout_parent_style_for_visited,
-                None,
-                font_metrics,
-                cascade_flags | CascadeFlags::VISITED_DEPENDENT_ONLY,
-                self.quirks_mode,
-                rule_cache,
-                rule_cache_conditions,
-                element,
-            ));
-        }
+        };
 
         // Read the comment on `precomputed_values_for_pseudo` to see why it's
         // difficult to assert that display: contents nodes never arrive here
         // (tl;dr: It doesn't apply for replaced elements and such, but the
         // computed value is still "contents").
         //
         // FIXME(emilio): We should assert that it holds if pseudo.is_none()!
         properties::cascade::<E>(
             &self.device,
             pseudo,
             inputs.rules.as_ref().unwrap_or(self.rule_tree.root()),
             guards,
             parent_style,
             parent_style_ignoring_first_line,
             layout_parent_style,
-            visited_values,
+            visited_rules,
             font_metrics,
-            cascade_flags,
             self.quirks_mode,
             rule_cache,
             rule_cache_conditions,
             element,
         )
     }
 
     /// Computes the cascade inputs for a lazily-cascaded pseudo-element.
@@ -1576,19 +1538,18 @@ impl Stylist {
             &self.device,
             /* pseudo = */ None,
             self.rule_tree.root(),
             guards,
             iter_declarations,
             Some(parent_style),
             Some(parent_style),
             Some(parent_style),
-            None,
             &metrics,
-            CascadeFlags::empty(),
+            CascadeMode::Unvisited { visited_rules: None },
             self.quirks_mode,
             /* rule_cache = */ None,
             &mut Default::default(),
             /* element = */ None,
         )
     }
 
     /// Accessor for a shared reference to the device.
--- a/servo/ports/geckolib/glue.rs
+++ b/servo/ports/geckolib/glue.rs
@@ -122,17 +122,17 @@ use style::gecko_bindings::structs::nsTA
 use style::gecko_bindings::structs::nsresult;
 use style::gecko_bindings::sugar::ownership::{FFIArcHelpers, HasFFI, HasArcFFI};
 use style::gecko_bindings::sugar::ownership::{HasSimpleFFI, Strong};
 use style::gecko_bindings::sugar::refptr::RefPtr;
 use style::gecko_properties;
 use style::invalidation::element::restyle_hints;
 use style::media_queries::MediaList;
 use style::parser::{Parse, ParserContext, self};
-use style::properties::{ComputedValues, DeclarationPushMode, Importance};
+use style::properties::{ComputedValues, Importance};
 use style::properties::{LonghandId, LonghandIdSet, PropertyDeclarationBlock, PropertyId};
 use style::properties::{PropertyDeclarationId, ShorthandId};
 use style::properties::{SourcePropertyDeclaration, StyleBuilder};
 use style::properties::{parse_one_declaration_into, parse_style_attribute};
 use style::properties::animated_properties::AnimationValue;
 use style::properties::animated_properties::compare_property_priority;
 use style::rule_cache::RuleCacheConditions;
 use style::rule_tree::{CascadeLevel, StrongRuleNode};
@@ -3271,17 +3271,16 @@ pub extern "C" fn Servo_ParseProperty(
 
     match result {
         Ok(()) => {
             let global_style_data = &*GLOBAL_STYLE_DATA;
             let mut block = PropertyDeclarationBlock::new();
             block.extend(
                 declarations.drain(),
                 Importance::Normal,
-                DeclarationPushMode::Append,
             );
             Arc::new(global_style_data.shared_lock.wrap(block)).into_strong()
         }
         Err(_) => RawServoDeclarationBlockStrong::null()
     }
 }
 
 #[no_mangle]
@@ -3579,41 +3578,28 @@ fn set_property(
         reporter.as_ref().map(|r| r as &ParseErrorReporter),
     );
 
     if result.is_err() {
         return false;
     }
 
     let importance = if is_important { Importance::Important } else { Importance::Normal };
-    let append_only = unsafe {
-        structs::StaticPrefs_sVarCache_layout_css_property_append_only
-    };
-    let mode = if append_only {
-        DeclarationPushMode::Append
-    } else {
-        let will_change = read_locked_arc(declarations, |decls: &PropertyDeclarationBlock| {
-            decls.will_change_in_update_mode(&source_declarations, importance)
-        });
-        if !will_change {
-            return false;
-        }
-        DeclarationPushMode::Update
-    };
+    let mut updates = Default::default();
+    let will_change = read_locked_arc(declarations, |decls: &PropertyDeclarationBlock| {
+        decls.prepare_for_update(&source_declarations, importance, &mut updates)
+    });
+    if !will_change {
+        return false;
+    }
 
     before_change_closure.invoke();
-
-    let result = write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.extend(
-            source_declarations.drain(),
-            importance,
-            mode,
-        )
+    write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
+        decls.update(source_declarations.drain(), importance, &mut updates)
     });
-    debug_assert!(result);
     true
 }
 
 #[no_mangle]
 pub unsafe extern "C" fn Servo_DeclarationBlock_SetProperty(
     declarations: RawServoDeclarationBlockBorrowed,
     property: *const nsACString,
     value: *const nsACString,
@@ -3641,17 +3627,16 @@ pub unsafe extern "C" fn Servo_Declarati
 pub unsafe extern "C" fn Servo_DeclarationBlock_SetPropertyToAnimationValue(
     declarations: RawServoDeclarationBlockBorrowed,
     animation_value: RawServoAnimationValueBorrowed,
 ) -> bool {
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
         decls.push(
             AnimationValue::as_arc(&animation_value).uncompute(),
             Importance::Normal,
-            DeclarationPushMode::Append,
         )
     })
 }
 
 #[no_mangle]
 pub unsafe extern "C" fn Servo_DeclarationBlock_SetPropertyById(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
@@ -3932,17 +3917,17 @@ pub unsafe extern "C" fn Servo_Declarati
     use style::properties::{PropertyDeclaration, LonghandId};
     use style::properties::longhands::_x_lang::computed_value::T as Lang;
 
     let long = get_longhand_from_id!(property);
     let prop = match_wrap_declared! { long,
         XLang => Lang(Atom::from_raw(value)),
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 #[allow(unreachable_code)]
 pub extern "C" fn Servo_DeclarationBlock_SetKeywordValue(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
@@ -4008,17 +3993,17 @@ pub extern "C" fn Servo_DeclarationBlock
         WhiteSpace => longhands::white_space::SpecifiedValue::from_gecko_keyword(value),
         CaptionSide => longhands::caption_side::SpecifiedValue::from_gecko_keyword(value),
         BorderTopStyle => BorderStyle::from_gecko_keyword(value),
         BorderRightStyle => BorderStyle::from_gecko_keyword(value),
         BorderBottomStyle => BorderStyle::from_gecko_keyword(value),
         BorderLeftStyle => BorderStyle::from_gecko_keyword(value),
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetIntValue(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
     value: i32
@@ -4029,17 +4014,17 @@ pub extern "C" fn Servo_DeclarationBlock
 
     let long = get_longhand_from_id!(property);
     let prop = match_wrap_declared! { long,
         XSpan => Span(value),
         // Gecko uses Integer values to signal that it is relative
         MozScriptLevel => MozScriptLevel::Relative(value),
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetPixelValue(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
     value: f32
@@ -4084,17 +4069,17 @@ pub extern "C" fn Servo_DeclarationBlock
             Box::new(BorderCornerRadius::new(length.clone(), length))
         },
         BorderBottomRightRadius => {
             let length = LengthOrPercentage::from(nocalc);
             Box::new(BorderCornerRadius::new(length.clone(), length))
         },
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetLengthValue(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
@@ -4122,17 +4107,17 @@ pub extern "C" fn Servo_DeclarationBlock
     };
 
     let prop = match_wrap_declared! { long,
         Width => MozLength::LengthOrPercentageOrAuto(nocalc.into()),
         FontSize => LengthOrPercentage::from(nocalc).into(),
         MozScriptMinSize => MozScriptMinSize(nocalc),
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetNumberValue(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
     value: f32,
@@ -4144,17 +4129,17 @@ pub extern "C" fn Servo_DeclarationBlock
     let long = get_longhand_from_id!(property);
 
     let prop = match_wrap_declared! { long,
         MozScriptSizeMultiplier => MozScriptSizeMultiplier(value),
         // Gecko uses Number values to signal that it is absolute
         MozScriptLevel => MozScriptLevel::MozAbsolute(value as i32),
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetPercentValue(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
     value: f32,
@@ -4172,17 +4157,17 @@ pub extern "C" fn Servo_DeclarationBlock
         Width => MozLength::LengthOrPercentageOrAuto(pc.into()),
         MarginTop => pc.into(),
         MarginRight => pc.into(),
         MarginBottom => pc.into(),
         MarginLeft => pc.into(),
         FontSize => LengthOrPercentage::from(pc).into(),
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetAutoValue(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
 ) {
@@ -4196,17 +4181,17 @@ pub extern "C" fn Servo_DeclarationBlock
         Height => MozLength::auto(),
         Width => MozLength::auto(),
         MarginTop => auto,
         MarginRight => auto,
         MarginBottom => auto,
         MarginLeft => auto,
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetCurrentColor(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
 ) {
@@ -4218,17 +4203,17 @@ pub extern "C" fn Servo_DeclarationBlock
 
     let prop = match_wrap_declared! { long,
         BorderTopColor => cc,
         BorderRightColor => cc,
         BorderBottomColor => cc,
         BorderLeftColor => cc,
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetColorValue(
     declarations: RawServoDeclarationBlockBorrowed,
     property: nsCSSPropertyID,
     value: structs::nscolor,
@@ -4246,17 +4231,17 @@ pub extern "C" fn Servo_DeclarationBlock
         BorderTopColor => color,
         BorderRightColor => color,
         BorderBottomColor => color,
         BorderLeftColor => color,
         Color => longhands::color::SpecifiedValue(color),
         BackgroundColor => color,
     };
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(prop, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(prop, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetFontFamily(
     declarations: RawServoDeclarationBlockBorrowed,
     value: *const nsAString,
 ) {
@@ -4267,17 +4252,17 @@ pub extern "C" fn Servo_DeclarationBlock
     let string = unsafe { (*value).to_string() };
     let mut input = ParserInput::new(&string);
     let mut parser = Parser::new(&mut input);
     let result = FontFamily::parse_specified(&mut parser);
     if let Ok(family) = result {
         if parser.is_exhausted() {
             let decl = PropertyDeclaration::FontFamily(family);
             write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-                decls.push(decl, Importance::Normal, DeclarationPushMode::Append);
+                decls.push(decl, Importance::Normal);
             })
         }
     }
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetBackgroundImage(
     declarations: RawServoDeclarationBlockBorrowed,
@@ -4300,32 +4285,32 @@ pub extern "C" fn Servo_DeclarationBlock
         QuirksMode::NoQuirks,
         None,
     );
     let url = SpecifiedImageUrl::parse_from_string(string.into(), &context);
     let decl = PropertyDeclaration::BackgroundImage(BackgroundImage(
         vec![Either::Second(Image::Url(url))]
     ));
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(decl, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(decl, Importance::Normal);
     });
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_DeclarationBlock_SetTextDecorationColorOverride(
     declarations: RawServoDeclarationBlockBorrowed,
 ) {
     use style::properties::PropertyDeclaration;
     use style::values::specified::text::TextDecorationLine;
 
     let mut decoration = TextDecorationLine::none();
     decoration |= TextDecorationLine::COLOR_OVERRIDE;
     let decl = PropertyDeclaration::TextDecorationLine(decoration);
     write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(decl, Importance::Normal, DeclarationPushMode::Append);
+        decls.push(decl, Importance::Normal);
     })
 }
 
 #[no_mangle]
 pub unsafe extern "C" fn Servo_CSSSupports2(
     property: *const nsACString,
     value: *const nsACString,
 ) -> bool {
@@ -4987,17 +4972,16 @@ pub unsafe extern "C" fn Servo_StyleSet_
                             }
 
                             id.to_physical(writing_mode)
                         }
                         PropertyDeclarationId::Custom(..) => {
                             custom_properties.push(
                                 declaration.clone(),
                                 Importance::Normal,
-                                DeclarationPushMode::Append,
                             );
                             continue;
                         }
                     };
 
                     if properties_set_at_current_offset.contains(id) {
                         continue;
                     }
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -327168,19 +327168,25 @@
     ]
    ],
    "css/cssom/cssstyledeclaration-mutationrecord-004.html": [
     [
      "/css/cssom/cssstyledeclaration-mutationrecord-004.html",
      {}
     ]
    ],
-   "css/cssom/cssstyledeclaration-setter-order.html": [
-    [
-     "/css/cssom/cssstyledeclaration-setter-order.html",
+   "css/cssom/cssstyledeclaration-setter-declarations.html": [
+    [
+     "/css/cssom/cssstyledeclaration-setter-declarations.html",
+     {}
+    ]
+   ],
+   "css/cssom/cssstyledeclaration-setter-logical.html": [
+    [
+     "/css/cssom/cssstyledeclaration-setter-logical.html",
      {}
     ]
    ],
    "css/cssom/escape.html": [
     [
      "/css/cssom/escape.html",
      {}
     ]
@@ -553870,33 +553876,37 @@
    "aa2adbfcc58f3a844e2e1f2c96e5efed2c81f2c3",
    "testharness"
   ],
   "css/cssom/cssstyledeclaration-mutability.html": [
    "5f29436964d01c57f61d513cee5b83281643ac54",
    "testharness"
   ],
   "css/cssom/cssstyledeclaration-mutationrecord-001.html": [
-   "5d455757e4c80b4781ea4263fa78bced1d6b8632",
+   "0ed8cb2c41f371fdb509731f2ad1cf11e047d46f",
    "testharness"
   ],
   "css/cssom/cssstyledeclaration-mutationrecord-002.html": [
    "f21e4ba8d5195a66a0a5e64c72731bab75a44ea4",
    "testharness"
   ],
   "css/cssom/cssstyledeclaration-mutationrecord-003.html": [
    "a7968f4e7d85d778425f584d2a42bf74b8583bdb",
    "testharness"
   ],
   "css/cssom/cssstyledeclaration-mutationrecord-004.html": [
    "958b71b8f1c58a809590459e6f085f3e1217e9c7",
    "testharness"
   ],
-  "css/cssom/cssstyledeclaration-setter-order.html": [
-   "3e0e768c466011bb3d91b3f0eff55e029a2aec0f",
+  "css/cssom/cssstyledeclaration-setter-declarations.html": [
+   "e530f6b573bfb9774dd732f7289156117fc4bd94",
+   "testharness"
+  ],
+  "css/cssom/cssstyledeclaration-setter-logical.html": [
+   "c454a34b964e2aa05790831cc2de20e169162dd5",
    "testharness"
   ],
   "css/cssom/escape.html": [
    "c9ed57c7ef7a035c25feff4ea60547a57d727f31",
    "testharness"
   ],
   "css/cssom/font-shorthand-serialization.html": [
    "9d0bce1c0d74bf90aca1eb8ee6aa2e2ed2b92b30",
deleted file mode 100644
--- a/testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-001.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[cssstyledeclaration-mutationrecord-001.html]
-  prefs: [layout.css.property-append-only:true]
deleted file mode 100644
--- a/testing/web-platform/meta/css/cssom/cssstyledeclaration-setter-order.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[cssstyledeclaration-setter-order.html]
-  prefs: [layout.css.property-append-only:true]
--- a/testing/web-platform/meta/html/webappapis/system-state-and-capabilities/the-navigator-object/protocol.html.ini
+++ b/testing/web-platform/meta/html/webappapis/system-state-and-capabilities/the-navigator-object/protocol.html.ini
@@ -4,38 +4,8 @@
     expected: FAIL
 
   [%s instead of subdomain name should throw SYNTAX_ERR]
     expected: FAIL
 
   [a url argument pointing to a different domain name, without %s should throw SYNTAX_ERR]
     expected: FAIL
 
-  [attempting to override the attachment protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the cid protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the livescript protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the mid protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the mocha protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the opera protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the operamail protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the res protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the shttp protocol should throw SECURITY_ERR]
-    expected: FAIL
-
-  [attempting to override the tcl protocol should throw SECURITY_ERR]
-    expected: FAIL
-
--- a/testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-001.html
+++ b/testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-001.html
@@ -1,20 +1,20 @@
 <!doctype html>
 <meta charset="utf-8">
-<title>CSSOM: CSSStyleDeclaration.setPropertyValue queues a mutation record when not actually mutated</title>
+<title>CSSOM: CSSStyleDeclaration.setPropertyValue queues a mutation record when changed</title>
 <link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
 <link rel="help" href="https://drafts.csswg.org/cssom/#update-style-attribute-for">
 <link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script>
   document.documentElement.style.top = "0px";
 
-  let test = async_test("CSSStyleDeclaration.setPropertyValue queues a mutation record, even if not mutated");
+  let test = async_test("CSSStyleDeclaration.setPropertyValue queues a mutation record when serialization is changed");
   let m = new MutationObserver(function(r) {
     assert_equals(r.length, 1);
     test.done();
   });
 
   m.observe(document.documentElement,  { attributes: true });
-  document.documentElement.style.top = "0px";
+  document.documentElement.style.top = "1px";
 </script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/cssstyledeclaration-setter-declarations.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<title>CSSOM test: declaration block after setting via CSSOM</title>
+<link rel="help" href="https://drafts.csswg.org/cssom/#set-a-css-declaration-value">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+  function generateCSSDeclBlock(props) {
+    let elem = document.createElement("div");
+    let cssText = props.map(({name, value, priority}) => {
+      let longhand = `${name}: ${value}`;
+      if (priority) {
+        longhand += "!" + priority;
+      }
+      return longhand + ";";
+    }).join(" ");
+    elem.setAttribute("style", cssText);
+    return elem.style;
+  }
+  function compareByName(a, b) {
+    if (a.name < b.name) return -1;
+    if (a.name > b.name) return 1;
+    return 0;
+  }
+  function checkDeclarationsAnyOrder(block, props, msg) {
+    let actual = [];
+    for (let name of block) {
+      let value = block.getPropertyValue(name);
+      let priority = block.getPropertyPriority(name);
+      actual.push({name, value, priority});
+    }
+    actual.sort(compareByName);
+    let expected = Array.from(props);
+    expected.sort(compareByName);
+    assert_object_equals(actual, expected, "Declaration block content should match " + msg);
+  }
+  function longhand(name, value, priority="") {
+    return {name, value, priority};
+  }
+  function* shorthand(name, value, priority="") {
+    for (let subprop of SUBPROPS[name]) {
+      yield longhand(subprop, value, priority);
+    }
+  }
+
+  const SUBPROPS = {
+    "margin": ["margin-top", "margin-right", "margin-bottom", "margin-left"],
+    "padding": ["padding-top", "padding-right", "padding-bottom", "padding-left"],
+  };
+
+  test(function() {
+    let expectedDecls = [
+      longhand("top", "1px"),
+      longhand("bottom", "2px"),
+      longhand("left", "3px", "important"),
+      longhand("right", "4px"),
+    ];
+    let block = generateCSSDeclBlock(expectedDecls);
+    checkDeclarationsAnyOrder(block, expectedDecls, "in initial block");
+
+    block.setProperty("top", "5px", "important");
+    expectedDecls[0] = longhand("top", "5px", "important");
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting existing property");
+
+    block.setProperty("bottom", "2px");
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting existing property with identical value");
+
+    block.setProperty("left", "3px");
+    expectedDecls[2].priority = "";
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting existing property with different priority");
+
+    block.setProperty("float", "none");
+    expectedDecls.push(longhand("float", "none"));
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting non-existing property");
+  }, "setProperty with longhand should update only the declaration being set");
+
+  test(function() {
+    let expectedDecls = [
+      longhand("top", "1px"),
+      longhand("bottom", "2px"),
+      longhand("left", "3px", "important"),
+      longhand("right", "4px"),
+    ];
+    let block = generateCSSDeclBlock(expectedDecls);
+    checkDeclarationsAnyOrder(block, expectedDecls, "in initial block");
+
+    block.top = "5px";
+    expectedDecls[0] = longhand("top", "5px");
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting existing property");
+
+    block.bottom = "2px";
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting existing property with identical value");
+
+    block.left = "3px";
+    expectedDecls[2].priority = "";
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting existing property with different priority");
+
+    block.float = "none";
+    expectedDecls.push(longhand("float", "none"));
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting non-existing property");
+  }, "property setter should update only the declaration being set");
+
+  test(function() {
+    let expectedDecls = [
+      ...shorthand("margin", "1px"),
+      longhand("top", "2px"),
+      ...shorthand("padding", "3px", "important"),
+    ];
+    let block = generateCSSDeclBlock(expectedDecls);
+    checkDeclarationsAnyOrder(block, expectedDecls, "in initial block");
+
+    block.setProperty("margin", "4px");
+    for (let i = 0; i < 4; i++) {
+      expectedDecls[i].value = "4px";
+    }
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting an existing shorthand");
+
+    block.setProperty("margin", "4px");
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting an existing shorthand with identical value");
+
+    block.setProperty("padding", "3px");
+    for (let i = 5; i < 9; i++) {
+      expectedDecls[i].priority = "";
+    }
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting an existing shorthand with different priority");
+
+    block.setProperty("margin-bottom", "5px", "important");
+    expectedDecls[2] = longhand("margin-bottom", "5px", "important");
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting a longhand in an existing shorthand");
+  }, "setProperty with shorthand should update only the declarations being set");
+
+  test(function() {
+    let expectedDecls = [
+      ...shorthand("margin", "1px"),
+      longhand("top", "2px"),
+      ...shorthand("padding", "3px", "important"),
+    ];
+    let block = generateCSSDeclBlock(expectedDecls);
+    checkDeclarationsAnyOrder(block, expectedDecls, "in initial block");
+
+    block.margin = "4px";
+    for (let i = 0; i < 4; i++) {
+      expectedDecls[i].value = "4px";
+    }
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting an existing shorthand");
+
+    block.margin = "4px";
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting an existing shorthand with identical value");
+
+    block.padding = "3px";
+    for (let i = 5; i < 9; i++) {
+      expectedDecls[i].priority = "";
+    }
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting an existing shorthand with different priority");
+
+    block.marginBottom = "5px";
+    expectedDecls[2] = longhand("margin-bottom", "5px");
+    checkDeclarationsAnyOrder(block, expectedDecls, "after setting a longhand in an existing shorthand");
+  }, "longhand property setter should update only the decoarations being set");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/cssstyledeclaration-setter-logical.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>CSSOM test: declaration block after setting via CSSOM</title>
+<link rel="help" href="https://drafts.csswg.org/cssom/#set-a-css-declaration-value">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<div id="test"></div>
+<script>
+  test(function() {
+    const PHYSICAL_PROPS = ["padding-top", "padding-right",
+                            "padding-bottom", "padding-left"];
+    const LOGICAL_PROPS = ["padding-block-start", "padding-block-end",
+                           "padding-inline-start", "padding-inline-end"];
+    let elem = document.getElementById("test");
+    let decls = new Map;
+
+    {
+      let css = []
+      let i = 0;
+      for (let name of [...PHYSICAL_PROPS, ...LOGICAL_PROPS]) {
+        let value = `${i++}px`;
+        css.push(`${name}: ${value};`);
+        decls.set(name, value);
+      }
+      elem.setAttribute("style", css.join(" "));
+    }
+
+    let style = elem.style;
+    function indexOfProperty(prop) {
+      return Array.prototype.indexOf.apply(style, [prop]);
+    }
+    function assertPropAfterProps(prop, props, msg) {
+      let index = indexOfProperty(prop);
+      for (let p of props) {
+        assert_less_than(indexOfProperty(p), index, `${prop} should be after ${p} ${msg}`);
+      }
+    }
+    // We are not changing any value in this test, just order.
+    function assertValueUnchanged() {
+      for (let [name, value] of decls.entries()) {
+        assert_equals(style.getPropertyValue(name), value,
+                      `value of ${name} shouldn't be changed`);
+      }
+    }
+
+    assertValueUnchanged();
+    // Check that logical properties are all after physical properties
+    // at the beginning.
+    for (let p of LOGICAL_PROPS) {
+      assertPropAfterProps(p, PHYSICAL_PROPS, "initially");
+    }
+
+    // Try setting a longhand.
+    style.setProperty("padding-top", "0px");
+    assertValueUnchanged();
+    // Check that padding-top is after logical properties, but other
+    // physical properties are still before logical properties.
+    assertPropAfterProps("padding-top", LOGICAL_PROPS, "after setting padding-top");
+    for (let p of LOGICAL_PROPS) {
+      assertPropAfterProps(p, PHYSICAL_PROPS.filter(prop => prop != "padding-top"),
+                           "after setting padding-top");
+    }
+
+    // Try setting a shorthand.
+    style.setProperty("padding", "0px 1px 2px 3px");
+    assertValueUnchanged();
+    // Check that all physical properties are now after logical properties.
+    for (let p of PHYSICAL_PROPS) {
+      assertPropAfterProps(p, LOGICAL_PROPS, "after setting padding shorthand");
+    }
+  }, "newly set declaration should be after all declarations " +
+  "in the same logical property group but have different logical kind");
+</script>
deleted file mode 100644
--- a/testing/web-platform/tests/css/cssom/cssstyledeclaration-setter-order.html
+++ /dev/null
@@ -1,108 +0,0 @@
-<!DOCTYPE html>
-<title>CSSOM test: order of declarations after setting via CSSOM</title>
-<link rel="help" href="https://drafts.csswg.org/cssom/#set-a-css-declaration-value">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<div id="log"></div>
-<script>
-  function generateCSSDeclBlock(props) {
-    let elem = document.createElement("div");
-    let cssText = props.map(([prop, value]) => `${prop}: ${value};`).join(" ");
-    elem.setAttribute("style", cssText);
-    return elem.style;
-  }
-  function checkOrder(block, props, msg) {
-    assert_array_equals(Array.from(block), props, `Property order should match ${msg}`);
-  }
-  function arrayWithItemsAtEnd(array, items) {
-    let result = array.filter(item => !items.includes(item));
-    return result.concat(items);
-  }
-
-  const SUBPROPS = {
-    "margin": ["margin-top", "margin-right", "margin-bottom", "margin-left"],
-    "padding": ["padding-top", "padding-right", "padding-bottom", "padding-left"],
-  };
-
-  test(function() {
-    let block = generateCSSDeclBlock([
-      ["top", "1px"],
-      ["bottom", "2px"],
-      ["left", "3px"],
-      ["right", "4px"],
-    ]);
-    let expectedOrder = ["top", "bottom", "left", "right"];
-    checkOrder(block, expectedOrder, "in initial block");
-
-    block.setProperty("top", "5px");
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, ["top"]);
-    checkOrder(block, expectedOrder, "after setting existing property");
-
-    block.setProperty("bottom", "2px");
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, ["bottom"]);
-    checkOrder(block, expectedOrder, "after setting existing property with identical value");
-  }, "setProperty with existing longhand should change order");
-
-  test(function() {
-    let block = generateCSSDeclBlock([
-      ["top", "1px"],
-      ["bottom", "2px"],
-      ["left", "3px"],
-      ["right", "4px"],
-    ]);
-    let expectedOrder = ["top", "bottom", "left", "right"];
-    checkOrder(block, expectedOrder, "in initial block");
-
-    block.top = "5px";
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, ["top"]);
-    checkOrder(block, expectedOrder, "after setting existing property");
-
-    block.bottom = "2px";
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, ["bottom"]);
-    checkOrder(block, expectedOrder, "after setting existing property with identical value");
-  }, "invoke property setter with existing longhand should change order");
-
-  test(function() {
-    let block = generateCSSDeclBlock([
-      ["margin", "1px"],
-      ["top", "2px"],
-      ["padding", "3px"],
-    ]);
-    let expectedOrder = SUBPROPS["margin"].concat(["top"]).concat(SUBPROPS["padding"]);
-    checkOrder(block, expectedOrder, "in initial block");
-
-    block.setProperty("margin", "4px");
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, SUBPROPS["margin"]);
-    checkOrder(block, expectedOrder, "after setting an existing shorthand");
-
-    block.setProperty("padding", "3px");
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, SUBPROPS["padding"]);
-    checkOrder(block, expectedOrder, "after setting an existing shorthand with identical value");
-
-    block.setProperty("margin-bottom", "5px");
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, ["margin-bottom"]);
-    checkOrder(block, expectedOrder, "after setting a longhand in an existing shorthand");
-  }, "setProperty with existing shorthand should change order");
-
-  test(function() {
-    let block = generateCSSDeclBlock([
-      ["margin", "1px"],
-      ["top", "2px"],
-      ["padding", "3px"],
-    ]);
-    let expectedOrder = SUBPROPS["margin"].concat(["top"]).concat(SUBPROPS["padding"]);
-    checkOrder(block, expectedOrder, "in initial block");
-
-    block.margin = "4px";
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, SUBPROPS["margin"]);
-    checkOrder(block, expectedOrder, "after setting an existing shorthand");
-
-    block.padding = "3px";
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, SUBPROPS["padding"]);
-    checkOrder(block, expectedOrder, "after setting an existing shorthand with identical value");
-
-    block.marginBottom = "5px";
-    expectedOrder = arrayWithItemsAtEnd(expectedOrder, ["margin-bottom"]);
-    checkOrder(block, expectedOrder, "after setting a longhand in an existing shorthand");
-  }, "invoke property setter with existing shorthand should change order");
-</script>
--- a/toolkit/moz.configure
+++ b/toolkit/moz.configure
@@ -609,21 +609,20 @@ simple_keyfile('Google API')
 id_and_secret_keyfile('Bing API')
 
 simple_keyfile('Adjust SDK')
 
 id_and_secret_keyfile('Leanplum SDK')
 
 simple_keyfile('Pocket API')
 
-# We support setting up the appropriate options for Stylo's build-time
-# bindings generation via setting LLVM_CONFIG or by providing explicit
-# configure options.  The Windows installer of LLVM/Clang doesn't provide
-# llvm-config, so we need both methods to support all of our tier-1
-# platforms.
+# We support setting up the appropriate options for bindgen via setting
+# LLVM_CONFIG or by providing explicit configure options.  The Windows
+# installer of LLVM/Clang doesn't provide llvm-config, so we need both
+# methods to support all of our tier-1 platforms.
 @depends(host)
 @imports('which')
 @imports('os')
 @imports('subprocess')
 def llvm_config_paths(host):
     llvm_supported_versions = ['6.0', '5.0', '4.0', '3.9']
     llvm_config_progs = []
     for version in llvm_supported_versions:
@@ -663,36 +662,36 @@ def llvm_config_paths(host):
     return llvm_config_progs
 
 llvm_config = check_prog('LLVM_CONFIG', llvm_config_paths,
                          when='--enable-compile-environment',
                          what='llvm-config', allow_missing=True)
 
 with only_when('--enable-compile-environment'):
     option('--with-libclang-path', nargs=1,
-           help='Absolute path to a directory containing Clang/LLVM libraries for Stylo (version 3.9.x or above)')
+           help='Absolute path to a directory containing Clang/LLVM libraries for bindgen (version 3.9.x or above)')
     option('--with-clang-path', nargs=1,
-           help='Absolute path to a Clang binary for Stylo bindgen (version 3.9.x or above)')
+           help='Absolute path to a Clang binary for bindgen (version 3.9.x or above)')
 
     def invoke_llvm_config(llvm_config, *options):
         '''Invoke llvm_config with the given options and return the first line of
         output.'''
         lines = check_cmd_output(llvm_config, *options).splitlines()
         return lines[0]
 
     @imports(_from='textwrap', _import='dedent')
     def check_minimum_llvm_config_version(llvm_config):
         version = Version(invoke_llvm_config(llvm_config, '--version'))
         min_version = Version('3.9.0')
         if version < min_version:
             die(dedent('''\
-            llvm installation {} is incompatible with Stylo bindgen.
+            llvm installation {} is incompatible with bindgen.
 
-            To compile Stylo, please install version {} or greater of
-            Clang + LLVM and ensure that the 'llvm-config' from that
+            Please install version {} or greater of Clang + LLVM
+            and ensure that the 'llvm-config' from that
             installation is first on your path.
 
             You can verify this by typing 'llvm-config --version'.
             '''.format(version, min_version)))
 
     @depends(llvm_config, '--with-libclang-path', '--with-clang-path',
              host_library_name_info, host)
     @imports('os.path')
@@ -722,18 +721,18 @@ with only_when('--enable-compile-environ
                     return (True, None)
             else:
                 return (False, list(set(libclang_choices)))
 
         if not libclang_path and not clang_path:
             # We must have LLVM_CONFIG in this case.
             if not llvm_config:
                 die(dedent('''\
-                Could not find LLVM/Clang installation for compiling stylo build-time
-                bindgen.  Please specify the 'LLVM_CONFIG' environment variable
+                Could not find LLVM/Clang installation for compiling bindgen.
+                Please specify the 'LLVM_CONFIG' environment variable
                 (recommended), pass the '--with-libclang-path' and '--with-clang-path'
                 options to configure, or put 'llvm-config' in your PATH.  Altering your
                 PATH may expose 'clang' as well, potentially altering your compiler,
                 which may not be what you intended.'''))
 
             check_minimum_llvm_config_version(llvm_config)
             libclang_arg = '--bindir' if host.os == 'WINNT' else '--libdir'
             libclang_path = invoke_llvm_config(llvm_config, libclang_arg)
--- a/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
+++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
@@ -3,48 +3,48 @@ let testURL = "https://example.com/brows
 
 add_task(async function() {
   // Load a page registering a protocol handler.
   let browser = gBrowser.selectedBrowser;
   browser.loadURI(testURL);
   await BrowserTestUtils.browserLoaded(browser, false, testURL);
 
   // Register the protocol handler by clicking the notificationbar button.
-  let notificationValue = "Protocol Registration: testprotocol";
+  let notificationValue = "Protocol Registration: web+testprotocol";
   let getNotification = () =>
     gBrowser.getNotificationBox().getNotificationWithValue(notificationValue);
   await BrowserTestUtils.waitForCondition(getNotification);
   let notification = getNotification();
   let button =
     notification.getElementsByClassName("notification-button-default")[0];
   ok(button, "got registration button");
   button.click();
 
   // Set the new handler as default.
   const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
                      getService(Ci.nsIExternalProtocolService);
-  let protoInfo = protoSvc.getProtocolHandlerInfo("testprotocol");
+  let protoInfo = protoSvc.getProtocolHandlerInfo("web+testprotocol");
   is(protoInfo.preferredAction, protoInfo.useHelperApp,
      "using a helper application is the preferred action");
   ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set");
   let handlers = protoInfo.possibleApplicationHandlers;
-  is(1, handlers.length, "only one handler registered for testprotocol");
+  is(1, handlers.length, "only one handler registered for web+testprotocol");
   let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp);
   ok(handler instanceof Ci.nsIWebHandlerApp, "the handler is a web handler");
   is(handler.uriTemplate, "https://example.com/foobar?uri=%s",
      "correct url template")
   protoInfo.preferredApplicationHandler = handler;
   protoInfo.alwaysAskBeforeHandling = false;
   const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].
                        getService(Ci.nsIHandlerService);
   handlerSvc.store(protoInfo);
 
   // Middle-click a testprotocol link and check the new tab is correct
   let link = "#link";
-  const expectedURL = "https://example.com/foobar?uri=testprotocol%3Atest";
+  const expectedURL = "https://example.com/foobar?uri=web%2Btestprotocol%3Atest";
 
   let promiseTabOpened =
     BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
   await BrowserTestUtils.synthesizeMouseAtCenter(link, {button: 1}, browser);
   let tab = await promiseTabOpened;
   gBrowser.selectedTab = tab;
   is(gURLBar.value, expectedURL,
      "the expected URL is displayed in the location bar");
--- a/uriloader/exthandler/tests/mochitest/protocolHandler.html
+++ b/uriloader/exthandler/tests/mochitest/protocolHandler.html
@@ -2,15 +2,15 @@
 <html>
   <head>
     <title>Protocol handler</title>
     <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
     <meta content="utf-8" http-equiv="encoding">
   </head>
   <body>
     <script type="text/javascript">
-      navigator.registerProtocolHandler("testprotocol",
+      navigator.registerProtocolHandler("web+testprotocol",
           "https://example.com/foobar?uri=%s",
           "Test Protocol");
     </script>
-    <a id="link" href="testprotocol:test">testprotocol link</a>
+    <a id="link" href="web+testprotocol:test">testprotocol link</a>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/xpcom/base/MemoryInfo.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/MemoryInfo.h"
+
+#include "mozilla/DebugOnly.h"
+
+#include <WinBase.h>
+
+namespace mozilla {
+
+/* static */ MemoryInfo
+MemoryInfo::Get(const void* aPtr, size_t aSize)
+{
+  MemoryInfo result;
+
+  result.mStart = uintptr_t(aPtr);
+  const char* ptr = reinterpret_cast<const char*>(aPtr);
+  const char* end = ptr + aSize;
+  DebugOnly<void*> base = nullptr;
+  while (ptr < end) {
+    MEMORY_BASIC_INFORMATION basicInfo;
+    if (!VirtualQuery(ptr, &basicInfo, sizeof(basicInfo))) {
+      break;
+    }
+
+    MOZ_ASSERT_IF(base, base == basicInfo.AllocationBase);
+    base = basicInfo.AllocationBase;
+
+    size_t regionSize = std::min(size_t(basicInfo.RegionSize),
+                                 size_t(end - ptr));
+
+    if (basicInfo.State == MEM_COMMIT) {
+      result.mCommitted += regionSize;
+    } else if (basicInfo.State == MEM_RESERVE) {
+      result.mReserved += regionSize;
+    } else if (basicInfo.State == MEM_FREE) {
+      result.mFree += regionSize;
+    } else {
+      MOZ_ASSERT_UNREACHABLE("Unexpected region state");
+    }
+    result.mSize += regionSize;
+    ptr += regionSize;
+
+    if (result.mType.isEmpty()) {
+      if (basicInfo.Type & MEM_IMAGE) {
+        result.mType += PageType::Image;
+      }
+      if (basicInfo.Type & MEM_MAPPED) {
+        result.mType += PageType::Mapped;
+      }
+      if (basicInfo.Type & MEM_PRIVATE) {
+        result.mType += PageType::Private;
+      }
+
+      // The first 8 bits of AllocationProtect are an enum. The remaining bits
+      // are flags.
+      switch (basicInfo.AllocationProtect & 0xff) {
+        case PAGE_EXECUTE_WRITECOPY:
+          result.mPerms += Perm::CopyOnWrite;
+          MOZ_FALLTHROUGH;
+        case PAGE_EXECUTE_READWRITE:
+          result.mPerms += Perm::Write;
+          MOZ_FALLTHROUGH;
+        case PAGE_EXECUTE_READ:
+          result.mPerms += Perm::Read;
+          MOZ_FALLTHROUGH;
+        case PAGE_EXECUTE:
+          result.mPerms += Perm::Execute;
+          break;
+
+        case PAGE_WRITECOPY:
+          result.mPerms += Perm::CopyOnWrite;
+          MOZ_FALLTHROUGH;
+        case PAGE_READWRITE:
+          result.mPerms += Perm::Write;
+          MOZ_FALLTHROUGH;
+        case PAGE_READONLY:
+          result.mPerms += Perm::Read;
+          break;
+
+        default:
+          break;
+      }
+
+      if (basicInfo.AllocationProtect & PAGE_GUARD) {
+        result.mPerms += Perm::Guard;
+      }
+      if (basicInfo.AllocationProtect & PAGE_NOCACHE) {
+        result.mPerms += Perm::NoCache;
+      }
+      if (basicInfo.AllocationProtect & PAGE_WRITECOMBINE) {
+        result.mPerms += Perm::WriteCombine;
+      }
+    }
+  }
+
+  result.mEnd = uintptr_t(ptr);
+  return result;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/xpcom/base/MemoryInfo.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MemoryInfo_h
+#define mozilla_MemoryInfo_h
+
+#include "mozilla/EnumSet.h"
+#include "nsTArray.h"
+
+/**
+ * MemoryInfo is a helper class which describes the attributes and sizes of a
+ * particular region of VM memory on Windows. It roughtly corresponds to the
+ * values in a MEMORY_BASIC_INFORMATION struct, summed over an entire region or
+ * memory.
+ */
+
+namespace mozilla {
+
+class MemoryInfo final
+{
+public:
+  enum class Perm : uint8_t
+  {
+    Read,
+    Write,
+    Execute,
+    CopyOnWrite,
+    Guard,
+    NoCache,
+    WriteCombine,
+  };
+  enum class PageType : uint8_t
+  {
+    Image,
+    Mapped,
+    Private,
+  };
+
+  using PermSet = EnumSet<Perm>;
+  using PageTypeSet = EnumSet<PageType>;
+
+  MemoryInfo() = default;
+  MOZ_IMPLICIT MemoryInfo(const MemoryInfo&) = default;
+
+  uintptr_t Start() const { return mStart; }
+  uintptr_t End() const { return mEnd; }
+
+  PageTypeSet Type() const { return mType; }
+  PermSet Perms() const { return mPerms; }
+
+  size_t Reserved() const { return mReserved; }
+  size_t Committed() const { return mCommitted; }
+  size_t Free() const { return mFree; }
+  size_t Size() const { return mSize; }
+
+  // Returns a MemoryInfo object containing the sums of all region sizes,
+  // divided into Reserved, Committed, and Free, depending on their State
+  // properties.
+  //
+  // The entire range of aSize bytes starting at aPtr must correspond to a
+  // single allocation. This restriction is enforced in debug builds.
+  static MemoryInfo Get(const void* aPtr, size_t aSize);
+
+private:
+  uintptr_t mStart = 0;
+  uintptr_t mEnd = 0;
+
+  size_t mReserved = 0;
+  size_t mCommitted = 0;
+  size_t mFree = 0;
+  size_t mSize = 0;
+
+  PageTypeSet mType{};
+
+  PermSet mPerms{};
+};
+
+} // namespace mozilla
+
+#endif // mozilla_MemoryInfo_h
new file mode 100644
--- /dev/null
+++ b/xpcom/base/MemoryMapping.cpp
@@ -0,0 +1,203 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/MemoryMapping.h"
+
+#include "mozilla/BinarySearch.h"
+#include "mozilla/FileUtils.h"
+
+#include <fstream>
+#include <string>
+
+namespace mozilla {
+
+namespace {
+struct VMFlagString
+{
+  const char* mName;
+  const char* mPrettyName;
+  VMFlag mFlag;
+};
+
+static const VMFlagString sVMFlagStrings[] = {
+  {"ac", "Accountable",   VMFlag::Accountable},
+  {"ar", "ArchSpecific",  VMFlag::ArchSpecific},
+  {"dc", "NoFork",        VMFlag::NoFork},
+  {"dd", "NoCore",        VMFlag::NoCore},
+  {"de", "NoExpand",      VMFlag::NoExpand},
+  {"dw", "DisabledWrite", VMFlag::DisabledWrite},
+  {"ex", "Executable",    VMFlag::Executable},
+  {"gd", "GrowsDown",     VMFlag::GrowsDown},
+  {"hg", "HugePage",      VMFlag::HugePage},
+  {"ht", "HugeTLB",       VMFlag::HugeTLB},
+  {"io", "IO",            VMFlag::IO},
+  {"lo", "Locked",        VMFlag::Locked},
+  {"me", "MayExecute",    VMFlag::MayExecute},
+  {"mg", "Mergeable",     VMFlag::Mergeable},
+  {"mm", "MixedMap",      VMFlag::MixedMap},
+  {"mr", "MayRead",       VMFlag::MayRead},
+  {"ms", "MayShare",      VMFlag::MayShare},
+  {"mw", "MayWrite",      VMFlag::MayWrite},
+  {"nh", "NoHugePage",    VMFlag::NoHugePage},
+  {"nl", "NonLinear",     VMFlag::NonLinear},
+  {"nr", "NotReserved",   VMFlag::NotReserved},
+  {"pf", "PurePFN",       VMFlag::PurePFN},
+  {"rd", "Readable",      VMFlag::Readable},
+  {"rr", "Random",        VMFlag::Random},
+  {"sd", "SoftDirty",     VMFlag::SoftDirty},
+  {"sh", "Shared",        VMFlag::Shared},
+  {"sr", "Sequential",    VMFlag::Sequential},
+  {"wr", "Writable",      VMFlag::Writable},
+};
+} // anonymous namespace
+
+constexpr size_t kVMFlags = size_t(-1);
+
+// An array of known field names which may be present in an smaps file, and the
+// offsets of the corresponding fields in a MemoryMapping class.
+const MemoryMapping::Field MemoryMapping::sFields[] = {
+  {"AnonHugePages",   offsetof(MemoryMapping, mAnonHugePages)},
+  {"Anonymous",       offsetof(MemoryMapping, mAnonymous)},
+  {"KernelPageSize",  offsetof(MemoryMapping, mKernelPageSize)},
+  {"LazyFree",        offsetof(MemoryMapping, mLazyFree)},
+  {"Locked",          offsetof(MemoryMapping, mLocked)},
+  {"MMUPageSize",     offsetof(MemoryMapping, mMMUPageSize)},
+  {"Private_Clean",   offsetof(MemoryMapping, mPrivate_Clean)},
+  {"Private_Dirty",   offsetof(MemoryMapping, mPrivate_Dirty)},
+  {"Private_Hugetlb", offsetof(MemoryMapping, mPrivate_Hugetlb)},
+  {"Pss",             offsetof(MemoryMapping, mPss)},
+  {"Referenced",      offsetof(MemoryMapping, mReferenced)},
+  {"Rss",             offsetof(MemoryMapping, mRss)},
+  {"Shared_Clean",    offsetof(MemoryMapping, mShared_Clean)},
+  {"Shared_Dirty",    offsetof(MemoryMapping, mShared_Dirty)},
+  {"Shared_Hugetlb",  offsetof(MemoryMapping, mShared_Hugetlb)},
+  {"ShmemPmdMapped",  offsetof(MemoryMapping, mShmemPmdMapped)},
+  {"Size",            offsetof(MemoryMapping, mSize)},
+  {"Swap",            offsetof(MemoryMapping, mSwap)},
+  {"SwapPss",         offsetof(MemoryMapping, mSwapPss)},
+  // VmFlags is a special case. It contains an array of flag strings, which
+  // describe attributes of the mapping, rather than a mapping size. We include
+  // it in this array to aid in parsing, but give it a separate sentinel value,
+  // and treat it specially.
+  {"VmFlags",         kVMFlags},
+};
+
+template <typename T, int n>
+const T*
+FindEntry(const char* aName, const T (&aEntries)[n])
+{
+  size_t index;
+  if (BinarySearchIf(aEntries, 0, n,
+                     [&] (const T& aEntry) {
+                       return strcmp(aName, aEntry.mName);
+                     },
+                     &index)) {
+    return &aEntries[index];
+  }
+  return nullptr;
+}
+
+using Perm = MemoryMapping::Perm;
+using PermSet = MemoryMapping::PermSet;
+
+nsresult
+GetMemoryMappings(nsTArray<MemoryMapping>& aMappings)
+{
+  std::ifstream stream("/proc/self/smaps");
+  if (stream.fail()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  MemoryMapping* current = nullptr;
+  std::string buffer;
+  while (std::getline(stream, buffer)) {
+    size_t start, end, offset;
+    char flags[4] = "---";
+    char name[512];
+
+    name[0] = 0;
+
+    // Match the start of an entry. A typical line looks something like:
+    //
+    // 1487118a7000-148711a5a000 r-xp 00000000 103:03 54004561                  /usr/lib/libc-2.27.so
+    if (sscanf(buffer.c_str(), "%zx-%zx %4c %zx %*u:%*u %*u %511s\n",
+               &start, &end, flags, &offset, name) >= 4) {
+      PermSet perms;
+      if (flags[0] == 'r') {
+        perms += Perm::Read;
+      }
+      if (flags[1] == 'w') {
+        perms += Perm::Write;
+      }
+      if (flags[2] == 'x') {
+        perms += Perm::Execute;
+      }
+      if (flags[3] == 'p') {
+        perms += Perm::Private;
+      } else if (flags[3] == 's') {
+        perms += Perm::Shared;
+      }
+
+      current = aMappings.AppendElement(MemoryMapping{start, end, perms, offset, name});
+      continue;
+    }
+    if (!current) {
+      continue;
+    }
+
+    nsAutoCStringN<128> line(buffer.c_str());
+    char* savePtr;
+    char* fieldName = strtok_r(line.BeginWriting(), ":", &savePtr);
+    if (!fieldName) {
+      continue;
+    }
+    auto* field = FindEntry(fieldName, MemoryMapping::sFields);
+    if (!field) {
+      continue;
+    }
+
+    if (field->mOffset == kVMFlags) {
+      while (char* flagName = strtok_r(nullptr, " \n", &savePtr)) {
+        if (auto* flag = FindEntry(flagName, sVMFlagStrings)) {
+          current->mFlags += flag->mFlag;
+        }
+      }
+      continue;
+    }
+
+    const char* rest = strtok_r(nullptr, "\n", &savePtr);
+    size_t value;
+    if (sscanf(rest, "%zd kB", &value) > 0) {
+      current->ValueForField(*field) = value * 1024;
+    }
+  }
+
+  return NS_OK;
+}
+
+void
+MemoryMapping::Dump(nsACString& aOut) const
+{
+  aOut.AppendPrintf("%zx-%zx Size: %zu Offset: %zx %s\n",
+                    mStart, mEnd,
+                    mEnd - mStart,
+                    mOffset, mName.get());
+
+  for (auto& field : MemoryMapping::sFields) {
+    if (field.mOffset < sizeof(*this)) {
+      aOut.AppendPrintf("  %s: %zd\n", field.mName, ValueForField(field));
+    }
+  }
+
+  aOut.AppendPrintf("  Flags: %x\n", mFlags.serialize());
+  for (auto& flag : sVMFlagStrings) {
+    if (mFlags.contains(flag.mFlag)) {
+      aOut.AppendPrintf("       : %s %s\n", flag.mName, flag.mPrettyName);
+    }
+  }
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/xpcom/base/MemoryMapping.h
@@ -0,0 +1,190 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MemoryMapping_h
+#define mozilla_MemoryMapping_h
+
+#include "mozilla/EnumSet.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+/**
+ * MemoryMapping is a helper class which describes an entry in the Linux
+ * /proc/<pid>/smaps file. See procfs(5) for details on the entry format.
+ *
+ * The GetMemoryMappings() function returns an array of such entries, sorted by
+ * start address, one for each entry in the current process's address space.
+ */
+
+namespace mozilla {
+
+enum class VMFlag : uint8_t
+{
+  Readable,      // rd  - readable
+  Writable,      // wr  - writable
+  Executable,    // ex  - executable
+  Shared,        // sh  - shared
+  MayRead,       // mr  - may read
+  MayWrite,      // mw  - may write
+  MayExecute,    // me  - may execute
+  MayShare,      // ms  - may share
+  GrowsDown,     // gd  - stack segment grows down
+  PurePFN,       // pf  - pure PFN range
+  DisabledWrite, // dw  - disabled write to the mapped file
+  Locked,        // lo  - pages are locked in memory
+  IO,            // io  - memory mapped I/O area
+  Sequential,    // sr  - sequential read advise provided
+  Random,        // rr  - random read advise provided
+  NoFork,        // dc  - do not copy area on fork
+  NoExpand,      // de  - do not expand area on remapping
+  Accountable,   // ac  - area is accountable
+  NotReserved,   // nr  - swap space is not reserved for the area
+  HugeTLB,       // ht  - area uses huge tlb pages
+  NonLinear,     // nl  - non-linear mapping
+  ArchSpecific,  // ar  - architecture specific flag
+  NoCore,        // dd  - do not include area into core dump
+  SoftDirty,     // sd  - soft-dirty flag
+  MixedMap,      // mm  - mixed map area
+  HugePage,      // hg  - huge page advise flag
+  NoHugePage,    // nh  - no-huge page advise flag
+  Mergeable,     // mg  - mergeable advise flag
+};
+
+using VMFlagSet = EnumSet<VMFlag>;
+
+class MemoryMapping final
+{
+public:
+  enum class Perm : uint8_t
+  {
+    Read,
+    Write,
+    Execute,
+    Shared,
+    Private,
+  };
+
+  using PermSet = EnumSet<Perm>;
+
+  MemoryMapping(uintptr_t aStart, uintptr_t aEnd,
+                PermSet aPerms, size_t aOffset,
+                const char* aName)
+    : mStart(aStart)
+    , mEnd(aEnd)
+    , mOffset(aOffset)
+    , mName(aName)
+    , mPerms(aPerms)
+  {}
+
+  const nsCString& Name() const { return mName; }
+
+  uintptr_t Start() const { return mStart; }
+  uintptr_t End() const { return mEnd; }
+
+  bool Includes(const void* aPtr) const
+  {
+    auto ptr = uintptr_t(aPtr);
+    return ptr >= mStart && ptr < mEnd;
+  }
+
+  PermSet Perms() const { return mPerms; }
+  VMFlagSet VMFlags() const { return mFlags; }
+
+  // For file mappings, the offset in the mapped file which corresponds to the
+  // start of the mapped region.
+  size_t Offset() const { return mOffset; }
+
+  size_t AnonHugePages() const { return mAnonHugePages; }
+  size_t Anonymous() const { return mAnonymous; }
+  size_t KernelPageSize() const { return mKernelPageSize; }
+  size_t LazyFree() const { return mLazyFree; }
+  size_t Locked() const { return mLocked; }
+  size_t MMUPageSize() const { return mMMUPageSize; }
+  size_t Private_Clean() const { return mPrivate_Clean; }
+  size_t Private_Dirty() const { return mPrivate_Dirty; }
+  size_t Private_Hugetlb() const { return mPrivate_Hugetlb; }
+  size_t Pss() const { return mPss; }
+  size_t Referenced() const { return mReferenced; }
+  size_t Rss() const { return mRss; }
+  size_t Shared_Clean() const { return mShared_Clean; }
+  size_t Shared_Dirty() const { return mShared_Dirty; }
+  size_t Shared_Hugetlb() const { return mShared_Hugetlb; }
+  size_t ShmemPmdMapped() const { return mShmemPmdMapped; }
+  size_t Size() const { return mSize; }
+  size_t Swap() const { return mSwap; }
+  size_t SwapPss() const { return mSwapPss; }
+
+  // Dumps a string representation of the entry, similar to its format in the
+  // smaps file, to the given string. Mainly useful for debugging.
+  void Dump(nsACString& aOut) const;
+
+  // These comparison operators are used for binary searching sorted arrays of
+  // MemoryMapping entries to find the one which contains a given pointer.
+  bool operator==(const void* aPtr) const { return Includes(aPtr); }
+  bool operator<(const void* aPtr) const { return mStart < uintptr_t(aPtr); }
+
+private:
+  friend nsresult GetMemoryMappings(nsTArray<MemoryMapping>& aMappings);
+
+  uintptr_t mStart = 0;
+  uintptr_t mEnd = 0;
+
+  size_t mOffset = 0;
+
+  nsCString mName;
+
+  // Members for size fields in the smaps file. Please keep these in sync with
+  // the sFields array.
+  size_t mAnonHugePages = 0;
+  size_t mAnonymous = 0;
+  size_t mKernelPageSize = 0;
+  size_t mLazyFree = 0;
+  size_t mLocked = 0;
+  size_t mMMUPageSize = 0;
+  size_t mPrivate_Clean = 0;
+  size_t mPrivate_Dirty = 0;
+  size_t mPrivate_Hugetlb = 0;
+  size_t mPss = 0;
+  size_t mReferenced = 0;
+  size_t mRss = 0;
+  size_t mShared_Clean = 0;
+  size_t mShared_Dirty = 0;
+  size_t mShared_Hugetlb = 0;
+  size_t mShmemPmdMapped = 0;
+  size_t mSize = 0;
+  size_t mSwap = 0;
+  size_t mSwapPss = 0;
+
+  PermSet mPerms{};
+  VMFlagSet mFlags{};
+
+  // Contains the name and offset of one of the above size_t fields, for use in
+  // parsing in dumping. The below helpers contain a list of the fields, and map
+  // Field entries to the appropriate member in a class instance.
+  struct Field
+  {
+    const char* mName;
+    size_t mOffset;
+  };
+
+  static const Field sFields[20];
+
+  size_t& ValueForField(const Field& aField)
+  {
+    char* fieldPtr = reinterpret_cast<char*>(this) + aField.mOffset;
+    return reinterpret_cast<size_t*>(fieldPtr)[0];
+  }
+  size_t ValueForField(const Field& aField) const
+  {
+    return const_cast<MemoryMapping*>(this)->ValueForField(aField);
+  }
+};
+
+nsresult GetMemoryMappings(nsTArray<MemoryMapping>& aMappings);
+
+} // namespace mozilla
+
+#endif // mozilla_MemoryMapping_h
--- a/xpcom/base/moz.build
+++ b/xpcom/base/moz.build
@@ -107,16 +107,18 @@ EXPORTS.mozilla += [
     'DebuggerOnGCRunnable.h',
     'DeferredFinalize.h',
     'EnumeratedArrayCycleCollection.h',
     'ErrorNames.h',
     'HoldDropJSObjects.h',
     'IntentionalCrash.h',
     'JSObjectHolder.h',
     'Logging.h',
+    'MemoryInfo.h',
+    'MemoryMapping.h',
     'MemoryReportingProcess.h',
     'nsMemoryInfoDumper.h',
     'NSPRLogModulesParser.h',
     'OwningNonNull.h',
     'SizeOfState.h',
     'StaticMutex.h',
     'StaticPtr.h',
 ]
@@ -168,16 +170,26 @@ UNIFIED_SOURCES += [
     'nsSystemInfo.cpp',
     'nsTraceRefcnt.cpp',
     'nsUUIDGenerator.cpp',
     'nsVersionComparator.cpp',
     'nsVersionComparatorImpl.cpp',
     'nsWeakReference.cpp',
 ]
 
+if CONFIG['OS_TARGET'] in ('Linux', 'Android'):
+    UNIFIED_SOURCES += [
+        'MemoryMapping.cpp',
+    ]
+
+if CONFIG['OS_TARGET'] == 'WINNT':
+    UNIFIED_SOURCES += [
+        'MemoryInfo.cpp',
+    ]
+
 GENERATED_FILES += [
     "error_list.rs",
     "ErrorList.h",
     "ErrorNamesInternal.h",
 ]
 
 GENERATED_FILES["ErrorList.h"].script = "ErrorList.py:error_list_h"
 GENERATED_FILES["ErrorNamesInternal.h"].script = "ErrorList.py:error_names_internal_h"
--- a/xpcom/base/nsMemoryReporterManager.cpp
+++ b/xpcom/base/nsMemoryReporterManager.cpp
@@ -20,29 +20,33 @@
 #include "nsIXPConnect.h"
 #ifdef MOZ_GECKO_PROFILER
 #include "GeckoProfilerReporter.h"
 #endif
 #if defined(XP_UNIX) || defined(MOZ_DMD)
 #include "nsMemoryInfoDumper.h"
 #endif
 #include "nsNetCID.h"
+#include "nsThread.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/MemoryReportingProcess.h"
 #include "mozilla/PodOperations.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/ResultExtensions.h"
 #include "mozilla/Services.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/UniquePtrExtensions.h"
 #include "mozilla/dom/MemoryReportTypes.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/gfx/GPUProcessManager.h"
 #include "mozilla/ipc/FileDescriptorUtils.h"
 
 #ifdef XP_WIN
+#include "mozilla/MemoryInfo.h"
+
 #include <process.h>
 #ifndef getpid
 #define getpid _getpid
 #endif
 #else
 #include <unistd.h>
 #endif
 
@@ -51,16 +55,18 @@ using namespace dom;
 
 #if defined(MOZ_MEMORY)
 #  define HAVE_JEMALLOC_STATS 1
 #  include "mozmemory.h"
 #endif  // MOZ_MEMORY
 
 #if defined(XP_LINUX)
 
+#include "mozilla/MemoryMapping.h"
+
 #include <malloc.h>
 #include <string.h>
 #include <stdlib.h>
 
 static MOZ_MUST_USE nsresult
 GetProcSelfStatmField(int aField, int64_t* aN)
 {
   // There are more than two fields, but we're only interested in the first
@@ -84,58 +90,25 @@ static MOZ_MUST_USE nsresult
 GetProcSelfSmapsPrivate(int64_t* aN)
 {
   // You might be tempted to calculate USS by subtracting the "shared" value
   // from the "resident" value in /proc/<pid>/statm. But at least on Linux,
   // statm's "shared" value actually counts pages backed by files, which has
   // little to do with whether the pages are actually shared. /proc/self/smaps
   // on the other hand appears to give us the correct information.
 
-  FILE* f = fopen("/proc/self/smaps", "r");
-  if (NS_WARN_IF(!f)) {
-    return NS_ERROR_UNEXPECTED;
-  }
-
-  // We carry over the end of the buffer to the beginning to make sure we only
-  // interpret complete lines.
-  static const uint32_t carryOver = 32;
-  static const uint32_t readSize = 4096;
+  nsTArray<MemoryMapping> mappings(1024);
+  MOZ_TRY(GetMemoryMappings(mappings));
 
   int64_t amount = 0;
-  char buffer[carryOver + readSize + 1];
-
-  // Fill the beginning of the buffer with spaces, as a sentinel for the first
-  // iteration.
-  memset(buffer, ' ', carryOver);
-
-  for (;;) {
-    size_t bytes = fread(buffer + carryOver, sizeof(*buffer), readSize, f);
-    char* end = buffer + bytes;
-    char* ptr = buffer;
-    end[carryOver] = '\0';
-    // We are looking for lines like "Private_{Clean,Dirty}: 4 kB".
-    while ((ptr = strstr(ptr, "Private"))) {
-      if (ptr >= end) {
-        break;
-      }
-      ptr += sizeof("Private_Xxxxx:");
-      amount += strtol(ptr, nullptr, 10);
-    }
-    if (bytes < readSize) {
-      // We do not expect any match within the end of the buffer.
-      MOZ_ASSERT(!strstr(end, "Private"));
-      break;
-    }
-    // Carry the end of the buffer over to the beginning.
-    memcpy(buffer, end, carryOver);
+  for (auto& mapping : mappings) {
+    amount += mapping.Private_Clean();
+    amount += mapping.Private_Dirty();
   }
-
-  fclose(f);
-  // Convert from kB to bytes.
-  *aN = amount * 1024;
+  *aN = amount;
   return NS_OK;
 }
 
 #define HAVE_VSIZE_AND_RESIDENT_REPORTERS 1
 static MOZ_MUST_USE nsresult
 VsizeDistinguishedAmount(int64_t* aN)
 {
   return GetProcSelfStatmField(0, aN);
@@ -1426,16 +1399,127 @@ public:
       "Memory used by dynamic atom objects and chars (which are stored "
       "at the end of each atom object).");
 
     return NS_OK;
   }
 };
 NS_IMPL_ISUPPORTS(AtomTablesReporter, nsIMemoryReporter)
 
+#if defined(XP_LINUX) || defined(XP_WIN)
+class ThreadStacksReporter final : public nsIMemoryReporter
+{
+  ~ThreadStacksReporter() = default;
+
+public:
+  NS_DECL_ISUPPORTS
+
+  NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
+                            nsISupports* aData, bool aAnonymize) override
+  {
+#ifdef XP_LINUX
+    nsTArray<MemoryMapping> mappings(1024);
+    MOZ_TRY(GetMemoryMappings(mappings));
+#endif
+
+    // Enumerating over active threads requires holding a lock, so we collect
+    // info on all threads, and then call our reporter callbacks after releasing
+    // the lock.
+    struct ThreadData
+    {
+      nsCString mName;
+      uint32_t mThreadId;
+      size_t mPrivateSize;
+    };
+    AutoTArray<ThreadData, 32> threads;
+
+    for (auto* thread : nsThread::Enumerate()) {
+      if (!thread->StackBase()) {
+        continue;
+      }
+
+#ifdef XP_LINUX
+      int idx = mappings.BinaryIndexOf(thread->StackBase());
+      if (idx < 0) {
+        continue;
+      }
+      // Referenced() is the combined size of all pages in the region which have
+      // ever been touched, and are therefore consuming memory. For stack
+      // regions, these pages are guaranteed to be un-shared unless we fork
+      // after creating threads (which we don't).
+      size_t privateSize = mappings[idx].Referenced();
+
+      // On Linux, we have to be very careful matching memory regions to thread
+      // stacks.
+      //
+      // To begin with, the kernel only reports VM stats for regions of all
+      // adjacent pages with the same flags, protection, and backing file.
+      // There's no way to get finer-grained usage information for a subset of
+      // those pages.
+      //
+      // Stack segments always have a guard page at the bottom of the stack
+      // (assuming we only support stacks that grow down), so there's no danger
+      // of them being merged with other stack regions. At the top, there's no
+      // protection page, and no way to allocate one without using pthreads
+      // directly and allocating our own stacks. So we get around the problem by
+      // adding an extra VM flag (NOHUGEPAGES) to our stack region, which we
+      // don't expect to be set on any heap regions. But this is not fool-proof.
+      //
+      // A second kink is that different C libraries (and different versions
+      // thereof) report stack base locations and sizes differently with regard
+      // to the guard page. For the libraries that include the guard page in the
+      // stack size base pointer, we need to adjust those values to compensate.
+      // But it's possible that our logic will get out of sync with library
+      // changes, or someone will compile with an unexpected library.
+      //
+      //
+      // The upshot of all of this is that there may be configurations that our
+      // special cases don't cover. And if there are, we want to know about it.
+      // So assert that total size of the memory region we're reporting actually
+      // matches the allocated size of the thread stack.
+#ifndef ANDROID
+      MOZ_ASSERT(mappings[idx].Size() == thread->StackSize(),
+                 "Mapping region size doesn't match stack allocation size");
+#endif
+#else
+      auto memInfo = MemoryInfo::Get(thread->StackBase(), thread->StackSize());
+      size_t privateSize = memInfo.Committed();
+#endif
+
+      threads.AppendElement(ThreadData{
+        nsCString(PR_GetThreadName(thread->GetPRThread())),
+        thread->ThreadId(),
+        // On Linux, it's possible (but unlikely) that our stack region will
+        // have been merged with adjacent heap regions, in which case we'll get
+        // combined size information for both. So we take the minimum of the
+        // reported private size and the requested stack size to avoid the
+        // possible of majorly over-reporting in that case.
+        std::min(privateSize, thread->StackSize()),
+      });
+    }
+
+    for (auto& thread : threads) {
+      nsPrintfCString path("explicit/thread-stacks/%s (tid=%u)",
+                           thread.mName.get(), thread.mThreadId);
+
+      aHandleReport->Callback(
+          EmptyCString(), path,
+          KIND_NONHEAP, UNITS_BYTES,
+          thread.mPrivateSize,
+          NS_LITERAL_CSTRING("The sizes of thread stacks which have been "
+                             "committed to memory."),
+          aData);
+    }
+
+    return NS_OK;
+  }
+};
+NS_IMPL_ISUPPORTS(ThreadStacksReporter, nsIMemoryReporter)
+#endif
+
 #ifdef DEBUG
 
 // Ideally, this would be implemented in BlockingResourceBase.cpp.
 // However, this ends up breaking the linking step of various unit tests due
 // to adding a new dependency to libdmd for a commonly used feature (mutexes)
 // in  DMD  builds. So instead we do it here.
 class DeadlockDetectorReporter final : public nsIMemoryReporter
 {
@@ -1587,16 +1671,20 @@ nsMemoryReporterManager::Init()
 #endif
 
 #ifdef HAVE_SYSTEM_HEAP_REPORTER
   RegisterStrongReporter(new SystemHeapReporter());
 #endif
 
   RegisterStrongReporter(new AtomTablesReporter());
 
+#if defined(XP_LINUX) || defined(XP_WIN)
+  RegisterStrongReporter(new ThreadStacksReporter());
+#endif
+
 #ifdef DEBUG
   RegisterStrongReporter(new DeadlockDetectorReporter());
 #endif
 
 #ifdef MOZ_GECKO_PROFILER
   // We have to register this here rather than in profiler_init() because
   // profiler_init() runs prior to nsMemoryReporterManager's creation.
   RegisterStrongReporter(new GeckoProfilerReporter());
--- a/xpcom/threads/nsThread.cpp
+++ b/xpcom/threads/nsThread.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsThread.h"
 
 #include "base/message_loop.h"
+#include "base/platform_thread.h"
 
 // Chromium's logging can sometimes leak through...
 #ifdef LOG
 #undef LOG
 #endif
 
 #include "mozilla/ReentrantMonitor.h"
 #include "nsMemoryPressure.h"
@@ -45,19 +46,33 @@
 #include "nsServiceManagerUtils.h"
 #include "GeckoProfiler.h"
 #include "InputEventStatistics.h"
 #include "ThreadEventTarget.h"
 
 #include "mozilla/dom/ContentChild.h"
 
 #ifdef XP_LINUX
+#ifdef __GLIBC__
+#include <gnu/libc-version.h>
+#endif
+#include <sys/mman.h>
 #include <sys/time.h>
 #include <sys/resource.h>
 #include <sched.h>
+#include <stdio.h>
+#endif
+
+#ifdef XP_WIN
+#include "mozilla/DynamicallyLinkedFunctionPtr.h"
+
+#include <Winbase.h>
+
+using GetCurrentThreadStackLimitsFn = void (WINAPI*)(
+  PULONG_PTR LowLimit, PULONG_PTR HighLimit);
 #endif
 
 #define HAVE_UALARM _BSD_SOURCE || (_XOPEN_SOURCE >= 500 ||                 \
                       _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED) &&           \
                       !(_POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700)
 
 #if defined(XP_LINUX) && !defined(ANDROID) && defined(_GNU_SOURCE)
 #define HAVE_SCHED_SETAFFINITY
@@ -371,33 +386,120 @@ namespace {
 
 struct ThreadInitData {
   nsThread* thread;
   const nsACString& name;
 };
 
 }
 
+/* static */ mozilla::OffTheBooksMutex&
+nsThread::ThreadListMutex()
+{
+  static OffTheBooksMutex sMutex("nsThread::ThreadListMutex");
+  return sMutex;
+}
+
+/* static */ LinkedList<nsThread>&
+nsThread::ThreadList()
+{
+  static LinkedList<nsThread> sList;
+  return sList;
+}
+
+/* static */ nsThreadEnumerator
+nsThread::Enumerate()
+{
+  return {};
+}
+
 /*static*/ void
 nsThread::ThreadFunc(void* aArg)
 {
   using mozilla::ipc::BackgroundChild;
 
   ThreadInitData* initData = static_cast<ThreadInitData*>(aArg);
   nsThread* self = initData->thread;  // strong reference
 
   self->mThread = PR_GetCurrentThread();
+  self->mThreadId = uint32_t(PlatformThread::CurrentId());
   self->mVirtualThread = GetCurrentVirtualThread();
   self->mEventTarget->SetCurrentThread();
   SetupCurrentThreadForChaosMode();
 
   if (!initData->name.IsEmpty()) {
     NS_SetCurrentThreadName(initData->name.BeginReading());
   }
 
+  {
+#if defined(XP_LINUX)
+    pthread_attr_t attr;
+    pthread_attr_init(&attr);
+    pthread_getattr_np(pthread_self(), &attr);
+
+    size_t stackSize;
+    pthread_attr_getstack(&attr, &self->mStackBase, &stackSize);
+
+    // Glibc prior to 2.27 reports the stack size and base including the guard
+    // region, so we need to compensate for it to get accurate accounting.
+    // Also, this behavior difference isn't guarded by a versioned symbol, so we
+    // actually need to check the runtime glibc version, not the version we were
+    // compiled against.
+    static bool sAdjustForGuardSize = ({
+#ifdef __GLIBC__
+      unsigned major, minor;
+      sscanf(gnu_get_libc_version(), "%u.%u", &major, &minor) < 2 ||
+        major < 2 || (major == 2 && minor < 27);
+#else
+      false;
+#endif
+    });
+    if (sAdjustForGuardSize) {
+      size_t guardSize;
+      pthread_attr_getguardsize(&attr, &guardSize);
+
+      // Note: This assumes that the stack grows down, as is the case on all of
+      // our tier 1 platforms. On platforms where the stack grows up, the
+      // mStackBase adjustment is unnecessary, but doesn't cause any harm other
+      // than under-counting stack memory usage by one page.
+      self->mStackBase = reinterpret_cast<char*>(self->mStackBase) + guardSize;
+      stackSize -= guardSize;
+    }
+
+    self->mStackSize = stackSize;
+
+    // This is a bit of a hack.
+    //
+    // We really do want the NOHUGEPAGE flag on our thread stacks, since we
+    // don't expect any of them to need anywhere near 2MB of space. But setting
+    // it here is too late to have an effect, since the first stack page has
+    // already been faulted in existence, and NSPR doesn't give us a way to set
+    // it beforehand.
+    //
+    // What this does get us, however, is a different set of VM flags on our
+    // thread stacks compared to normal heap memory. Which makes the Linux
+    // kernel report them as separate regions, even when they are adjacent to
+    // heap memory. This allows us to accurately track the actual memory
+    // consumption of our allocated stacks.
+    madvise(self->mStackBase, stackSize, MADV_NOHUGEPAGE);
+
+    pthread_attr_destroy(&attr);
+#elif defined(XP_WIN)
+    static const DynamicallyLinkedFunctionPtr<GetCurrentThreadStackLimitsFn>
+      sGetStackLimits(L"kernel32.dll", "GetCurrentThreadStackLimits");
+
+    if (sGetStackLimits) {
+      ULONG_PTR stackBottom, stackTop;
+      sGetStackLimits(&stackBottom, &stackTop);
+      self->mStackBase = reinterpret_cast<void*>(stackBottom);
+      self->mStackSize = stackTop - stackBottom;
+    }
+#endif
+  }
+
   // Inform the ThreadManager
   nsThreadManager::get().RegisterCurrentThread(*self);
 
   mozilla::IOInterposer::RegisterCurrentThread();
 
   // This must come after the call to nsThreadManager::RegisterCurrentThread(),
   // because that call is needed to properly set up this thread as an nsThread,
   // which profiler_register_thread() requires. See bug 1347007.
@@ -563,16 +665,17 @@ nsThread::nsThread(NotNull<SynchronizedE
   , mCurrentPerformanceCounter(nullptr)
 {
 }
 
 nsThread::~nsThread()
 {
   NS_ASSERTION(mRequestedShutdownContexts.IsEmpty(),
                "shouldn't be waiting on other threads to shutdown");
+  MOZ_ASSERT(!isInList());
 #ifdef DEBUG
   // We deliberately leak these so they can be tracked by the leak checker.
   // If you're having nsThreadShutdownContext leaks, you can set:
   //   XPCOM_MEM_LOG_CLASSES=nsThreadShutdownContext
   // during a test run and that will at least tell you what thread is
   // requesting shutdown on another, which can be helpful for diagnosing
   // the leak.
   for (size_t i = 0; i < mRequestedShutdownContexts.Length(); ++i) {
@@ -596,16 +699,21 @@ nsThread::Init(const nsACString& aName)
   // ThreadFunc is responsible for setting mThread
   if (!PR_CreateThread(PR_USER_THREAD, ThreadFunc, &initData,
                        PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD,
                        PR_JOINABLE_THREAD, mStackSize)) {
     NS_RELEASE_THIS();
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
+  {
+    OffTheBooksMutexAutoLock mal(ThreadListMutex());
+    ThreadList().insertBack(this);
+  }
+
   // ThreadFunc will wait for this event to be run before it tries to access
   // mThread.  By delaying insertion of this event into the queue, we ensure
   // that mThread is set properly.
   {
     mEvents->PutEvent(do_AddRef(startup), EventPriority::Normal); // retain a reference
   }
 
   // Wait for thread to call ThreadManager::SetupCurrentThread, which completes
@@ -710,16 +818,23 @@ nsThread::ShutdownInternal(bool aSync)
     return nullptr;
   }
 
   // Prevent multiple calls to this method
   if (!mShutdownRequired.compareExchange(true, false)) {
     return nullptr;
   }
 
+  {
+    OffTheBooksMutexAutoLock mal(ThreadListMutex());
+    if (isInList()) {
+      removeFrom(ThreadList());
+    }
+  }
+
   NotNull<nsThread*> currentThread =
     WrapNotNull(nsThreadManager::get().GetCurrentThread());
 
   nsAutoPtr<nsThreadShutdownContext>& context =
     *currentThread->mRequestedShutdownContexts.AppendElement();
   context = new nsThreadShutdownContext(WrapNotNull(this), currentThread, aSync);
 
   // Set mShutdownContext and wake up the thread in case it is waiting for
--- a/xpcom/threads/nsThread.h
+++ b/xpcom/threads/nsThread.h
@@ -10,37 +10,43 @@
 #include "mozilla/Mutex.h"
 #include "nsIIdlePeriod.h"
 #include "nsIThreadInternal.h"
 #include "nsISupportsPriority.h"
 #include "nsThreadUtils.h"
 #include "nsString.h"
 #include "nsTObserverArray.h"
 #include "mozilla/Attributes.h"
+#include "mozilla/LinkedList.h"
 #include "mozilla/SynchronizedEventQueue.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/TimeStamp.h"
 #include "nsAutoPtr.h"
 #include "mozilla/AlreadyAddRefed.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/Array.h"
 #include "mozilla/dom/DocGroup.h"
 
 namespace mozilla {
 class CycleCollectedJSContext;
 class ThreadEventTarget;
 }
 
 using mozilla::NotNull;
 
+class nsThreadEnumerator;
+
 // A native thread
 class nsThread
   : public nsIThreadInternal
   , public nsISupportsPriority
+  , private mozilla::LinkedListElement<nsThread>
 {
+  friend mozilla::LinkedList<nsThread>;
+  friend mozilla::LinkedListElement<nsThread>;
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIEVENTTARGET_FULL
   NS_DECL_NSITHREAD
   NS_DECL_NSITHREADINTERNAL
   NS_DECL_NSISUPPORTSPRIORITY
 
   enum MainThreadFlag
@@ -60,16 +66,21 @@ public:
   nsresult InitCurrentThread();
 
   // The PRThread corresponding to this thread.
   PRThread* GetPRThread()
   {
     return mThread;
   }
 
+  const void* StackBase() const { return mStackBase; }
+  size_t StackSize() const { return mStackSize; }
+
+  uint32_t ThreadId() const { return mThreadId; }
+
   // If this flag is true, then the nsThread was created using
   // nsIThreadManager::NewThread.
   bool ShutdownRequired()
   {
     return mShutdownRequired;
   }
 
   // Clear the observer list.
@@ -127,48 +138,57 @@ public:
 
   bool ShuttingDown()
   {
     return mShutdownContext != nullptr;
   }
 
   virtual mozilla::PerformanceCounter* GetPerformanceCounter(nsIRunnable* aEvent);
 
+  static nsThreadEnumerator Enumerate();
+
 private:
   void DoMainThreadSpecificProcessing(bool aReallyWait);
 
 protected:
   friend class nsThreadShutdownEvent;
 
+  friend class nsThreadEnumerator;
+
   virtual ~nsThread();
 
   static void ThreadFunc(void* aArg);
 
   // Helper
   already_AddRefed<nsIThreadObserver> GetObserver()
   {
     nsIThreadObserver* obs;
     nsThread::GetObserver(&obs);
     return already_AddRefed<nsIThreadObserver>(obs);
   }
 
   struct nsThreadShutdownContext* ShutdownInternal(bool aSync);
 
+  static mozilla::OffTheBooksMutex& ThreadListMutex();
+  static mozilla::LinkedList<nsThread>& ThreadList();
+
   RefPtr<mozilla::SynchronizedEventQueue> mEvents;
   RefPtr<mozilla::ThreadEventTarget> mEventTarget;
 
   mozilla::CycleCollectedJSContext* mScriptObserver;
 
   // Only accessed on the target thread.
   nsAutoTObserverArray<NotNull<nsCOMPtr<nsIThreadObserver>>, 2> mEventObservers;
 
   int32_t   mPriority;
+  uint32_t  mThreadId;
   PRThread* mThread;
   uint32_t  mNestedEventLoopDepth;
   uint32_t  mStackSize;
+  void*     mStackBase = nullptr;
 
   // The shutdown context for ourselves.
   struct nsThreadShutdownContext* mShutdownContext;
   // The shutdown contexts for any other threads we've asked to shut down.
   nsTArray<nsAutoPtr<struct nsThreadShutdownContext>> mRequestedShutdownContexts;
 
   mozilla::Atomic<bool> mShutdownRequired;
   MainThreadFlag mIsMainThread;
@@ -179,16 +199,30 @@ protected:
   mozilla::TimeStamp mNextIdleDeadline;
   // Used to track which event is being executed by ProcessNextEvent
   nsCOMPtr<nsIRunnable> mCurrentEvent;
   mozilla::TimeStamp mCurrentEventStart;
   uint32_t mCurrentEventLoopDepth;
   RefPtr<mozilla::PerformanceCounter> mCurrentPerformanceCounter;
 };
 
+class MOZ_STACK_CLASS nsThreadEnumerator final
+{
+public:
+  nsThreadEnumerator()
+    : mMal(nsThread::ThreadListMutex())
+  {}
+
+  auto begin() { return nsThread::ThreadList().begin(); }
+  auto end() { return nsThread::ThreadList().end(); }
+
+private:
+  mozilla::OffTheBooksMutexAutoLock mMal;
+};
+
 #if defined(XP_UNIX) && !defined(ANDROID) && !defined(DEBUG) && HAVE_UALARM \
   && defined(_GNU_SOURCE)
 # define MOZ_CANARY
 
 extern int sCanaryOutputFD;
 #endif
 
 #endif  // nsThread_h__