Merge mozilla-central to mozilla-inbound. a=merge on a CLOSED TREE
authorDaniel Varga <dvarga@mozilla.com>
Sat, 11 May 2019 00:53:46 +0300
changeset 535394 ab4794c0be03d757f9304c01569d6711dd3884bb
parent 535393 2c874d7d890cd9a96aeef5de5acf4c24c4e78bd8 (current diff)
parent 535332 2abcefb31ba7b2a1c573edc5695b772826c6a078 (diff)
child 535395 131aa3a88866bc3886463b9c53710522e061b12d
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. a=merge on a CLOSED TREE
gfx/wr/webrender/src/prim_store/mod.rs
layout/painting/crashtests/1549909.html
testing/web-platform/meta/css/css-transforms/transform-flattening-001.html.ini
testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-008.html.ini
testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-009.html.ini
testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-013.html.ini
--- a/accessible/generic/Accessible.cpp
+++ b/accessible/generic/Accessible.cpp
@@ -310,23 +310,29 @@ uint64_t Accessible::VisibilityState() c
     // Element having display:contents is considered visible semantically,
     // despite it doesn't have a visually visible box.
     if (mContent->IsElement() && mContent->AsElement()->IsDisplayContents()) {
       return states::OFFSCREEN;
     }
     return states::INVISIBLE;
   }
 
-  // Walk the parent frame chain to see if there's invisible parent or the frame
-  // is in background tab.
   if (!frame->StyleVisibility()->IsVisible()) return states::INVISIBLE;
 
+  // It's invisible if the presshell is hidden by a visibility:hidden element in
+  // an ancestor document.
+  if (frame->PresShell()->IsUnderHiddenEmbedderElement()) {
+    return states::INVISIBLE;
+  }
+
   // Offscreen state if the document's visibility state is not visible.
   if (Document()->IsHidden()) return states::OFFSCREEN;
 
+  // Walk the parent frame chain to see if the frame is in background tab or
+  // scrolled out.
   nsIFrame* curFrame = frame;
   do {
     nsView* view = curFrame->GetView();
     if (view && view->GetVisibility() == nsViewVisibility_kHide)
       return states::INVISIBLE;
 
     if (nsLayoutUtils::IsPopup(curFrame)) return 0;
 
@@ -361,18 +367,16 @@ uint64_t Accessible::VisibilityState() c
         const nscoord kMinPixels = nsPresContext::CSSPixelsToAppUnits(12);
         scrollPortRect.Deflate(kMinPixels, kMinPixels);
         if (!scrollPortRect.Intersects(frameRect)) return states::OFFSCREEN;
       }
     }
 
     if (!parentFrame) {
       parentFrame = nsLayoutUtils::GetCrossDocParentFrame(curFrame);
-      if (parentFrame && !parentFrame->StyleVisibility()->IsVisible())
-        return states::INVISIBLE;
     }
 
     curFrame = parentFrame;
   } while (curFrame);
 
   // Zero area rects can occur in the first frame of a multi-frame text flow,
   // in which case the rendered text is not empty and the frame should not be
   // marked invisible.
--- a/accessible/tests/mochitest/states/test_textbox.xul
+++ b/accessible/tests/mochitest/states/test_textbox.xul
@@ -107,14 +107,14 @@
 
   <vbox flex="1">
     <textbox id="textbox"/>
     <textbox id="password" type="password"/>
 
     <textbox id="readonly_textbox" readonly="true"/>
     <textbox id="disabled_textbox" disabled="true"/>
 
-    <textbox id="searchbox" flex="1" type="search" results="historyTree"/>
+    <textbox id="searchbox" flex="1" is="search-textbox" results="historyTree"/>
     <textbox id="searchfield" placeholder="Search all add-ons"
-             type="search" searchbutton="true"/>
+             is="search-textbox" searchbutton="true"/>
   </vbox>
   </hbox>
 </window>
--- a/accessible/tests/mochitest/tree/test_txtctrl.xul
+++ b/accessible/tests/mochitest/tree/test_txtctrl.xul
@@ -37,35 +37,32 @@
       // default textbox
       testAccessibleTree("txc", accTree);
 
       //////////////////////////////////////////////////////////////////////////
       // search textbox
       accTree =
         { SECTION: [
           { ENTRY: [ { TEXT_LEAF: [] } ] },
-          { MENUPOPUP: [] }
         ] };
       testAccessibleTree("txc_search", accTree);
 
       //////////////////////////////////////////////////////////////////////////
       // search textbox with search button
 
       if (MAC) {
         accTree =
           { SECTION: [
             { ENTRY: [ { TEXT_LEAF: [] } ] },
-            { MENUPOPUP: [] }
           ] };
       } else {
         accTree =
           { SECTION: [
             { ENTRY: [ { TEXT_LEAF: [] } ] },
             { PUSHBUTTON: [] },
-            { MENUPOPUP: [] }
           ] };
       }
 
       testAccessibleTree("txc_search_searchbutton", accTree);
 
       //////////////////////////////////////////////////////////////////////////
       // number textbox
 
@@ -151,17 +148,17 @@
       <div id="content" style="display: none">
       </div>
       <pre id="test">
       </pre>
     </body>
 
     <vbox flex="1">
       <textbox id="txc" value="hello"/>
-      <textbox id="txc_search" type="search" value="hello"/>
-      <textbox id="txc_search_searchbutton" searchbutton="true" type="search" value="hello"/>
+      <textbox id="txc_search" is="search-textbox" value="hello"/>
+      <textbox id="txc_search_searchbutton" searchbutton="true" is="search-textbox" value="hello"/>
       <textbox id="txc_number" type="number" value="44"/>
       <textbox id="txc_password" type="password" value="hello"/>
       <textbox id="txc_autocomplete" type="autocomplete" value="hello"/>
     </vbox>
   </hbox>
 
 </window>
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -1104,19 +1104,19 @@ var gIdentityHandler = {
       if (aPermission.state == SitePermissions.getDefault(aPermission.id)) {
         menulist.value = "0";
       } else {
         menulist.value = aPermission.state;
       }
 
       // Avoiding listening to the "select" event on purpose. See Bug 1404262.
       menulist.addEventListener("command", () => {
-        SitePermissions.set(gBrowser.currentURI,
-                            aPermission.id,
-                            menulist.selectedItem.value);
+        SitePermissions.setForPrincipal(gBrowser.contentPrincipal,
+                                        aPermission.id,
+                                        menulist.selectedItem.value);
       });
 
       container.appendChild(img);
       container.appendChild(nameLabel);
       container.appendChild(menulist);
       container.setAttribute("aria-labelledby", nameLabelId);
       block.appendChild(container);
 
@@ -1160,35 +1160,35 @@ var gIdentityHandler = {
           ["camera", "microphone", "screen"].includes(aPermission.id)) {
         let windowId = this._sharingState.windowId;
         if (aPermission.id == "screen") {
           windowId = "screen:" + windowId;
         } else {
           // If we set persistent permissions or the sharing has
           // started due to existing persistent permissions, we need
           // to handle removing these even for frames with different hostnames.
-          let uris = browser._devicePermissionURIs || [];
-          for (let uri of uris) {
+          let principals = browser._devicePermissionPrincipals || [];
+          for (let principal of principals) {
             // It's not possible to stop sharing one of camera/microphone
             // without the other.
             for (let id of ["camera", "microphone"]) {
               if (this._sharingState[id]) {
-                let perm = SitePermissions.get(uri, id);
+                let perm = SitePermissions.getForPrincipal(principal, id);
                 if (perm.state == SitePermissions.ALLOW &&
                     perm.scope == SitePermissions.SCOPE_PERSISTENT) {
-                  SitePermissions.remove(uri, id);
+                  SitePermissions.removeFromPrincipal(principal, id);
                 }
               }
             }
           }
         }
         browser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId);
         webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser);
       }
-      SitePermissions.remove(gBrowser.currentURI, aPermission.id, browser);
+      SitePermissions.removeFromPrincipal(gBrowser.contentPrincipal, aPermission.id, browser);
 
       this._permissionReloadHint.removeAttribute("hidden");
       PanelView.forNode(this._identityPopupMainView)
                .descriptionHeightWorkaround();
     });
 
     container.appendChild(button);
 
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -713,16 +713,17 @@ html|input.urlbar-input {
   min-width: 200px;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] {
   -moz-binding: none;
   height: auto;
 }
 
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .ac-site-icon,
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-site-icon {
   margin-inline-start: 0;
   display: initial;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > .ac-text-overflow-container > .ac-title-text {
   text-overflow: initial;
   white-space: initial;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7250,75 +7250,78 @@ var CanvasPermissionPromptHelper = {
   },
 
   uninit() {
     Services.obs.removeObserver(this, this._permissionsPrompt);
     Services.obs.removeObserver(this, this._permissionsPromptHideDoorHanger);
   },
 
   // aSubject is an nsIBrowser (e10s) or an nsIDOMWindow (non-e10s).
-  // aData is an URL string.
+  // aData is an Origin string.
   observe(aSubject, aTopic, aData) {
     if (aTopic != this._permissionsPrompt &&
         aTopic != this._permissionsPromptHideDoorHanger) {
       return;
     }
 
     let browser;
     if (aSubject instanceof Ci.nsIDOMWindow) {
       let contentWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
       browser = contentWindow.docShell.chromeEventHandler;
     } else {
       browser = aSubject;
     }
 
-    let uri = Services.io.newURI(aData);
     if (gBrowser.selectedBrowser !== browser) {
       // Must belong to some other window.
       return;
     }
 
     let message = gNavigatorBundle.getFormattedString("canvas.siteprompt", ["<>"], 1);
 
-    function setCanvasPermission(aURI, aPerm, aPersistent) {
-      Services.perms.add(aURI, "canvas", aPerm,
-                          aPersistent ? Ci.nsIPermissionManager.EXPIRE_NEVER
-                                      : Ci.nsIPermissionManager.EXPIRE_SESSION);
+    let principal = Services.scriptSecurityManager
+                            .createCodebasePrincipalFromOrigin(aData);
+
+    function setCanvasPermission(aPerm, aPersistent) {
+      Services.perms.addFromPrincipal(
+        principal, "canvas", aPerm,
+        aPersistent ? Ci.nsIPermissionManager.EXPIRE_NEVER
+                    : Ci.nsIPermissionManager.EXPIRE_SESSION);
     }
 
     let mainAction = {
       label: gNavigatorBundle.getString("canvas.allow"),
       accessKey: gNavigatorBundle.getString("canvas.allow.accesskey"),
       callback(state) {
-        setCanvasPermission(uri, Ci.nsIPermissionManager.ALLOW_ACTION,
+        setCanvasPermission(Ci.nsIPermissionManager.ALLOW_ACTION,
                             state && state.checkboxChecked);
       },
     };
 
     let secondaryActions = [{
       label: gNavigatorBundle.getString("canvas.notAllow"),
       accessKey: gNavigatorBundle.getString("canvas.notAllow.accesskey"),
       callback(state) {
-        setCanvasPermission(uri, Ci.nsIPermissionManager.DENY_ACTION,
+        setCanvasPermission(Ci.nsIPermissionManager.DENY_ACTION,
                             state && state.checkboxChecked);
       },
     }];
 
     let checkbox = {
       // In PB mode, we don't want the "always remember" checkbox
       show: !PrivateBrowsingUtils.isWindowPrivate(window),
     };
     if (checkbox.show) {
       checkbox.checked = true;
       checkbox.label = gBrowserBundle.GetStringFromName("canvas.remember");
     }
 
     let options = {
       checkbox,
-      name: uri.asciiHost,
+      name: principal.URI.host,
       learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "fingerprint-permission",
       dismissed: aTopic == this._permissionsPromptHideDoorHanger,
     };
     PopupNotifications.show(browser, this._permissionsPrompt, message,
                             this._notificationIcon, mainAction,
                             secondaryActions, options);
   },
 };
--- a/browser/base/content/pageinfo/permissions.js
+++ b/browser/base/content/pageinfo/permissions.js
@@ -1,47 +1,45 @@
 /* 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/. */
 
 /* import-globals-from pageInfo.js */
 
 const {SitePermissions} = ChromeUtils.import("resource:///modules/SitePermissions.jsm");
 
-var gPermURI;
 var gPermPrincipal;
 var gUsageRequest;
 
 // Array of permissionIDs sorted alphabetically by label.
 var gPermissions = SitePermissions.listPermissions()
   .filter(p => SitePermissions.getPermissionLabel(p) != null)
   .sort((a, b) => {
     let firstLabel = SitePermissions.getPermissionLabel(a);
     let secondLabel = SitePermissions.getPermissionLabel(b);
     return firstLabel.localeCompare(secondLabel);
   });
 
 var permissionObserver = {
   observe(aSubject, aTopic, aData) {
     if (aTopic == "perm-changed") {
       var permission = aSubject.QueryInterface(Ci.nsIPermission);
-      if (permission.matchesURI(gPermURI, true) && gPermissions.includes(permission.type)) {
+      if (permission.matches(gPermPrincipal, true) && gPermissions.includes(permission.type)) {
           initRow(permission.type);
       }
     }
   },
 };
 
 function onLoadPermission(uri, principal) {
   var permTab = document.getElementById("permTab");
-  if (SitePermissions.isSupportedURI(uri)) {
-    gPermURI = uri;
+  if (SitePermissions.isSupportedPrincipal(principal)) {
     gPermPrincipal = principal;
     var hostText = document.getElementById("hostText");
-    hostText.value = gPermURI.displayPrePath;
+    hostText.value = uri.displayPrePath;
 
     for (var i of gPermissions) {
       initRow(i);
     }
     Services.obs.addObserver(permissionObserver, "perm-changed");
     onUnloadRegistry.push(onUnloadPermission);
     permTab.hidden = false;
   } else {
@@ -58,17 +56,17 @@ function onUnloadPermission() {
   }
 }
 
 function initRow(aPartId) {
   createRow(aPartId);
 
   var checkbox = document.getElementById(aPartId + "Def");
   var command  = document.getElementById("cmd_" + aPartId + "Toggle");
-  var {state, scope} = SitePermissions.get(gPermURI, aPartId);
+  var {state, scope} = SitePermissions.getForPrincipal(gPermPrincipal, aPartId);
   let defaultState = SitePermissions.getDefault(aPartId);
 
   // Since cookies preferences have many different possible configuration states
   // we don't consider any permission except "no permission" to be default.
   if (aPartId == "cookie") {
     state = Services.perms.testPermissionFromPrincipal(gPermPrincipal, "cookie");
 
     if (state == SitePermissions.UNKNOWN) {
@@ -163,29 +161,29 @@ function createRow(aPartId) {
 
   document.getElementById("permList").appendChild(row);
 }
 
 function onCheckboxClick(aPartId) {
   var command  = document.getElementById("cmd_" + aPartId + "Toggle");
   var checkbox = document.getElementById(aPartId + "Def");
   if (checkbox.checked) {
-    SitePermissions.remove(gPermURI, aPartId);
+    SitePermissions.removeFromPrincipal(gPermPrincipal, aPartId);
     command.setAttribute("disabled", "true");
   } else {
     onRadioClick(aPartId);
     command.removeAttribute("disabled");
   }
 }
 
 function onRadioClick(aPartId) {
   var radioGroup = document.getElementById(aPartId + "RadioGroup");
   var id = radioGroup.selectedItem ? radioGroup.selectedItem.id : "#1";
   var permission = parseInt(id.split("#")[1]);
-  SitePermissions.set(gPermURI, aPartId, permission);
+  SitePermissions.setForPrincipal(gPermPrincipal, aPartId, permission);
 }
 
 function setRadioState(aPartId, aValue) {
   var radio = document.getElementById(aPartId + "#" + aValue);
   if (radio) {
     radio.radioGroup.selectedItem = radio;
   }
 }
--- a/browser/base/content/test/forms/browser_selectpopup.js
+++ b/browser/base/content/test/forms/browser_selectpopup.js
@@ -635,17 +635,17 @@ async function performSelectSearchTests(
 
     select.options[1].selected = true;
     select.focus();
   });
 
   let selectPopup = win.document.getElementById("ContentSelectDropdown").menupopup;
   await openSelectPopup(selectPopup, false, "select", win);
 
-  let searchElement = selectPopup.querySelector("textbox");
+  let searchElement = selectPopup.querySelector(".contentSelectDropdown-searchbox");
   searchElement.focus();
 
   EventUtils.synthesizeKey("O", {}, win);
   is(selectPopup.children[2].hidden, false, "First option should be visible");
   is(selectPopup.children[3].hidden, false, "Second option should be visible");
 
   EventUtils.synthesizeKey("3", {}, win);
   is(selectPopup.children[2].hidden, true, "First option should be hidden");
--- a/browser/base/content/test/forms/browser_selectpopup_searchfocus.js
+++ b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js
@@ -23,20 +23,19 @@ add_task(async function test_focus_on_se
 
   let menulist = document.getElementById("ContentSelectDropdown");
   let selectPopup = menulist.menupopup;
 
   let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
   await BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mousedown" }, gBrowser.selectedBrowser);
   await popupShownPromise;
 
-  let searchInput = selectPopup.querySelector("textbox[type='search']");
+  let searchInput = selectPopup.querySelector(".contentSelectDropdown-searchbox");
   searchInput.scrollIntoView();
-  let searchFocused = BrowserTestUtils.waitForEvent(searchInput, "focus");
+  let searchFocused = BrowserTestUtils.waitForEvent(searchInput, "focus", true);
   await EventUtils.synthesizeMouseAtCenter(searchInput, {}, window);
   await searchFocused;
 
   is(selectPopup.state, "open", "select popup should still be open after clicking on the search field");
 
   await hideSelectPopup(selectPopup, "escape");
   BrowserTestUtils.removeTab(tab);
 });
-
--- a/browser/components/places/content/bookmarksSidebar.xul
+++ b/browser/components/places/content/bookmarksSidebar.xul
@@ -32,17 +32,17 @@
   <script src="chrome://browser/content/places/places-tree.js"/>
   <script src="chrome://global/content/editMenuOverlay.js"/>
 
 #include placesCommands.inc.xul
 #include placesContextMenu.inc.xul
 #include bookmarksHistoryTooltip.inc.xul
 
   <hbox id="sidebar-search-container" align="center">
-    <textbox id="search-box" flex="1" type="search"
+    <textbox id="search-box" flex="1" is="search-textbox"
              placeholder="&bookmarksSearch.placeholder;"
              aria-controls="bookmarks-view"
              oncommand="searchBookmarks(this.value);"/>
   </hbox>
 
   <tree id="bookmarks-view"
         class="sidebar-placesTree"
         is="places-tree"
--- a/browser/components/places/content/historySidebar.xul
+++ b/browser/components/places/content/historySidebar.xul
@@ -40,17 +40,17 @@
     <key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/>
   </keyset>
 #endif
 
 #include placesContextMenu.inc.xul
 #include bookmarksHistoryTooltip.inc.xul
 
   <hbox id="sidebar-search-container">
-    <textbox id="search-box" flex="1" type="search"
+    <textbox id="search-box" flex="1" is="search-textbox"
              placeholder="&historySearch.placeholder;"
              aria-controls="historyTree"
              oncommand="searchHistory(this.value);"/>
     <button id="viewButton" style="min-width:0px !important;" type="menu"
             label="&view.label;" accesskey="&view.accesskey;" selectedsort="day"
             persist="selectedsort">
       <menupopup>
         <menuitem id="bydayandsite" label="&byDayAndSite.label;"
--- a/browser/components/places/content/places.xul
+++ b/browser/components/places/content/places.xul
@@ -328,17 +328,17 @@
         </menu>
       </menubar>
 #endif
 
       <spacer id="libraryToolbarSpacer" flex="1"/>
 
       <textbox id="searchFilter"
                flex="1"
-               type="search"
+               is="search-textbox"
                aria-controls="placeContent"
                oncommand="PlacesSearchBox.search(this.value);"
                collection="bookmarks">
       </textbox>
       <toolbarbutton id="clearDownloadsButton"
 #ifdef XP_MACOSX
                      class="tabbable"
 #endif
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -378,17 +378,17 @@
            data-l10n-id="download-always-ask-where"/>
   </radiogroup>
 </groupbox>
 
 <groupbox id="applicationsGroup" data-category="paneGeneral" hidden="true">
   <label><html:h2 data-l10n-id="applications-header"/></label>
   <description data-l10n-id="applications-description"/>
   <textbox id="filter" flex="1"
-           type="search"
+           is="search-textbox"
            data-l10n-id="applications-filter"
            aria-controls="handlersView"/>
 
   <listheader equalsize="always">
     <treecol id="typeColumn" data-l10n-id="applications-type-column" value="type"
              persist="sortDirection"
              flex="1" sortDirection="ascending"/>
     <treecol id="actionColumn" data-l10n-id="applications-action-column" value="action"
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -168,17 +168,17 @@
             <hbox align="top">
               <image class="info-icon"></image>
             </hbox>
             <hbox align="center" flex="1">
               <label class="policies-label" flex="1" data-l10n-id="policies-notice"></label>
             </hbox>
           </hbox>
           <textbox
-            type="search" id="searchInput"
+            is="search-textbox" id="searchInput"
             data-l10n-id="search-input-box"
             data-l10n-attrs="style"
             hidden="true" clickSelectsAll="true"/>
         </hbox>
         <vbox id="mainPrefPane">
 #include searchResults.xul
 #include main.xul
 #include home.xul
--- a/browser/components/preferences/siteDataSettings.xul
+++ b/browser/components/preferences/siteDataSettings.xul
@@ -26,17 +26,17 @@
 
   <script src="chrome://browser/content/preferences/siteDataSettings.js"/>
 
   <vbox flex="1" class="contentPane">
     <description id="settingsDescription" data-l10n-id="site-data-settings-description"/>
     <separator class="thin"/>
 
     <hbox id="searchBoxContainer">
-      <textbox id="searchBox" type="search" flex="1"
+      <textbox id="searchBox" is="search-textbox" flex="1"
         data-l10n-id="site-data-search-textbox"/>
     </hbox>
     <separator class="thin"/>
 
     <listheader>
       <treecol flex="4" width="50" data-l10n-id="site-data-column-host" id="hostCol"/>
       <treecol flex="1" width="50" data-l10n-id="site-data-column-cookies" id="cookiesCol"/>
       <!-- Sorted by usage so the user can quickly see which sites use the most data. -->
--- a/browser/components/preferences/sitePermissions.js
+++ b/browser/components/preferences/sitePermissions.js
@@ -369,23 +369,21 @@ var gSitePermissionsManager = {
 
   onApplyChanges() {
     // Stop observing permission changes since we are about
     // to write out the pending adds/deletes and don't need
     // to update the UI
     this.uninit();
 
     for (let p of this._permissionsToChange.values()) {
-      let uri = Services.io.newURI(p.origin);
-      SitePermissions.set(uri, p.type, p.capability);
+      SitePermissions.setForPrincipal(p.principal, p.type, p.capability);
     }
 
     for (let p of this._permissionsToDelete.values()) {
-      let uri = Services.io.newURI(p.origin);
-      SitePermissions.remove(uri, p.type);
+      SitePermissions.removeFromPrincipal(p.principal, p.type);
     }
 
     if (this._checkbox.checked) {
       Services.prefs.setIntPref(this._defaultPermissionStatePrefName, SitePermissions.BLOCK);
     } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
       Services.prefs.setIntPref(this._defaultPermissionStatePrefName, SitePermissions.UNKNOWN);
     }
 
--- a/browser/components/preferences/sitePermissions.xul
+++ b/browser/components/preferences/sitePermissions.xul
@@ -31,17 +31,17 @@
     <key data-l10n-id="permissions-close-key" modifiers="accel" oncommand="window.close();"/>
   </keyset>
 
   <vbox class="contentPane">
     <description id="permissionsText" control="url"/>
     <separator class="thin"/>
     <hbox align="start">
       <textbox id="searchBox" flex="1" data-l10n-id="permissions-searchbox"
-               type="search" oncommand="gSitePermissionsManager.buildPermissionsList();"/>
+               is="search-textbox" oncommand="gSitePermissionsManager.buildPermissionsList();"/>
     </hbox>
     <separator class="thin"/>
     <listheader>
       <treecol id="siteCol" data-l10n-id="permissions-site-name" flex="3" width="50"
                onclick="gSitePermissionsManager.buildPermissionsList(event.target)"/>
       <treecol id="statusCol" data-l10n-id="permissions-status" flex="1" width="50"
                data-isCurrentSortCol="true"
                onclick="gSitePermissionsManager.buildPermissionsList(event.target);"/>
@@ -61,17 +61,17 @@
             icon="clear"
             oncommand="gSitePermissionsManager.onAllPermissionsDelete();"/>
   </hbox>
 
   <spacer flex="1"/>
   <checkbox id="permissionsDisableCheckbox"/>
   <description id="permissionsDisableDescription"/>
   <spacer flex="1"/>
-  <hbox id="browserNotificationsPermissionExtensionContent" 
+  <hbox id="browserNotificationsPermissionExtensionContent"
         class="extension-controlled" align="center" hidden="true">
     <description control="disableNotificationsPermissionExtension" flex="1"/>
     <button id="disableNotificationsPermissionExtension"
             class="extension-controlled-button accessory-button"
             data-l10n-id="disable-extension"/>
   </hbox>
   <hbox class="actionButtons" align="right" flex="1">
     <button oncommand="close();" icon="close" id="cancel"
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -63,17 +63,17 @@ var Translation = {
                                                  : aData.state;
     trUI.detectedLanguage = aData.detectedLanguage;
     trUI.translatedFrom = aData.translatedFrom;
     trUI.translatedTo = aData.translatedTo;
     trUI.originalShown = aData.originalShown;
 
     trUI.showURLBarIcon();
 
-    if (trUI.shouldShowInfoBar(aBrowser.currentURI))
+    if (trUI.shouldShowInfoBar(aBrowser.contentPrincipal))
       trUI.showTranslationInfoBar();
   },
 
   openProviderAttribution() {
     let attribution = this.supportedEngines[this.translationEngine];
     const {BrowserWindowTracker} = ChromeUtils.import("resource:///modules/BrowserWindowTracker.jsm");
     BrowserWindowTracker.getTopWindow().openWebLinkIn(attribution, "tab");
   },
@@ -229,33 +229,33 @@ TranslationUI.prototype = {
     let notificationBox = this.notificationBox;
     let notif = notificationBox.appendNotification("", "translation", null,
       notificationBox.PRIORITY_INFO_HIGH, null, null,
       "translation-notification");
     notif.init(this);
     return notif;
   },
 
-  shouldShowInfoBar(aURI) {
+  shouldShowInfoBar(aPrincipal) {
     // Never show the infobar automatically while the translation
     // service is temporarily unavailable.
     if (Translation.serviceUnavailable)
       return false;
 
     // Check if we should never show the infobar for this language.
     let neverForLangs =
       Services.prefs.getCharPref("browser.translation.neverForLanguages");
     if (neverForLangs.split(",").includes(this.detectedLanguage)) {
       TranslationTelemetry.recordAutoRejectedTranslationOffer();
       return false;
     }
 
     // or if we should never show the infobar for this domain.
     let perms = Services.perms;
-    if (perms.testExactPermission(aURI, "translate") == perms.DENY_ACTION) {
+    if (perms.testExactPermissionFromPrincipal(aPrincipal, "translate") == perms.DENY_ACTION) {
       TranslationTelemetry.recordAutoRejectedTranslationOffer();
       return false;
     }
 
     return true;
   },
 
   receiveMessage(msg) {
--- a/browser/components/translation/content/translation-notification.js
+++ b/browser/components/translation/content/translation-notification.js
@@ -307,40 +307,40 @@ class MozTranslationNotification extends
 
     // We may need to disable the menuitems if they have already been used.
     // Check if translation is already disabled for this language:
     let neverForLangs =
       Services.prefs.getCharPref("browser.translation.neverForLanguages");
     item.disabled = neverForLangs.split(",").includes(lang);
 
     // Check if translation is disabled for the domain:
-    let uri = this.translation.browser.currentURI;
+    let principal = this.translation.browser.contentPrincipal;
     let perms = Services.perms;
     item = this._getAnonElt("neverForSite");
     item.disabled =
-      perms.testExactPermission(uri, "translate") == perms.DENY_ACTION;
+      perms.testExactPermissionFromPrincipal(principal, "translate") == perms.DENY_ACTION;
   }
 
   neverForLanguage() {
     const kPrefName = "browser.translation.neverForLanguages";
 
     let val = Services.prefs.getCharPref(kPrefName);
     if (val)
       val += ",";
     val += this._getAnonElt("neverForLanguage").langCode;
 
     Services.prefs.setCharPref(kPrefName, val);
 
     this.closeCommand();
   }
 
   neverForSite() {
-    let uri = this.translation.browser.currentURI;
+    let principal = this.translation.browser.contentPrincipal;
     let perms = Services.perms;
-    perms.add(uri, "translate", perms.DENY_ACTION);
+    perms.addFromPrincipal(principal, "translate", perms.DENY_ACTION);
 
     this.closeCommand();
   }
 
   openProviderAttribution() {
     Translation.openProviderAttribution();
   }
 }
--- a/browser/components/translation/test/browser_translation_exceptions.js
+++ b/browser/components/translation/test/browser_translation_exceptions.js
@@ -107,18 +107,18 @@ var gTests = [
     // Show the infobar for example.com and fr.
     Translation.documentStateReceived(gBrowser.selectedBrowser,
                                       {state: Translation.STATE_OFFER,
                                        originalShown: true,
                                        detectedLanguage: "fr"});
     let notif = await getInfoBar();
     ok(notif, "the infobar is visible");
     let ui = gBrowser.selectedBrowser.translationUI;
-    let uri = gBrowser.selectedBrowser.currentURI;
-    ok(ui.shouldShowInfoBar(uri, "fr"),
+    let principal = gBrowser.selectedBrowser.contentPrincipal;
+    ok(ui.shouldShowInfoBar(principal, "fr"),
        "check shouldShowInfoBar initially returns true");
 
     // Open the "options" drop down.
     await openPopup(notif._getAnonElt("options"));
     ok(notif._getAnonElt("options").getAttribute("open"),
        "the options menu is open");
 
     // Check that the item is not disabled.
@@ -129,17 +129,17 @@ var gTests = [
     notif._getAnonElt("neverForLanguage").click();
     notif = await getInfoBar();
     ok(!notif, "infobar hidden");
 
     // Check this has been saved to the exceptions list.
     let langs = getLanguageExceptions();
     is(langs.length, 1, "one language in the exception list");
     is(langs[0], "fr", "correct language in the exception list");
-    ok(!ui.shouldShowInfoBar(uri, "fr"),
+    ok(!ui.shouldShowInfoBar(principal, "fr"),
        "the infobar wouldn't be shown anymore");
 
     // Reopen the infobar.
     PopupNotifications.getNotification("translate").anchorElement.click();
     notif = await getInfoBar();
     // Open the "options" drop down.
     await openPopup(notif._getAnonElt("options"));
     ok(notif._getAnonElt("neverForLanguage").disabled,
@@ -157,18 +157,18 @@ var gTests = [
     // Show the infobar for example.com and fr.
     Translation.documentStateReceived(gBrowser.selectedBrowser,
                                       {state: Translation.STATE_OFFER,
                                        originalShown: true,
                                        detectedLanguage: "fr"});
     let notif = await getInfoBar();
     ok(notif, "the infobar is visible");
     let ui = gBrowser.selectedBrowser.translationUI;
-    let uri = gBrowser.selectedBrowser.currentURI;
-    ok(ui.shouldShowInfoBar(uri, "fr"),
+    let principal = gBrowser.selectedBrowser.contentPrincipal;
+    ok(ui.shouldShowInfoBar(principal, "fr"),
        "check shouldShowInfoBar initially returns true");
 
     // Open the "options" drop down.
     await openPopup(notif._getAnonElt("options"));
     ok(notif._getAnonElt("options").getAttribute("open"),
        "the options menu is open");
 
     // Check that the item is not disabled.
@@ -179,17 +179,17 @@ var gTests = [
     notif._getAnonElt("neverForSite").click();
     notif = await getInfoBar();
     ok(!notif, "infobar hidden");
 
     // Check this has been saved to the exceptions list.
     let sites = getDomainExceptions();
     is(sites.length, 1, "one site in the exception list");
     is(sites[0].origin, "http://example.com", "correct site in the exception list");
-    ok(!ui.shouldShowInfoBar(uri, "fr"),
+    ok(!ui.shouldShowInfoBar(principal, "fr"),
        "the infobar wouldn't be shown anymore");
 
     // Reopen the infobar.
     PopupNotifications.getNotification("translate").anchorElement.click();
     notif = await getInfoBar();
     // Open the "options" drop down.
     await openPopup(notif._getAnonElt("options"));
     ok(notif._getAnonElt("neverForSite").disabled,
--- a/browser/modules/PermissionUI.jsm
+++ b/browser/modules/PermissionUI.jsm
@@ -359,19 +359,19 @@ var PermissionPromptPrototype = {
       return;
     }
 
     if (this.usePermissionManager &&
         this.permissionKey) {
       // If we're reading and setting permissions, then we need
       // to check to see if we already have a permission setting
       // for this particular principal.
-      let {state} = SitePermissions.get(requestingURI,
-                                        this.permissionKey,
-                                        this.browser);
+      let {state} = SitePermissions.getForPrincipal(this.principal,
+                                                    this.permissionKey,
+                                                    this.browser);
 
       if (state == SitePermissions.BLOCK) {
         // If this block was done based on a global user setting, we want to show
         // a post prompt to give the user some more granular control without
         // annoying them too much.
         if (this.postPromptEnabled &&
             SitePermissions.getDefault(this.permissionKey) == SitePermissions.BLOCK) {
           this.postPrompt();
@@ -434,29 +434,29 @@ var PermissionPromptPrototype = {
             if ((state && state.checkboxChecked && state.source != "esc-press") ||
                 promptAction.scope == SitePermissions.SCOPE_PERSISTENT) {
               // Permanently store permission.
               let scope = SitePermissions.SCOPE_PERSISTENT;
               // Only remember permission for session if in PB mode.
               if (PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
                 scope = SitePermissions.SCOPE_SESSION;
               }
-              SitePermissions.set(this.principal.URI,
-                                  this.permissionKey,
-                                  promptAction.action,
-                                  scope);
+              SitePermissions.setForPrincipal(this.principal,
+                                              this.permissionKey,
+                                              promptAction.action,
+                                              scope);
             } else if (promptAction.action == SitePermissions.BLOCK) {
               // Temporarily store BLOCK permissions only
               // SitePermissions does not consider subframes when storing temporary
               // permissions on a tab, thus storing ALLOW could be exploited.
-              SitePermissions.set(this.principal.URI,
-                                  this.permissionKey,
-                                  promptAction.action,
-                                  SitePermissions.SCOPE_TEMPORARY,
-                                  this.browser);
+              SitePermissions.setForPrincipal(this.principal,
+                                              this.permissionKey,
+                                              promptAction.action,
+                                              SitePermissions.SCOPE_TEMPORARY,
+                                              this.browser);
             }
 
             // Grant permission if action is ALLOW.
             // Record buttonAction for telemetry.
             if (promptAction.action == SitePermissions.ALLOW) {
               this._buttonAction = "accept";
               this.allow();
             } else {
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -254,34 +254,51 @@ var SitePermissions = {
   SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}",
   SCOPE_POLICY: "{SitePermissions.SCOPE_POLICY}",
   SCOPE_GLOBAL: "{SitePermissions.SCOPE_GLOBAL}",
 
   _permissionsArray: null,
   _defaultPrefBranch: Services.prefs.getBranch("permissions.default."),
 
   /**
+   * Deprecated! Please use getAllByPrincipal(principal) instead.
    * Gets all custom permissions for a given URI.
    * Install addon permission is excluded, check bug 1303108.
    *
    * @return {Array} a list of objects with the keys:
    *          - id: the permissionId of the permission
    *          - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY)
    *          - state: a constant representing the current permission state
    *            (e.g. SitePermissions.ALLOW)
    */
   getAllByURI(uri) {
     if (!(uri instanceof Ci.nsIURI))
       throw new Error("uri parameter should be an nsIURI");
+
+    let principal = uri ? Services.scriptSecurityManager.createCodebasePrincipal(uri, {}) : null;
+    return this.getAllByPrincipal(principal);
+  },
+
+  /**
+   * Gets all custom permissions for a given principal.
+   * Install addon permission is excluded, check bug 1303108.
+   *
+   * @return {Array} a list of objects with the keys:
+   *          - id: the permissionId of the permission
+   *          - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY)
+   *          - state: a constant representing the current permission state
+   *            (e.g. SitePermissions.ALLOW)
+   */
+  getAllByPrincipal(principal) {
     let result = [];
-    if (!this.isSupportedURI(uri)) {
+    if (!this.isSupportedPrincipal(principal)) {
       return result;
     }
 
-    let permissions = Services.perms.getAllForURI(uri);
+    let permissions = Services.perms.getAllForPrincipal(principal);
     while (permissions.hasMoreElements()) {
       let permission = permissions.getNext();
 
       // filter out unknown permissions
       if (gPermissionObject[permission.type]) {
         // XXX Bug 1303108 - Control Center should only show non-default permissions
         if (permission.type == "install") {
           continue;
@@ -333,17 +350,17 @@ var SitePermissions = {
       permission.scope = this.SCOPE_TEMPORARY;
       permissions[permission.id] = permission;
     }
 
     for (let permission of GloballyBlockedPermissions.getAll(browser)) {
       permissions[permission.id] = permission;
     }
 
-    for (let permission of this.getAllByURI(browser.currentURI)) {
+    for (let permission of this.getAllByPrincipal(browser.contentPrincipal)) {
       permissions[permission.id] = permission;
     }
 
     return Object.values(permissions);
   },
 
   /**
    * Returns a list of objects with detailed information on all permissions
@@ -361,30 +378,46 @@ var SitePermissions = {
    *           - label: the localized label, or null if none is available.
    */
   getAllPermissionDetailsForBrowser(browser) {
     return this.getAllForBrowser(browser).map(({id, scope, state}) =>
       ({id, scope, state, label: this.getPermissionLabel(id)}));
   },
 
   /**
+   * Deprecated! Please use isSupportedPrincipal(principal) instead.
    * Checks whether a UI for managing permissions should be exposed for a given
    * URI. This excludes file URIs, for instance, as they don't have a host,
    * even though nsIPermissionManager can still handle them.
    *
    * @param {nsIURI} uri
    *        The URI to check.
    *
    * @return {boolean} if the URI is supported.
    */
   isSupportedURI(uri) {
     return uri && ["http", "https", "moz-extension"].includes(uri.scheme);
   },
 
   /**
+   * Checks whether a UI for managing permissions should be exposed for a given
+   * principal. This excludes file URIs, for instance, as they don't have a host,
+   * even though nsIPermissionManager can still handle them.
+   *
+   * @param {nsIPrincipal} principal
+   *        The principal to check.
+   *
+   * @return {boolean} if the principal is supported.
+   */
+  isSupportedPrincipal(principal) {
+    return principal && principal.URI &&
+      ["http", "https", "moz-extension"].includes(principal.URI.scheme);
+  },
+
+ /**
    * Gets an array of all permission IDs.
    *
    * @return {Array<String>} an array of all permission IDs.
    */
   listPermissions() {
     if (this._permissionsArray === null) {
       let permissions = Object.keys(gPermissionObject);
 
@@ -473,25 +506,50 @@ var SitePermissions = {
    *           - state: The current state of the permission
    *             (e.g. SitePermissions.ALLOW)
    *           - scope: The scope of the permission
    *             (e.g. SitePermissions.SCOPE_PERSISTENT)
    */
   get(uri, permissionID, browser) {
     if ((!uri && !browser) || (uri && !(uri instanceof Ci.nsIURI)))
       throw new Error("uri parameter should be an nsIURI or a browser parameter is needed");
+
+    let principal = uri ? Services.scriptSecurityManager.createCodebasePrincipal(uri, {}) : null;
+    return this.getForPrincipal(principal, permissionID, browser);
+  },
+
+ /**
+   * Returns the state and scope of a particular permission for a given principal.
+   *
+   * This method will NOT dispatch a "PermissionStateChange" event on the specified
+   * browser if a temporary permission was removed because it has expired.
+   *
+   * @param {nsIPrincipal} principal
+   *        The principal to check.
+   * @param {String} permissionID
+   *        The id of the permission.
+   * @param {Browser} browser (optional)
+   *        The browser object to check for temporary permissions.
+   *
+   * @return {Object} an object with the keys:
+   *           - state: The current state of the permission
+   *             (e.g. SitePermissions.ALLOW)
+   *           - scope: The scope of the permission
+   *             (e.g. SitePermissions.SCOPE_PERSISTENT)
+   */
+  getForPrincipal(principal, permissionID, browser) {
     let defaultState = this.getDefault(permissionID);
     let result = { state: defaultState, scope: this.SCOPE_PERSISTENT };
-    if (this.isSupportedURI(uri)) {
+    if (this.isSupportedPrincipal(principal)) {
       let permission = null;
       if (permissionID in gPermissionObject &&
         gPermissionObject[permissionID].exactHostMatch) {
-        permission = Services.perms.getPermissionObjectForURI(uri, permissionID, true);
+        permission = Services.perms.getPermissionObject(principal, permissionID, true);
       } else {
-        permission = Services.perms.getPermissionObjectForURI(uri, permissionID, false);
+        permission = Services.perms.getPermissionObject(principal, permissionID, false);
       }
 
       if (permission) {
         result.state = permission.capability;
         if (permission.expireType == Services.perms.EXPIRE_SESSION) {
           result.scope = this.SCOPE_SESSION;
         } else if (permission.expireType == Services.perms.EXPIRE_POLICY) {
           result.scope = this.SCOPE_POLICY;
@@ -509,16 +567,17 @@ var SitePermissions = {
         result.scope = this.SCOPE_TEMPORARY;
       }
     }
 
     return result;
   },
 
   /**
+   * Deprecated! Use setForPrincipal(...) instead.
    * Sets the state of a particular permission for a given URI or browser.
    * This method will dispatch a "PermissionStateChange" event on the specified
    * browser if a temporary permission was set
    *
    * @param {nsIURI} uri
    *        The URI to set the permission for.
    *        Note that this will be ignored if the scope is set to SCOPE_TEMPORARY
    * @param {String} permissionID
@@ -529,87 +588,130 @@ var SitePermissions = {
    *        The scope of the permission. Defaults to SCOPE_PERSISTENT.
    * @param {Browser} browser (optional)
    *        The browser object to set temporary permissions on.
    *        This needs to be provided if the scope is SCOPE_TEMPORARY!
    */
   set(uri, permissionID, state, scope = this.SCOPE_PERSISTENT, browser = null) {
     if ((!uri && !browser) || (uri && !(uri instanceof Ci.nsIURI)))
       throw new Error("uri parameter should be an nsIURI or a browser parameter is needed");
+
+    let principal = uri ? Services.scriptSecurityManager.createCodebasePrincipal(uri, {}) : null;
+    return this.setForPrincipal(principal, permissionID, state, scope, browser);
+  },
+
+  /**
+   * Sets the state of a particular permission for a given principal or browser.
+   * This method will dispatch a "PermissionStateChange" event on the specified
+   * browser if a temporary permission was set
+   *
+   * @param {nsIPrincipal} principal
+   *        The principal to set the permission for.
+   *        Note that this will be ignored if the scope is set to SCOPE_TEMPORARY
+   * @param {String} permissionID
+   *        The id of the permission.
+   * @param {SitePermissions state} state
+   *        The state of the permission.
+   * @param {SitePermissions scope} scope (optional)
+   *        The scope of the permission. Defaults to SCOPE_PERSISTENT.
+   * @param {Browser} browser (optional)
+   *        The browser object to set temporary permissions on.
+   *        This needs to be provided if the scope is SCOPE_TEMPORARY!
+   */
+  setForPrincipal(principal, permissionID, state, scope = this.SCOPE_PERSISTENT, browser = null) {
     if (scope == this.SCOPE_GLOBAL && state == this.BLOCK) {
       GloballyBlockedPermissions.set(browser, permissionID);
       browser.dispatchEvent(new browser.ownerGlobal.CustomEvent("PermissionStateChange"));
       return;
     }
 
     if (state == this.UNKNOWN || state == this.getDefault(permissionID)) {
       // Because they are controlled by two prefs with many states that do not
       // correspond to the classical ALLOW/DENY/PROMPT model, we want to always
       // allow the user to add exceptions to their cookie rules without removing them.
       if (permissionID != "cookie") {
-        this.remove(uri, permissionID, browser);
+        this.removeFromPrincipal(principal, permissionID, browser);
         return;
       }
     }
 
     if (state == this.ALLOW_COOKIES_FOR_SESSION && permissionID != "cookie") {
       throw new Error("ALLOW_COOKIES_FOR_SESSION can only be set on the cookie permission");
     }
 
     // Save temporary permissions.
     if (scope == this.SCOPE_TEMPORARY) {
       // We do not support setting temp ALLOW for security reasons.
       // In its current state, this permission could be exploited by subframes
       // on the same page. This is because for BLOCK we ignore the request
-      // URI and only consider the current browser URI, to avoid notification spamming.
+      // principal and only consider the current browser principal, to avoid notification spamming.
       //
       // If you ever consider removing this line, you likely want to implement
       // a more fine-grained TemporaryPermissions that temporarily blocks for the
       // entire browser, but temporarily allows only for specific frames.
       if (state != this.BLOCK) {
         throw new Error("'Block' is the only permission we can save temporarily on a browser");
       }
 
       if (!browser) {
         throw new Error("TEMPORARY scoped permissions require a browser object");
       }
 
       TemporaryPermissions.set(browser, permissionID, state);
 
       browser.dispatchEvent(new browser.ownerGlobal
                                        .CustomEvent("PermissionStateChange"));
-    } else if (this.isSupportedURI(uri)) {
+    } else if (this.isSupportedPrincipal(principal)) {
       let perms_scope = Services.perms.EXPIRE_NEVER;
       if (scope == this.SCOPE_SESSION) {
         perms_scope = Services.perms.EXPIRE_SESSION;
       } else if (scope == this.SCOPE_POLICY) {
         perms_scope = Services.perms.EXPIRE_POLICY;
       }
 
-      Services.perms.add(uri, permissionID, state, perms_scope);
+      Services.perms.addFromPrincipal(principal, permissionID, state, perms_scope);
     }
   },
 
   /**
+   * Deprecated! Please use removeFromPrincipal(principal, permissionID, browser).
    * Removes the saved state of a particular permission for a given URI and/or browser.
    * This method will dispatch a "PermissionStateChange" event on the specified
    * browser if a temporary permission was removed.
    *
    * @param {nsIURI} uri
    *        The URI to remove the permission for.
    * @param {String} permissionID
    *        The id of the permission.
    * @param {Browser} browser (optional)
    *        The browser object to remove temporary permissions on.
    */
   remove(uri, permissionID, browser) {
     if ((!uri && !browser) || (uri && !(uri instanceof Ci.nsIURI)))
       throw new Error("uri parameter should be an nsIURI or a browser parameter is needed");
-    if (this.isSupportedURI(uri))
-      Services.perms.remove(uri, permissionID);
+
+    let principal = uri ? Services.scriptSecurityManager.createCodebasePrincipal(uri, {}) : null;
+    return this.removeFromPrincipal(principal, permissionID, browser);
+  },
+
+  /**
+   * Removes the saved state of a particular permission for a given principal and/or browser.
+   * This method will dispatch a "PermissionStateChange" event on the specified
+   * browser if a temporary permission was removed.
+   *
+   * @param {nsIPrincipal} principal
+   *        The principal to remove the permission for.
+   * @param {String} permissionID
+   *        The id of the permission.
+   * @param {Browser} browser (optional)
+   *        The browser object to remove temporary permissions on.
+   */
+  removeFromPrincipal(principal, permissionID, browser) {
+    if (this.isSupportedPrincipal(principal))
+      Services.perms.removeFromPrincipal(principal, permissionID);
 
     // TemporaryPermissions.get() deletes expired permissions automatically,
     if (TemporaryPermissions.get(browser, permissionID)) {
       // If it exists but has not expired, remove it explicitly.
       TemporaryPermissions.remove(browser, permissionID);
       // Send a PermissionStateChange event only if the permission hasn't expired.
       browser.dispatchEvent(new browser.ownerGlobal
                                        .CustomEvent("PermissionStateChange"));
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -403,30 +403,24 @@ function stopRecording(aBrowser, aReques
     set.delete(aRequest.windowID + aRequest.mediaSource + aRequest.rawID);
   }
 }
 
 function prompt(aBrowser, aRequest) {
   let { audioDevices, videoDevices, sharingScreen, sharingAudio,
         requestTypes } = aRequest;
 
-  let uri;
-  try {
-    // This fails for principals that serialize to "null", e.g. file URIs.
-    uri = Services.io.newURI(aRequest.origin);
-  } catch (e) {
-    uri = Services.io.newURI(aRequest.documentURI);
-  }
+  let principal = aBrowser.contentPrincipal;
 
   // If the user has already denied access once in this tab,
   // deny again without even showing the notification icon.
   if ((audioDevices.length && SitePermissions
-        .get(uri, "microphone", aBrowser).state == SitePermissions.BLOCK) ||
+        .getForPrincipal(principal, "microphone", aBrowser).state == SitePermissions.BLOCK) ||
       (videoDevices.length && SitePermissions
-        .get(uri, sharingScreen ? "screen" : "camera", aBrowser).state == SitePermissions.BLOCK)) {
+        .getForPrincipal(principal, sharingScreen ? "screen" : "camera", aBrowser).state == SitePermissions.BLOCK)) {
     denyRequest(aBrowser, aRequest);
     return;
   }
 
   // Tell the browser to refresh the identity block display in case there
   // are expired permission states.
   aBrowser.dispatchEvent(new aBrowser.ownerGlobal
                                      .CustomEvent("PermissionStateChange"));
@@ -469,29 +463,29 @@ function prompt(aBrowser, aRequest) {
       accessKey: stringBundle.getString("getUserMedia.dontAllow.accesskey"),
       callback(aState) {
         denyRequest(notification.browser, aRequest);
         let scope = SitePermissions.SCOPE_TEMPORARY;
         if (aState && aState.checkboxChecked) {
           scope = SitePermissions.SCOPE_PERSISTENT;
         }
         if (audioDevices.length)
-          SitePermissions.set(uri, "microphone",
-                              SitePermissions.BLOCK, scope, notification.browser);
+          SitePermissions.setForPrincipal(principal, "microphone",
+                                          SitePermissions.BLOCK, scope, notification.browser);
         if (videoDevices.length)
-          SitePermissions.set(uri, sharingScreen ? "screen" : "camera",
-                              SitePermissions.BLOCK, scope, notification.browser);
+          SitePermissions.setForPrincipal(principal, sharingScreen ? "screen" : "camera",
+                                          SitePermissions.BLOCK, scope, notification.browser);
       },
     },
   ];
 
   let productName = gBrandBundle.GetStringFromName("brandShortName");
 
   let options = {
-    name: getHostOrExtensionName(uri),
+    name: getHostOrExtensionName(principal.URI),
     persistent: true,
     hideClose: true,
     eventCallback(aTopic, aNewBrowser) {
       if (aTopic == "swapping")
         return true;
 
       let doc = this.browser.ownerDocument;
 
@@ -517,25 +511,25 @@ function prompt(aBrowser, aRequest) {
 
       // BLOCK is handled immediately by MediaManager if it has been set
       // persistently in the permission manager. If it has been set on the tab,
       // it is handled synchronously before we add the notification.
       // Handling of ALLOW is delayed until the popupshowing event,
       // to avoid granting permissions automatically to background tabs.
       if (aRequest.secure) {
         let micAllowed =
-          SitePermissions.get(uri, "microphone").state == SitePermissions.ALLOW;
+          SitePermissions.getForPrincipal(principal, "microphone").state == SitePermissions.ALLOW;
         let camAllowed =
-          SitePermissions.get(uri, "camera").state == SitePermissions.ALLOW;
+          SitePermissions.getForPrincipal(principal, "camera").state == SitePermissions.ALLOW;
 
         let perms = Services.perms;
         let mediaManagerPerm =
-          perms.testExactPermission(uri, "MediaManagerVideo");
+          perms.testExactPermissionFromPrincipal(principal, "MediaManagerVideo");
         if (mediaManagerPerm) {
-          perms.remove(uri, "MediaManagerVideo");
+          perms.removeFromPrincipal(principal, "MediaManagerVideo");
         }
 
         // Screen sharing shouldn't follow the camera permissions.
         if (videoDevices.length && sharingScreen)
           camAllowed = false;
 
         let activeCamera;
         let activeMic;
@@ -559,31 +553,31 @@ function prompt(aBrowser, aRequest) {
           }
         }
 
         if ((!audioDevices.length || micAllowed || activeMic) &&
             (!videoDevices.length || camAllowed || activeCamera)) {
           let allowedDevices = [];
           if (videoDevices.length) {
             allowedDevices.push((activeCamera || videoDevices[0]).deviceIndex);
-            Services.perms.add(uri, "MediaManagerVideo",
-                               Services.perms.ALLOW_ACTION,
-                               Services.perms.EXPIRE_SESSION);
+            Services.perms.addFromPrincipal(principal, "MediaManagerVideo",
+                                            Services.perms.ALLOW_ACTION,
+                                            Services.perms.EXPIRE_SESSION);
           }
           if (audioDevices.length) {
             allowedDevices.push((activeMic || audioDevices[0]).deviceIndex);
           }
 
           // Remember on which URIs we found persistent permissions so that we
           // can remove them if the user clicks 'Stop Sharing'. There's no
           // other way for the stop sharing code to know the hostnames of frames
           // using devices until bug 1066082 is fixed.
           let browser = this.browser;
-          browser._devicePermissionURIs = browser._devicePermissionURIs || [];
-          browser._devicePermissionURIs.push(uri);
+          browser._devicePermissionPrincipals = browser._devicePermissionPrincipals || [];
+          browser._devicePermissionPrincipals.push(principal);
 
           let camNeeded = videoDevices.length > 0;
           let micNeeded = audioDevices.length > 0;
           checkOSPermission(camNeeded, micNeeded).then((havePermission) => {
             if (havePermission) {
               let mm = browser.messageManager;
               mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
                                                    windowID: aRequest.windowID,
@@ -734,19 +728,19 @@ function prompt(aBrowser, aRequest) {
 
             let [pre, post] = string.split("<>");
             warning.textContent = pre;
             warning.appendChild(learnMore);
             warning.appendChild(chromeWin.document.createTextNode(post));
           }
 
           let perms = Services.perms;
-          let chromeUri = Services.io.newURI(doc.documentURI);
-          perms.add(chromeUri, "MediaManagerVideo", perms.ALLOW_ACTION,
-                    perms.EXPIRE_SESSION);
+          let chromePrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+          perms.addFromPrincipal(chromePrincipal, "MediaManagerVideo", perms.ALLOW_ACTION,
+                                 perms.EXPIRE_SESSION);
 
           video.deviceId = deviceId;
           let constraints = { video: { mediaSource: type, deviceId: {exact: deviceId } } };
           chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(stream => {
             if (video.deviceId != deviceId) {
               // The user has selected a different device or closed the panel
               // before getUserMedia finished.
               stream.getTracks().forEach(t => t.stop());
@@ -811,31 +805,31 @@ function prompt(aBrowser, aRequest) {
         if (videoDevices.length) {
           let listId = "webRTC-select" + (sharingScreen ? "Window" : "Camera") + "-menulist";
           let videoDeviceIndex = doc.getElementById(listId).value;
           let allowVideoDevice = videoDeviceIndex != "-1";
           if (allowVideoDevice) {
             allowedDevices.push(videoDeviceIndex);
             // Session permission will be removed after use
             // (it's really one-shot, not for the entire session)
-            perms.add(uri, "MediaManagerVideo", perms.ALLOW_ACTION,
-                      perms.EXPIRE_SESSION);
+            perms.addFromPrincipal(principal, "MediaManagerVideo", perms.ALLOW_ACTION,
+                                   perms.EXPIRE_SESSION);
             if (!webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
               webrtcUI.activePerms.set(aBrowser.outerWindowID, new Set());
             }
 
             for (let device of videoDevices) {
               if (device.deviceIndex == videoDeviceIndex) {
                 webrtcUI.activePerms.get(aBrowser.outerWindowID)
                         .add(aRequest.windowID + device.mediaSource + device.id);
                 break;
               }
             }
             if (remember)
-              SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
+              SitePermissions.setForPrincipal(principal, "camera", SitePermissions.ALLOW);
           }
         }
         if (audioDevices.length) {
           if (!sharingAudio) {
             let audioDeviceIndex = doc.getElementById("webRTC-selectMicrophone-menulist").value;
             let allowMic = audioDeviceIndex != "-1";
             if (allowMic) {
               allowedDevices.push(audioDeviceIndex);
@@ -846,34 +840,34 @@ function prompt(aBrowser, aRequest) {
               for (let device of audioDevices) {
                 if (device.deviceIndex == audioDeviceIndex) {
                   webrtcUI.activePerms.get(aBrowser.outerWindowID)
                           .add(aRequest.windowID + device.mediaSource + device.id);
                   break;
                 }
               }
               if (remember)
-                SitePermissions.set(uri, "microphone", SitePermissions.ALLOW);
+                SitePermissions.setForPrincipal(principal, "microphone", SitePermissions.ALLOW);
             }
           } else {
             // Only one device possible for audio capture.
             allowedDevices.push(0);
           }
         }
 
         if (!allowedDevices.length) {
           denyRequest(notification.browser, aRequest);
           return;
         }
 
         if (remember) {
           // Remember on which URIs we set persistent permissions so that we
           // can remove them if the user clicks 'Stop Sharing'.
-          aBrowser._devicePermissionURIs = aBrowser._devicePermissionURIs || [];
-          aBrowser._devicePermissionURIs.push(uri);
+          aBrowser._devicePermissionPrincipals = aBrowser._devicePermissionPrincipals || [];
+          aBrowser._devicePermissionPrincipals.push(principal);
         }
 
         let camNeeded = videoDevices.length > 0;
         let micNeeded = audioDevices.length > 0;
         let havePermission = await checkOSPermission(camNeeded, micNeeded);
         if (!havePermission) {
           denyRequestNoPermission(notification.browser, aRequest);
           return;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -81,17 +81,17 @@
 
 /** Begin titlebar **/
 
 #titlebar {
   /* Centrally align content items vertically */
   -moz-box-pack: center;
 }
 
-#titlebar-buttonbox > .titlebar-button {
+.titlebar-button {
   display: none;
 }
 
 /* Making the toolbox position:relative (browser.inc.css) occludes titlebar indicators
  * if the toolbox has a background. Fix this by positioning the relevant elements, too: */
 #titlebar-secondary-buttonbox {
   position: relative;
   z-index: 1;
--- a/browser/themes/shared/autocomplete.inc.css
+++ b/browser/themes/shared/autocomplete.inc.css
@@ -34,28 +34,66 @@
   background-color: var(--arrowpanel-dimmed);
 }
 
 .autocomplete-richlistitem[selected] {
   background-color: Highlight;
   color: HighlightText;
 }
 
-/* Login form autocompletion */
+/* Login form autocompletion with and without origin showing */
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .ac-site-icon,
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="login"] > .ac-site-icon {
   display: initial;
   list-style-image: url(chrome://browser/skin/login.svg);
   -moz-context-properties: fill;
   fill: GrayText;
 }
 
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"][selected] > .login-wrapper > .ac-site-icon,
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="login"] > .ac-site-icon[selected] {
   fill: HighlightText;
 }
 
+/* Login form autocompletion with origin showing */
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] {
+  height: auto;
+  padding: 4px;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper {
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: row;
+  margin: 0;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .ac-site-icon {
+  margin-inline-start: auto;
+  margin-inline-end: 4px;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .login-text {
+  /* The text should flex while the icon should not */
+  flex: 1;
+  /* width/min-width are needed to get the text-overflow: ellipsis to work for the children */
+  min-width: 0;
+  width: 0;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .login-text > .login-row {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .login-text > .login-origin {
+  padding-top: 2px !important;
+  opacity: .6;
+}
 
 /* Insecure field warning */
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] {
   background-color: var(--arrowpanel-dimmed);
   border-bottom: 1px solid var(--panel-separator-color);
   padding-bottom: 4px;
   padding-top: 4px;
 }
--- a/browser/themes/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -78,16 +78,20 @@
 #downloadsSummary > .downloadTypeIcon {
   list-style-image: url("chrome://browser/skin/downloads/download-summary.svg");
 }
 
 #downloadsSummaryDescription {
   color: -moz-nativehyperlinktext;
 }
 
+:root[lwt-popup-brighttext] #downloadsSummaryDescription {
+  color: #75baff; /* --blue-30 */
+}
+
 /*** List items and similar elements in the summary ***/
 
 #downloadsSummary,
 @item@ {
   height: var(--downloads-item-height);
 }
 
 @item@ {
--- a/devtools/client/aboutdebugging-new/test/browser/browser.ini
+++ b/devtools/client/aboutdebugging-new/test/browser/browser.ini
@@ -60,17 +60,17 @@ skip-if = (os == 'linux' && bits == 32) 
 [browser_aboutdebugging_devtoolstoolbox_contextmenu_markupview.js]
 [browser_aboutdebugging_devtoolstoolbox_focus.js]
 [browser_aboutdebugging_devtoolstoolbox_menubar.js]
 [browser_aboutdebugging_devtoolstoolbox_performance.js]
 skip-if = os == 'linux' && e10s && (asan || debug) # Same skip-if as old perf panel test suite. Bug 1254821
 [browser_aboutdebugging_devtoolstoolbox_reload.js]
 skip-if = verify || ccov # test loads the toolbox 2 times for each panel, might timeout or OOM
 [browser_aboutdebugging_devtoolstoolbox_shortcuts.js]
-skip-if = (os == "win" && ccov) # Bug 1521349
+skip-if = ccov # Bug 1521349, Bug 1548015
 [browser_aboutdebugging_devtoolstoolbox_splitconsole_key.js]
 [browser_aboutdebugging_devtoolstoolbox_target_destroyed.js]
 skip-if = debug || asan # This test leaks. See bug 1529005
 [browser_aboutdebugging_devtoolstoolbox_tooltip_markupview.js]
 [browser_aboutdebugging_fenix_runtime_display.js]
 [browser_aboutdebugging_message_close.js]
 [browser_aboutdebugging_navigate.js]
 [browser_aboutdebugging_persist_connection.js]
--- a/devtools/client/debugger/src/components/Editor/DebugLine.js
+++ b/devtools/client/debugger/src/components/Editor/DebugLine.js
@@ -4,17 +4,18 @@
 
 // @flow
 import { PureComponent } from "react";
 import {
   toEditorPosition,
   getDocument,
   hasDocument,
   startOperation,
-  endOperation
+  endOperation,
+  getTokenEnd
 } from "../../utils/editor";
 import { isException } from "../../utils/pause";
 import { getIndentation } from "../../utils/indentation";
 import { connect } from "../../utils/connect";
 import {
   getVisibleSelectedFrame,
   getPauseReason,
   getSourceWithContent,
@@ -71,19 +72,23 @@ export class DebugLine extends PureCompo
 
     let { line, column } = toEditorPosition(frame.location);
     const { markTextClass, lineClass } = this.getTextClasses(why);
     doc.addLineClass(line, "line", lineClass);
 
     const lineText = doc.getLine(line);
     column = Math.max(column, getIndentation(lineText));
 
+    // If component updates because user clicks on
+    // another source tab, codeMirror will be null.
+    const columnEnd = doc.cm ? getTokenEnd(doc.cm, line, column) : null;
+
     this.debugExpression = doc.markText(
       { ch: column, line },
-      { ch: null, line },
+      { ch: columnEnd, line },
       { className: markTextClass }
     );
   }
 
   clearDebugLine(why: Why, frame: Frame, source: ?SourceWithContent) {
     if (!isDocumentReady(source, frame)) {
       return;
     }
--- a/devtools/client/debugger/src/components/Editor/Editor.css
+++ b/devtools/client/debugger/src/components/Editor/Editor.css
@@ -100,18 +100,17 @@ html[dir="rtl"] .editor-mount {
 .new-breakpoint .CodeMirror-gutter-elt:nth-child(2) {
   z-index: 0;
 }
 
 .theme-dark .editor-wrapper .CodeMirror-line .cm-comment {
   color: var(--theme-comment);
 }
 
-.debug-expression,
-.debug-expression ~ .CodeMirror-widget {
+.debug-expression {
   background-color: var(--debug-expression-background);
 }
 
 .debug-expression-error {
   background-color: var(--debug-expression-error-background);
 }
 
 :not(.conditional-breakpoint-panel) .new-debug-line .CodeMirror-line {
--- a/devtools/client/debugger/src/components/QuickOpenModal.js
+++ b/devtools/client/debugger/src/components/QuickOpenModal.js
@@ -110,21 +110,17 @@ export class QuickOpenModal extends Comp
     }
   }
 
   componentDidUpdate(prevProps: Props) {
     const nowEnabled = !prevProps.enabled && this.props.enabled;
     const queryChanged = prevProps.query !== this.props.query;
 
     if (this.refs.resultList && this.refs.resultList.refs) {
-      scrollList(
-        this.refs.resultList.refs,
-        this.state.selectedIndex,
-        nowEnabled || !queryChanged
-      );
+      scrollList(this.refs.resultList.refs, this.state.selectedIndex);
     }
 
     if (nowEnabled || queryChanged) {
       this.updateResults(this.props.query);
     }
   }
 
   closeModal = () => {
--- a/devtools/client/debugger/src/utils/editor/index.js
+++ b/devtools/client/debugger/src/utils/editor/index.js
@@ -254,8 +254,17 @@ export function clearLineClass(codeMirro
 
 export function getTextForLine(codeMirror: Object, line: number): string {
   return codeMirror.getLine(line - 1).trim();
 }
 
 export function getCursorLine(codeMirror: Object): number {
   return codeMirror.getCursor().line;
 }
+
+export function getTokenEnd(codeMirror: Object, line: number, column: number) {
+  const token = codeMirror.getTokenAt({
+    line: line,
+    ch: column + 1
+  });
+
+  return token.end;
+}
--- a/devtools/client/debugger/src/utils/result-list.js
+++ b/devtools/client/debugger/src/utils/result-list.js
@@ -1,42 +1,31 @@
 /* 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/>. */
 
 // @flow
 
 import { isFirefox } from "devtools-environment";
-import { transitionTimeout } from "../components/shared/Modal";
 
-export function scrollList(
-  resultList: Element[],
-  index: number,
-  delayed: boolean = false
-): void {
+export function scrollList(resultList: Element[], index: number): void {
   if (!resultList.hasOwnProperty(index)) {
     return;
   }
 
   const resultEl = resultList[index];
 
   const scroll = () => {
     if (isFirefox()) {
-      resultEl.scrollIntoView({ block: "center", behavior: "smooth" });
+      resultEl.scrollIntoView({ block: "nearest", behavior: "auto" });
     } else {
       chromeScrollList(resultEl, index);
     }
   };
 
-  if (delayed) {
-    // Wait for Modal Transition timeout before scrolling to resultEl.
-    setTimeout(scroll, transitionTimeout + 10);
-    return;
-  }
-
   scroll();
 }
 
 function chromeScrollList(elem: Element, index: number): void {
   const resultsEl: any = elem.parentNode;
 
   if (!resultsEl || resultsEl.children.length === 0) {
     return;
--- a/devtools/client/framework/test/browser_menu_api.js
+++ b/devtools/client/framework/test/browser_menu_api.js
@@ -16,16 +16,18 @@ add_task(async function() {
   const tab = await addTab(URL);
   const target = await TargetFactory.forTab(tab);
   const toolbox = await gDevTools.showToolbox(target, "webconsole");
 
   // This test will involve localized strings, make sure the necessary FTL file is
   // available in the toolbox top window.
   toolbox.topWindow.MozXULElement.insertFTLIfNeeded("toolkit/main-window/editmenu.ftl");
 
+  loadFTL(toolbox, "toolkit/main-window/editmenu.ftl");
+
   await testMenuItems();
   await testMenuPopup(toolbox);
   await testSubmenu(toolbox);
 });
 
 function testMenuItems() {
   const menu = new Menu();
   const menuItem1 = new MenuItem();
--- a/devtools/client/framework/test/head.js
+++ b/devtools/client/framework/test/head.js
@@ -422,8 +422,24 @@ function setupPreferencesForBrowserToolb
     ["devtools.browser-toolbox.allow-unsafe-script", true],
     // On debug test runner, it takes more than the default time (20s)
     // to get a initialized console
     ["devtools.debugger.remote-timeout", 120000],
   ]};
 
   return SpecialPowers.pushPrefEnv(options);
 }
+
+/**
+ * Load FTL.
+ *
+ * @param {Toolbox} toolbox
+ *        Toolbox instance.
+ * @param {String} path
+ *        Path to the FTL file.
+ */
+function loadFTL(toolbox, path) {
+  const win = toolbox.doc.ownerGlobal;
+
+  if (win.MozXULElement) {
+    win.MozXULElement.insertFTLIfNeeded(path);
+  }
+}
--- a/devtools/client/framework/toolbox.xul
+++ b/devtools/client/framework/toolbox.xul
@@ -11,16 +11,19 @@
 <!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
 %toolboxDTD;
 <!ENTITY % globalKeysDTD SYSTEM "chrome://global/locale/globalKeys.dtd">
 %globalKeysDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">
+  <linkset>
+    <html:link rel="localization" href="devtools/tooltips.ftl"/>
+  </linkset>
 
   <html:link href="chrome://browser/skin/window.svg" rel="shortcut icon"/>
   <script src="chrome://devtools/content/shared/theme-switching.js"/>
   <script src="chrome://global/content/viewSourceUtils.js"/>
 
   <script src="chrome://global/content/globalOverlay.js"/>
   <script src="chrome://devtools/content/framework/toolbox-init.js"/>
 
--- a/devtools/client/inspector/rules/models/element-style.js
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -1,25 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const Services = require("Services");
 const promise = require("promise");
 const Rule = require("devtools/client/inspector/rules/models/rule");
 const UserProperties = require("devtools/client/inspector/rules/models/user-properties");
 const { ELEMENT_STYLE } = require("devtools/shared/specs/styles");
 
 loader.lazyRequireGetter(this, "promiseWarn", "devtools/client/inspector/shared/utils", true);
 loader.lazyRequireGetter(this, "parseDeclarations", "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "parseNamedDeclarations", "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "parseSingleValue", "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "isCssVariable", "devtools/shared/fronts/css-properties", true);
 
+const PREF_INACTIVE_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";
+
 /**
  * ElementStyle is responsible for the following:
  *   Keeps track of which properties are overridden.
  *   Maintains a list of Rule objects for a given element.
  */
 class ElementStyle {
   /**
    * @param  {Element} element
@@ -60,16 +63,23 @@ class ElementStyle {
 
     if (this.ruleView.isNewRulesView) {
       this.pageStyle.on("stylesheet-updated", this.onRefresh);
       this.ruleView.inspector.styleChangeTracker.on("style-changed", this.onRefresh);
       this.ruleView.selection.on("pseudoclass", this.onRefresh);
     }
   }
 
+  get unusedCssEnabled() {
+    if (!this._unusedCssEnabled) {
+      this._unusedCssEnabled = Services.prefs.getBoolPref(PREF_INACTIVE_CSS_ENABLED);
+    }
+    return this._unusedCssEnabled;
+  }
+
   destroy() {
     if (this.destroyed) {
       return;
     }
 
     this.destroyed = true;
 
     for (const rule of this.rules) {
@@ -120,17 +130,17 @@ class ElementStyle {
 
       this.rules = [];
 
       for (const entry of entries) {
         this._maybeAddRule(entry, existingRules);
       }
 
       // Mark overridden computed styles.
-      this.markOverriddenAll();
+      this.onRuleUpdated();
 
       this._sortRulesForPseudoElement();
 
       if (this.ruleView.isNewRulesView) {
         this.subscribeRulesToLocationChange();
       }
 
       // We're done with the previous list of rules.
@@ -237,36 +247,38 @@ class ElementStyle {
       return false;
     }
 
     this.rules.push(rule);
     return true;
   }
 
   /**
-   * Calls markOverridden with all supported pseudo elements
+   * Calls updateDeclarations with all supported pseudo elements
    */
-  markOverriddenAll() {
+  onRuleUpdated() {
     this.variables.clear();
-    this.markOverridden();
+    this.updateDeclarations();
 
     for (const pseudo of this.cssProperties.pseudoElements) {
-      this.markOverridden(pseudo);
+      this.updateDeclarations(pseudo);
     }
   }
 
   /**
-   * Mark the properties listed in this.rules for a given pseudo element
-   * with an overridden flag if an earlier property overrides it.
+   * Mark the declarations for a given pseudo element with an overridden flag if
+   * an earlier property overrides it and update the editor to show it in the
+   * UI. If there is any inactive CSS we also update the editors state to show
+   * the inactive CSS icon.
    *
    * @param  {String} pseudo
    *         Which pseudo element to flag as overridden.
    *         Empty string or undefined will default to no pseudo element.
    */
-  markOverridden(pseudo = "") {
+  updateDeclarations(pseudo = "") {
     // Gather all the text properties applied by these rules, ordered
     // from more- to less-specific. Text properties from keyframes rule are
     // excluded from being marked as overridden since a number of criteria such
     // as time, and animation overlay are required to be check in order to
     // determine if the property is overridden.
     const textProps = [];
     for (const rule of this.rules) {
       if ((rule.matchedSelectors.length > 0 ||
@@ -340,26 +352,32 @@ class ElementStyle {
         taken[computedProp.name] = computedProp;
 
         if (isCssVariable(computedProp.name)) {
           this.variables.set(computedProp.name, computedProp.value);
         }
       }
     }
 
-    // For each TextProperty, mark it overridden if all of its
-    // computed properties are marked overridden. Update the text
-    // property's associated editor, if any. This will clear the
-    // _overriddenDirty state on all computed properties.
+    // For each TextProperty, mark it overridden if all of its computed
+    // properties are marked overridden. Update the text property's associated
+    // editor, if any. This will clear the _overriddenDirty state on all
+    // computed properties. For each editor we also show or hide the inactive
+    // CSS icon as needed.
     for (const textProp of textProps) {
       // _updatePropertyOverridden will return true if the
       // overridden state has changed for the text property.
       if (this._updatePropertyOverridden(textProp)) {
         textProp.updateEditor();
       }
+
+      // For each editor show or hide the inactive CSS icon as needed.
+      if (textProp.editor && this.unusedCssEnabled) {
+        textProp.editor.updatePropertyState();
+      }
     }
   }
 
   /**
    * Adds a new declaration to the rule.
    *
    * @param  {String} ruleId
    *         The id of the Rule to be modified.
@@ -576,30 +594,30 @@ class ElementStyle {
 
       // Remove the old rule and insert the new rule according to where it appears
       // in the list of applied styles.
       this.rules.splice(oldIndex, 1);
       // If the selector no longer matches, then we leave the rule in
       // the same relative position.
       this.rules.splice(newIndex === -1 ? oldIndex : newIndex, 0, newRule);
 
-      // Mark any properties that are overridden according to the new list of rules.
-      this.markOverriddenAll();
+      // Recompute, mark and update the UI for any properties that are
+      // overridden or contain inactive CSS according to the new list of rules.
+      this.onRuleUpdated();
 
       // In order to keep the new rule in place of the old in the rules view, we need
       // to remove the rule again if the rule was inserted to its new index according
       // to the list of applied styles.
       // Note: you might think we would replicate the list-modification logic above,
       // but that is complicated due to the way the UI installs pseudo-element rules
       // and the like.
       if (newIndex !== -1) {
         this.rules.splice(newIndex, 1);
         this.rules.splice(oldIndex, 0, newRule);
       }
-
       this._changed();
     } catch (e) {
       console.error(e);
     }
   }
 
   /**
    * Subscribes all the rules to location changes.
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -303,17 +303,17 @@ class Rule {
   }
 
   /**
    * Helper function for applyProperties that is called when the actor
    * does not support as-authored styles.  Store disabled properties
    * in the element style's store.
    */
   _applyPropertiesNoAuthored(modifications) {
-    this.elementStyle.markOverriddenAll();
+    this.elementStyle.onRuleUpdated();
 
     const disabledProps = [];
 
     for (const prop of this.textProps) {
       if (prop.invisible) {
         continue;
       }
       if (!prop.enabled) {
@@ -414,17 +414,17 @@ class Rule {
           const modifications = this.domRule.startModifyingProperties(
             this.cssProperties);
           modifier(modifications);
           if (this.domRule.canSetRuleText) {
             return this._applyPropertiesAuthored(modifications);
           }
           return this._applyPropertiesNoAuthored(modifications);
         }).then(() => {
-          this.elementStyle.markOverriddenAll();
+          this.elementStyle.onRuleUpdated();
 
           if (resultPromise === this._applyingModifications) {
             this._applyingModifications = null;
             this.elementStyle._changed();
           }
         }).catch(promiseWarn);
 
     this._applyingModifications = resultPromise;
--- a/devtools/client/inspector/rules/models/text-property.js
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -227,16 +227,29 @@ class TextProperty {
     // true.
     if (!this.rule.domRule.declarations[selfIndex]) {
       return true;
     }
 
     return this.rule.domRule.declarations[selfIndex].isValid;
   }
 
+  isUsed() {
+    const selfIndex = this.rule.textProps.indexOf(this);
+    const declarations = this.rule.domRule.declarations;
+
+    // StyleRuleActor's declarations may have a isUsed flag (if the server is the right
+    // version). Just return true if the information is missing.
+    if (!declarations || !declarations[selfIndex] || !declarations[selfIndex].isUsed) {
+      return { used: true };
+    }
+
+    return declarations[selfIndex].isUsed;
+  }
+
   /**
    * Validate the name of this property.
    *
    * @return {Boolean} true if the property name is valid, false otherwise.
    */
   isNameValid() {
     const selfIndex = this.rule.textProps.indexOf(this);
 
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -11,25 +11,26 @@ const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const {l10n} = require("devtools/shared/inspector/css-logic");
 const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
 const OutputParser = require("devtools/client/shared/output-parser");
 const {PrefObserver} = require("devtools/client/shared/prefs");
 const ElementStyle = require("devtools/client/inspector/rules/models/element-style");
 const RuleEditor = require("devtools/client/inspector/rules/views/rule-editor");
 const {
-  VIEW_NODE_SELECTOR_TYPE,
+  VIEW_NODE_FONT_TYPE,
+  VIEW_NODE_IMAGE_URL_TYPE,
+  VIEW_NODE_INACTIVE_CSS,
+  VIEW_NODE_LOCATION_TYPE,
   VIEW_NODE_PROPERTY_TYPE,
-  VIEW_NODE_VALUE_TYPE,
-  VIEW_NODE_IMAGE_URL_TYPE,
-  VIEW_NODE_LOCATION_TYPE,
+  VIEW_NODE_SELECTOR_TYPE,
   VIEW_NODE_SHAPE_POINT_TYPE,
   VIEW_NODE_SHAPE_SWATCH,
+  VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_VARIABLE_TYPE,
-  VIEW_NODE_FONT_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
 const {createChild, promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {debounce} = require("devtools/shared/debounce");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 loader.lazyRequireGetter(this, "flashElementOn", "devtools/client/inspector/markup/utils", true);
 loader.lazyRequireGetter(this, "flashElementOff", "devtools/client/inspector/markup/utils", true);
@@ -420,16 +421,19 @@ CssRuleView.prototype = {
         enabled: prop.enabled,
         overridden: prop.overridden,
         pseudoElement: prop.rule.pseudoElement,
         sheetHref: prop.rule.domRule.href,
         textProperty: prop,
         toggleActive: getShapeToggleActive(node),
         point: getShapePoint(node),
       };
+    } else if (classes.contains("ruleview-unused-warning") && prop) {
+      type = VIEW_NODE_INACTIVE_CSS;
+      value = prop.isUsed();
     } else if (classes.contains("ruleview-shapeswatch") && prop) {
       type = VIEW_NODE_SHAPE_SWATCH;
       value = {
         enabled: prop.enabled,
         overridden: prop.overridden,
         textProperty: prop,
       };
     } else if ((classes.contains("ruleview-variable") ||
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -190,16 +190,18 @@ skip-if = (os == "win" && debug) # bug 9
 [browser_rules_grid-toggle_03.js]
 [browser_rules_grid-toggle_04.js]
 [browser_rules_grid-toggle_05.js]
 [browser_rules_gridline-names-autocomplete.js]
 [browser_rules_guessIndentation.js]
 [browser_rules_highlight-element-rule.js]
 [browser_rules_highlight-property.js]
 [browser_rules_highlight-used-fonts.js]
+[browser_rules_inactive_css_flexbox.js]
+[browser_rules_inactive_css_grid.js]
 [browser_rules_inherited-properties_01.js]
 [browser_rules_inherited-properties_02.js]
 [browser_rules_inherited-properties_03.js]
 [browser_rules_inherited-properties_04.js]
 [browser_rules_inline-source-map.js]
 [browser_rules_inline-style-order.js]
 [browser_rules_invalid.js]
 [browser_rules_invalid-source-map.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js
@@ -0,0 +1,160 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test inactive flex properties.
+
+const TEST_URI = `
+<head>
+  <style>
+    #container {
+      width: 200px;
+      height: 100px;
+      border: 1px solid #000;
+      align-content: space-between;
+      order: 1;
+    }
+
+    .flex-item {
+      flex-basis: auto;
+      flex-grow: 1;
+      flex-shrink: 1;
+      flex-direction: row;
+    }
+
+    #self-aligned {
+      align-self: stretch;
+    }
+  </style>
+</head>
+<body>
+    <h1>browser_rules_inactive_css_flexbox.js</h1>
+    <div id="container" style="display:flex">
+      <div class="flex-item item-1" style="order:1">1</div>
+      <div class="flex-item item-2" style="order:2">2</div>
+      <div class="flex-item item-3" style="order:3">3</div>
+    </div>
+    <div id="self-aligned"></div>
+</body>`;
+
+const BEFORE = [
+  {
+    selector: "#self-aligned",
+    inactiveDeclarations: [
+      {
+        declaration: {
+          "align-self": "stretch",
+        },
+        ruleIndex: 1,
+      },
+    ],
+  },
+  {
+    selector: ".item-2",
+    activeDeclarations: [
+      {
+        declarations: {
+          "order": "2",
+        },
+        ruleIndex: 0,
+      },
+      {
+        declarations: {
+          "flex-basis": "auto",
+          "flex-grow": "1",
+          "flex-shrink": "1",
+        },
+        ruleIndex: 1,
+      },
+    ],
+    inactiveDeclarations: [
+      {
+        declaration: {
+          "flex-direction": "row",
+        },
+        ruleIndex: 1,
+      },
+    ],
+  },
+  {
+    selector: "#container",
+    activeDeclarations: [
+      {
+        declarations: {
+          "display": "flex",
+        },
+        ruleIndex: 0,
+      },
+      {
+        declarations: {
+          width: "200px",
+          height: "100px",
+          border: "1px solid #000",
+          "align-content": "space-between",
+        },
+        ruleIndex: 1,
+      },
+    ],
+    inactiveDeclarations: [
+      {
+        declaration: {
+          "order": "1",
+        },
+        ruleIndex: 1,
+      },
+    ],
+  },
+];
+
+const AFTER = [
+  {
+    selector: ".item-2",
+    inactiveDeclarations: [
+      {
+        declaration: {
+          "order": "2",
+        },
+        ruleIndex: 0,
+      },
+      {
+        declaration: {
+          "flex-basis": "auto",
+        },
+        ruleIndex: 1,
+      },
+      {
+        declaration: {
+          "flex-grow": "1",
+        },
+        ruleIndex: 1,
+      },
+      {
+        declaration: {
+          "flex-shrink": "1",
+        },
+        ruleIndex: 1,
+      },
+      {
+        declaration: {
+          "flex-direction": "row",
+        },
+        ruleIndex: 1,
+      },
+    ],
+  },
+];
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  const {inspector, view} = await openRuleView();
+
+  await runInactiveCSSTests(view, inspector, BEFORE);
+
+  // Toggle `display:flex` to disabled.
+  await toggleDeclaration(inspector, view, 0, {
+    display: "flex",
+  });
+  await runInactiveCSSTests(view, inspector, AFTER);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js
@@ -0,0 +1,163 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test inactive grid properties.
+
+const TEST_URI = `
+<head>
+  <style>
+    #container {
+      width: 200px;
+      height: 100px;
+      border: 1px solid #000;
+      column-gap: 10px;
+      row-gap: 10px;
+      align-self: start;
+    }
+
+    .item-1 {
+      grid-column-start: 1;
+      grid-column-end: 3;
+      grid-row-start: 1;
+      grid-row-end: auto;
+      flex-direction: row
+    }
+
+    #self-aligned {
+      align-self: stretch;
+    }
+  </style>
+</head>
+<body>
+    <h1>browser_rules_inactive_css_grid.js</h1>
+    <div id="container" style="display:grid">
+      <div class="grid-item item-1">1</div>
+      <div class="grid-item item-2">2</div>
+      <div class="grid-item item-3">3</div>
+      <div class="grid-item item-4">4</div>
+    </div>
+    <div id="self-aligned"></div>
+</body>`;
+
+const BEFORE = [
+  {
+    selector: "#self-aligned",
+    inactiveDeclarations: [
+      {
+        declaration: {
+          "align-self": "stretch",
+        },
+        ruleIndex: 1,
+      },
+    ],
+  },
+  {
+    selector: ".item-1",
+    activeDeclarations: [
+      {
+        declarations: {
+          "grid-column-start": "1",
+          "grid-column-end": "3",
+          "grid-row-start": "1",
+          "grid-row-end": "auto",
+        },
+        ruleIndex: 1,
+      },
+    ],
+    inactiveDeclarations: [
+      {
+        declaration: {
+          "flex-direction": "row",
+        },
+        ruleIndex: 1,
+      },
+    ],
+  },
+  {
+    selector: "#container",
+    activeDeclarations: [
+      {
+        declarations: {
+          display: "grid",
+        },
+        ruleIndex: 0,
+      },
+      {
+        declarations: {
+          width: "200px",
+          height: "100px",
+          border: "1px solid #000",
+          "column-gap": "10px",
+          "row-gap": "10px",
+        },
+        ruleIndex: 1,
+      },
+    ],
+    inactiveDeclarations: [
+      {
+        declaration: {
+          "align-self": "start",
+        },
+        ruleIndex: 1,
+      },
+    ],
+  },
+];
+
+const AFTER = [
+  {
+    activeDeclarations: [
+      {
+        declarations: {
+          display: "grid",
+        },
+        ruleIndex: 0,
+      },
+      {
+        declarations: {
+          width: "200px",
+          height: "100px",
+          border: "1px solid #000",
+        },
+        ruleIndex: 1,
+      },
+    ],
+    inactiveDeclarations: [
+      {
+        declaration: {
+          "column-gap": "10px",
+        },
+        ruleIndex: 1,
+      },
+      {
+        declaration: {
+          "row-gap": "10px",
+        },
+        ruleIndex: 1,
+      },
+      {
+        declaration: {
+          "align-self": "start",
+        },
+        ruleIndex: 1,
+      },
+    ],
+  },
+];
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  const {inspector, view} = await openRuleView();
+
+  await runInactiveCSSTests(view, inspector, BEFORE);
+
+  // Toggle `display:grid` to disabled.
+  await toggleDeclaration(inspector, view, 0, {
+    display: "grid",
+  });
+  await view.once("ruleview-refreshed");
+  await runInactiveCSSTests(view, inspector, AFTER);
+});
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -563,17 +563,17 @@ async function toggleClassPanelCheckBox(
   const onMutation = view.inspector.once("markupmutation");
   checkBox.click();
   info("Waiting for a markupmutation as a result of toggling this class");
   await onMutation;
 }
 
 /**
  * Verify the content of the class-panel.
- * @param {CssRuleView} view The rule-view isntance
+ * @param {CssRuleView} view The rule-view instance
  * @param {Array} classes The list of expected classes. Each item in this array is an
  * object with the following properties: {name: {String}, state: {Boolean}}
  */
 function checkClassPanelContent(view, classes) {
   const checkBoxNodeList = view.classPanel.querySelectorAll("[type=checkbox]");
   is(checkBoxNodeList.length, classes.length,
      "The panel contains the expected number of checkboxes");
 
@@ -601,8 +601,248 @@ async function openEyedropper(view, swat
 
   const dropperButton = tooltip.container.querySelector("#eyedropper-button");
 
   info("Click on the eyedropper icon");
   const onOpened = tooltip.once("eyedropper-opened");
   dropperButton.click();
   await onOpened;
 }
+
+/**
+ * Gets a set of declarations for a rule index.
+ *
+ * @param {ruleView} view
+ *        The rule-view instance.
+ * @param {Number} ruleIndex
+ *        The index we expect the rule to have in the rule-view.
+ *
+ * @returns A map containing stringified property declarations e.g.
+ *          [
+ *            {
+ *              "color:red":
+ *                {
+ *                  propertyName: "color",
+ *                  propertyValue: "red",
+ *                  warning: "This won't work",
+ *                  used: true,
+ *                }
+ *            },
+ *            ...
+ *          ]
+ */
+function getPropertiesForRuleIndex(view, ruleIndex) {
+  const declaration = new Map();
+  const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+
+  for (const currProp of ruleEditor.rule.textProps) {
+    const icon = currProp.editor.unusedState;
+    const unused = currProp.editor.element.classList.contains("unused");
+
+    declaration.set(`${currProp.name}:${currProp.value}`, {
+      propertyName: currProp.name,
+      propertyValue: currProp.value,
+      icon: icon,
+      data: currProp.isUsed(),
+      warning: unused,
+      used: !unused,
+    });
+  }
+
+  return declaration;
+}
+
+/**
+ * Toggle a declaration disabled or enabled.
+ *
+ * @param {InspectorPanel} inspector
+ *        The instance of InspectorPanel currently loaded in the toolbox.
+ * @param {ruleView} view
+ *        The rule-view instance
+ * @param {Number} ruleIndex
+ *        The index of the CSS rule where we can find the declaration to be
+ *        toggled.
+ * @param {Object} declaration
+ *        An object representing the declaration e.g. { color: "red" }.
+ */
+async function toggleDeclaration(inspector, view, ruleIndex, declaration) {
+  const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
+  const [[ name, value ]] = Object.entries(declaration);
+
+  let textProp = null;
+  for (const currProp of ruleEditor.rule.textProps) {
+    if (currProp.name === name && currProp.value === value) {
+      textProp = currProp;
+      break;
+    }
+  }
+
+  const dec = `${name}:${value}`;
+  ok(textProp, `Declaration "${dec}" found`);
+
+  const newStatus = textProp.enabled ? "disabled" : "enabled";
+  info(`Toggling declaration "${dec}" of rule ${ruleIndex} to ${newStatus}`);
+
+  await togglePropStatus(view, textProp);
+  info("Toggled successfully.");
+}
+
+/**
+ * Check that a declaration is marked inactive and that it has the expected
+ * warning.
+ *
+ * @param {ruleView} view
+ *        The rule-view instance.
+ * @param {Number} ruleIndex
+ *        The index we expect the rule to have in the rule-view.
+ * @param {Object} declaration
+ *        An object representing the declaration e.g. { color: "red" }.
+ */
+async function checkDeclarationIsInactive(view, ruleIndex, declaration) {
+  const declarations = getPropertiesForRuleIndex(view, ruleIndex);
+  const [[ name, value ]] = Object.entries(declaration);
+  const dec = `${name}:${value}`;
+  const { used, warning } = declarations.get(dec);
+
+  ok(!used, `"${dec}" is inactive`);
+  ok(warning, `"${dec}" has a warning`);
+
+  await checkInteractiveTooltip(view, ruleIndex, declaration);
+}
+
+/**
+ * Check that a declaration is marked active.
+ *
+ * @param {ruleView} view
+ *        The rule-view instance.
+ * @param {Number} ruleIndex
+ *        The index we expect the rule to have in the rule-view.
+ * @param {Object} declaration
+ *        An object representing the declaration e.g. { color: "red" }.
+ */
+function checkDeclarationIsActive(view, ruleIndex, declaration) {
+  const declarations = getPropertiesForRuleIndex(view, ruleIndex);
+  const [[ name, value ]] = Object.entries(declaration);
+  const dec = `${name}:${value}`;
+  const { used, warning } = declarations.get(dec);
+
+  ok(used, `${dec} is active`);
+  ok(!warning, `${dec} has no warning`);
+}
+
+/**
+ * Check that a tooltip contains the correct value.
+ *
+ * @param {ruleView} view
+ *        The rule-view instance.
+ * @param {Number} ruleIndex
+ *        The index we expect the rule to have in the rule-view.
+ * @param {Object} declaration
+ *        An object representing the declaration e.g. { color: "red" }.
+ */
+async function checkInteractiveTooltip(view, ruleIndex, declaration) {
+  // Get the declaration
+  const declarations = getPropertiesForRuleIndex(view, ruleIndex);
+  const [[ name, value ]] = Object.entries(declaration);
+  const dec = `${name}:${value}`;
+  const { icon, data } = declarations.get(dec);
+
+  // Get the tooltip.
+  const tooltip = view.tooltips.getTooltip("interactiveTooltip");
+
+  // Get the HTML template.
+  const inactiveCssTooltipHelper = view.tooltips.inactiveCssTooltipHelper;
+  const template = inactiveCssTooltipHelper.getTemplate(data, tooltip);
+
+  // Translate the template using Fluent.
+  const { doc } = tooltip;
+  await doc.l10n.translateFragment(template);
+
+  // Get the expected HTML content of the now translated template.
+  const expected = template.firstElementChild.outerHTML;
+
+  // Show the tooltip for the correct icon.
+  const onTooltipReady = tooltip.once("shown");
+  await view.tooltips.onInteractiveTooltipTargetHover(icon);
+  tooltip.show(icon);
+  await onTooltipReady;
+
+  // Get the tooltip's actual HTML content.
+  const actual = tooltip.panel.firstElementChild.outerHTML;
+
+  // Hide the tooltip.
+  const onTooltipHidden = tooltip.once("hidden");
+  tooltip.hide();
+  await onTooltipHidden;
+
+  // Finally, check the values.
+  is(actual, expected, "Tooltip contains the correct value.");
+}
+
+/**
+ * Inactive CSS test runner.
+ *
+ * @param {ruleView} view
+ *        The rule-view instance.
+ * @param {InspectorPanel} inspector
+ *        The instance of InspectorPanel currently loaded in the toolbox.
+ * @param {Array} tests
+ *        An array of test object for this method to consume e.g.
+ *          [
+ *            {
+ *              selector: "#flex-item",
+ *              activeDeclarations: [
+ *                {
+ *                  declarations: {
+ *                    "order": "2",
+ *                  },
+ *                  ruleIndex: 0,
+ *                },
+ *                {
+ *                  declarations: {
+ *                    "flex-basis": "auto",
+ *                    "flex-grow": "1",
+ *                    "flex-shrink": "1",
+ *                  },
+ *                  ruleIndex: 1,
+ *                },
+ *              ],
+ *              inactiveDeclarations: [
+ *                {
+ *                  declaration: {
+ *                    "flex-direction": "row",
+ *                  },
+ *                  ruleIndex: 1,
+ *                },
+ *              ],
+ *            },
+ *            ...
+ *          ]
+ */
+async function runInactiveCSSTests(view, inspector, tests) {
+  for (const test of tests) {
+    if (test.selector) {
+      await selectNode(test.selector, inspector);
+    }
+
+    if (test.activeDeclarations) {
+      info("Checking whether declarations are marked as used.");
+
+      for (const activeDeclarations of test.activeDeclarations) {
+        for (const [name, value] of Object.entries(activeDeclarations.declarations)) {
+          checkDeclarationIsActive(view, activeDeclarations.ruleIndex, {
+            [name]: value,
+          });
+        }
+      }
+    }
+
+    if (test.inactiveDeclarations) {
+      info("Checking that declarations are unused and have a warning.");
+
+      for (const inactiveDeclaration of test.inactiveDeclarations) {
+        await checkDeclarationIsInactive(view,
+                                         inactiveDeclaration.ruleIndex,
+                                         inactiveDeclaration.declaration);
+      }
+    }
+  }
+}
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -696,17 +696,17 @@ RuleEditor.prototype = {
       if (newRuleIndex === -1) {
         newRuleIndex = oldIndex;
       }
 
       // Remove the old rule and insert the new rule.
       rules.splice(oldIndex, 1);
       rules.splice(newRuleIndex, 0, newRule);
       elementStyle._changed();
-      elementStyle.markOverriddenAll();
+      elementStyle.onRuleUpdated();
 
       // We install the new editor in place of the old -- you might
       // think we would replicate the list-modification logic above,
       // but that is complicated due to the way the UI installs
       // pseudo-element rules and the like.
       this.element.parentNode.replaceChild(editor.element, this.element);
 
       // Remove highlight for modified selector
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -177,16 +177,21 @@ TextPropertyEditor.prototype = {
     appendText(this.valueContainer, ";");
 
     this.warning = createChild(this.container, "div", {
       class: "ruleview-warning",
       hidden: "",
       title: l10n("rule.warning.title"),
     });
 
+    this.unusedState = createChild(this.container, "div", {
+      class: "ruleview-unused-warning",
+      hidden: "",
+    });
+
     // Filter button that filters for the current property name and is
     // displayed when the property is overridden by another rule.
     this.filterProperty = createChild(this.container, "div", {
       class: "ruleview-overridden-rule-filter",
       hidden: "",
       title: l10n("rule.filterProperty.title"),
     });
 
@@ -591,18 +596,18 @@ TextPropertyEditor.prototype = {
     // - all of the computed properties have defined values. In case the current property
     //   value contains CSS variables, then the computed properties will be missing and we
     //   want to avoid showing them.
     return this.prop.computed.some(c => c.name !== this.prop.name) &&
            !this.prop.computed.every(c => !c.value);
   },
 
   /**
-   * Update the visibility of the enable checkbox, the warning indicator and
-   * the filter property, as well as the overridden state of the property.
+   * Update the visibility of the enable checkbox, the warning indicator, the used
+   * indicator and the filter property, as well as the overridden state of the property.
    */
   updatePropertyState: function() {
     if (this.prop.enabled) {
       this.enable.style.removeProperty("visibility");
       this.enable.setAttribute("checked", "");
     } else {
       this.enable.style.visibility = "visible";
       this.enable.removeAttribute("checked");
@@ -623,16 +628,26 @@ TextPropertyEditor.prototype = {
 
     if (!this.editing &&
         (this.prop.overridden || !this.prop.enabled ||
          !this.prop.isKnownProperty)) {
       this.element.classList.add("ruleview-overridden");
     } else {
       this.element.classList.remove("ruleview-overridden");
     }
+
+    const { used } = this.prop.isUsed();
+
+    if (this.editing || this.prop.overridden || !this.prop.enabled || used) {
+      this.element.classList.remove("unused");
+      this.unusedState.hidden = true;
+    } else {
+      this.element.classList.add("unused");
+      this.unusedState.hidden = false;
+    }
   },
 
   /**
    * Update the indicator for computed styles. The computed styles themselves
    * are populated on demand, when they become visible.
    */
   _updateComputed: function() {
     this.computed.innerHTML = "";
--- a/devtools/client/inspector/shared/node-types.js
+++ b/devtools/client/inspector/shared/node-types.js
@@ -5,17 +5,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /**
  * Types of nodes used in the rule and computed view.
  */
 
-exports.VIEW_NODE_SELECTOR_TYPE = 1;
-exports.VIEW_NODE_PROPERTY_TYPE = 2;
-exports.VIEW_NODE_VALUE_TYPE = 3;
-exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
-exports.VIEW_NODE_LOCATION_TYPE = 5;
-exports.VIEW_NODE_SHAPE_POINT_TYPE = 6;
-exports.VIEW_NODE_VARIABLE_TYPE = 7;
-exports.VIEW_NODE_FONT_TYPE = 8;
-exports.VIEW_NODE_SHAPE_SWATCH = 9;
+exports.VIEW_NODE_FONT_TYPE = "font-type";
+exports.VIEW_NODE_IMAGE_URL_TYPE = "image-url-type";
+exports.VIEW_NODE_INACTIVE_CSS = "inactive-css";
+exports.VIEW_NODE_LOCATION_TYPE = "location-type";
+exports.VIEW_NODE_PROPERTY_TYPE = "property-type";
+exports.VIEW_NODE_SELECTOR_TYPE = "selector-type";
+exports.VIEW_NODE_SHAPE_POINT_TYPE = "shape-point-type";
+exports.VIEW_NODE_SHAPE_SWATCH = "shape-swatch";
+exports.VIEW_NODE_VALUE_TYPE = "value-type";
+exports.VIEW_NODE_VARIABLE_TYPE = "variable-type";
--- a/devtools/client/inspector/shared/tooltips-overlay.js
+++ b/devtools/client/inspector/shared/tooltips-overlay.js
@@ -12,37 +12,41 @@
  */
 
 const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const {
   VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_FONT_TYPE,
   VIEW_NODE_IMAGE_URL_TYPE,
+  VIEW_NODE_INACTIVE_CSS,
   VIEW_NODE_VARIABLE_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 
 loader.lazyRequireGetter(this, "getColor",
   "devtools/client/shared/theme", true);
 loader.lazyRequireGetter(this, "HTMLTooltip",
   "devtools/client/shared/widgets/tooltip/HTMLTooltip", true);
 loader.lazyRequireGetter(this, "getImageDimensions",
   "devtools/client/shared/widgets/tooltip/ImageTooltipHelper", true);
 loader.lazyRequireGetter(this, "setImageTooltip",
   "devtools/client/shared/widgets/tooltip/ImageTooltipHelper", true);
 loader.lazyRequireGetter(this, "setBrokenImageTooltip",
   "devtools/client/shared/widgets/tooltip/ImageTooltipHelper", true);
 loader.lazyRequireGetter(this, "setVariableTooltip",
   "devtools/client/shared/widgets/tooltip/VariableTooltipHelper", true);
+loader.lazyRequireGetter(this, "InactiveCssTooltipHelper",
+  "devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper", false);
 
 const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
 
 // Types of existing tooltips
 const TOOLTIP_IMAGE_TYPE = "image";
 const TOOLTIP_FONTFAMILY_TYPE = "font-family";
+const TOOLTIP_INACTIVE_CSS = "inactive-css";
 const TOOLTIP_VARIABLE_TYPE = "variable";
 
 /**
  * Manages all tooltips in the style-inspector.
  *
  * @param {CssRuleView|CssComputedView} view
  *        Either the rule-view or computed-view panel
  */
@@ -76,26 +80,31 @@ TooltipsOverlay.prototype = {
    */
   addToView: function() {
     if (this._isStarted || this._isDestroyed) {
       return;
     }
 
     this._isStarted = true;
 
-    // Instantiate the preview tooltip when the rule/computed view is hovered over in
-    // order to call tooltip.starTogglingOnHover. This will allow the preview tooltip
-    // to be shown when an appropriate element is hovered over.
-    if (flags.testing) {
-      this.getTooltip("previewTooltip");
-    } else {
-      // Lazily get the preview tooltip to avoid loading HTMLTooltip.
-      this.view.element.addEventListener("mousemove", () => {
-        this.getTooltip("previewTooltip");
-      }, { once: true });
+    this.inactiveCssTooltipHelper = new InactiveCssTooltipHelper();
+
+    // Instantiate the interactiveTooltip and preview tooltip when the
+    // rule/computed view is hovered over in order to call
+    // `tooltip.startTogglingOnHover`. This will allow the tooltip to be shown
+    // when an appropriate element is hovered over.
+    for (const type of ["interactiveTooltip", "previewTooltip"]) {
+      if (flags.testing) {
+        this.getTooltip(type);
+      } else {
+        // Lazily get the preview tooltip to avoid loading HTMLTooltip.
+        this.view.element.addEventListener("mousemove", () => {
+          this.getTooltip(type);
+        }, { once: true });
+      }
     }
   },
 
   /**
    * Lazily fetch and initialize the different tooltips that are used in the inspector.
    * These tooltips are attached to the toolbox document if they require a popup panel.
    * Otherwise, it is attached to the inspector panel document if it is an inline editor.
    *
@@ -121,16 +130,26 @@ TooltipsOverlay.prototype = {
         tooltip = new SwatchCubicBezierTooltip(doc);
         break;
       case "filterEditor":
         const SwatchFilterTooltip =
           require("devtools/client/shared/widgets/tooltip/SwatchFilterTooltip");
         tooltip = new SwatchFilterTooltip(doc,
           this._cssProperties.getValidityChecker(this.view.inspector.panelDoc));
         break;
+      case "interactiveTooltip":
+        tooltip = new HTMLTooltip(doc, {
+          type: "doorhanger",
+          useXulWrapper: true,
+        });
+        tooltip.startTogglingOnHover(this.view.element,
+          this.onInteractiveTooltipTargetHover.bind(this), {
+            interactive: true,
+          });
+        break;
       case "previewTooltip":
         tooltip = new HTMLTooltip(doc, {
           type: "arrow",
           useXulWrapper: true,
         });
         tooltip.startTogglingOnHover(this.view.element,
           this._onPreviewTooltipTargetHover.bind(this));
         break;
@@ -149,16 +168,18 @@ TooltipsOverlay.prototype = {
     if (!this._isStarted || this._isDestroyed) {
       return;
     }
 
     for (const [, tooltip] of this._instances) {
       tooltip.destroy();
     }
 
+    this.inactiveCssTooltipHelper.destroy();
+
     this._isStarted = false;
   },
 
   /**
    * Given a hovered node info, find out which type of tooltip should be shown,
    * if any
    *
    * @param {Object} nodeInfo
@@ -176,16 +197,21 @@ TooltipsOverlay.prototype = {
     if ((type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") ||
         (type === VIEW_NODE_FONT_TYPE)) {
       const value = prop.value.toLowerCase();
       if (value !== "inherit" && value !== "unset" && value !== "initial") {
         tooltipType = TOOLTIP_FONTFAMILY_TYPE;
       }
     }
 
+    // Inactive CSS tooltip
+    if (type === VIEW_NODE_INACTIVE_CSS) {
+      tooltipType = TOOLTIP_INACTIVE_CSS;
+    }
+
     // Variable preview tooltip
     if (type === VIEW_NODE_VARIABLE_TYPE) {
       tooltipType = TOOLTIP_VARIABLE_TYPE;
     }
 
     return tooltipType;
   },
 
@@ -248,16 +274,64 @@ TooltipsOverlay.prototype = {
       await this._setVariablePreviewTooltip(variable);
       return true;
     }
 
     return false;
   },
 
   /**
+   * Executed by the tooltip when the pointer hovers over an element of the
+   * view. Used to decide whether the tooltip should be shown or not and to
+   * actually put content in it.
+   * Checks if the hovered target is a css value we support tooltips for.
+   *
+   * @param  {DOMNode} target
+   *         The currently hovered node
+   * @return {Boolean}
+   *         true if shown, false otherwise.
+   */
+  async onInteractiveTooltipTargetHover(target) {
+    const nodeInfo = this.view.getNodeInfo(target);
+    if (!nodeInfo) {
+      // The hovered node isn't something we care about.
+      return false;
+    }
+
+    const type = this._getTooltipType(nodeInfo);
+    if (!type) {
+      // There is no tooltip type defined for the hovered node.
+      return false;
+    }
+
+    // Remove previous tooltip instances.
+    for (const [, tooltip] of this._instances) {
+      if (tooltip.isVisible()) {
+        if (tooltip.revert) {
+          tooltip.revert();
+        }
+        tooltip.hide();
+      }
+    }
+
+    if (type === TOOLTIP_INACTIVE_CSS) {
+    // Ensure this is the correct node and not a parent.
+      if (!target.classList.contains("ruleview-unused-warning")) {
+        return false;
+      }
+
+      await this.inactiveCssTooltipHelper.setContent(
+        nodeInfo.value, this.getTooltip("interactiveTooltip"));
+      return true;
+    }
+
+    return false;
+  },
+
+  /**
    * Set the content of the preview tooltip to display an image preview. The image URL can
    * be relative, a call will be made to the debuggee to retrieve the image content as an
    * imageData URI.
    *
    * @param {String} imageUrl
    *        The image url value (may be relative or absolute).
    * @return {Promise} A promise that resolves when the preview tooltip content is ready
    */
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -770,30 +770,30 @@ async function assertTooltipShownOnHover
  * @param {CssRuleView|ComputedView|...} view
  *        The instance of an inspector panel
  * @param {DOMElement} target
  *        The DOM Element on which a tooltip should appear
  *
  * @return a promise that resolves with the tooltip object
  */
 async function assertShowPreviewTooltip(view, target) {
+  const name = "previewTooltip";
+
+  // Get the tooltip. If it does not exist one will be created.
+  const tooltip = view.tooltips.getTooltip(name);
+  ok(tooltip, `Tooltip '${name}' has been instantiated`);
+
+  const shown = tooltip.once("shown");
   const mouseEvent = new target.ownerDocument.defaultView.MouseEvent("mousemove", {
     bubbles: true,
   });
   target.dispatchEvent(mouseEvent);
 
-  const name = "previewTooltip";
-  ok(view.tooltips._instances.has(name),
-    `Tooltip '${name}' has been instantiated`);
-  const tooltip = view.tooltips.getTooltip(name);
-
-  if (!tooltip.isVisible()) {
-    info("Waiting for tooltip to be shown");
-    await tooltip.once("shown");
-  }
+  info("Waiting for tooltip to be shown");
+  await shown;
 
   ok(tooltip.isVisible(), `The tooltip '${name}' is visible`);
 
   return tooltip;
 }
 
 /**
  * Given a `tooltip` instance, fake a mouse event on `target` DOM element
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -94,16 +94,17 @@ devtools.jar:
     skin/images/copy.svg (themes/images/copy.svg)
     skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
     skin/images/performance-details-waterfall.svg (themes/images/performance-details-waterfall.svg)
     skin/images/performance-details-call-tree.svg (themes/images/performance-details-call-tree.svg)
     skin/images/performance-details-flamegraph.svg (themes/images/performance-details-flamegraph.svg)
     skin/breadcrumbs.css (themes/breadcrumbs.css)
     skin/chart.css (themes/chart.css)
     skin/widgets.css (themes/widgets.css)
+    skin/images/alerticon-unused.svg (themes/images/alerticon-unused.svg)
     skin/rules.css (themes/rules.css)
     skin/images/command-paintflashing.svg (themes/images/command-paintflashing.svg)
     skin/images/command-screenshot.svg (themes/images/command-screenshot.svg)
     skin/images/command-responsivemode.svg (themes/images/command-responsivemode.svg)
     skin/images/command-replay.svg (themes/images/command-replay.svg)
     skin/images/command-pick.svg (themes/images/command-pick.svg)
     skin/images/command-pick-accessibility.svg (themes/images/command-pick-accessibility.svg)
     skin/images/command-frames.svg (themes/images/command-frames.svg)
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/tooltips.ftl
@@ -0,0 +1,41 @@
+# 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/.
+
+### Localization for Developer Tools tooltips.
+
+learn-more = <span data-l10n-name="link">Learn more</span>
+
+## In the Rule View when a CSS property cannot be successfully applied we display
+## an icon. When this icon is hovered this message is displayed to explain why
+## the property is not applied.
+## Variables:
+##   $property (string) - A CSS property name e.g. "color".
+
+inactive-css-not-grid-or-flex-container = <strong>{ $property }</strong> has no effect on this element since it’s neither a flex container nor a grid container.
+
+inactive-css-not-grid-or-flex-item = <strong>{ $property }</strong> has no effect on this element since it’s not a grid or flex item.
+
+inactive-css-not-grid-item = <strong>{ $property }</strong> has no effect on this element since it’s not a grid item.
+
+inactive-css-not-grid-container = <strong>{ $property }</strong> has no effect on this element since it’s not a grid container.
+
+inactive-css-not-flex-item = <strong>{ $property }</strong> has no effect on this element since it’s not a flex item.
+
+inactive-css-not-flex-container = <strong>{ $property }</strong> has no effect on this element since it’s not a flex container.
+
+## In the Rule View when a CSS property cannot be successfully applied we display
+## an icon. When this icon is hovered this message is displayed to explain how
+## the problem can be solved.
+
+inactive-css-not-grid-or-flex-container-fix = Try adding <strong>display:grid</strong> or <strong>display:flex</strong>. { learn-more }
+
+inactive-css-not-grid-or-flex-item-fix = Try adding <strong>display:grid</strong>, <strong>display:flex</strong>, <strong>display:inline-grid</strong> or <strong>display:inline-flex</strong>. { learn-more }
+
+inactive-css-not-grid-item-fix =Try adding <strong>display:grid</strong> or <strong>display:inline-grid</strong> to the item’s parent. { learn-more }
+
+inactive-css-not-grid-container-fix = Try adding <strong>display:grid</strong> or <strong>display:inline-grid</strong>. { learn-more }
+
+inactive-css-not-flex-item-fix = Try adding <strong>display:flex</strong> or <strong>display:inline-flex</strong> to the item’s parent. { learn-more }
+
+inactive-css-not-flex-container-fix = Try adding <strong>display:flex</strong> or <strong>display:inline-flex</strong>. { learn-more }
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -45,16 +45,22 @@ pref("devtools.inspector.show_pseudo_ele
 // The default size for image preview tooltips in the rule-view/computed-view/markup-view
 pref("devtools.inspector.imagePreviewTooltipSize", 300);
 // Enable user agent style inspection in rule-view
 pref("devtools.inspector.showUserAgentStyles", false);
 // Show all native anonymous content
 pref("devtools.inspector.showAllAnonymousContent", false);
 // Show user agent shadow roots
 pref("devtools.inspector.showUserAgentShadowRoots", false);
+// Enable Inactive CSS detection
+#if defined(NIGHTLY_BUILD)
+pref("devtools.inspector.inactive.css.enabled", true);
+#else
+pref("devtools.inspector.inactive.css.enabled", false);
+#endif
 // Enable the new Rules View
 pref("devtools.inspector.new-rulesview.enabled", false);
 
 // Grid highlighter preferences
 pref("devtools.gridinspector.gridOutlineMaxColumns", 50);
 pref("devtools.gridinspector.gridOutlineMaxRows", 50);
 pref("devtools.gridinspector.showGridAreas", false);
 pref("devtools.gridinspector.showGridLineNumbers", false);
--- a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
@@ -716,16 +716,21 @@ HTMLTooltip.prototype = {
     return { width, height };
   },
 
   /**
    * Hide the current tooltip. The event "hidden" will be fired when the tooltip
    * is hidden.
    */
   async hide({ fromMouseup = false } = {}) {
+    // Exit if the disable autohide setting is in effect.
+    if (Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
+      return;
+    }
+
     this.doc.defaultView.clearTimeout(this.attachEventsTimer);
     if (!this.isVisible()) {
       this.emit("hidden");
       return;
     }
 
     // If the tooltip is hidden from a mouseup event, wait for a potential click event
     // to be consumed before removing event listeners.
@@ -821,21 +826,16 @@ HTMLTooltip.prototype = {
    * If the element that received the mousedown and the mouseup are different, click
    * will not be fired.
    */
   _onMouseup: function(e) {
     if (this._isInTooltipContainer(e.target)) {
       return;
     }
 
-    // If the disable autohide setting is in effect, ignore.
-    if (Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
-      return;
-    }
-
     this.hide({ fromMouseup: true });
   },
 
   _isInTooltipContainer: function(node) {
     // Check if the target is the tooltip arrow.
     if (this.arrow && this.arrow === node) {
       return true;
     }
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper.js
@@ -0,0 +1,128 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=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/. */
+
+"use strict";
+
+loader.lazyRequireGetter(this, "openDocLink", "devtools/client/shared/link", true);
+
+class InactiveCssTooltipHelper {
+  constructor() {
+    this.addTab = this.addTab.bind(this);
+  }
+
+  /**
+   * Fill the tooltip with inactive CSS information.
+   *
+   * @param {String} propertyName
+   *        The property name to be displayed in bold.
+   * @param {String} text
+   *        The main text, which follows property name.
+   */
+  async setContent(data, tooltip) {
+    const fragment = this.getTemplate(data, tooltip);
+    const { doc } = tooltip;
+
+    tooltip.panel.innerHTML = "";
+
+    tooltip.panel.addEventListener("click", this.addTab);
+    tooltip.once("hidden", () => {
+      tooltip.panel.removeEventListener("click", this.addTab);
+    });
+
+    // Because Fluent is async we need to manually translate the fragment and
+    // then insert it into the tooltip. This is needed in order for the tooltip
+    // to size to the contents properly and for tests.
+    await doc.l10n.translateFragment(fragment);
+    doc.l10n.pauseObserving();
+    tooltip.panel.appendChild(fragment);
+    doc.l10n.resumeObserving();
+
+    // Size the content.
+    tooltip.setContentSize({width: 275, height: Infinity});
+  }
+/**
+ * Get the template that the Fluent string will be merged with. This template
+ * looks something like this but there is a variable amount of properties in the
+ * fix section:
+ *
+ * <div class="devtools-tooltip-inactive-css">
+ *   <p data-l10n-id="inactive-css-not-grid-or-flex-container"
+ *      data-l10n-args="{&quot;property&quot;:&quot;align-content&quot;}">
+ *     <strong></strong>
+ *   </p>
+ *   <p data-l10n-id="inactive-css-not-grid-or-flex-container-fix">
+ *     <strong></strong>
+ *     <strong></strong>
+ *     <span data-l10n-name="link" class="link"></span>
+ *   </p>
+ * </div>
+ *
+ * @param {Object} data
+ *        An object in the following format: {
+ *          fixId: "inactive-css-not-grid-item-fix", // Fluent id containing the
+ *                                                   // Inactive CSS fix.
+ *          msgId: "inactive-css-not-grid-item", // Fluent id containing the
+ *                                               // Inactive CSS message.
+ *          numFixProps: 2, // Number of properties in the fix section of the
+ *                          // tooltip.
+ *          property: "color", // Property name
+ *        }
+ * @param {HTMLTooltip} tooltip
+ *        The tooltip we are targetting.
+ */
+  getTemplate(data, tooltip) {
+    const XHTML_NS = "http://www.w3.org/1999/xhtml";
+    const { fixId, msgId, numFixProps, property } = data;
+    const { doc } = tooltip;
+
+    this._currentTooltip = tooltip;
+    this._currentUrl = `https://developer.mozilla.org/docs/Web/CSS/${property}`;
+
+    const templateNode = doc.createElementNS(XHTML_NS, "template");
+
+    // eslint-disable-next-line
+    templateNode.innerHTML = `
+    <div class="devtools-tooltip-inactive-css">
+      <p data-l10n-id="${msgId}"
+         data-l10n-args='${JSON.stringify({property})}'>
+        <strong></strong>
+      </p>
+      <p data-l10n-id="${fixId}">
+        ${"<strong></strong>".repeat(numFixProps)}
+        <span data-l10n-name="link" class="link"></span>
+      </p>
+    </div>`;
+
+    return doc.importNode(templateNode.content, true);
+  }
+
+  /**
+   * Hide the tooltip, open `this._currentUrl` in a new tab and focus it.
+   *
+   * @param {DOMEvent} event
+   *        The click event originating from the tooltip.
+   *
+   */
+  addTab(event) {
+    // The XUL panel swallows click events so handlers can't be added directly
+    // to the link span. As a workaround we listen to all click events in the
+    // panel and if a link span is clicked we proceed.
+    if (event.target.className !== "link") {
+      return;
+    }
+
+    const tooltip = this._currentTooltip;
+    tooltip.hide();
+    openDocLink(this._currentUrl);
+  }
+
+  destroy() {
+    this._currentTooltip = null;
+    this._currentUrl = null;
+  }
+}
+
+module.exports = InactiveCssTooltipHelper;
--- a/devtools/client/shared/widgets/tooltip/moz.build
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -3,16 +3,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'EventTooltipHelper.js',
     'HTMLTooltip.js',
     'ImageTooltipHelper.js',
+    'inactive-css-tooltip-helper.js',
     'InlineTooltip.js',
     'RulePreviewTooltip.js',
     'SwatchBasedEditorTooltip.js',
     'SwatchColorPickerTooltip.js',
     'SwatchCubicBezierTooltip.js',
     'SwatchFilterTooltip.js',
     'TooltipToggle.js',
     'VariableTooltipHelper.js'
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/alerticon-unused.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16">
+  <path stroke="context-stroke" fill="none" d="M15.5 8.5C15.5 12.36 12.36 15.5 8.5 15.5C4.63 15.5 1.5 12.36 1.5 8.5C1.5 4.64 4.63 1.5 8.5 1.5C12.36 1.5 15.5 4.64 15.5 8.5Z"/>
+  <path fill="context-fill" d="M8.98 7.47C9.52 7.47 9.96 7.91 9.96 8.45C9.96 9.42 9.96 11.33 9.96 12.29C9.96 12.83 9.52 13.27 8.98 13.27C8.59 13.27 8.4 13.27 8.01 13.27C7.47 13.27 7.03 12.83 7.03 12.29C7.03 11.33 7.03 9.42 7.03 8.45C7.03 7.91 7.47 7.47 8.01 7.47C8.4 7.47 8.59 7.47 8.98 7.47Z"/>
+  <path fill="context-fill" d="M9.96 5.36C9.96 6.16 9.3 6.81 8.49 6.81C7.69 6.81 7.03 6.16 7.03 5.36C7.03 4.57 7.69 3.92 8.49 3.92C9.3 3.92 9.96 4.57 9.96 5.36Z"/>
+</svg>
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -2,38 +2,41 @@
  * 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/. */
 
 /* CSS Variables specific to this panel that aren't defined by the themes */
 :root {
   --rule-highlight-background-color: var(--theme-highlight-yellow);
   --rule-header-background-color: var(--theme-toolbar-background);
   --rule-pseudo-class-text-color: var(--yellow-70) ;
+
   /* This should be --yellow-50 but since we need an opacity of 0.4, we hard-code the
   resulting color here for now. */
   --rule-property-highlight-background-color: #FFF697;
 }
 
 :root.theme-dark {
   --rule-highlight-background-color: #521C76;
   --rule-header-background-color: #222225;
   --rule-pseudo-class-text-color: var(--yellow-50);
+
   /* This should be --yellow-50 but since we need an opacity of 0.3, we hard-code the
   resulting color here for now. */
   --rule-property-highlight-background-color: #605913;
 }
 
 /* Rule View Tabpanel */
 
 #sidebar-panel-ruleview {
   margin: 0;
   display: flex;
   flex-direction: column;
   width: 100%;
   height: 100%;
+
   /* Override the min-width from .inspector-tabpanel, as the rule panel can support small
      widths */
   min-width: 100px;
 }
 
 /* Rule View Toolbar */
 
 #ruleview-toolbar-container {
@@ -42,17 +45,17 @@
   padding: 0;
 }
 
 #ruleview-toolbar {
   display: flex;
 }
 
 #ruleview-toolbar > .devtools-searchbox:first-child {
-  padding-inline-start: 0px;
+  padding-inline-start: 0;
 }
 
 #ruleview-command-toolbar {
   display: flex;
 }
 
 .ruleview-reveal-panel {
   background: var(--rule-header-background-color);
@@ -186,16 +189,18 @@
   cursor: pointer;
 }
 
 .ruleview-computedlist,
 .ruleview-expandable-container[hidden],
 .ruleview-overridden-items[hidden],
 .ruleview-overridden-rule-filter[hidden],
 .ruleview-warning[hidden],
+.ruleview-unused-warning[hidden],
+.ruleview-used[hidden],
 .ruleview-overridden .ruleview-grid {
   display: none;
 }
 
 .ruleview-computedlist[user-open],
 .ruleview-computedlist[filter-open],
 .ruleview-overridden-items {
   display: block;
@@ -254,17 +259,17 @@
   cursor: pointer;
 }
 
 .ruleview-expandable-header:hover {
   background-color: var(--theme-toolbar-background-hover);
 }
 
 .ruleview-rule-pseudo-element {
-  padding-left:20px;
+  padding-left: 20px;
   border-left: solid 10px;
 }
 
 .ruleview-rule {
   border-bottom: 1px solid var(--theme-splitter-color);
   padding: 2px 4px;
 }
 
@@ -312,16 +317,21 @@
 .ruleview-rule.uneditable .ruleview-propertyvaluecontainer >
 .ruleview-propertyvalue,
 .ruleview-rule[uneditable=true] .ruleview-namecontainer > .ruleview-propertyname,
 .ruleview-rule[uneditable=true] .ruleview-propertyvaluecontainer >
 .ruleview-propertyvalue {
   border-bottom-color: transparent;
 }
 
+.ruleview-property.unused .ruleview-namecontainer,
+.ruleview-property.unused .ruleview-propertyvaluecontainer {
+  opacity: 0.5;
+}
+
 .ruleview-overridden-rule-filter {
   display: inline-block;
   width: 14px;
   height: 14px;
   margin-inline-start: 3px;
   background-image: url(chrome://devtools/skin/images/filter-small.svg);
   background-position: center;
   background-repeat: no-repeat;
@@ -354,27 +364,41 @@
   position: relative;
   float: left;
   left: -38px;
   box-sizing: content-box;
   border-left: 10px solid transparent;
   background-clip: content-box;
 }
 
-.ruleview-warning {
+.ruleview-warning,
+.ruleview-unused-warning {
   display: inline-block;
   width: 12px;
   height: 12px;
   margin-inline-start: 5px;
   background-image: url(chrome://devtools/skin/images/alert.svg);
   background-size: cover;
   -moz-context-properties: fill;
   fill: var(--yellow-60);
 }
 
+.ruleview-unused-warning {
+  background-image: url(chrome://devtools/skin/images/alerticon-unused.svg);
+  background-color: var(--theme-sidebar-background);
+  -moz-context-properties: fill, stroke;
+  fill: var(--theme-icon-dimmed-color);
+  stroke: var(--theme-icon-dimmed-color);
+}
+
+.ruleview-unused-warning:hover {
+  fill: var(--theme-icon-color);
+  stroke: var(--theme-icon-color);
+}
+
 .ruleview-rule:not(:hover) .ruleview-enableproperty {
   visibility: hidden;
 }
 
 .ruleview-expander {
   vertical-align: middle;
 }
 
@@ -413,21 +437,21 @@
   position: relative;
 }
 
 .ruleview-overridden-item::before,
 .ruleview-overridden-item::after {
   content: "";
   position: absolute;
   display: block;
-  border: 0px solid var(--theme-text-color-alt);
+  border: 0 solid var(--theme-text-color-alt);
 }
 
 .ruleview-overridden-item::before {
-  top: 0px;
+  top: 0;
   left: -15px;
   height: 0.8em;
   width: 10px;
   border-left-width: 0.5px;
   border-bottom-width: 0.5px;
 }
 
 .ruleview-overridden-item::after {
@@ -450,16 +474,17 @@
 .ruleview-flex,
 .ruleview-grid,
 .ruleview-shapeswatch,
 .ruleview-swatch {
   cursor: pointer;
   width: 1em;
   height: 1em;
   vertical-align: middle;
+
   /* align the swatch with its value */
   margin-top: -1px;
   margin-inline-end: 5px;
   display: inline-block;
   position: relative;
 }
 
 /* Icon swatches not using the .ruleview-swatch class (flex, grid, shape) */
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -62,23 +62,45 @@
 :root[platform="mac"] {
   --theme-arrowpanel-border-radius: 3.5px;
 }
 
 :root[platform="mac"].theme-light {
   --theme-arrowpanel-separator: hsla(210,4%,10%,.14);
 }
 
+strong {
+  font-weight: bold;
+}
+
 /* Tooltip: CSS variables tooltip */
 
 .devtools-tooltip-css-variable {
   color: var(--theme-body-color);
   padding: 2px;
 }
 
+/* Tooltip: Inactive CSS tooltip */
+
+.devtools-tooltip-inactive-css {
+  color: var(--theme-body-color);
+  padding: 7px 10px;
+  margin: 0;
+}
+
+.devtools-tooltip-inactive-css p {
+  margin-block-start: 0;
+  margin-block-end: 0;
+}
+
+.devtools-tooltip-inactive-css .link {
+  color: var(--theme-highlight-blue);
+  cursor: pointer;
+}
+
 /* Tooltip: Tiles */
 
 .devtools-tooltip-tiles {
   background-color: #eee;
   background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
     linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
   background-size: 20px 20px;
   background-position: 0 0, 10px 10px;
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -16,16 +16,18 @@ const TrackChangeEmitter = require("devt
 const {pageStyleSpec, styleRuleSpec, ELEMENT_STYLE} = require("devtools/shared/specs/styles");
 
 loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "SharedCssLogic", "devtools/shared/inspector/css-logic");
 loader.lazyRequireGetter(this, "getDefinedGeometryProperties",
   "devtools/server/actors/highlighters/geometry-editor", true);
 loader.lazyRequireGetter(this, "isCssPropertyKnown",
   "devtools/server/actors/css-properties", true);
+loader.lazyRequireGetter(this, "inactivePropertyHelper",
+  "devtools/server/actors/utils/inactive-property-helper", true);
 loader.lazyRequireGetter(this, "parseNamedDeclarations",
   "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "prettifyCSS",
   "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "UPDATE_PRESERVING_RULES",
   "devtools/server/actors/stylesheets", true);
 loader.lazyRequireGetter(this, "UPDATE_GENERAL",
   "devtools/server/actors/stylesheets", true);
@@ -1278,24 +1280,28 @@ var StyleRuleActor = protocol.ActorClass
     // and so that we can safely determine if a declaration is valid rather than
     // have the client guess it.
     if (form.authoredText || form.cssText) {
       // authoredText may be an empty string when deleting all properties; it's ok to use.
       const cssText = (typeof form.authoredText === "string")
         ? form.authoredText
         : form.cssText;
       const declarations = parseNamedDeclarations(isCssPropertyKnown, cssText, true);
+      const el = this.pageStyle.cssLogic.viewedElement;
+      const style = this.pageStyle.cssLogic.computedStyle;
 
       // We need to grab CSS from the window, since calling supports() on the
       // one from the current global will fail due to not being an HTML global.
       const CSS = this.pageStyle.inspector.targetActor.window.CSS;
       form.declarations = declarations.map(decl => {
         // Use the 1-arg CSS.supports() call so that we also accept !important
         // in the value.
         decl.isValid = CSS.supports(`${decl.name}:${decl.value}`);
+        decl.isUsed = inactivePropertyHelper.isPropertyUsed(
+          el, style, this.rawRule, decl.name);
         // Check property name. All valid CSS properties support "initial" as a value.
         decl.isNameValid = CSS.supports(decl.name, "initial");
         return decl;
       });
       // Cache parsed declarations so we don't needlessly re-parse authoredText every time
       // we need need to check previous property names and values when tracking changes.
       this._declarations = declarations;
     }
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/utils/inactive-property-helper.js
@@ -0,0 +1,382 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=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/. */
+
+"use strict";
+
+const Services = require("Services");
+
+const PREF_UNUSED_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";
+
+class InactivePropertyHelper {
+  /**
+   * A list of rules for when CSS properties have no effect.
+   *
+   * In certain situations, CSS properties do not have any effect. A common
+   * example is trying to set a width on an inline element like a <span>.
+   *
+   * There are so many properties in CSS that it's difficult to remember which
+   * ones do and don't apply in certain situations. Some are straight-forward
+   * like `flex-wrap` only applying to an element that has `display:flex`.
+   * Others are less trivial like setting something other than a color on a
+   * `:visited` pseudo-class.
+   *
+   * This file contains "rules" in the form of objects with the following
+   * properties:
+   * {
+   *   invalidProperties (see note):
+   *     Array of CSS property names that are inactive if the rule matches.
+   *   validProperties (see note):
+   *     Array of CSS property names that are active if the rule matches.
+   *   when:
+   *     The rule itself, a JS function used to identify the conditions
+   *     indicating whether a property is valid or not.
+   *   fixId:
+   *     A Fluent id containing a suggested solution to the problem that is
+   *     causing a property to be inactive.
+   *   msgId:
+   *     A Fluent id containing an error message explaining why a property is
+   *     inactive in this situation.
+   *   numFixProps:
+   *     The number of properties we suggest in the fixId string.
+   * }
+   *
+   * NOTE: validProperties and invalidProperties are mutually exclusive.
+   *
+   * The main export is `isPropertyUsed()`, which can be used to check if a
+   * property is used or not, and why.
+   */
+  get VALIDATORS() {
+    return [
+      // Flex container property used on non-flex container.
+      {
+        invalidProperties: [
+          "flex-direction",
+          "flex-flow",
+          "flex-wrap",
+        ],
+        when: () => !this.flexContainer,
+        fixId: "inactive-css-not-flex-container-fix",
+        msgId: "inactive-css-not-flex-container",
+        numFixProps: 2,
+      },
+      // Flex item property used on non-flex item.
+      {
+        invalidProperties: [
+          "flex",
+          "flex-basis",
+          "flex-grow",
+          "flex-shrink",
+          "order",
+        ],
+        when: () => !this.flexItem,
+        fixId: "inactive-css-not-flex-item-fix",
+        msgId: "inactive-css-not-flex-item",
+        numFixProps: 2,
+      },
+      // Grid container property used on non-grid container.
+      {
+        invalidProperties: [
+          "grid-auto-columns",
+          "grid-auto-flow",
+          "grid-auto-rows",
+          "grid-template",
+          "grid-gap",
+          "row-gap",
+          "column-gap",
+          "justify-items",
+        ],
+        when: () => !this.gridContainer,
+        fixId: "inactive-css-not-grid-container-fix",
+        msgId: "inactive-css-not-grid-container",
+        numFixProps: 2,
+      },
+      // Grid item property used on non-grid item.
+      {
+        invalidProperties: [
+          "grid-area",
+          "grid-column",
+          "grid-column-end",
+          "grid-column-start",
+          "grid-row",
+          "grid-row-end",
+          "grid-row-start",
+          "justify-self",
+        ],
+        when: () => !this.gridItem,
+        fixId: "inactive-css-not-grid-item-fix",
+        msgId: "inactive-css-not-grid-item",
+        numFixProps: 2,
+      },
+      // Grid and flex item properties used on non-grid or non-flex item.
+      {
+        invalidProperties: [
+          "align-self",
+        ],
+        when: () => !this.gridItem && !this.flexItem,
+        fixId: "inactive-css-not-grid-or-flex-item-fix",
+        msgId: "inactive-css-not-grid-or-flex-item",
+        numFixProps: 4,
+      },
+      // Grid and flex container properties used on non-grid or non-flex container.
+      {
+        invalidProperties: [
+          "align-content",
+          "align-items",
+          "justify-content",
+        ],
+        when: () => !this.gridContainer && !this.flexContainer,
+        fixId: "inactive-css-not-grid-or-flex-container-fix",
+        msgId: "inactive-css-not-grid-or-flex-container",
+        numFixProps: 2,
+      },
+    ];
+  }
+
+  get unusedCssEnabled() {
+    if (!this._unusedCssEnabled) {
+      this._unusedCssEnabled = Services.prefs.getBoolPref(PREF_UNUSED_CSS_ENABLED);
+    }
+    return this._unusedCssEnabled;
+  }
+
+  /**
+   * Is this CSS property having any effect on this element?
+   *
+   * @param {DOMNode} el
+   *        The DOM element.
+   * @param {Style} elStyle
+   *        The computed style for this DOMNode.
+   * @param {DOMRule} cssRule
+   *        The CSS rule the property is defined in.
+   * @param {String} property
+   *        The CSS property name.
+   *
+   * @return {Object} object
+   * @return {Boolean} object.fixId
+   *         A Fluent id containing a suggested solution to the problem that is
+   *         causing a property to be inactive.
+   * @return {Boolean} object.msgId
+   *         A Fluent id containing an error message explaining why a property
+   *         is inactive in this situation.
+   * @return {Boolean} object.numFixProps
+   *         The number of properties we suggest in the fixId string.
+   * @return {Boolean} object.property
+   *         The inactive property name.
+   * @return {Boolean} object.used
+   *         true if the property is used.
+   */
+  isPropertyUsed(el, elStyle, cssRule, property) {
+    if (!this.unusedCssEnabled) {
+      return {used: true};
+    }
+
+    let fixId = "";
+    let msgId = "";
+    let numFixProps = 0;
+    let used = true;
+
+    this.VALIDATORS.some(validator => {
+      // First check if this rule cares about this property.
+      let isRuleConcerned = false;
+
+      if (validator.invalidProperties) {
+        isRuleConcerned = validator.invalidProperties === "*" ||
+                          validator.invalidProperties.includes(property);
+      } else if (validator.validProperties) {
+        isRuleConcerned = !validator.validProperties.includes(property);
+      }
+
+      if (!isRuleConcerned) {
+        return false;
+      }
+
+      this.select(el, elStyle, cssRule, property);
+
+      // And then run the validator, gathering the error message if the
+      // validator passes.
+      if (validator.when()) {
+        fixId = validator.fixId;
+        msgId = validator.msgId;
+        numFixProps = validator.numFixProps;
+        used = false;
+
+        return true;
+      }
+
+      return false;
+    });
+
+    return {
+      fixId,
+      msgId,
+      numFixProps,
+      property,
+      used,
+    };
+  }
+
+  /**
+   * Focus on a node.
+   *
+   * @param {DOMNode} node
+   *        Node to focus on.
+   */
+  select(node, style, cssRule, property) {
+    this._node = node;
+    this._cssRule = cssRule;
+    this._property = property;
+    this._style = style;
+  }
+
+  /**
+   * Provide a public reference to node.
+   */
+  get node() {
+    return this._node;
+  }
+
+  /**
+   * Cache and provide node's computed style.
+   */
+  get style() {
+    return this._style;
+  }
+
+  /**
+   * Check if the current node's propName is set to one of the values passed in
+   * the values array.
+   *
+   * @param {String} propName
+   *        Property name to check.
+   * @param {Array} values
+   *        Values to compare against.
+   */
+  checkStyle(propName, values) {
+    return this.checkStyleForNode(this.node, propName, values);
+  }
+
+  /**
+   * Check if a node's propName is set to one of the values passed in the values
+   * array.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   * @param {String} propName
+   *        Property name to check.
+   * @param {Array} values
+   *        Values to compare against.
+   */
+  checkStyleForNode(node, propName, values) {
+    return values.some(value => this.style[propName] === value);
+  }
+
+  /**
+   * Check if the current node is a flex container i.e. a node that has a style
+   * of `display:flex` or `display:inline-flex`.
+   */
+  get flexContainer() {
+    return this.checkStyle("display", ["flex", "inline-flex"]);
+  }
+
+  /**
+   * Check if the current node is a flex item.
+   */
+  get flexItem() {
+    return this.isFlexItem(this.node);
+  }
+
+  /**
+   * Check if the current node is a grid container i.e. a node that has a style
+   * of `display:grid` or `display:inline-grid`.
+   */
+  get gridContainer() {
+    return this.checkStyle("display", ["grid", "inline-grid"]);
+  }
+
+  /**
+   * Check if the current node is a grid item.
+   */
+  get gridItem() {
+    return this.isGridItem(this.node);
+  }
+
+  /**
+   * Check if a node is a flex item.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   */
+  isFlexItem(node) {
+    return !!node.parentFlexElement;
+  }
+
+  /**
+   * Check if a node is a flex container.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   */
+  isFlexContainer(node) {
+    return !!node.getAsFlexContainer();
+  }
+
+  /**
+   * Check if a node is a grid container.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   */
+  isGridContainer(node) {
+    return !!node.getGridFragments().length > 0;
+  }
+
+  /**
+   * Check if a node is a grid item.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   */
+  isGridItem(node) {
+    return !!this.getParentGridElement(this.node);
+  }
+
+  getParentGridElement(node) {
+    if (node.nodeType === node.ELEMENT_NODE) {
+      const display = this.style.display;
+
+      if (!display || display === "none" || display === "contents") {
+        // Doesn't generate a box, not a grid item.
+        return null;
+      }
+      const position = this.style.position;
+      if (position === "absolute" ||
+          position === "fixed" ||
+          this.style.cssFloat !== "none") {
+        // Out of flow, not a grid item.
+        return null;
+      }
+    } else if (node.nodeType !== node.TEXT_NODE) {
+      return null;
+    }
+
+    for (let p = node.flattenedTreeParentNode; p; p = p.flattenedTreeParentNode) {
+      const style = node.ownerGlobal.getComputedStyle(p);
+      const display = style.display;
+
+      if (display.includes("grid") && !!p.getGridFragments().length > 0) {
+        // It's a grid item!
+        return p;
+      }
+      if (display !== "contents") {
+        return null; // Not a grid item, for sure.
+      }
+      // display: contents, walk to the parent
+    }
+    return null;
+  }
+}
+
+exports.inactivePropertyHelper = new InactivePropertyHelper();
--- a/devtools/server/actors/utils/moz.build
+++ b/devtools/server/actors/utils/moz.build
@@ -8,15 +8,16 @@ DevToolsModules(
     'accessibility.js',
     'actor-registry-utils.js',
     'actor-registry.js',
     'breakpoint-actor-map.js',
     'css-grid-utils.js',
     'dbg-source.js',
     'event-breakpoints.js',
     'event-loop.js',
+    'inactive-property-helper.js',
     'make-debugger.js',
     'shapes-utils.js',
     'stack.js',
     'TabSources.js',
     'track-change-emitter.js',
     'walker-search.js',
 )
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -7657,17 +7657,18 @@ nsresult nsDocShell::RestoreFromHistory(
   // Save off the root view's parent and sibling so that we can insert the
   // new content viewer's root view at the same position.  Also save the
   // bounds of the root view's widget.
 
   nsView* rootViewSibling = nullptr;
   nsView* rootViewParent = nullptr;
   nsIntRect newBounds(0, 0, 0, 0);
 
-  if (PresShell* oldPresShell = GetPresShell()) {
+  PresShell* oldPresShell = GetPresShell();
+  if (oldPresShell) {
     nsViewManager* vm = oldPresShell->GetViewManager();
     if (vm) {
       nsView* oldRootView = vm->GetRootView();
 
       if (oldRootView) {
         rootViewSibling = oldRootView->GetNextSibling();
         rootViewParent = oldRootView->GetParent();
 
@@ -7999,16 +8000,25 @@ nsresult nsDocShell::RestoreFromHistory(
     }
   }
 
   // The FinishRestore call below can kill these, null them out so we don't
   // have invalid pointer lying around.
   newRootView = rootViewSibling = rootViewParent = nullptr;
   newVM = nullptr;
 
+  // If the IsUnderHiddenEmbedderElement() state has been changed, we need to
+  // update it.
+  if (oldPresShell && presShell &&
+      presShell->IsUnderHiddenEmbedderElement() !=
+          oldPresShell->IsUnderHiddenEmbedderElement()) {
+    presShell->SetIsUnderHiddenEmbedderElement(
+        oldPresShell->IsUnderHiddenEmbedderElement());
+  }
+
   // Simulate the completion of the load.
   nsDocShell::FinishRestore();
 
   // Restart plugins, and paint the content.
   if (presShell) {
     presShell->Thaw();
   }
 
--- a/dom/base/nsIScriptChannel.idl
+++ b/dom/base/nsIScriptChannel.idl
@@ -10,23 +10,23 @@
  * program provided via its URI to compute the data it should return.
  *
  * If a channel implements this interface, the execution of the program in
  * question will be restricted in the following ways:
  *
  * - If the channel does not have an owner principal, the program will not be
  *   executed at all, no matter what.  This is necessary because in this
  *   circumstance we have no way to tell whether script execution is allowed at
- *   all for the originating security context of this channel. 
+ *   all for the originating security context of this channel.
  * - If the channel has an owner principal, how it is executed is controlled by
  *   this interface.  However if the owner principal does not subsume the
  *   principal of the environment in which the program is to be executed the
  *   execution will be forced to happen in a sandbox.
  */
-[scriptable, uuid(33234b99-9588-4c7d-9da6-86b8b7cba565)]
+[uuid(33234b99-9588-4c7d-9da6-86b8b7cba565)]
 interface nsIScriptChannel : nsISupports
 {
   /**
    * Possible ways of executing the program.
    */
 
   /**
    * Don't execute at all.
--- a/dom/canvas/CanvasUtils.cpp
+++ b/dom/canvas/CanvasUtils.cpp
@@ -119,18 +119,18 @@ bool IsImageExtractionAllowed(Document* 
   // Load Permission Manager service.
   nsCOMPtr<nsIPermissionManager> permissionManager =
       do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, false);
 
   // Check if the site has permission to extract canvas data.
   // Either permit or block extraction if a stored permission setting exists.
   uint32_t permission;
-  rv = permissionManager->TestPermission(
-      topLevelDocURI, PERMISSION_CANVAS_EXTRACT_DATA, &permission);
+  rv = permissionManager->TestPermissionFromPrincipal(
+      principal, PERMISSION_CANVAS_EXTRACT_DATA, &permission);
   NS_ENSURE_SUCCESS(rv, false);
   switch (permission) {
     case nsIPermissionManager::ALLOW_ACTION:
       return true;
     case nsIPermissionManager::DENY_ACTION:
       return false;
     default:
       break;
@@ -162,30 +162,34 @@ bool IsImageExtractionAllowed(Document* 
         docURISpec.get());
     nsContentUtils::ReportToConsoleNonLocalized(
         message, nsIScriptError::warningFlag, NS_LITERAL_CSTRING("Security"),
         aDocument);
   }
 
   // Prompt the user (asynchronous).
   nsPIDOMWindowOuter* win = aDocument->GetWindow();
+  nsAutoCString origin;
+  rv = principal->GetOrigin(origin);
+  NS_ENSURE_SUCCESS(rv, false);
+
   if (XRE_IsContentProcess()) {
     BrowserChild* browserChild = BrowserChild::GetFrom(win);
     if (browserChild) {
-      browserChild->SendShowCanvasPermissionPrompt(topLevelDocURISpec,
+      browserChild->SendShowCanvasPermissionPrompt(origin,
                                                    isAutoBlockCanvas);
     }
   } else {
     nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
     if (obs) {
       obs->NotifyObservers(win,
                            isAutoBlockCanvas
                                ? TOPIC_CANVAS_PERMISSIONS_PROMPT_HIDE_DOORHANGER
                                : TOPIC_CANVAS_PERMISSIONS_PROMPT,
-                           NS_ConvertUTF8toUTF16(topLevelDocURISpec).get());
+                           NS_ConvertUTF8toUTF16(origin).get());
     }
   }
 
   // We don't extract the image for now -- user may override at prompt.
   return false;
 }
 
 bool GetCanvasContextType(const nsAString& str,
--- a/dom/chrome-webidl/JSWindowActor.webidl
+++ b/dom/chrome-webidl/JSWindowActor.webidl
@@ -46,8 +46,18 @@ JSWindowActorChild implements JSWindowAc
 // WebIDL callback interface version of the nsIObserver interface for use when
 // calling the observe method on JSWindowActors.
 //
 // NOTE: This isn't marked as ChromeOnly, as it has no interface object, and
 // thus cannot be conditionally exposed.
 callback interface MozObserverCallback {
   void observe(nsISupports subject, ByteString topic, DOMString? data);
 };
+
+// WebIDL callback interface calling the `willDestroy` and `didDestroy`
+// method on JSWindowActors.
+[MOZ_CAN_RUN_SCRIPT_BOUNDARY]
+callback MozActorDestroyCallback = void();
+
+dictionary MozActorDestroyCallbacks {
+  [ChromeOnly] MozActorDestroyCallback willDestroy;
+  [ChromeOnly] MozActorDestroyCallback didDestroy;
+};
--- a/dom/events/IMEStateManager.cpp
+++ b/dom/events/IMEStateManager.cpp
@@ -459,17 +459,19 @@ nsresult IMEStateManager::OnChangeFocusI
     aContent = nullptr;
   }
 
   nsCOMPtr<nsIWidget> oldWidget = sWidget;
   nsCOMPtr<nsIWidget> newWidget =
       aPresContext ? aPresContext->GetRootWidget() : nullptr;
   bool focusActuallyChanging =
       (sContent != aContent || sPresContext != aPresContext ||
-       oldWidget != newWidget || remoteHasFocus);
+       oldWidget != newWidget ||
+       (remoteHasFocus &&
+        (aAction.mFocusChange != InputContextAction::MENU_GOT_PSEUDO_FOCUS)));
 
   // If old widget has composition, we may need to commit composition since
   // a native IME context is shared on all editors on some widgets or all
   // widgets (it depends on platforms).
   if (oldWidget && focusActuallyChanging && sTextCompositions) {
     RefPtr<TextComposition> composition =
         sTextCompositions->GetCompositionFor(oldWidget);
     if (composition) {
@@ -669,24 +671,16 @@ void IMEStateManager::OnInstalledMenuKey
        GetBoolName(sActiveChildInputContext.mInPrivateBrowsing)));
 
   sInstalledMenuKeyboardListener = aInstalling;
 
   InputContextAction action(InputContextAction::CAUSE_UNKNOWN,
                             aInstalling
                                 ? InputContextAction::MENU_GOT_PSEUDO_FOCUS
                                 : InputContextAction::MENU_LOST_PSEUDO_FOCUS);
-  BrowserParent* focused = BrowserParent::GetFocused();
-  if (focused) {
-    if (aInstalling) {
-      OnFocusMovedBetweenBrowsers(focused, nullptr);
-    } else {
-      OnFocusMovedBetweenBrowsers(nullptr, focused);
-    }
-  }
   OnChangeFocusInternal(sPresContext, sContent, action);
 }
 
 // static
 bool IMEStateManager::OnMouseButtonEventInEditor(
     nsPresContext* aPresContext, nsIContent* aContent,
     WidgetMouseEvent* aMouseEvent) {
   MOZ_LOG(sISMLog, LogLevel::Info,
--- a/dom/file/ipc/FileCreatorParent.cpp
+++ b/dom/file/ipc/FileCreatorParent.cpp
@@ -75,23 +75,23 @@ nsresult FileCreatorParent::CreateBlobIm
   MOZ_ASSERT(!NS_IsMainThread());
 
   nsCOMPtr<nsIFile> file;
   nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(file));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
+  bool exists;
+  rv = file->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
   if (aExistenceCheck) {
-    bool exists;
-    nsresult rv = file->Exists(&exists);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
-    }
-
     if (!exists) {
       return NS_ERROR_FILE_NOT_FOUND;
     }
 
     bool isDir;
     rv = file->IsDirectory(&isDir);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
@@ -101,17 +101,19 @@ nsresult FileCreatorParent::CreateBlobIm
       return NS_ERROR_FILE_IS_DIRECTORY;
     }
   }
 
   RefPtr<FileBlobImpl> impl = new FileBlobImpl(file);
 
   // If the file doesn't exist, we cannot have its path, its size and so on.
   // Let's set them now.
-  if (!aExistenceCheck) {
+  if (!exists) {
+    MOZ_ASSERT(!aExistenceCheck);
+
     impl->SetMozFullPath(aPath);
     impl->SetLastModified(0);
     impl->SetEmptySize();
   }
 
   if (!aName.IsEmpty()) {
     impl->SetName(aName);
   }
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_createFile.js
@@ -0,0 +1,40 @@
+add_task(async function() {
+  const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+  do_get_profile();
+
+  let existingFile = Services.dirsvc.QueryInterface(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
+  existingFile.append("exists.js");
+  existingFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+  var outStream = Cc["@mozilla.org/network/file-output-stream;1"]
+                      .createInstance(Ci.nsIFileOutputStream);
+  outStream.init(existingFile, 0x02 | 0x08 | 0x20, // write, create, truncate
+                 0666, 0);
+
+  var fileData = "Hello World!";
+  outStream.write(fileData, fileData.length);
+  outStream.close();
+
+  ok(existingFile.exists(), "exists.js exists");
+
+  let unknownFile = Services.dirsvc.QueryInterface(Ci.nsIProperties).get("TmpD", Ci.nsIFile);
+  unknownFile.append("wow.txt");
+
+  ok(!unknownFile.exists(), unknownFile.path + " doesn't exist");
+
+  let a = await File.createFromNsIFile(existingFile, { existenceCheck: false });
+  ok(a.size != 0, "The size is correctly set");
+
+  let b = await File.createFromNsIFile(unknownFile, { existenceCheck: false });
+  ok(b.size == 0, "The size is 0 for unknown file");
+
+  let c = await File.createFromNsIFile(existingFile, { existenceCheck: true });
+  ok(c.size != 0, "The size is correctly set");
+
+  let d = await File.createFromNsIFile(unknownFile, { existenceCheck: true }).then(_ => true, _ => false);
+  ok(d === false, "Exception thrown");
+
+  existingFile.remove(true);
+  ok(!existingFile.exists(), "exists.js doesn't exist anymore");
+});
--- a/dom/file/tests/xpcshell.ini
+++ b/dom/file/tests/xpcshell.ini
@@ -1,3 +1,4 @@
 [DEFAULT]
 
 [test_bloburi.js]
+[test_createFile.js]
--- a/dom/ipc/BrowserParent.cpp
+++ b/dom/ipc/BrowserParent.cpp
@@ -3753,33 +3753,33 @@ mozilla::ipc::IPCResult BrowserParent::R
   }
 
   widget->LookUpDictionary(aText, aFontRangeArray, aIsVertical,
                            TransformChildToParent(aPoint));
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult BrowserParent::RecvShowCanvasPermissionPrompt(
-    const nsCString& aFirstPartyURI, const bool& aHideDoorHanger) {
+    const nsCString& aOrigin, const bool& aHideDoorHanger) {
   nsCOMPtr<nsIBrowser> browser =
       mFrameElement ? mFrameElement->AsBrowser() : nullptr;
   if (!browser) {
     // If the tab is being closed, the browser may not be available.
     // In this case we can ignore the request.
     return IPC_OK();
   }
   nsCOMPtr<nsIObserverService> os = services::GetObserverService();
   if (!os) {
     return IPC_FAIL_NO_REASON(this);
   }
   nsresult rv = os->NotifyObservers(
       browser,
       aHideDoorHanger ? "canvas-permissions-prompt-hide-doorhanger"
                       : "canvas-permissions-prompt",
-      NS_ConvertUTF8toUTF16(aFirstPartyURI).get());
+      NS_ConvertUTF8toUTF16(aOrigin).get());
   if (NS_FAILED(rv)) {
     return IPC_FAIL_NO_REASON(this);
   }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult BrowserParent::RecvVisitURI(
     const URIParams& aURI, const Maybe<URIParams>& aLastVisitedURI,
--- a/dom/ipc/BrowserParent.h
+++ b/dom/ipc/BrowserParent.h
@@ -703,17 +703,17 @@ class BrowserParent final : public PBrow
 
   mozilla::ipc::IPCResult RecvSetDimensions(const uint32_t& aFlags,
                                             const int32_t& aX,
                                             const int32_t& aY,
                                             const int32_t& aCx,
                                             const int32_t& aCy);
 
   mozilla::ipc::IPCResult RecvShowCanvasPermissionPrompt(
-      const nsCString& aFirstPartyURI, const bool& aHideDoorHanger);
+      const nsCString& aOrigin, const bool& aHideDoorHanger);
 
   mozilla::ipc::IPCResult RecvSetSystemFont(const nsCString& aFontName);
   mozilla::ipc::IPCResult RecvGetSystemFont(nsCString* aFontName);
 
   mozilla::ipc::IPCResult RecvVisitURI(const URIParams& aURI,
                                        const Maybe<URIParams>& aLastVisitedURI,
                                        const uint32_t& aFlags);
 
--- a/dom/ipc/JSWindowActor.cpp
+++ b/dom/ipc/JSWindowActor.cpp
@@ -34,16 +34,47 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(JSWindowActor)
 
 JSWindowActor::JSWindowActor() : mNextQueryId(0) {}
 
 nsIGlobalObject* JSWindowActor::GetParentObject() const {
   return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
 }
 
+void JSWindowActor::StartDestroy() {
+  DestroyCallback(DestroyCallbackFunction::WillDestroy);
+}
+
+void JSWindowActor::AfterDestroy() {
+  DestroyCallback(DestroyCallbackFunction::DidDestroy);
+}
+
+void JSWindowActor::DestroyCallback(DestroyCallbackFunction callback) {
+  AutoEntryScript aes(xpc::PrivilegedJunkScope(),
+                      "JSWindowActor destroy callback");
+  JSContext* cx = aes.cx();
+  MozActorDestroyCallbacks callbacksHolder;
+  NS_ENSURE_TRUE_VOID(GetWrapper());
+  JS::Rooted<JS::Value> val(cx, JS::ObjectValue(*GetWrapper()));
+  if (NS_WARN_IF(!callbacksHolder.Init(cx, val))) {
+    return;
+  }
+
+  // Destroy callback is optional.
+  if (callback == DestroyCallbackFunction::WillDestroy) {
+    if (callbacksHolder.mWillDestroy.WasPassed()) {
+      callbacksHolder.mWillDestroy.Value()->Call();
+    }
+  } else {
+    if (callbacksHolder.mDidDestroy.WasPassed()) {
+      callbacksHolder.mDidDestroy.Value()->Call();
+    }
+  }
+}
+
 void JSWindowActor::RejectPendingQueries() {
   // Take our queries out, in case somehow rejecting promises can trigger
   // additions or removals.
   nsRefPtrHashtable<nsUint64HashKey, Promise> pendingQueries;
   mPendingQueries.SwapElements(pendingQueries);
   for (auto iter = pendingQueries.Iter(); !iter.Done(); iter.Next()) {
     iter.Data()->MaybeReject(NS_ERROR_NOT_AVAILABLE);
   }
--- a/dom/ipc/JSWindowActor.h
+++ b/dom/ipc/JSWindowActor.h
@@ -34,16 +34,17 @@ class QueryPromiseHandler;
 class JSWindowActor : public nsISupports, public nsWrapperCache {
  public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(JSWindowActor)
 
   JSWindowActor();
 
   enum class Type { Parent, Child };
+  enum class DestroyCallbackFunction { WillDestroy, DidDestroy };
 
   const nsString& Name() const { return mName; }
 
   void SendAsyncMessage(JSContext* aCx, const nsAString& aMessageName,
                         JS::Handle<JS::Value> aObj,
                         JS::Handle<JS::Value> aTransfers, ErrorResult& aRv);
 
   already_AddRefed<Promise> SendQuery(JSContext* aCx,
@@ -66,16 +67,22 @@ class JSWindowActor : public nsISupports
   virtual void SendRawMessage(const JSWindowActorMessageMeta& aMetadata,
                               ipc::StructuredCloneData&& aData,
                               ErrorResult& aRv) = 0;
 
   virtual ~JSWindowActor() = default;
 
   void SetName(const nsAString& aName);
 
+  void StartDestroy();
+
+  void AfterDestroy();
+
+  void DestroyCallback(DestroyCallbackFunction willDestroy);
+
  private:
   void ReceiveMessageOrQuery(JSContext* aCx,
                              const JSWindowActorMessageMeta& aMetadata,
                              JS::Handle<JS::Value> aData, ErrorResult& aRv);
 
   void ReceiveQueryReply(JSContext* aCx,
                          const JSWindowActorMessageMeta& aMetadata,
                          JS::Handle<JS::Value> aData, ErrorResult& aRv);
--- a/dom/ipc/JSWindowActorChild.cpp
+++ b/dom/ipc/JSWindowActorChild.cpp
@@ -10,16 +10,18 @@
 #include "mozilla/dom/WindowGlobalParent.h"
 #include "mozilla/dom/MessageManagerBinding.h"
 #include "mozilla/dom/BrowsingContext.h"
 #include "nsGlobalWindowInner.h"
 
 namespace mozilla {
 namespace dom {
 
+JSWindowActorChild::~JSWindowActorChild() { MOZ_ASSERT(!mManager); }
+
 JSObject* JSWindowActorChild::WrapObject(JSContext* aCx,
                                          JS::Handle<JSObject*> aGivenProto) {
   return JSWindowActorChild_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 WindowGlobalChild* JSWindowActorChild::Manager() const { return mManager; }
 
 void JSWindowActorChild::Init(const nsAString& aName,
@@ -53,17 +55,17 @@ class AsyncMessageToParent : public Runn
   RefPtr<WindowGlobalParent> mParent;
 };
 
 }  // anonymous namespace
 
 void JSWindowActorChild::SendRawMessage(const JSWindowActorMessageMeta& aMeta,
                                         ipc::StructuredCloneData&& aData,
                                         ErrorResult& aRv) {
-  if (NS_WARN_IF(!mManager || mManager->IsClosed())) {
+  if (NS_WARN_IF(!mCanSend || !mManager || mManager->IsClosed())) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   if (mManager->IsInProcess()) {
     RefPtr<WindowGlobalParent> wgp = mManager->GetParentActor();
     nsCOMPtr<nsIRunnable> runnable =
         new AsyncMessageToParent(aMeta, std::move(aData), wgp);
@@ -107,16 +109,26 @@ BrowsingContext* JSWindowActorChild::Get
 Nullable<WindowProxyHolder> JSWindowActorChild::GetContentWindow(
     ErrorResult& aRv) {
   if (BrowsingContext* bc = GetBrowsingContext(aRv)) {
     return WindowProxyHolder(bc);
   }
   return nullptr;
 }
 
+void JSWindowActorChild::StartDestroy() {
+  JSWindowActor::StartDestroy();
+  mCanSend = false;
+}
+
+void JSWindowActorChild::AfterDestroy() {
+  JSWindowActor::AfterDestroy();
+  mManager = nullptr;
+}
+
 NS_IMPL_CYCLE_COLLECTION_INHERITED(JSWindowActorChild, JSWindowActor, mManager)
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(JSWindowActorChild,
                                                JSWindowActor)
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSWindowActorChild)
 NS_INTERFACE_MAP_END_INHERITING(JSWindowActor)
--- a/dom/ipc/JSWindowActorChild.h
+++ b/dom/ipc/JSWindowActorChild.h
@@ -42,28 +42,30 @@ class JSWindowActorChild final : public 
 
   static already_AddRefed<JSWindowActorChild> Constructor(GlobalObject& aGlobal,
                                                           ErrorResult& aRv) {
     return MakeAndAddRef<JSWindowActorChild>();
   }
 
   WindowGlobalChild* Manager() const;
   void Init(const nsAString& aName, WindowGlobalChild* aManager);
-
+  void StartDestroy();
+  void AfterDestroy();
   Document* GetDocument(ErrorResult& aRv);
   BrowsingContext* GetBrowsingContext(ErrorResult& aRv);
   Nullable<WindowProxyHolder> GetContentWindow(ErrorResult& aRv);
 
  protected:
   void SendRawMessage(const JSWindowActorMessageMeta& aMeta,
                       ipc::StructuredCloneData&& aData,
                       ErrorResult& aRv) override;
 
  private:
-  ~JSWindowActorChild() = default;
+  ~JSWindowActorChild();
 
+  bool mCanSend = true;
   RefPtr<WindowGlobalChild> mManager;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_JSWindowActorChild_h
--- a/dom/ipc/JSWindowActorParent.cpp
+++ b/dom/ipc/JSWindowActorParent.cpp
@@ -7,16 +7,18 @@
 #include "mozilla/dom/JSWindowActorBinding.h"
 #include "mozilla/dom/JSWindowActorParent.h"
 #include "mozilla/dom/WindowGlobalParent.h"
 #include "mozilla/dom/MessageManagerBinding.h"
 
 namespace mozilla {
 namespace dom {
 
+JSWindowActorParent::~JSWindowActorParent() { MOZ_ASSERT(!mManager); }
+
 JSObject* JSWindowActorParent::WrapObject(JSContext* aCx,
                                           JS::Handle<JSObject*> aGivenProto) {
   return JSWindowActorParent_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 WindowGlobalParent* JSWindowActorParent::Manager() const { return mManager; }
 
 void JSWindowActorParent::Init(const nsAString& aName,
@@ -50,17 +52,17 @@ class AsyncMessageToChild : public Runna
   RefPtr<WindowGlobalChild> mChild;
 };
 
 }  // anonymous namespace
 
 void JSWindowActorParent::SendRawMessage(const JSWindowActorMessageMeta& aMeta,
                                          ipc::StructuredCloneData&& aData,
                                          ErrorResult& aRv) {
-  if (NS_WARN_IF(!mManager || mManager->IsClosed())) {
+  if (NS_WARN_IF(!mCanSend || !mManager || mManager->IsClosed())) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   if (mManager->IsInProcess()) {
     RefPtr<WindowGlobalChild> wgc = mManager->GetChildActor();
     nsCOMPtr<nsIRunnable> runnable =
         new AsyncMessageToChild(aMeta, std::move(aData), wgc);
@@ -78,16 +80,26 @@ void JSWindowActorParent::SendRawMessage
   }
 
   if (NS_WARN_IF(!mManager->SendRawMessage(aMeta, msgData))) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
 }
 
+void JSWindowActorParent::StartDestroy() {
+  JSWindowActor::StartDestroy();
+  mCanSend = false;
+}
+
+void JSWindowActorParent::AfterDestroy() {
+  JSWindowActor::AfterDestroy();
+  mManager = nullptr;
+}
+
 NS_IMPL_CYCLE_COLLECTION_INHERITED(JSWindowActorParent, JSWindowActor, mManager)
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(JSWindowActorParent,
                                                JSWindowActor)
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSWindowActorParent)
 NS_INTERFACE_MAP_END_INHERITING(JSWindowActor)
--- a/dom/ipc/JSWindowActorParent.h
+++ b/dom/ipc/JSWindowActorParent.h
@@ -37,24 +37,27 @@ class JSWindowActorParent final : public
 
   static already_AddRefed<JSWindowActorParent> Constructor(
       GlobalObject& aGlobal, ErrorResult& aRv) {
     return MakeAndAddRef<JSWindowActorParent>();
   }
 
   WindowGlobalParent* Manager() const;
   void Init(const nsAString& aName, WindowGlobalParent* aManager);
+  void StartDestroy();
+  void AfterDestroy();
 
  protected:
   void SendRawMessage(const JSWindowActorMessageMeta& aMeta,
                       ipc::StructuredCloneData&& aData,
                       ErrorResult& aRv) override;
 
  private:
-  ~JSWindowActorParent() = default;
+  ~JSWindowActorParent();
 
+  bool mCanSend = true;
   RefPtr<WindowGlobalParent> mManager;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_JSWindowActorParent_h
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -615,19 +615,19 @@ parent:
     // arrive before the BrowserChild attempts to use its cross-process compositor
     // bridge.
     sync EnsureLayersConnected() returns (CompositorOptions compositorOptions);
 
     /**
      * This function is used to notify the parent that it should display a
      * canvas permission prompt.
      *
-     * @param aFirstPartyURI first party of the tab that is requesting access.
+     * @param aOrigin origin string of the document that is requesting access.
      */
-    async ShowCanvasPermissionPrompt(nsCString aFirstPartyURI,
+    async ShowCanvasPermissionPrompt(nsCString aOrigin,
                                      bool aHideDoorHanger);
 
     sync SetSystemFont(nsCString aFontName);
     sync GetSystemFont() returns (nsCString retval);
 
     sync SetPrefersReducedMotionOverrideForTest(bool aValue);
     sync ResetPrefersReducedMotionOverrideForTest();
 
--- a/dom/ipc/WindowGlobalChild.cpp
+++ b/dom/ipc/WindowGlobalChild.cpp
@@ -139,16 +139,26 @@ already_AddRefed<BrowserChild> WindowGlo
 }
 
 void WindowGlobalChild::Destroy() {
   // Perform async IPC shutdown unless we're not in-process, and our
   // BrowserChild is in the process of being destroyed, which will destroy us as
   // well.
   RefPtr<BrowserChild> browserChild = GetBrowserChild();
   if (!browserChild || !browserChild->IsDestroyed()) {
+    // Make a copy so that we can avoid potential iterator invalidation when
+    // calling the user-provided Destroy() methods.
+    nsTArray<RefPtr<JSWindowActorChild>> windowActors(mWindowActors.Count());
+    for (auto iter = mWindowActors.Iter(); !iter.Done(); iter.Next()) {
+      windowActors.AppendElement(iter.UserData());
+    }
+
+    for (auto& windowActor : windowActors) {
+      windowActor->StartDestroy();
+    }
     SendDestroy();
   }
 
   mIPCClosed = true;
 }
 
 static nsresult ChangeFrameRemoteness(WindowGlobalChild* aWgc,
                                       BrowsingContext* aBc,
@@ -291,22 +301,25 @@ void WindowGlobalChild::ActorDestroy(Act
   mIPCClosed = true;
   gWindowGlobalChildById->Remove(mInnerWindowId);
 
   // Destroy our JSWindowActors, and reject any pending queries.
   nsRefPtrHashtable<nsStringHashKey, JSWindowActorChild> windowActors;
   mWindowActors.SwapElements(windowActors);
   for (auto iter = windowActors.Iter(); !iter.Done(); iter.Next()) {
     iter.Data()->RejectPendingQueries();
+    iter.Data()->AfterDestroy();
   }
+  windowActors.Clear();
 }
 
 WindowGlobalChild::~WindowGlobalChild() {
   MOZ_ASSERT(!gWindowGlobalChildById ||
              !gWindowGlobalChildById->Contains(mInnerWindowId));
+  MOZ_ASSERT(!mWindowActors.Count());
 }
 
 JSObject* WindowGlobalChild::WrapObject(JSContext* aCx,
                                         JS::Handle<JSObject*> aGivenProto) {
   return WindowGlobalChild_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 nsISupports* WindowGlobalChild::GetParentObject() {
--- a/dom/ipc/WindowGlobalParent.cpp
+++ b/dom/ipc/WindowGlobalParent.cpp
@@ -165,16 +165,26 @@ IPCResult WindowGlobalParent::RecvBecome
   mBrowsingContext->SetCurrentWindowGlobal(this);
   return IPC_OK();
 }
 
 IPCResult WindowGlobalParent::RecvDestroy() {
   if (!mIPCClosed) {
     RefPtr<BrowserParent> browserParent = GetRemoteTab();
     if (!browserParent || !browserParent->IsDestroyed()) {
+      // Make a copy so that we can avoid potential iterator invalidation when
+      // calling the user-provided Destroy() methods.
+      nsTArray<RefPtr<JSWindowActorParent>> windowActors(mWindowActors.Count());
+      for (auto iter = mWindowActors.Iter(); !iter.Done(); iter.Next()) {
+        windowActors.AppendElement(iter.UserData());
+      }
+
+      for (auto& windowActor : windowActors) {
+        windowActor->StartDestroy();
+      }
       Unused << Send__delete__(this);
     }
   }
   return IPC_OK();
 }
 
 IPCResult WindowGlobalParent::RecvRawMessage(
     const JSWindowActorMessageMeta& aMeta, const ClonedMessageData& aData) {
@@ -294,27 +304,30 @@ void WindowGlobalParent::ActorDestroy(Ac
   gWindowGlobalParentsById->Remove(mInnerWindowId);
   mBrowsingContext->UnregisterWindowGlobal(this);
 
   // Destroy our JSWindowActors, and reject any pending queries.
   nsRefPtrHashtable<nsStringHashKey, JSWindowActorParent> windowActors;
   mWindowActors.SwapElements(windowActors);
   for (auto iter = windowActors.Iter(); !iter.Done(); iter.Next()) {
     iter.Data()->RejectPendingQueries();
+    iter.Data()->AfterDestroy();
   }
+  windowActors.Clear();
 
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   if (obs) {
     obs->NotifyObservers(this, "window-global-destroyed", nullptr);
   }
 }
 
 WindowGlobalParent::~WindowGlobalParent() {
   MOZ_ASSERT(!gWindowGlobalParentsById ||
              !gWindowGlobalParentsById->Contains(mInnerWindowId));
+  MOZ_ASSERT(!mWindowActors.Count());
 }
 
 JSObject* WindowGlobalParent::WrapObject(JSContext* aCx,
                                          JS::Handle<JSObject*> aGivenProto) {
   return WindowGlobalParent_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 nsISupports* WindowGlobalParent::GetParentObject() {
--- a/dom/ipc/tests/browser_JSWindowActor.js
+++ b/dom/ipc/tests/browser_JSWindowActor.js
@@ -401,8 +401,79 @@ declTest("getActor with includeChrome", 
   includeChrome: true,
 
   async test(_browser, win) {
     let parent = win.docShell.browsingContext.currentWindowGlobal;
     let actorParent = parent.getActor("Test");
     ok(actorParent, "JSWindowActorParent should have value.");
   },
 });
+
+declTest("destroy actor by iframe remove", {
+  allFrames: true,
+
+  async test(browser) {
+    await ContentTask.spawn(browser, {}, async function() {
+      // Create and append an iframe into the window's document.
+      let frame = content.document.createElement("iframe");
+      frame.id = "frame";
+      content.document.body.appendChild(frame);
+      await ContentTaskUtils.waitForEvent(frame, "load");
+      is(content.window.frames.length, 1, "There should be an iframe.");
+      let child = frame.contentWindow.window.getWindowGlobalChild();
+      let actorChild = child.getActor("Test");
+      ok(actorChild, "JSWindowActorChild should have value.");
+
+      let willDestroyPromise = new Promise(resolve => {
+        const TOPIC = "test-js-window-actor-willdestroy";
+        Services.obs.addObserver(function obs(subject, topic, data) {
+          ok(data, "willDestroyCallback data should be true.");
+
+          Services.obs.removeObserver(obs, TOPIC);
+          resolve();
+        }, TOPIC);
+      });
+
+      let didDestroyPromise = new Promise(resolve => {
+        const TOPIC = "test-js-window-actor-diddestroy";
+        Services.obs.addObserver(function obs(subject, topic, data) {
+          ok(data, "didDestroyCallback data should be true.");
+
+          Services.obs.removeObserver(obs, TOPIC);
+          resolve();
+        }, TOPIC);
+      });
+
+      info("Remove frame");
+      content.document.getElementById("frame").remove();
+      await Promise.all([willDestroyPromise, didDestroyPromise]);
+
+      Assert.throws(() => child.getActor("Test"),
+        /InvalidStateError/, "Should throw if frame destroy.");
+    });
+  },
+});
+
+declTest("destroy actor by page navigates", {
+  allFrames: true,
+
+  async test(browser) {
+    info("creating an in-process frame");
+    await ContentTask.spawn(browser, URL, async function(url) {
+      let frame = content.document.createElement("iframe");
+      frame.src = url;
+      content.document.body.appendChild(frame);
+    });
+
+    info("navigating page");
+    await ContentTask.spawn(browser, TEST_URL, async function(url) {
+      let frame = content.document.querySelector("iframe");
+      frame.contentWindow.location = url;
+      let child = frame.contentWindow.window.getWindowGlobalChild();
+      let actorChild = child.getActor("Test");
+      ok(actorChild, "JSWindowActorChild should have value.");
+      await ContentTaskUtils.waitForEvent(frame, "load");
+
+      Assert.throws(() => child.getActor("Test"),
+              /InvalidStateError/, "Should throw if frame destroy.");
+    });
+  },
+});
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -4102,16 +4102,20 @@ SourceListener::InitializeAsync() {
                }
 
                // SetTrack() queued the tracks. We add them synchronously here
                // to avoid races.
                stream->FinishAddTracks();
 
                if (audioDevice) {
                  nsresult rv = audioDevice->Start();
+                 if (rv == NS_ERROR_NOT_AVAILABLE) {
+                   PR_Sleep(200);
+                   rv = audioDevice->Start();
+                 }
                  if (NS_FAILED(rv)) {
                    nsString log;
                    if (rv == NS_ERROR_NOT_AVAILABLE) {
                      log.AssignLiteral("Concurrent mic process limit.");
                      aHolder.Reject(
                          MakeRefPtr<MediaMgrError>(
                              MediaMgrError::Name::NotReadableError, log),
                          __func__);
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -778,17 +778,17 @@ skip-if = toolkit == 'android' # bug 130
 skip-if = android_version == '17' # bug 1306917, 1323778, android(bug 1232305)
 tags=capturestream
 [test_bug1512958.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
 tags=msg capturestream
 [test_can_play_type.html]
 skip-if = (android_version == '23' && debug) || (android_version == '25' && debug) # android(bug 1232305)
 [test_can_play_type_mpeg.html]
-skip-if = (android_version == '23' && debug) || (android_version == '25' && debug) || (os == 'win' && processor == 'aarch64') # bug 1526080 # android(bug 1232305)
+skip-if = (os == "android") || (os == 'win' && processor == 'aarch64') # bug 1526080 # android(bug 1232305) Bug 1548858
 [test_can_play_type_no_ogg.html]
 skip-if = (android_version == '23' && debug) || (android_version == '25' && debug) # android(bug 1232305)
 [test_can_play_type_ogg.html]
 skip-if = (android_version == '23' && debug) || (android_version == '25' && debug) # android(bug 1232305)
 [test_chaining.html]
 skip-if = android_version == '17' # android(bug 1232305)
 [test_clone_media_element.html]
 skip-if = toolkit == 'android' # bug 1108558, android(bug 1232305)
--- a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
@@ -550,17 +550,17 @@ nsresult MediaEngineWebRTCMicrophoneSour
 
   MOZ_ASSERT(mState == kAllocated || mState == kStopped);
 
   CubebUtils::AudioDeviceID deviceID = mDeviceInfo->DeviceID();
   if (mStream->GraphImpl()->InputDeviceID() &&
       mStream->GraphImpl()->InputDeviceID() != deviceID) {
     // For now, we only allow opening a single audio input device per document,
     // because we can only have one MSG per document.
-    return NS_ERROR_FAILURE;
+    return NS_ERROR_NOT_AVAILABLE;
   }
 
   RefPtr<MediaEngineWebRTCMicrophoneSource> that = this;
   NS_DispatchToMainThread(NS_NewRunnableFunction(
       __func__, [that, deviceID, stream = mStream, track = mTrackID] {
         if (stream->IsDestroyed()) {
           return;
         }
--- a/dom/tests/mochitest/fetch/mochitest.ini
+++ b/dom/tests/mochitest/fetch/mochitest.ini
@@ -53,33 +53,33 @@ prefs =
 skip-if = toolkit == 'android' && !is_fennec # Bug 1427396
 [test_headers_mainthread.html]
 [test_fetch_basic.html]
 [test_fetch_basic_sw_reroute.html]
 skip-if = toolkit == 'android' && !is_fennec # Bug 1525959, 1427396
 [test_fetch_basic_sw_empty_reroute.html]
 [test_fetch_basic_http.html]
 [test_fetch_basic_http_sw_reroute.html]
-skip-if = toolkit == 'android' && !is_fennec # Bug 1532023, 1427396
+skip-if = toolkit == 'android' && !is_fennec || (os == "linux" && !debug) # Bug 1532023, 1427396
 [test_fetch_basic_http_sw_empty_reroute.html]
 skip-if = toolkit == 'android' && !is_fennec # Bug 1532097
 [test_fetch_cached_redirect.html]
 [test_fetch_cors.html]
 skip-if = toolkit == 'android' # Bug 1210282
 [test_fetch_cors_sw_reroute.html]
 skip-if = os == "linux" || (os == "win" && os_version == "10.0") || toolkit == 'android' # Bug 1210282
 [test_fetch_cors_sw_empty_reroute.html]
 skip-if = toolkit == 'android' # Bug 1210282
 [test_fetch_csp_block.html]
 [test_fetch_observer.html]
 [test_fetch_user_control_rp.html]
 [test_formdataparsing.html]
 skip-if = asan # Bug 1325942
 [test_formdataparsing_sw_reroute.html]
-skip-if = asan || toolkit == 'android' && !is_fennec # Bug 1325942, 1427396
+skip-if = asan || toolkit == 'android' && !is_fennec || (os == "linux" && !debug) # Bug 1325942, 1427396
 [test_request.html]
 [test_request_context.html]
 [test_request_sw_reroute.html]
 skip-if = toolkit == 'android' && !is_fennec # Bug 1427396
 [test_response.html]
 [test_response_sw_reroute.html]
 skip-if = toolkit == 'android' && !is_fennec # Bug 1427396
 [test_temporaryFileBlob.html]
--- a/editor/reftests/xul/emptytextbox-4.xul
+++ b/editor/reftests/xul/emptytextbox-4.xul
@@ -2,11 +2,11 @@
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml"
         title="Textbox tests">
 
   <script type="text/javascript" src="platform.js"/>
 
-  <textbox type="search"/>
-      
+  <textbox is="search-textbox"/>
+
 </window>
--- a/extensions/permissions/nsPermission.cpp
+++ b/extensions/permissions/nsPermission.cpp
@@ -26,18 +26,17 @@ nsPermission::nsPermission(nsIPrincipal*
       mExpireTime(aExpireTime),
       mModificationTime(aModificationTime) {}
 
 already_AddRefed<nsIPrincipal> nsPermission::ClonePrincipalForPermission(
     nsIPrincipal* aPrincipal) {
   MOZ_ASSERT(aPrincipal);
 
   mozilla::OriginAttributes attrs = aPrincipal->OriginAttributesRef();
-  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID |
-                        mozilla::OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);
+  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
 
   nsAutoCString originNoSuffix;
   nsresult rv = aPrincipal->GetOriginNoSuffix(originNoSuffix);
   NS_ENSURE_SUCCESS(rv, nullptr);
 
   nsCOMPtr<nsIURI> uri;
   rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix);
   NS_ENSURE_SUCCESS(rv, nullptr);
--- a/extensions/permissions/nsPermission.h
+++ b/extensions/permissions/nsPermission.h
@@ -18,18 +18,17 @@ class nsPermission : public nsIPermissio
   NS_DECL_ISUPPORTS
   NS_DECL_NSIPERMISSION
 
   static already_AddRefed<nsPermission> Create(
       nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t aCapability,
       uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime);
 
   // This method creates a new nsIPrincipal with a stripped OriginAttributes (no
-  // userContextId, and no FirstPartyDomain) and a codebase equal to the origin
-  // of 'aPrincipal'.
+  // userContextId) and a codebase equal to the origin of 'aPrincipal'.
   static already_AddRefed<nsIPrincipal> ClonePrincipalForPermission(
       nsIPrincipal* aPrincipal);
 
  protected:
   nsPermission(nsIPrincipal* aPrincipal, const nsACString& aType,
                uint32_t aCapability, uint32_t aExpireType, int64_t aExpireTime,
                int64_t aModificationTime);
 
--- a/extensions/permissions/nsPermissionManager.cpp
+++ b/extensions/permissions/nsPermissionManager.cpp
@@ -158,19 +158,18 @@ nsresult GetOriginFromPrincipal(nsIPrinc
     return NS_ERROR_FAILURE;
   }
 
   // mPrivateBrowsingId must be set to false because PermissionManager is not
   // supposed to have any knowledge of private browsing. Allowing it to be true
   // changes the suffix being hashed.
   attrs.mPrivateBrowsingId = 0;
 
-  // Disable userContext and firstParty isolation for permissions.
-  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID |
-                        mozilla::OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);
+  // Disable userContext for permissions.
+  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
 
   attrs.CreateSuffix(suffix);
   aOrigin.Append(suffix);
   return NS_OK;
 }
 
 nsresult GetPrincipalFromOrigin(const nsACString& aOrigin,
                                 nsIPrincipal** aPrincipal) {
@@ -180,19 +179,18 @@ nsresult GetPrincipalFromOrigin(const ns
     return NS_ERROR_FAILURE;
   }
 
   // mPrivateBrowsingId must be set to false because PermissionManager is not
   // supposed to have any knowledge of private browsing. Allowing it to be true
   // changes the suffix being hashed.
   attrs.mPrivateBrowsingId = 0;
 
-  // Disable userContext and firstParty isolation for permissions.
-  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID |
-                        mozilla::OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);
+  // Disable userContext for permissions.
+  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
 
   nsCOMPtr<nsIURI> uri;
   nsresult rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIPrincipal> principal =
       mozilla::BasePrincipal::CreateCodebasePrincipal(uri, attrs);
   principal.forget(aPrincipal);
@@ -273,19 +271,18 @@ already_AddRefed<nsIPrincipal> GetNextSu
   nsCOMPtr<nsIURI> newURI = GetNextSubDomainURI(uri);
   if (!newURI) {
     return nullptr;
   }
 
   // Copy the attributes over
   mozilla::OriginAttributes attrs = aPrincipal->OriginAttributesRef();
 
-  // Disable userContext and firstParty isolation for permissions.
-  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID |
-                        mozilla::OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);
+  // Disable userContext for permissions.
+  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
 
   nsCOMPtr<nsIPrincipal> principal =
       mozilla::BasePrincipal::CreateCodebasePrincipal(newURI, attrs);
 
   return principal.forget();
 }
 
 class MOZ_STACK_CLASS UpgradeHostToOriginHelper {
@@ -3326,19 +3323,18 @@ void nsPermissionManager::GetKeyForOrigi
     return;
   }
 
   // mPrivateBrowsingId must be set to false because PermissionManager is not
   // supposed to have any knowledge of private browsing. Allowing it to be true
   // changes the suffix being hashed.
   attrs.mPrivateBrowsingId = 0;
 
-  // Disable userContext and firstParty isolation for permissions.
-  attrs.StripAttributes(OriginAttributes::STRIP_USER_CONTEXT_ID |
-                        OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);
+  // Disable userContext for permissions.
+  attrs.StripAttributes(OriginAttributes::STRIP_USER_CONTEXT_ID);
 
 #ifdef DEBUG
   // Parse the origin string into a principal, and extract some useful
   // information from it for assertions.
   nsCOMPtr<nsIPrincipal> dbgPrincipal;
   MOZ_ALWAYS_SUCCEEDS(
       GetPrincipalFromOrigin(aOrigin, getter_AddRefs(dbgPrincipal)));
   nsCOMPtr<nsIURI> dbgUri;
--- a/extensions/permissions/test/unit/test_permmanager_defaults.js
+++ b/extensions/permissions/test/unit/test_permmanager_defaults.js
@@ -92,85 +92,85 @@ add_task(async function do_test() {
   pm.removeAll();
 
   Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
                pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
   Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
                pm.testPermissionFromPrincipal(principal3, TEST_PERMISSION));
   Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
                pm.testPermissionFromPrincipal(principal4, TEST_PERMISSION));
-  // make sure principals with userContextId or firstPartyDomain use the same permissions
+  // make sure principals with userContextId use the same permissions
   Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
                pm.testPermissionFromPrincipal(principal6, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
+  // make sure principals with a firstPartyDomain use different permissions
+  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
+  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION));
 
   // Asking for this permission to be removed should result in that permission
   // having UNKNOWN_ACTION
   pm.removeFromPrincipal(principal, TEST_PERMISSION);
   Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
   // make sure principals with userContextId or firstPartyDomain use the same permissions
   Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal6, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
-               pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
-               pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION));
   // and we should have this UNKNOWN_ACTION reflected in the DB
   await checkCapabilityViaDB(Ci.nsIPermissionManager.UNKNOWN_ACTION);
   // but the permission should *not* appear in the enumerator.
   Assert.equal(null, findCapabilityViaEnum());
 
   // and a subsequent RemoveAll should restore the default
   pm.removeAll();
 
   Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
                pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
-  // make sure principals with userContextId or firstPartyDomain use the same permissions
+  // make sure principals with userContextId use the same permissions
   Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
                pm.testPermissionFromPrincipal(principal6, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
+  // make sure principals with firstPartyDomain use different permissions
+  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION,
+  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION));
   // and allow it to again be seen in the enumerator.
   Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION, findCapabilityViaEnum());
 
   // now explicitly add a permission - this too should override the default.
   pm.addFromPrincipal(principal, TEST_PERMISSION, Ci.nsIPermissionManager.DENY_ACTION);
 
   // it should be reflected in a permission check, in the enumerator and the DB
   Assert.equal(Ci.nsIPermissionManager.DENY_ACTION,
                pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
-  // make sure principals with userContextId or firstPartyDomain use the same permissions
+  // make sure principals with userContextId use the same permissions
   Assert.equal(Ci.nsIPermissionManager.DENY_ACTION,
                pm.testPermissionFromPrincipal(principal6, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.DENY_ACTION,
+  // make sure principals with firstPartyDomain use different permissions
+  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.DENY_ACTION,
+  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION));
   Assert.equal(Ci.nsIPermissionManager.DENY_ACTION, findCapabilityViaEnum());
   await checkCapabilityViaDB(Ci.nsIPermissionManager.DENY_ACTION);
 
   // explicitly add a different permission - in this case we are no longer
   // replacing the default, but instead replacing the replacement!
   pm.addFromPrincipal(principal, TEST_PERMISSION, Ci.nsIPermissionManager.PROMPT_ACTION);
 
   // it should be reflected in a permission check, in the enumerator and the DB
   Assert.equal(Ci.nsIPermissionManager.PROMPT_ACTION,
                pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
-  // make sure principals with userContextId or firstPartyDomain use the same permissions
+  // make sure principals with userContextId use the same permissions
   Assert.equal(Ci.nsIPermissionManager.PROMPT_ACTION,
                pm.testPermissionFromPrincipal(principal6, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.PROMPT_ACTION,
+  // make sure principals with firstPartyDomain use different permissions
+  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION));
-  Assert.equal(Ci.nsIPermissionManager.PROMPT_ACTION,
+  Assert.equal(Ci.nsIPermissionManager.UNKNOWN_ACTION,
                pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION));
   Assert.equal(Ci.nsIPermissionManager.PROMPT_ACTION, findCapabilityViaEnum());
   await checkCapabilityViaDB(Ci.nsIPermissionManager.PROMPT_ACTION);
 
   // --------------------------------------------------------------
   // check default permissions and removeAllSince work as expected.
   pm.removeAll(); // ensure only defaults are there.
 
--- a/extensions/permissions/test/unit/test_permmanager_matches.js
+++ b/extensions/permissions/test/unit/test_permmanager_matches.js
@@ -73,39 +73,39 @@ function run_test() {
   let perm_n = pm.getPermissionObject(uri0_n, "test/matches", true);
   pm.addFromPrincipal(uri0_y_, "test/matches", pm.ALLOW_ACTION);
   let perm_y_ = pm.getPermissionObject(uri0_y_, "test/matches", true);
   pm.addFromPrincipal(uri0_1, "test/matches", pm.ALLOW_ACTION);
   let perm_1 = pm.getPermissionObject(uri0_n, "test/matches", true);
   pm.addFromPrincipal(uri0_cnn, "test/matches", pm.ALLOW_ACTION);
   let perm_cnn = pm.getPermissionObject(uri0_n, "test/matches", true);
 
-  matches_always(perm_n, [uri0_n, uri0_1, uri0_cnn]);
-  matches_weak(perm_n, [uri1_n, uri1_1, uri1_cnn]);
+  matches_always(perm_n, [uri0_n, uri0_1]);
+  matches_weak(perm_n, [uri1_n, uri1_1]);
   matches_never(perm_n, [uri2_n, uri3_n, uri4_n, uri5_n,
                            uri0_y_, uri1_y_, uri2_y_, uri3_y_, uri4_y_, uri5_y_,
                            uri2_1, uri3_1, uri4_1, uri5_1,
-                           uri2_cnn, uri3_cnn, uri4_cnn, uri5_cnn]);
+                           uri0_cnn, uri1_cnn, uri2_cnn, uri3_cnn, uri4_cnn, uri5_cnn]);
 
   matches_always(perm_y_, [uri0_y_]);
   matches_weak(perm_y_, [uri1_y_]);
   matches_never(perm_y_, [uri2_y_, uri3_y_, uri4_y_, uri5_y_,
                               uri0_n, uri1_n, uri2_n, uri3_n, uri4_n, uri5_n,
                               uri0_1, uri1_1, uri2_1, uri3_1, uri4_1, uri5_1,
                               uri0_cnn, uri1_cnn, uri2_cnn, uri3_cnn, uri4_cnn, uri5_cnn]);
 
-  matches_always(perm_1, [uri0_n, uri0_1, uri0_cnn]);
-  matches_weak(perm_1, [uri1_n, uri1_1, uri1_cnn]);
+  matches_always(perm_1, [uri0_n, uri0_1]);
+  matches_weak(perm_1, [uri1_n, uri1_1]);
   matches_never(perm_1, [uri2_n, uri3_n, uri4_n, uri5_n,
                          uri0_y_, uri1_y_, uri2_y_, uri3_y_, uri4_y_, uri5_y_,
                          uri2_1, uri3_1, uri4_1, uri5_1,
-                         uri2_cnn, uri3_cnn, uri4_cnn, uri5_cnn]);
+                         uri0_cnn, uri1_cnn, uri2_cnn, uri3_cnn, uri4_cnn, uri5_cnn]);
 
-  matches_always(perm_cnn, [uri0_n, uri0_1, uri0_cnn]);
-  matches_weak(perm_cnn, [uri1_n, uri1_1, uri1_cnn]);
+  matches_always(perm_cnn, [uri0_n, uri0_1]);
+  matches_weak(perm_cnn, [uri1_n, uri1_1]);
   matches_never(perm_cnn, [uri2_n, uri3_n, uri4_n, uri5_n,
                            uri0_y_, uri1_y_, uri2_y_, uri3_y_, uri4_y_, uri5_y_,
                            uri2_1, uri3_1, uri4_1, uri5_1,
-                           uri2_cnn, uri3_cnn, uri4_cnn, uri5_cnn]);
+                           uri0_cnn, uri1_cnn, uri2_cnn, uri3_cnn, uri4_cnn, uri5_cnn]);
 
   // Clean up!
   pm.removeAll();
 }
--- a/gfx/2d/NativeFontResourceFreeType.cpp
+++ b/gfx/2d/NativeFontResourceFreeType.cpp
@@ -34,17 +34,18 @@ already_AddRefed<T> NativeFontResourceFr
   }
   memcpy(fontData.get(), aFontData, aDataLength);
 
   FT_Face face =
       Factory::NewFTFaceFromData(aFTLibrary, fontData.get(), aDataLength, 0);
   if (!face) {
     return nullptr;
   }
-  if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != FT_Err_Ok) {
+  if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != FT_Err_Ok &&
+      FT_Select_Charmap(face, FT_ENCODING_MS_SYMBOL) != FT_Err_Ok) {
     Factory::ReleaseFTFace(face);
     return nullptr;
   }
 
   RefPtr<T> resource = new T(std::move(fontData), aDataLength, face);
   return resource.forget();
 }
 
--- a/gfx/thebes/gfxFT2FontList.cpp
+++ b/gfx/thebes/gfxFT2FontList.cpp
@@ -116,18 +116,19 @@ class AutoFTFace {
       }
     } else {
       mFace = Factory::NewFTFace(nullptr, aFontEntry->mFilename.get(),
                                  aFontEntry->mFTFontIndex);
       if (!mFace) {
         NS_WARNING("failed to create freetype face");
       }
     }
-    if (FT_Err_Ok != FT_Select_Charmap(mFace, FT_ENCODING_UNICODE)) {
-      NS_WARNING("failed to select Unicode charmap");
+    if (FT_Err_Ok != FT_Select_Charmap(mFace, FT_ENCODING_UNICODE) &&
+        FT_Err_Ok != FT_Select_Charmap(mFace, FT_ENCODING_MS_SYMBOL)) {
+      NS_WARNING("failed to select Unicode or symbol charmap");
     }
     mOwnsFace = true;
   }
 
   ~AutoFTFace() {
     if (mFace && mOwnsFace) {
       Factory::ReleaseFTFace(mFace);
       if (mFontDataBuf) {
@@ -263,17 +264,18 @@ FT2FontEntry* FT2FontEntry::CreateFontEn
   // Ownership of aFontData is passed in here; the fontEntry must
   // retain it as long as the FT_Face needs it, and ensure it is
   // eventually deleted.
   FT_Face face = Factory::NewFTFaceFromData(nullptr, aFontData, aLength, 0);
   if (!face) {
     free((void*)aFontData);
     return nullptr;
   }
-  if (FT_Err_Ok != FT_Select_Charmap(face, FT_ENCODING_UNICODE)) {
+  if (FT_Err_Ok != FT_Select_Charmap(face, FT_ENCODING_UNICODE) &&
+      FT_Err_Ok != FT_Select_Charmap(face, FT_ENCODING_MS_SYMBOL)) {
     Factory::ReleaseFTFace(face);
     free((void*)aFontData);
     return nullptr;
   }
   // Create our FT2FontEntry, which inherits the name of the userfont entry
   // as it's not guaranteed that the face has valid names (bug 737315)
   FT2FontEntry* fe = FT2FontEntry::CreateFontEntry(face, nullptr, 0, aFontName,
                                                    aFontData, aLength);
@@ -1110,18 +1112,19 @@ void gfxFT2FontList::FindFontsInOmnijar(
   }
 }
 
 // Given the freetype face corresponding to an entryName and face index,
 // add the face to the available font list and to the faceList string
 void gfxFT2FontList::AddFaceToList(const nsCString& aEntryName, uint32_t aIndex,
                                    StandardFile aStdFile, FT_Face aFace,
                                    nsCString& aFaceList) {
-  if (FT_Err_Ok != FT_Select_Charmap(aFace, FT_ENCODING_UNICODE)) {
-    // ignore faces that don't support a Unicode charmap
+  if (FT_Err_Ok != FT_Select_Charmap(aFace, FT_ENCODING_UNICODE) &&
+      FT_Err_Ok != FT_Select_Charmap(aFace, FT_ENCODING_MS_SYMBOL)) {
+    // ignore faces that don't support a Unicode or symbol charmap
     return;
   }
 
   // build the font entry name and create an FT2FontEntry,
   // but do -not- keep a reference to the FT_Face
   RefPtr<FT2FontEntry> fe =
       CreateNamedFontEntry(aFace, aEntryName.get(), aIndex);
 
--- a/gfx/thebes/gfxFT2Fonts.cpp
+++ b/gfx/thebes/gfxFT2Fonts.cpp
@@ -178,18 +178,22 @@ already_AddRefed<ScaledFont> gfxFT2Font:
   }
 
   RefPtr<ScaledFont> scaledFont(mAzureScaledFont);
   return scaledFont.forget();
 }
 
 void gfxFT2Font::FillGlyphDataForChar(FT_Face face, uint32_t ch,
                                       CachedGlyphData* gd) {
-  if (!face->charmap || face->charmap->encoding != FT_ENCODING_UNICODE) {
-    FT_Select_Charmap(face, FT_ENCODING_UNICODE);
+  if (!face->charmap || (face->charmap->encoding != FT_ENCODING_UNICODE &&
+                         face->charmap->encoding != FT_ENCODING_MS_SYMBOL)) {
+    if (FT_Err_Ok != FT_Select_Charmap(face, FT_ENCODING_UNICODE) &&
+        FT_Err_Ok != FT_Select_Charmap(face, FT_ENCODING_MS_SYMBOL)) {
+      NS_WARNING("failed to select Unicode or symbol charmap!");
+    }
   }
   FT_UInt gid = FT_Get_Char_Index(face, ch);
 
   if (gid == 0) {
     // this font doesn't support this char!
     NS_ASSERTION(gid != 0,
                  "We don't have a glyph, but font indicated that it supported "
                  "this char in tables?");
--- a/gfx/thebes/gfxFT2Utils.cpp
+++ b/gfx/thebes/gfxFT2Utils.cpp
@@ -19,21 +19,26 @@
 uint32_t gfxFT2LockedFace::GetGlyph(uint32_t aCharCode) {
   if (MOZ_UNLIKELY(!mFace)) return 0;
 
 #ifdef HAVE_FONTCONFIG_FCFREETYPE_H
   // FcFreeTypeCharIndex will search starting from the most recently
   // selected charmap.  This can cause non-determistic behavior when more
   // than one charmap supports a character but with different glyphs, as
   // with older versions of MS Gothic, for example.  Always prefer a Unicode
-  // charmap, if there is one.  (FcFreeTypeCharIndex usually does the
-  // appropriate Unicode conversion, but some fonts have non-Roman glyphs
-  // for FT_ENCODING_APPLE_ROMAN characters.)
-  if (!mFace->charmap || mFace->charmap->encoding != FT_ENCODING_UNICODE) {
-    FT_Select_Charmap(mFace, FT_ENCODING_UNICODE);
+  // charmap, if there is one; failing that, try MS_SYMBOL.
+  // (FcFreeTypeCharIndex usually does the appropriate Unicode conversion,
+  // but some fonts have non-Roman glyphs for FT_ENCODING_APPLE_ROMAN
+  // characters.)
+  if (!mFace->charmap || (mFace->charmap->encoding != FT_ENCODING_UNICODE &&
+                          mFace->charmap->encoding != FT_ENCODING_MS_SYMBOL)) {
+    if (FT_Err_Ok != FT_Select_Charmap(mFace, FT_ENCODING_UNICODE) &&
+        FT_Err_Ok != FT_Select_Charmap(mFace, FT_ENCODING_MS_SYMBOL)) {
+      NS_WARNING("failed to select Unicode or symbol charmap");
+    }
   }
 
   return FcFreeTypeCharIndex(mFace, aCharCode);
 #else
   return FT_Get_Char_Index(mFace, aCharCode);
 #endif
 }
 
--- a/gfx/thebes/gfxFcPlatformFontList.cpp
+++ b/gfx/thebes/gfxFcPlatformFontList.cpp
@@ -1983,17 +1983,18 @@ gfxFontEntry* gfxFcPlatformFontList::Mak
     const nsACString& aFontName, WeightRange aWeightForEntry,
     StretchRange aStretchForEntry, SlantStyleRange aStyleForEntry,
     const uint8_t* aFontData, uint32_t aLength) {
   FT_Face face = Factory::NewFTFaceFromData(nullptr, aFontData, aLength, 0);
   if (!face) {
     free((void*)aFontData);
     return nullptr;
   }
-  if (FT_Err_Ok != FT_Select_Charmap(face, FT_ENCODING_UNICODE)) {
+  if (FT_Err_Ok != FT_Select_Charmap(face, FT_ENCODING_UNICODE) &&
+      FT_Err_Ok != FT_Select_Charmap(face, FT_ENCODING_MS_SYMBOL)) {
     Factory::ReleaseFTFace(face);
     free((void*)aFontData);
     return nullptr;
   }
 
   return new gfxFontconfigFontEntry(aFontName, aWeightForEntry,
                                     aStretchForEntry, aStyleForEntry, aFontData,
                                     aLength, face);
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -2862,22 +2862,21 @@ impl ClipBatcher {
         }
 
         // Get the world rect of the clip rectangle. If we can't transform it due
         // to the matrix, just fall back to drawing the entire clip mask.
         let local_clip_rect = LayoutRect::new(
             clip_instance.local_pos,
             clip_rect_size,
         );
-        let transform = clip_scroll_tree.get_relative_transform(
+        let transform = clip_scroll_tree.get_world_transform(
             clip_instance.spatial_node_index,
-            ROOT_SPATIAL_NODE_INDEX,
         );
         let world_clip_rect = match project_rect(
-            &transform.flattened.with_destination::<WorldPixel>(),
+            &transform.into_transform(),
             &local_clip_rect,
             world_rect,
         ) {
             Some(rect) => rect,
             None => return false,
         };
 
         // Work out how many tiles to draw this clip mask in, stretched across the
--- a/gfx/wr/webrender/src/clip.rs
+++ b/gfx/wr/webrender/src/clip.rs
@@ -234,21 +234,22 @@ pub struct ClipNodeInstance {
 #[derive(Debug, Copy, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipNodeRange {
     pub first: u32,
     pub count: u32,
 }
 
-// A helper struct for converting between coordinate systems
-// of clip sources and primitives.
+/// A helper struct for converting between coordinate systems
+/// of clip sources and primitives.
 // todo(gw): optimize:
 //  separate arrays for matrices
 //  cache and only build as needed.
+//TODO: merge with `CoordinateSpaceMapping`?
 #[derive(Debug, MallocSizeOf)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 enum ClipSpaceConversion {
     Local,
     ScaleOffset(ScaleOffset),
     Transform(LayoutToWorldTransform),
 }
 
@@ -1337,28 +1338,31 @@ fn add_clip_node_to_current_chain(
     clip_scroll_tree: &ClipScrollTree,
 ) -> bool {
     let clip_node = &clip_data_store[node.handle];
     let clip_spatial_node = &clip_scroll_tree.spatial_nodes[node.spatial_node_index.0 as usize];
     let ref_spatial_node = &clip_scroll_tree.spatial_nodes[spatial_node_index.0 as usize];
 
     // Determine the most efficient way to convert between coordinate
     // systems of the primitive and clip node.
+    //Note: this code is different from `get_relative_transform` in a way that we only try
+    // getting the relative transform if it's Local or ScaleOffset,
+    // falling back to the world transform otherwise.
     let conversion = if spatial_node_index == node.spatial_node_index {
         ClipSpaceConversion::Local
     } else if ref_spatial_node.coordinate_system_id == clip_spatial_node.coordinate_system_id {
         let scale_offset = ref_spatial_node.coordinate_system_relative_scale_offset
             .inverse()
             .accumulate(&clip_spatial_node.coordinate_system_relative_scale_offset);
         ClipSpaceConversion::ScaleOffset(scale_offset)
     } else {
         ClipSpaceConversion::Transform(
             clip_scroll_tree
                 .get_world_transform(node.spatial_node_index)
-                .flattened
+                .into_transform()
         )
     };
 
     // If we can convert spaces, try to reduce the size of the region
     // requested, and cache the conversion information for the next step.
     if let Some(clip_rect) = clip_node.item.get_local_clip_rect(node.local_pos) {
         match conversion {
             ClipSpaceConversion::Local => {
--- a/gfx/wr/webrender/src/clip_scroll_tree.rs
+++ b/gfx/wr/webrender/src/clip_scroll_tree.rs
@@ -1,23 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ExternalScrollId, PropertyBinding, ReferenceFrameKind, TransformStyle};
 use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollLocation, ScrollSensitivity};
 use api::units::*;
-use euclid::TypedTransform3D;
+use euclid::{TypedPoint2D, TypedScale, TypedTransform3D};
 use crate::gpu_types::TransformPalette;
 use crate::internal_types::{FastHashMap, FastHashSet};
 use crate::print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
 use crate::scene::SceneProperties;
 use crate::spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, ScrollFrameKind};
 use std::{ops, u32};
-use crate::util::{LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset};
+use crate::util::{LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset, scale_factors};
 
 pub type ScrollStates = FastHashMap<ExternalScrollId, ScrollFrameInfo>;
 
 /// An id that identifies coordinate systems in the ClipScrollTree. Each
 /// coordinate system has an id and those ids will be shared when the coordinates
 /// system are the same or are in the same axis-aligned space. This allows
 /// for optimizing mask generation.
 #[derive(Debug, Copy, Clone, PartialEq)]
@@ -25,25 +25,25 @@ pub type ScrollStates = FastHashMap<Exte
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct CoordinateSystemId(pub u32);
 
 /// A node in the hierarchy of coordinate system
 /// transforms.
 #[derive(Debug)]
 pub struct CoordinateSystem {
     pub transform: LayoutTransform,
-    pub transform_style: TransformStyle,
+    pub should_flatten: bool,
     pub parent: Option<CoordinateSystemId>,
 }
 
 impl CoordinateSystem {
     fn root() -> Self {
         CoordinateSystem {
             transform: LayoutTransform::identity(),
-            transform_style: TransformStyle::Flat,
+            should_flatten: false,
             parent: None,
         }
     }
 }
 
 #[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -87,27 +87,16 @@ impl ops::Not for VisibleFace {
     fn not(self) -> Self {
         match self {
             VisibleFace::Front => VisibleFace::Back,
             VisibleFace::Back => VisibleFace::Front,
         }
     }
 }
 
-impl VisibleFace {
-    /// A convenient constructor from methods like `is_backface_visible()`
-    pub fn from_bool(is_backface: bool) -> Self {
-        if is_backface {
-            VisibleFace::Back
-        } else {
-            VisibleFace::Front
-        }
-    }
-}
-
 pub struct ClipScrollTree {
     /// Nodes which determine the positions (offsets and transforms) for primitives
     /// and clips.
     pub spatial_nodes: Vec<SpatialNode>,
 
     /// A list of transforms that establish new coordinate systems.
     /// Spatial nodes only establish a new coordinate system when
     /// they have a transform that is not a simple 2d translation.
@@ -143,23 +132,119 @@ pub struct TransformUpdateState {
     /// transformed by this node will not be displayed and display items not transformed by this
     /// node will not be clipped by clips that are transformed by this node.
     pub invertible: bool,
 
     /// True if this node is a part of Preserve3D hierarchy.
     pub preserves_3d: bool,
 }
 
-/// A processed relative transform between two nodes in the clip-scroll tree.
-#[derive(Debug, Default)]
-pub struct RelativeTransform<U> {
-    /// The flattened transform, produces Z = 0 at all times.
-    pub flattened: TypedTransform3D<f32, LayoutPixel, U>,
-    /// True if the original transform had perspective.
-    pub has_perspective: bool,
+
+/// Transformation between two nodes in the clip-scroll tree that can sometimes be
+/// encoded more efficiently than with a full matrix.
+#[derive(Debug, Clone)]
+pub enum CoordinateSpaceMapping<Src, Dst> {
+    Local,
+    ScaleOffset(ScaleOffset),
+    Transform(TypedTransform3D<f32, Src, Dst>),
+}
+
+impl<Src, Dst> CoordinateSpaceMapping<Src, Dst> {
+    pub fn into_transform(self) -> TypedTransform3D<f32, Src, Dst> {
+        match self {
+            CoordinateSpaceMapping::Local => TypedTransform3D::identity(),
+            CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset.to_transform(),
+            CoordinateSpaceMapping::Transform(transform) => transform,
+        }
+    }
+
+    pub fn visible_face(&self) -> VisibleFace {
+        match *self {
+            CoordinateSpaceMapping::Transform(ref transform) if transform.is_backface_visible() => VisibleFace::Back,
+            CoordinateSpaceMapping::Local |
+            CoordinateSpaceMapping::Transform(_) |
+            CoordinateSpaceMapping::ScaleOffset(_) => VisibleFace::Front,
+
+        }
+    }
+
+    pub fn is_perspective(&self) -> bool {
+        match *self {
+            CoordinateSpaceMapping::Local |
+            CoordinateSpaceMapping::ScaleOffset(_) => false,
+            CoordinateSpaceMapping::Transform(ref transform) => transform.has_perspective_component(),
+        }
+    }
+
+    pub fn project_2d_origin(&self) -> Option<TypedPoint2D<f32, Dst>> {
+        match *self {
+            CoordinateSpaceMapping::Local => Some(TypedPoint2D::zero()),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => Some(
+                scale_offset.offset.to_point() * TypedScale::new(1.0)
+            ),
+            CoordinateSpaceMapping::Transform(ref transform) => {
+                transform.transform_point2d(&TypedPoint2D::zero())
+            }
+        }
+    }
+
+    pub fn inverse_project_2d_origin(&self) -> Option<TypedPoint2D<f32, Src>> {
+        match *self {
+            CoordinateSpaceMapping::Local => Some(TypedPoint2D::zero()),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => Some(
+                scale_offset.inverse().offset.to_point() * TypedScale::new(1.0)
+            ),
+            CoordinateSpaceMapping::Transform(ref transform) => {
+                transform.inverse_project_2d_origin()
+            }
+        }
+    }
+
+    pub fn scale_factors(&self) -> (f32, f32) {
+        match *self {
+            CoordinateSpaceMapping::Local => (1.0, 1.0),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => (scale_offset.scale.x, scale_offset.scale.y),
+            CoordinateSpaceMapping::Transform(ref transform) => scale_factors(transform),
+        }
+    }
+
+    pub fn inverse(&self) -> Option<CoordinateSpaceMapping<Dst, Src>> {
+        match *self {
+            CoordinateSpaceMapping::Local => Some(CoordinateSpaceMapping::Local),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+                Some(CoordinateSpaceMapping::ScaleOffset(scale_offset.inverse()))
+            }
+            CoordinateSpaceMapping::Transform(ref transform) => {
+                transform.inverse().map(CoordinateSpaceMapping::Transform)
+            }
+        }
+    }
+
+    pub fn with_destination<NewDst>(self) -> CoordinateSpaceMapping<Src, NewDst> {
+        match self {
+            CoordinateSpaceMapping::Local => CoordinateSpaceMapping::Local,
+            CoordinateSpaceMapping::ScaleOffset(scale_offset) => CoordinateSpaceMapping::ScaleOffset(scale_offset),
+            CoordinateSpaceMapping::Transform(transform) => CoordinateSpaceMapping::Transform(
+                transform.with_destination::<NewDst>()
+            ),
+        }
+    }
+
+    pub fn post_mul_transform<NewDst>(
+        &self, other: &CoordinateSpaceMapping<Dst, NewDst>
+    ) -> TypedTransform3D<f32, Src, NewDst>
+    where Self: Clone
+    {
+        let matrix = self.clone().into_transform();
+        match *other {
+            CoordinateSpaceMapping::Local => matrix.with_destination::<NewDst>(),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => matrix.post_mul(&scale_offset.to_transform()),
+            CoordinateSpaceMapping::Transform(ref transform) => matrix.post_mul(transform),
+        }
+    }
 }
 
 impl ClipScrollTree {
     pub fn new() -> Self {
         ClipScrollTree {
             spatial_nodes: Vec::new(),
             coord_systems: Vec::new(),
             pending_scroll_offsets: FastHashMap::default(),
@@ -198,85 +283,69 @@ impl ClipScrollTree {
     }
 
     /// Calculate the relative transform from `child_index` to `parent_index`.
     /// This method will panic if the nodes are not connected!
     pub fn get_relative_transform(
         &self,
         child_index: SpatialNodeIndex,
         parent_index: SpatialNodeIndex,
-    ) -> RelativeTransform<LayoutPixel> {
+    ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
         assert!(child_index.0 >= parent_index.0);
+        if child_index == parent_index {
+            return CoordinateSpaceMapping::Local;
+        }
+
         let child = &self.spatial_nodes[child_index.0 as usize];
         let parent = &self.spatial_nodes[parent_index.0 as usize];
-        let mut has_perspective = false;
 
         if child.coordinate_system_id == parent.coordinate_system_id {
-            return RelativeTransform {
-                flattened: parent.coordinate_system_relative_scale_offset
-                    .inverse()
-                    .accumulate(&child.coordinate_system_relative_scale_offset)
-                    .to_transform(),
-                has_perspective,
-            }
+            let scale_offset = parent.coordinate_system_relative_scale_offset
+                .inverse()
+                .accumulate(&child.coordinate_system_relative_scale_offset);
+            return CoordinateSpaceMapping::ScaleOffset(scale_offset);
         }
 
         let mut coordinate_system_id = child.coordinate_system_id;
         let mut transform = child.coordinate_system_relative_scale_offset.to_transform();
-        let mut transform_style = child.transform_style();
 
         // we need to update the associated parameters of a transform in two cases:
         // 1) when the flattening happens, so that we don't lose that original 3D aspects
         // 2) when we reach the end of iteration, so that our result is up to date
 
         while coordinate_system_id != parent.coordinate_system_id {
             let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
 
-            if coord_system.transform_style == TransformStyle::Flat {
-                has_perspective |= transform.has_perspective_component();
-                if transform_style != TransformStyle::Flat {
-                    //Note: this function makes the transform to ignore the Z coordinate of inputs
-                    // *even* for computing the X and Y coordinates of the output.
-                    //transform = transform.project_to_2d();
-                    transform.m13 = 0.0;
-                    transform.m23 = 0.0;
-                    transform.m33 = 1.0;
-                    transform.m43 = 0.0;
-                }
+            if coord_system.should_flatten {
+                transform.m13 = 0.0;
+                transform.m23 = 0.0;
+                transform.m33 = 1.0;
+                transform.m43 = 0.0;
             }
 
             coordinate_system_id = coord_system.parent.expect("invalid parent!");
             transform = transform.post_mul(&coord_system.transform);
-            transform_style = coord_system.transform_style;
         }
 
-        has_perspective |= transform.has_perspective_component();
-
         transform = transform.post_mul(
             &parent.coordinate_system_relative_scale_offset
                 .inverse()
                 .to_transform(),
         );
 
-        RelativeTransform {
-            flattened: transform,
-            has_perspective,
-        }
+        CoordinateSpaceMapping::Transform(transform)
     }
 
     /// Calculate the relative transform from `child_index` to the scene root.
     pub fn get_world_transform(
         &self,
         index: SpatialNodeIndex,
-    ) -> RelativeTransform<WorldPixel> {
-        let relative = self.get_relative_transform(index, ROOT_SPATIAL_NODE_INDEX);
-        RelativeTransform {
-            flattened: relative.flattened.with_destination::<WorldPixel>(),
-            has_perspective: relative.has_perspective,
-        }
+    ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
+        self.get_relative_transform(index, ROOT_SPATIAL_NODE_INDEX)
+            .with_destination::<WorldPixel>()
     }
 
     /// Returns true if the spatial node is the same as the parent, or is
     /// a child of the parent.
     pub fn is_same_or_child_of(
         &self,
         spatial_node_index: SpatialNodeIndex,
         parent_spatial_node_index: SpatialNodeIndex,
@@ -389,26 +458,21 @@ impl ClipScrollTree {
         let node_index = self.find_nearest_scrolling_ancestor(node_index);
         self.spatial_nodes[node_index.0 as usize].scroll(scroll_location)
     }
 
     pub fn update_tree(
         &mut self,
         pan: WorldPoint,
         scene_properties: &SceneProperties,
-        mut transform_palette: Option<&mut TransformPalette>,
     ) {
         if self.spatial_nodes.is_empty() {
             return;
         }
 
-        if let Some(ref mut palette) = transform_palette {
-            palette.allocate(self.spatial_nodes.len());
-        }
-
         self.coord_systems.clear();
         self.coord_systems.push(CoordinateSystem::root());
 
         let root_node_index = self.root_reference_frame_index();
         let state = TransformUpdateState {
             parent_reference_frame_transform: LayoutVector2D::new(pan.x, pan.y).into(),
             parent_accumulated_scroll_offset: LayoutVector2D::zero(),
             nearest_scrolling_ancestor_offset: LayoutVector2D::zero(),
@@ -424,31 +488,39 @@ impl ClipScrollTree {
         while let Some((node_index, mut state)) = self.nodes_to_update.pop() {
             let (previous, following) = self.spatial_nodes.split_at_mut(node_index.0 as usize);
             let node = match following.get_mut(0) {
                 Some(node) => node,
                 None => continue,
             };
 
             node.update(&mut state, &mut self.coord_systems, scene_properties, &*previous);
-            if let Some(ref mut palette) = transform_palette {
-                node.push_gpu_data(palette, node_index);
-            }
 
             if !node.children.is_empty() {
                 node.prepare_state_for_children(&mut state);
                 self.nodes_to_update.extend(node.children
                     .iter()
                     .rev()
                     .map(|child_index| (*child_index, state.clone()))
                 );
             }
         }
     }
 
+    pub fn build_transform_palette(&self) -> TransformPalette {
+        let mut palette = TransformPalette::new(self.spatial_nodes.len());
+        //TODO: this could be faster by a bit of dynamic programming
+        for i in 0 .. self.spatial_nodes.len() {
+            let index = SpatialNodeIndex(i as u32);
+            let world_transform = self.get_world_transform(index).into_transform();
+            palette.set_world_transform(index, world_transform);
+        }
+        palette
+    }
+
     pub fn finalize_and_apply_pending_scroll_offsets(&mut self, old_states: ScrollStates) {
         for node in &mut self.spatial_nodes {
             let external_id = match node.node_type {
                 SpatialNodeType::ScrollFrame(ScrollFrameInfo { external_id: Some(id), ..} ) => id,
                 _ => continue,
             };
 
             if let Some(scrolling_state) = old_states.get(&external_id) {
@@ -539,57 +611,53 @@ impl ClipScrollTree {
         &self,
         index: SpatialNodeIndex,
         pt: &mut T,
     ) {
         let node = &self.spatial_nodes[index.0 as usize];
         match node.node_type {
             SpatialNodeType::StickyFrame(ref sticky_frame_info) => {
                 pt.new_level(format!("StickyFrame"));
-                pt.add_item(format!("index: {:?}", index));
                 pt.add_item(format!("sticky info: {:?}", sticky_frame_info));
             }
             SpatialNodeType::ScrollFrame(scrolling_info) => {
                 pt.new_level(format!("ScrollFrame"));
-                pt.add_item(format!("index: {:?}", index));
                 pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
                 pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
                 pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset));
                 pt.add_item(format!("external_scroll_offset: {:?}", scrolling_info.external_scroll_offset));
             }
-            SpatialNodeType::ReferenceFrame(ref _info) => {
+            SpatialNodeType::ReferenceFrame(ref info) => {
                 pt.new_level(format!("ReferenceFrame"));
-                pt.add_item(format!("index: {:?}", index));
+                pt.add_item(format!("kind: {:?}", info.kind));
+                pt.add_item(format!("transform_style: {:?}", info.transform_style));
             }
         }
 
+        pt.add_item(format!("index: {:?}", index));
         pt.add_item(format!("world_viewport_transform: {:?}", node.world_viewport_transform));
-        pt.add_item(format!("world_content_transform: {:?}", node.world_content_transform));
         pt.add_item(format!("coordinate_system_id: {:?}", node.coordinate_system_id));
+        pt.add_item(format!("coordinate_system_scale_offset: {:?}", node.coordinate_system_relative_scale_offset));
 
         for child_index in &node.children {
             self.print_node(*child_index, pt);
         }
 
         pt.end_level();
     }
 
     /// Get the visible face of the transfrom from the specified node to its parent.
     pub fn get_local_visible_face(&self, node_index: SpatialNodeIndex) -> VisibleFace {
         let node = &self.spatial_nodes[node_index.0 as usize];
         let parent_index = match node.parent {
             Some(index) => index,
             None => return VisibleFace::Front
         };
-        VisibleFace::from_bool(
-            self
-                .get_relative_transform(node_index, parent_index)
-                .flattened
-                .is_backface_visible()
-        )
+        self.get_relative_transform(node_index, parent_index)
+            .visible_face()
     }
 
     #[allow(dead_code)]
     pub fn print(&self) {
         if !self.spatial_nodes.is_empty() {
             let mut pt = PrintTree::new("clip_scroll tree");
             self.print_with(&mut pt);
         }
@@ -630,17 +698,17 @@ fn test_pt(
     parent: SpatialNodeIndex,
     expected_x: f32,
     expected_y: f32,
 ) {
     use euclid::approxeq::ApproxEq;
     const EPSILON: f32 = 0.0001;
 
     let p = LayoutPoint::new(px, py);
-    let m = cst.get_relative_transform(child, parent).flattened;
+    let m = cst.get_relative_transform(child, parent).into_transform();
     let pt = m.transform_point2d(&p).unwrap();
     assert!(pt.x.approx_eq_eps(&expected_x, &EPSILON) &&
             pt.y.approx_eq_eps(&expected_y, &EPSILON),
             "p: {:?} -> {:?}\nm={:?}",
             p, pt, m,
             );
 }
 
@@ -673,17 +741,17 @@ fn test_cst_simple_translation() {
 
     let child3 = add_reference_frame(
         &mut cst,
         Some(child2),
         LayoutTransform::create_translation(200.0, 200.0, 0.0),
         LayoutVector2D::zero(),
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     test_pt(100.0, 100.0, &cst, child1, root, 200.0, 100.0);
     test_pt(100.0, 100.0, &cst, child2, root, 200.0, 150.0);
     test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 150.0);
     test_pt(100.0, 100.0, &cst, child3, root, 400.0, 350.0);
 }
 
 #[test]
@@ -715,17 +783,17 @@ fn test_cst_simple_scale() {
 
     let child3 = add_reference_frame(
         &mut cst,
         Some(child2),
         LayoutTransform::create_scale(2.0, 2.0, 1.0),
         LayoutVector2D::zero(),
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     test_pt(100.0, 100.0, &cst, child1, root, 400.0, 100.0);
     test_pt(100.0, 100.0, &cst, child2, root, 400.0, 200.0);
     test_pt(100.0, 100.0, &cst, child3, root, 800.0, 400.0);
     test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 200.0);
     test_pt(100.0, 100.0, &cst, child3, child1, 200.0, 400.0);
 }
 
@@ -765,17 +833,17 @@ fn test_cst_scale_translation() {
 
     let child4 = add_reference_frame(
         &mut cst,
         Some(child3),
         LayoutTransform::create_scale(3.0, 2.0, 1.0),
         LayoutVector2D::zero(),
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     test_pt(100.0, 100.0, &cst, child1, root, 200.0, 150.0);
     test_pt(100.0, 100.0, &cst, child2, root, 300.0, 450.0);
     test_pt(100.0, 100.0, &cst, child4, root, 1100.0, 450.0);
 
     test_pt(0.0, 0.0, &cst, child4, child1, 400.0, -400.0);
     test_pt(100.0, 100.0, &cst, child4, child1, 1000.0, 400.0);
     test_pt(100.0, 100.0, &cst, child2, child1, 200.0, 400.0);
@@ -799,12 +867,12 @@ fn test_cst_translation_rotate() {
 
     let child1 = add_reference_frame(
         &mut cst,
         Some(root),
         LayoutTransform::create_rotation(0.0, 0.0, 1.0, Angle::degrees(90.0)),
         LayoutVector2D::zero(),
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     test_pt(100.0, 0.0, &cst, child1, root, 0.0, -100.0);
 }
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -507,16 +507,17 @@ impl<'a> DisplayListFlattener<'a> {
             }
         );
 
         let tile_cache = TileCache::new(
             main_scroll_root,
             &prim_list.prim_instances,
             *self.pipeline_clip_chain_stack.last().unwrap(),
             &self.prim_store.pictures,
+            &self.clip_scroll_tree,
         );
 
         let pic_index = self.prim_store.pictures.alloc().init(PicturePrimitive::new_image(
             Some(PictureCompositeMode::TileCache { clear_color: ColorF::new(1.0, 1.0, 1.0, 1.0) }),
             Picture3DContext::Out,
             self.scene.root_pipeline_id.unwrap(),
             None,
             true,
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -527,22 +527,21 @@ impl FrameBuilder {
             .total_primitives
             .set(self.prim_store.prim_count());
 
         resource_cache.begin_frame(stamp);
         gpu_cache.begin_frame(stamp);
 
         self.globals.update(gpu_cache);
 
-        let mut transform_palette = TransformPalette::new();
         clip_scroll_tree.update_tree(
             pan,
             scene_properties,
-            Some(&mut transform_palette),
         );
+        let mut transform_palette = clip_scroll_tree.build_transform_palette();
         self.clip_store.clear_old_instances();
 
         let mut render_tasks = RenderTaskTree::new(
             stamp.frame_id(),
             render_task_counters,
         );
         let mut surfaces = Vec::new();
 
--- a/gfx/wr/webrender/src/gpu_types.rs
+++ b/gfx/wr/webrender/src/gpu_types.rs
@@ -471,34 +471,29 @@ struct RelativeTransformKey {
 //           should be relative to.
 pub struct TransformPalette {
     transforms: Vec<TransformData>,
     metadata: Vec<TransformMetadata>,
     map: FastHashMap<RelativeTransformKey, usize>,
 }
 
 impl TransformPalette {
-    pub fn new() -> Self {
+    pub fn new(count: usize) -> Self {
         let _ = VECS_PER_TRANSFORM;
         TransformPalette {
-            transforms: Vec::new(),
-            metadata: Vec::new(),
+            transforms: vec![TransformData::invalid(); count],
+            metadata: vec![TransformMetadata::invalid(); count],
             map: FastHashMap::default(),
         }
     }
 
     pub fn finish(self) -> Vec<TransformData> {
         self.transforms
     }
 
-    pub fn allocate(&mut self, count: usize) {
-        self.transforms = vec![TransformData::invalid(); count];
-        self.metadata = vec![TransformMetadata::invalid(); count];
-    }
-
     pub fn set_world_transform(
         &mut self,
         index: SpatialNodeIndex,
         transform: LayoutToWorldTransform,
     ) {
         register_transform(
             &mut self.metadata,
             &mut self.transforms,
@@ -530,17 +525,17 @@ impl TransformPalette {
 
             *self.map
                 .entry(key)
                 .or_insert_with(|| {
                     let transform = clip_scroll_tree.get_relative_transform(
                         child_index,
                         parent_index,
                     )
-                    .flattened
+                    .into_transform()
                     .with_destination::<PicturePixel>();
 
                     register_transform(
                         metadata,
                         transforms,
                         child_index,
                         parent_index,
                         transform,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -4,29 +4,29 @@
 
 use api::{FilterOp, MixBlendMode, PipelineId, PremultipliedColorF};
 use api::{PropertyBinding, PropertyBindingId};
 use api::{DebugFlags, RasterSpace, ColorF, ImageKey, ClipMode};
 use api::units::*;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipChainId, ClipChainNode, ClipItem, ClipStore, ClipDataStore, ClipChainStack};
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX,
-    ClipScrollTree, CoordinateSystemId, SpatialNodeIndex, VisibleFace
+    ClipScrollTree, CoordinateSystemId, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace
 };
 use crate::debug_colors;
-use euclid::{size2, vec3, TypedPoint2D, TypedScale, TypedSize2D};
+use euclid::{size2, vec3, TypedPoint2D, TypedScale, TypedSize2D, Vector2D};
 use euclid::approxeq::ApproxEq;
 use crate::frame_builder::{FrameVisibilityContext, FrameVisibilityState};
 use crate::intern::ItemUid;
 use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter};
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::UvRectKind;
 use plane_split::{Clipper, Polygon, Splitter};
-use crate::prim_store::{CoordinateSpaceMapping, SpaceMapper};
+use crate::prim_store::SpaceMapper;
 use crate::prim_store::{PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
 use crate::prim_store::{get_raster_rects, PrimitiveScratchBuffer, VectorKey, PointKey};
 use crate::prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex, RectangleKey};
 use crate::print_tree::PrintTreePrinter;
 use crate::render_backend::DataStores;
 use crate::render_task::{ClearMode, RenderTask, TileBlit};
 use crate::render_task::{RenderTaskId, RenderTaskLocation};
 use crate::resource_cache::ResourceCache;
@@ -698,30 +698,32 @@ fn collect_ref_prims(
 }
 
 impl TileCache {
     pub fn new(
         spatial_node_index: SpatialNodeIndex,
         prim_instances: &[PrimitiveInstance],
         root_clip_chain_id: ClipChainId,
         pictures: &[PicturePrimitive],
+        clip_scroll_tree: &ClipScrollTree,
     ) -> Self {
         // Build the list of reference primitives
         // for this picture cache.
         let reference_prims = ReferencePrimitiveList::new(
             prim_instances,
             pictures,
         );
 
         TileCache {
             spatial_node_index,
             tiles: Vec::new(),
             map_local_to_world: SpaceMapper::new(
                 ROOT_SPATIAL_NODE_INDEX,
                 WorldRect::zero(),
+                clip_scroll_tree,
             ),
             tiles_to_draw: Vec::new(),
             opacity_bindings: FastHashMap::default(),
             dirty_region: DirtyRegion::new(),
             world_origin: WorldPoint::zero(),
             world_tile_size: WorldSize::zero(),
             tile_count: TileSize::zero(),
             scroll_offset: None,
@@ -788,17 +790,16 @@ impl TileCache {
         }
 
         let DeviceIntSize { width: tile_width, height: tile_height, _unit: _ } =
             Self::tile_dimensions(frame_context.config.testing);
 
         // Work out the scroll offset to apply to the world reference point.
         let scroll_offset_point = frame_context.clip_scroll_tree
             .get_world_transform(self.spatial_node_index)
-            .flattened
             .inverse_project_2d_origin()
             .unwrap_or_else(LayoutPoint::zero);
 
         let scroll_offset = WorldVector2D::new(scroll_offset_point.x, scroll_offset_point.y);
         let scroll_delta = match self.scroll_offset {
             Some(prev) => prev - scroll_offset,
             None => WorldVector2D::zero(),
         };
@@ -828,16 +829,17 @@ impl TileCache {
         }.unwrap_or(WorldVector2D::zero());
 
         // Assume no tiles are valid to draw by default
         self.tiles_to_draw.clear();
 
         self.map_local_to_world = SpaceMapper::new(
             ROOT_SPATIAL_NODE_INDEX,
             frame_context.screen_world_rect,
+            frame_context.clip_scroll_tree,
         );
 
         let world_mapper = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             self.spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
@@ -1434,25 +1436,23 @@ impl TileCache {
                 // Note: this is the only place where we don't know beforehand if the tile-affecting
                 // spatial node is below or above the current picture.
                 let inverse_origin = if self.spatial_node_index >= spatial_node_index {
                     frame_context.clip_scroll_tree
                         .get_relative_transform(
                             self.spatial_node_index,
                             spatial_node_index,
                         )
-                        .flattened
-                        .transform_point2d(&LayoutPoint::zero())
+                        .project_2d_origin()
                 } else {
                     frame_context.clip_scroll_tree
                         .get_relative_transform(
                             spatial_node_index,
                             self.spatial_node_index,
                         )
-                        .flattened
                         .inverse_project_2d_origin()
                 };
                 // Store the result of transforming a fixed point by this
                 // transform.
                 // TODO(gw): This could in theory give incorrect results for a
                 //           primitive behind the near plane.
                 let key = inverse_origin
                     .unwrap_or_else(LayoutPoint::zero)
@@ -1838,16 +1838,17 @@ impl SurfaceInfo {
 
         let pic_bounds = map_surface_to_world
             .unmap(&map_surface_to_world.bounds)
             .unwrap_or(PictureRect::max_rect());
 
         let map_local_to_surface = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
+            clip_scroll_tree,
         );
 
         SurfaceInfo {
             rect: PictureRect::zero(),
             map_local_to_surface,
             render_tasks: None,
             raster_spatial_node_index,
             surface_spatial_node_index,
@@ -2395,16 +2396,17 @@ impl PicturePrimitive {
         );
 
         let pic_bounds = map_pic_to_world.unmap(&map_pic_to_world.bounds)
                                          .unwrap_or(PictureRect::max_rect());
 
         let map_local_to_pic = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
+            frame_context.clip_scroll_tree,
         );
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             surface_spatial_node_index,
             raster_spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
@@ -2792,60 +2794,66 @@ impl PicturePrimitive {
         clip_scroll_tree: &ClipScrollTree,
         prim_spatial_node_index: SpatialNodeIndex,
         original_local_rect: LayoutRect,
         combined_local_clip_rect: &LayoutRect,
         world_rect: WorldRect,
         plane_split_anchor: usize,
     ) -> bool {
         let transform = clip_scroll_tree
-            .get_world_transform(prim_spatial_node_index)
-            .flattened;
-        let matrix = transform.cast();
+            .get_world_transform(prim_spatial_node_index);
+        let matrix = transform.clone().into_transform().cast();
 
         // Apply the local clip rect here, before splitting. This is
         // because the local clip rect can't be applied in the vertex
         // shader for split composites, since we are drawing polygons
         // rather that rectangles. The interpolation still works correctly
         // since we determine the UVs by doing a bilerp with a factor
         // from the original local rect.
         let local_rect = match original_local_rect
             .intersection(combined_local_clip_rect)
         {
             Some(rect) => rect.cast(),
             None => return false,
         };
         let world_rect = world_rect.cast();
 
-        if transform.is_simple_translation() {
-            let inv_transform = clip_scroll_tree
-                .get_world_transform(prim_spatial_node_index)
-                .flattened
-                .inverse()
-                .expect("Simple translation, really?");
-            let polygon = Polygon::from_transformed_rect_with_inverse(
-                local_rect,
-                &matrix,
-                &inv_transform.cast(),
-                plane_split_anchor,
-            ).unwrap();
-            splitter.add(polygon);
-        } else {
-            let mut clipper = Clipper::new();
-            let results = clipper.clip_transformed(
-                Polygon::from_rect(
+        match transform {
+            CoordinateSpaceMapping::Local => {
+                let polygon = Polygon::from_rect(
+                    local_rect * TypedScale::new(1.0),
+                    plane_split_anchor,
+                );
+                splitter.add(polygon);
+            }
+            CoordinateSpaceMapping::ScaleOffset(scale_offset) if scale_offset.scale == Vector2D::new(1.0, 1.0) => {
+                let inv_matrix = scale_offset.inverse().to_transform().cast();
+                let polygon = Polygon::from_transformed_rect_with_inverse(
                     local_rect,
+                    &matrix,
+                    &inv_matrix,
                     plane_split_anchor,
-                ),
-                &matrix,
-                Some(world_rect),
-            );
-            if let Ok(results) = results {
-                for poly in results {
-                    splitter.add(poly);
+                ).unwrap();
+                splitter.add(polygon);
+            }
+            CoordinateSpaceMapping::ScaleOffset(_) |
+            CoordinateSpaceMapping::Transform(_) => {
+                let mut clipper = Clipper::new();
+                let results = clipper.clip_transformed(
+                    Polygon::from_rect(
+                        local_rect,
+                        plane_split_anchor,
+                    ),
+                    &matrix,
+                    Some(world_rect),
+                );
+                if let Ok(results) = results {
+                    for poly in results {
+                        splitter.add(poly);
+                    }
                 }
             }
         }
 
         true
     }
 
     pub fn resolve_split_planes(
@@ -2861,20 +2869,19 @@ impl PicturePrimitive {
         ordered.clear();
 
         // Process the accumulated split planes and order them for rendering.
         // Z axis is directed at the screen, `sort` is ascending, and we need back-to-front order.
         for poly in splitter.sort(vec3(0.0, 0.0, 1.0)) {
             let spatial_node_index = self.prim_list.prim_instances[poly.anchor].spatial_node_index;
             let transform = match clip_scroll_tree
                 .get_world_transform(spatial_node_index)
-                .flattened
                 .inverse()
             {
-                Some(transform) => transform,
+                Some(transform) => transform.into_transform(),
                 // logging this would be a bit too verbose
                 None => continue,
             };
 
             let local_points = [
                 transform.transform_point3d(&poly.points[0].cast()).unwrap(),
                 transform.transform_point3d(&poly.points[1].cast()).unwrap(),
                 transform.transform_point3d(&poly.points[2].cast()).unwrap(),
@@ -3010,17 +3017,17 @@ impl PicturePrimitive {
                     0.0
                 }
             };
 
             // Check if there is perspective, and thus whether a new
             // rasterization root should be established.
             let establishes_raster_root = frame_context.clip_scroll_tree
                 .get_relative_transform(surface_spatial_node_index, parent_raster_node_index)
-                .has_perspective;
+                .is_perspective();
 
             // Disallow subpixel AA if an intermediate surface is needed.
             // TODO(lsalzman): allow overriding parent if intermediate surface is opaque
             let allow_subpixel_aa = match composite_mode {
                 PictureCompositeMode::TileCache { clear_color, .. } => {
                     // If the tile cache has an opaque background, then it's fine to use
                     // subpixel rendering (this is the common case).
                     clear_color.a >= 1.0
@@ -3074,23 +3081,22 @@ impl PicturePrimitive {
         state.pop_picture();
 
         for cluster in &mut self.prim_list.clusters {
             // Skip the cluster if backface culled.
             if !cluster.is_backface_visible {
                 // For in-preserve-3d primitives and pictures, the backface visibility is
                 // evaluated relative to the containing block.
                 if let Picture3DContext::In { ancestor_index, .. } = self.context_3d {
-                    match CoordinateSpaceMapping::<LayoutPoint, LayoutPoint>::new(
-                        ancestor_index,
-                        cluster.spatial_node_index,
-                        &frame_context.clip_scroll_tree,
-                    ) {
-                        (_, VisibleFace::Back) => continue,
-                        (_, VisibleFace::Front) => (),
+                    match frame_context.clip_scroll_tree
+                        .get_relative_transform(cluster.spatial_node_index, ancestor_index)
+                        .visible_face()
+                    {
+                        VisibleFace::Back => continue,
+                        VisibleFace::Front => (),
                     }
                 }
             }
 
             // No point including this cluster if it can't be transformed
             let spatial_node = &frame_context
                 .clip_scroll_tree
                 .spatial_nodes[cluster.spatial_node_index.0 as usize];
@@ -3378,16 +3384,17 @@ fn build_ref_prims(
     prim_map: &mut FastHashMap<ItemUid, WorldPoint>,
     clip_scroll_tree: &ClipScrollTree,
 ) {
     prim_map.clear();
 
     let mut map_local_to_world = SpaceMapper::new(
         ROOT_SPATIAL_NODE_INDEX,
         WorldRect::zero(),
+        clip_scroll_tree,
     );
 
     for ref_prim in ref_prims {
         map_local_to_world.set_target_spatial_node(
             ref_prim.spatial_node_index,
             clip_scroll_tree,
         );
 
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -7,17 +7,17 @@ use api::{FilterOp, ImageRendering, Repe
 use api::{PremultipliedColorF, PropertyBinding, Shadow, GradientStop};
 use api::{BoxShadowClipMode, LineStyle, LineOrientation};
 use api::{PrimitiveKeyKind, RasterSpace};
 use api::units::*;
 use crate::border::{get_max_scale_for_border, build_border_instances};
 use crate::border::BorderSegmentCacheKey;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipStore};
-use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex, VisibleFace};
+use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace};
 use crate::clip::{ClipDataStore, ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem};
 use crate::debug_colors;
 use crate::debug_render::DebugItem;
 use crate::display_list_flattener::{CreateShadow, IsVisible};
 use euclid::{SideOffsets2D, TypedTransform3D, TypedRect, TypedScale, TypedSize2D, TypedPoint2D};
 use euclid::approxeq::ApproxEq;
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use crate::frame_builder::{FrameVisibilityContext, FrameVisibilityState};
@@ -45,19 +45,18 @@ use crate::renderer::{MAX_VERTEX_TEXTURE
 use crate::resource_cache::{ImageProperties, ImageRequest};
 use crate::scene::SceneProperties;
 use crate::segment::SegmentBuilder;
 use std::{cmp, fmt, hash, ops, u32, usize, mem};
 #[cfg(debug_assertions)]
 use std::sync::atomic::{AtomicUsize, Ordering};
 use crate::storage;
 use crate::texture_cache::TEXTURE_REGION_DIMENSIONS;
-use crate::util::{ScaleOffset, MatrixHelpers, MaxRect, Recycler};
-use crate::util::{pack_as_float, project_rect, raster_rect_to_device_pixels};
-use crate::util::{scale_factors, clamp_to_scale_factor};
+use crate::util::{MatrixHelpers, MaxRect, Recycler};
+use crate::util::{clamp_to_scale_factor, pack_as_float, project_rect, raster_rect_to_device_pixels};
 use crate::internal_types::LayoutPrimitiveInfo;
 use smallvec::SmallVec;
 
 pub mod borders;
 pub mod gradient;
 pub mod image;
 pub mod line_dec;
 pub mod picture;
@@ -127,103 +126,85 @@ impl PrimitiveOpacity {
         PrimitiveOpacity{
             is_opaque: self.is_opaque && other.is_opaque
         }
     }
 }
 
 
 #[derive(Debug, Clone)]
-pub enum CoordinateSpaceMapping<F, T> {
-    Local,
-    ScaleOffset(ScaleOffset),
-    Transform(TypedTransform3D<f32, F, T>),
-}
-
-impl<F, T> CoordinateSpaceMapping<F, T> {
-    pub fn new(
-        ref_spatial_node_index: SpatialNodeIndex,
-        target_node_index: SpatialNodeIndex,
-        clip_scroll_tree: &ClipScrollTree,
-    ) -> (Self, VisibleFace) {
-        let spatial_nodes = &clip_scroll_tree.spatial_nodes;
-        let ref_spatial_node = &spatial_nodes[ref_spatial_node_index.0 as usize];
-        let target_spatial_node = &spatial_nodes[target_node_index.0 as usize];
-
-        if ref_spatial_node_index == target_node_index {
-            (CoordinateSpaceMapping::Local, VisibleFace::Front)
-        } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
-            let scale_offset = ref_spatial_node.coordinate_system_relative_scale_offset
-                .inverse()
-                .accumulate(&target_spatial_node.coordinate_system_relative_scale_offset);
-            (CoordinateSpaceMapping::ScaleOffset(scale_offset), VisibleFace::Front)
-        } else {
-            let relative = clip_scroll_tree
-                .get_relative_transform(target_node_index, ref_spatial_node_index);
-            (
-                CoordinateSpaceMapping::Transform(
-                    relative.flattened.with_source::<F>().with_destination::<T>()
-                ),
-                VisibleFace::from_bool(relative.flattened.is_backface_visible())
-            )
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
 pub struct SpaceMapper<F, T> {
     kind: CoordinateSpaceMapping<F, T>,
     pub ref_spatial_node_index: SpatialNodeIndex,
+    ref_world_inv_transform: CoordinateSpaceMapping<WorldPixel, T>,
     pub current_target_spatial_node_index: SpatialNodeIndex,
     pub bounds: TypedRect<f32, T>,
     visible_face: VisibleFace,
 }
 
 impl<F, T> SpaceMapper<F, T> where F: fmt::Debug {
     pub fn new(
         ref_spatial_node_index: SpatialNodeIndex,
         bounds: TypedRect<f32, T>,
+        clip_scroll_tree: &ClipScrollTree,
     ) -> Self {
         SpaceMapper {
             kind: CoordinateSpaceMapping::Local,
             ref_spatial_node_index,
+            ref_world_inv_transform: clip_scroll_tree
+                .get_world_transform(ref_spatial_node_index)
+                .inverse()
+                .unwrap()
+                .with_destination::<T>(),
             current_target_spatial_node_index: ref_spatial_node_index,
             bounds,
             visible_face: VisibleFace::Front,
         }
     }
 
     pub fn new_with_target(
         ref_spatial_node_index: SpatialNodeIndex,
         target_node_index: SpatialNodeIndex,
         bounds: TypedRect<f32, T>,
         clip_scroll_tree: &ClipScrollTree,
     ) -> Self {
-        let mut mapper = SpaceMapper::new(ref_spatial_node_index, bounds);
+        let mut mapper = SpaceMapper::new(ref_spatial_node_index, bounds, clip_scroll_tree);
         mapper.set_target_spatial_node(target_node_index, clip_scroll_tree);
         mapper
     }
 
     pub fn set_target_spatial_node(
         &mut self,
         target_node_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
     ) {
-        if target_node_index != self.current_target_spatial_node_index {
-            self.current_target_spatial_node_index = target_node_index;
-
-            let (kind, visible_face) = CoordinateSpaceMapping::new(
-                self.ref_spatial_node_index,
-                target_node_index,
-                clip_scroll_tree,
-            );
-
-            self.kind = kind;
-            self.visible_face = visible_face;
+        if target_node_index == self.current_target_spatial_node_index {
+            return
         }
+
+        let ref_spatial_node = &clip_scroll_tree.spatial_nodes[self.ref_spatial_node_index.0 as usize];
+        let target_spatial_node = &clip_scroll_tree.spatial_nodes[target_node_index.0 as usize];
+
+        self.kind = if self.ref_spatial_node_index == target_node_index {
+            CoordinateSpaceMapping::Local
+        } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
+            let scale_offset = ref_spatial_node.coordinate_system_relative_scale_offset
+                .inverse()
+                .accumulate(&target_spatial_node.coordinate_system_relative_scale_offset);
+            CoordinateSpaceMapping::ScaleOffset(scale_offset)
+        } else {
+            let transform = clip_scroll_tree
+                .get_world_transform(target_node_index)
+                .post_mul_transform(&self.ref_world_inv_transform)
+                .with_source::<F>();
+            CoordinateSpaceMapping::Transform(transform)
+        };
+
+        self.visible_face = self.kind.visible_face();
+        self.current_target_spatial_node_index = target_node_index;
     }
 
     pub fn get_transform(&self) -> TypedTransform3D<f32, F, T> {
         match self.kind {
             CoordinateSpaceMapping::Local => {
                 TypedTransform3D::identity()
             }
             CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
@@ -1778,16 +1759,17 @@ impl PrimitiveStore {
             surface.surface_spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let mut map_local_to_raster = SpaceMapper::new(
             surface.raster_spatial_node_index,
             RasterRect::max_rect(),
+            frame_context.clip_scroll_tree,
         );
 
         let mut surface_rect = PictureRect::zero();
 
         for prim_instance in &mut prim_list.prim_instances {
             prim_instance.reset();
 
             if prim_instance.is_chased() {
@@ -2209,27 +2191,25 @@ impl PrimitiveStore {
         match prim_instance.kind {
             PrimitiveInstanceKind::TextRun { data_handle, run_index, .. } => {
                 let prim_data = &mut frame_state.data_stores.text_run[data_handle];
                 let run = &mut self.text_runs[run_index];
 
                 // The transform only makes sense for screen space rasterization
                 let relative_transform = frame_context
                     .clip_scroll_tree
-                    .get_relative_transform(
-                        prim_instance.spatial_node_index,
-                        ROOT_SPATIAL_NODE_INDEX,
-                    );
+                    .get_world_transform(prim_instance.spatial_node_index)
+                    .into_transform();
                 let prim_offset = prim_instance.prim_origin.to_vector() - run.reference_frame_relative_offset;
 
                 run.request_resources(
                     prim_offset,
                     &prim_data.font,
                     &prim_data.glyphs,
-                    &relative_transform.flattened.with_destination::<WorldPixel>(),
+                    &relative_transform,
                     surface,
                     raster_space,
                     frame_state.resource_cache,
                     frame_state.gpu_cache,
                     frame_state.render_tasks,
                     frame_state.scratch,
                 );
             }
@@ -2785,26 +2765,23 @@ impl PrimitiveStore {
                 // Update the template this instane references, which may refresh the GPU
                 // cache with any shared template data.
                 border_data.update(common_data, frame_state);
 
                 // TODO(gw): For now, the scale factors to rasterize borders at are
                 //           based on the true world transform of the primitive. When
                 //           raster roots with local scale are supported in future,
                 //           that will need to be accounted for here.
-                let relative_transform = frame_context
+                let scale = frame_context
                     .clip_scroll_tree
-                    .get_relative_transform(
-                        prim_instance.spatial_node_index,
-                        ROOT_SPATIAL_NODE_INDEX,
-                    );
+                    .get_world_transform(prim_instance.spatial_node_index)
+                    .scale_factors();
 
                 // Scale factors are normalized to a power of 2 to reduce the number of
-                // resolution changes
-                let scale = scale_factors(&relative_transform.flattened);
+                // resolution changes.
                 // For frames with a changing scale transform round scale factors up to
                 // nearest power-of-2 boundary so that we don't keep having to redraw
                 // the content as it scales up and down. Rounding up to nearest
                 // power-of-2 boundary ensures we never scale up, only down --- avoiding
                 // jaggies. It also ensures we never scale down by more than a factor of
                 // 2, avoiding bad downscaling quality.
                 let scale_width = clamp_to_scale_factor(scale.0, false);
                 let scale_height = clamp_to_scale_factor(scale.1, false);
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -549,17 +549,16 @@ impl Document {
     fn rebuild_hit_tester(&mut self) {
         if let Some(ref mut frame_builder) = self.frame_builder {
             let accumulated_scale_factor = self.view.accumulated_scale_factor();
             let pan = self.view.pan.to_f32() / accumulated_scale_factor;
 
             self.clip_scroll_tree.update_tree(
                 pan,
                 &self.dynamic_properties,
-                None,
             );
 
             self.hit_tester = Some(frame_builder.create_hit_tester(
                 &self.clip_scroll_tree,
                 &self.data_stores.clip,
             ));
             self.hit_tester_is_valid = true;
         }
@@ -1201,17 +1200,17 @@ impl RenderBackend {
 
     fn prepare_for_frames(&mut self) {
         self.resource_cache.prepare_for_frames(SystemTime::now());
         self.gpu_cache.prepare_for_frames();
     }
 
     fn bookkeep_after_frames(&mut self) {
         self.resource_cache.bookkeep_after_frames();
-        self.gpu_cache.bookkeep_after_frames();   
+        self.gpu_cache.bookkeep_after_frames();
     }
 
     fn requires_frame_build(&mut self) -> bool {
         self.resource_cache.requires_frame_build() || self.gpu_cache.requires_frame_build()
     }
 
     fn prepare_transactions(
         &mut self,
--- a/gfx/wr/webrender/src/spatial_node.rs
+++ b/gfx/wr/webrender/src/spatial_node.rs
@@ -3,17 +3,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ExternalScrollId, PipelineId, PropertyBinding, ReferenceFrameKind, ScrollClamping, ScrollLocation};
 use api::{TransformStyle, ScrollSensitivity, StickyOffsetBounds};
 use api::units::*;
 use crate::clip_scroll_tree::{CoordinateSystem, CoordinateSystemId, SpatialNodeIndex, TransformUpdateState};
 use euclid::SideOffsets2D;
-use crate::gpu_types::TransformPalette;
 use crate::scene::SceneProperties;
 use crate::util::{LayoutFastTransform, LayoutToWorldFastTransform, ScaleOffset, TransformedRectKind};
 
 #[derive(Clone, Debug)]
 pub enum SpatialNodeType {
     /// A special kind of node that adjusts its position based on the position
     /// of its parent node and a given set of sticky positioning offset bounds.
     /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
@@ -35,17 +34,17 @@ pub struct SpatialNode {
     /// our parent reference frame, plus any accumulated scrolling offsets from nodes
     /// between our reference frame and this node. For reference frames, we also include
     /// whatever local transformation this reference frame provides.
     pub world_viewport_transform: LayoutToWorldFastTransform,
 
     /// World transform for content transformed by this node.
     pub world_content_transform: LayoutToWorldFastTransform,
 
-    /// The current transform kind of world_content_transform.
+    /// The current transform kind of this node.
     pub transform_kind: TransformedRectKind,
 
     /// Pipeline that this layer belongs to
     pub pipeline_id: PipelineId,
 
     /// Parent layer. If this is None, we are the root node.
     pub parent: Option<SpatialNodeIndex>,
 
@@ -231,47 +230,32 @@ impl SpatialNode {
         state: &TransformUpdateState,
     ) {
         self.invertible = false;
         self.coordinate_system_id = state.current_coordinate_system_id;
         self.world_content_transform = LayoutToWorldFastTransform::identity();
         self.world_viewport_transform = LayoutToWorldFastTransform::identity();
     }
 
-    pub fn push_gpu_data(
-        &mut self,
-        transform_palette: &mut TransformPalette,
-        node_index: SpatialNodeIndex,
-    ) {
-        if self.invertible {
-            transform_palette.set_world_transform(
-                node_index,
-                self.world_content_transform
-                    .to_transform()
-                    .into_owned()
-            );
-        }
-    }
-
     pub fn update(
         &mut self,
         state: &mut TransformUpdateState,
         coord_systems: &mut Vec<CoordinateSystem>,
         scene_properties: &SceneProperties,
         previous_spatial_nodes: &[SpatialNode],
     ) {
         // If any of our parents was not rendered, we are not rendered either and can just
         // quit here.
         if !state.invertible {
             self.mark_uninvertible(state);
             return;
         }
 
         self.update_transform(state, coord_systems, scene_properties, previous_spatial_nodes);
-        self.transform_kind = self.world_content_transform.kind();
+        self.transform_kind = self.world_viewport_transform.kind();
 
         // If this node is a reference frame, we check if it has a non-invertible matrix.
         // For non-reference-frames we assume that they will produce only additional
         // translations which should be invertible.
         match self.node_type {
             SpatialNodeType::ReferenceFrame(info) if !info.invertible => {
                 self.mark_uninvertible(state);
                 return;
@@ -358,17 +342,20 @@ impl SpatialNode {
 
                         let transform = state.coordinate_system_relative_scale_offset
                             .to_transform()
                             .pre_mul(&relative_transform);
 
                         // Push that new coordinate system and record the new id.
                         let coord_system = CoordinateSystem {
                             transform,
-                            transform_style: info.transform_style,
+                            should_flatten: match (info.transform_style, info.kind) {
+                                (TransformStyle::Flat, ReferenceFrameKind::Transform) => true,
+                                (_, _) => false,
+                            },
                             parent: Some(state.current_coordinate_system_id),
                         };
                         state.current_coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
                         coord_systems.push(coord_system);
                     }
                 }
 
                 // Ensure that the current coordinate system ID is propagated to child
@@ -388,26 +375,27 @@ impl SpatialNode {
                 // provided by our own sticky positioning.
                 let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset;
                 self.world_viewport_transform = if accumulated_offset != LayoutVector2D::zero() {
                     state.parent_reference_frame_transform.pre_translate(&accumulated_offset)
                 } else {
                     state.parent_reference_frame_transform
                 };
 
-                // The transformation for any content inside of us is the viewport transformation, plus
-                // whatever scrolling offset we supply as well.
                 let scroll_offset = self.scroll_offset();
                 self.world_content_transform = if scroll_offset != LayoutVector2D::zero() {
                     self.world_viewport_transform.pre_translate(&scroll_offset)
                 } else {
                     self.world_viewport_transform
                 };
 
-                let added_offset = state.parent_accumulated_scroll_offset + sticky_offset + scroll_offset;
+
+                // The transformation for any content inside of us is the viewport transformation, plus
+                // whatever scrolling offset we supply as well.
+                let added_offset = accumulated_offset + self.scroll_offset();
                 self.coordinate_system_relative_scale_offset =
                     state.coordinate_system_relative_scale_offset.offset(added_offset.to_untyped());
 
                 if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
                     info.current_offset = sticky_offset;
                 }
 
                 self.coordinate_system_id = state.current_coordinate_system_id;
@@ -637,24 +625,16 @@ impl SpatialNode {
     }
 
     pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
         match self.node_type {
             SpatialNodeType::ScrollFrame(info) if info.external_id == Some(external_id) => true,
             _ => false,
         }
     }
-
-    pub fn transform_style(&self) -> TransformStyle {
-        match self.node_type {
-            SpatialNodeType::ReferenceFrame(ref info) => info.transform_style,
-            SpatialNodeType::StickyFrame(_) |
-            SpatialNodeType::ScrollFrame(_) => TransformStyle::Flat,
-        }
-    }
 }
 
 /// Defines whether we have an implicit scroll frame for a pipeline root,
 /// or an explicitly defined scroll frame from the display list.
 #[derive(Copy, Clone, Debug)]
 pub enum ScrollFrameKind {
     PipelineRoot,
     Explicit,
@@ -847,17 +827,17 @@ fn test_cst_perspective_relative_scroll(
         PropertyBinding::Value(transform),
         ReferenceFrameKind::Perspective {
             scrolling_relative_to: Some(ext_scroll_id),
         },
         LayoutVector2D::zero(),
         pipeline_id,
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     let scroll_offset = compute_offset_from(
         cst.spatial_nodes[ref_frame.0 as usize].parent,
         ext_scroll_id,
         &cst.spatial_nodes,
     );
 
     assert!(scroll_offset.x.approx_eq(&0.0));
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-twice-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+  items:
+    - type: rect
+      bounds: [100, 100, 200, 100]
+      color: green
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-twice.yaml
@@ -0,0 +1,21 @@
+# This test ensures that we flatten the "flat" style trasformations.
+# If the flattening doesn't happen here, the rect gets rotated back
+# to the original position.
+---
+root:
+  items:
+    -
+      bounds: [100, 100, 0, 0]
+      type: stacking-context
+      transform: rotate-x(45)
+      transform-origin: 0 0
+      items:
+        -
+          type: "stacking-context"
+          transform: rotate-x(-45)
+          transform-origin: 0 0
+          items:
+            -
+              bounds: [0, 0, 200, 200]
+              type: rect
+              color: green
--- a/gfx/wr/wrench/reftests/transforms/reftest.list
+++ b/gfx/wr/wrench/reftests/transforms/reftest.list
@@ -32,10 +32,11 @@ platform(linux,mac) fuzzy(9,348) == pers
 == snapped-preserve-3d.yaml snapped-preserve-3d-ref.yaml
 platform(linux,mac) fuzzy(1,122) == border-scale.yaml border-scale.png
 platform(linux,mac) fuzzy(1,16) == border-scale-2.yaml border-scale-2.png
 platform(linux,mac) fuzzy(1,69) == border-scale-3.yaml border-scale-3.png
 platform(linux,mac) fuzzy(1,74) == border-scale-4.yaml border-scale-4.png
 # Just make sure we aren't crashing here
 != large-raster-root.yaml blank.yaml
 == flatten-preserve-3d-root.yaml flatten-preserve-3d-root-ref.yaml
+== flatten-twice.yaml flatten-twice-ref.yaml
 == strange-w.yaml strange-w-ref.yaml
 == big-axis-aligned-scale.yaml big-axis-aligned-scale-ref.yaml
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -250,16 +250,34 @@ static bool GetBuildConfiguration(JSCont
   value = BooleanValue(true);
 #else
   value = BooleanValue(false);
 #endif
   if (!JS_SetProperty(cx, info, "arm64-simulator", value)) {
     return false;
   }
 
+#ifdef JS_CODEGEN_MIPS32
+  value = BooleanValue(true);
+#else
+  value = BooleanValue(false);
+#endif
+  if (!JS_SetProperty(cx, info, "mips32", value)) {
+    return false;
+  }
+
+#ifdef JS_CODEGEN_MIPS64
+  value = BooleanValue(true);
+#else
+  value = BooleanValue(false);
+#endif
+  if (!JS_SetProperty(cx, info, "mips64", value)) {
+    return false;
+  }
+
 #ifdef JS_SIMULATOR_MIPS32
   value = BooleanValue(true);
 #else
   value = BooleanValue(false);
 #endif
   if (!JS_SetProperty(cx, info, "mips32-simulator", value)) {
     return false;
   }
--- a/js/src/frontend/BytecodeEmitter.cpp
+++ b/js/src/frontend/BytecodeEmitter.cpp
@@ -7018,22 +7018,27 @@ bool BytecodeEmitter::emitSelfHostedResu
   if (!emit2(JSOP_RESUME, operand)) {
     return false;
   }
 
   return true;
 }
 
 bool BytecodeEmitter::emitSelfHostedForceInterpreter() {
+  // JSScript::hasForceInterpreterOp() relies on JSOP_FORCEINTERPRETER being the
+  // first bytecode op in the script.
+  MOZ_ASSERT(bytecodeSection().code().empty());
+
   if (!emit1(JSOP_FORCEINTERPRETER)) {
     return false;
   }
   if (!emit1(JSOP_UNDEFINED)) {
     return false;
   }
+
   return true;
 }
 
 bool BytecodeEmitter::emitSelfHostedAllowContentIter(BinaryNode* callNode) {
   ListNode* argsList = &callNode->right()->as<ListNode>();
 
   if (argsList->count() != 1) {
     reportNeedMoreArgsError(callNode, "allowContentIter", "1", "", argsList);
--- a/js/src/jit-test/tests/ion/bug1433496.js
+++ b/js/src/jit-test/tests/ion/bug1433496.js
@@ -1,6 +1,6 @@
-// |jit-test| --spectre-mitigations=on
+// |jit-test| --spectre-mitigations=on; skip-if: getBuildConfiguration()['mips32'] || getBuildConfiguration()['mips64']
 function f() {
     return arguments[arguments.length];
 }
 for (var i = 0; i < 10; i++)
     assertEq(f(), undefined);
--- a/js/src/jit/BaselineCompiler.cpp
+++ b/js/src/jit/BaselineCompiler.cpp
@@ -947,26 +947,16 @@ template <>
 void BaselineInterpreterCodeGen::pushUint16BytecodeOperandArg(
     Register scratch) {
   masm.loadPtr(frame.addressOfInterpreterPC(), scratch);
   LoadUint16Operand(masm, scratch, scratch);
   pushArg(scratch);
 }
 
 template <>
-void BaselineCompilerCodeGen::loadResumeIndexBytecodeOperand(Register dest) {
-  masm.move32(Imm32(GET_RESUMEINDEX(handler.pc())), dest);
-}
-
-template <>
-void BaselineInterpreterCodeGen::loadResumeIndexBytecodeOperand(Register dest) {
-  MOZ_CRASH("NYI: interpreter loadResumeIndexBytecodeOperand");
-}
-
-template <>
 void BaselineCompilerCodeGen::loadInt32LengthBytecodeOperand(Register dest) {
   uint32_t length = GET_UINT32(handler.pc());
   MOZ_ASSERT(length <= INT32_MAX,
              "the bytecode emitter must fail to compile code that would "
              "produce a length exceeding int32_t range");
   masm.move32(Imm32(AssertedCast<int32_t>(length)), dest);
 }
 
@@ -1395,17 +1385,18 @@ bool BaselineCompiler::emitDebugTrap() {
   // Flush any pending constant pools to prevent incorrect
   // PCMappingEntry offsets. See Bug 1446819.
   masm.flush();
   // Fix up the PCMappingEntry to avoid any constant pool.
   pcMappingEntries_.back().nativeOffset = masm.currentOffset();
 #endif
 
   // Emit patchable call to debug trap handler.
-  JitCode* handlerCode = cx->runtime()->jitRuntime()->debugTrapHandler(cx);
+  JitCode* handlerCode = cx->runtime()->jitRuntime()->debugTrapHandler(
+      cx, DebugTrapHandlerKind::Compiler);
   if (!handlerCode) {
     return false;
   }
   mozilla::DebugOnly<CodeOffset> offset =
       masm.toggledCall(handlerCode, enabled);
 
 #ifdef DEBUG
   // Patchable call offset has to match the pc mapping offset.
@@ -5608,22 +5599,26 @@ template <typename Handler>
 bool BaselineCodeGen<Handler>::emit_JSOP_YIELD() {
   // Store generator in R0.
   frame.popRegsAndSync(1);
 
   Register genObj = R2.scratchReg();
   masm.unboxObject(R0, genObj);
 
   if (frame.hasKnownStackDepth(1)) {
-    // If the expression stack is empty, we can inline the YIELD.
+    // If the expression stack is empty, we can inline the YIELD. Note that this
+    // branch is never taken for the interpreter because it doesn't know static
+    // stack depths.
 
     Register temp = R1.scratchReg();
     Address resumeIndexSlot(genObj,
                             AbstractGeneratorObject::offsetOfResumeIndexSlot());
-    loadResumeIndexBytecodeOperand(temp);
+    jsbytecode* pc = handler.maybePC();
+    MOZ_ASSERT(pc, "compiler-only code never has a null pc");
+    masm.move32(Imm32(GET_RESUMEINDEX(pc)), temp);
     masm.storeValue(JSVAL_TYPE_INT32, temp, resumeIndexSlot);
 
     Register envObj = R0.scratchReg();
     Address envChainSlot(
         genObj, AbstractGeneratorObject::offsetOfEnvironmentChainSlot());
     masm.loadPtr(frame.addressOfEnvironmentChain(), envObj);
     masm.guardedCallPreBarrier(envChainSlot, MIRType::Value);
     masm.storeValue(JSVAL_TYPE_OBJECT, envObj, envChainSlot);
@@ -5772,23 +5767,27 @@ bool BaselineCodeGen<Handler>::emitGener
   Register callee = regs.takeAny();
   masm.unboxObject(
       Address(genObj, AbstractGeneratorObject::offsetOfCalleeSlot()), callee);
 
   // Load the return value.
   ValueOperand retVal = regs.takeAnyValue();
   masm.loadValue(frame.addressOfStackValue(-1), retVal);
 
-  // Branch to interpret if the script does not have a BaselineScript (if the
-  // Baseline Interpreter is not enabled). Note that we don't relazify generator
-  // scripts, so the function is guaranteed to be non-lazy.
+  // Branch to interpret if the script does not have a TypeScript or
+  // BaselineScript (depending on whether the Baseline Interpreter is enabled).
+  // Note that we don't relazify generator scripts, so the function is
+  // guaranteed to be non-lazy.
   Label interpret;
   Register scratch1 = regs.takeAny();
   masm.loadPtr(Address(callee, JSFunction::offsetOfScript()), scratch1);
-  if (!JitOptions.baselineInterpreter) {
+  if (JitOptions.baselineInterpreter) {
+    Address typesAddr(scratch1, JSScript::offsetOfTypes());
+    masm.branchPtr(Assembler::Equal, typesAddr, ImmPtr(nullptr), &interpret);
+  } else {
     Address baselineAddr(scratch1, JSScript::offsetOfBaselineScript());
     masm.branchPtr(Assembler::BelowOrEqual, baselineAddr,
                    ImmPtr(BASELINE_DISABLED_SCRIPT), &interpret);
   }
 
 #ifdef JS_TRACE_LOGGING
   if (JS::TraceLoggerSupported() && !emitTraceLoggerResume(scratch1, regs)) {
     return false;
@@ -5994,39 +5993,38 @@ bool BaselineCodeGen<Handler>::emitGener
 #endif
     masm.jump(code);
 
     // Pop arguments and frame pointer (pushed by prepareVMCall) from
     // framePushed.
     masm.implicitPop((fun.explicitStackSlots() + 1) * sizeof(void*));
   }
 
-  // If the generator script has no JIT code, call into the VM.
-  if (interpret.used()) {
-    masm.bind(&interpret);
-
-    prepareVMCall();
-    if (resumeKind == GeneratorResumeKind::Next) {
-      pushArg(ImmGCPtr(cx->names().next));
-    } else if (resumeKind == GeneratorResumeKind::Throw) {
-      pushArg(ImmGCPtr(cx->names().throw_));
-    } else {
-      MOZ_ASSERT(resumeKind == GeneratorResumeKind::Return);
-      pushArg(ImmGCPtr(cx->names().return_));
-    }
-
-    masm.loadValue(frame.addressOfStackValue(-1), retVal);
-    pushArg(retVal);
-    pushArg(genObj);
-
-    using Fn = bool (*)(JSContext*, HandleObject, HandleValue,
-                        HandlePropertyName, MutableHandleValue);
-    if (!callVM<Fn, jit::InterpretResume>()) {
-      return false;
-    }
+  // Call into the VM to run in the C++ interpreter if there's no TypeScript or
+  // BaselineScript.
+  masm.bind(&interpret);
+
+  prepareVMCall();
+  if (resumeKind == GeneratorResumeKind::Next) {
+    pushArg(ImmGCPtr(cx->names().next));
+  } else if (resumeKind == GeneratorResumeKind::Throw) {
+    pushArg(ImmGCPtr(cx->names().throw_));
+  } else {
+    MOZ_ASSERT(resumeKind == GeneratorResumeKind::Return);
+    pushArg(ImmGCPtr(cx->names().return_));
+  }
+
+  masm.loadValue(frame.addressOfStackValue(-1), retVal);
+  pushArg(retVal);
+  pushArg(genObj);
+
+  using Fn = bool (*)(JSContext*, HandleObject, HandleValue, HandlePropertyName,
+                      MutableHandleValue);
+  if (!callVM<Fn, jit::InterpretResume>()) {
+    return false;
   }
 
   // After the generator returns, we restore the stack pointer, switch back to
   // the current realm, push the return value, and we're done.
   masm.bind(&returnTarget);
   masm.computeEffectiveAddress(frame.addressOfStackValue(-1),
                                masm.getStackPointer());
   if (JSScript* script = handler.maybeScript()) {
@@ -6563,27 +6561,22 @@ MethodStatus BaselineCompiler::emitBody(
     }
 
     // Emit traps for breakpoints and step mode.
     if (MOZ_UNLIKELY(compileDebugInstrumentation()) && !emitDebugTrap()) {
       return Method_Error;
     }
 
     switch (op) {
-      // ===== NOT Yet Implemented =====
       case JSOP_FORCEINTERPRETER:
-        // Intentionally not implemented.
+        // Caller must have checked script->hasForceInterpreterOp().
       case JSOP_UNUSED71:
       case JSOP_UNUSED149:
       case JSOP_LIMIT:
-        // === !! WARNING WARNING WARNING !! ===
-        // DO NOT add new ops to this list! All bytecode ops MUST have Baseline
-        // support. Follow-up bugs are not acceptable.
-        JitSpew(JitSpew_BaselineAbort, "Unhandled op: %s", CodeName[op]);
-        return Method_CantCompile;
+        MOZ_CRASH("Unexpected op");
 
 #define EMIT_OP(OP)                                            \
   case OP:                                                     \
     if (MOZ_UNLIKELY(!this->emit_##OP())) return Method_Error; \
     break;
         OPCODE_LIST(EMIT_OP)
 #undef EMIT_OP
     }
@@ -6610,26 +6603,112 @@ MethodStatus BaselineCompiler::emitBody(
     prevpc = handler.pc();
 #endif
   }
 
   MOZ_ASSERT(JSOp(*prevpc) == JSOP_RETRVAL);
   return Method_Compiled;
 }
 
-JitCode* JitRuntime::generateDebugTrapHandler(JSContext* cx) {
+bool BaselineInterpreterGenerator::emitDebugTrap() {
+  JitRuntime* jrt = cx->runtime()->jitRuntime();
+
+  JitCode* handlerCode =
+      jrt->debugTrapHandler(cx, DebugTrapHandlerKind::Interpreter);
+  if (!handlerCode) {
+    return false;
+  }
+
+  CodeOffset offset = masm.toggledCall(handlerCode, /* enabled = */ false);
+  if (!debugTrapOffsets_.append(offset.offset())) {
+    ReportOutOfMemory(cx);
+    return false;
+  }
+
+  return true;
+}
+
+bool BaselineInterpreterGenerator::emitInterpreterLoop() {
+  MOZ_CRASH("NYI: interpreter emitInterpreterLoop");
+
+  return true;
+}
+
+bool BaselineInterpreterGenerator::generate(BaselineInterpreter& interpreter) {
+  if (!emitPrologue()) {
+    return false;
+  }
+
+  if (!emitInterpreterLoop()) {
+    return false;
+  }
+
+  if (!emitEpilogue()) {
+    return false;
+  }
+
+  if (!emitOutOfLinePostBarrierSlot()) {
+    return false;
+  }
+
+  Linker linker(masm, "BaselineInterpreter");
+  if (masm.oom()) {
+    ReportOutOfMemory(cx);
+    return false;
+  }
+
+  JitCode* code = linker.newCode(cx, CodeKind::Other);
+  if (!code) {
+    return false;
+  }
+
+#ifdef JS_ION_PERF
+  writePerfSpewerJitCodeProfile(code, "BaselineInterpreter");
+#endif
+
+#ifdef MOZ_VTUNE
+  vtune::MarkStub(code, "BaselineInterpreter");
+#endif
+
+  interpreter.init(
+      code, interpretOpOffset_, profilerEnterFrameToggleOffset_.offset(),
+      profilerExitFrameToggleOffset_.offset(),
+      handler.debuggeeCheckOffset().offset(), std::move(debugTrapOffsets_));
+
+  return true;
+}
+
+JitCode* JitRuntime::generateDebugTrapHandler(JSContext* cx,
+                                              DebugTrapHandlerKind kind) {
   StackMacroAssembler masm;
 
   AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All());
   regs.takeUnchecked(BaselineFrameReg);
   regs.takeUnchecked(ICStubReg);
+  regs.takeUnchecked(PCRegAtStart);
   Register scratch1 = regs.takeAny();
   Register scratch2 = regs.takeAny();
   Register scratch3 = regs.takeAny();
 
+  if (kind == DebugTrapHandlerKind::Interpreter) {
+    // The interpreter calls this for every script when debugging, so check if
+    // the script has any breakpoints or is in step mode before calling into
+    // C++.
+    Label hasDebugScript;
+    Address scriptAddr(BaselineFrameReg,
+                       BaselineFrame::reverseOffsetOfInterpreterScript());
+    masm.loadPtr(scriptAddr, scratch1);
+    masm.branchTest32(Assembler::NonZero,
+                      Address(scratch1, JSScript::offsetOfMutableFlags()),
+                      Imm32(int32_t(JSScript::MutableFlags::HasDebugScript)),
+                      &hasDebugScript);
+    masm.abiret();
+    masm.bind(&hasDebugScript);
+  }
+
   // Load the return address in scratch1.
   masm.loadAbiReturnAddress(scratch1);
 
   // Load BaselineFrame pointer in scratch2.
   masm.loadBaselineFramePtr(BaselineFrameReg, scratch2);
 
   // Enter a stub frame and call the HandleDebugTrap VM function. Ensure
   // the stub frame has a nullptr ICStub pointer, since this pointer is marked
@@ -6647,16 +6726,23 @@ JitCode* JitRuntime::generateDebugTrapHa
 
   EmitBaselineLeaveStubFrame(masm);
 
   // If the stub returns |true|, we have to perform a forced return
   // (return from the JS frame). If the stub returns |false|, just return
   // from the trap stub so that execution continues at the current pc.
   Label forcedReturn;
   masm.branchIfTrueBool(ReturnReg, &forcedReturn);
+
+  if (kind == DebugTrapHandlerKind::Interpreter) {
+    // We have to reload the bytecode pc register.
+    Address pcAddr(BaselineFrameReg,
+                   BaselineFrame::reverseOffsetOfInterpreterPC());
+    masm.loadPtr(pcAddr, PCRegAtStart);
+  }
   masm.abiret();
 
   masm.bind(&forcedReturn);
   masm.loadValue(
       Address(BaselineFrameReg, BaselineFrame::reverseOffsetOfReturnValue()),
       JSReturnOperand);
   masm.moveToStackPtr(BaselineFrameReg);
   masm.pop(BaselineFrameReg);
@@ -6686,13 +6772,10 @@ JitCode* JitRuntime::generateDebugTrapHa
 #endif
 #ifdef MOZ_VTUNE
   vtune::MarkStub(handlerCode, "DebugTrapHandler");
 #endif
 
   return handlerCode;
 }
 
-// Instantiate explicitly for now to make sure it compiles.
-template class jit::BaselineCodeGen<BaselineInterpreterHandler>;
-
 }  // namespace jit
 }  // namespace js
--- a/js/src/jit/BaselineCompiler.h
+++ b/js/src/jit/BaselineCompiler.h
@@ -322,17 +322,16 @@ class BaselineCodeGen {
   void pushScriptObjectArg(ScriptObjectType type);
   void pushScriptNameArg(Register scratch1, Register scratch2);
   void pushScriptScopeArg();
 
   // Pushes a bytecode operand as argument for a VM function.
   void pushUint8BytecodeOperandArg(Register scratch);
   void pushUint16BytecodeOperandArg(Register scratch);
 
-  void loadResumeIndexBytecodeOperand(Register dest);
   void loadInt32LengthBytecodeOperand(Register dest);
   void loadInt32IndexBytecodeOperand(ValueOperand dest);
   void loadNumFormalArguments(Register dest);
 
   // Loads the current JSScript* in dest.
   void loadScript(Register dest);
 
   // Subtracts |script->nslots() * sizeof(Value)| from reg.
@@ -692,16 +691,28 @@ class BaselineInterpreterHandler {
   bool needsEarlyStackCheck() const { return true; }
 
   JSObject* maybeNoCloneSingletonObject() { return nullptr; }
 };
 
 using BaselineInterpreterCodeGen = BaselineCodeGen<BaselineInterpreterHandler>;
 
 class BaselineInterpreterGenerator final : private BaselineInterpreterCodeGen {
+  // Offsets of patchable call instructions for debugger breakpoints/stepping.
+  js::Vector<uint32_t, 0, SystemAllocPolicy> debugTrapOffsets_;
+
+  // Offset of the code to start interpreting a bytecode op.
+  uint32_t interpretOpOffset_ = 0;
+
  public:
   explicit BaselineInterpreterGenerator(JSContext* cx);
+
+  MOZ_MUST_USE bool generate(BaselineInterpreter& interpreter);
+
+ private:
+  MOZ_MUST_USE bool emitInterpreterLoop();
+  MOZ_MUST_USE bool emitDebugTrap();
 };
 
 }  // namespace jit
 }  // namespace js
 
 #endif /* jit_BaselineCompiler_h */
--- a/js/src/jit/BaselineJIT.cpp
+++ b/js/src/jit/BaselineJIT.cpp
@@ -294,16 +294,20 @@ static MethodStatus CanEnterBaselineJIT(
   if (!CanLikelyAllocateMoreExecutableMemory()) {
     return Method_Skipped;
   }
 
   if (!cx->realm()->ensureJitRealmExists(cx)) {
     return Method_Error;
   }
 
+  if (script->hasForceInterpreterOp()) {
+    return Method_CantCompile;
+  }
+
   // Frames can be marked as debuggee frames independently of its underlying
   // script being a debuggee script, e.g., when performing
   // Debugger.Frame.prototype.eval.
   bool forceDebugInstrumentation =
       osrSourceFrame && osrSourceFrame.isDebuggee();
   return BaselineCompile(cx, script, forceDebugInstrumentation);
 }
 
@@ -311,16 +315,20 @@ static MethodStatus CanEnterBaselineInte
                                                 HandleScript script) {
   MOZ_ASSERT(jit::IsBaselineEnabled(cx));
   MOZ_ASSERT(JitOptions.baselineInterpreter);
 
   if (script->types()) {
     return Method_Compiled;
   }
 
+  if (script->hasForceInterpreterOp()) {
+    return Method_CantCompile;
+  }
+
   // Check script warm-up counter.
   if (script->getWarmUpCount() <=
       JitOptions.baselineInterpreterWarmUpThreshold) {
     return Method_Skipped;
   }
 
   if (!cx->realm()->ensureJitRealmExists(cx)) {
     return Method_Error;
@@ -1426,13 +1434,14 @@ void BaselineInterpreter::init(JitCode* 
   debuggeeCheckOffset_ = debuggeeCheckOffset;
   debugTrapOffsets_ = std::move(debugTrapOffsets);
 }
 
 bool jit::GenerateBaselineInterpreter(JSContext* cx,
                                       BaselineInterpreter& interpreter) {
   // Temporary JitOptions check to prevent crashes for now.
   if (JitOptions.baselineInterpreter) {
-    MOZ_CRASH("NYI: GenerateBaselineInterpreter");
+    BaselineInterpreterGenerator generator(cx);
+    return generator.generate(interpreter);
   }
 
   return true;
 }
--- a/js/src/jit/Ion.cpp
+++ b/js/src/jit/Ion.cpp
@@ -159,17 +159,17 @@ JitRuntime::JitRuntime()
       enterJITOffset_(0),
       bailoutHandlerOffset_(0),
       argumentsRectifierOffset_(0),
       argumentsRectifierReturnOffset_(0),
       invalidatorOffset_(0),
       lazyLinkStubOffset_(0),
       interpreterStubOffset_(0),
       doubleToInt32ValueStubOffset_(0),
-      debugTrapHandler_(nullptr),
+      debugTrapHandlers_(),
       baselineDebugModeOSRHandler_(nullptr),
       baselineInterpreter_(),
       trampolineCode_(nullptr),
       jitcodeGlobalTable_(nullptr),
 #ifdef DEBUG
       ionBailAfter_(0),
 #endif
       numFinishedBuilders_(0),
@@ -325,24 +325,28 @@ bool JitRuntime::generateTrampolines(JSC
 #endif
 #ifdef MOZ_VTUNE
   vtune::MarkStub(trampolineCode_, "Trampolines");
 #endif
 
   return true;
 }
 
-JitCode* JitRuntime::debugTrapHandler(JSContext* cx) {
-  if (!debugTrapHandler_) {
+JitCode* JitRuntime::debugTrapHandler(JSContext* cx,
+                                      DebugTrapHandlerKind kind) {
+  if (!debugTrapHandlers_[kind]) {
     // JitRuntime code stubs are shared across compartments and have to
     // be allocated in the atoms zone.
-    AutoAllocInAtomsZone az(cx);
-    debugTrapHandler_ = generateDebugTrapHandler(cx);
+    mozilla::Maybe<AutoAllocInAtomsZone> az;
+    if (!cx->zone()->isAtomsZone()) {
+      az.emplace(cx);
+    }
+    debugTrapHandlers_[kind] = generateDebugTrapHandler(cx, kind);
   }
-  return debugTrapHandler_;
+  return debugTrapHandlers_[kind];
 }
 
 JitRuntime::IonBuilderList& JitRuntime::ionLazyLinkList(JSRuntime* rt) {
   MOZ_ASSERT(CurrentThreadCanAccessRuntime(rt),
              "Should only be mutated by the main thread.");
   return ionLazyLinkList_.ref();
 }
 
--- a/js/src/jit/JitRealm.h
+++ b/js/src/jit/JitRealm.h
@@ -114,16 +114,18 @@ class BaselineICFallbackCode {
   TrampolinePtr addr(BaselineICFallbackKind kind) const {
     return TrampolinePtr(code_->raw() + offsets_[kind]);
   }
   uint8_t* bailoutReturnAddr(BailoutReturnKind kind) const {
     return code_->raw() + bailoutReturnOffsets_[kind];
   }
 };
 
+enum class DebugTrapHandlerKind { Interpreter, Compiler, Count };
+
 typedef void (*EnterJitCode)(void* code, unsigned argc, Value* argv,
                              InterpreterFrame* fp, CalleeToken calleeToken,
                              JSObject* envChain, size_t numStackValues,
                              Value* vp);
 
 class JitcodeGlobalTable;
 
 class JitRuntime {
@@ -185,17 +187,19 @@ class JitRuntime {
   // Thunk to enter the interpreter from JIT code.
   WriteOnceData<uint32_t> interpreterStubOffset_;
 
   // Thunk to convert the value in R0 to int32 if it's a double.
   // Note: this stub treats -0 as +0 and may clobber R1.scratchReg().
   WriteOnceData<uint32_t> doubleToInt32ValueStubOffset_;
 
   // Thunk used by the debugger for breakpoint and step mode.
-  WriteOnceData<JitCode*> debugTrapHandler_;
+  mozilla::EnumeratedArray<DebugTrapHandlerKind, DebugTrapHandlerKind::Count,
+                           WriteOnceData<JitCode*>>
+      debugTrapHandlers_;
 
   // Thunk used to fix up on-stack recompile of baseline scripts.
   WriteOnceData<JitCode*> baselineDebugModeOSRHandler_;
   WriteOnceData<void*> baselineDebugModeOSRHandlerNoFrameRegPopAddr_;
 
   // BaselineInterpreter state.
   BaselineInterpreter baselineInterpreter_;
 
@@ -259,17 +263,17 @@ class JitRuntime {
   BailoutTable generateBailoutTable(MacroAssembler& masm, Label* bailoutTail,
                                     uint32_t frameClass);
   void generateBailoutHandler(MacroAssembler& masm, Label* bailoutTail);
   void generateInvalidator(MacroAssembler& masm, Label* bailoutTail);
   uint32_t generatePreBarrier(JSContext* cx, MacroAssembler& masm,
                               MIRType type);
   void generateMallocStub(MacroAssembler& masm);
   void generateFreeStub(MacroAssembler& masm);
-  JitCode* generateDebugTrapHandler(JSContext* cx);
+  JitCode* generateDebugTrapHandler(JSContext* cx, DebugTrapHandlerKind kind);
   JitCode* generateBaselineDebugModeOSRHandler(
       JSContext* cx, uint32_t* noFrameRegPopOffsetOut);
 
   bool generateVMWrapper(JSContext* cx, MacroAssembler& masm,
                          const VMFunctionData& f, void* nativeFun,
                          uint32_t* wrapperOffset);
 
   template <typename IdT>
@@ -321,17 +325,17 @@ class JitRuntime {
     MOZ_ASSERT(trampolineCode_);
     return trampolineCode(functionWrapperOffsets_[size_t(funId)]);
   }
   TrampolinePtr getVMWrapper(TailCallVMFunctionId funId) const {
     MOZ_ASSERT(trampolineCode_);
     return trampolineCode(tailCallFunctionWrapperOffsets_[size_t(funId)]);
   }
 
-  JitCode* debugTrapHandler(JSContext* cx);
+  JitCode* debugTrapHandler(JSContext* cx, DebugTrapHandlerKind kind);
   JitCode* getBaselineDebugModeOSRHandler(JSContext* cx);
   void* getBaselineDebugModeOSRHandlerAddress(JSContext* cx, bool popFrameReg);
 
   BaselineInterpreter& baselineInterpreter() { return baselineInterpreter_; }
 
   TrampolinePtr getGenericBailoutHandler() const {
     return trampolineCode(bailoutHandlerOffset_);
   }
--- a/js/src/jit/VMFunctions.cpp
+++ b/js/src/jit/VMFunctions.cpp
@@ -22,16 +22,17 @@
 #include "vm/SelfHosting.h"
 #include "vm/TraceLogging.h"
 
 #include "jit/BaselineFrame-inl.h"
 #include "jit/JitFrames-inl.h"
 #include "jit/VMFunctionList-inl.h"
 #include "vm/Debugger-inl.h"
 #include "vm/Interpreter-inl.h"
+#include "vm/JSScript-inl.h"
 #include "vm/NativeObject-inl.h"
 #include "vm/StringObject-inl.h"
 #include "vm/TypeInference-inl.h"
 
 using namespace js;
 using namespace js::jit;
 
 namespace js {
@@ -1047,16 +1048,24 @@ bool GlobalNameConflictsCheckFromIon(JSC
 }
 
 bool InitFunctionEnvironmentObjects(JSContext* cx, BaselineFrame* frame) {
   return frame->initFunctionEnvironmentObjects(cx);
 }
 
 bool NewArgumentsObject(JSContext* cx, BaselineFrame* frame,
                         MutableHandleValue res) {
+  // BaselineCompiler calls ensureHasAnalyzedArgsUsage at compile time. The
+  // interpreters have to do this as part of JSOP_ARGUMENTS.
+  if (frame->runningInInterpreter()) {
+    if (!frame->script()->ensureHasAnalyzedArgsUsage(cx)) {
+      return false;
+    }
+  }
+
   ArgumentsObject* obj = ArgumentsObject::createExpected(cx, frame);
   if (!obj) {
     return false;
   }
   res.setObject(*obj);
   return true;
 }
 
@@ -1237,17 +1246,18 @@ bool RecreateLexicalEnv(JSContext* cx, B
 
 bool DebugLeaveThenRecreateLexicalEnv(JSContext* cx, BaselineFrame* frame,
                                       jsbytecode* pc) {
   MOZ_ALWAYS_TRUE(DebugLeaveLexicalEnv(cx, frame, pc));
   return frame->recreateLexicalEnvironment(cx);
 }
 
 bool DebugLeaveLexicalEnv(JSContext* cx, BaselineFrame* frame, jsbytecode* pc) {
-  MOZ_ASSERT(frame->script()->baselineScript()->hasDebugInstrumentation());
+  MOZ_ASSERT_IF(!frame->runningInInterpreter(),
+                frame->script()->baselineScript()->hasDebugInstrumentation());
   if (cx->realm()->isDebuggee()) {
     DebugEnvironments::onPopLexical(cx, frame, pc);
   }
   return true;
 }
 
 bool PushVarEnv(JSContext* cx, BaselineFrame* frame, HandleScope scope) {
   return frame->pushVarEnvironment(cx, scope);
--- a/js/src/jit/arm/Architecture-arm.h
+++ b/js/src/jit/arm/Architecture-arm.h
@@ -159,17 +159,16 @@ class Registers {
   }
   static uint32_t LastBit(SetType x) {
     return 31 - mozilla::CountLeadingZeroes32(x);
   }
 };
 
 // Smallest integer type that can hold a register bitmask.
 typedef uint16_t PackedRegisterMask;
-typedef uint16_t PackedRegisterMask;
 
 class FloatRegisters {
  public:
   enum FPRegisterID {
     s0,
     s1,
     s2,
     s3,
--- a/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h
+++ b/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h
@@ -782,28 +782,37 @@ void MacroAssembler::branchTestMagic(Con
 
 void MacroAssembler::branchToComputedAddress(const BaseIndex& addr) {
   loadPtr(addr, ScratchRegister);
   branch(ScratchRegister);
 }
 
 void MacroAssembler::cmp32Move32(Condition cond, Register lhs, Register rhs,
                                  Register src, Register dest) {
-  MOZ_CRASH();
+  Register scratch = ScratchRegister;
+  MOZ_ASSERT(src != scratch && dest != scratch);
+  cmp32Set(cond, lhs, rhs, scratch);
+  as_movn(dest, src, scratch);
 }
 
 void MacroAssembler::cmp32MovePtr(Condition cond, Register lhs, Imm32 rhs,
                                   Register src, Register dest) {
-  MOZ_CRASH();
+  Register scratch = ScratchRegister;
+  MOZ_ASSERT(src != scratch && dest != scratch);
+  cmp32Set(cond, lhs, rhs, scratch);
+  as_movn(dest, src, scratch);
 }
 
 void MacroAssembler::cmp32Move32(Condition cond, Register lhs,
                                  const Address& rhs, Register src,
                                  Register dest) {
-  MOZ_CRASH();
+  SecondScratchRegisterScope scratch2(*this);
+  MOZ_ASSERT(lhs != scratch2 && src != scratch2 && dest != scratch2);
+  load32(rhs, scratch2);
+  cmp32Move32(cond, lhs, scratch2, src, dest);
 }
 
 void MacroAssembler::cmp32Load32(Condition cond, Register lhs,
                                  const Address& rhs, const Address& src,
                                  Register dest) {
   // This is never used, but must be present to facilitate linking on mips(64).
   MOZ_CRASH("No known use cases");
 }
--- a/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp
+++ b/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp
@@ -491,29 +491,29 @@ void MacroAssemblerMIPSShared::ma_load_u
     base = ScratchRegister;
     lowOffset = Imm16(0).encode();
     hiOffset = Imm16(size / 8 - 1).encode();
   }
 
   BufferOffset load;
   switch (size) {
     case SizeHalfWord:
-      if (extension != ZeroExtend) {
+      if (extension == ZeroExtend) {
         load = as_lbu(temp, base, hiOffset);
       } else {
         load = as_lb(temp, base, hiOffset);
       }
       as_lbu(dest, base, lowOffset);
       ma_ins(dest, temp, 8, 24);
       break;
     case SizeWord:
       load = as_lwl(dest, base, hiOffset);
       as_lwr(dest, base, lowOffset);
 #ifdef JS_CODEGEN_MIPS64
-      if (extension != ZeroExtend) {
+      if (extension == ZeroExtend) {
         as_dext(dest, dest, 0, 32);
       }
 #endif
       break;
 #ifdef JS_CODEGEN_MIPS64
     case SizeDouble:
       load = as_ldl(dest, base, hiOffset);
       as_ldr(dest, base, lowOffset);
--- a/js/src/jit/mips64/CodeGenerator-mips64.cpp
+++ b/js/src/jit/mips64/CodeGenerator-mips64.cpp
@@ -75,16 +75,17 @@ void CodeGenerator::visitUnbox(LUnbox* u
       case MIRType::String:
         masm.unboxString(inputReg, result);
         break;
       case MIRType::Symbol:
         masm.unboxSymbol(inputReg, result);
         break;
       case MIRType::BigInt:
         masm.unboxBigInt(inputReg, result);
+        break;
       default:
         MOZ_CRASH("Given MIRType cannot be unboxed.");
     }
     return;
   }
 
   Address inputAddr = ToAddress(input);
   switch (mir->type()) {
@@ -100,16 +101,17 @@ void CodeGenerator::visitUnbox(LUnbox* u
     case MIRType::String:
       masm.unboxString(inputAddr, result);
       break;
     case MIRType::Symbol:
       masm.unboxSymbol(inputAddr, result);
       break;
     case MIRType::BigInt:
       masm.unboxBigInt(inputAddr, result);
+      break;
     default:
       MOZ_CRASH("Given MIRType cannot be unboxed.");
   }
 }
 
 void CodeGeneratorMIPS64::splitTagForTest(const ValueOperand& value,
                                           ScratchTagScope& tag) {
   masm.splitTag(value.valueReg(), tag);
--- a/js/src/vm/JSScript.h
+++ b/js/src/vm/JSScript.h
@@ -2096,16 +2096,22 @@ class JSScript : public js::gc::TenuredC
   // Script bytecode is immutable after creation.
   jsbytecode* code() const {
     if (!scriptData_) {
       return nullptr;
     }
     return scriptData_->code();
   }
 
+  bool hasForceInterpreterOp() const {
+    // JSOP_FORCEINTERPRETER, if present, must be the first op.
+    MOZ_ASSERT(length() >= 1);
+    return JSOp(*code()) == JSOP_FORCEINTERPRETER;
+  }
+
   js::AllBytecodesIterable allLocations() {
     return js::AllBytecodesIterable(this);
   }
 
   js::BytecodeLocation location() { return js::BytecodeLocation(this, code()); }
 
   bool isUncompleted() const {
     // code() becomes non-null only if this script is complete.
--- a/js/src/vm/Opcodes.h
+++ b/js/src/vm/Opcodes.h
@@ -2232,17 +2232,18 @@
      *   Category: Variables and Scopes
      *   Type: Arguments
      *   Operands: uint8_t numHops
      *   Stack: => callee
      */ \
     MACRO(JSOP_ENVCALLEE, 206, "envcallee", NULL, 2, 0, 1, JOF_UINT8) \
     /*
      * No-op bytecode only emitted in some self-hosted functions. Not handled
-     * by the JITs so the script always runs in the interpreter.
+     * by the JITs or Baseline Interpreter so the script always runs in the C++
+     * interpreter.
      *
      *   Category: Other
      *   Operands:
      *   Stack: =>
      */ \
     MACRO(JSOP_FORCEINTERPRETER, 207, "forceinterpreter", NULL, 1, 0, 0, JOF_BYTE) \
     /*
      * Bytecode emitted after 'yield' expressions. This is useful for the
--- a/js/src/vm/TypeInference.cpp
+++ b/js/src/vm/TypeInference.cpp
@@ -3486,16 +3486,20 @@ TypeScript::TypeScript(JSScript* script,
 
   FillBytecodeTypeMap(script, bytecodeTypeMap());
 }
 
 bool JSScript::makeTypes(JSContext* cx) {
   MOZ_ASSERT(!types_);
   cx->check(this);
 
+  // Scripts that will never run in the Baseline Interpreter or the JITs don't
+  // need a TypeScript.
+  MOZ_ASSERT(!hasForceInterpreterOp());
+
   AutoEnterAnalysis enter(cx);
 
   UniquePtr<jit::ICScript> icScript(jit::ICScript::create(cx, this));
   if (!icScript) {
     return false;
   }
 
   // We need to call prepareForDestruction on ICScript before we |delete| it.
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -40,16 +40,17 @@
 
 #include "gfxContext.h"
 #include "gfxPrefs.h"
 #include "gfxUserFontSet.h"
 #include "nsContentList.h"
 #include "nsPresContext.h"
 #include "nsIContent.h"
 #include "mozilla/dom/BrowserBridgeChild.h"
+#include "mozilla/dom/BrowsingContext.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/PointerEventHandler.h"
 #include "mozilla/dom/PopupBlocker.h"
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/DocumentInlines.h"
 #include "nsAnimationManager.h"
 #include "nsNameSpaceManager.h"  // for Pref-related rule management (bugs 22963,20760,31816)
 #include "nsFrame.h"
@@ -816,16 +817,17 @@ PresShell::PresShell()
       mResizeEventPending(false),
       mFontSizeInflationForceEnabled(false),
       mFontSizeInflationDisabledInMasterProcess(false),
       mFontSizeInflationEnabled(false),
       mPaintingIsFrozen(false),
       mIsNeverPainting(false),
       mResolutionUpdated(false),
       mResolutionUpdatedByApz(false),
+      mUnderHiddenEmbedderElement(false),
       mDocumentLoading(false),
       mNoDelayedMouseEvents(false),
       mNoDelayedKeyEvents(false),
       mApproximateFrameVisibilityVisited(false),
       mNextPaintCompressed(false),
       mHasCSSBackgroundColor(true),
       mIsLastChromeOnlyEscapeKeyConsumed(false),
       mHasReceivedPaintMessage(false),
@@ -1050,16 +1052,36 @@ void PresShell::Init(Document* aDocument
 
   if (mPresContext->IsRootContentDocument()) {
     mZoomConstraintsClient = new ZoomConstraintsClient();
     mZoomConstraintsClient->Init(this, mDocument);
 
     // We call this to create mMobileViewportManager, if it is needed.
     UpdateViewportOverridden(false);
   }
+
+  if (nsCOMPtr<nsIDocShell> docShell = mPresContext->GetDocShell()) {
+    BrowsingContext* bc = nsDocShell::Cast(docShell)->GetBrowsingContext();
+    bool embedderFrameIsHidden = true;
+    if (Element* embedderElement = bc->GetEmbedderElement()) {
+      if (auto embedderFrame = embedderElement->GetPrimaryFrame()) {
+        embedderFrameIsHidden = !embedderFrame->StyleVisibility()->IsVisible();
+      }
+    }
+
+    if (BrowsingContext* parent = bc->GetParent()) {
+      if (nsCOMPtr<nsIDocShell> parentDocShell = parent->GetDocShell()) {
+        if (PresShell* parentPresShell = parentDocShell->GetPresShell()) {
+          mUnderHiddenEmbedderElement =
+              parentPresShell->IsUnderHiddenEmbedderElement() ||
+              embedderFrameIsHidden;
+        }
+      }
+    }
+  }
 }
 
 enum TextPerfLogType { eLog_reflow, eLog_loaddone, eLog_totals };
 
 static void LogTextPerfStats(gfxTextPerfMetrics* aTextPerf,
                              PresShell* aPresShell,
                              const gfxTextPerfMetrics::TextCounts& aCounts,
                              float aTime, TextPerfLogType aLogType,
@@ -8806,17 +8828,17 @@ void PresShell::DidPaintWindow() {
       nsCOMPtr<nsIDOMChromeWindow> chromeWin(do_QueryInterface(window));
       if (chromeWin) {
         obsvc->NotifyObservers(chromeWin, "widget-first-paint", nullptr);
       }
     }
   }
 }
 
-bool PresShell::IsVisible() {
+bool PresShell::IsVisible() const {
   if (!mIsActive || !mViewManager) return false;
 
   nsView* view = mViewManager->GetRootView();
   if (!view) return true;
 
   // inner view of subdoc frame
   view = view->GetParent();
   if (!view) return true;
@@ -10879,16 +10901,53 @@ void PresShell::NotifyStyleSheetServiceS
   }
 }
 
 void PresShell::NotifyStyleSheetServiceSheetRemoved(StyleSheet* aSheet,
                                                     uint32_t aSheetType) {
   RemoveSheet(ToOrigin(aSheetType), aSheet);
 }
 
+void PresShell::SetIsUnderHiddenEmbedderElement(
+    bool aUnderHiddenEmbedderElement) {
+  if (mUnderHiddenEmbedderElement == aUnderHiddenEmbedderElement) {
+    return;
+  }
+
+  mUnderHiddenEmbedderElement = aUnderHiddenEmbedderElement;
+
+  if (nsCOMPtr<nsIDocShell> docShell = mPresContext->GetDocShell()) {
+    BrowsingContext* bc = nsDocShell::Cast(docShell)->GetBrowsingContext();
+
+    // Propagate to children.
+    for (BrowsingContext* child : bc->GetChildren()) {
+      bool embedderFrameIsHidden = true;
+      if (Element* embedderElement = bc->GetEmbedderElement()) {
+        if (auto embedderFrame = embedderElement->GetPrimaryFrame()) {
+          embedderFrameIsHidden =
+              !embedderFrame->StyleVisibility()->IsVisible();
+        }
+      }
+
+      if (nsIDocShell* childDocShell = child->GetDocShell()) {
+        PresShell* presShell = childDocShell->GetPresShell();
+        if (!presShell) {
+          continue;
+        }
+
+        presShell->SetIsUnderHiddenEmbedderElement(
+            aUnderHiddenEmbedderElement || embedderFrameIsHidden);
+      }
+      // FIXME: Bug 1518919 - In the case where the BrowsingContext has no
+      // docshell which means it's out-of-process iframe, we need to propagate
+      // the info via an IPC call.
+    }
+  }
+}
+
 nsIContent* PresShell::EventHandler::GetOverrideClickTarget(
     WidgetGUIEvent* aGUIEvent, nsIFrame* aFrame) {
   if (aGUIEvent->mMessage != eMouseUp) {
     return nullptr;
   }
 
   MOZ_ASSERT(aGUIEvent->mClass == eMouseEventClass);
   WidgetMouseEvent* mouseEvent = aGUIEvent->AsMouseEvent();
--- a/layout/base/PresShell.h
+++ b/layout/base/PresShell.h
@@ -1038,17 +1038,21 @@ class PresShell final : public nsStubDoc
   MOZ_CAN_RUN_SCRIPT void WillPaintWindow();
   /**
    * Notify that we called Paint with PaintFlags::PaintComposite.
    * Fires on the presshell for the painted widget.
    * This is issued at a time when it's safe to modify widget geometry.
    */
   MOZ_CAN_RUN_SCRIPT void DidPaintWindow();
 
-  bool IsVisible();
+  bool IsVisible() const;
+  bool IsUnderHiddenEmbedderElement() const {
+    return mUnderHiddenEmbedderElement;
+  }
+  void SetIsUnderHiddenEmbedderElement(bool aUnderHiddenEmbedderElement);
   MOZ_CAN_RUN_SCRIPT
   void DispatchSynthMouseMove(WidgetGUIEvent* aEvent);
 
   /* Temporarily ignore the Displayport for better paint performance. We
    * trigger a repaint once suppression is disabled. Without that
    * the displayport may get left at the suppressed size for an extended
    * period of time and result in unnecessary checkerboarding (see bug
    * 1255054). */
@@ -3070,16 +3074,20 @@ class PresShell final : public nsStubDoc
 
   // Whether the most recent change to the pres shell resolution was
   // originated by the main thread.
   bool mResolutionUpdated : 1;
 
   // True if the resolution has been ever changed by APZ.
   bool mResolutionUpdatedByApz : 1;
 
+  // Whether this presshell is hidden by 'vibility:hidden' on an ancestor
+  // nsSubDocumentFrame.
+  bool mUnderHiddenEmbedderElement : 1;
+
   bool mDocumentLoading : 1;
   bool mNoDelayedMouseEvents : 1;
   bool mNoDelayedKeyEvents : 1;
 
   bool mApproximateFrameVisibilityVisited : 1;
 
   bool mNextPaintCompressed : 1;
 
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -4702,23 +4702,16 @@ nsIFrame* nsLayoutUtils::GetParentOrPlac
 }
 
 nsIFrame* nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(nsIFrame* aFrame) {
   nsIFrame* f = GetParentOrPlaceholderFor(aFrame);
   if (f) return f;
   return GetCrossDocParentFrame(aFrame);
 }
 
-nsIFrame* nsLayoutUtils::GetDisplayListParent(nsIFrame* aFrame) {
-  if (aFrame->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT) {
-    return aFrame->GetParent();
-  }
-  return nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(aFrame);
-}
-
 nsIFrame* nsLayoutUtils::GetNextContinuationOrIBSplitSibling(nsIFrame* aFrame) {
   nsIFrame* result = aFrame->GetNextContinuation();
   if (result) return result;
 
   if ((aFrame->GetStateBits() & NS_FRAME_PART_OF_IBSPLIT) != 0) {
     // We only store the ib-split sibling annotation with the first
     // frame in the continuation chain. Walk back to find that frame now.
     aFrame = aFrame->FirstContinuation();
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -1392,25 +1392,16 @@ class nsLayoutUtils {
 
   /**
    * If aFrame is an out of flow frame, return its placeholder, otherwise
    * return its (possibly cross-doc) parent.
    */
   static nsIFrame* GetParentOrPlaceholderForCrossDoc(nsIFrame* aFrame);
 
   /**
-   * Returns the frame that would act as the parent of aFrame when
-   * descending through the frame tree in display list building.
-   * Usually the same as GetParentOrPlaceholderForCrossDoc, except
-   * that pushed floats are treated as children of their containing
-   * block.
-   */
-  static nsIFrame* GetDisplayListParent(nsIFrame* aFrame);
-
-  /**
    * Get a frame's next-in-flow, or, if it doesn't have one, its
    * block-in-inline-split sibling.
    */
   static nsIFrame* GetNextContinuationOrIBSplitSibling(nsIFrame* aFrame);
 
   /**
    * Get the first frame in the continuation-plus-ib-split-sibling chain
    * containing aFrame.
--- a/layout/base/tests/chrome/chrome.ini
+++ b/layout/base/tests/chrome/chrome.ini
@@ -51,8 +51,15 @@ skip-if = (os == "linux" && bits == 32) 
 [test_printpreview_bug396024.xul]
 skip-if = (verify && (os == 'win'))
 [test_printpreview_bug482976.xul]
 skip-if = (verify && (os == 'win'))
 [test_scrolling_repaints.html]
 [test_will_change.html]
 skip-if = webrender
 [test_getClientRectsAndTexts.html]
+[test_css_visibility_propagation.xul]
+support-files =
+  window_css_visibility_propagation-1.html
+  window_css_visibility_propagation-2.html
+  window_css_visibility_propagation-3.html
+  window_css_visibility_propagation-4.html
+  frame_css_visibility_propagation.html
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/chrome/frame_css_visibility_propagation.html
@@ -0,0 +1,1 @@
+<button id="button">Button</button>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/chrome/test_css_visibility_propagation.xul
@@ -0,0 +1,171 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script src="chrome://mochikit/content/tests/SimpleTest/AddTask.js"/>
+<body xmlns="http://www.w3.org/1999/xhtml"></body>
+<script>
+<![CDATA[
+const baseURL = "chrome://mochitests/content/chrome/layout/base/tests/chrome/";
+
+// Tests that iframe visibility is updated when it's swapped.
+add_task(async () => {
+  // Open two new windows to swap iframes.
+  const window1 =
+      window.open(baseURL + "window_css_visibility_propagation-1.html",
+                  "_blank", "chrome");
+  const window2 =
+      window.open(baseURL + "window_css_visibility_propagation-2.html",
+                  "_blank", "chrome");
+
+  const loadWindow1 =
+      new Promise(resolve => window1.addEventListener("load", resolve));
+  const loadWindow2 =
+      new Promise(resolve => window2.addEventListener("load", resolve));
+
+  await Promise.all([ loadWindow1, loadWindow2 ]);
+
+  // Hide the parent of iframe2.
+  let parent = window2.document.getElementById("parent");
+  parent.style.visibility = "hidden";
+  parent.getBoundingClientRect();
+
+  const iframe2 = window2.document.querySelector("iframe");
+  let target = iframe2.contentDocument.getElementById("button");
+  target.focus();
+
+  // iframe2 is now in a visibility:hidden element in window2,
+  // so that Element.focus() shouldn't work.
+  isnot(iframe2.contentDocument.activeElement, target,
+        "Element.focus() shouldn't work in invisible iframe");
+
+  // Make the parent visible.
+  parent.style.visibility = "";
+  parent.getBoundingClientRect();
+
+  target.focus();
+
+  // iframe2 is visible now, so focus() should work.
+  is(iframe2.contentDocument.activeElement, target,
+     "Element.focus() should work in visible iframe");
+
+  target.blur();
+  isnot(iframe2.contentDocument.activeElement, target,
+        "The target element shouldn't be activeElement");
+
+  // Swap the content in iframe1 for the content in iframe2.
+  const iframe1 = window1.document.querySelector("iframe");
+  iframe1.swapFrameLoaders(iframe2);
+  await new Promise(resolve => setTimeout(resolve, 0));
+
+  target = iframe1.contentDocument.getElementById("button");
+  target.focus();
+
+  // iframe1 is in a visibility:hidden element in window1,
+  // so that Element.focus() shouldn't work.
+  isnot(iframe1.contentDocument.activeElement, target,
+        "Element.focus() shouldn't work in invisible iframe");
+
+  parent = window1.document.getElementById("parent");
+  parent.style.visibility = "visible";
+  parent.getBoundingClientRect();
+
+  target.focus();
+
+  // Now iframe1 is in a visibility:visible element, so that
+  // Element.focus() should just work.
+  is(iframe1.contentDocument.activeElement, target,
+     "Element.focus() should work in visible iframe");
+
+  window1.close();
+  window2.close();
+});
+
+// Tests that ancestor's visibility change doesn't clobber child
+// iframe's visibility if the child iframe is hidden by an
+// element in the ancestor document.
+add_task(async () => {
+  const tabReady = new Promise(resolve => {
+    window.addEventListener("message", event => {
+      if (event.data == "ready") {
+        resolve();
+      }
+    }, { once: true });
+  });
+  const tabWindow =
+      window.open(baseURL + "window_css_visibility_propagation-3.html");
+  await tabReady;
+
+  const childIFrame = tabWindow.document.querySelector("iframe");
+
+  const grandChildIFrame =
+      childIFrame.contentDocument.querySelector("iframe");
+  let target = grandChildIFrame.contentDocument.getElementById("button");
+  target.focus();
+
+  is(grandChildIFrame.contentDocument.activeElement, target,
+     "Element.focus() should work in visible iframe");
+  target.blur();
+
+  // Hide the parent element of the grand child iframe.
+  let parent = childIFrame.contentDocument.getElementById("parent");
+  parent.style.visibility = "hidden";
+  parent.getBoundingClientRect();
+
+  target.focus();
+
+  isnot(grandChildIFrame.contentDocument.activeElement, target,
+        "Element.focus() shouldn't work in invisible iframe");
+
+  // Hide the parent element of the child iframe.
+  parent = tabWindow.document.getElementById("parent");
+  parent.style.visibility = "hidden";
+  parent.getBoundingClientRect();
+
+  target.focus();
+
+  isnot(grandChildIFrame.contentDocument.activeElement, target,
+        "Element.focus() shouldn't work in invisible iframe");
+
+  // Make the parent element of the child iframe visible.
+  parent.style.visibility = "visible";
+  parent.getBoundingClientRect();
+
+  target.focus();
+
+  // Even if the child iframe is visible, but still the grand child is
+  // hidden by the parent element of the grand child iframe so that
+  // we can't focus to the element in the grand child iframe.
+  isnot(grandChildIFrame.contentDocument.activeElement, target,
+        "Element.focus() shouldn't work in invisible iframe");
+
+  tabWindow.close();
+});
+
+// Tests that an iframe is initially hidden by a visibility:hidden element in
+// the parent document.
+add_task(async () => {
+  const tabReady = new Promise(resolve => {
+    window.addEventListener("message", event => {
+      if (event.data == "ready") {
+        resolve();
+      }
+    }, { once: true });
+  });
+  const tabWindow =
+      window.open(baseURL + "window_css_visibility_propagation-4.html");
+  await tabReady;
+
+  const iframe = tabWindow.document.querySelector("iframe");
+  let target = iframe.contentDocument.getElementById("button");
+  target.focus();
+
+  isnot(iframe.contentDocument.activeElement, target,
+        "Element.focus() shouldn't work in invisible iframe");
+
+  tabWindow.close();
+});
+]]>
+</script>
+</window>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/chrome/window_css_visibility_propagation-1.html
@@ -0,0 +1,3 @@
+<div id="parent" style="visibility: hidden">
+  <iframe src="about:mozilla"/>
+</div>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/chrome/window_css_visibility_propagation-2.html
@@ -0,0 +1,3 @@
+<div id="parent">
+  <iframe src="frame_css_visibility_propagation.html"/>
+</div>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/chrome/window_css_visibility_propagation-3.html
@@ -0,0 +1,3 @@
+<div id="parent">
+  <iframe onload="opener.postMessage('ready');" src="window_css_visibility_propagation-2.html"/>
+</div>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/chrome/window_css_visibility_propagation-4.html
@@ -0,0 +1,3 @@
+<div id="parent" style="visibility:hidden">
+  <iframe onload="opener.postMessage('ready');" src="frame_css_visibility_propagation.html"/>
+</div>
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -388,16 +388,20 @@ bool nsIFrame::CheckAndClearDisplayListS
   return result;
 }
 
 bool nsIFrame::IsVisibleConsideringAncestors(uint32_t aFlags) const {
   if (!StyleVisibility()->IsVisible()) {
     return false;
   }
 
+  if (PresShell()->IsUnderHiddenEmbedderElement()) {
+    return false;
+  }
+
   const nsIFrame* frame = this;
   while (frame) {
     nsView* view = frame->GetView();
     if (view && view->GetVisibility() == nsViewVisibility_kHide) return false;
 
     nsIFrame* parent = frame->GetParent();
     nsDeckFrame* deck = do_QueryFrame(parent);
     if (deck) {
@@ -411,18 +415,16 @@ bool nsIFrame::IsVisibleConsideringAnces
       if (!parent) break;
 
       if ((aFlags & nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY) == 0 &&
           parent->PresContext()->IsChrome() &&
           !frame->PresContext()->IsChrome()) {
         break;
       }
 
-      if (!parent->StyleVisibility()->IsVisible()) return false;
-
       frame = parent;
     }
   }
 
   return true;
 }
 
 void nsIFrame::FindCloserFrameForSelection(
--- a/layout/generic/nsSubDocumentFrame.cpp
+++ b/layout/generic/nsSubDocumentFrame.cpp
@@ -146,19 +146,42 @@ void nsSubDocumentFrame::Init(nsIContent
         ::EndSwapDocShellsForViews(mInnerView->GetFirstChild());
       } else {
         // Presentation is for a different document, don't restore it.
         frameloader->Hide();
       }
     }
   }
 
+  PropagateIsUnderHiddenEmbedderElementToSubView(
+      PresShell()->IsUnderHiddenEmbedderElement() ||
+      !StyleVisibility()->IsVisible());
+
   nsContentUtils::AddScriptRunner(new AsyncFrameInit(this));
 }
 
+void nsSubDocumentFrame::PropagateIsUnderHiddenEmbedderElementToSubView(
+    bool aIsUnderHiddenEmbedderElement) {
+  // FIXME: Bug 1518919 - In the case where we have mFrameLoader and its
+  // IsRemoteFrame() is true, the iframe is out-of-process iframe, so we need
+  // to notify the change via nsFrameLoader.
+
+  if (!mInnerView) {
+    return;
+  }
+
+  nsView* subdocView = mInnerView->GetFirstChild();
+  while (subdocView) {
+    if (mozilla::PresShell* presShell = subdocView->GetPresShell()) {
+      presShell->SetIsUnderHiddenEmbedderElement(aIsUnderHiddenEmbedderElement);
+    }
+    subdocView = subdocView->GetNextSibling();
+  }
+}
+
 void nsSubDocumentFrame::ShowViewer() {
   if (mCallingShow) {
     return;
   }
 
   if (!PresContext()->IsDynamic()) {
     // We let the printing code take care of loading the document; just
     // create the inner view for it to use.
@@ -893,16 +916,34 @@ nsresult nsSubDocumentFrame::AttributeCh
     // Notify the frameloader
     RefPtr<nsFrameLoader> frameloader = FrameLoader();
     if (frameloader) frameloader->MarginsChanged(margins.width, margins.height);
   }
 
   return NS_OK;
 }
 
+void nsSubDocumentFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) {
+  nsAtomicContainerFrame::DidSetComputedStyle(aOldComputedStyle);
+
+  // If this presshell has invisible ancestors, we don't need to propagate the
+  // visibility style change to the subdocument since the subdocument should
+  // have already set the IsUnderHiddenEmbedderElement flag in
+  // nsSubDocumentFrame::Init.
+  if (PresShell()->IsUnderHiddenEmbedderElement()) {
+    return;
+  }
+
+  const bool isVisible = StyleVisibility()->IsVisible();
+  if (!aOldComputedStyle ||
+      isVisible != aOldComputedStyle->StyleVisibility()->IsVisible()) {
+    PropagateIsUnderHiddenEmbedderElementToSubView(!isVisible);
+  }
+}
+
 nsIFrame* NS_NewSubDocumentFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
   return new (aPresShell)
       nsSubDocumentFrame(aStyle, aPresShell->GetPresContext());
 }
 
 NS_IMPL_FRAMEARENA_HELPERS(nsSubDocumentFrame)
 
 class nsHideViewer : public Runnable {
@@ -1196,21 +1237,27 @@ void nsSubDocumentFrame::EndSwapDocShell
   // Now make sure we reflow both frames, in case their contents
   // determine their size.
   // And repaint them, for good measure, in case there's nothing
   // interesting that happens during reflow.
   if (weakThis.IsAlive()) {
     PresShell()->FrameNeedsReflow(this, IntrinsicDirty::TreeChange,
                                   NS_FRAME_IS_DIRTY);
     InvalidateFrameSubtree();
+    PropagateIsUnderHiddenEmbedderElementToSubView(
+        PresShell()->IsUnderHiddenEmbedderElement() ||
+        !StyleVisibility()->IsVisible());
   }
   if (weakOther.IsAlive()) {
     other->PresShell()->FrameNeedsReflow(other, IntrinsicDirty::TreeChange,
                                          NS_FRAME_IS_DIRTY);
     other->InvalidateFrameSubtree();
+    other->PropagateIsUnderHiddenEmbedderElementToSubView(
+        other->PresShell()->IsUnderHiddenEmbedderElement() ||
+        !other->StyleVisibility()->IsVisible());
   }
 }
 
 void nsSubDocumentFrame::ClearDisplayItems() {
   DisplayItemArray* items = GetProperty(DisplayItems());
   if (!items) {
     return;
   }
--- a/layout/generic/nsSubDocumentFrame.h
+++ b/layout/generic/nsSubDocumentFrame.h
@@ -74,16 +74,18 @@ class nsSubDocumentFrame final : public 
               nsReflowStatus& aStatus) override;
 
   void BuildDisplayList(nsDisplayListBuilder* aBuilder,
                         const nsDisplayListSet& aLists) override;
 
   nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
                             int32_t aModType) override;
 
+  void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override;
+
   // if the content is "visibility:hidden", then just hide the view
   // and all our contents. We don't extend "visibility:hidden" to
   // the child content ourselves, since it belongs to a different
   // document and CSS doesn't inherit in there.
   bool SupportsVisibilityHidden() override { return false; }
 
 #ifdef ACCESSIBILITY
   mozilla::a11y::AccType AccessibleType() override;
@@ -112,16 +114,19 @@ class nsSubDocumentFrame final : public 
     if (!mDidCreateDoc && !mCallingShow) {
       ShowViewer();
     }
   }
 
   nsFrameLoader* FrameLoader() const;
   void ResetFrameLoader();
 
+  void PropagateIsUnderHiddenEmbedderElementToSubView(
+      bool aIsUnderHiddenEmbedderElement);
+
  protected:
   friend class AsyncFrameInit;
 
   // Helper method to look up the HTML marginwidth & marginheight attributes.
   mozilla::CSSIntSize GetMarginAttributes();
 
   bool IsInline() { return mIsInline; }
 
--- a/layout/painting/RetainedDisplayListBuilder.cpp
+++ b/layout/painting/RetainedDisplayListBuilder.cpp
@@ -292,17 +292,21 @@ bool AnyContentAncestorModified(nsIFrame
     if (f->IsFrameModified()) {
       return true;
     }
 
     if (aStopAtFrame && f == aStopAtFrame) {
       break;
     }
 
-    f = nsLayoutUtils::GetDisplayListParent(f);
+    if (f->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT) {
+      f = f->GetParent();
+    } else {
+      f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f);
+    }
   }
 
   return false;
 }
 
 static Maybe<const ActiveScrolledRoot*> SelectContainerASR(
     const DisplayItemClipChain* aClipChain, const ActiveScrolledRoot* aItemASR,
     Maybe<const ActiveScrolledRoot*>& aContainerASR) {
@@ -1197,17 +1201,17 @@ static void AddFramesForContainingBlock(
 // descendants of a modified frame (us, or another frame we'll get to soon).
 // This is combined with the work required for MarkFrameForDisplayIfVisible,
 // so that we can avoid an extra ancestor walk, and we can reuse the flag
 // to detect when we've already visited an ancestor (and thus all further
 // ancestors must also be visited).
 static void FindContainingBlocks(nsIFrame* aFrame,
                                  nsTArray<nsIFrame*>& aExtraFrames) {
   for (nsIFrame* f = aFrame; f;
-       f = nsLayoutUtils::GetDisplayListParent(f)) {
+       f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f)) {
     if (f->ForceDescendIntoIfVisible()) return;
     f->SetForceDescendIntoIfVisible(true);
     CRR_LOG("Considering OOFs for %p\n", f);
 
     AddFramesForContainingBlock(f, f->GetChildList(nsIFrame::kFloatList),
                                 aExtraFrames);
     AddFramesForContainingBlock(f, f->GetChildList(f->GetAbsoluteListID()),
                                 aExtraFrames);
deleted file mode 100644
--- a/layout/painting/crashtests/1549909.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<style>
-* {
-  -webkit-column-break-after: always;
-  float: left;
-  column-width: 0px;
-}
-</style>
-}
-<video controls="controls">
--- a/layout/painting/crashtests/crashtests.list
+++ b/layout/painting/crashtests/crashtests.list
@@ -13,10 +13,9 @@ load 1454105-1.html
 load 1455944-1.html
 load 1465305-1.html
 load 1468124-1.html
 load 1469472.html
 load 1477831-1.html
 load 1504033.html
 load 1514544-1.html
 load 1547420-1.html
-load 1549909.html
 
--- a/layout/reftests/native-theme/searchfield-mirrored-when-rtl-ref.xul
+++ b/layout/reftests/native-theme/searchfield-mirrored-when-rtl-ref.xul
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 
 <window title="mirrored searchfield"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <hbox>
-    <textbox type="search" width="200" id="searchbox" style="-moz-transform: scaleX(-1);"/>
+    <textbox is="search-textbox" width="200" id="searchbox" style="-moz-transform: scaleX(-1);"/>
     <spacer flex="1"/>
   </hbox>
 </window>
--- a/layout/reftests/native-theme/searchfield-mirrored-when-rtl.xul
+++ b/layout/reftests/native-theme/searchfield-mirrored-when-rtl.xul
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 
 <window title="RTL searchfield"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <hbox>
-    <textbox type="search" width="200" id="searchbox" style="direction: rtl;"/>
+    <textbox is="search-textbox" width="200" id="searchbox" style="direction: rtl;"/>
     <spacer flex="1"/>
   </hbox>
 </window>
--- a/media/libdav1d/asm/moz.build
+++ b/media/libdav1d/asm/moz.build
@@ -183,16 +183,17 @@ elif CONFIG['CPU_ARCH'] == 'arm' or CONF
 
     # BITDEPTH .S files
     if CONFIG['CPU_ARCH'] == 'aarch64':
         SOURCES += [
             '../../../third_party/dav1d/src/arm/64/cdef.S',
             '../../../third_party/dav1d/src/arm/64/loopfilter.S',
             '../../../third_party/dav1d/src/arm/64/looprestoration.S',
             '../../../third_party/dav1d/src/arm/64/mc.S',
+            '../../../third_party/dav1d/src/arm/64/msac.S',
         ]
     elif CONFIG['CPU_ARCH'] == 'arm':
         SOURCES += [
             '../../../third_party/dav1d/src/arm/32/looprestoration.S',
             '../../../third_party/dav1d/src/arm/32/mc.S',
         ]
 
 if CONFIG['CPU_ARCH'] in ('x86', 'x86_64'):
--- a/media/libdav1d/dav1d.rc
+++ b/media/libdav1d/dav1d.rc
@@ -1,12 +1,12 @@
-#define API_VERSION_NUMBER 1,0,1,0
-#define API_VERSION_NUMBER_STR "1.0.1"
-#define PROJECT_VERSION_NUMBER 0,2,2,0
-#define PROJECT_VERSION_NUMBER_STR "0.2.2"
+#define API_VERSION_NUMBER 1,1,0,0
+#define API_VERSION_NUMBER_STR "1.1.0"
+#define PROJECT_VERSION_NUMBER 0,3,0,0
+#define PROJECT_VERSION_NUMBER_STR "0.3.0"
 
 #include <windows.h>
 
 1 VERSIONINFO
 FILETYPE VFT_DLL
 FILEOS VOS_NT_WINDOWS32
 PRODUCTVERSION PROJECT_VERSION_NUMBER
 FILEVERSION API_VERSION_NUMBER
--- a/media/libdav1d/moz.yaml
+++ b/media/libdav1d/moz.yaml
@@ -15,15 +15,15 @@ origin:
   description: dav1d, a fast AV1 decoder
 
   # Full URL for the package's homepage/etc
   # Usually different from repository url
   url: https://code.videolan.org/videolan/dav1d
 
   # Human-readable identifier for this version/release
   # Generally "version NNN", "tag SSS", "bookmark SSS"
-  release: commit f8cac8c56b3e8afec0e356b297c373a352746a1b (2019-04-22T14:37:04.000Z).
+  release: commit a713643eadcf50c9f7fd2ea22a598127c959a723 (2019-05-09T07:52:54.000Z).
 
   # The package's license, where possible using the mnemonic from
   # https://spdx.org/licenses/
   # Multiple licenses can be specified (as a YAML list)
   # A "LICENSE" file must exist containing the full license text
   license: BSD-2-Clause
--- a/media/libdav1d/vcs_version.h
+++ b/media/libdav1d/vcs_version.h
@@ -1,2 +1,2 @@
 /* auto-generated, do not edit */
-#define DAV1D_VERSION "0.2.2-1-gf8cac8c"
+#define DAV1D_VERSION "0.3.0-13-ga713643"
--- a/media/libdav1d/version.h
+++ b/media/libdav1d/version.h
@@ -23,12 +23,12 @@
  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
 #ifndef DAV1D_VERSION_H
 #define DAV1D_VERSION_H
 
 #define DAV1D_API_VERSION_MAJOR 1
-#define DAV1D_API_VERSION_MINOR 0
-#define DAV1D_API_VERSION_PATCH 1
+#define DAV1D_API_VERSION_MINOR 1
+#define DAV1D_API_VERSION_PATCH 0
 
 #endif /* DAV1D_VERSION_H */
--- a/mobile/android/chrome/content/OfflineApps.js
+++ b/mobile/android/chrome/content/OfflineApps.js
@@ -4,20 +4,21 @@
 "use strict";
 
 var OfflineApps = {
   offlineAppRequested: function(aContentWindow) {
     if (!Services.prefs.getBoolPref("browser.offline-apps.notify"))
       return;
 
     let tab = BrowserApp.getTabForWindow(aContentWindow);
+    let principal = aContentWindow.document.nodePrincipal;
     let currentURI = aContentWindow.document.documentURIObject;
 
     // Don't bother showing UI if the user has already made a decision
-    if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION)
+    if (Services.perms.testExactPermissionFromPrincipal(principal, "offline-app") != Services.perms.UNKNOWN_ACTION)
       return;
 
     try {
       if (Services.prefs.getBoolPref("offline-apps.allow_by_default")) {
         // All pages can use offline capabilities, no need to ask the user
         return;
       }
     } catch (e) {
@@ -45,26 +46,26 @@ var OfflineApps = {
 
     let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name + "'" : host;
     let message = strings.formatStringFromName("offlineApps.ask", [requestor], 1);
     let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
     NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
   },
 
   allowSite: function(aDocument) {
-    Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.ALLOW_ACTION);
+    Services.perms.addFromPrincipal(aDocument.nodePrincipal, "offline-app", Services.perms.ALLOW_ACTION);
 
     // When a site is enabled while loading, manifest resources will
     // start fetching immediately.  This one time we need to do it
     // ourselves.
     this._startFetching(aDocument);
   },
 
   disallowSite: function(aDocument) {
-    Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.DENY_ACTION);
+    Services.perms.addFromPrincipal(aDocument.nodePrincipal, "offline-app", Services.perms.DENY_ACTION);
   },
 
   _startFetching: function(aDocument) {
     if (!aDocument.documentElement)
       return;
 
     let manifest = aDocument.documentElement.getAttribute("manifest");
     if (!manifest)
--- a/mobile/android/chrome/content/PermissionsHelper.js
+++ b/mobile/android/chrome/content/PermissionsHelper.js
@@ -45,29 +45,29 @@ var PermissionsHelper = {
     "native-intent": {
       label: "helperapps.openWithList2",
       allowed: "helperapps.always",
       denied: "helperapps.never",
     },
   },
 
   onEvent: function onEvent(event, data, callback) {
-    let uri = BrowserApp.selectedBrowser.currentURI;
+    let principal = BrowserApp.selectedBrowser.contentPrincipal;
     let check = false;
 
     switch (event) {
       case "Permissions:Check":
         check = true;
         // fall-through
 
       case "Permissions:Get":
         let permissions = [];
         for (let i = 0; i < this._permissonTypes.length; i++) {
           let type = this._permissonTypes[i];
-          let value = this.getPermission(uri, type);
+          let value = this.getPermission(principal, type);
 
           // Only add the permission if it was set by the user
           if (value == Services.perms.UNKNOWN_ACTION)
             continue;
 
           if (check) {
             GlobalEventDispatcher.sendRequest({
               type: "Permissions:CheckResult",
@@ -127,58 +127,58 @@ var PermissionsHelper = {
    * Gets the permission value stored for a specified permission type.
    *
    * @param aType
    *        The permission type string stored in permission manager.
    *        e.g. "geolocation", "indexedDB", "popup"
    *
    * @return A permission value defined in nsIPermissionManager.
    */
-  getPermission: function getPermission(aURI, aType) {
+  getPermission: function getPermission(aPrincipal, aType) {
     // Password saving isn't a nsIPermissionManager permission type, so handle
     // it seperately.
     if (aType == "password") {
       // By default, login saving is enabled, so if it is disabled, the
       // user selected the never remember option
       if (!Services.logins.getLoginSavingEnabled(aURI.displayPrePath))
         return Services.perms.DENY_ACTION;
 
       // Check to see if the user ever actually saved a login
       if (Services.logins.countLogins(aURI.displayPrePath, "", ""))
         return Services.perms.ALLOW_ACTION;
 
       return Services.perms.UNKNOWN_ACTION;
     }
 
-    // Geolocation consumers use testExactPermission
+    // Geolocation consumers use testExactPermissionForPrincipal
     if (aType == "geolocation")
-      return Services.perms.testExactPermission(aURI, aType);
+      return Services.perms.testExactPermissionForPrincipal(aPrincipal, aType);
 
-    return Services.perms.testPermission(aURI, aType);
+    return Services.perms.testPermissionForPrincipal(aPrincipal, aType);
   },
 
   /**
    * Clears a user-set permission value for the site given a permission type.
    *
    * @param aType
    *        The permission type string stored in permission manager.
    *        e.g. "geolocation", "indexedDB", "popup"
    */
-  clearPermission: function clearPermission(aURI, aType, aContext) {
+  clearPermission: function clearPermission(aPrincipal, aType, aContext) {
     // Password saving isn't a nsIPermissionManager permission type, so handle
     // it seperately.
     if (aType == "password") {
       // Get rid of exisiting stored logings
       let logins = Services.logins.findLogins({}, aURI.displayPrePath, "", "");
       for (let i = 0; i < logins.length; i++) {
         Services.logins.removeLogin(logins[i]);
       }
       // Re-set login saving to enabled
       Services.logins.setLoginSavingEnabled(aURI.displayPrePath, true);
     } else {
-      Services.perms.remove(aURI, aType);
+      Services.perms.removeFromPrincipal(aPrincipal, aType);
       // Clear content prefs set in ContentPermissionPrompt.js
       Cc["@mozilla.org/content-pref/service;1"]
         .getService(Ci.nsIContentPrefService2)
         .removeByDomainAndName(aURI.spec, aType + ".request.remember", aContext);
     }
   },
 };
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1873,28 +1873,28 @@ var BrowserApp = {
             let normalizedUrl = Services.io.newURI("https://" + browser.currentURI.hostPort);
             if (data.allowContent) {
               // Add the current host in the 'trackingprotection' consumer of
               // the permission manager using a normalized URI. This effectively
               // places this host on the tracking protection white list.
               if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
                 PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
               } else {
-                Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
+                Services.perms.addFromPrincipal(browser.contentPrincipal, "trackingprotection", Services.perms.ALLOW_ACTION);
                 Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
               }
             } else {
               // Remove the current host from the 'trackingprotection' consumer
               // of the permission manager. This effectively removes this host
               // from the tracking protection white list (any list actually).
               // eslint-disable-next-line no-lonely-if
               if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
                 PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
               } else {
-                Services.perms.remove(normalizedUrl, "trackingprotection");
+                Services.perms.removeFromPrincipal(browser.contentPrincipal, "trackingprotection");
                 Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
               }
             }
           }
         }
 
         // Try to use the session history to reload so that framesets are
         // handled properly. If the window has no session history, fall back
@@ -5322,17 +5322,17 @@ var PopupBlockerObserver = {
   onUpdateBlockedPopups: function onUpdateBlockedPopups(aEvent) {
     let browser = BrowserApp.selectedBrowser;
     if (aEvent.originalTarget != browser)
       return;
 
     if (!browser.blockedPopups)
       return;
 
-    let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
+    let result = Services.perms.testExactPermissionFromPrincipal(BrowserApp.selectedBrowser.contentPrincipal, "popup");
     if (result == Ci.nsIPermissionManager.DENY_ACTION)
       return;
 
     // Only show the notification again if we've not already shown it. Since
     // notifications are per-browser, we don't need to worry about re-adding
     // it.
     if (!browser.blockedPopups.reported) {
       if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
@@ -5370,18 +5370,18 @@ var PopupBlockerObserver = {
       }
       // Record the fact that we've reported this blocked popup, so we don't
       // show it again.
       browser.blockedPopups.reported = true;
     }
   },
 
   allowPopupsForSite: function allowPopupsForSite(aAllow) {
-    let currentURI = BrowserApp.selectedBrowser.currentURI;
-    Services.perms.add(currentURI, "popup", aAllow
+    let principal = BrowserApp.selectedBrowser.contentPrincipal;
+    Services.perms.addFromPrincipal(principal, "popup", aAllow
                        ? Ci.nsIPermissionManager.ALLOW_ACTION
                        : Ci.nsIPermissionManager.DENY_ACTION);
     dump("Allowing popups for: " + currentURI);
   },
 
   showPopupsForSite: function showPopupsForSite() {
     let uri = BrowserApp.selectedBrowser.currentURI;
     let {blockedPopups} = BrowserApp.selectedBrowser;
--- a/mobile/android/components/geckoview/GeckoViewPermission.js
+++ b/mobile/android/components/geckoview/GeckoViewPermission.js
@@ -44,28 +44,29 @@ GeckoViewPermission.prototype = {
         break;
       }
     }
   },
 
   receiveMessage(aMsg) {
     switch (aMsg.name) {
       case "GeckoView:AddCameraPermission": {
-        let uri;
+        let principal;
         try {
           // This fails for principals that serialize to "null", e.g. file URIs.
-          uri = Services.io.newURI(aMsg.data.origin);
+          principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(aMsg.data.origin);
         } catch (e) {
-          uri = Services.io.newURI(aMsg.data.documentURI);
+          principal = Services.scriptSecurityManager.createCodebasePrincipal(
+            Services.io.newURI(aMsg.data.documentURI), {});
         }
         // Although the lifetime is "session" it will be removed upon
         // use so it's more of a one-shot.
-        Services.perms.add(uri, "MediaManagerVideo",
-                           Services.perms.ALLOW_ACTION,
-                           Services.perms.EXPIRE_SESSION);
+        Services.perms.addFromPrincipal(principal, "MediaManagerVideo",
+                                        Services.perms.ALLOW_ACTION,
+                                        Services.perms.EXPIRE_SESSION);
         break;
       }
     }
   },
 
   handleMediaAskDevicePermission: function(aType, aCallback) {
     let perms = [];
     if (aType === "video" || aType === "all") {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -58,16 +58,18 @@ class ContentDelegateTest : BaseSessionT
                 assertThat("Title should match", title,
                            equalTo(forEachCall("Title1", "Title2")))
             }
         })
     }
 
     @Test fun download() {
         sessionRule.session.loadTestPath(DOWNLOAD_HTML_PATH)
+        // disable test on pgo for frequently failing Bug 1543355
+            assumeThat(sessionRule.env.isDebugBuild, equalTo(true))
 
         sessionRule.waitUntilCalled(object : Callbacks.NavigationDelegate, Callbacks.ContentDelegate {
 
             @AssertCalled(count = 2)
             override fun onLoadRequest(session: GeckoSession,
                                        request: LoadRequest):
                                        GeckoResult<AllowOrDeny>? {
                 return null
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/SessionLifecycleTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/SessionLifecycleTest.kt
@@ -96,16 +96,18 @@ class SessionLifecycleTest : BaseSession
         }
 
         sessionRule.session.reload()
         sessionRule.session.waitForPageStop()
     }
 
     @Test(expected = IllegalStateException::class)
     fun readFromParcel_throwOnAlreadyOpen() {
+        //disable readFromParcel_throwOnAlreadyOpen for frequent failures Bug 1532186
+        assumeThat(sessionRule.env.isDebugBuild, equalTo(true))
         // Throw exception if retrying to open again; otherwise we would leak the old open window.
         sessionRule.session.toParcel { parcel ->
             sessionRule.createOpenSession().readFromParcel(parcel)
         }
     }
 
     @Test fun readFromParcel_canLoadPageAfterRead() {
         val newSession = sessionRule.createClosedSession()
--- a/mobile/android/modules/WebrtcUI.jsm
+++ b/mobile/android/modules/WebrtcUI.jsm
@@ -156,17 +156,17 @@ var WebrtcUI = {
       },
       function(error) {
         Cu.reportError(error);
       },
       aSubject.innerWindowID,
       aSubject.callID);
   },
 
-  getDeviceButtons: function(audioDevices, videoDevices, aCallID, aUri) {
+  getDeviceButtons: function(audioDevices, videoDevices, aCallID, aPrincipal) {
     return [{
       label: Strings.browser.GetStringFromName("getUserMedia.denyRequest.label"),
       callback: function() {
         Services.obs.notifyObservers(null, "getUserMedia:response:deny", aCallID);
       },
     },
     {
       label: Strings.browser.GetStringFromName("getUserMedia.shareRequest.label"),
@@ -182,17 +182,17 @@ var WebrtcUI = {
         let videoId = 0;
         if (inputs && inputs.videoSource != undefined)
           videoId = inputs.videoSource;
         if (videoDevices[videoId]) {
           allowedDevices.appendElement(videoDevices[videoId]);
           let perms = Services.perms;
           // Although the lifetime is "session" it will be removed upon
           // use so it's more of a one-shot.
-          perms.add(aUri, "MediaManagerVideo", perms.ALLOW_ACTION, perms.EXPIRE_SESSION);
+          perms.addFromPrincipal(aPrincipal, "MediaManagerVideo", perms.ALLOW_ACTION, perms.EXPIRE_SESSION);
         }
 
         Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
       },
       positive: true,
     }];
   },
 
@@ -310,29 +310,29 @@ var WebrtcUI = {
     else if (audioDevices.length)
       requestType = "Microphone";
     else if (videoDevices.length)
       requestType = "Camera";
     else
       return;
 
     let chromeWin = this.getChromeWindow(aContentWindow);
-    let uri = aContentWindow.document.documentURIObject;
-    let host = uri.host;
+    let principal = aContentWindow.document.nodePrincipal;
+    let host = principal.URI.host;
     let requestor = (chromeWin.BrowserApp && chromeWin.BrowserApp.manifest) ?
           "'" + chromeWin.BrowserApp.manifest.name + "'" : host;
     let message = Strings.browser.formatStringFromName("getUserMedia.share" + requestType + ".message", [ requestor ], 1);
 
     let options = { inputs: [] };
     if (videoDevices.length > 1 || audioDevices.length > 0) {
       // videoSource is both the string used for l10n lookup and the object that will be returned
       this._addDevicesToOptions(videoDevices, "videoSource", options);
     }
 
     if (audioDevices.length > 1 || videoDevices.length > 0) {
       this._addDevicesToOptions(audioDevices, "audioDevice", options);
     }
 
-    let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID, uri);
+    let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID, principal);
 
     DoorHanger.show(aContentWindow, message, "webrtc-request", buttons, options, "WEBRTC");
   },
 };
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4716,16 +4716,17 @@ pref("signon.formlessCapture.enabled",  
 pref("signon.privateBrowsingCapture.enabled", false);
 pref("signon.storeWhenAutocompleteOff",     true);
 pref("signon.debug",                        false);
 pref("signon.recipes.path",                 "chrome://passwordmgr/content/recipes.json");
 pref("signon.schemeUpgrades",               false);
 // This temporarily prevents the master password to reprompt for autocomplete.
 pref("signon.masterPasswordReprompt.timeout_ms", 900000); // 15 Minutes
 pref("signon.showAutoCompleteFooter", false);
+pref("signon.showAutoCompleteOrigins", false);
 
 // Satchel (Form Manager) prefs
 pref("browser.formfill.debug",            false);
 pref("browser.formfill.enable",           true);
 pref("browser.formfill.expire_days",      180);
 pref("browser.formfill.agedWeight",       2);
 pref("browser.formfill.bucketSize",       1);
 pref("browser.formfill.maxTimeGroupings", 25);
--- a/netwerk/base/nsIRequestContext.idl
+++ b/netwerk/base/nsIRequestContext.idl
@@ -21,17 +21,17 @@ interface nsIStreamListener;
 [ptr] native SpdyPushCachePtr(mozilla::net::SpdyPushCache);
 
 /**
  * Requests capable of tail-blocking must implement this
  * interfaces (typically channels).
  * If the request is tail-blocked, it will be held in its request
  * context queue until unblocked.
  */
-[scriptable, uuid(7EB361D4-37A5-42C9-AFAE-F6C88FE7C394)]
+[uuid(7EB361D4-37A5-42C9-AFAE-F6C88FE7C394)]
 interface nsIRequestTailUnblockCallback : nsISupports
 {
   /**
    * Called when the requests is unblocked and proceed.
    * @param result
    *    NS_OK - the request is OK to go, unblocking is not
    *            caused by cancelation of the request.
    *    any error - the request must behave as it were canceled
@@ -43,17 +43,17 @@ interface nsIRequestTailUnblockCallback 
 /**
  * The nsIRequestContext is used to maintain state about connections
  * that are in some way associated with each other (often by being part
  * of the same load group) and how they interact with blocking items like
  * HEAD css/js loads.
  *
  * This used to be known as nsILoadGroupConnectionInfo and nsISchedulingContext.
  */
-[scriptable, uuid(658e3e6e-8633-4b1a-8d66-fa9f72293e63)]
+[uuid(658e3e6e-8633-4b1a-8d66-fa9f72293e63)]
 interface nsIRequestContext : nsISupports
 {
   /**
    * A unique identifier for this request context
    */
   [notxpcom, nostdcall] readonly attribute unsigned long long ID;
 
   /**
--- a/netwerk/protocol/res/SubstitutingProtocolHandler.cpp
+++ b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp
@@ -321,18 +321,24 @@ nsresult SubstitutingProtocolHandler::Ne
         if (last < src) {
           spec.Append(last, src - last);
         }
         spec.Append(ch);
         src += 2;
         last = src + 1;  // src will be incremented by the loop
       }
     }
+    if (*src == '?' || *src == '#') {
+      break;  // Don't escape %2f and %2e in the query or ref parts of the URI
+    }
   }
-  if (last < src) spec.Append(last, src - last);
+
+  if (last < end) {
+    spec.Append(last, end - last);
+  }
 
   nsCOMPtr<nsIURI> base(aBaseURI);
   nsCOMPtr<nsIURL> uri;
   rv = NS_MutateURI(new SubstitutingURL::Mutator())
            .Apply(NS_MutatorMethod(&nsIStandardURLMutator::Init,
                                    nsIStandardURL::URLTYPE_STANDARD, -1, spec,
                                    aCharset, base, nullptr))
            .Finalize(uri);
--- a/netwerk/test/unit/test_URIs.js
+++ b/netwerk/test/unit/test_URIs.js
@@ -618,23 +618,34 @@ function check_schemeIsNull()
   uri = gIoService.newURI("dummyscheme://example.com");
   Assert.ok(!uri.schemeIs(null));
   uri = gIoService.newURI("jar:resource://gre/chrome.toolkit.jar!/");
   Assert.ok(!uri.schemeIs(null));
   uri = gIoService.newURI("moz-icon://.unknown?size=32");
   Assert.ok(!uri.schemeIs(null));
 }
 
+// Check that characters in the query of moz-extension aren't improperly unescaped (Bug 1547882)
+function check_mozextension_query() {
+  let uri = gIoService.newURI("moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html");
+  uri = uri.mutate().setQuery("u=https%3A%2F%2Fnews.ycombinator.com%2F").finalize();
+  Assert.equal(uri.query, "u=https%3A%2F%2Fnews.ycombinator.com%2F");
+  uri = gIoService.newURI("moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html?u=https%3A%2F%2Fnews.ycombinator.com%2F");
+  Assert.equal(uri.spec, "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html?u=https%3A%2F%2Fnews.ycombinator.com%2F");
+  Assert.equal(uri.query, "u=https%3A%2F%2Fnews.ycombinator.com%2F");
+}
+
 // TEST MAIN FUNCTION
 // ------------------
 function run_test()
 {
   check_nested_mutations();
   check_space_escaping();
   check_schemeIsNull();
+  check_mozextension_query();
 
   // UTF-8 check - From bug 622981
   // ASCII
   let base = gIoService.newURI("http://example.org/xenia?");
   let resolved = gIoService.newURI("?x", null, base);
   let expected = gIoService.newURI("http://example.org/xenia?x");
   do_info("Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec);
   Assert.ok(resolved.equals(expected));
--- a/netwerk/test/unit/test_bug337744.js
+++ b/netwerk/test/unit/test_bug337744.js
@@ -1,11 +1,12 @@
 /* verify that certain invalid URIs are not parsed by the resource
    protocol handler */
 
+"use strict";
 
 const specs = [
   "resource://res-test//",
   "resource://res-test/?foo=http:",
   "resource://res-test/?foo=" + encodeURIComponent("http://example.com/"),
   "resource://res-test/?foo=" + encodeURIComponent("x\\y"),
   "resource://res-test/..%2F",
   "resource://res-test/..%2f",
@@ -34,58 +35,41 @@ function get_channel(spec)
 
   var channel = NetUtil.newChannel({
     uri: NetUtil.newURI(spec),
     loadingPrincipal: principal,
     securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
     contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
   });
 
-  try {
-    channel.asyncOpen(null);
-    ok(false, "asyncOpen() of URI: " + spec + "should throw");
-  }
-  catch (e) {
-    // make sure we get the right error code in the exception
-    // ERROR code for NS_ERROR_DOM_BAD_URI is 1012
-    equal(e.code, 1012);
-  }
-
-  try {
-    channel.open();
-    ok(false, "Open() of uri: " + spec + "should throw");
-  }
-  catch (e) {
-    // make sure we get the right error code in the exception
-    // ERROR code for NS_ERROR_DOM_BAD_URI is 1012
-    equal(e.code, 1012);
-  }
+  Assert.throws(() => { channel.asyncOpen(null); }, /NS_ERROR_DOM_BAD_URI/, `asyncOpen() of uri: ${spec} should throw`);
+  Assert.throws(() => { channel.open(); }, /NS_ERROR_DOM_BAD_URI/, `Open() of uri: ${spec} should throw`);
 
   return channel;
 }
 
 function check_safe_resolution(spec, rootURI)
 {
   info(`Testing URL "${spec}"`);
 
   let channel = get_channel(spec);
 
   ok(channel.name.startsWith(rootURI), `URL resolved safely to ${channel.name}`);
-  ok(!/%2f/i.test(channel.name), `URL contains no escaped / characters`);
+  let startOfQuery = channel.name.indexOf("?");
+  if (startOfQuery == -1) {
+    ok(!/%2f/i.test(channel.name), `URL contains no escaped / characters`);
+  } else {
+    // Escaped slashes are allowed in the query or hash part of the URL
+    ok(!channel.name.replace(/\?.*/, "").includes("%2f"), `URL contains no escaped slashes before the query ${channel.name}`);
+  }
 }
 
 function check_resolution_error(spec)
 {
-  try {
-    get_channel(spec);
-    ok(false, "Expected an error");
-  } catch (e) {
-    equal(e.result, Cr.NS_ERROR_MALFORMED_URI,
-          "Expected a malformed URI error");
-  }
+  Assert.throws(() => { get_channel(spec); }, /NS_ERROR_MALFORMED_URI/, "Expected a malformed URI error");
 }
 
 function run_test() {
   // resource:/// and resource://gre/ are resolved specially, so we need
   // to create a temporary resource package to test the standard logic
   // with.
 
   let resProto = Cc['@mozilla.org/network/protocol;1?name=resource'].getService(Ci.nsIResProtocolHandler);
--- a/taskcluster/ci/test/raptor.yml
+++ b/taskcluster/ci/test/raptor.yml
@@ -87,18 +87,23 @@ raptor-tp6-2-firefox-profiling:
             - --gecko-profile
 
 raptor-tp6-3-firefox:
     description: "Raptor tp6-3 on Firefox"
     try-name: raptor-tp6-3-firefox
     treeherder-symbol: Rap(tp6-3)
     run-on-projects:
         by-test-platform:
-            windows10-aarch64/opt: ['try']
-            default: built-projects
+            windows10-64-ux/opt: ['try', 'mozilla-central']
+            windows10-aarch64/opt: ['try', 'mozilla-central']
+            (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
+            android-hw-.*-api-16/opt: ['try']
+            android-hw-.*-aarch64/opt: ['try']
+            macosx64(?:-qr)?/opt: ['mozilla-central', 'try']
+            default: ['try', 'trunk', 'mozilla-beta']
     mozharness:
         extra-options:
             - --test=raptor-tp6-3
 
 raptor-tp6-3-firefox-profiling:
     description: "Raptor tp6-3 on Firefox with Gecko Profiling"
     try-name: raptor-tp6-3-firefox-profiling
     treeherder-symbol: Rap-Prof(tp6-3)
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -135,18 +135,20 @@ class MochitestRunner(MozbuildObject):
             manifest = TestManifest()
             manifest.tests.extend(tests)
             options.manifestFile = manifest
 
             # When developing mochitest-plain tests, it's often useful to be able to
             # refresh the page to pick up modifications. Therefore leave the browser
             # open if only running a single mochitest-plain test. This behaviour can
             # be overridden by passing in --keep-open=false.
-            flavor = getattr(options, 'flavor', 'plain')
-            if len(tests) == 1 and options.keep_open is None and flavor == 'plain':
+            if (len(tests) == 1
+                    and options.keep_open is None
+                    and not options.headless
+                    and getattr(options, 'flavor', 'plain') == 'plain'):
                 options.keep_open = True
 
         # We need this to enable colorization of output.
         self.log_manager.enable_unstructured()
         result = mochitest.run_test_harness(parser, options)
         self.log_manager.disable_unstructured()
         return result
 
--- a/testing/mozbase/mozdevice/mozdevice/adb.py
+++ b/testing/mozbase/mozdevice/mozdevice/adb.py
@@ -3036,17 +3036,21 @@ class ADBDevice(ADBCommand):
                     extra_type_param = "--es"
                 acmd.extend([extra_type_param, str(key), str(val)])
 
         if url:
             acmd.extend(["-d", url])
 
         cmd = self._escape_command_line(acmd)
         self._logger.info('launch_application: %s' % cmd)
-        self.shell_output(cmd, timeout=timeout)
+        cmd_output = self.shell_output(cmd, timeout=timeout)
+        if 'Error:' in cmd_output:
+            for line in cmd_output.split('\n'):
+                self._logger.info(line)
+            raise ADBError('launch_activity %s/%s failed' % (app_name, activity_name))
 
     def launch_fennec(self, app_name, intent="android.intent.action.VIEW",
                       moz_env=None, extra_args=None, url=None, wait=True,
                       fail_if_running=True, timeout=None):
         """Convenience method to launch Fennec on Android with various
         debugging arguments
 
         :param str app_name: Name of fennec application (e.g.
--- a/testing/mozbase/mozlog/setup.py
+++ b/testing/mozbase/mozlog/setup.py
@@ -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/.
 
 from __future__ import absolute_import
 
 from setuptools import setup, find_packages
 
 PACKAGE_NAME = 'mozlog'
-PACKAGE_VERSION = '4.0'
+PACKAGE_VERSION = '4.1'
 DEPS = [
     'blessings>=1.3',
     'mozterm',
     'six >= 1.10.0',
 ]
 
 
 setup(name=PACKAGE_NAME,
--- a/testing/mozbase/mozprofile/setup.py
+++ b/testing/mozbase/mozprofile/setup.py
@@ -6,17 +6,17 @@ from __future__ import absolute_import
 
 from setuptools import setup
 
 PACKAGE_NAME = 'mozprofile'
 PACKAGE_VERSION = '2.2.0'
 
 deps = [
     'mozfile>=1.2',
-    'mozlog~=4.0',
+    'mozlog~=4.1',
     'six>=1.10.0,<2',
 ]
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
       description="Library to create and modify Mozilla application profiles",
       long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html",
       classifiers=['Environment :: Console',
--- a/testing/mozbase/mozrunner/setup.py
+++ b/testing/mozbase/mozrunner/setup.py
@@ -10,17 +10,17 @@ PACKAGE_NAME = 'mozrunner'
 PACKAGE_VERSION = '7.4.0'
 
 desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
 
 deps = [
     'mozdevice>=3.0.1',
     'mozfile>=1.2',
     'mozinfo>=0.7,<2',
-    'mozlog~=4.0',
+    'mozlog~=4.1',
     'mozprocess>=0.23,<2',
     'mozprofile~=2.1',
     'six>=1.10.0,<2',
 ]
 
 EXTRAS_REQUIRE = {'crash': ['mozcrash >= 1.0']}
 
 setup(name=PACKAGE_NAME,
--- a/testing/mozbase/mozversion/setup.py
+++ b/testing/mozbase/mozversion/setup.py
@@ -18,15 +18,15 @@ setup(name='mozversion',
       keywords='mozilla',
       author='Mozilla Automation and Testing Team',
       author_email='tools@lists.mozilla.org',
       url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['mozversion'],
       include_package_data=True,
       zip_safe=False,
-      install_requires=['mozlog ~= 4.0',
+      install_requires=['mozlog ~= 4.1',
                         'six >= 1.10.0'],
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       mozversion = mozversion:cli
       """)
--- a/testing/mozharness/mozharness/mozilla/testing/android.py
+++ b/testing/mozharness/mozharness/mozilla/testing/android.py
@@ -332,21 +332,22 @@ class AndroidMixin(object):
 
     def install_apk(self, apk):
         """
            Install the specified apk.
         """
         import mozdevice
         try:
             self.device.install_app(apk)
-        except (mozdevice.ADBError, mozdevice.ADBTimeoutError):
-            self.info('Failed to install %s on %s' %
-                      (self.installer_path, self.device_name))
+        except (mozdevice.ADBError, mozdevice.ADBTimeoutError), e:
+            self.info('Failed to install %s on %s: %s %s' %
+                      (self.installer_path, self.device_name,
+                       type(e).__name__, e))
             self.fatal('INFRA-ERROR: Failed to install %s' %
-                       self.installer_path,
+                       os.path.basename(self.installer_path),
                        EXIT_STATUS_DICT[TBPL_RETRY])
 
     def is_boot_completed(self):
         import mozdevice
         try:
             out = self.device.get_prop('sys.boot_completed', timeout=30)
             if out.strip() == '1':
                 return True
--- a/testing/mozharness/mozharness/mozilla/testing/raptor.py
+++ b/testing/mozharness/mozharness/mozilla/testing/raptor.py
@@ -393,16 +393,34 @@ class Raptor(TestingMixin, MercurialScri
             self.query_abs_dirs()['abs_test_install_dir'], 'raptor'
         )
         if self.config.get('run_local'):
             self.raptor_path = os.path.join(self.repo_path, 'testing', 'raptor')
 
     # Action methods. {{{1
     # clobber defined in BaseScript
 
+    def clobber(self):
+        # Recreate the upload directory for storing the logcat collected
+        # during apk installation.
+        super(Raptor, self).clobber()
+        upload_dir = self.query_abs_dirs()['abs_blob_upload_dir']
+        if not os.path.isdir(upload_dir):
+            self.mkdir_p(upload_dir)
+
+    def install_apk(self, apk):
+        # Override AnroidMixin's install_apk in order to capture
+        # logcat during the installation. If the installation fails,
+        # the logcat file will be left in the upload directory.
+        self.logcat_start()
+        try:
+            super(Raptor, self).install_apk(apk)
+        finally:
+            self.logcat_stop()
+
     def download_and_extract(self, extract_dirs=None, suite_categories=None):
         if 'MOZ_FETCHES' in os.environ:
             self.fetch_content()
 
         return super(Raptor, self).download_and_extract(
             suite_categories=['common', 'raptor']
         )
 
--- a/testing/raptor/raptor/raptor.py
+++ b/testing/raptor/raptor/raptor.py
@@ -884,16 +884,21 @@ class RaptorAndroid(Raptor):
             else:
                 self.device.launch_activity(self.config['binary'],
                                             self.config['activity'],
                                             self.config['intent'],
                                             extra_args=extra_args,
                                             url='about:blank',
                                             e10s=True,
                                             fail_if_running=False)
+
+            # Check if app has started and it's running
+            if not self.device.process_exist(self.config['binary']):
+                raise Exception("Error launching %s. App did not start properly!" %
+                                self.config['binary'])
         except Exception as e:
             self.log.error("Exception launching %s" % self.config['binary'])
             self.log.error("Exception: %s %s" % (type(e).__name__, str(e)))
             if self.config['power_test']:
                 finish_android_power_test(self, test_name)
             raise
 
         # give our control server the device and app info
--- a/testing/web-platform/meta/css/compositing/mix-blend-mode/mix-blend-mode-both-parent-and-blended-with-3D-transform.html.ini
+++ b/testing/web-platform/meta/css/compositing/mix-blend-mode/mix-blend-mode-both-parent-and-blended-with-3D-transform.html.ini
@@ -1,5 +1,4 @@
 [mix-blend-mode-both-parent-and-blended-with-3D-transform.html]
   expected:
     if (os == "android") and not e10s: FAIL
     if (os == "android") and e10s: FAIL
-    if webrender or (os == "android" and not e10s): FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-transforms/transform-flattening-001.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[transform-flattening-001.html]
-  expected:
-    if webrender: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-008.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[transform3d-preserve3d-008.html]
-  expected:
-    if webrender: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-009.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[transform3d-preserve3d-009.html]
-  expected:
-    if webrender: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-013.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[transform3d-preserve3d-013.html]
-  expected:
-    if webrender: FAIL
--- a/testing/web-platform/meta/html/semantics/embedded-content/media-elements/track/track-element/track-cues-cuechange.html.ini
+++ b/testing/web-platform/meta/html/semantics/embedded-content/media-elements/track/track-element/track-cues-cuechange.html.ini
@@ -1,3 +1,5 @@
 [track-cues-cuechange.html]
+  disabled:
+    if (os == 'win'): https://bugzilla.mozilla.org/show_bug.cgi?id=1550381   
   expected:
     if not debug and (os == "win") and (bits == "32"): TIMEOUT
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/service-workers/service-worker/update-bytecheck.https.html.ini
@@ -0,0 +1,4 @@
+[update-bytecheck.https.html]
+  disabled:
+    if debug and (os == "win"): https://bugzilla.mozilla.org/show_bug.cgi?id=1525580
+    if webrender and (os == "win"): https://bugzilla.mozilla.org/show_bug.cgi?id=1525580
--- a/third_party/dav1d/.gitlab-ci.yml
+++ b/third_party/dav1d/.gitlab-ci.yml
@@ -5,19 +5,19 @@ stages:
 
 style-check:
     image: registry.videolan.org:5000/dav1d-debian-unstable:20190215130514
     stage: style
     tags:
         - debian
         - amd64
     script:
-        - git grep -n -P "\t|\r| $" -- . ':(exclude)*/compat/*' && exit 1
-        - git grep -n -i "david" -- . ':(exclude)THANKS.md' ':(exclude).gitlab-ci.yml' && exit 1
-        - for i in $(git ls-files -- . ':(exclude)*/compat/*'); do
+        - git grep -I -n -P "\t|\r| $" -- . ':(exclude)*/compat/*' && exit 1
+        - git grep -I -n -i "david" -- . ':(exclude)THANKS.md' ':(exclude).gitlab-ci.yml' && exit 1
+        - git grep -I -l -z "" -- . ':(exclude)*/compat/*' | while IFS= read -r -d '' i; do
               if [ -n "$(tail -c 1 "$i")" ]; then
                   echo "No newline at end of $i";
                   exit 1;
               fi;
           done
         - git remote rm upstream 2> /dev/null || true
         - git remote add upstream https://code.videolan.org/videolan/dav1d.git
         - git fetch -q upstream master
@@ -50,16 +50,17 @@ build-debian-static:
     stage: build
     tags:
         - debian
         - amd64
     script:
         - meson build --buildtype release --default-library static --werror
         - ninja -C build
         - cd build && meson test -v
+        - nm -A -g src/libdav1d.a | grep " [ABCDGRST] " | (! grep -v " _*dav1d_")
 
 build-debian32:
     image: registry.videolan.org:5000/dav1d-debian-unstable:20181218135732
     stage: build
     tags:
         - debian
         - amd64
     script:
@@ -81,16 +82,17 @@ build-win32:
                       --werror
                       --libdir lib
                       --prefix "$(pwd)/build/dav1d_install"
                       --cross-file /opt/crossfiles/i686-w64-mingw32.meson
                       -Ddefault_library=both
         - ninja -C build
         - ninja -C build install
         - cd build && meson test -v
+        - i686-w64-mingw32-nm -A -g src/libdav1d.a | grep " [ABCDGRST] " | (! grep -E -v " \.| _*dav1d_")
     artifacts:
         name: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
         paths:
             - build/dav1d_install/
         expire_in: 1 week
 
 build-win32-unaligned-stack:
     image: registry.videolan.org:5000/vlc-debian-llvm-mingw:20190218133533
@@ -119,16 +121,17 @@ build-win64:
                       --werror
                       --libdir lib
                       --prefix "$(pwd)/build/dav1d_install"
                       --cross-file /opt/crossfiles/x86_64-w64-mingw32.meson
                       -Ddefault_library=both
         - ninja -C build
         - ninja -C build install
         - cd build && meson test -v
+        - x86_64-w64-mingw32-nm -A -g src/libdav1d.a | grep " [ABCDGRST] " | (! grep -E -v " \.| _*dav1d_")
     artifacts:
         name: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
         paths:
             - build/dav1d_install/
         expire_in: 1 week
 
 build-win-arm32:
     image: registry.videolan.org:5000/vlc-debian-llvm-mingw:20190218133533
@@ -139,32 +142,34 @@ build-win-arm32:
     script:
         - meson build --buildtype release
                       --werror
                       --libdir lib
                       --prefix "$(pwd)/build/dav1d_install"
                       --cross-file /opt/crossfiles/armv7-w64-mingw32.meson
                       -Ddefault_library=both
         - ninja -C build
+        - armv7-w64-mingw32-nm -A -g build/src/libdav1d.a | grep " [ABCDGRST] " | (! grep -E -v " \.| _*dav1d_")
 
 build-win-arm64:
     image: registry.videolan.org:5000/vlc-debian-llvm-mingw:20190218133533
     stage: build
     tags:
         - debian
         - amd64
     script:
         - meson build --buildtype release
                       --werror
                       --libdir lib
                       --prefix "$(pwd)/build/dav1d_install"
                       --cross-file /opt/crossfiles/aarch64-w64-mingw32.meson
                       -Ddefault_library=both
         - ninja -C build
         - ninja -C build install
+        - aarch64-w64-mingw32-nm -A -g build/src/libdav1d.a | grep " [ABCDGRST] " | (! grep -E -v " \.| _*dav1d_")
     artifacts:
         name: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
         paths:
             - build/dav1d_install/
         expire_in: 1 week
 
 build-debian-aarch64:
     stage: build
@@ -265,16 +270,36 @@ test-debian:
         - test -d cache/dav1d-test-data.git && GIT_DIR=cache/dav1d-test-data.git git fetch --refmap=refs/heads/master:refs/heads/master origin master
         - test -d cache/dav1d-test-data.git || git clone --bare https://code.videolan.org/videolan/dav1d-test-data.git cache/dav1d-test-data.git
         - git clone cache/dav1d-test-data.git tests/dav1d-test-data
         - meson build --buildtype release -Dtestdata_tests=true -Dlogging=false
         - ninja -C build
         - cd build && time meson test -v
     dependencies: []
 
+test-debian-unaligned-stack:
+    image: registry.videolan.org:5000/dav1d-debian-unstable:20190215130514
+    stage: test
+    tags:
+        - debian
+        - amd64
+    cache:
+        key: testdata.git-20190215
+        paths:
+            - cache/dav1d-test-data.git/
+    script:
+        - test -d cache || mkdir cache
+        - test -d cache/dav1d-test-data.git && GIT_DIR=cache/dav1d-test-data.git git fetch --refmap=refs/heads/master:refs/heads/master origin master
+        - test -d cache/dav1d-test-data.git || git clone --bare https://code.videolan.org/videolan/dav1d-test-data.git cache/dav1d-test-data.git
+        - git clone cache/dav1d-test-data.git tests/dav1d-test-data
+        - meson build --buildtype release -Dtestdata_tests=true -Dlogging=false -Dstack_alignment=16
+        - ninja -C build
+        - cd build && time meson test -v
+    dependencies: []
+
 test-debian-asan:
     image: registry.videolan.org:5000/dav1d-debian-unstable:20190215130514
     stage: test
     tags:
         - debian
         - amd64
     cache:
         key: testdata.git-20190215
--- a/third_party/dav1d/NEWS
+++ b/third_party/dav1d/NEWS
@@ -1,10 +1,18 @@
-Changes for 0.2.2 'Antelope':
-----------------------------
+Changes for 0.3.0 'Sailfish':
+------------------------------
+
+This is the final release for the numerous speed improvements of 0.3.0-rc.
+It mostly:
+ - Fixes an annoying crash on SSSE3 that happened in the itx functions
+
+
+Changes for 0.2.2 (0.3.0-rc) 'Antelope':
+-----------------------------
 
  - Large improvement on MSAC decoding with SSE, bringing 4-6% speed increase
    The impact is important on SSSE3, SSE4 and AVX-2 cpus
  - SSSE3 optimizations for all blocks size in itx
  - SSSE3 optimizations for ipred_paeth and ipref_cfl (420, 422 and 444)
  - Speed improvements on CDEF for SSE4 CPUs
  - NEON optimizations for SGR and loop filter
  - Minor crashes, improvements and build changes
--- a/third_party/dav1d/include/dav1d/common.h
+++ b/third_party/dav1d/include/dav1d/common.h
@@ -23,16 +23,17 @@
  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
 #ifndef DAV1D_COMMON_H
 #define DAV1D_COMMON_H
 
+#include <errno.h>
 #include <stddef.h>
 #include <stdint.h>
 
 #ifndef DAV1D_API
     #if defined _WIN32
       #if defined DAV1D_BUILDING_DLL
         #define DAV1D_API __declspec(dllexport)
       #else
@@ -42,16 +43,22 @@
       #if __GNUC__ >= 4
         #define DAV1D_API __attribute__ ((visibility ("default")))
       #else
         #define DAV1D_API
       #endif
     #endif
 #endif
 
+#if EPERM > 0
+#define DAV1D_ERR(e) (-(e)) ///< Negate POSIX error code.
+#else
+#define DAV1D_ERR(e) (e)
+#endif
+
 /**
  * A reference-counted object wrapper for a user-configurable pointer.
  */
 typedef struct Dav1dUserData {
     const uint8_t *data; ///< data pointer
     struct Dav1dRef *ref; ///< allocation origin
 } Dav1dUserData;
 
--- a/third_party/dav1d/include/dav1d/data.h
+++ b/third_party/dav1d/include/dav1d/data.h
@@ -57,17 +57,17 @@ DAV1D_API uint8_t * dav1d_data_create(Da
  * @param           buf The data to be wrapped.
  * @param            sz Size of the data.
  * @param free_callback Function to be called when we release our last
  *                      reference to this data. In this callback, $buf will be
  *                      the $buf argument to this function, and $cookie will
  *                      be the $cookie input argument to this function.
  * @param        cookie Opaque parameter passed to free_callback().
  *
- * @return 0 on success. A negative errno value on error.
+ * @return 0 on success. A negative DAV1D_ERR value on error.
  */
 DAV1D_API int dav1d_data_wrap(Dav1dData *data, const uint8_t *buf, size_t sz,
                               void (*free_callback)(const uint8_t *buf, void *cookie),
                               void *cookie);
 
 /**
  * Wrap a user-provided data pointer into a reference counted object.
  *
@@ -82,17 +82,17 @@ DAV1D_API int dav1d_data_wrap(Dav1dData 
  * @param     user_data The user data to be wrapped.
  * @param free_callback Function to be called when we release our last
  *                      reference to this data. In this callback, $user_data
  *                      will be the $user_data argument to this function, and
  *                      $cookie will be the $cookie input argument to this
  *                      function.
  * @param        cookie Opaque parameter passed to $free_callback.
  *
- * @return 0 on success. A negative errno value on error.
+ * @return 0 on success. A negative DAV1D_ERR value on error.
  */
 DAV1D_API int dav1d_data_wrap_user_data(Dav1dData *data,
                                         const uint8_t *user_data,
                                         void (*free_callback)(const uint8_t *user_data,
                                                               void *cookie),
                                         void *cookie);
 
 /**
--- a/third_party/dav1d/include/dav1d/dav1d.h
+++ b/third_party/dav1d/include/dav1d/dav1d.h
@@ -85,28 +85,28 @@ DAV1D_API void dav1d_default_settings(Da
  *
  * @param c_out The decoder instance to open. *c_out will be set to the
  *              allocated context.
  * @param     s Input settings context.
  *
  * @note The context must be freed using dav1d_close() when decoding is
  *       finished.
  *
- * @return 0 on success, or < 0 (a negative errno code) on error.
+ * @return 0 on success, or < 0 (a negative DAV1D_ERR code) on error.
  */
 DAV1D_API int dav1d_open(Dav1dContext **c_out, const Dav1dSettings *s);
 
 /**
  * Parse a Sequence Header OBU from bitstream data.
  *
  * @param out Output Sequence Header.
  * @param buf The data to be parser.
  * @param sz  Size of the data.
  *
- * @return 0 on success, or < 0 (a negative errno code) on error.
+ * @return 0 on success, or < 0 (a negative DAV1D_ERR code) on error.
  *
  * @note It is safe to feed this function data containing other OBUs than a
  *       Sequence Header, as they will simply be ignored. If there is more than
  *       one Sequence Header OBU present, only the last will be returned.
  */
 DAV1D_API int dav1d_parse_sequence_header(Dav1dSequenceHeader *out,
                                           const uint8_t *buf, const size_t sz);
 
@@ -114,37 +114,37 @@ DAV1D_API int dav1d_parse_sequence_heade
  * Feed bitstream data to the decoder.
  *
  * @param   c Input decoder instance.
  * @param  in Input bitstream data. On success, ownership of the reference is
  *            passed to the library.
  *
  * @return
  *         0: Success, and the data was consumed.
- *   -EAGAIN: The data can't be consumed. dav1d_get_picture() should be called
- *            to get one or more frames before the function can consume new
- *            data.
- *   other negative errno codes: Error during decoding or because of invalid
- *                               passed-in arguments.
+ *  DAV1D_ERR(EAGAIN): The data can't be consumed. dav1d_get_picture() should
+ *                     be called to get one or more frames before the function
+ *                     can consume new data.
+ *  other negative DAV1D_ERR codes: Error during decoding or because of invalid
+ *                                  passed-in arguments.
  */
 DAV1D_API int dav1d_send_data(Dav1dContext *c, Dav1dData *in);
 
 /**
  * Return a decoded picture.
  *
  * @param   c Input decoder instance.
  * @param out Output frame. The caller assumes ownership of the returned
  *            reference.
  *
  * @return
  *         0: Success, and a frame is returned.
- *   -EAGAIN: Not enough data to output a frame. dav1d_send_data() should be
- *            called with new input.
- *   other negative errno codes: Error during decoding or because of invalid
- *                               passed-in arguments.
+ *  DAV1D_ERR(EAGAIN): Not enough data to output a frame. dav1d_send_data()
+ *                     should be called with new input.
+ *  other negative DAV1D_ERR codes: Error during decoding or because of invalid
+ *                                  passed-in arguments.
  *
  * @note To drain buffered frames from the decoder (i.e. on end of stream),
  *       call this function until it returns -EAGAIN.
  *
  * @code{.c}
  *  Dav1dData data = { 0 };
  *  Dav1dPicture p = { 0 };
  *  int res;
--- a/third_party/dav1d/include/dav1d/picture.h
+++ b/third_party/dav1d/include/dav1d/picture.h
@@ -104,17 +104,17 @@ typedef struct Dav1dPicAllocator {
      *             stride[1].
      *             The allocator can fill the pic allocator_data pointer with
      *             a custom pointer that will be passed to
      *             release_picture_callback().
      * @param cookie Custom pointer passed to all calls.
      *
      * @note No fields other than data, stride and allocator_data must be filled
      *       by this callback.
-     * @return 0 on success. A negative errno value on error.
+     * @return 0 on success. A negative DAV1D_ERR value on error.
      */
     int (*alloc_picture_callback)(Dav1dPicture *pic, void *cookie);
     /**
      * Release the picture buffer.
      *
      * If frame threading is used, this function may be called by the main
      * thread (the thread which calls dav1d_get_picture()) or any of the frame
      * threads and thus must be thread-safe. If frame threading is not used,
--- a/third_party/dav1d/meson.build
+++ b/third_party/dav1d/meson.build
@@ -18,24 +18,24 @@
 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 project('dav1d', ['c'],
-    version: '0.2.2',
+    version: '0.3.0',
     default_options: ['c_std=c99',
                       'warning_level=2',
                       'buildtype=release',
                       'b_ndebug=if-release'],
     meson_version: '>= 0.47.0')
 
-dav1d_soname_version   = '1.0.1'
+dav1d_soname_version   = '1.1.0'
 dav1d_api_version_array    = dav1d_soname_version.split('.')
 dav1d_api_version_major    = dav1d_api_version_array[0]
 dav1d_api_version_minor    = dav1d_api_version_array[1]
 dav1d_api_version_revision = dav1d_api_version_array[2]
 
 dav1d_src_root = meson.current_source_dir()
 cc = meson.get_compiler('c')
 
--- a/third_party/dav1d/src/arm/32/mc.S
+++ b/third_party/dav1d/src/arm/32/mc.S
@@ -22,17 +22,17 @@
  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
 #include "src/arm/asm.S"
-#include "src/arm/32/util.S"
+#include "util.S"
 
 .macro avg dst0, dst1, t0, t1, t2, t3
         vld1.16         {\t0,\t1},   [r2, :128]!
         vld1.16         {\t2,\t3},   [r3, :128]!
         vadd.i16        \t0,   \t0,  \t2
         vadd.i16        \t1,   \t1,  \t3
         vqrshrun.s16    \dst0, \t0,  #5
         vqrshrun.s16    \dst1, \t1,  #5
--- a/third_party/dav1d/src/arm/64/mc.S
+++ b/third_party/dav1d/src/arm/64/mc.S
@@ -22,17 +22,17 @@
  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
 #include "src/arm/asm.S"
-#include "src/arm/64/util.S"
+#include "util.S"
 
 .macro avg dst, t0, t1
         ld1             {\t0\().8h},   [x2],  16
         ld1             {\t1\().8h},   [x3],  16
         add             \t0\().8h,   \t0\().8h,   \t1\().8h
         sqrshrun        \dst\().8b,  \t0\().8h,   #5
 .endm
 
@@ -698,17 +698,17 @@ endfunc
         st1             {\r2\().8h, \r3\().8h}, [x8], \strd
 .endif
 .endm
 
 .macro make_8tap_fn op, type, type_h, type_v
 function \op\()_8tap_\type\()_8bpc_neon, export=1
         mov             x8,  \type_h
         mov             x9,  \type_v
-        b               \op\()_8tap\()_neon
+        b               \op\()_8tap_neon
 endfunc
 .endm
 
 // No spaces in these expressions, due to gas-preprocessor.
 #define REGULAR ((0*15<<7)|3*15)
 #define SMOOTH  ((1*15<<7)|4*15)
 #define SHARP   ((2*15<<7)|3*15)
 
new file mode 100644
--- /dev/null
+++ b/third_party/dav1d/src/arm/64/msac.S
@@ -0,0 +1,280 @@
+/*
+ * Copyright © 2019, VideoLAN and dav1d authors
+ * Copyright © 2019, Martin Storsjo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "src/arm/asm.S"
+#include "util.S"
+
+#define BUF_POS 0
+#define BUF_END 8
+#define DIF 16
+#define RNG 24
+#define CNT 28
+#define ALLOW_UPDATE_CDF 32
+
+const coeffs
+        .short 60, 56, 52, 48, 44, 40, 36, 32, 28, 24, 20, 16, 12, 8, 4, 0
+        .short 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 0, 0
+endconst
+
+const bits
+        .short   0x1,   0x2,   0x4,   0x8,   0x10,   0x20,   0x40,   0x80
+        .short 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000
+endconst
+
+.macro ld1_n d0, d1, src, sz, n
+.if \n <= 8
+        ld1             {\d0\sz},  [\src]
+.else
+        ld1             {\d0\sz, \d1\sz},  [\src]
+.endif
+.endm
+
+.macro st1_n s0, s1, dst, sz, n
+.if \n <= 8
+        st1             {\s0\sz},  [\dst]
+.else
+        st1             {\s0\sz, \s1\sz},  [\dst]
+.endif
+.endm
+
+.macro ushr_n d0, d1, s0, s1, shift, sz, n
+        ushr            \d0\sz,  \s0\sz,  \shift
+.if \n == 16
+        ushr            \d1\sz,  \s1\sz,  \shift
+.endif
+.endm
+
+.macro add_n d0, d1, s0, s1, s2, s3, sz, n
+        add             \d0\sz,  \s0\sz,  \s2\sz
+.if \n == 16
+        add             \d1\sz,  \s1\sz,  \s3\sz
+.endif
+.endm
+
+.macro sub_n d0, d1, s0, s1, s2, s3, sz, n
+        sub             \d0\sz,  \s0\sz,  \s2\sz
+.if \n == 16
+        sub             \d1\sz,  \s1\sz,  \s3\sz
+.endif
+.endm
+
+.macro and_n d0, d1, s0, s1, s2, s3, sz, n
+        and             \d0\sz,  \s0\sz,  \s2\sz
+.if \n == 16
+        and             \d1\sz,  \s1\sz,  \s3\sz
+.endif
+.endm
+
+.macro cmhs_n d0, d1, s0, s1, s2, s3, sz, n
+        cmhs            \d0\sz,  \s0\sz,  \s2\sz
+.if \n == 16
+        cmhs            \d1\sz,  \s1\sz,  \s3\sz
+.endif
+.endm
+
+.macro urhadd_n d0, d1, s0, s1, s2, s3, sz, n
+        urhadd          \d0\sz,  \s0\sz,  \s2\sz
+.if \n == 16
+        urhadd          \d1\sz,  \s1\sz,  \s3\sz
+.endif
+.endm
+
+.macro sshl_n d0, d1, s0, s1, s2, s3, sz, n
+        sshl            \d0\sz,  \s0\sz,  \s2\sz
+.if \n == 16
+        sshl            \d1\sz,  \s1\sz,  \s3\sz
+.endif
+.endm
+
+.macro umull_n d0, d1, d2, d3, s0, s1, s2, s3, n
+        umull           \d0\().4s, \s0\().4h,  \s2\().4h
+.if \n >= 8
+        umull2          \d1\().4s, \s0\().8h,  \s2\().8h
+.endif
+.if \n == 16
+        umull           \d2\().4s, \s1\().4h,  \s3\().4h
+        umull2          \d3\().4s, \s1\().8h,  \s3\().8h
+.endif
+.endm
+
+.macro shrn_n d0, d1, s0, s1, s2, s3, shift, n
+        shrn            \d0\().4h,  \s0\().4s, \shift
+.if \n >= 8
+        shrn2           \d0\().8h,  \s1\().4s, \shift
+.endif
+.if \n == 16
+        shrn            \d1\().4h,  \s2\().4s, \shift
+        shrn2           \d1\().8h,  \s3\().4s, \shift
+.endif
+.endm
+
+.macro str_n            idx0, idx1, dstreg, dstoff, n
+        str             q\idx0,  [\dstreg, \dstoff]
+.if \n == 16
+        str             q\idx1,  [\dstreg, \dstoff + 16]
+.endif
+.endm
+
+// unsigned dav1d_msac_decode_symbol_adapt4_neon(MsacContext *s, uint16_t *cdf,
+//                                               size_t n_symbols);
+
+function msac_decode_symbol_adapt4_neon, export=1
+.macro decode_update sz, szb, n
+        sub             sp,  sp,  #48
+        add             x8,  x0,  #RNG
+        ld1_n           v0,  v1,  x1,  \sz, \n                    // cdf
+        ld1r            {v4\sz},  [x8]                            // rng
+        movrel          x9,  coeffs, 32
+        sub             x9,  x9,  x2, lsl #1
+        ushr_n          v2,  v3,  v0,  v1,  #6, \sz, \n           // cdf >> EC_PROB_SHIFT
+        str             h4,  [sp, #14]                            // store original u = s->rng
+        ushr            v4\sz,  v4\sz,  #8                        // r = rng >> 8