Merge mozilla-central to inbound
authorDorel Luca <dluca@mozilla.com>
Sat, 20 Apr 2019 00:46:59 +0300
changeset 470285 b330e84e1458ac4e11343638f622329a48bb952b
parent 470284 0a223da4c1e482cff28ae415e52ef50853b85ff3 (current diff)
parent 470206 30b70a449280e6df10496a51e89c01b8c98bb92e (diff)
child 470286 1305877ca306a4ff075c9ef84acf8ba4141e8888
push id35892
push userrgurzau@mozilla.com
push dateSat, 20 Apr 2019 09:55:32 +0000
treeherdermozilla-central@a092972b53f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
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 inbound
devtools/server/actors/utils/closest-scripts.js
devtools/server/tests/unit/test_setBreakpoint-at-the-beginning-of-a-line.js
devtools/server/tests/unit/test_setBreakpoint-at-the-end-of-a-line.js
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -119,42 +119,40 @@ var gSync = {
 
     this._definePrefGetters();
 
     if (!this.SYNC_ENABLED) {
       this.onSyncDisabled();
       return;
     }
 
-    // Label for the sync buttons, also set on the icon for accessibility.
-    let syncIcon = document.getElementById("appMenu-fxa-icon");
-    if (!syncIcon) {
+    this._generateNodeGetters();
+
+    // Label for the sync buttons.
+    if (!this.appMenuLabel) {
       // We are in a window without our elements - just abort now, without
       // setting this._initialized, so we don't attempt to remove observers.
       return;
     }
     let syncNow = document.getElementById("PanelUI-remotetabs-syncnow");
     let label = this.syncStrings.GetStringFromName("syncnow.label");
-    syncIcon.setAttribute("label", label);
     syncNow.setAttribute("label", label);
     // We start with every menuitem hidden (except for the "setup sync" state),
     // so that we don't need to init the sync UI on windows like pageInfo.xul
     // (see bug 1384856).
     // maybeUpdateUIState() also optimizes for this - if we should be in the
     // "setup sync" state, that function assumes we are already in it and
     // doesn't re-initialize the UI elements.
     document.getElementById("sync-setup").hidden = false;
     document.getElementById("PanelUI-remotetabs-setupsync").hidden = false;
 
     for (let topic of this._obs) {
       Services.obs.addObserver(this, topic, true);
     }
 
-    this._generateNodeGetters();
-
     this.maybeUpdateUIState();
 
     EnsureFxAccountsWebChannel();
 
     this._initialized = true;
   },
 
   uninit() {
@@ -193,17 +191,17 @@ var gSync = {
     }
   },
 
   updateAllUI(state) {
     this.updatePanelPopup(state);
     this.updateState(state);
     this.updateSyncButtonsTooltip(state);
     this.updateSyncStatus(state);
-    this.updateFxAToolbarPanel(state);
+    this.updateFxAPanel(state);
   },
 
   updateSendToDeviceTitle() {
     let string = gBrowserBundle.GetStringFromName("sendTabsToDevice.label");
     let title = PluralForm.get(1, string).replace("#1", 1);
     if (gBrowser.selectedTab.multiselected) {
       let tabCount = gBrowser.selectedTabs.length;
       title = PluralForm.get(tabCount, string).replace("#1", tabCount);
@@ -296,21 +294,17 @@ var gSync = {
     if (anchor.getAttribute("open") == "true") {
       PanelUI.hide();
     } else {
       this.emitFxaToolbarTelemetry("toolbar_icon");
       PanelUI.showSubView(viewId, anchor, aEvent);
     }
   },
 
-  updateFxAToolbarPanel(state = {}) {
-    if (!gFxaToolbarEnabled) {
-      return;
-    }
-
+  updateFxAPanel(state = {}) {
     const mainWindowEl = document.documentElement;
 
     // The Firefox Account toolbar currently handles 3 different states for
     // users. The default `not_configured state shows an empty avatar, `unverified`
     // state shows an avatar with an email icon and the `verified` state will show
     // the users custom profile image or a filled avatar.
     let stateValue = "not_configured";
     if (state.status === UIState.STATUS_LOGIN_FAILED || state.status === UIState.STATUS_NOT_VERIFIED) {
@@ -369,25 +363,22 @@ var gSync = {
       const hasAvatar = state.avatarURL && !state.avatarURL.includes(FXA_NO_AVATAR_ZEROS);
       let extraOptions = {"fxa_status": state.status, "fxa_avatar": hasAvatar ? "true" : "false"};
       Services.telemetry.recordEvent("fxa_avatar_menu", "click", type, null, extraOptions);
     }
   },
 
   updatePanelPopup(state) {
     let defaultLabel = this.appMenuStatus.getAttribute("defaultlabel");
-    // The localization string is for the signed in text, but it's the default text as well
-    let defaultTooltiptext = this.appMenuStatus.getAttribute("signedinTooltiptext");
-
     const status = state.status;
     // Reset the status bar to its original state.
     this.appMenuLabel.setAttribute("label", defaultLabel);
-    this.appMenuStatus.setAttribute("tooltiptext", defaultTooltiptext);
     this.appMenuContainer.removeAttribute("fxastatus");
     this.appMenuAvatar.style.removeProperty("list-style-image");
+    this.appMenuLabel.classList.remove("subviewbutton-nav");
 
     if (status == UIState.STATUS_NOT_CONFIGURED) {
       return;
     }
 
     // At this point we consider sync to be configured (but still can be in an error state).
     if (status == UIState.STATUS_LOGIN_FAILED) {
       let tooltipDescription = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
@@ -403,16 +394,18 @@ var gSync = {
       this.appMenuLabel.setAttribute("label", unverifiedLabel);
       this.appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
       return;
     }
 
     // At this point we consider sync to be logged-in.
     this.appMenuContainer.setAttribute("fxastatus", "signedin");
     this.appMenuLabel.setAttribute("label", state.displayName || state.email);
+    this.appMenuLabel.classList.add("subviewbutton-nav");
+    this.appMenuStatus.removeAttribute("tooltiptext");
 
     if (state.avatarURL) {
       let bgImage = "url(\"" + state.avatarURL + "\")";
       this.appMenuAvatar.style.listStyleImage = bgImage;
 
       let img = new Image();
       img.onerror = () => {
         // Clear the image if it has trouble loading. Since this callback is asynchronous
@@ -447,31 +440,32 @@ var gSync = {
     if (state.syncing != syncingUI) { // Do we need to update the UI?
       state.syncing ? this.onActivityStart() : this.onActivityStop();
     }
   },
 
   onMenuPanelCommand() {
     switch (this.appMenuContainer.getAttribute("fxastatus")) {
     case "signedin":
-      this.openPrefs("menupanel", "fxaSignedin");
+      const panel = document.getElementById("appMenu-fxa-status");
+      PanelUI.showSubView("PanelUI-fxa", panel);
       break;
     case "error":
       if (this.appMenuContainer.getAttribute("fxastatus") == "unverified") {
         this.openPrefs("menupanel", "fxaError");
       } else {
         this.openSignInAgainPage("menupanel");
       }
+      PanelUI.hide();
       break;
     default:
       this.openPrefs("menupanel", "fxa");
+      PanelUI.hide();
       break;
     }
-
-    PanelUI.hide();
   },
 
   async openSignInAgainPage(entryPoint) {
     const url = await FxAccounts.config.promiseForceSigninURI(entryPoint);
     switchToTabHavingURI(url, true, {
       replaceQueryString: true,
       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
     });
@@ -781,17 +775,16 @@ var gSync = {
   onActivityStart() {
     clearTimeout(this._syncAnimationTimer);
     this._syncStartTime = Date.now();
 
     let label = this.syncStrings.GetStringFromName("syncingtabs.label");
     let remotetabsSyncNowEl = document.getElementById("PanelUI-remotetabs-syncnow");
     let fxaMenuSyncNowEl = document.getElementById("PanelUI-fxa-menu-syncnow-button");
     let syncElements = [
-      document.getElementById("appMenu-fxa-icon"),
       remotetabsSyncNowEl,
       fxaMenuSyncNowEl,
     ];
 
     syncElements.forEach((el) => {
       el.setAttribute("syncstatus", "active");
       el.setAttribute("disabled", "true");
     });
@@ -801,17 +794,16 @@ var gSync = {
   },
 
   _onActivityStop() {
     if (!gBrowser)
       return;
 
     let label = this.syncStrings.GetStringFromName("syncnow.label");
     let syncElements = [
-      document.getElementById("appMenu-fxa-icon"),
       document.getElementById("PanelUI-remotetabs-syncnow"),
       document.getElementById("PanelUI-fxa-menu-syncnow-button"),
     ];
 
     syncElements.forEach((el) => {
       el.removeAttribute("syncstatus");
       el.removeAttribute("disabled");
       el.setAttribute("label", label);
@@ -902,24 +894,21 @@ var gSync = {
     } else if (status == UIState.STATUS_LOGIN_FAILED) {
       // "need to reconnect/re-enter your password"
       tooltiptext = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
     } else {
       // Sync appears configured - format the "last synced at" time.
       tooltiptext = this.formatLastSyncDate(state.lastSync);
     }
 
-    let syncIcon = document.getElementById("appMenu-fxa-icon");
-    if (syncIcon) {
+    if (this.appMenuLabel) {
       let syncNow = document.getElementById("PanelUI-remotetabs-syncnow");
       if (tooltiptext) {
-        syncIcon.setAttribute("tooltiptext", tooltiptext);
         syncNow.setAttribute("tooltiptext", tooltiptext);
       } else {
-        syncIcon.removeAttribute("tooltiptext");
         syncNow.removeAttribute("tooltiptext");
       }
     }
   },
 
   get relativeTimeFormat() {
     delete this.relativeTimeFormat;
     return this.relativeTimeFormat = new Services.intl.RelativeTimeFormat(undefined, {style: "long"});
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -485,38 +485,40 @@ var gNavigatorBundle = {
 };
 
 function showFxaToolbarMenu(enable) {
   // We only show the Firefox Account toolbar menu if the feature is enabled and
   // if sync is enabled.
   const syncEnabled = Services.prefs.getBoolPref("identity.fxaccounts.enabled", false);
   const mainWindowEl = document.documentElement;
   const fxaPanelEl = document.getElementById("PanelUI-fxa");
+
+  mainWindowEl.setAttribute("fxastatus", "not_configured");
+  fxaPanelEl.addEventListener("ViewShowing", gSync.updateSendToDeviceTitle);
+
   if (enable && syncEnabled) {
-    fxaPanelEl.addEventListener("ViewShowing", gSync.updateSendToDeviceTitle);
-
-    mainWindowEl.setAttribute("fxastatus", "not_configured");
+    mainWindowEl.setAttribute("fxatoolbarmenu", "visible");
+
     // We have to manually update the sync state UI when toggling the FxA toolbar
     // because it could show an invalid icon if the user is logged in and no sync
     // event was performed yet.
     gSync.maybeUpdateUIState();
 
     // Enabled FxA toolbar telemetry
     Services.telemetry.setEventRecordingEnabled("fxa_avatar_menu", true);
 
     // We set an attribute here so that we can toggle the custom
     // badge depending on whether the FxA menu was ever accessed.
     if (!gFxaToolbarAccessed) {
       mainWindowEl.setAttribute("fxa_avatar_badged", "badged");
     } else {
       mainWindowEl.removeAttribute("fxa_avatar_badged");
     }
   } else {
-    mainWindowEl.removeAttribute("fxastatus");
-    fxaPanelEl.removeEventListener("ViewShowing", gSync.updateSendToDeviceTitle);
+    mainWindowEl.removeAttribute("fxatoolbarmenu");
   }
 }
 
 function UpdateBackForwardCommands(aWebNavigation) {
   var backCommand = document.getElementById("Browser:Back");
   var forwardCommand = document.getElementById("Browser:Forward");
 
   // Avoid setting attributes on commands if the value hasn't changed!
--- a/browser/base/content/test/general/browser_bug839103.js
+++ b/browser/base/content/test/general/browser_bug839103.js
@@ -54,17 +54,17 @@ async function testBody(testRoot) {
   info("received dynamic style sheet applicable state change event");
   is(evt.type, "StyleSheetApplicableStateChanged", "evt.type has expected value");
   is(evt.target, doc, "event targets correct document");
   is(evt.stylesheet, link.sheet, "evt.stylesheet has the right value");
   is(evt.applicable, true, "evt.applicable has the right value");
 
   stateChanged =
     ContentTaskUtils.waitForEvent(this, "StyleSheetApplicableStateChanged", true);
-  link.disabled = true;
+  link.sheet.disabled = true;
 
   evt = await stateChanged;
   is(evt.type, "StyleSheetApplicableStateChanged", "evt.type has expected value");
   info("received dynamic style sheet applicable state change event after media=\"\" changed");
   is(evt.target, doc, "event targets correct document");
   is(evt.stylesheet, link.sheet, "evt.stylesheet has the right value");
   is(evt.applicable, false, "evt.applicable has the right value");
 
--- a/browser/base/content/test/sync/browser_sync.js
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -33,22 +33,19 @@ add_task(async function test_ui_state_si
   const origRelativeTimeFormat = gSync.relativeTimeFormat;
   gSync.relativeTimeFormat = {
     formatBestUnit(date) {
       return origRelativeTimeFormat.formatBestUnit(date, {now: relativeDateAnchor});
     },
   };
 
   gSync.updateAllUI(state);
-
-  let statusBarTooltip = gSync.appMenuStatus.getAttribute("signedinTooltiptext");
   let lastSyncTooltip = gSync.formatLastSyncDate(new Date(state.lastSync));
   checkPanelUIStatusBar({
     label: "Foo Bar",
-    tooltip: statusBarTooltip,
     fxastatus: "signedin",
     avatarURL: "https://foo.bar",
     syncing: false,
     syncNowTooltip: lastSyncTooltip,
   });
   checkRemoteTabsPanel("PanelUI-remotetabs-main", false);
   checkMenuBarItem("sync-syncnowitem");
   checkFxaToolbarButtonPanel("PanelUI-fxa-menu");
@@ -63,17 +60,16 @@ add_task(async function test_ui_state_sy
     displayName: "Foo Bar",
     avatarURL: "https://foo.bar",
     lastSync: new Date(),
     syncing: true,
   };
 
   gSync.updateAllUI(state);
 
-  checkSyncNowButton("appMenu-fxa-icon", true);
   checkSyncNowButton("PanelUI-remotetabs-syncnow", true);
 
   // Be good citizens and remove the "syncing" state.
   gSync.updateAllUI({
     status: UIState.STATUS_SIGNED_IN,
     email: "foo@bar.com",
     lastSync: new Date(),
     syncing: false,
@@ -85,20 +81,18 @@ add_task(async function test_ui_state_sy
 add_task(async function test_ui_state_unconfigured() {
   let state = {
     status: UIState.STATUS_NOT_CONFIGURED,
   };
 
   gSync.updateAllUI(state);
 
   let signedOffLabel = gSync.appMenuStatus.getAttribute("defaultlabel");
-  let statusBarTooltip = gSync.appMenuStatus.getAttribute("signedinTooltiptext");
   checkPanelUIStatusBar({
     label: signedOffLabel,
-    tooltip: statusBarTooltip,
   });
   checkRemoteTabsPanel("PanelUI-remotetabs-setupsync");
   checkMenuBarItem("sync-setup");
   checkFxaToolbarButtonPanel("PanelUI-fxa-signin");
   checkFxaToolbarButtonAvatar("not_configured");
 });
 
 add_task(async function test_ui_state_unverified() {
@@ -153,31 +147,29 @@ add_task(async function test_ui_state_lo
 
 function checkPanelUIStatusBar({label, tooltip, fxastatus, avatarURL, syncing, syncNowTooltip}) {
   let labelNode = document.getElementById("appMenu-fxa-label");
   let tooltipNode = document.getElementById("appMenu-fxa-status");
   let statusNode = document.getElementById("appMenu-fxa-container");
   let avatar = document.getElementById("appMenu-fxa-avatar");
 
   is(labelNode.getAttribute("label"), label, "fxa label has the right value");
-  is(tooltipNode.getAttribute("tooltiptext"), tooltip, "fxa tooltip has the right value");
+  if (tooltipNode.getAttribute("tooltiptext")) {
+    is(tooltipNode.getAttribute("tooltiptext"), tooltip, "fxa tooltip has the right value");
+  }
   if (fxastatus) {
     is(statusNode.getAttribute("fxastatus"), fxastatus, "fxa fxastatus has the right value");
   } else {
     ok(!statusNode.hasAttribute("fxastatus"), "fxastatus is unset");
   }
   if (avatarURL) {
     is(avatar.style.listStyleImage, `url("${avatarURL}")`, "fxa avatar URL is set");
   } else {
     ok(!statusNode.style.listStyleImage, "fxa avatar URL is unset");
   }
-
-  if (syncing != undefined && syncNowTooltip != undefined) {
-    checkSyncNowButton("appMenu-fxa-icon", syncing, syncNowTooltip);
-  }
 }
 
 function checkRemoteTabsPanel(expectedShownItemId, syncing, syncNowTooltip) {
   checkItemsVisibilities(["PanelUI-remotetabs-main",
                           "PanelUI-remotetabs-setupsync",
                           "PanelUI-remotetabs-reauthsync",
                           "PanelUI-remotetabs-unverified"],
                           expectedShownItemId);
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -215,33 +215,29 @@
                        label-update-restart="&updateRestart.panelUI.label2;"
                        oncommand="PanelUI._onBannerItemSelected(event)"
                        wrap="true"
                        hidden="true"/>
         <toolbaritem id="appMenu-fxa-container" class="toolbaritem-combined-buttons sync-ui-item">
           <hbox id="appMenu-fxa-status"
                 flex="1"
                 defaultlabel="&fxaSignIn.label;"
-                signedinTooltiptext="&fxaSignedIn.tooltip;"
+# Despite the name, the tooltip says "Open Sync Preferences" and it is only used when *not* signed in.
+# Bug 1542334 changed the behaviour of the item when signed in so the tooltip was no longer appropriate there.
                 tooltiptext="&fxaSignedIn.tooltip;"
                 errorlabel="&fxaSignInError.label;"
                 unverifiedlabel="&fxaUnverified.label;"
                 onclick="if (event.which == 1) gSync.onMenuPanelCommand();">
             <image id="appMenu-fxa-avatar"/>
             <toolbarbutton id="appMenu-fxa-label"
-                           class="subviewbutton subviewbutton-iconic"
-                           label="&fxaSignIn.label;"
-                           fxabrandname="&syncBrand.fxAccount.label;"/>
+                class="subviewbutton subviewbutton-iconic"
+                label="&fxaSignIn.label;"
+                fxabrandname="&syncBrand.fxAccount.label;"
+                closemenu="none"/>
           </hbox>
-          <toolbarseparator orient="vertical"/>
-          <toolbarbutton id="appMenu-fxa-icon"
-                         class="subviewbutton subviewbutton-iconic"
-                         onmouseover="gSync.refreshSyncButtonsTooltip();"
-                         oncommand="gSync.doSync();"
-                         closemenu="none"/>
         </toolbaritem>
         <toolbarseparator class="sync-ui-item"/>
         <toolbaritem>
           <toolbarbutton id="appMenu-tp-button"
                class="subviewbutton subviewbutton-iconic"
                oncommand="ContentBlocking.openPreferences('appMenu-trackingprotection');">
             <image id="appMenu-tp-icon" class="toolbarbutton-icon"/>
             <label id="appMenu-tp-label" class="toolbarbutton-text"/>
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -518,33 +518,31 @@ toolbarbutton[constrain-size="true"][cui
 /* FxAccount indicator bits. */
 
 /* Add the .toolbaritem-combined-buttons class to increase the specificity so as
  * to override the end margin for .toolbaritem-combined-buttons items further down. */
 #appMenu-fxa-container.toolbaritem-combined-buttons:not([fxastatus="signedin"]) {
   margin-inline-end: 0;
 }
 
-#appMenu-fxa-label,
-#appMenu-fxa-icon {
+#appMenu-fxa-label {
   -moz-context-properties: fill;
   fill: currentColor;
   list-style-image: url(chrome://browser/skin/sync.svg);
 }
 
 #appMenu-fxa-label {
   -moz-box-flex: 1;
 }
 
 @keyframes syncRotate {
   from { transform: rotate(0); }
   to { transform: rotate(360deg); }
 }
 
-#appMenu-fxa-icon[syncstatus="active"] > .toolbarbutton-icon,
 #PanelUI-fxa-menu-syncnow-button[syncstatus="active"] > .toolbarbutton-icon,
 #PanelUI-remotetabs-syncnow[syncstatus="active"] > .toolbarbutton-icon {
   animation: syncRotate 0.8s linear infinite;
   fill: var(--toolbarbutton-icon-fill-attention);
 }
 
 #appMenu-fxa-status {
   -moz-box-align: center;
@@ -552,18 +550,16 @@ toolbarbutton[constrain-size="true"][cui
 
 #appMenu-fxa-avatar {
   pointer-events: none;
   list-style-image: url(chrome://browser/skin/fxa/default-avatar.svg);
 }
 
 /* Handle different UI states. */
 #appMenu-fxa-container[fxastatus="signedin"] > #appMenu-fxa-status > #appMenu-fxa-label > .toolbarbutton-icon,
-#appMenu-fxa-container:not([fxastatus="signedin"]) > toolbarseparator,
-#appMenu-fxa-container:not([fxastatus="signedin"]) > #appMenu-fxa-icon,
 #appMenu-fxa-container:not([fxastatus="signedin"]) > #appMenu-fxa-status > #appMenu-fxa-avatar {
   display: none;
 }
 
 #appMenu-fxa-container[fxastatus="signedin"] > #appMenu-fxa-status > #appMenu-fxa-label {
   /* 12px space before the avatar, then 16px for the avatar */
   padding-inline-start: 28px;
   margin-inline-start: -28px;
@@ -690,17 +686,17 @@ toolbarbutton[constrain-size="true"][cui
 :root[fxastatus="not_configured"] #fxa-avatar-image {
   list-style-image: url(chrome://browser/skin/fxa/avatar-empty.svg);
 }
 
 :root[fxastatus="not_configured"][fxa_avatar_badged="badged"] #fxa-avatar-image {
   list-style-image: url(chrome://browser/skin/fxa/avatar-empty-badged.svg);
 }
 
-:root:not([fxastatus]) #fxa-toolbar-menu-button {
+:root:not([fxatoolbarmenu]) #fxa-toolbar-menu-button {
   display: none;
 }
 
 :root[fxastatus="signedin"] #fxa-menu-avatar,
 :root[fxastatus="signedin"] #fxa-avatar-image {
   border-radius: 50%;
   list-style-image: var(--avatar-image-url);
 }
@@ -1164,17 +1160,16 @@ panelmultiview .toolbaritem-combined-but
   background-color: var(--arrowpanel-dimmed-even-further);
 }
 
 #appMenu-zoomReset-button > .toolbarbutton-text {
   min-width: calc(3ch + 8px);
   text-align: center;
 }
 
-.toolbaritem-combined-buttons > toolbarseparator[orient="vertical"] + .subviewbutton,
 #appMenu-zoom-controls > toolbarseparator[orient="vertical"] + .subviewbutton {
   margin-inline-start: 0;
 }
 
 .PanelUI-subView .toolbaritem-combined-buttons > .subviewbutton-iconic > .toolbarbutton-text,
 .PanelUI-subView .toolbaritem-combined-buttons > .subviewbutton:not(.subviewbutton-iconic) > .toolbarbutton-icon {
   display: none;
 }
@@ -1325,17 +1320,16 @@ menuitem.panel-subview-footer@menuStateA
 #PanelUI-historyItems > toolbarbutton {
   list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.svg");
   -moz-context-properties: fill;
   fill: currentColor;
 }
 
 #appMenu-fxa-avatar,
 #appMenu-fxa-label > .toolbarbutton-icon,
-#appMenu-fxa-icon > .toolbarbutton-icon,
 #PanelUI-containersItems > .subviewbutton > .toolbarbutton-icon,
 #PanelUI-remotetabs-tabslist > toolbarbutton[itemtype="tab"] > .toolbarbutton-icon,
 #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
   width: 16px;
   height: 16px;
 }
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -9,17 +9,16 @@
 const { Ci } = require("chrome");
 const { setBreakpointAtEntryPoints } = require("devtools/server/actors/breakpoint");
 const { createValueGrip } = require("devtools/server/actors/object/utils");
 const { ActorClassWithSpec } = require("devtools/shared/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, fetch } = DevToolsUtils;
 const { joinURI } = require("devtools/shared/path");
 const { sourceSpec } = require("devtools/shared/specs/source");
-const { findClosestScriptBySource } = require("devtools/server/actors/utils/closest-scripts");
 
 loader.lazyRequireGetter(this, "arrayBufferGrip", "devtools/server/actors/array-buffer", true);
 
 function isEvalSource(source) {
   const introType = source.introductionType;
 
   // Script elements that are dynamically created are treated as eval sources.
   // We detect these by looking at whether there was another script on the stack
@@ -428,24 +427,24 @@ const SourceActor = ActorClassWithSpec(s
    * @param BreakpointActor actor
    *        The BreakpointActor to be set as a breakpoint handler.
    *
    * @returns A Promise that resolves to the given BreakpointActor.
    */
   applyBreakpoint: function(actor) {
     const { line, column } = actor.location;
 
-    // Find all scripts that match the given source actor and line
-    // number.
-    let scripts = this._findDebuggeeScripts({ line });
-    scripts = scripts.filter((script) => !actor.hasScript(script));
-
     // Find all entry points that correspond to the given location.
     const entryPoints = [];
     if (column === undefined) {
+      // Find all scripts that match the given source actor and line
+      // number.
+      const scripts = this._findDebuggeeScripts({ line })
+        .filter((script) => !actor.hasScript(script));
+
       // This is a line breakpoint, so we add a breakpoint on the first
       // breakpoint on the line.
       const lineMatches = [];
       for (const script of scripts) {
         const possibleBreakpoints = script.getPossibleBreakpoints({ line });
         for (const possibleBreakpoint of possibleBreakpoints) {
           lineMatches.push({ ...possibleBreakpoint, script });
         }
@@ -461,64 +460,33 @@ const SourceActor = ActorClassWithSpec(s
         const firstColumnMatches = lineMatches
           .filter(m => m.columnNumber === firstColumn);
 
         for (const { script, offset } of firstColumnMatches) {
           entryPoints.push({ script, offsets: [offset] });
         }
       }
     } else {
-      // Compute columnToOffsetMaps for each script so that we can
-      // find matching entrypoints for the column breakpoint.
-      const columnToOffsetMaps = scripts.map(script =>
-        [
-          script,
-          script.getPossibleBreakpoints({ line }),
-        ]
-      );
-
-      // This is a column breakpoint, so we are interested in all column
-      // offsets that correspond to the given line *and* column number.
-      for (const [script, columnToOffsetMap] of columnToOffsetMaps) {
-        for (const { columnNumber, offset } of columnToOffsetMap) {
-          if (columnNumber >= column && columnNumber <= column + 1) {
-            entryPoints.push({ script, offsets: [offset] });
-          }
-        }
-      }
+      // Find all scripts that match the given source actor, line,
+      // and column number.
+      const scripts = this._findDebuggeeScripts({ line, column })
+        .filter((script) => !actor.hasScript(script));
 
-      // If we don't find any matching entrypoints,
-      // then we should see if the breakpoint comes before or after the column offsets.
-      if (entryPoints.length === 0) {
-        // It's not entirely clear if the scripts that make it here can come
-        // from a variety of sources. This function allows filtering by URL
-        // so it seems like it may be possible and we are erring on the side
-        // of caution by handling it here.
-        const closestScripts = findClosestScriptBySource(
-          columnToOffsetMaps.map(pair => pair[0]),
+      for (const script of scripts) {
+        // Check to see if the script contains a breakpoint position at
+        // this line and column.
+        const possibleBreakpoint = script.getPossibleBreakpoints({
           line,
-          column,
-        );
+          minColumn: column,
+          maxColumn: column + 1,
+        }).pop();
 
-        const columnToOffsetLookup = new Map(columnToOffsetMaps);
-        for (const script of closestScripts) {
-          const columnToOffsetMap = columnToOffsetLookup.get(script);
-
-          if (columnToOffsetMap.length > 0) {
-            const firstColumnOffset = columnToOffsetMap[0];
-            const lastColumnOffset = columnToOffsetMap[columnToOffsetMap.length - 1];
-
-            if (column < firstColumnOffset.columnNumber) {
-              entryPoints.push({ script, offsets: [firstColumnOffset.offset] });
-            }
-
-            if (column > lastColumnOffset.columnNumber) {
-              entryPoints.push({ script, offsets: [lastColumnOffset.offset] });
-            }
-          }
+        if (possibleBreakpoint) {
+          const { offset } = possibleBreakpoint;
+          entryPoints.push({ script, offsets: [offset] });
         }
       }
     }
 
     setBreakpointAtEntryPoints(actor, entryPoints);
   },
 });
 
deleted file mode 100644
--- a/devtools/server/actors/utils/closest-scripts.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/* 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 { findSourceOffset } = require("devtools/server/actors/utils/dbg-source");
-
-function findClosestScriptBySource(scripts, generatedLine, generatedColumn) {
-  const bySource = new Map();
-  for (const script of scripts) {
-    const { source } = script;
-    if (script.format !== "js" || !source) {
-      continue;
-    }
-
-    let sourceScripts = bySource.get(source);
-    if (!sourceScripts) {
-      sourceScripts = [];
-      bySource.set(source, sourceScripts);
-    }
-
-    sourceScripts.push(script);
-  }
-
-  const closestScripts = [];
-  for (const sourceScripts of bySource.values()) {
-    const closest = findClosestScript(sourceScripts, generatedLine, generatedColumn);
-    if (closest) {
-      closestScripts.push(closest);
-    }
-  }
-  return closestScripts;
-}
-exports.findClosestScriptBySource = findClosestScriptBySource;
-
-function findClosestScript(scripts, generatedLine, generatedColumn) {
-  if (scripts.length === 0) {
-    return null;
-  }
-  const { source } = scripts[0];
-
-  const offset = findSourceOffset(
-    source,
-    generatedLine,
-    generatedColumn,
-  );
-
-  let closest = null;
-  for (const script of scripts) {
-    if (script.source !== source) {
-      throw new Error("All scripts must be from a single source.");
-    }
-
-    if (
-      offset >= script.sourceStart &&
-      offset < script.sourceStart + script.sourceLength &&
-      (!closest || script.sourceLength < closest.sourceLength)
-    ) {
-      closest = script;
-    }
-  }
-
-  return closest;
-}
--- a/devtools/server/actors/utils/moz.build
+++ b/devtools/server/actors/utils/moz.build
@@ -4,17 +4,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/.
 
 DevToolsModules(
     'accessibility.js',
     'actor-registry-utils.js',
     'actor-registry.js',
     'breakpoint-actor-map.js',
-    'closest-scripts.js',
     'css-grid-utils.js',
     'dbg-source.js',
     'event-breakpoints.js',
     'event-loop.js',
     'make-debugger.js',
     'shapes-utils.js',
     'stack.js',
     'TabSources.js',
deleted file mode 100644
--- a/devtools/server/tests/unit/test_setBreakpoint-at-the-beginning-of-a-line.js
+++ /dev/null
@@ -1,33 +0,0 @@
-"use strict";
-
-const SOURCE_URL = getFileUrl("setBreakpoint-on-column.js");
-
-add_task(threadClientTest(async ({ threadClient, debuggee, client }) => {
-  const promise = waitForNewSource(threadClient, SOURCE_URL);
-  loadSubScript(SOURCE_URL, debuggee);
-  const { source } = await promise;
-
-  const location = { sourceUrl: source.url, line: 4, column: 2 };
-  setBreakpoint(threadClient, location);
-
-  const packet = await executeOnNextTickAndWaitForPause(function() {
-    Cu.evalInSandbox("f()", debuggee);
-  }, client);
-
-  Assert.equal(packet.type, "paused");
-  const why = packet.why;
-  Assert.equal(why.type, "breakpoint");
-  Assert.equal(why.actors.length, 1);
-
-  const frame = packet.frame;
-  Assert.equal(frame.where.actor, source.actor);
-  Assert.equal(frame.where.line, location.line);
-  Assert.equal(frame.where.column, 10);
-
-  const variables = frame.environment.bindings.variables;
-  Assert.equal(variables.a.value.type, "undefined");
-  Assert.equal(variables.b.value.type, "undefined");
-  Assert.equal(variables.c.value.type, "undefined");
-
-  await resume(threadClient);
-}, { doNotRunWorker: true }));
--- a/devtools/server/tests/unit/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js
+++ b/devtools/server/tests/unit/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js
@@ -4,17 +4,17 @@ const SOURCE_URL = getFileUrl("setBreakp
 
 add_task(threadClientTest(async ({ threadClient, debuggee, client }) => {
   const promise = waitForNewSource(threadClient, SOURCE_URL);
   loadSubScript(SOURCE_URL, debuggee);
   const { source } = await promise;
 
   // Pause inside of the nested function so we can make sure that we don't
   // add any other breakpoints at other places on this line.
-  const location = { sourceUrl: source.url, line: 3, column: 47 };
+  const location = { sourceUrl: source.url, line: 3, column: 56 };
   setBreakpoint(threadClient, location);
 
   const packet = await executeOnNextTickAndWaitForPause(function() {
     Cu.evalInSandbox("f()", debuggee);
   }, client);
 
   Assert.equal(packet.type, "paused");
   const why = packet.why;
deleted file mode 100644
--- a/devtools/server/tests/unit/test_setBreakpoint-at-the-end-of-a-line.js
+++ /dev/null
@@ -1,34 +0,0 @@
-"use strict";
-
-const SOURCE_URL = getFileUrl("setBreakpoint-on-column.js");
-
-add_task(threadClientTest(async ({ threadClient, debuggee, client }) => {
-  const promise = waitForNewSource(threadClient, SOURCE_URL);
-  loadSubScript(SOURCE_URL, debuggee);
-  const { source } = await promise;
-
-  const location = { sourceUrl: source.url, line: 4, column: 42 };
-  setBreakpoint(threadClient, location);
-
-  const packet = await executeOnNextTickAndWaitForPause(function() {
-    Cu.evalInSandbox("f()", debuggee);
-  }, client);
-
-  Assert.equal(packet.type, "paused");
-  const why = packet.why;
-  Assert.equal(why.type, "breakpoint");
-  Assert.equal(why.actors.length, 1);
-
-  const frame = packet.frame;
-  const where = frame.where;
-  Assert.equal(where.actor, source.actor);
-  Assert.equal(where.line, location.line);
-  Assert.equal(where.column, 32);
-
-  const variables = frame.environment.bindings.variables;
-  Assert.equal(variables.a.value, 1);
-  Assert.equal(variables.b.value, 2);
-  Assert.equal(variables.c.value.type, "undefined");
-
-  await resume(threadClient);
-}, { doNotRunWorker: true }));
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -215,19 +215,17 @@ reason = bug 937197
 [test_protocolSpec.js]
 [test_registerClient.js]
 [test_client_request.js]
 [test_symbols-01.js]
 [test_symbols-02.js]
 [test_get-executable-lines.js]
 [test_xpcshell_debugging.js]
 support-files = xpcshell_debugging_script.js
-[test_setBreakpoint-at-the-beginning-of-a-line.js]
 [test_setBreakpoint-at-the-beginning-of-a-minified-fn.js]
-[test_setBreakpoint-at-the-end-of-a-line.js]
 [test_setBreakpoint-at-the-end-of-a-minified-fn.js]
 [test_setBreakpoint-on-column.js]
 [test_setBreakpoint-on-column-in-gcd-script.js]
 [test_setBreakpoint-on-line.js]
 [test_setBreakpoint-on-line-in-gcd-script.js]
 [test_setBreakpoint-on-line-with-multiple-offsets.js]
 [test_setBreakpoint-on-line-with-multiple-statements.js]
 [test_setBreakpoint-on-line-with-no-offsets.js]
--- a/dom/base/nsContentSink.cpp
+++ b/dom/base/nsContentSink.cpp
@@ -753,16 +753,17 @@ nsresult nsContentSink::ProcessStyleLink
       url.forget(),
       nullptr,
       net::AttributeReferrerPolicyFromString(aReferrerPolicy),
       CORS_NONE,
       aTitle,
       aMedia,
       aAlternate ? Loader::HasAlternateRel::Yes : Loader::HasAlternateRel::No,
       Loader::IsInline::No,
+      Loader::IsExplicitlyEnabled::No,
   };
 
   auto loadResultOrErr =
       mCSSLoader->LoadStyleLink(info, mRunsToCompletion ? nullptr : this);
   if (loadResultOrErr.isErr()) {
     return loadResultOrErr.unwrapErr();
   }
 
--- a/dom/base/nsIStyleSheetLinkingElement.h
+++ b/dom/base/nsIStyleSheetLinkingElement.h
@@ -19,36 +19,47 @@ class nsIURI;
   {                                                  \
     0xa8b79f3b, 0x9d18, 0x4f9c, {                    \
       0xb1, 0xaa, 0x8c, 0x9b, 0x1b, 0xaa, 0xac, 0xad \
     }                                                \
   }
 
 class nsIStyleSheetLinkingElement : public nsISupports {
  public:
-  enum class ForceUpdate {
+  enum class ForceUpdate : uint8_t {
+    No,
     Yes,
-    No,
   };
 
-  enum class Completed {
+  enum class Completed : uint8_t {
+    No,
     Yes,
+  };
+
+  enum class HasAlternateRel : uint8_t {
     No,
+    Yes,
   };
 
-  enum class HasAlternateRel { Yes, No };
-
-  enum class IsAlternate {
+  enum class IsAlternate : uint8_t {
+    No,
     Yes,
-    No,
   };
 
-  enum class IsInline { Yes, No };
+  enum class IsInline : uint8_t {
+    No,
+    Yes,
+  };
 
-  enum class MediaMatched {
+  enum class IsExplicitlyEnabled : uint8_t {
+    No,
+    Yes,
+  };
+
+  enum class MediaMatched : uint8_t {
     Yes,
     No,
   };
 
   struct Update {
    private:
     bool mWillNotify;
     bool mIsAlternate;
@@ -85,24 +96,24 @@ class nsIStyleSheetLinkingElement : publ
     mozilla::net::ReferrerPolicy mReferrerPolicy;
     mozilla::CORSMode mCORSMode;
     nsString mTitle;
     nsString mMedia;
     nsString mIntegrity;
 
     bool mHasAlternateRel;
     bool mIsInline;
+    IsExplicitlyEnabled mIsExplicitlyEnabled;
 
     SheetInfo(const mozilla::dom::Document&, nsIContent*,
               already_AddRefed<nsIURI> aURI,
               already_AddRefed<nsIPrincipal> aTriggeringPrincipal,
-              mozilla::net::ReferrerPolicy aReferrerPolicy,
-              mozilla::CORSMode aCORSMode, const nsAString& aTitle,
-              const nsAString& aMedia, HasAlternateRel aHasAlternateRel,
-              IsInline aIsInline);
+              mozilla::net::ReferrerPolicy aReferrerPolicy, mozilla::CORSMode,
+              const nsAString& aTitle, const nsAString& aMedia, HasAlternateRel,
+              IsInline, IsExplicitlyEnabled);
 
     ~SheetInfo();
   };
 
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_ISTYLESHEETLINKINGELEMENT_IID)
 
   /**
    * Used to make the association between a style sheet and
--- a/dom/base/nsStyleLinkElement.cpp
+++ b/dom/base/nsStyleLinkElement.cpp
@@ -35,26 +35,28 @@ using namespace mozilla;
 using namespace mozilla::dom;
 
 nsStyleLinkElement::SheetInfo::SheetInfo(
     const Document& aDocument, nsIContent* aContent,
     already_AddRefed<nsIURI> aURI,
     already_AddRefed<nsIPrincipal> aTriggeringPrincipal,
     mozilla::net::ReferrerPolicy aReferrerPolicy, mozilla::CORSMode aCORSMode,
     const nsAString& aTitle, const nsAString& aMedia,
-    HasAlternateRel aHasAlternateRel, IsInline aIsInline)
+    HasAlternateRel aHasAlternateRel, IsInline aIsInline,
+    IsExplicitlyEnabled aIsExplicitlyEnabled)
     : mContent(aContent),
       mURI(aURI),
       mTriggeringPrincipal(aTriggeringPrincipal),
       mReferrerPolicy(aReferrerPolicy),
       mCORSMode(aCORSMode),
       mTitle(aTitle),
       mMedia(aMedia),
       mHasAlternateRel(aHasAlternateRel == HasAlternateRel::Yes),
-      mIsInline(aIsInline == IsInline::Yes) {
+      mIsInline(aIsInline == IsInline::Yes),
+      mIsExplicitlyEnabled(aIsExplicitlyEnabled) {
   MOZ_ASSERT(!mIsInline || aContent);
   MOZ_ASSERT_IF(aContent, aContent->OwnerDoc() == &aDocument);
 
   if (mReferrerPolicy == net::ReferrerPolicy::RP_Unset) {
     mReferrerPolicy = aDocument.GetReferrerPolicy();
   }
 
   if (!mIsInline && aContent && aContent->IsElement()) {
--- a/dom/html/HTMLLinkElement.cpp
+++ b/dom/html/HTMLLinkElement.cpp
@@ -80,22 +80,28 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_IN
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLLinkElement,
                                              nsGenericHTMLElement,
                                              nsIStyleSheetLinkingElement, Link)
 
 NS_IMPL_ELEMENT_CLONE(HTMLLinkElement)
 
-bool HTMLLinkElement::Disabled() {
+bool HTMLLinkElement::Disabled() const {
+  if (StaticPrefs::dom_link_disabled_attribute_enabled()) {
+    return GetBoolAttr(nsGkAtoms::disabled);
+  }
   StyleSheet* ss = GetSheet();
   return ss && ss->Disabled();
 }
 
-void HTMLLinkElement::SetDisabled(bool aDisabled) {
+void HTMLLinkElement::SetDisabled(bool aDisabled, ErrorResult& aRv) {
+  if (StaticPrefs::dom_link_disabled_attribute_enabled()) {
+    return SetHTMLBoolAttr(nsGkAtoms::disabled, aDisabled, aRv);
+  }
   if (StyleSheet* ss = GetSheet()) {
     ss->SetDisabled(aDisabled);
   }
 }
 
 void HTMLLinkElement::OnDNSPrefetchRequested() {
   UnsetFlags(HTML_LINK_DNS_PREFETCH_DEFERRED);
   SetFlags(HTML_LINK_DNS_PREFETCH_REQUESTED);
@@ -309,17 +315,19 @@ nsresult HTMLLinkElement::AfterSetAttr(i
     }
   }
 
   if (aValue) {
     if (aNameSpaceID == kNameSpaceID_None &&
         (aName == nsGkAtoms::href || aName == nsGkAtoms::rel ||
          aName == nsGkAtoms::title || aName == nsGkAtoms::media ||
          aName == nsGkAtoms::type || aName == nsGkAtoms::as ||
-         aName == nsGkAtoms::crossorigin)) {
+         aName == nsGkAtoms::crossorigin ||
+         (aName == nsGkAtoms::disabled &&
+          StaticPrefs::dom_link_disabled_attribute_enabled()))) {
       bool dropSheet = false;
       if (aName == nsGkAtoms::rel) {
         nsAutoString value;
         aValue->ToString(value);
         uint32_t linkTypes = nsStyleLinkElement::ParseLinkTypes(value);
         if (GetSheet()) {
           dropSheet = !(linkTypes & nsStyleLinkElement::eSTYLESHEET);
         }
@@ -333,28 +341,35 @@ nsresult HTMLLinkElement::AfterSetAttr(i
       if ((aName == nsGkAtoms::as || aName == nsGkAtoms::type ||
            aName == nsGkAtoms::crossorigin || aName == nsGkAtoms::media) &&
           IsInComposedDoc()) {
         UpdatePreload(aName, aValue, aOldValue);
       }
 
       const bool forceUpdate = dropSheet || aName == nsGkAtoms::title ||
                                aName == nsGkAtoms::media ||
-                               aName == nsGkAtoms::type;
+                               aName == nsGkAtoms::type ||
+                               aName == nsGkAtoms::disabled;
 
       Unused << UpdateStyleSheetInternal(
           nullptr, nullptr, forceUpdate ? ForceUpdate::Yes : ForceUpdate::No);
     }
   } else {
-    // Since removing href or rel makes us no longer link to a
-    // stylesheet, force updates for those too.
     if (aNameSpaceID == kNameSpaceID_None) {
+      if (aName == nsGkAtoms::disabled &&
+          StaticPrefs::dom_link_disabled_attribute_enabled()) {
+        mExplicitlyEnabled = true;
+      }
+      // Since removing href or rel makes us no longer link to a stylesheet,
+      // force updates for those too.
       if (aName == nsGkAtoms::href || aName == nsGkAtoms::rel ||
           aName == nsGkAtoms::title || aName == nsGkAtoms::media ||
-          aName == nsGkAtoms::type) {
+          aName == nsGkAtoms::type ||
+          (aName == nsGkAtoms::disabled &&
+           StaticPrefs::dom_link_disabled_attribute_enabled())) {
         Unused << UpdateStyleSheetInternal(nullptr, nullptr, ForceUpdate::Yes);
       }
       if ((aName == nsGkAtoms::as || aName == nsGkAtoms::type ||
            aName == nsGkAtoms::crossorigin || aName == nsGkAtoms::media) &&
           IsInComposedDoc()) {
         UpdatePreload(aName, aValue, aOldValue);
       }
     }
@@ -410,16 +425,20 @@ Maybe<nsStyleLinkElement::SheetInfo> HTM
   if (!(linkTypes & nsStyleLinkElement::eSTYLESHEET)) {
     return Nothing();
   }
 
   if (!IsCSSMimeTypeAttribute(*this)) {
     return Nothing();
   }
 
+  if (StaticPrefs::dom_link_disabled_attribute_enabled() && Disabled()) {
+    return Nothing();
+  }
+
   nsAutoString title;
   nsAutoString media;
   GetTitleAndMediaForElement(*this, title, media);
 
   bool alternate = linkTypes & nsStyleLinkElement::eALTERNATE;
   if (alternate && title.IsEmpty()) {
     // alternates must have title.
     return Nothing();
@@ -439,16 +458,17 @@ Maybe<nsStyleLinkElement::SheetInfo> HTM
       uri.forget(),
       prin.forget(),
       GetReferrerPolicyAsEnum(),
       GetCORSMode(),
       title,
       media,
       alternate ? HasAlternateRel::Yes : HasAlternateRel::No,
       IsInline::No,
+      mExplicitlyEnabled ? IsExplicitlyEnabled::Yes : IsExplicitlyEnabled::No,
   });
 }
 
 EventStates HTMLLinkElement::IntrinsicState() const {
   return Link::LinkState() | nsGenericHTMLElement::IntrinsicState();
 }
 
 void HTMLLinkElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes,
--- a/dom/html/HTMLLinkElement.h
+++ b/dom/html/HTMLLinkElement.h
@@ -73,18 +73,18 @@ class HTMLLinkElement final : public nsG
 
   void CreateAndDispatchEvent(Document* aDoc, const nsAString& aEventName);
 
   virtual void OnDNSPrefetchDeferred() override;
   virtual void OnDNSPrefetchRequested() override;
   virtual bool HasDeferredDNSPrefetchRequest() override;
 
   // WebIDL
-  bool Disabled();
-  void SetDisabled(bool aDisabled);
+  bool Disabled() const;
+  void SetDisabled(bool aDisabled, ErrorResult& aRv);
 
   void GetHref(nsAString& aValue) {
     GetURIAttr(nsGkAtoms::href, nullptr, aValue);
   }
   void SetHref(const nsAString& aHref, nsIPrincipal* aTriggeringPrincipal,
                ErrorResult& aRv) {
     SetHTMLAttr(nsGkAtoms::href, aHref, aTriggeringPrincipal, aRv);
   }
@@ -165,16 +165,22 @@ class HTMLLinkElement final : public nsG
                                 const nsAString& aMedia, Document* aDocument);
 
  protected:
   virtual ~HTMLLinkElement();
 
   // nsStyleLinkElement
   Maybe<SheetInfo> GetStyleSheetInfo() final;
 
- protected:
   RefPtr<nsDOMTokenList> mRelList;
+
+  // The "explicitly enabled" flag. This flag is set whenever the `disabled`
+  // attribute is explicitly unset, and makes alternate stylesheets not be
+  // disabled by default anymore.
+  //
+  // See https://github.com/whatwg/html/issues/3840#issuecomment-481034206.
+  bool mExplicitlyEnabled = false;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_HTMLLinkElement_h
--- a/dom/html/HTMLStyleElement.cpp
+++ b/dom/html/HTMLStyleElement.cpp
@@ -40,17 +40,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLStyleElement,
                                              nsGenericHTMLElement,
                                              nsIStyleSheetLinkingElement,
                                              nsIMutationObserver)
 
 NS_IMPL_ELEMENT_CLONE(HTMLStyleElement)
 
-bool HTMLStyleElement::Disabled() {
+bool HTMLStyleElement::Disabled() const {
   StyleSheet* ss = GetSheet();
   return ss && ss->Disabled();
 }
 
 void HTMLStyleElement::SetDisabled(bool aDisabled) {
   if (StyleSheet* ss = GetSheet()) {
     ss->SetDisabled(aDisabled);
   }
@@ -181,16 +181,17 @@ Maybe<nsStyleLinkElement::SheetInfo> HTM
       nullptr,
       prin.forget(),
       net::ReferrerPolicy::RP_Unset,
       CORS_NONE,
       title,
       media,
       HasAlternateRel::No,
       IsInline::Yes,
+      IsExplicitlyEnabled::No,
   });
 }
 
 JSObject* HTMLStyleElement::WrapNode(JSContext* aCx,
                                      JS::Handle<JSObject*> aGivenProto) {
   return HTMLStyleElement_Binding::Wrap(aCx, this, aGivenProto);
 }
 
--- a/dom/html/HTMLStyleElement.h
+++ b/dom/html/HTMLStyleElement.h
@@ -51,17 +51,17 @@ class HTMLStyleElement final : public ns
   virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
 
   // nsIMutationObserver
   NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
 
-  bool Disabled();
+  bool Disabled() const;
   void SetDisabled(bool aDisabled);
   void GetMedia(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::media, aValue); }
   void SetMedia(const nsAString& aMedia, ErrorResult& aError) {
     SetHTMLAttr(nsGkAtoms::media, aMedia, aError);
   }
   void GetType(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); }
   void SetType(const nsAString& aType, ErrorResult& aError) {
     SetHTMLAttr(nsGkAtoms::type, aType, aError);
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -9446,17 +9446,17 @@ nsresult DatabaseConnection::CommitWrite
 
   mInWriteTransaction = false;
   return NS_OK;
 }
 
 void DatabaseConnection::RollbackWriteTransaction() {
   AssertIsOnConnectionThread();
   MOZ_ASSERT(!mInReadTransaction);
-  MOZ_ASSERT(mStorageConnection);
+  MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);
 
   AUTO_PROFILER_LABEL("DatabaseConnection::RollbackWriteTransaction", DOM);
 
   if (!mInWriteTransaction) {
     return;
   }
 
   DatabaseConnection::CachedStatement stmt;
@@ -10058,16 +10058,23 @@ nsresult DatabaseConnection::AutoSavepoi
              aTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH ||
              aTransaction->GetMode() == IDBTransaction::CLEANUP ||
              aTransaction->GetMode() == IDBTransaction::VERSION_CHANGE);
 
   DatabaseConnection* connection = aTransaction->GetDatabase()->GetConnection();
   MOZ_ASSERT(connection);
   connection->AssertIsOnConnectionThread();
 
+  // This is just a quick fix for preventing accessing the nullptr. The cause is
+  // probably because the connection was unexpectedly closed.
+  if (!connection->GetUpdateRefcountFunction()) {
+    NS_WARNING("The connection was closed for some reasons!");
+    return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+  }
+
   MOZ_ASSERT(!mConnection);
   MOZ_ASSERT(!mDEBUGTransaction);
 
   nsresult rv = connection->StartSavepoint();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
--- a/dom/svg/SVGStyleElement.cpp
+++ b/dom/svg/SVGStyleElement.cpp
@@ -192,13 +192,14 @@ Maybe<nsStyleLinkElement::SheetInfo> SVG
       net::ReferrerPolicy::RP_Unset,
       // FIXME(bug 1459822): Why does this need a crossorigin attribute, but
       // HTMLStyleElement doesn't?
       AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)),
       title,
       media,
       HasAlternateRel::No,
       IsInline::Yes,
+      IsExplicitlyEnabled::No,
   });
 }
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/webidl/HTMLLinkElement.webidl
+++ b/dom/webidl/HTMLLinkElement.webidl
@@ -9,17 +9,17 @@
  * © Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and
  * Opera Software ASA. You are granted a license to use, reproduce
  * and create derivative works of this document.
  */
 
 // http://www.whatwg.org/specs/web-apps/current-work/#the-link-element
 [HTMLConstructor]
 interface HTMLLinkElement : HTMLElement {
-  [Pure]
+  [CEReactions, SetterThrows, Pure]
            attribute boolean disabled;
   [CEReactions, SetterNeedsSubjectPrincipal=NonSystem, SetterThrows, Pure]
            attribute DOMString href;
   [CEReactions, SetterThrows, Pure]
            attribute DOMString? crossOrigin;
   [CEReactions, SetterThrows, Pure]
            attribute DOMString rel;
   [PutForwards=value]
--- a/dom/xml/XMLStylesheetProcessingInstruction.cpp
+++ b/dom/xml/XMLStylesheetProcessingInstruction.cpp
@@ -132,16 +132,17 @@ XMLStylesheetProcessingInstruction::GetS
       uri.forget(),
       nullptr,
       net::RP_Unset,
       CORS_NONE,
       title,
       media,
       alternate ? HasAlternateRel::Yes : HasAlternateRel::No,
       IsInline::No,
+      IsExplicitlyEnabled::No,
   });
 }
 
 already_AddRefed<CharacterData>
 XMLStylesheetProcessingInstruction::CloneDataNode(
     mozilla::dom::NodeInfo* aNodeInfo, bool aCloneText) const {
   nsAutoString data;
   GetData(data);
--- a/js/src/jit/BaselineCompiler.cpp
+++ b/js/src/jit/BaselineCompiler.cpp
@@ -469,17 +469,18 @@ void BaselineCompilerCodeGen::emitInitia
 }
 
 template <>
 void BaselineInterpreterCodeGen::emitInitializeLocals() {
   // Push |undefined| for all locals.
 
   Register scratch = R0.scratchReg();
   loadScript(scratch);
-  masm.load32(Address(scratch, JSScript::offsetOfNfixed()), scratch);
+  masm.loadPtr(Address(scratch, JSScript::offsetOfScriptData()), scratch);
+  masm.load32(Address(scratch, SharedScriptData::offsetOfNfixed()), scratch);
 
   Label top, done;
   masm.bind(&top);
   masm.branchTest32(Assembler::Zero, scratch, scratch, &done);
   {
     masm.pushValue(UndefinedValue());
     masm.sub32(Imm32(1), scratch);
     masm.jump(&top);
@@ -774,17 +775,18 @@ void BaselineCompilerCodeGen::subtractSc
 }
 
 template <>
 void BaselineInterpreterCodeGen::subtractScriptSlotsSize(Register reg,
                                                          Register scratch) {
   // reg = reg - script->nslots() * sizeof(Value)
   MOZ_ASSERT(reg != scratch);
   loadScript(scratch);
-  masm.load32(Address(scratch, JSScript::offsetOfNslots()), scratch);
+  masm.loadPtr(Address(scratch, JSScript::offsetOfScriptData()), scratch);
+  masm.load32(Address(scratch, SharedScriptData::offsetOfNslots()), scratch);
   static_assert(sizeof(Value) == 8,
                 "shift by 3 below assumes Value is 8 bytes");
   masm.lshiftPtr(Imm32(3), scratch);
   masm.subPtr(scratch, reg);
 }
 
 template <>
 void BaselineCompilerCodeGen::loadGlobalLexicalEnvironment(Register dest) {
--- a/js/src/jit/CacheIRCompiler.cpp
+++ b/js/src/jit/CacheIRCompiler.cpp
@@ -2692,17 +2692,18 @@ bool CacheIRCompiler::emitLoadFunctionLe
       obj, FunctionExtended::offsetOfExtendedSlot(BOUND_FUN_LENGTH_SLOT));
   masm.branchTestInt32(Assembler::NotEqual, boundLength, failure->label());
   masm.unboxInt32(boundLength, scratch);
   masm.jump(&done);
 
   masm.bind(&interpreted);
   // Load the length from the function's script.
   masm.loadPtr(Address(obj, JSFunction::offsetOfScript()), scratch);
-  masm.load16ZeroExtend(Address(scratch, JSScript::offsetOfFunLength()),
+  masm.loadPtr(Address(scratch, JSScript::offsetOfScriptData()), scratch);
+  masm.load16ZeroExtend(Address(scratch, SharedScriptData::offsetOfFunLength()),
                         scratch);
 
   masm.bind(&done);
   EmitStoreResult(masm, scratch, JSVAL_TYPE_INT32, output);
   return true;
 }
 
 bool CacheIRCompiler::emitLoadStringLengthResult() {
--- a/js/src/jit/CodeGenerator.cpp
+++ b/js/src/jit/CodeGenerator.cpp
@@ -13818,17 +13818,19 @@ void CodeGenerator::visitFinishBoundFunc
     // Load the length property of a bound function.
     masm.unboxInt32(Address(target, boundLengthOffset), temp1);
     masm.jump(&lengthLoaded);
   }
   masm.bind(&isInterpreted);
   {
     // Load the length property of an interpreted function.
     masm.loadPtr(Address(target, JSFunction::offsetOfScript()), temp1);
-    masm.load16ZeroExtend(Address(temp1, JSScript::offsetOfFunLength()), temp1);
+    masm.loadPtr(Address(temp1, JSScript::offsetOfScriptData()), temp1);
+    masm.load16ZeroExtend(Address(temp1, SharedScriptData::offsetOfFunLength()),
+                          temp1);
   }
   masm.bind(&lengthLoaded);
 
   // Compute the bound function length: Max(0, target.length - argCount).
   Label nonNegative;
   masm.sub32(argCount, temp1);
   masm.branch32(Assembler::GreaterThanOrEqual, temp1, Imm32(0), &nonNegative);
   masm.move32(Imm32(0), temp1);
--- a/js/src/jit/ExecutableAllocator.h
+++ b/js/src/jit/ExecutableAllocator.h
@@ -211,17 +211,17 @@ class ExecutableAllocator {
   MOZ_MUST_USE
   static bool makeExecutable(void* start, size_t size) {
     return ReprotectRegion(start, size, ProtectionSetting::Executable);
   }
 
   static void poisonCode(JSRuntime* rt, JitPoisonRangeVector& ranges);
 
 #if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64) || \
-    defined(JS_SIMULATOR_ARM64) || defined(JS_CODEGEN_NONE)
+    defined(JS_CODEGEN_NONE)
   static void cacheFlush(void*, size_t) {}
 #elif defined(JS_SIMULATOR_ARM) || defined(JS_SIMULATOR_MIPS32) || \
     defined(JS_SIMULATOR_MIPS64)
   static void cacheFlush(void* code, size_t size) {
     js::jit::SimulatorProcess::FlushICache(code, size);
   }
 #elif defined(JS_CODEGEN_MIPS32) || defined(JS_CODEGEN_MIPS64)
   static void cacheFlush(void* code, size_t size) {
@@ -284,17 +284,17 @@ class ExecutableAllocator {
           "mov     r2, #0x0\n"
           "svc     0x0\n"
           "pop     {r7}\n"
           :
           : "r"(start), "r"(end)
           : "r0", "r1", "r2");
     }
   }
-#elif defined(JS_CODEGEN_ARM64)
+#elif defined(JS_SIMULATOR_ARM64) || defined(JS_CODEGEN_ARM64)
   static void cacheFlush(void* code, size_t size) {
     vixl::CPU::EnsureIAndDCacheCoherency(code, size);
   }
 #elif defined(__sparc__)
   static void cacheFlush(void* code, size_t size) {
     sync_instruction_memory((caddr_t)code, size);
   }
 #endif
new file mode 100644
--- /dev/null
+++ b/js/src/jit/arm64/vixl/MozCachingDecoder.h
@@ -0,0 +1,179 @@
+#ifndef VIXL_A64_MOZ_CACHING_DECODER_A64_H_
+#define VIXL_A64_MOZ_CACHING_DECODER_A64_H_
+
+#include "mozilla/HashTable.h"
+
+#include "jit/arm64/vixl/Decoder-vixl.h"
+#include "js/AllocPolicy.h"
+
+#ifdef DEBUG
+#define JS_CACHE_SIMULATOR_ARM64 1
+#endif
+
+#ifdef JS_CACHE_SIMULATOR_ARM64
+namespace vixl {
+
+// This enumeration list the different kind of instructions which can be
+// decoded. These kind correspond to the set of visitor defined by the default
+// Decoder.
+enum class InstDecodedKind : uint8_t {
+  NotDecodedYet,
+#define DECLARE(E) E,
+  VISITOR_LIST(DECLARE)
+#undef DECLARE
+};
+
+// A SinglePageDecodeCache is used to store the decoded kind of all instructions
+// in an executable page of code. Each time an instruction is decoded, its
+// decoded kind is recorded in this structure. The previous instruction value is
+// also recorded in this structure when using a debug build.
+//
+// The next time the same offset is visited, the instruction would be decoded
+// using the previously recorded decode kind. It is also compared against the
+// previously recorded bits of the instruction to check for potential missing
+// cache invalidations, in debug builds.
+//
+// This structure stores the equivalent of a single page of code to have better
+// memory locality when using the simulator. As opposed to having a hash-table
+// for all instructions. However a hash-table is used by the CachingDecoder to
+// map the prefixes of page addresses to these SinglePageDecodeCaches.
+class SinglePageDecodeCache {
+ public:
+  static const uintptr_t PageSize = 1 << 12;
+  static const uintptr_t PageMask = PageSize - 1;
+  static const uintptr_t InstSize = vixl::kInstructionSize;
+  static const uintptr_t InstMask = InstSize - 1;
+  static const uintptr_t InstPerPage = PageSize / InstSize;
+
+  SinglePageDecodeCache(const Instruction* inst)
+    : pageStart_(PageStart(inst))
+  {
+    memset(&decodeCache_, int(InstDecodedKind::NotDecodedYet), sizeof(decodeCache_));
+  }
+  // Compute the start address of the page which contains this instruction.
+  static uintptr_t PageStart(const Instruction* inst) {
+    return uintptr_t(inst) & ~PageMask;
+  }
+  // Returns whether the instruction decoded kind is stored in this
+  // SinglePageDecodeCache.
+  bool contains(const Instruction* inst) {
+    return pageStart_ == PageStart(inst);
+  }
+  void clearDecode(const Instruction* inst) {
+    uintptr_t offset = (uintptr_t(inst) & PageMask) / InstSize;
+    decodeCache_[offset] = InstDecodedKind::NotDecodedYet;
+  }
+  InstDecodedKind* decodePtr(const Instruction* inst) {
+    uintptr_t offset = (uintptr_t(inst) & PageMask) / InstSize;
+    uint32_t instValue = *reinterpret_cast<const uint32_t*>(inst);
+    instCache_[offset] = instValue;
+    return &decodeCache_[offset];
+  }
+  InstDecodedKind decode(const Instruction* inst) const {
+    uintptr_t offset = (uintptr_t(inst) & PageMask) / InstSize;
+    InstDecodedKind val = decodeCache_[offset];
+    uint32_t instValue = *reinterpret_cast<const uint32_t*>(inst);
+    MOZ_ASSERT_IF(val != InstDecodedKind::NotDecodedYet,
+                  instCache_[offset] == instValue);
+    return val;
+  }
+
+ private:
+  // Record the address at which the corresponding code page starts.
+  const uintptr_t pageStart_;
+
+  // Cache what instruction got decoded previously, in order to assert if we see
+  // any stale instructions after.
+  uint32_t instCache_[InstPerPage];
+
+  // Cache the decoding of the instruction such that we can skip the decoding
+  // part.
+  InstDecodedKind decodeCache_[InstPerPage];
+};
+
+// A DecoderVisitor which will record which visitor function should be called
+// the next time we want to decode the same instruction.
+class CachingDecoderVisitor : public DecoderVisitor {
+ public:
+  CachingDecoderVisitor() = default;
+  virtual ~CachingDecoderVisitor() {}
+
+#define DECLARE(A) virtual void Visit##A(const Instruction* instr) { \
+    if (last_) { \
+      MOZ_ASSERT(*last_ == InstDecodedKind::NotDecodedYet); \
+      *last_ = InstDecodedKind::A; \
+      last_ = nullptr; \
+    } \
+  };
+
+  VISITOR_LIST(DECLARE)
+#undef DECLARE
+
+  void setDecodePtr(InstDecodedKind* ptr) {
+    last_ = ptr;
+  }
+
+ private:
+  InstDecodedKind* last_;
+};
+
+// The Caching decoder works by extending the default vixl Decoder class. It
+// extends it by overloading the Decode function.
+//
+// The overloaded Decode function checks whether the instruction given as
+// argument got decoded before or since it got invalidated. If it was not
+// previously decoded, the value of the instruction is recorded as well as the
+// kind of instruction. Otherwise, the value of the instruction is checked
+// against the previously recorded value and the instruction kind is used to
+// skip the decoding visitor and resume the execution of instruction.
+//
+// The caching decoder stores the equivalent of a page of executable code in a
+// hash-table. Each SinglePageDecodeCache stores an array of decoded kind as
+// well as the value of the previously decoded instruction.
+//
+// When testing if an instruction was decoded before, we check if the address of
+// the instruction is contained in the last SinglePageDecodeCache. If it is not,
+// then the hash-table entry is queried and created if necessary, and the last
+// SinglePageDecodeCache is updated. Then, the last SinglePageDecodeCache
+// necessary contains the decoded kind of the instruction given as argument.
+//
+// The caching decoder add an extra function for flushing the cache, which is in
+// charge of clearing the decoded kind of instruction in the range of addresses
+// given as argument. This is indirectly called by
+// CPU::EnsureIAndDCacheCoherency.
+class CachingDecoder : public Decoder {
+  using ICacheMap = mozilla::HashMap<uintptr_t, SinglePageDecodeCache*>;
+ public:
+  CachingDecoder()
+      : lastPage_(nullptr)
+  {
+    PrependVisitor(&cachingDecoder_);
+  }
+  ~CachingDecoder() {
+    RemoveVisitor(&cachingDecoder_);
+  }
+
+  void Decode(const Instruction* instr);
+  void Decode(Instruction* instr) {
+    Decode(const_cast<const Instruction*>(instr));
+  }
+
+  void FlushICache(void* start, size_t size);
+
+ private:
+  // Record the type of the decoded instruction, to avoid decoding it a second
+  // time the next time we execute it.
+  CachingDecoderVisitor cachingDecoder_;
+
+  // Store the mapping of Instruction pointer to the corresponding
+  // SinglePageDecodeCache.
+  ICacheMap iCache_;
+
+  // Record the last SinglePageDecodeCache seen, such that we can quickly access
+  // it for the next instruction.
+  SinglePageDecodeCache* lastPage_;
+};
+
+}
+#endif // !JS_CACHE_SIMULATOR_ARM64
+#endif // !VIXL_A64_MOZ_CACHING_DECODER_A64_H_
--- a/js/src/jit/arm64/vixl/MozCpu-vixl.cpp
+++ b/js/src/jit/arm64/vixl/MozCpu-vixl.cpp
@@ -20,16 +20,17 @@
 // 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 "jit/arm64/vixl/Cpu-vixl.h"
+#include "jit/arm64/vixl/Simulator-vixl.h"
 #include "jit/arm64/vixl/Utils-vixl.h"
 #include "util/Windows.h"
 
 namespace vixl {
 
 uint32_t CPU::GetCacheType() {
 #if defined(__aarch64__) && !defined(_MSC_VER)
   uint64_t cache_type_register;
@@ -43,17 +44,34 @@ uint32_t CPU::GetCacheType() {
   // neither EnsureIAndDCacheCoherency nor the simulator will need this
   // information.
   return 0;
 #endif
 }
 
 
 void CPU::EnsureIAndDCacheCoherency(void *address, size_t length) {
-#if defined(_MSC_VER) && defined(_M_ARM64)
+#ifdef JS_CACHE_SIMULATOR_ARM64
+  // This code attempt to emulate what the following assembly sequence is doing,
+  // which is sending the information to other cores that some cache line have
+  // to be invalidated and applying them on the current core.
+  //
+  // This is done by recording the current range to be flushed to all
+  // simulators, then if there is a simulator associated with the current
+  // thread, applying all flushed ranges as the "isb" instruction would do.
+  using js::jit::SimulatorProcess;
+  js::jit::AutoLockSimulatorCache alsc;
+  if (length > 0) {
+    SimulatorProcess::recordICacheFlush(address, length);
+  }
+  Simulator* sim = vixl::Simulator::Current();
+  if (sim) {
+    sim->FlushICache();
+  }
+#elif defined(_MSC_VER) && defined(_M_ARM64)
   FlushInstructionCache(GetCurrentProcess(), address, length);
 #elif defined(__aarch64__)
   // Implement the cache synchronisation for all targets where AArch64 is the
   // host, even if we're building the simulator for an AAarch64 host. This
   // allows for cases where the user wants to simulate code as well as run it
   // natively.
 
   if (length == 0) {
--- a/js/src/jit/arm64/vixl/MozSimulator-vixl.cpp
+++ b/js/src/jit/arm64/vixl/MozSimulator-vixl.cpp
@@ -22,16 +22,17 @@
 // 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 "mozilla/DebugOnly.h"
 
 #include "jit/arm64/vixl/Debugger-vixl.h"
+#include "jit/arm64/vixl/MozCachingDecoder.h"
 #include "jit/arm64/vixl/Simulator-vixl.h"
 #include "jit/IonTypes.h"
 #include "js/UniquePtr.h"
 #include "js/Utility.h"
 #include "threading/LockGuard.h"
 #include "vm/Runtime.h"
 
 js::jit::SimulatorProcess* js::jit::SimulatorProcess::singleton_ = nullptr;
@@ -149,38 +150,56 @@ void Simulator::init(Decoder* decoder, F
   // time they are encountered. This warning can be silenced using
   // SilenceExclusiveAccessWarning().
   print_exclusive_access_warning_ = true;
 }
 
 
 Simulator* Simulator::Current() {
   JSContext* cx = js::TlsContext.get();
-  MOZ_ASSERT(js::CurrentThreadCanAccessRuntime(cx->runtime()));
+  if (!cx) {
+    return nullptr;
+  }
+  JSRuntime* rt = cx->runtime();
+  if (!rt) {
+    return nullptr;
+  }
+  MOZ_ASSERT(js::CurrentThreadCanAccessRuntime(rt));
   return cx->simulator();
 }
 
 
 Simulator* Simulator::Create() {
-  Decoder *decoder = js_new<vixl::Decoder>();
+  Decoder *decoder = js_new<Decoder>();
   if (!decoder)
     return nullptr;
 
   // FIXME: This just leaks the Decoder object for now, which is probably OK.
   // FIXME: We should free it at some point.
   // FIXME: Note that it can't be stored in the SimulatorRuntime due to lifetime conflicts.
   js::UniquePtr<Simulator> sim;
-  if (getenv("USE_DEBUGGER") != nullptr)
+  if (getenv("USE_DEBUGGER") != nullptr) {
     sim.reset(js_new<Debugger>(decoder, stdout));
-  else
+  } else {
     sim.reset(js_new<Simulator>(decoder, stdout));
+  }
 
   // Check if Simulator:init ran out of memory.
-  if (sim && sim->oom())
+  if (sim && sim->oom()) {
     return nullptr;
+  }
+
+#ifdef JS_CACHE_SIMULATOR_ARM64
+  // Register the simulator in the Simulator process to handle cache flushes
+  // across threads.
+  js::jit::AutoLockSimulatorCache alsc;
+  if (!SimulatorProcess::registerSimulator(sim.get())) {
+    return nullptr;
+  }
+#endif
 
   return sim.release();
 }
 
 
 void Simulator::Destroy(Simulator* sim) {
   js_delete(sim);
 }
@@ -280,60 +299,45 @@ int64_t Simulator::call(uint8_t* entry, 
 
   int64_t result = xreg(0);
   if (getenv("USE_DEBUGGER"))
       printf("LEAVE\n");
   return result;
 }
 
 
-// Protects the icache and redirection properties of the simulator.
-class AutoLockSimulatorCache : public js::LockGuard<js::Mutex>
-{
-  friend class Simulator;
-  using Base = js::LockGuard<js::Mutex>;
-
- public:
-  explicit AutoLockSimulatorCache()
-    : Base(SimulatorProcess::singleton_->lock_)
-  {
-  }
-};
-
-
 // When the generated code calls a VM function (masm.callWithABI) we need to
 // call that function instead of trying to execute it with the simulator
 // (because it's x64 code instead of AArch64 code). We do that by redirecting the VM
 // call to a svc (Supervisor Call) instruction that is handled by the
 // simulator. We write the original destination of the jump just at a known
 // offset from the svc instruction so the simulator knows what to call.
 class Redirection
 {
   friend class Simulator;
 
   Redirection(void* nativeFunction, ABIFunctionType type)
     : nativeFunction_(nativeFunction),
     type_(type),
     next_(nullptr)
   {
     next_ = SimulatorProcess::redirection();
-    // TODO: Flush ICache?
     SimulatorProcess::setRedirection(this);
 
     Instruction* instr = (Instruction*)(&svcInstruction_);
     vixl::Assembler::svc(instr, kCallRtRedirected);
   }
 
  public:
   void* addressOfSvcInstruction() { return &svcInstruction_; }
   void* nativeFunction() const { return nativeFunction_; }
   ABIFunctionType type() const { return type_; }
 
   static Redirection* Get(void* nativeFunction, ABIFunctionType type) {
-    AutoLockSimulatorCache alsr;
+    js::jit::AutoLockSimulatorCache alsr;
 
     // TODO: Store srt_ in the simulator for this assertion.
     // VIXL_ASSERT_IF(pt->simulator(), pt->simulator()->srt_ == srt);
 
     Redirection* current = SimulatorProcess::redirection();
     for (; current != nullptr; current = current->next_) {
       if (current->nativeFunction_ == nativeFunction) {
         VIXL_ASSERT(current->type() == type);
@@ -708,19 +712,137 @@ Simulator::VisitCallRedirection(const In
 
   // Simulate a return.
   set_lr(savedLR);
   set_pc((Instruction*)savedLR);
   if (getenv("USE_DEBUGGER"))
     printf("SVCRET\n");
 }
 
+#ifdef JS_CACHE_SIMULATOR_ARM64
+void
+Simulator::FlushICache()
+{
+  // Flush the caches recorded by the current thread as well as what got
+  // recorded from other threads before this call.
+  auto& vec = SimulatorProcess::getICacheFlushes(this);
+  for (auto& flush : vec) {
+    decoder_->FlushICache(flush.start, flush.length);
+  }
+  vec.clear();
+}
+
+void CachingDecoder::Decode(const Instruction* instr) {
+  InstDecodedKind state;
+  if (lastPage_ && lastPage_->contains(instr)) {
+    state = lastPage_->decode(instr);
+  } else {
+    uintptr_t key = SinglePageDecodeCache::PageStart(instr);
+    ICacheMap::AddPtr p = iCache_.lookupForAdd(key);
+    if (p) {
+      lastPage_ = p->value();
+      state = lastPage_->decode(instr);
+    } else {
+      js::AutoEnterOOMUnsafeRegion oomUnsafe;
+      SinglePageDecodeCache* newPage = js_new<SinglePageDecodeCache>(instr);
+      if (!newPage || !iCache_.add(p, key, newPage)) {
+        oomUnsafe.crash("Simulator SinglePageDecodeCache");
+      }
+      lastPage_ = newPage;
+      state = InstDecodedKind::NotDecodedYet;
+    }
+  }
+
+  switch (state) {
+  case InstDecodedKind::NotDecodedYet: {
+    cachingDecoder_.setDecodePtr(lastPage_->decodePtr(instr));
+    this->Decoder::Decode(instr);
+    break;
+  }
+#define CASE(A) \
+  case InstDecodedKind::A: { \
+    Visit##A(instr); \
+    break; \
+  }
+
+  VISITOR_LIST(CASE)
+#undef CASE
+  }
+}
+
+void CachingDecoder::FlushICache(void* start, size_t size) {
+  MOZ_ASSERT(uintptr_t(start) % vixl::kInstructionSize == 0);
+  MOZ_ASSERT(size % vixl::kInstructionSize == 0);
+  const uint8_t* it = reinterpret_cast<const uint8_t*>(start);
+  const uint8_t* end = it + size;
+  SinglePageDecodeCache* last = nullptr;
+  for (; it < end; it += vixl::kInstructionSize) {
+    auto instr = reinterpret_cast<const Instruction*>(it);
+    if (last && last->contains(instr)) {
+      last->clearDecode(instr);
+    } else {
+      uintptr_t key = SinglePageDecodeCache::PageStart(instr);
+      ICacheMap::Ptr p = iCache_.lookup(key);
+      if (p) {
+        last = p->value();
+        last->clearDecode(instr);
+      }
+    }
+  }
+}
+#endif
 
 }  // namespace vixl
 
+namespace js {
+namespace jit {
+
+#ifdef JS_CACHE_SIMULATOR_ARM64
+void SimulatorProcess::recordICacheFlush(void* start, size_t length) {
+  MOZ_ASSERT(singleton_->lock_.ownedByCurrentThread());
+  AutoEnterOOMUnsafeRegion oomUnsafe;
+  ICacheFlush range{start, length};
+  for (auto& s : singleton_->pendingFlushes_) {
+    if (!s.records.append(range)) {
+      oomUnsafe.crash("Simulator recordFlushICache");
+    }
+  }
+}
+
+SimulatorProcess::ICacheFlushes& SimulatorProcess::getICacheFlushes(Simulator* sim) {
+  MOZ_ASSERT(singleton_->lock_.ownedByCurrentThread());
+  for (auto& s : singleton_->pendingFlushes_) {
+    if (s.thread == sim) {
+      return s.records;
+    }
+  }
+  MOZ_CRASH("Simulator is not registered in the SimulatorProcess");
+}
+
+bool SimulatorProcess::registerSimulator(Simulator* sim) {
+  MOZ_ASSERT(singleton_->lock_.ownedByCurrentThread());
+  ICacheFlushes empty;
+  SimFlushes simFlushes{sim, std::move(empty)};
+  return singleton_->pendingFlushes_.append(std::move(simFlushes));
+}
+
+void SimulatorProcess::unregisterSimulator(Simulator* sim) {
+  MOZ_ASSERT(singleton_->lock_.ownedByCurrentThread());
+  for (auto& s : singleton_->pendingFlushes_) {
+    if (s.thread == sim) {
+      singleton_->pendingFlushes_.erase(&s);
+      return;
+    }
+  }
+  MOZ_CRASH("Simulator is not registered in the SimulatorProcess");
+}
+#endif // !JS_CACHE_SIMULATOR_ARM64
+
+} // namespace jit
+} // namespace js
 
 vixl::Simulator* JSContext::simulator() const {
   return simulator_;
 }
 
 
 uintptr_t* JSContext::addressOfSimulatorStackLimit() {
   return simulator_->addressOfStackLimit();
--- a/js/src/jit/arm64/vixl/Simulator-vixl.h
+++ b/js/src/jit/arm64/vixl/Simulator-vixl.h
@@ -33,16 +33,17 @@
 
 #include "mozilla/Vector.h"
 
 #include "jit/arm64/vixl/Assembler-vixl.h"
 #include "jit/arm64/vixl/Disasm-vixl.h"
 #include "jit/arm64/vixl/Globals-vixl.h"
 #include "jit/arm64/vixl/Instructions-vixl.h"
 #include "jit/arm64/vixl/Instrument-vixl.h"
+#include "jit/arm64/vixl/MozCachingDecoder.h"
 #include "jit/arm64/vixl/Simulator-Constants-vixl.h"
 #include "jit/arm64/vixl/Utils-vixl.h"
 #include "jit/IonTypes.h"
 #include "js/AllocPolicy.h"
 #include "vm/MutexIDs.h"
 #include "vm/PosixNSPR.h"
 #include "wasm/WasmSignalHandlers.h"
 
@@ -692,16 +693,19 @@ class SimExclusiveGlobalMonitor {
   const int kPassProbability;
   uint32_t seed_;
 };
 
 class Redirection;
 
 class Simulator : public DecoderVisitor {
  public:
+#ifdef JS_CACHE_SIMULATOR_ARM64
+  using Decoder = CachingDecoder;
+#endif
   explicit Simulator(Decoder* decoder, FILE* stream = stdout);
   ~Simulator();
 
   // Moz changes.
   void init(Decoder* decoder, FILE* stream);
   static Simulator* Current();
   static Simulator* Create();
   static void Destroy(Simulator* sim);
@@ -710,16 +714,19 @@ class Simulator : public DecoderVisitor 
   bool overRecursed(uintptr_t newsp = 0) const;
   bool overRecursedWithExtra(uint32_t extra) const;
   int64_t call(uint8_t* entry, int argument_count, ...);
   static void* RedirectNativeFunction(void* nativeFunction, js::jit::ABIFunctionType type);
   void setGPR32Result(int32_t result);
   void setGPR64Result(int64_t result);
   void setFP32Result(float result);
   void setFP64Result(double result);
+#ifdef JS_CACHE_SIMULATOR_ARM64
+  void FlushICache();
+#endif
   void VisitCallRedirection(const Instruction* instr);
   static uintptr_t StackLimit() {
     return Simulator::Current()->stackLimit();
   }
   static bool supportsAtomics() {
     return true;
   }
   template<typename T> T Read(uintptr_t address);
@@ -2693,16 +2700,36 @@ class SimulatorProcess
     : lock_(mutexid::Arm64SimulatorLock)
     , redirection_(nullptr)
   {}
 
   // Synchronizes access between main thread and compilation threads.
   js::Mutex lock_;
   vixl::Redirection* redirection_;
 
+#ifdef JS_CACHE_SIMULATOR_ARM64
+  // For each simulator, record what other thread registered as instruction
+  // being invalidated.
+  struct ICacheFlush {
+    void* start;
+    size_t length;
+  };
+  using ICacheFlushes = mozilla::Vector<ICacheFlush, 2>;
+  struct SimFlushes {
+    Simulator* thread;
+    ICacheFlushes records;
+  };
+  mozilla::Vector<SimFlushes, 1> pendingFlushes_;
+
+  static void recordICacheFlush(void* start, size_t length);
+  static ICacheFlushes& getICacheFlushes(Simulator* sim);
+  static MOZ_MUST_USE bool registerSimulator(Simulator* sim);
+  static void unregisterSimulator(Simulator* sim);
+#endif
+
   static void setRedirection(vixl::Redirection* redirection) {
     MOZ_ASSERT(singleton_->lock_.ownedByCurrentThread());
     singleton_->redirection_ = redirection;
   }
 
   static vixl::Redirection* redirection() {
     MOZ_ASSERT(singleton_->lock_.ownedByCurrentThread());
     return singleton_->redirection_;
@@ -2713,13 +2740,25 @@ class SimulatorProcess
     return !!singleton_;
   }
   static void destroy() {
     js_delete(singleton_);
     singleton_ = nullptr;
   }
 };
 
+// Protects the icache and redirection properties of the simulator.
+class AutoLockSimulatorCache : public js::LockGuard<js::Mutex>
+{
+  using Base = js::LockGuard<js::Mutex>;
+
+ public:
+  explicit AutoLockSimulatorCache()
+    : Base(SimulatorProcess::singleton_->lock_)
+  {
+  }
+};
+
 } // namespace jit
 } // namespace js
 
 #endif  // JS_SIMULATOR_ARM64
 #endif  // VIXL_A64_SIMULATOR_A64_H_
--- a/js/src/vm/JSScript.cpp
+++ b/js/src/vm/JSScript.cpp
@@ -705,36 +705,43 @@ static void DefaultInitializeElements(vo
 template <typename T>
 void SharedScriptData::initElements(size_t offset, size_t length) {
   uintptr_t base = reinterpret_cast<uintptr_t>(this);
   DefaultInitializeElements<T>(reinterpret_cast<void*>(base + offset), length);
 }
 
 SharedScriptData::SharedScriptData(uint32_t codeLength, uint32_t noteLength,
                                    uint32_t natoms)
-    : codeLength_(codeLength), noteLength_(noteLength), natoms_(natoms) {
+    : codeLength_(codeLength) {
   // Variable-length data begins immediately after SharedScriptData itself.
   size_t cursor = sizeof(*this);
 
   // Default-initialize trailing arrays.
 
   static_assert(alignof(SharedScriptData) >= alignof(GCPtrAtom),
                 "Incompatible alignment");
   initElements<GCPtrAtom>(cursor, natoms);
   cursor += natoms * sizeof(GCPtrAtom);
 
   static_assert(alignof(GCPtrAtom) >= alignof(jsbytecode),
                 "Incompatible alignment");
+  codeOffset_ = cursor;
   initElements<jsbytecode>(cursor, codeLength);
   cursor += codeLength * sizeof(jsbytecode);
 
   static_assert(alignof(jsbytecode) >= alignof(jssrcnote),
                 "Incompatible alignment");
   initElements<jssrcnote>(cursor, noteLength);
   cursor += noteLength * sizeof(jssrcnote);
+  tailOffset_ = cursor;
+
+  // Check that we correctly recompute the expected values.
+  MOZ_ASSERT(this->natoms() == natoms);
+  MOZ_ASSERT(this->codeLength() == codeLength);
+  MOZ_ASSERT(this->noteLength() == noteLength);
 
   // Sanity check
   MOZ_ASSERT(AllocationSize(codeLength, noteLength, natoms) == cursor);
 }
 
 template <XDRMode mode>
 /* static */
 XDRResult SharedScriptData::XDR(XDRState<mode>* xdr, HandleScript script) {
@@ -745,30 +752,37 @@ XDRResult SharedScriptData::XDR(XDRState
   JSContext* cx = xdr->cx();
   SharedScriptData* ssd = nullptr;
 
   if (mode == XDR_ENCODE) {
     ssd = script->scriptData();
 
     natoms = ssd->natoms();
     codeLength = ssd->codeLength();
-    noteLength = ssd->numNotes();
+    noteLength = ssd->noteLength();
   }
 
   MOZ_TRY(xdr->codeUint32(&natoms));
   MOZ_TRY(xdr->codeUint32(&codeLength));
   MOZ_TRY(xdr->codeUint32(&noteLength));
 
   if (mode == XDR_DECODE) {
     if (!script->createSharedScriptData(cx, codeLength, noteLength, natoms)) {
       return xdr->fail(JS::TranscodeResult_Throw);
     }
     ssd = script->scriptData();
   }
 
+  MOZ_TRY(xdr->codeUint32(&ssd->mainOffset));
+  MOZ_TRY(xdr->codeUint32(&ssd->nfixed));
+  MOZ_TRY(xdr->codeUint32(&ssd->nslots));
+  MOZ_TRY(xdr->codeUint32(&ssd->bodyScopeIndex));
+  MOZ_TRY(xdr->codeUint16(&ssd->funLength));
+  MOZ_TRY(xdr->codeUint16(&ssd->numBytecodeTypeSets));
+
   JS_STATIC_ASSERT(sizeof(jsbytecode) == 1);
   JS_STATIC_ASSERT(sizeof(jssrcnote) == 1);
 
   jsbytecode* code = ssd->code();
   jssrcnote* notes = ssd->notes();
   MOZ_TRY(xdr->codeBytes(code, codeLength));
   MOZ_TRY(xdr->codeBytes(notes, noteLength));
 
@@ -821,18 +835,16 @@ XDRResult js::XDRScript(XDRState<mode>* 
   uint32_t nfixed = 0;
   uint32_t nslots = 0;
   uint32_t bodyScopeIndex = 0;
   uint32_t sourceStart = 0;
   uint32_t sourceEnd = 0;
   uint32_t toStringStart = 0;
   uint32_t toStringEnd = 0;
   uint32_t immutableFlags = 0;
-  uint16_t funLength = 0;
-  uint16_t numBytecodeTypeSets = 0;
 
   // NOTE: |mutableFlags| are not preserved by XDR.
 
   JSContext* cx = xdr->cx();
   RootedScript script(cx);
 
   if (mode == XDR_ENCODE) {
     script = scriptp.get();
@@ -870,34 +882,29 @@ XDRResult js::XDRScript(XDRState<mode>* 
     bodyScopeIndex = script->bodyScopeIndex();
 
     sourceStart = script->sourceStart();
     sourceEnd = script->sourceEnd();
     toStringStart = script->toStringStart();
     toStringEnd = script->toStringEnd();
 
     immutableFlags = script->immutableFlags_;
-
-    funLength = script->funLength();
-    numBytecodeTypeSets = script->numBytecodeTypeSets();
   }
 
   MOZ_TRY(xdr->codeUint32(&lineno));
   MOZ_TRY(xdr->codeUint32(&column));
   MOZ_TRY(xdr->codeUint32(&mainOffset));
   MOZ_TRY(xdr->codeUint32(&nfixed));
   MOZ_TRY(xdr->codeUint32(&nslots));
   MOZ_TRY(xdr->codeUint32(&bodyScopeIndex));
   MOZ_TRY(xdr->codeUint32(&sourceStart));
   MOZ_TRY(xdr->codeUint32(&sourceEnd));
   MOZ_TRY(xdr->codeUint32(&toStringStart));
   MOZ_TRY(xdr->codeUint32(&toStringEnd));
   MOZ_TRY(xdr->codeUint32(&immutableFlags));
-  MOZ_TRY(xdr->codeUint16(&funLength));
-  MOZ_TRY(xdr->codeUint16(&numBytecodeTypeSets));
 
   RootedScriptSourceObject sourceObject(cx, sourceObjectArg);
   Maybe<CompileOptions> options;
 
   if (mode == XDR_DECODE) {
     // When loading from the bytecode cache, we get the CompileOptions from
     // the document. If the noScriptRval or selfHostingMode flag doesn't
     // match, we should fail. This only applies to the top-level and not
@@ -958,23 +965,17 @@ XDRResult js::XDRScript(XDRState<mode>* 
                               sourceEnd, toStringStart, toStringEnd);
     if (!script) {
       return xdr->fail(JS::TranscodeResult_Throw);
     }
     scriptp.set(script);
 
     script->lineno_ = lineno;
     script->column_ = column;
-    script->mainOffset_ = mainOffset;
-    script->nfixed_ = nfixed;
-    script->nslots_ = nslots;
-    script->bodyScopeIndex_ = bodyScopeIndex;
     script->immutableFlags_ = immutableFlags;
-    script->funLength_ = funLength;
-    script->numBytecodeTypeSets_ = numBytecodeTypeSets;
 
     if (script->hasFlag(ImmutableFlags::ArgsHasVarBinding)) {
       // Call setArgumentsHasVarBinding to initialize the
       // NeedsArgsAnalysis flag.
       script->setArgumentsHasVarBinding();
     }
 
     // Set the script in its function now so that inner scripts to be
@@ -2930,17 +2931,17 @@ bool ScriptSource::setSourceMapURL(JSCon
  *
  * SharedScriptData::data contains data that can be shared within a
  * runtime. The atoms() data is placed first to simplify its alignment.
  *
  * Array elements   Pointed to by         Length
  * --------------   -------------         ------
  * GCPtrAtom        atoms()               natoms()
  * jsbytecode       code()                codeLength()
- * jsscrnote        notes()               numNotes()
+ * jsscrnote        notes()               noteLength()
  */
 
 SharedScriptData* js::SharedScriptData::new_(JSContext* cx, uint32_t codeLength,
                                              uint32_t noteLength,
                                              uint32_t natoms) {
   // Compute size including trailing arrays
   size_t size = AllocationSize(codeLength, noteLength, natoms);
 
@@ -2951,20 +2952,16 @@ SharedScriptData* js::SharedScriptData::
     return nullptr;
   }
 
   // Constuct the SharedScriptData. Trailing arrays are uninitialized but
   // GCPtrs are put into a safe state.
   return new (raw) SharedScriptData(codeLength, noteLength, natoms);
 }
 
-inline js::ScriptBytecodeHasher::Lookup::Lookup(SharedScriptData* data)
-    : scriptData(data),
-      hash(mozilla::HashBytes(scriptData->data(), scriptData->dataLength())) {}
-
 bool JSScript::createSharedScriptData(JSContext* cx, uint32_t codeLength,
                                       uint32_t noteLength, uint32_t natoms) {
   MOZ_ASSERT(!scriptData_);
   scriptData_ = SharedScriptData::new_(cx, codeLength, noteLength, natoms);
   return !!scriptData_;
 }
 
 void JSScript::freeScriptData() { scriptData_ = nullptr; }
@@ -2977,17 +2974,17 @@ void JSScript::freeScriptData() { script
  */
 bool JSScript::shareScriptData(JSContext* cx) {
   SharedScriptData* ssd = scriptData();
   MOZ_ASSERT(ssd);
   MOZ_ASSERT(ssd->refCount() == 1);
 
   // Calculate the hash before taking the lock. Because the data is reference
   // counted, it also will be freed after releasing the lock if necessary.
-  ScriptBytecodeHasher::Lookup lookup(ssd);
+  SharedScriptDataHasher::Lookup lookup(ssd);
 
   AutoLockScriptData lock(cx->runtime());
 
   ScriptDataTable::AddPtr p = cx->scriptDataTable(lock).lookupForAdd(lookup);
   if (p) {
     MOZ_ASSERT(ssd != *p);
     scriptData_ = *p;
   } else {
@@ -3461,18 +3458,16 @@ bool JSScript::initFunctionPrototype(JSC
   uint32_t numTryNotes = 0;
   uint32_t numScopeNotes = 0;
   uint32_t nresumeoffsets = 0;
   if (!createPrivateScriptData(cx, script, numScopes, numConsts, numObjects,
                                numTryNotes, numScopeNotes, nresumeoffsets)) {
     return false;
   }
 
-  script->numBytecodeTypeSets_ = 0;
-
   RootedScope enclosing(cx, &cx->global()->emptyGlobalScope());
   Scope* functionProtoScope = FunctionScope::create(cx, nullptr, false, false,
                                                     functionProto, enclosing);
   if (!functionProtoScope) {
     return false;
   }
 
   mozilla::Span<GCPtrScope> scopes = script->data_->scopes();
@@ -3523,18 +3518,16 @@ static bool NeedsFunctionEnvironmentObje
       return true;
     }
   }
 
   return false;
 }
 
 void JSScript::initFromFunctionBox(frontend::FunctionBox* funbox) {
-  funLength_ = funbox->length;
-
   setFlag(ImmutableFlags::FunHasExtensibleScope, funbox->hasExtensibleScope());
   setFlag(ImmutableFlags::NeedsHomeObject, funbox->needsHomeObject());
   setFlag(ImmutableFlags::IsDerivedClassConstructor,
           funbox->isDerivedClassConstructor());
   setFlag(ImmutableFlags::HasMappedArgsObj, funbox->hasMappedArgsObj());
   setFlag(ImmutableFlags::FunctionHasThisBinding, funbox->hasThisBinding());
   setFlag(ImmutableFlags::FunctionHasExtraBodyVarScope,
           funbox->hasExtraBodyVarScope());
@@ -3573,21 +3566,16 @@ bool JSScript::fullyInitFromEmitter(JSCo
       static_cast<uint64_t>(bce->bytecodeSection().maxStackDepth());
   if (nslots > UINT32_MAX) {
     bce->reportError(nullptr, JSMSG_NEED_DIET, js_script_str);
     return false;
   }
 
   // Initialize POD fields
   script->lineno_ = bce->firstLine;
-  script->mainOffset_ = bce->mainOffset();
-  script->nfixed_ = bce->maxFixedSlots;
-  script->nslots_ = nslots;
-  script->bodyScopeIndex_ = bce->bodyScopeIndex;
-  script->numBytecodeTypeSets_ = bce->bytecodeSection().typesetCount();
 
   // Initialize script flags from BytecodeEmitter
   script->setFlag(ImmutableFlags::Strict, bce->sc->strict());
   script->setFlag(ImmutableFlags::BindingsAccessedDynamically,
                   bce->sc->bindingsAccessedDynamically());
   script->setFlag(ImmutableFlags::HasSingletons, bce->hasSingletons);
   script->setFlag(ImmutableFlags::IsForEval, bce->sc->isEvalContext());
   script->setFlag(ImmutableFlags::IsModule, bce->sc->isModuleContext());
@@ -3602,17 +3590,17 @@ bool JSScript::fullyInitFromEmitter(JSCo
   }
 
   // Create and initialize PrivateScriptData
   if (!PrivateScriptData::InitFromEmitter(cx, script, bce)) {
     return false;
   }
 
   // Create and initialize SharedScriptData
-  if (!SharedScriptData::InitFromEmitter(cx, script, bce)) {
+  if (!SharedScriptData::InitFromEmitter(cx, script, bce, nslots)) {
     return false;
   }
   if (!script->shareScriptData(cx)) {
     return false;
   }
 
   // NOTE: JSScript is now constructed and should be linked in.
 
@@ -4226,23 +4214,17 @@ JSScript* js::detail::CopyScript(JSConte
                            src->toStringEnd()));
   if (!dst) {
     return nullptr;
   }
 
   // Copy POD fields
   dst->lineno_ = src->lineno();
   dst->column_ = src->column();
-  dst->mainOffset_ = src->mainOffset();
-  dst->nfixed_ = src->nfixed();
-  dst->nslots_ = src->nslots();
-  dst->bodyScopeIndex_ = src->bodyScopeIndex();
   dst->immutableFlags_ = src->immutableFlags_;
-  dst->funLength_ = src->funLength();
-  dst->numBytecodeTypeSets_ = src->numBytecodeTypeSets();
 
   dst->setFlag(JSScript::ImmutableFlags::HasNonSyntacticScope,
                scopes[0]->hasOnChain(ScopeKind::NonSyntactic));
 
   if (src->argumentsHasVarBinding()) {
     dst->setArgumentsHasVarBinding();
   }
 
@@ -4517,31 +4499,43 @@ bool JSScript::hasBreakpointsAt(jsbyteco
   if (!site) {
     return false;
   }
 
   return site->enabledCount > 0;
 }
 
 /* static */ bool SharedScriptData::InitFromEmitter(
-    JSContext* cx, js::HandleScript script, frontend::BytecodeEmitter* bce) {
+    JSContext* cx, js::HandleScript script, frontend::BytecodeEmitter* bce,
+    uint32_t nslots) {
   uint32_t natoms = bce->perScriptData().atomIndices()->count();
   uint32_t codeLength = bce->bytecodeSection().code().length();
 
   // The + 1 is to account for the final SN_MAKE_TERMINATOR that is appended
   // when the notes are copied to their final destination by copySrcNotes.
   uint32_t noteLength = bce->bytecodeSection().notes().length() + 1;
 
   // Create and initialize SharedScriptData
   if (!script->createSharedScriptData(cx, codeLength, noteLength, natoms)) {
     return false;
   }
 
   js::SharedScriptData* data = script->scriptData_;
 
+  // Initialize POD fields
+  data->mainOffset = bce->mainOffset();
+  data->nfixed = bce->maxFixedSlots;
+  data->nslots = nslots;
+  data->bodyScopeIndex = bce->bodyScopeIndex;
+  data->numBytecodeTypeSets = bce->bytecodeSection().typesetCount();
+
+  if (bce->sc->isFunctionBox()) {
+    data->funLength = bce->sc->asFunctionBox()->length;
+  }
+
   // Initialize trailing arrays
   std::copy_n(bce->bytecodeSection().code().begin(), codeLength, data->code());
   bce->copySrcNotes(data->notes(), noteLength);
   InitAtomMap(*bce->perScriptData().atomIndices(), data->atoms());
 
   return true;
 }
 
--- a/js/src/vm/JSScript.h
+++ b/js/src/vm/JSScript.h
@@ -1499,19 +1499,52 @@ class alignas(JS::Value) PrivateScriptDa
 class alignas(uintptr_t) SharedScriptData final {
   // This class is reference counted as follows: each pointer from a JSScript
   // counts as one reference plus there may be one reference from the shared
   // script data table.
   mozilla::Atomic<uint32_t, mozilla::SequentiallyConsistent,
                   mozilla::recordreplay::Behavior::DontPreserve>
       refCount_ = {};
 
+  uint32_t codeOffset_ = 0;  // Byte-offset from 'this'
   uint32_t codeLength_ = 0;
-  uint32_t noteLength_ = 0;
-  uint32_t natoms_ = 0;
+  uint32_t tailOffset_ = 0;
+
+  // Offset of main entry point from code, after predef'ing prologue.
+  uint32_t mainOffset = 0;
+
+  // Fixed frame slots.
+  uint32_t nfixed = 0;
+
+  // Slots plus maximum stack depth.
+  uint32_t nslots = 0;
+
+  // Index into the scopes array of the body scope.
+  uint32_t bodyScopeIndex = 0;
+
+#if JS_BITS_PER_WORD == 64
+  uint32_t padding_ = 0;
+#endif
+
+  // ES6 function length.
+  uint16_t funLength = 0;
+
+  // Number of type sets used in this script for dynamic type monitoring.
+  uint16_t numBytecodeTypeSets = 0;
+
+  // NOTE: The raw bytes of this structure are used for hashing so use explicit
+  // padding values as needed for predicatable results across compilers.
+
+  friend class ::JSScript;
+
+ private:
+  // Layout of trailing arrays
+  size_t atomOffset() const { return sizeof(SharedScriptData); };
+  size_t codeOffset() const { return codeOffset_; }
+  size_t noteOffset() const { return codeOffset_ + codeLength_; }
 
   // Size to allocate
   static size_t AllocationSize(uint32_t codeLength, uint32_t noteLength,
                                uint32_t natoms);
 
   template <typename T>
   void initElements(size_t offset, size_t length);
 
@@ -1527,96 +1560,109 @@ class alignas(uintptr_t) SharedScriptDat
   void Release() {
     MOZ_ASSERT(refCount_ != 0);
     uint32_t remain = --refCount_;
     if (remain == 0) {
       js_free(this);
     }
   }
 
-  size_t dataLength() const {
-    return (natoms_ * sizeof(GCPtrAtom)) + codeLength_ + noteLength_;
-  }
-  const uint8_t* data() const {
-    return reinterpret_cast<const uint8_t*>(this + 1);
+  // Span over all raw bytes in this struct and its trailing arrays.
+  mozilla::Span<const uint8_t> allocSpan() const {
+    size_t allocSize = tailOffset_;
+    return mozilla::MakeSpan(reinterpret_cast<const uint8_t*>(this), allocSize);
   }
-  uint8_t* data() { return reinterpret_cast<uint8_t*>(this + 1); }
-
-  uint32_t natoms() const { return natoms_; }
+
+  // Span over all immutable bytes in allocation. This excludes part of
+  // structure used for reference counting and is the basis of how we
+  // de-duplicate data.
+  mozilla::Span<const uint8_t> immutableData() const {
+    // The refCount_ must be first field of structure.
+    static_assert(offsetof(SharedScriptData, refCount_) == 0,
+                  "refCount_ must be at start of SharedScriptData");
+    constexpr size_t dataOffset = sizeof(refCount_);
+
+    static_assert(offsetof(SharedScriptData, numBytecodeTypeSets) +
+                          sizeof(numBytecodeTypeSets) ==
+                      sizeof(SharedScriptData),
+                  "SharedScriptData should not have padding after last field");
+
+    return allocSpan().From(dataOffset);
+  }
+
+  uint32_t natoms() const {
+    return (codeOffset_ - atomOffset()) / sizeof(GCPtrAtom);
+  }
   GCPtrAtom* atoms() {
-    if (!natoms_) {
-      return nullptr;
-    }
-    return reinterpret_cast<GCPtrAtom*>(data());
+    uintptr_t base = reinterpret_cast<uintptr_t>(this);
+    return reinterpret_cast<GCPtrAtom*>(base + atomOffset());
   }
 
   uint32_t codeLength() const { return codeLength_; }
   jsbytecode* code() {
-    return reinterpret_cast<jsbytecode*>(data() + natoms_ * sizeof(GCPtrAtom));
+    uintptr_t base = reinterpret_cast<uintptr_t>(this);
+    return reinterpret_cast<jsbytecode*>(base + codeOffset_);
+  }
+
+  uint32_t noteLength() const { return tailOffset_ - noteOffset(); }
+  jssrcnote* notes() {
+    uintptr_t base = reinterpret_cast<uintptr_t>(this);
+    return reinterpret_cast<jssrcnote*>(base + noteOffset());
   }
 
-  uint32_t numNotes() const { return noteLength_; }
-  jssrcnote* notes() {
-    return reinterpret_cast<jssrcnote*>(data() + natoms_ * sizeof(GCPtrAtom) +
-                                        codeLength_);
+  static constexpr size_t offsetOfCodeOffset() {
+    return offsetof(SharedScriptData, codeOffset_);
+  }
+  static constexpr size_t offsetOfNfixed() {
+    return offsetof(SharedScriptData, nfixed);
+  }
+  static constexpr size_t offsetOfNslots() {
+    return offsetof(SharedScriptData, nslots);
+  }
+  static constexpr size_t offsetOfFunLength() {
+    return offsetof(SharedScriptData, funLength);
   }
 
   void traceChildren(JSTracer* trc);
 
-  static constexpr size_t offsetOfNatoms() {
-    return offsetof(SharedScriptData, natoms_);
-  }
-
   template <XDRMode mode>
   static MOZ_MUST_USE XDRResult XDR(js::XDRState<mode>* xdr,
                                     js::HandleScript script);
 
   static bool InitFromEmitter(JSContext* cx, js::HandleScript script,
-                              js::frontend::BytecodeEmitter* bce);
+                              js::frontend::BytecodeEmitter* bce,
+                              uint32_t nslots);
 
   // Mark this SharedScriptData for use in a new zone
   void markForCrossZone(JSContext* cx);
 
   // SharedScriptData has trailing data so isn't copyable or movable.
   SharedScriptData(const SharedScriptData&) = delete;
   SharedScriptData& operator=(const SharedScriptData&) = delete;
 };
 
-struct ScriptBytecodeHasher {
-  class Lookup {
-    friend struct ScriptBytecodeHasher;
-
-    RefPtr<SharedScriptData> scriptData;
-    HashNumber hash;
-
-   public:
-    explicit Lookup(SharedScriptData* data);
-  };
-
-  static HashNumber hash(const Lookup& l) { return l.hash; }
+// Two SharedScriptData instances may be de-duplicated if they have the same
+// data in their immutableData() span. This Hasher enables that comparison.
+struct SharedScriptDataHasher {
+  using Lookup = RefPtr<SharedScriptData>;
+
+  static HashNumber hash(const Lookup& l) {
+    mozilla::Span<const uint8_t> immutableData = l->immutableData();
+    return mozilla::HashBytes(immutableData.data(), immutableData.size());
+  }
+
   static bool match(SharedScriptData* entry, const Lookup& lookup) {
-    const SharedScriptData* data = lookup.scriptData;
-    if (entry->natoms() != data->natoms()) {
-      return false;
-    }
-    if (entry->codeLength() != data->codeLength()) {
-      return false;
-    }
-    if (entry->numNotes() != data->numNotes()) {
-      return false;
-    }
-    return mozilla::ArrayEqual<uint8_t>(entry->data(), data->data(),
-                                        data->dataLength());
+    return entry->immutableData() == lookup->immutableData();
   }
 };
 
 class AutoLockScriptData;
 
 using ScriptDataTable =
-    HashSet<SharedScriptData*, ScriptBytecodeHasher, SystemAllocPolicy>;
+    HashSet<SharedScriptData*, SharedScriptDataHasher, SystemAllocPolicy>;
 
 extern void SweepScriptData(JSRuntime* rt);
 
 extern void FreeScriptData(JSRuntime* rt);
 
 } /* namespace js */
 
 namespace JS {
@@ -1673,28 +1719,16 @@ class JSScript : public js::gc::TenuredC
   uint32_t dataSize_ = 0;
 
   /* Base line number of script. */
   uint32_t lineno_ = 0;
 
   /* Base column of script, optionally set. */
   uint32_t column_ = 0;
 
-  /* Offset of main entry point from code, after predef'ing prologue. */
-  uint32_t mainOffset_ = 0;
-
-  /* Fixed frame slots. */
-  uint32_t nfixed_ = 0;
-
-  /* Slots plus maximum stack depth. */
-  uint32_t nslots_ = 0;
-
-  /* Index into the scopes array of the body scope */
-  uint32_t bodyScopeIndex_ = 0;
-
   // Range of characters in scriptSource which contains this script's
   // source, that is, the range used by the Parser to produce this script.
   //
   // Most scripted functions have sourceStart_ == toStringStart_ and
   // sourceEnd_ == toStringEnd_. However, for functions with extra
   // qualifiers (e.g. generators, async) and for class constructors (which
   // need to return the entire class source), their values differ.
   //
@@ -1883,24 +1917,16 @@ class JSScript : public js::gc::TenuredC
     SpewEnabled = 1 << 25,
   };
 
  private:
   // Note: don't make this a bitfield! It makes it hard to read these flags
   // from JIT code.
   uint32_t mutableFlags_ = 0;
 
-  // 16-bit fields.
-
-  /* ES6 function length. */
-  uint16_t funLength_ = 0;
-
-  /* Number of type sets used in this script for dynamic type monitoring. */
-  uint16_t numBytecodeTypeSets_ = 0;
-
   //
   // End of fields.  Start methods.
   //
 
  private:
   template <js::XDRMode mode>
   friend js::XDRResult js::XDRScript(js::XDRState<mode>* xdr,
                                      js::HandleScope enclosingScope,
@@ -1909,17 +1935,17 @@ class JSScript : public js::gc::TenuredC
                                      js::MutableHandleScript scriptp);
 
   template <js::XDRMode mode>
   friend js::XDRResult js::SharedScriptData::XDR(js::XDRState<mode>* xdr,
                                                  js::HandleScript script);
 
   friend bool js::SharedScriptData::InitFromEmitter(
       JSContext* cx, js::HandleScript script,
-      js::frontend::BytecodeEmitter* bce);
+      js::frontend::BytecodeEmitter* bce, uint32_t nslot);
 
   template <js::XDRMode mode>
   friend js::XDRResult js::PrivateScriptData::XDR(
       js::XDRState<mode>* xdr, js::HandleScript script,
       js::HandleScriptSourceObject sourceObject,
       js::HandleScope scriptEnclosingScope, js::HandleFunction fun);
 
   friend bool js::PrivateScriptData::Clone(
@@ -2070,44 +2096,44 @@ class JSScript : public js::gc::TenuredC
     return size_t(pc - code());
   }
 
   jsbytecode* offsetToPC(size_t offset) const {
     MOZ_ASSERT(offset < length());
     return code() + offset;
   }
 
-  size_t mainOffset() const { return mainOffset_; }
+  size_t mainOffset() const { return scriptData_->mainOffset; }
 
   uint32_t lineno() const { return lineno_; }
 
   uint32_t column() const { return column_; }
 
   void setColumn(size_t column) { column_ = column; }
 
   // The fixed part of a stack frame is comprised of vars (in function and
   // module code) and block-scoped locals (in all kinds of code).
-  size_t nfixed() const { return nfixed_; }
+  size_t nfixed() const { return scriptData_->nfixed; }
 
   // Number of fixed slots reserved for slots that are always live. Only
   // nonzero for function or module code.
   size_t numAlwaysLiveFixedSlots() const {
     if (bodyScope()->is<js::FunctionScope>()) {
       return bodyScope()->as<js::FunctionScope>().nextFrameSlot();
     }
     if (bodyScope()->is<js::ModuleScope>()) {
       return bodyScope()->as<js::ModuleScope>().nextFrameSlot();
     }
     return 0;
   }
 
   // Calculate the number of fixed slots that are live at a particular bytecode.
   size_t calculateLiveFixed(jsbytecode* pc);
 
-  size_t nslots() const { return nslots_; }
+  size_t nslots() const { return scriptData_->nslots; }
 
   unsigned numArgs() const {
     if (bodyScope()->is<js::FunctionScope>()) {
       return bodyScope()
           ->as<js::FunctionScope>()
           .numPositionalFormalParameters();
     }
     return 0;
@@ -2123,24 +2149,24 @@ class JSScript : public js::gc::TenuredC
     }
     return scope->as<js::FunctionScope>().hasParameterExprs();
   }
 
   // If there are more than MaxBytecodeTypeSets JOF_TYPESET ops in the script,
   // the first MaxBytecodeTypeSets - 1 JOF_TYPESET ops have their own TypeSet
   // and all other JOF_TYPESET ops share the last TypeSet.
   static constexpr size_t MaxBytecodeTypeSets = UINT16_MAX;
-  static_assert(sizeof(numBytecodeTypeSets_) == 2,
-                "MaxBytecodeTypeSets must match sizeof(numBytecodeTypeSets_)");
-
-  size_t numBytecodeTypeSets() const { return numBytecodeTypeSets_; }
-
-  size_t funLength() const { return funLength_; }
-
-  static size_t offsetOfFunLength() { return offsetof(JSScript, funLength_); }
+  static_assert(sizeof(js::SharedScriptData::numBytecodeTypeSets) == 2,
+                "MaxBytecodeTypeSets must match sizeof(numBytecodeTypeSets)");
+
+  size_t numBytecodeTypeSets() const {
+    return scriptData_->numBytecodeTypeSets;
+  }
+
+  size_t funLength() const { return scriptData_->funLength; }
 
   uint32_t sourceStart() const { return sourceStart_; }
 
   uint32_t sourceEnd() const { return sourceEnd_; }
 
   uint32_t sourceLength() const { return sourceEnd_ - sourceStart_; }
 
   uint32_t toStringStart() const { return toStringStart_; }
@@ -2339,22 +2365,16 @@ class JSScript : public js::gc::TenuredC
   }
 
   static constexpr size_t offsetOfMutableFlags() {
     return offsetof(JSScript, mutableFlags_);
   }
   static size_t offsetOfImmutableFlags() {
     return offsetof(JSScript, immutableFlags_);
   }
-  static constexpr size_t offsetOfNfixed() {
-    return offsetof(JSScript, nfixed_);
-  }
-  static constexpr size_t offsetOfNslots() {
-    return offsetof(JSScript, nslots_);
-  }
   static constexpr size_t offsetOfScriptData() {
     return offsetof(JSScript, scriptData_);
   }
   static constexpr size_t offsetOfTypes() { return offsetof(JSScript, types_); }
 
   bool hasAnyIonScript() const { return hasIonScript(); }
 
   bool hasIonScript() const {
@@ -2531,19 +2551,19 @@ class JSScript : public js::gc::TenuredC
   js::TypeScript* types() { return types_; }
 
   void maybeReleaseTypes();
 
   inline js::GlobalObject& global() const;
   inline bool hasGlobal(const js::GlobalObject* global) const;
   js::GlobalObject& uninlinedGlobal() const;
 
-  uint32_t bodyScopeIndex() const { return bodyScopeIndex_; }
-
-  js::Scope* bodyScope() const { return getScope(bodyScopeIndex_); }
+  uint32_t bodyScopeIndex() const { return scriptData_->bodyScopeIndex; }
+
+  js::Scope* bodyScope() const { return getScope(bodyScopeIndex()); }
 
   js::Scope* outermostScope() const {
     // The body scope may not be the outermost scope in the script when
     // the decl env scope is present.
     size_t index = 0;
     return getScope(index);
   }
 
@@ -2702,17 +2722,17 @@ class JSScript : public js::gc::TenuredC
   jsbytecode* tableSwitchCasePC(jsbytecode* pc, uint32_t caseIndex) const {
     return offsetToPC(tableSwitchCaseOffset(pc, caseIndex));
   }
 
   bool hasLoops();
 
   uint32_t numNotes() const {
     MOZ_ASSERT(scriptData_);
-    return scriptData_->numNotes();
+    return scriptData_->noteLength();
   }
   jssrcnote* notes() const {
     MOZ_ASSERT(scriptData_);
     return scriptData_->notes();
   }
 
   size_t natoms() const {
     MOZ_ASSERT(scriptData_);
--- a/layout/reftests/bugs/360746-1.html
+++ b/layout/reftests/bugs/360746-1.html
@@ -1,28 +1,28 @@
 <html>
   <head>
     <title>Testcase bug 360746 - The right panel has disappeared at andrewdupont.net</title>
     <style>
       div { height: 20px }
       #test1 {
-        background-color: red;
+        background-color: green;
       }
       #test2 {
         background-color: red;
       }
       #test3 {
         background-color: green;
       }
       #test4 {
         background-color: green;
       }
     </style>
     <link href="data:text/css;charset=utf-8," title="narrow" rel="stylesheet" type="text/css" />
-    <link href="data:text/css;charset=utf-8,%23test1%20%7Bbackground-color%3A%20green%3B%7D" title="medium" rel="alternate stylesheet" type="text/css" />
+    <link href="data:text/css;charset=utf-8,%23test1%20%7Bbackground-color%3A%20red%3B%7D" title="medium" rel="alternate stylesheet" type="text/css" />
     <link href="data:text/css;charset=utf-8,%23test2%20%7Bbackground-color%3A%20green%3B%7D" type="text/css" />    
     <link href="data:text/css;charset=utf-8,%23test3%20%7Bbackground-color%3A%20red%3B%7D" rel="stylesheet" type="text/css" />    
     <link href="data:text/css;charset=utf-8,%23test4%20%7Bbackground-color%3A%20red%3B%7D" rel="stylesheet" type="text/css" />    
     <script>
       document.getElementsByTagName('link')[1].disabled = false;
       document.getElementsByTagName('link')[1].rel = 'stylesheet';
     </script>
   </head>
--- a/layout/style/Loader.cpp
+++ b/layout/style/Loader.cpp
@@ -1083,35 +1083,35 @@ static Loader::MediaMatched MediaListMat
   return Loader::MediaMatched::No;
 }
 
 /**
  * PrepareSheet() handles setting the media and title on the sheet, as
  * well as setting the enabled state based on the title and whether
  * the sheet had "alternate" in its rel.
  */
-Loader::MediaMatched Loader::PrepareSheet(StyleSheet* aSheet,
-                                          const nsAString& aTitle,
-                                          const nsAString& aMediaString,
-                                          MediaList* aMediaList,
-                                          IsAlternate aIsAlternate) {
+Loader::MediaMatched Loader::PrepareSheet(
+    StyleSheet* aSheet, const nsAString& aTitle, const nsAString& aMediaString,
+    MediaList* aMediaList, IsAlternate aIsAlternate,
+    IsExplicitlyEnabled aIsExplicitlyEnabled) {
   MOZ_ASSERT(aSheet, "Must have a sheet!");
 
   RefPtr<MediaList> mediaList(aMediaList);
 
   if (!aMediaString.IsEmpty()) {
     NS_ASSERTION(!aMediaList,
                  "must not provide both aMediaString and aMediaList");
     mediaList = MediaList::Create(aMediaString);
   }
 
   aSheet->SetMedia(mediaList);
 
   aSheet->SetTitle(aTitle);
-  aSheet->SetEnabled(aIsAlternate == IsAlternate::No);
+  aSheet->SetEnabled(aIsAlternate == IsAlternate::No ||
+                     aIsExplicitlyEnabled == IsExplicitlyEnabled::Yes);
   return MediaListMatches(mediaList, mDocument);
 }
 
 /**
  * InsertSheetInTree handles ordering of sheets in the document or shadow root.
  *
  * Here we have two types of sheets -- those with linking elements and
  * those without.  The latter are loaded by Link: headers, and are only added to
@@ -1830,18 +1830,18 @@ Result<Loader::LoadSheetResult, nsresult
   if (NS_FAILED(rv)) {
     return Err(rv);
   }
   NS_ASSERTION(state == eSheetNeedsParser,
                "Inline sheets should not be cached");
 
   LOG(("  Sheet is alternate: %d", static_cast<int>(isAlternate)));
 
-  auto matched =
-      PrepareSheet(sheet, aInfo.mTitle, aInfo.mMedia, nullptr, isAlternate);
+  auto matched = PrepareSheet(sheet, aInfo.mTitle, aInfo.mMedia, nullptr,
+                              isAlternate, aInfo.mIsExplicitlyEnabled);
 
   InsertSheetInTree(*sheet, aInfo.mContent);
 
   nsIPrincipal* principal = aInfo.mContent->NodePrincipal();
   if (aInfo.mTriggeringPrincipal) {
     // The triggering principal may be an expanded principal, which is safe to
     // use for URL security checks, but not as the loader principal for a
     // stylesheet. So treat this as principal inheritance, and downgrade if
@@ -1935,18 +1935,18 @@ Result<Loader::LoadSheetResult, nsresult
   rv = CreateSheet(aInfo, principal, eAuthorSheetFeatures, syncLoad, state,
                    &sheet);
   if (NS_FAILED(rv)) {
     return Err(rv);
   }
 
   LOG(("  Sheet is alternate: %d", static_cast<int>(isAlternate)));
 
-  auto matched =
-      PrepareSheet(sheet, aInfo.mTitle, aInfo.mMedia, nullptr, isAlternate);
+  auto matched = PrepareSheet(sheet, aInfo.mTitle, aInfo.mMedia, nullptr,
+                              isAlternate, aInfo.mIsExplicitlyEnabled);
 
   InsertSheetInTree(*sheet, aInfo.mContent);
 
   nsCOMPtr<nsIStyleSheetLinkingElement> owningElement(
       do_QueryInterface(aInfo.mContent));
 
   if (state == eSheetComplete) {
     LOG(("  Sheet already complete: 0x%p", sheet.get()));
@@ -2102,17 +2102,18 @@ nsresult Loader::LoadChildSheet(StyleShe
     // For now, use CORS_NONE for child sheets
     rv = CreateSheet(aURL, nullptr, principal, aParentSheet->ParsingMode(),
                      CORS_NONE, aParentSheet->GetReferrerPolicy(),
                      EmptyString(),  // integrity is only checked on main sheet
                      aParentData ? aParentData->mSyncLoad : false, state,
                      &sheet);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    PrepareSheet(sheet, empty, empty, aMedia, IsAlternate::No);
+    PrepareSheet(sheet, empty, empty, aMedia, IsAlternate::No,
+                 IsExplicitlyEnabled::No);
   }
 
   MOZ_ASSERT(sheet);
   InsertChildSheet(*sheet, *aParentSheet);
 
   if (state == eSheetComplete) {
     LOG(("  Sheet already complete"));
     // We're completely done.  No need to notify, even, since the
@@ -2213,17 +2214,18 @@ nsresult Loader::InternalLoadNonDocument
   StyleSheetState state;
   RefPtr<StyleSheet> sheet;
   bool syncLoad = (aObserver == nullptr);
   const nsAString& empty = EmptyString();
   rv = CreateSheet(aURL, nullptr, aOriginPrincipal, aParsingMode, aCORSMode,
                    aReferrerPolicy, aIntegrity, syncLoad, state, &sheet);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  PrepareSheet(sheet, empty, empty, nullptr, IsAlternate::No);
+  PrepareSheet(sheet, empty, empty, nullptr, IsAlternate::No,
+               IsExplicitlyEnabled::No);
 
   if (state == eSheetComplete) {
     LOG(("  Sheet already complete"));
     if (aObserver || !mObservers.IsEmpty()) {
       rv = PostLoadEvent(aURL, sheet, aObserver, IsAlternate::No,
                          MediaMatched::Yes, nullptr);
     }
     if (aSheet) {
--- a/layout/style/Loader.h
+++ b/layout/style/Loader.h
@@ -194,16 +194,17 @@ enum StyleSheetState {
 class Loader final {
   typedef mozilla::net::ReferrerPolicy ReferrerPolicy;
 
  public:
   typedef nsIStyleSheetLinkingElement::Completed Completed;
   typedef nsIStyleSheetLinkingElement::HasAlternateRel HasAlternateRel;
   typedef nsIStyleSheetLinkingElement::IsAlternate IsAlternate;
   typedef nsIStyleSheetLinkingElement::IsInline IsInline;
+  typedef nsIStyleSheetLinkingElement::IsExplicitlyEnabled IsExplicitlyEnabled;
   typedef nsIStyleSheetLinkingElement::MediaMatched MediaMatched;
   typedef nsIStyleSheetLinkingElement::Update LoadSheetResult;
   typedef nsIStyleSheetLinkingElement::SheetInfo SheetInfo;
 
   Loader();
   // aDocGroup is used for dispatching SheetLoadData in PostLoadEvent(). It
   // can be null if you want to use this constructor, and there's no
   // document when the Loader is constructed.
@@ -475,18 +476,18 @@ class Loader final {
                        StyleSheetState& aSheetState,
                        RefPtr<StyleSheet>* aSheet);
 
   // Pass in either a media string or the MediaList from the CSSParser.  Don't
   // pass both.
   //
   // This method will set the sheet's enabled state based on aIsAlternate
   MediaMatched PrepareSheet(StyleSheet* aSheet, const nsAString& aTitle,
-                            const nsAString& aMediaString,
-                            dom::MediaList* aMediaList, IsAlternate);
+                            const nsAString& aMediaString, dom::MediaList*,
+                            IsAlternate, IsExplicitlyEnabled);
 
   // Inserts a style sheet in a document or a ShadowRoot.
   void InsertSheetInTree(StyleSheet& aSheet, nsIContent* aLinkingContent);
   // Inserts a style sheet into a parent style sheet.
   void InsertChildSheet(StyleSheet& aSheet, StyleSheet& aParentSheet);
 
   nsresult InternalLoadNonDocumentSheet(
       nsIURI* aURL, bool aIsPreload, SheetParsingMode aParsingMode,
--- a/layout/style/StyleSheet.cpp
+++ b/layout/style/StyleSheet.cpp
@@ -421,16 +421,19 @@ void StyleSheet::AddStyleSet(ServoStyleS
   MOZ_DIAGNOSTIC_ASSERT(!mStyleSets.Contains(aStyleSet),
                         "style set already registered");
   mStyleSets.AppendElement(aStyleSet);
 }
 
 void StyleSheet::DropStyleSet(ServoStyleSet* aStyleSet) {
   bool found = mStyleSets.RemoveElement(aStyleSet);
   MOZ_DIAGNOSTIC_ASSERT(found, "didn't find style set");
+#ifndef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+  Unused << found;
+#endif
 }
 
 void StyleSheet::EnsureUniqueInner() {
   MOZ_ASSERT(mInner->mSheets.Length() != 0, "unexpected number of outers");
   mState |= State::ForcedUniqueInner;
 
   if (HasUniqueInner()) {
     // already unique
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -248,16 +248,28 @@ VARCACHE_PREF(
 // NOTE: This preference is used in unit tests. If it is removed or its default
 // value changes, please update test_sharedMap_var_caches.js accordingly.
 VARCACHE_PREF(
   "dom.mutation-events.cssom.disabled",
    dom_mutation_events_cssom_disabled,
   bool, true
 )
 
+// Whether the disabled attribute in HTMLLinkElement disables the sheet loading
+// altogether, or forwards to the inner stylesheet method without attribute
+// reflection.
+//
+// Historical behavior is the second, the first is being discussed at:
+// https://github.com/whatwg/html/issues/3840
+VARCACHE_PREF(
+  "dom.link.disabled_attribute.enabled",
+   dom_link_disabled_attribute_enabled,
+  bool, true
+)
+
 VARCACHE_PREF(
   "dom.performance.enable_scheduler_timing",
   dom_performance_enable_scheduler_timing,
   RelaxedAtomicBool, true
 )
 
 // Should we defer timeouts and intervals while loading a page.  Released
 // on Idle or when the page is loaded.
--- a/taskcluster/ci/test/raptor.yml
+++ b/taskcluster/ci/test/raptor.yml
@@ -379,17 +379,21 @@ raptor-tp6m-2-refbrow:
             - --binary-path=org.mozilla.reference.browser
             - --activity=BrowserTestActivity
 
 raptor-tp6m-3-geckoview:
     description: "Raptor tp6m-3 on Geckoview"
     try-name: raptor-tp6m-3-geckoview
     treeherder-symbol: Rap(tp6m-3)
     target: geckoview_example.apk
-    run-on-projects: ['try', 'mozilla-central']
+    run-on-projects:
+        by-test-platform:
+            android-hw.*/pgo: ['try', 'mozilla-central']
+            android-hw.*aarch64/opt: ['try', 'mozilla-central']
+            default: ['try']
     tier: 2
     mozharness:
         extra-options:
             - --test=raptor-tp6m-3
             - --app=geckoview
             - --binary=org.mozilla.geckoview_example
 
 raptor-tp6m-3-refbrow:
@@ -407,17 +411,21 @@ raptor-tp6m-3-refbrow:
             - --binary-path=org.mozilla.reference.browser
             - --activity=BrowserTestActivity
 
 raptor-tp6m-4-geckoview:
     description: "Raptor tp6m-4 on Geckoview"
     try-name: raptor-tp6m-4-geckoview
     treeherder-symbol: Rap(tp6m-4)
     target: geckoview_example.apk
-    run-on-projects: ['try', 'mozilla-central']
+    run-on-projects:
+        by-test-platform:
+            android-hw.*/pgo: ['try', 'mozilla-central']
+            android-hw.*aarch64/opt: ['try', 'mozilla-central']
+            default: ['try']
     tier: 2
     mozharness:
         extra-options:
             - --test=raptor-tp6m-4
             - --app=geckoview
             - --binary=org.mozilla.geckoview_example
 
 raptor-tp6m-4-refbrow:
@@ -501,17 +509,21 @@ raptor-tp6m-6-refbrow:
             - --app=refbrow
             - --binary-path=org.mozilla.reference.browser
             - --activity=BrowserTestActivity
 
 raptor-tp6m-7-geckoview:
     description: "Raptor tp6m-7 on Geckoview"
     try-name: raptor-tp6m-7-geckoview
     treeherder-symbol: Rap(tp6m-7)
-    run-on-projects: ['try', 'mozilla-central']
+    run-on-projects:
+        by-test-platform:
+            android-hw.*/pgo: ['try', 'mozilla-central']
+            android-hw.*aarch64/opt: ['try', 'mozilla-central']
+            default: ['try']
     target: geckoview_example.apk
     tier: 2
     mozharness:
         extra-options:
             - --test=raptor-tp6m-7
             - --app=geckoview
             - --binary=org.mozilla.geckoview_example
             - --activity=GeckoViewActivity
@@ -564,17 +576,21 @@ raptor-tp6m-8-refbrow:
             - --binary-path=org.mozilla.reference.browser
             - --activity=BrowserTestActivity
 
 raptor-tp6m-9-geckoview:
     description: "Raptor tp6m-9 on Geckoview"
     try-name: raptor-tp6m-9-geckoview
     treeherder-symbol: Rap(tp6m-9)
     target: geckoview_example.apk
-    run-on-projects: ['try', 'mozilla-central']
+    run-on-projects:
+        by-test-platform:
+            android-hw.*/pgo: ['try', 'mozilla-central']
+            android-hw.*aarch64/opt: ['try', 'mozilla-central']
+            default: ['try']
     tier: 2
     mozharness:
         extra-options:
             - --test=raptor-tp6m-9
             - --app=geckoview
             - --binary=org.mozilla.geckoview_example
             - --activity=GeckoViewActivity
 
@@ -593,17 +609,21 @@ raptor-tp6m-9-refbrow:
             - --binary-path=org.mozilla.reference.browser
             - --activity=BrowserTestActivity
 
 raptor-tp6m-10-geckoview:
     description: "Raptor tp6m-10 on Geckoview"
     try-name: raptor-tp6m-10-geckoview
     treeherder-symbol: Rap(tp6m-10)
     target: geckoview_example.apk
-    run-on-projects: ['try', 'mozilla-central']
+    run-on-projects:
+        by-test-platform:
+            android-hw.*/pgo: ['try', 'mozilla-central']
+            android-hw.*aarch64/opt: ['try', 'mozilla-central']
+            default: ['try']
     tier: 2
     mozharness:
         extra-options:
             - --test=raptor-tp6m-10
             - --app=geckoview
             - --binary=org.mozilla.geckoview_example
             - --activity=GeckoViewActivity
 
--- a/testing/web-platform/tests/css/css-transitions/disconnected-element-001.html
+++ b/testing/web-platform/tests/css/css-transitions/disconnected-element-001.html
@@ -18,101 +18,95 @@ on it from the completed transitions.">
 <script src="./support/helper.js" type="text/javascript"></script>
 
 </head>
 <body>
 <div id="log"></div>
 
 <script>
 promise_test(async t => {
-  // Create element but do not attach it to the document
+  // Create element and remove it from the document
   const div = addDiv(t, {
     style: 'transition: background-color 100s; background-color: red',
   });
+  div.remove();
+
+  // Attach event listeners
+  div.addEventListener('transitionrun', t.step_func(() => {
+    assert_unreached('transitionrun event should not be fired');
+  }));
 
   // Resolve before-change style
   getComputedStyle(div).backgroundColor;
 
-  // Set up after-change style
+  // Set up and resolve after-change style
   div.style.backgroundColor = 'green';
-
-  assert_equals(
-    getComputedStyle(div).backgroundColor,
-    'rgb(255, 0, 0)',
-    'No transition should run'
-  );
+  getComputedStyle(div).backgroundColor;
 
-  // Wait a frame just to be sure the UA does not start the transition on the
-  // next frame.
+  // There should be no events received for the triggered transition.
   await waitForFrame();
-
-  assert_equals(
-    getComputedStyle(div).backgroundColor,
-    'rgb(255, 0, 0)',
-    'No transition should run even after waiting a frame'
-  );
+  await waitForFrame();
 }, 'Transitions do not run on an element not in the document');
 
 test(t => {
   // Create element but do not attach it to the document
   const div = addDiv(t, {
     style: 'transition: background-color 100s; background-color: red',
   });
+  div.remove();
 
   // Resolve before-change style
   getComputedStyle(div).backgroundColor;
 
   // Add to document
   document.documentElement.append(div);
 
-  // Set up after-change style
+  // Set up and resolve after-change style
   div.style.backgroundColor = 'green';
+  getComputedStyle(div).backgroundColor;
 
+  // We should have jumped immediately to the after-change style rather than
+  // transitioning to it.
   assert_equals(
     getComputedStyle(div).backgroundColor,
     'rgb(0, 128, 0)',
     'No transition should run'
   );
 }, 'Transitions do not run for an element newly added to the document');
 
 promise_test(async t => {
   // Create element and attach it to the document
   const div = addDiv(t, {
     style: 'transition: background-color 100s; background-color: red',
   });
-  document.documentElement.append(div);
 
   // Attach event listeners
   div.addEventListener('transitionrun', t.step_func(() => {
     assert_unreached('transitionrun event should not be fired');
   }));
 
   // Resolve before-change style
   getComputedStyle(div).backgroundColor;
 
   // Set up after-change style
   div.style.backgroundColor = 'green';
 
   // But remove the document before the next style change event
   div.remove();
 
   // There should be no events received for the triggered transition.
-  //
-  // (We can't verify the presence/absence of transitions by querying
-  // getComputedStyle for this case because it will return an empty string.)
   await waitForFrame();
   await waitForFrame();
 }, 'Transitions do not run for an element newly removed from the document');
 
 promise_test(async t => {
   // Create element and attach it to the document
   const div = addDiv(t, {
     style: 'transition: background-color 100s; background-color: red',
   });
-  document.documentElement.append(div);
 
   // Attach event listeners
   const eventWatcher = new EventWatcher(t, div, [
     'transitionrun',
     'transitioncancel',
   ]);
 
   // Trigger transition
@@ -126,23 +120,21 @@ promise_test(async t => {
   div.remove();
 
   await eventWatcher.wait_for('transitioncancel');
 }, 'Transitions are canceled when an element is removed from the document');
 
 promise_test(async t => {
   // Create a container element. We'll need this later.
   const container = addDiv(t);
-  document.documentElement.append(container);
 
   // Create element and attach it to the document
   const div = addDiv(t, {
     style: 'transition: background-color 100s; background-color: red',
   });
-  document.documentElement.append(div);
 
   // Attach event listeners
   const eventWatcher = new EventWatcher(t, div, [
     'transitionrun',
     'transitioncancel',
   ]);
 
   // Trigger transition
@@ -159,40 +151,35 @@ promise_test(async t => {
   assert_equals(
     getComputedStyle(div).backgroundColor,
     'rgb(0, 128, 0)',
     'There should be no transition after re-parenting'
   );
 }, 'Transitions are canceled when an element is re-parented');
 
 promise_test(async t => {
-  // Create a container element. We'll need this later.
-  const container = addDiv(t);
-  document.documentElement.append(container);
-
   // Create element and attach it to the document
   const div = addDiv(t, {
     style: 'transition: background-color 100s; background-color: red',
   });
-  document.documentElement.append(div);
 
   // Attach event listeners
   const eventWatcher = new EventWatcher(t, div, [
     'transitionrun',
     'transitioncancel',
   ]);
 
   // Trigger transition
   getComputedStyle(div).backgroundColor;
   div.style.backgroundColor = 'green';
   getComputedStyle(div).backgroundColor;
 
   await eventWatcher.wait_for('transitionrun');
 
-  // Re-parent element to same container
+  // Re-parent element to same parent
   document.documentElement.append(div);
 
   await eventWatcher.wait_for('transitioncancel');
   assert_equals(
     getComputedStyle(div).backgroundColor,
     'rgb(0, 128, 0)',
     'There should be no transition after re-parenting'
   );
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-001.tentative.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<title>&lt;link disabled&gt;, HTMLLinkElement.disabled and CSSStyleSheet.disabled interactions</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1281135">
+<link rel="help" href="https://github.com/whatwg/html/issues/3840">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link title="alt" rel="stylesheet" disabled href="data:text/css,html { background: green }">
+<script>
+function assert_applies(applies) {
+  (applies ? assert_equals : assert_not_equals)(getComputedStyle(document.documentElement).backgroundColor, "rgb(0, 128, 0)");
+}
+
+const link = document.querySelector("link[disabled]");
+
+test(function() {
+  assert_equals(document.styleSheets.length, 0);
+  assert_applies(false);
+}, "<link disabled> prevents the stylesheet from being in document.styleSheets (from parser)");
+
+async_test(function(t) {
+  assert_true(link.disabled);
+
+  link.onload = t.step_func_done(function() {
+    assert_equals(document.styleSheets.length, 1);
+    let sheet = document.styleSheets[0];
+    assert_equals(sheet.ownerNode, link);
+    assert_applies(true);
+
+    link.disabled = true;
+    assert_equals(sheet.ownerNode, null);
+    assert_false(sheet.disabled);
+    assert_applies(false);
+    assert_true(link.hasAttribute("disabled"));
+
+    assert_equals(document.styleSheets.length, 0);
+    assert_applies(false);
+  });
+
+  link.disabled = false;
+  assert_true(!link.hasAttribute("disabled"));
+  assert_false(link.disabled);
+}, "HTMLLinkElement.disabled reflects the <link disabled> attribute, and behaves consistently");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-002.tentative.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<title>&lt;link disabled&gt;, HTMLLinkElement.disabled and CSSStyleSheet.disabled interactions (alternate)</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1281135">
+<link rel="help" href="https://github.com/whatwg/html/issues/3840">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link title="alt" rel="alternate stylesheet" disabled href="data:text/css,html { background: green }">
+<script>
+function assert_applies(applies) {
+  (applies ? assert_equals : assert_not_equals)(getComputedStyle(document.documentElement).backgroundColor, "rgb(0, 128, 0)");
+}
+
+const link = document.querySelector("link[disabled]");
+
+async_test(function(t) {
+  assert_true(link.disabled);
+
+  link.onload = t.step_func_done(function() {
+    assert_equals(document.styleSheets.length, 1);
+    let sheet = document.styleSheets[0];
+    assert_equals(sheet.ownerNode, link);
+    assert_applies(true);
+
+    link.disabled = true;
+    assert_equals(sheet.ownerNode, null);
+    assert_false(sheet.disabled);
+    assert_applies(false);
+    assert_true(link.hasAttribute("disabled"));
+    assert_equals(document.styleSheets.length, 0);
+  });
+
+  link.disabled = false;
+  assert_true(!link.hasAttribute("disabled"));
+  assert_false(link.disabled);
+}, "HTMLLinkElement.disabled reflects the <link disabled> attribute, and behaves consistently, when the sheet is an alternate");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-003.tentative.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<title>&lt;link disabled&gt;'s "explicitly enabled" state persists after getting disconnected from the tree</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1281135">
+<link rel="help" href="https://github.com/whatwg/html/issues/3840">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link title="alt" rel="alternate stylesheet" disabled href="data:text/css,html { background: green }">
+<script>
+function assert_applies(applies) {
+  (applies ? assert_equals : assert_not_equals)(getComputedStyle(document.documentElement).backgroundColor, "rgb(0, 128, 0)");
+}
+
+const link = document.querySelector("link[disabled]");
+async_test(function(t) {
+  assert_true(link.disabled);
+  link.disabled = false;
+  assert_false(link.disabled);
+  assert_true(!link.hasAttribute("disabled"));
+  link.remove();
+
+  link.onload = t.step_func_done(function() {
+    assert_equals(document.styleSheets.length, 1);
+    let sheet = document.styleSheets[0];
+    assert_equals(sheet.ownerNode, link);
+    assert_applies(true);
+  });
+
+  document.head.appendChild(link);
+}, "HTMLLinkElement.disabled's explicitly enabled state persists when disconnected and connected again");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-004.tentative.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<title>&lt;link disabled&gt;'s "explicitly enabled" state doesn't persist for clones</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1281135">
+<link rel="help" href="https://github.com/whatwg/html/issues/3840">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link title="alt" rel="alternate stylesheet" disabled href="data:text/css,html { background: green }">
+<script>
+function assert_applies(applies) {
+  (applies ? assert_equals : assert_not_equals)(getComputedStyle(document.documentElement).backgroundColor, "rgb(0, 128, 0)");
+}
+
+const link = document.querySelector("link[disabled]");
+
+async_test(function(t) {
+  link.remove();
+  link.disabled = false; // `link` is explicitly enabled.
+
+  let clonesLoaded = 0;
+
+  for (let shallow of [true, false]) {
+    const clone = link.cloneNode(shallow);
+    clone.onload = t.step_func(function() {
+      assert_false(link.disabled);
+      // Even though it's not disabled, it still doesn't apply, since it's an alternate.
+      assert_applies(false);
+      if (++clonesLoaded == 2) {
+        link.onload = t.step_func_done(function() {
+          assert_false(link.disabled);
+          assert_applies(true); // `link` is still explicitly enabled.
+        });
+        document.head.appendChild(link);
+      }
+    });
+    document.head.appendChild(clone);
+  }
+}, "HTMLLinkElement.disabled's explicitly enabled state doesn't persist on clones");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-005.tentative.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>&lt;link disabled&gt;'s "explicitly enabled" persists across rel changes</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1281135">
+<link rel="help" href="https://github.com/whatwg/html/issues/3840">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link title="alt" rel="yadayada" disabled href="data:text/css,html { background: green }">
+<script>
+function assert_applies(applies) {
+  (applies ? assert_equals : assert_not_equals)(getComputedStyle(document.documentElement).backgroundColor, "rgb(0, 128, 0)");
+}
+
+const link = document.querySelector("link[disabled]");
+
+async_test(function(t) {
+  link.onload = t.step_func_done(function() {
+    assert_applies(true);
+    link.setAttribute("rel", "alternate stylesheet");
+    assert_applies(true);
+    assert_false(link.disabled);
+  });
+  link.disabled = false;
+  link.setAttribute("rel", "stylesheet");
+}, "HTMLLinkElement.disabled's explicitly enabled state persists regardless of rel");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-006.tentative.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>&lt;link disabled&gt;'s "explicitly enabled" state isn't magically set from the setter</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1281135">
+<link rel="help" href="https://github.com/whatwg/html/issues/3840">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+function assert_applies(applies) {
+  (applies ? assert_equals : assert_not_equals)(getComputedStyle(document.documentElement).backgroundColor, "rgb(0, 128, 0)");
+}
+
+async_test(function(t) {
+  const link = document.createElement("link");
+  link.setAttribute("rel", "alternate stylesheet");
+  link.setAttribute("title", "alt");
+  link.href = "data:text/css,html { background: green }";
+  link.disabled = false; // This should do nothing, and is the point of this test.
+  link.onload = t.step_func_done(function() {
+    assert_applies(false); // Should not apply, since it's an alternate that hasn't been enabled.
+    assert_false(link.disabled);
+  });
+  document.head.appendChild(link);
+}, "HTMLLinkElement.disabled setter does nothing if the attribute isn't present already.");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-007.tentative.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>&lt;link disabled&gt;'s "explicitly enabled" state works when set explicitly back and forth</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1281135">
+<link rel="help" href="https://github.com/whatwg/html/issues/3840">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+function assert_applies(applies) {
+  (applies ? assert_equals : assert_not_equals)(getComputedStyle(document.documentElement).backgroundColor, "rgb(0, 128, 0)");
+}
+
+async_test(function(t) {
+  const link = document.createElement("link");
+  link.setAttribute("rel", "alternate stylesheet");
+  link.setAttribute("title", "alt");
+  link.href = "data:text/css,html { background: green }";
+  link.disabled = true;
+  link.disabled = false; // This should make it "explicitly enabled".
+  link.onload = t.step_func_done(function() {
+    assert_applies(true); // Should apply, since it's explicitly enabled.
+    assert_false(link.disabled);
+  });
+  document.head.appendChild(link);
+}, "HTMLLinkElement.disabled setter sets the explicitly enabled state if toggled back and forth.");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-alternate-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<title>CSS Test Reference</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<style>
+  html { background: green }
+</style>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/HTMLLinkElement-disabled-alternate.tentative.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>CSS Test: alternate stylesheets can be disabled by HTMLLinkElement.disabled if they have the disabled attribute already</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1281135">
+<link rel="help" href="https://github.com/whatwg/html/issues/3840">
+<link rel="match" href="HTMLLinkElement-disabled-alternate-ref.html">
+<link title="alt" rel="alternate stylesheet" href="data:text/css,html { background: green }" disabled onload="document.documentElement.className = ''">
+<script>
+  onload = function() {
+    const link = document.querySelector("link[rel='alternate stylesheet']");
+    link.disabled = false;
+  }
+</script>
--- a/toolkit/content/widgets/checkbox.js
+++ b/toolkit/content/widgets/checkbox.js
@@ -25,17 +25,17 @@ class MozCheckbox extends MozElements.Ba
         // Prevent page from scrolling on the space key.
         event.preventDefault();
       }
     });
   }
 
   static get inheritedAttributes() {
     return {
-      ".checkbox-check": "disabled",
+      ".checkbox-check": "disabled,checked",
       ".checkbox-label": "text=label,accesskey",
       ".checkbox-icon": "src",
     };
   }
 
   connectedCallback() {
     if (this.delayConnectedCallback()) {
       return;