Merge mozilla-central to mozilla-beta a=merge l10n=merge CLOSED TREE DEVEDITION_62_0b2_BUILD1 DEVEDITION_62_0b2_RELEASE
authorBrindusan Cristian <cbrindusan@mozilla.com>
Thu, 21 Jun 2018 17:53:57 +0300
changeset 810281 3178ed26d318f5b65b8e15a2392794bd70c13296
parent 808974 b37931e5b0101134ee1aaeb452cb8926be1cc46c (current diff)
parent 809260 1c33a38da75d550750923358e73d7af2102b9c1d (diff)
child 810282 57ddf5eb2b6c4e528dcf92af1a492a43820278c9
push id113949
push userbmo:mstriemer@mozilla.com
push dateMon, 25 Jun 2018 17:23:48 +0000
reviewersmerge
milestone62.0
Merge mozilla-central to mozilla-beta a=merge l10n=merge CLOSED TREE
devtools/client/locales/en-US/appcacheutils.properties
devtools/client/shared/AppCacheUtils.jsm
servo/components/style/properties/longhand/background.mako.rs
servo/components/style/properties/longhand/border.mako.rs
servo/components/style/properties/longhand/box.mako.rs
servo/components/style/properties/longhand/color.mako.rs
servo/components/style/properties/longhand/column.mako.rs
servo/components/style/properties/longhand/counters.mako.rs
servo/components/style/properties/longhand/effects.mako.rs
servo/components/style/properties/longhand/font.mako.rs
servo/components/style/properties/longhand/inherited_box.mako.rs
servo/components/style/properties/longhand/inherited_svg.mako.rs
servo/components/style/properties/longhand/inherited_table.mako.rs
servo/components/style/properties/longhand/inherited_text.mako.rs
servo/components/style/properties/longhand/inherited_ui.mako.rs
servo/components/style/properties/longhand/list.mako.rs
servo/components/style/properties/longhand/margin.mako.rs
servo/components/style/properties/longhand/outline.mako.rs
servo/components/style/properties/longhand/padding.mako.rs
servo/components/style/properties/longhand/position.mako.rs
servo/components/style/properties/longhand/svg.mako.rs
servo/components/style/properties/longhand/table.mako.rs
servo/components/style/properties/longhand/text.mako.rs
servo/components/style/properties/longhand/ui.mako.rs
servo/components/style/properties/longhand/xul.mako.rs
servo/components/style/properties/shorthand/background.mako.rs
servo/components/style/properties/shorthand/border.mako.rs
servo/components/style/properties/shorthand/box.mako.rs
servo/components/style/properties/shorthand/column.mako.rs
servo/components/style/properties/shorthand/font.mako.rs
servo/components/style/properties/shorthand/inherited_svg.mako.rs
servo/components/style/properties/shorthand/inherited_text.mako.rs
servo/components/style/properties/shorthand/list.mako.rs
servo/components/style/properties/shorthand/margin.mako.rs
servo/components/style/properties/shorthand/mask.mako.rs
servo/components/style/properties/shorthand/outline.mako.rs
servo/components/style/properties/shorthand/padding.mako.rs
servo/components/style/properties/shorthand/position.mako.rs
servo/components/style/properties/shorthand/serialize.mako.rs
servo/components/style/properties/shorthand/text.mako.rs
testing/web-platform/meta/clear-site-data/navigation.https.html.ini
--- a/.hgignore
+++ b/.hgignore
@@ -3,16 +3,19 @@
 # Filenames that should be ignored wherever they appear
 ~$
 \.py(c|o)$
 (?i)(^|/)TAGS$
 (^|/)ID$
 (^|/)\.DS_Store$
 \.pdb
 \.egg-info
+\.gcda
+\.gcno
+\.gcov
 
 # Vim swap files.
 ^\.sw.$
 .[^/]*\.sw.$
 
 # Emacs directory variable files.
 \.dir-locals\.el
 
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -210,37 +210,37 @@ var StarUI = {
           }, delay);
           this._autoCloseTimerEnabled = true;
         }
         break;
     }
   },
 
   _bookmarkPopupInitialized: false,
-  async showEditBookmarkPopup(aNode, aAnchorElement, aPosition, aIsNewBookmark, aUrl) {
+  async showEditBookmarkPopup(aNode, aAnchorElement, aPosition, aIsNewBookmark, aUrl, aIsCurrentBrowser = true) {
     // Slow double-clicks (not true double-clicks) shouldn't
     // cause the panel to flicker.
     if (this.panel.state == "showing" ||
         this.panel.state == "open") {
       return;
     }
 
     this._isNewBookmark = aIsNewBookmark;
     this._itemGuids = null;
 
     if (this._bookmarkPopupInitialized) {
-      await this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl);
+      await this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl, aIsCurrentBrowser);
       return;
     }
     this._bookmarkPopupInitialized = true;
 
-    await this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl);
+    await this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl, aIsCurrentBrowser);
   },
 
-  async _doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl) {
+  async _doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl, aIsCurrentBrowser) {
     if (this.panel.state != "closed")
       return;
 
     this._blockCommands(); // un-done in the popuphidden handler
 
     this._element("editBookmarkPanelTitle").value =
       this._isNewBookmark ?
         gNavigatorBundle.getString("editBookmarkPanel.newBookmarkTitle") :
@@ -266,16 +266,18 @@ var StarUI = {
       let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
       let label = PluralForm.get(bookmarksCount, forms)
                             .replace("#1", bookmarksCount);
       removeButton.label = label;
       removeButton.setAttribute("accesskey",
         gNavigatorBundle.getString("editBookmark.removeBookmarks.accesskey"));
     }
 
+    this._setIconAndPreviewImage(aIsCurrentBrowser);
+
     this.beginBatch();
 
     if (aAnchorElement && aAnchorElement.closest("#urlbar")) {
       this._anchorToolbarButton = aAnchorElement;
       aAnchorElement.setAttribute("open", "true");
     } else {
       this._anchorToolbarButton = null;
     }
@@ -285,26 +287,50 @@ var StarUI = {
       if (target.parentNode) {
         // By targeting the panel's parent and using a capturing listener, we
         // can have our listener called before others waiting for the panel to
         // be shown (which probably expect the panel to be fully initialized)
         target = target.parentNode;
       }
       target.addEventListener("popupshown", function(event) {
         fn();
-      }, {"capture": true, "once": true});
+      }, {capture: true, once: true});
     };
     gEditItemOverlay.initPanel({ node: aNode,
                                  onPanelReady,
                                  hiddenRows: ["location",
                                               "loadInSidebar", "keyword"],
                                  focusedElement: "preferred"});
+
     this.panel.openPopup(aAnchorElement, aPosition);
   },
 
+  _setIconAndPreviewImage(aIsCurrentBrowser) {
+    let faviconImage = this._element("editBookmarkPanelFavicon");
+    faviconImage.removeAttribute("iconloadingprincipal");
+    faviconImage.removeAttribute("src");
+
+    document.mozSetImageElement("editBookmarkPanelImageCanvas", null);
+
+    if (!aIsCurrentBrowser) {
+      return;
+    }
+
+    let tab = gBrowser.selectedTab;
+    if (tab.hasAttribute("image") && !tab.hasAttribute("busy")) {
+      faviconImage.setAttribute("iconloadingprincipal",
+                                tab.getAttribute("iconloadingprincipal"));
+      faviconImage.setAttribute("src", tab.getAttribute("image"));
+    }
+
+    let canvas = PageThumbs.createCanvas(window);
+    PageThumbs.captureToCanvas(gBrowser.selectedBrowser, canvas);
+    document.mozSetImageElement("editBookmarkPanelImageCanvas", canvas);
+  },
+
   panelShown:
   function SU_panelShown(aEvent) {
     if (aEvent.target == this.panel) {
       if (this._element("editBookmarkPanelContent").hidden) {
         // Note this isn't actually used anymore, we should remove this
         // once we decide not to bring back the page bookmarked notification
         this.panel.focus();
       }
@@ -392,16 +418,17 @@ var PlacesCommandHook = {
    *        Option to provide a URL to bookmark rather than the current page
    * @param [optional] aTitle
    *        Option to provide a title for a bookmark to use rather than the
    *        getting the current page's title
    */
   async bookmarkPage(aBrowser, aShowEditUI, aUrl = null, aTitle = null) {
     // If aUrl is provided, we want to bookmark that url rather than the
     // the current page
+    let isCurrentBrowser = !aUrl;
     let url = aUrl ? new URL(aUrl) : new URL(aBrowser.currentURI.spec);
     let info = await PlacesUtils.bookmarks.fetch({ url });
     let isNewBookmark = !info;
     if (!info) {
       let parentGuid = PlacesUtils.bookmarks.unfiledGuid;
       info = { url, parentGuid };
       // Bug 1148838 - Make this code work for full page plugins.
       let charset = null;
@@ -447,23 +474,23 @@ var PlacesCommandHook = {
     if (!aShowEditUI)
       return;
 
     let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
 
     let anchor = BookmarkingUI.anchor;
     if (anchor) {
       await StarUI.showEditBookmarkPopup(node, anchor, "bottomcenter topright",
-                                         isNewBookmark, url);
+                                         isNewBookmark, url, isCurrentBrowser);
       return;
     }
 
     // Fall back to showing the panel over the content area.
     await StarUI.showEditBookmarkPopup(node, aBrowser, "overlap", isNewBookmark,
-                                       url);
+                                       url, isCurrentBrowser);
   },
 
   /**
    * Adds a bookmark to the page targeted by a link.
    * @param parentId
    *        The folder in which to create a new bookmark if aURL isn't
    *        bookmarked.
    * @param url (string)
--- a/browser/base/content/browser-safebrowsing.js
+++ b/browser/base/content/browser-safebrowsing.js
@@ -54,17 +54,17 @@ var gSafeBrowsing = {
    *        Information about the reasons for blocking the resource.
    *        In the case false positive, it may contain SafeBrowsing
    *        matching list and provider of the list
    * @return String the report phishing URL.
    */
   getReportURL(name, info) {
     let reportInfo = info;
     if (!reportInfo) {
-      let pageUri = gBrowser.currentURI.clone();
+      let pageUri = gBrowser.currentURI;
 
       // Remove the query to avoid including potentially sensitive data
       if (pageUri instanceof Ci.nsIURL) {
         pageUri = pageUri.mutate()
                          .setQuery("")
                          .finalize();
       }
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7888,31 +7888,34 @@ var MenuTouchModeObserver = {
 
   uninit() {
     window.removeEventListener("popupshowing", this, true);
   },
 };
 
 var TabContextMenu = {
   contextTab: null,
-  _updateToggleMuteMenuItem(aTab, aConditionFn) {
+  _updateToggleMuteMenuItems(aTab, aConditionFn) {
     ["muted", "soundplaying"].forEach(attr => {
       if (!aConditionFn || aConditionFn(attr)) {
         if (aTab.hasAttribute(attr)) {
           aTab.toggleMuteMenuItem.setAttribute(attr, "true");
+          aTab.toggleMultiSelectMuteMenuItem.setAttribute(attr, "true");
         } else {
           aTab.toggleMuteMenuItem.removeAttribute(attr);
+          aTab.toggleMultiSelectMuteMenuItem.removeAttribute(attr);
         }
       }
     });
   },
   updateContextMenu: function updateContextMenu(aPopupMenu) {
     this.contextTab = aPopupMenu.triggerNode.localName == "tab" ?
                       aPopupMenu.triggerNode : gBrowser.selectedTab;
     let disabled = gBrowser.tabs.length == 1;
+    let multiselectionContext = this.contextTab.multiselected;
 
     var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple");
     for (let menuItem of menuItems)
       menuItem.disabled = disabled;
 
     if (this.contextTab.hasAttribute("customizemode"))
       document.getElementById("context_openTabInWindow").disabled = true;
 
@@ -7937,58 +7940,76 @@ var TabContextMenu = {
     // Disable "Close other Tabs" if there are no unpinned tabs.
     let unpinnedTabsToClose = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs;
     if (!this.contextTab.pinned) {
       unpinnedTabsToClose--;
     }
     document.getElementById("context_closeOtherTabs").disabled = unpinnedTabsToClose < 1;
 
     // Only one of close_tab/close_selected_tabs should be visible
-    let hasMultiSelectedTabs = !!gBrowser.multiSelectedTabsCount;
-    document.getElementById("context_closeTab").hidden = hasMultiSelectedTabs;
-    document.getElementById("context_closeSelectedTabs").hidden = !hasMultiSelectedTabs;
+    document.getElementById("context_closeTab").hidden = multiselectionContext;
+    document.getElementById("context_closeSelectedTabs").hidden = !multiselectionContext;
 
     // Hide "Bookmark All Tabs" for a pinned tab.  Update its state if visible.
     let bookmarkAllTabs = document.getElementById("context_bookmarkAllTabs");
     bookmarkAllTabs.hidden = this.contextTab.pinned;
     if (!bookmarkAllTabs.hidden)
       PlacesCommandHook.updateBookmarkAllTabsCommand();
 
+    let toggleMute = document.getElementById("context_toggleMuteTab");
+    let toggleMultiSelectMute = document.getElementById("context_toggleMuteSelectedTabs");
+
+    // Only one of mute_unmute_tab/mute_unmute_selected_tabs should be visible
+    toggleMute.hidden = multiselectionContext;
+    toggleMultiSelectMute.hidden = !multiselectionContext;
+
     // Adjust the state of the toggle mute menu item.
-    let toggleMute = document.getElementById("context_toggleMuteTab");
     if (this.contextTab.hasAttribute("activemedia-blocked")) {
       toggleMute.label = gNavigatorBundle.getString("playTab.label");
       toggleMute.accessKey = gNavigatorBundle.getString("playTab.accesskey");
     } else if (this.contextTab.hasAttribute("muted")) {
       toggleMute.label = gNavigatorBundle.getString("unmuteTab.label");
       toggleMute.accessKey = gNavigatorBundle.getString("unmuteTab.accesskey");
     } else {
       toggleMute.label = gNavigatorBundle.getString("muteTab.label");
       toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey");
     }
 
+    // Adjust the state of the toggle mute menu item for multi-selected tabs.
+    if (this.contextTab.hasAttribute("activemedia-blocked")) {
+      toggleMultiSelectMute.label = gNavigatorBundle.getString("playTabs.label");
+      toggleMultiSelectMute.accessKey = gNavigatorBundle.getString("playTabs.accesskey");
+    } else if (this.contextTab.hasAttribute("muted")) {
+      toggleMultiSelectMute.label = gNavigatorBundle.getString("unmuteSelectedTabs.label");
+      toggleMultiSelectMute.accessKey = gNavigatorBundle.getString("unmuteSelectedTabs.accesskey");
+    } else {
+      toggleMultiSelectMute.label = gNavigatorBundle.getString("muteSelectedTabs.label");
+      toggleMultiSelectMute.accessKey = gNavigatorBundle.getString("muteSelectedTabs.accesskey");
+    }
+
     this.contextTab.toggleMuteMenuItem = toggleMute;
-    this._updateToggleMuteMenuItem(this.contextTab);
+    this.contextTab.toggleMultiSelectMuteMenuItem = toggleMultiSelectMute;
+    this._updateToggleMuteMenuItems(this.contextTab);
 
     this.contextTab.addEventListener("TabAttrModified", this);
     aPopupMenu.addEventListener("popuphiding", this);
 
     gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
 
     updateTabMenuUserContextUIVisibility("context_reopenInContainer");
   },
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "popuphiding":
         gBrowser.removeEventListener("TabAttrModified", this);
         aEvent.target.removeEventListener("popuphiding", this);
         break;
       case "TabAttrModified":
         let tab = aEvent.target;
-        this._updateToggleMuteMenuItem(tab,
+        this._updateToggleMuteMenuItems(tab,
           attr => aEvent.detail.changed.includes(attr));
         break;
     }
   }
 };
 
 // Prompt user to restart the browser in safe mode
 function safeModeRestart() {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -95,16 +95,18 @@
 
   <popupset id="mainPopupSet">
     <menupopup id="tabContextMenu"
                onpopupshowing="if (event.target == this) TabContextMenu.updateContextMenu(this);"
                onpopuphidden="if (event.target == this) TabContextMenu.contextTab = null;">
       <menuitem id="context_reloadTab" label="&reloadTab.label;" accesskey="&reloadTab.accesskey;"
                 oncommand="gBrowser.reloadTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
+      <menuitem id="context_toggleMuteSelectedTabs" hidden="true"
+                oncommand="gBrowser.toggleMuteAudioOnMultiSelectedTabs(TabContextMenu.contextTab);"/>
       <menuseparator/>
       <menuitem id="context_pinTab" label="&pinTab.label;"
                 accesskey="&pinTab.accesskey;"
                 oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_unpinTab" label="&unpinTab.label;" hidden="true"
                 accesskey="&unpinTab.accesskey;"
                 oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_duplicateTab" label="&duplicateTab.label;"
@@ -227,16 +229,20 @@
            ignorekeys="true"
            hidden="true"
            tabspecific="true"
            onpopupshown="StarUI.panelShown(event);"
            aria-labelledby="editBookmarkPanelTitle">
       <box class="panel-header">
         <label id="editBookmarkPanelTitle"/>
       </box>
+      <box>
+        <html:img id="editBookmarkPanelFavicon"/>
+      </box>
+      <box id="editBookmarkPanelImage"/>
 #include ../../components/places/content/editBookmarkPanel.inc.xul
       <hbox id="editBookmarkPanelBottomButtons" pack="end">
 #ifndef XP_UNIX
         <button id="editBookmarkPanelDoneButton"
                 class="editBookmarkPanelBottomButton"
                 label="&editBookmark.done.label;"
                 default="true"
                 oncommand="StarUI.panel.hidePopup();"/>
--- a/browser/base/content/moz.build
+++ b/browser/base/content/moz.build
@@ -94,17 +94,17 @@ with Files("test/touch/**"):
 
 with Files("test/trackingUI/**"):
     BUG_COMPONENT = ("Firefox", "Tracking Protection")
 
 with Files("test/urlbar/**"):
     BUG_COMPONENT = ("Firefox", "Address Bar")
 
 with Files("test/webextensions/**"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: Untriaged")
+    BUG_COMPONENT = ("WebExtensions", "Untriaged")
 
 with Files("test/webrtc/**"):
     BUG_COMPONENT = ("Core", "WebRTC")
 
 with Files("aboutNetError.xhtml"):
     BUG_COMPONENT = ("Firefox", "Security")
 
 with Files("blockedSite.xhtml"):
@@ -154,12 +154,12 @@ with Files("hiddenWindow.xul"):
 
 with Files("macWindow.inc.xul"):
     BUG_COMPONENT = ("Firefox", "Shell Integration")
 
 with Files("tabbrowser*"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browser")
 
 with Files("webext-panels*"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: Frontend")
+    BUG_COMPONENT = ("WebExtensions", "Frontend")
 
 with Files("webrtcIndicator*"):
     BUG_COMPONENT = ("Firefox", "Device Permissions")
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -3699,16 +3699,40 @@ window._gBrowser = {
     }
     return gBrowser.selectedTab;
   },
 
   set lastMultiSelectedTab(aTab) {
     this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
   },
 
+  toggleMuteAudioOnMultiSelectedTabs(aTab) {
+    const selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet)
+                                    .filter(tab => tab.isConnected);
+    let tabsToToggle;
+
+    if (aTab.activeMediaBlocked) {
+      tabsToToggle = selectedTabs.filter(tab =>
+        tab.activeMediaBlocked || tab.linkedBrowser.audioMuted
+      );
+    } else {
+      let tabMuted = aTab.linkedBrowser.audioMuted;
+      tabsToToggle = selectedTabs.filter(tab =>
+        // When a user is looking to mute selected tabs, then media-blocked tabs
+        // should not be toggled. Otherwise those media-blocked tabs are going into a
+        // playing and unmuted state.
+        tab.linkedBrowser.audioMuted == tabMuted && !tab.activeMediaBlocked ||
+        tab.activeMediaBlocked && tabMuted
+      );
+    }
+    for (let tab of tabsToToggle) {
+      tab.toggleMuteAudio();
+    }
+  },
+
   activateBrowserForPrintPreview(aBrowser) {
     this._printPreviewBrowsers.add(aBrowser);
     if (this._switcher) {
       this._switcher.activateBrowserForPrintPreview(aBrowser);
     }
     aBrowser.docShellIsActive = true;
   },
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2010,30 +2010,34 @@
             } else {
               gBrowser.addToMultiSelectedTabs(this);
               gBrowser.lastMultiSelectedTab = this;
             }
             return;
           }
 
           const overCloseButton = event.originalTarget.getAttribute("anonid") == "close-button";
-          if (gBrowser.multiSelectedTabsCount > 0 && !overCloseButton) {
+          if (gBrowser.multiSelectedTabsCount > 0 && !overCloseButton && !this._overPlayingIcon) {
             // Tabs were previously multi-selected and user clicks on a tab
             // without holding Ctrl/Cmd Key
 
             // Force positional attributes to update when the
             // target (of the click) is the "active" tab.
             let updatePositionalAttr = gBrowser.selectedTab == this;
 
             gBrowser.clearMultiSelectedTabs(updatePositionalAttr);
           }
         }
 
         if (this._overPlayingIcon) {
-          this.toggleMuteAudio();
+          if (this.multiselected) {
+            gBrowser.toggleMuteAudioOnMultiSelectedTabs(this);
+          } else {
+            this.toggleMuteAudio();
+          }
           return;
         }
 
         if (event.originalTarget.getAttribute("anonid") == "close-button") {
           if (this.multiselected) {
             gBrowser.removeMultiSelectedTabs();
           } else {
             gBrowser.removeTab(this, {
--- a/browser/base/content/test/sanitize/browser_sanitize-timespans.js
+++ b/browser/base/content/test/sanitize/browser_sanitize-timespans.js
@@ -448,17 +448,18 @@ async function setupHistory() {
   addPlace("http://2hour.com/", "Less than 2 hours ago", now_uSec - 90 * kUsecPerMin);
   addPlace("http://2hour10minutes.com/", "2 hours 10 minutes ago", now_uSec - 130 * kUsecPerMin);
   addPlace("http://4hour.com/", "Less than 4 hours ago", now_uSec - 180 * kUsecPerMin);
   addPlace("http://4hour10minutes.com/", "4 hours 10 minutesago", now_uSec - 250 * kUsecPerMin);
 
   let today = new Date();
   today.setHours(0);
   today.setMinutes(0);
-  today.setSeconds(1);
+  today.setSeconds(0);
+  today.setMilliseconds(1);
   addPlace("http://today.com/", "Today", today.getTime() * 1000);
 
   let lastYear = new Date();
   lastYear.setFullYear(lastYear.getFullYear() - 1);
   addPlace("http://before-today.com/", "Before Today", lastYear.getTime() * 1000);
   await PlacesTestUtils.addVisits(places);
 }
 
@@ -559,17 +560,18 @@ async function setupFormHistory() {
 
   timestamp = now_uSec - 250 * kUsecPerMin;
   results = await searchEntries(["guid"], { fieldname: "4hour10minutes" });
   await update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
 
   let today = new Date();
   today.setHours(0);
   today.setMinutes(0);
-  today.setSeconds(1);
+  today.setSeconds(0);
+  today.setMilliseconds(1);
   timestamp = today.getTime() * 1000;
   results = await searchEntries(["guid"], { fieldname: "today" });
   await update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
 
   let lastYear = new Date();
   lastYear.setFullYear(lastYear.getFullYear() - 1);
   timestamp = lastYear.getTime() * 1000;
   results = await searchEntries(["guid"], { fieldname: "b4today" });
@@ -650,17 +652,18 @@ async function setupDownloads() {
   download.startTime = new Date(now_mSec - 250 * kMsecPerMin), // 250 minutes ago
   download.canceled = true;
   await publicList.add(download);
 
   // Add "today" download
   let today = new Date();
   today.setHours(0);
   today.setMinutes(0);
-  today.setSeconds(1);
+  today.setSeconds(0);
+  today.setMilliseconds(1);
 
   download = await Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
     target: "fakefile-today"
   });
   download.startTime = today, // 12:00:01 AM this morning
   download.canceled = true;
   await publicList.add(download);
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -45,8 +45,12 @@ skip-if = (debug && os == 'mac') || (deb
 [browser_visibleTabs_contextMenu.js]
 [browser_open_newtab_start_observer_notification.js]
 [browser_bug_1387976_restore_lazy_tab_browser_muted_state.js]
 [browser_multiselect_tabs_using_Ctrl.js]
 [browser_multiselect_tabs_using_Shift.js]
 [browser_multiselect_tabs_close.js]
 [browser_multiselect_tabs_positional_attrs.js]
 [browser_multiselect_tabs_close_using_shortcuts.js]
+[browser_multiselect_tabs_mute_unmute.js]
+support-files =
+  ../general/audio.ogg
+  ../general/file_mediaPlayback.html
--- a/browser/base/content/test/tabs/browser_multiselect_tabs_close.js
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close.js
@@ -64,32 +64,32 @@ add_task(async function usingTabContextM
     let tab3 = await addTab();
     let tab4 = await addTab();
 
     let menuItemCloseTab = document.getElementById("context_closeTab");
     let menuItemCloseSelectedTabs = document.getElementById("context_closeSelectedTabs");
 
     is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
 
-    // Check the context menu with zero multiselected tabs
-    updateTabContextMenu(tab4);
-    is(menuItemCloseTab.hidden, false, "Close Tab is visible");
-    is(menuItemCloseSelectedTabs.hidden, true, "Close Selected Tabs is hidden");
-
     await triggerClickOn(tab1, { ctrlKey: true });
     await triggerClickOn(tab2, { ctrlKey: true });
 
     ok(tab1.multiselected, "Tab1 is multiselected");
     ok(tab2.multiselected, "Tab2 is multiselected");
     ok(!tab3.multiselected, "Tab3 is not multiselected");
     ok(!tab4.multiselected, "Tab4 is not multiselected");
     is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
 
-    // Check the context menu with two multiselected tabs
+    // Check the context menu with a non-multiselected tab
     updateTabContextMenu(tab4);
+    is(menuItemCloseTab.hidden, false, "Close Tab is visible");
+    is(menuItemCloseSelectedTabs.hidden, true, "Close Selected Tabs is hidden");
+
+    // Check the context menu with a multiselected tab
+    updateTabContextMenu(tab2);
     is(menuItemCloseTab.hidden, true, "Close Tab is hidden");
     is(menuItemCloseSelectedTabs.hidden, false, "Close Selected Tabs is visible");
 
     let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1);
     let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2);
     menuItemCloseSelectedTabs.click();
     await tab1Closing;
     await tab2Closing;
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js
@@ -0,0 +1,389 @@
+const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
+const PAGE = "https://example.com/browser/browser/base/content/test/general/file_mediaPlayback.html";
+
+async function wait_for_tab_playing_event(tab, expectPlaying) {
+  if (tab.soundPlaying == expectPlaying) {
+    ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+    return true;
+  }
+  return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, (event) => {
+    if (event.detail.changed.includes("soundplaying")) {
+      is(tab.hasAttribute("soundplaying"), expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+      is(tab.soundPlaying, expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+      return true;
+    }
+    return false;
+  });
+}
+
+async function waitForTabMuteStateChangeEvent(tab) {
+  return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, (event) => {
+    for (let attr of ["activemedia-blocked", "muted", "soundplaying"]) {
+      if (event.detail.changed.includes(attr)) {
+        return true;
+      }
+    }
+    return false;
+  });
+}
+
+async function is_audio_playing(tab) {
+  let browser = tab.linkedBrowser;
+  let isPlaying = await ContentTask.spawn(browser, {}, async function() {
+    let audio = content.document.querySelector("audio");
+    return !audio.paused;
+  });
+  return isPlaying;
+}
+
+async function play(tab) {
+  let browser = tab.linkedBrowser;
+  await ContentTask.spawn(browser, {}, async function() {
+    let audio = content.document.querySelector("audio");
+    audio.play();
+  });
+
+  // If the tab has already been muted, it means the tab won't get soundplaying,
+  // so we don't need to check this attribute.
+  if (browser.audioMuted) {
+    return;
+  }
+
+  await waitForTabMuteStateChangeEvent(tab);
+}
+
+function disable_non_test_mouse(disable) {
+  let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindowUtils);
+  utils.disableNonTestMouseEvents(disable);
+}
+
+function hover_icon(icon, tooltip) {
+  disable_non_test_mouse(true);
+
+  let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+  EventUtils.synthesizeMouse(icon, 1, 1, {type: "mouseover"});
+  EventUtils.synthesizeMouse(icon, 2, 2, {type: "mousemove"});
+  EventUtils.synthesizeMouse(icon, 3, 3, {type: "mousemove"});
+  EventUtils.synthesizeMouse(icon, 4, 4, {type: "mousemove"});
+  return popupShownPromise;
+}
+
+function leave_icon(icon) {
+  EventUtils.synthesizeMouse(icon, 0, 0, {type: "mouseout"});
+  EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+  EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+  EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+
+  disable_non_test_mouse(false);
+}
+
+// The set of tabs which have ever had their mute state changed.
+// Used to determine whether the tab should have a muteReason value.
+let everMutedTabs = new WeakSet();
+
+function get_wait_for_mute_promise(tab, expectMuted) {
+  return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => {
+    if (event.detail.changed.includes("muted") || event.detail.changed.includes("activemedia-blocked")) {
+      is(tab.hasAttribute("muted"), expectMuted, "The tab should " + (expectMuted ? "" : "not ") + "be muted");
+      is(tab.muted, expectMuted, "The tab muted property " + (expectMuted ? "" : "not ") + "be true");
+
+      if (expectMuted || everMutedTabs.has(tab)) {
+        everMutedTabs.add(tab);
+        is(tab.muteReason, null, "The tab should have a null muteReason value");
+      } else {
+        is(tab.muteReason, undefined, "The tab should have an undefined muteReason value");
+      }
+      return true;
+    }
+    return false;
+  });
+}
+
+async function test_mute_tab(tab, icon, expectMuted) {
+  let mutedPromise = waitForTabMuteStateChangeEvent(tab);
+
+  let activeTab = gBrowser.selectedTab;
+
+  let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+
+  await hover_icon(icon, tooltip);
+  EventUtils.synthesizeMouseAtCenter(icon, {button: 0});
+  leave_icon(icon);
+
+  is(gBrowser.selectedTab, activeTab, "Clicking on mute should not change the currently selected tab");
+
+  // If the audio is playing, we should check whether clicking on icon affects
+  // the media element's playing state.
+  let isAudioPlaying = await is_audio_playing(tab);
+  if (isAudioPlaying) {
+    await wait_for_tab_playing_event(tab, !expectMuted);
+  }
+
+  return mutedPromise;
+}
+
+function muted(tab) {
+  return tab.linkedBrowser.audioMuted;
+}
+
+function activeMediaBlocked(tab) {
+  return tab.activeMediaBlocked;
+}
+
+async function addMediaTab() {
+  const tab = BrowserTestUtils.addTab(gBrowser, PAGE, { skipAnimation: true });
+  const browser = gBrowser.getBrowserForTab(tab);
+  await BrowserTestUtils.browserLoaded(browser);
+  return tab;
+}
+
+add_task(async function setPref() {
+  await SpecialPowers.pushPrefEnv({
+    set: [[PREF_MULTISELECT_TABS, true]]
+  });
+});
+
+add_task(async function muteTabs_usingButton() {
+  let tab0 = await addMediaTab();
+  let tab1 = await addMediaTab();
+  let tab2 = await addMediaTab();
+  let tab3 = await addMediaTab();
+  let tab4 = await addMediaTab();
+
+  let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+  await BrowserTestUtils.switchTab(gBrowser, tab0);
+  await play(tab0);
+  await play(tab1);
+  await play(tab2);
+
+  // Multiselecting tab1, tab2 and tab3
+  await BrowserTestUtils.switchTab(gBrowser, tab1);
+  await triggerClickOn(tab3, { shiftKey: true });
+
+  is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+  ok(!tab0.multiselected, "Tab0 is not multiselected");
+  ok(!tab4.multiselected, "Tab4 is not multiselected");
+
+  // tab1,tab2 and tab3 should be multiselected.
+  for (let i = 1; i <= 3; i++) {
+    ok(tabs[i].multiselected, "Tab" + i + " is multiselected");
+  }
+
+  // All five tabs are unmuted
+  for (let i = 0; i < 5; i++) {
+    ok(!muted(tabs[i]), "Tab" + i + " is not muted");
+  }
+
+  // Mute tab0 which is not multiselected, thus other tabs muted state should not be affected
+  let tab0MuteAudioBtn = document.getAnonymousElementByAttribute(tab0, "anonid", "soundplaying-icon");
+  await test_mute_tab(tab0, tab0MuteAudioBtn, true);
+
+  ok(muted(tab0), "Tab0 is muted");
+  for (let i = 1; i <= 4; i++) {
+    ok(!muted(tabs[i]), "Tab" + i + " is not muted");
+  }
+
+  // Now we multiselect tab0
+  await triggerClickOn(tab0, { ctrlKey: true });
+
+  // tab0, tab1, tab2, tab3 are multiselected
+  for (let i = 0; i <= 3; i++) {
+    ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+  }
+  ok(!tab4.multiselected, "tab4 is not multiselected");
+
+  // Check mute state
+  ok(muted(tab0), "Tab0 is still muted");
+  ok(!muted(tab1) && !activeMediaBlocked(tab1), "Tab1 is not muted");
+  ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+  ok(!muted(tab3) && !activeMediaBlocked(tab3), "Tab3 is not muted and not activemedia-blocked");
+  ok(!muted(tab4) && !activeMediaBlocked(tab4), "Tab4 is not muted and not activemedia-blocked");
+
+  // Mute tab1 which is mutliselected, thus other multiselected tabs should be affected too
+  // in the following way:
+  //  a) muted tabs (tab0) will remain muted.
+  //  b) unmuted tabs (tab1, tab3) will become muted.
+  //  b) media-blocked tabs (tab2) will remain media-blocked.
+  // However tab4 (unmuted) which is not multiselected should not be affected.
+  let tab1MuteAudioBtn = document.getAnonymousElementByAttribute(tab1, "anonid", "soundplaying-icon");
+  await test_mute_tab(tab1, tab1MuteAudioBtn, true);
+
+  // Check mute state
+  ok(muted(tab0), "Tab0 is still muted");
+  ok(muted(tab1), "Tab1 is muted");
+  ok(activeMediaBlocked(tab2), "Tab2 is still media-blocked");
+  ok(muted(tab3), "Tab3 is now muted");
+  ok(!muted(tab4) && !activeMediaBlocked(tab4), "Tab4 is not muted and not activemedia-blocked");
+
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
+
+add_task(async function unmuteTabs_usingButton() {
+  let tab0 = await addMediaTab();
+  let tab1 = await addMediaTab();
+  let tab2 = await addMediaTab();
+  let tab3 = await addMediaTab();
+  let tab4 = await addMediaTab();
+
+  let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+  await BrowserTestUtils.switchTab(gBrowser, tab0);
+  await play(tab0);
+  await play(tab1);
+  await play(tab2);
+
+  // Mute tab3 and tab4
+  tab3.toggleMuteAudio();
+  tab4.toggleMuteAudio();
+
+  // Multiselecting tab0, tab1, tab2 and tab3
+  await triggerClickOn(tab3, { shiftKey: true });
+
+  // Check mutliselection
+  for (let i = 0; i <= 3; i++) {
+    ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+  }
+  ok(!tab4.multiselected, "tab4 is not multiselected");
+
+  // Check tabs mute state
+  ok(!muted(tab0) && !activeMediaBlocked(tab0), "Tab0 is not muted and not media-blocked");
+  ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+  ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+  ok(muted(tab3), "Tab3 is muted");
+  ok(muted(tab4), "Tab4 is muted");
+  is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+  // unmute tab0 which is mutliselected, thus other multiselected tabs should be affected too
+  // in the following way:
+  //  a) muted tabs (tab3) will become unmuted.
+  //  b) unmuted tabs (tab0) will remain unmuted.
+  //  b) media-blocked tabs (tab1, tab2) will get playing. (media not blocked anymore)
+  // However tab4 (muted) which is not multiselected should not be affected.
+  let tab3MuteAudioBtn = document.getAnonymousElementByAttribute(tab3, "anonid", "soundplaying-icon");
+  await test_mute_tab(tab3, tab3MuteAudioBtn, false);
+
+  ok(!muted(tab0) && !activeMediaBlocked(tab0), "Tab0 is unmuted and not media-blocked");
+  ok(!muted(tab1) && !activeMediaBlocked(tab1), "Tab1 is unmuted and not media-blocked");
+  ok(!muted(tab2) && !activeMediaBlocked(tab2), "Tab2 is unmuted and not media-blocked");
+  ok(!muted(tab3) && !activeMediaBlocked(tab3), "Tab3 is unmuted and not media-blocked");
+  ok(muted(tab4), "Tab4 is muted");
+  is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
+
+add_task(async function playTabs_usingButton() {
+  let tab0 = await addMediaTab();
+  let tab1 = await addMediaTab();
+  let tab2 = await addMediaTab();
+  let tab3 = await addMediaTab();
+  let tab4 = await addMediaTab();
+
+  let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+  await BrowserTestUtils.switchTab(gBrowser, tab0);
+  await play(tab0);
+  await play(tab1);
+  await play(tab2);
+
+  // Multiselecting tab0, tab1, tab2 and tab3.
+  await triggerClickOn(tab3, { shiftKey: true });
+
+  // Mute tab0 and tab4
+  tab0.toggleMuteAudio();
+  tab4.toggleMuteAudio();
+
+  // Check mutliselection
+  for (let i = 0; i <= 3; i++) {
+    ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+  }
+  ok(!tab4.multiselected, "tab4 is not multiselected");
+
+  // Check mute state
+  ok(muted(tab0), "Tab0 is muted");
+  ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+  ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+  ok(!muted(tab3) && !activeMediaBlocked(tab3), "Tab3 is not muted and not activemedia-blocked");
+  ok(muted(tab4), "Tab4 is muted");
+  is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+  // play tab2 which is mutliselected, thus other multiselected tabs should be affected too
+  // in the following way:
+  //  a) muted tabs (tab0) will become unmuted.
+  //  b) unmuted tabs (tab3) will remain unmuted.
+  //  b) media-blocked tabs (tab1, tab2) will get playing. (media not blocked anymore)
+  // However tab4 (muted) which is not multiselected should not be affected.
+  let tab2MuteAudioBtn = document.getAnonymousElementByAttribute(tab2, "anonid", "soundplaying-icon");
+  await test_mute_tab(tab2, tab2MuteAudioBtn, false);
+
+  ok(!muted(tab0) && !activeMediaBlocked(tab0), "Tab0 is unmuted and not activemedia-blocked");
+  ok(!muted(tab1) && !activeMediaBlocked(tab1), "Tab1 is unmuted and not activemedia-blocked");
+  ok(!muted(tab2) && !activeMediaBlocked(tab2), "Tab2 is unmuted and not activemedia-blocked");
+  ok(!muted(tab3) && !activeMediaBlocked(tab3), "Tab3 is unmuted and not activemedia-blocked");
+  ok(muted(tab4), "Tab4 is muted");
+  is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
+
+add_task(async function checkTabContextMenu() {
+  let tab0 = await addMediaTab();
+  let tab1 = await addMediaTab();
+  let tab2 = await addMediaTab();
+  let tab3 = await addMediaTab();
+  let tabs = [tab0, tab1, tab2, tab3];
+
+  let menuItemToggleMuteTab = document.getElementById("context_toggleMuteTab");
+  let menuItemToggleMuteSelectedTabs = document.getElementById("context_toggleMuteSelectedTabs");
+
+  await play(tab0);
+  tab0.toggleMuteAudio();
+  await play(tab1);
+  tab2.toggleMuteAudio();
+
+  // Mutliselect tab0, tab1, tab2.
+  await triggerClickOn(tab0, { ctrlKey: true });
+  await triggerClickOn(tab1, { ctrlKey: true });
+  await triggerClickOn(tab2, { ctrlKey: true });
+
+  // Check mutliselected tabs
+  for (let i = 0; i <= 2; i++) {
+    ok(tabs[i].multiselected, "Tab" + i + " is multi-selected");
+  }
+  ok(!tab3.multiselected, "Tab3 is not multiselected");
+
+  // Check mute state for tabs
+  ok(!muted(tab0) && !activeMediaBlocked(tab0), "Tab0 is not muted and not activemedia-blocked");
+  ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+  ok(muted(tab2), "Tab2 is muted");
+  ok(!muted(tab3, "Tab3 is not muted"));
+
+  let labels = ["Mute Tabs", "Play Tabs", "Unmute Tabs"];
+
+  for (let i = 0; i <= 2; i++) {
+    updateTabContextMenu(tabs[i]);
+    ok(menuItemToggleMuteTab.hidden,
+      "toggleMuteAudio menu for one tab is hidden - contextTab" + i);
+    ok(!menuItemToggleMuteSelectedTabs.hidden,
+      "toggleMuteAudio menu for selected tab is not hidden - contextTab" + i);
+    is(menuItemToggleMuteSelectedTabs.label, labels[i], labels[i] + " should be shown");
+  }
+
+  updateTabContextMenu(tab3);
+  ok(!menuItemToggleMuteTab.hidden, "toggleMuteAudio menu for one tab is not hidden");
+  ok(menuItemToggleMuteSelectedTabs.hidden, "toggleMuteAudio menu for selected tab is hidden");
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -1373,17 +1373,16 @@ CustomizeMode.prototype = {
       tbb.setAttribute("aria-checked", isActive);
       tbb.setAttribute("role", "menuitemradio");
       if (isActive) {
         tbb.setAttribute("active", "true");
       }
       tbb.addEventListener("focus", previewTheme);
       tbb.addEventListener("mouseover", previewTheme);
       tbb.addEventListener("blur", resetPreview);
-      tbb.addEventListener("mouseout", resetPreview);
 
       return tbb;
     }
 
     let themes = [];
     let lwts = LightweightThemeManager.usedThemes;
     let currentLwt = LightweightThemeManager.currentTheme;
 
@@ -1414,16 +1413,33 @@ CustomizeMode.prototype = {
           button.theme.userDisabled = false;
         else
           LightweightThemeManager.currentTheme = button.theme;
         onThemeSelected(panel);
       });
       panel.insertBefore(button, recommendedLabel);
     }
 
+    function panelMouseOut(e) {
+      // mouseout events bubble, so we get mouseout events for the buttons
+      // in the panel. Here, we only care when the mouse actually leaves
+      // the panel. For some reason event.target might not be the panel
+      // even when the mouse *is* leaving the panel, so we check
+      // explicitOriginalTarget instead.
+      if (e.explicitOriginalTarget == panel) {
+        resetPreview();
+      }
+    }
+
+    panel.addEventListener("mouseout", panelMouseOut);
+    panel.addEventListener("popuphidden", () => {
+      panel.removeEventListener("mouseout", panelMouseOut);
+      resetPreview();
+    }, {once: true});
+
     let lwthemePrefs = Services.prefs.getBranch("lightweightThemes.");
     let recommendedThemes = lwthemePrefs.getStringPref("recommendedThemes");
     recommendedThemes = JSON.parse(recommendedThemes);
     let sb = Services.strings.createBundle("chrome://browser/locale/lightweightThemes.properties");
     for (let theme of recommendedThemes) {
       try {
         theme.name = sb.GetStringFromName("lightweightThemes." + theme.id + ".name");
         theme.description = sb.GetStringFromName("lightweightThemes." + theme.id + ".description");
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -122,20 +122,20 @@ var Policies = {
     }
   },
 
   "Cookies": {
     onBeforeUIStartup(manager, param) {
       addAllowDenyPermissions("cookie", param.Allow, param.Block);
 
       if (param.Block) {
-        const hosts = param.Block.map(uri => uri.host).sort().join("\n");
+        const hosts = param.Block.map(url => url.hostname).sort().join("\n");
         runOncePerModification("clearCookiesForBlockedHosts", hosts, () => {
           for (let blocked of param.Block) {
-            Services.cookies.removeCookiesWithOriginAttributes("{}", blocked.host);
+            Services.cookies.removeCookiesWithOriginAttributes("{}", blocked.hostname);
           }
         });
       }
 
       if (param.Default !== undefined ||
           param.AcceptThirdParty !== undefined ||
           param.Locked) {
         const ACCEPT_COOKIES = 0;
@@ -503,19 +503,19 @@ var Policies = {
     }
   },
 
   "Homepage": {
     onBeforeUIStartup(manager, param) {
       // |homepages| will be a string containing a pipe-separated ('|') list of
       // URLs because that is what the "Home page" section of about:preferences
       // (and therefore what the pref |browser.startup.homepage|) accepts.
-      let homepages = param.URL.spec;
+      let homepages = param.URL.href;
       if (param.Additional && param.Additional.length > 0) {
-        homepages += "|" + param.Additional.map(url => url.spec).join("|");
+        homepages += "|" + param.Additional.map(url => url.href).join("|");
       }
       if (param.Locked) {
         setAndLockPref("browser.startup.homepage", homepages);
         setAndLockPref("browser.startup.page", 1);
         setAndLockPref("pref.browser.homepage.disable_button.current_page", true);
         setAndLockPref("pref.browser.homepage.disable_button.bookmark_page", true);
         setAndLockPref("pref.browser.homepage.disable_button.restore_default", true);
       } else {
@@ -558,24 +558,24 @@ var Policies = {
   "OfferToSaveLogins": {
     onBeforeUIStartup(manager, param) {
       setAndLockPref("signon.rememberSignons", param);
     }
   },
 
   "OverrideFirstRunPage": {
     onProfileAfterChange(manager, param) {
-      let url = param ? param.spec : "";
+      let url = param ? param.href : "";
       setAndLockPref("startup.homepage_welcome_url", url);
     }
   },
 
   "OverridePostUpdatePage": {
     onProfileAfterChange(manager, param) {
-      let url = param ? param.spec : "";
+      let url = param ? param.href : "";
       setAndLockPref("startup.homepage_override_url", url);
       // The pref startup.homepage_override_url is only used
       // as a fallback when the update.xml file hasn't provided
       // a specific post-update URL.
       manager.disallowFeature("postUpdateCustomPage");
     }
   },
 
@@ -852,28 +852,28 @@ function setDefaultPermission(policyName
  *        The list of URLs to be set as DENY_ACTION for the chosen permission.
  */
 function addAllowDenyPermissions(permissionName, allowList, blockList) {
   allowList = allowList || [];
   blockList = blockList || [];
 
   for (let origin of allowList) {
     try {
-      Services.perms.add(origin,
+      Services.perms.add(Services.io.newURI(origin.href),
                          permissionName,
                          Ci.nsIPermissionManager.ALLOW_ACTION,
                          Ci.nsIPermissionManager.EXPIRE_POLICY);
     } catch (ex) {
       log.error(`Added by default for ${permissionName} permission in the permission
-      manager - ${origin.spec}`);
+      manager - ${origin.href}`);
     }
   }
 
   for (let origin of blockList) {
-    Services.perms.add(origin,
+    Services.perms.add(Services.io.newURI(origin.href),
                        permissionName,
                        Ci.nsIPermissionManager.DENY_ACTION,
                        Ci.nsIPermissionManager.EXPIRE_POLICY);
   }
 }
 
 /**
  * runOnce
--- a/browser/components/enterprisepolicies/helpers/BookmarksPolicies.jsm
+++ b/browser/components/enterprisepolicies/helpers/BookmarksPolicies.jsm
@@ -3,43 +3,43 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /*
  * A Bookmark object received through the policy engine will be an
  * object with the following properties:
  *
- * - URL (nsIURI)
+ * - URL (URL)
  *   (required) The URL for this bookmark
  *
  * - Title (string)
  *   (required) The title for this bookmark
  *
  * - Placement (string)
  *   (optional) Either "toolbar" or "menu". If missing or invalid,
  *   "toolbar" will be used
  *
  * - Folder (string)
  *   (optional) The name of the folder to put this bookmark into.
  *   If present, a folder with this name will be created in the
  *   chosen placement above, and the bookmark will be created there.
  *   If missing, the bookmark will be created directly into the
  *   chosen placement.
  *
- * - Favicon (nsIURI)
+ * - Favicon (URL)
  *   (optional) An http:, https: or data: URL with the favicon.
  *   If possible, we recommend against using this property, in order
  *   to keep the json file small.
  *   If a favicon is not provided through the policy, it will be loaded
  *   naturally after the user first visits the bookmark.
  *
  *
  * Note: The Policy Engine automatically converts the strings given to
- * the URL and favicon properties into a nsIURI object.
+ * the URL and favicon properties into a URL object.
  *
  * The schema for this object is defined in policies-schema.json.
  */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
@@ -104,17 +104,17 @@ async function calculateLists(specifiedB
   // --------- STEP 1 ---------
   // Build two Maps (one with the existing bookmarks, another with
   // the specified bookmarks), to make iteration quicker.
 
   // LIST A
   // MAP of url (string) -> bookmarks objects from the Policy Engine
   let specifiedBookmarksMap = new Map();
   for (let bookmark of specifiedBookmarks) {
-    specifiedBookmarksMap.set(bookmark.URL.spec, bookmark);
+    specifiedBookmarksMap.set(bookmark.URL.href, bookmark);
   }
 
   // LIST B
   // MAP of url (string) -> bookmarks objects from Places
   let existingBookmarksMap = new Map();
   await PlacesUtils.bookmarks.fetch(
     { guidPrefix: BookmarksPolicies.BOOKMARK_GUID_PREFIX },
     (bookmark) => existingBookmarksMap.set(bookmark.url.href, bookmark)
@@ -188,60 +188,60 @@ async function calculateLists(specifiedB
   };
 }
 
 async function insertBookmark(bookmark) {
   let parentGuid = await getParentGuid(bookmark.Placement,
                                        bookmark.Folder);
 
   await PlacesUtils.bookmarks.insert({
-    url: bookmark.URL,
+    url: Services.io.newURI(bookmark.URL.href),
     title: bookmark.Title,
     guid: generateGuidWithPrefix(BookmarksPolicies.BOOKMARK_GUID_PREFIX),
     parentGuid,
   });
 
   if (bookmark.Favicon) {
     await setFaviconForBookmark(bookmark).catch(
       () => log.error(`Error setting favicon for ${bookmark.Title}`));
   }
 }
 
 async function setFaviconForBookmark(bookmark) {
   let faviconURI;
   let nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({});
 
-  switch (bookmark.Favicon.scheme) {
-    case "data":
+  switch (bookmark.Favicon.protocol) {
+    case "data:":
       // data urls must first call replaceFaviconDataFromDataURL, using a
       // fake URL. Later, it's needed to call setAndFetchFaviconForPage
       // with the same URL.
-      faviconURI = Services.io.newURI("fake-favicon-uri:" + bookmark.URL.spec);
+      faviconURI = Services.io.newURI("fake-favicon-uri:" + bookmark.URL.href);
 
       PlacesUtils.favicons.replaceFaviconDataFromDataURL(
         faviconURI,
-        bookmark.Favicon.spec,
+        bookmark.Favicon.href,
         0, /* max expiration length */
         nullPrincipal
       );
       break;
 
-    case "http":
-    case "https":
-      faviconURI = bookmark.Favicon;
+    case "http:":
+    case "https:":
+      faviconURI = Services.io.newURI(bookmark.Favicon.href);
       break;
 
     default:
       log.error(`Bad URL given for favicon on bookmark "${bookmark.Title}"`);
       return Promise.resolve();
   }
 
   return new Promise(resolve => {
     PlacesUtils.favicons.setAndFetchFaviconForPage(
-      bookmark.URL,
+      Services.io.newURI(bookmark.URL.href),
       faviconURI,
       false, /* forceReload */
       PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
       resolve,
       nullPrincipal
     );
   });
 }
--- a/browser/components/enterprisepolicies/helpers/ProxyPolicies.jsm
+++ b/browser/components/enterprisepolicies/helpers/ProxyPolicies.jsm
@@ -35,17 +35,17 @@ var EXPORTED_SYMBOLS = [ "ProxyPolicies"
 
 var ProxyPolicies = {
   configureProxySettings(param, setPref) {
     if (param.Mode) {
       setPref("network.proxy.type", PROXY_TYPES_MAP.get(param.Mode));
     }
 
     if (param.AutoConfigURL) {
-      setPref("network.proxy.autoconfig_url", param.AutoConfigURL.spec);
+      setPref("network.proxy.autoconfig_url", param.AutoConfigURL.href);
     }
 
     if (param.UseProxyForDNS !== undefined) {
       setPref("network.proxy.socks_remote_dns", param.UseProxyForDNS);
     }
 
     if (param.AutoLogin !== undefined) {
       setPref("signon.autologin.proxy", param.AutoLogin);
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js
@@ -10,17 +10,17 @@ ChromeUtils.import("resource://gre/modul
 const FAVICON_DATA = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwMDAsTBZbkNwAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAABNElEQVQ4y8WSsU0DURBE3yyWIaAJaqAAN4DPSL6AlIACKIEOyJEgRsIgOOkiInJqgAKowNg7BHdn7MOksNl+zZ//dvbDf5cAiklp22BdVtXdeTEpDYDB9m1VzU6OJuVp2NdEQCaI96fH2YHG4+mDduKYNMYINTcjcGbXzQVDEAphG0k48zUsajIbnAiMIXThpW8EICE0RAK4dvoKg9NIcTiQ589otyHOZLnwqK5nLwBFUZ4igc3iM0d1ff8CMC6mZ6Ihiaqq3gi1aUAnArD00SW1fq5OLBg0ymYmSZsR2/t4e/rGyCLW0sbp3oq+yTYqVgytQWui2FS7XYF7GFprY921T4CNQt8zr47dNzCkIX7y/jBtH+v+RGMQrc828W8pApnZbmEVQp/Ae7BlOy2ttib81/UFc+WRWEbjckIAAAAASUVORK5CYII=";
 
 const { BookmarksPolicies } = ChromeUtils.import("resource:///modules/policies/BookmarksPolicies.jsm", {});
 
 let CURRENT_POLICY;
 
 const BASE_POLICY = {
   "policies": {
-    "display_bookmarks_toolbar": true,
+    "DisplayBookmarksToolbar": true,
     "Bookmarks": [
       {
         "Title": "Bookmark 1",
         "URL": "https://bookmark1.example.com/",
         "Favicon": FAVICON_DATA
       },
       {
         "Title": "Bookmark 2",
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_proxy.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_proxy.js
@@ -11,19 +11,19 @@ add_task(async function test_proxy_modes
   let { ProxyPolicies, PROXY_TYPES_MAP } = ChromeUtils.import("resource:///modules/policies/ProxyPolicies.jsm", {});
 
   for (let [mode, expectedValue] of PROXY_TYPES_MAP) {
     ProxyPolicies.configureProxySettings({Mode: mode}, (_, value) => {
       is(value, expectedValue, "Correct proxy mode");
     });
   }
 
-  let autoconfigURL = Services.io.newURI("data:text/plain,test");
+  let autoconfigURL = new URL("data:text/plain,test");
   ProxyPolicies.configureProxySettings({AutoConfigURL: autoconfigURL}, (_, value) => {
-    is(value, autoconfigURL.spec, "AutoconfigURL correctly set");
+    is(value, autoconfigURL.href, "AutoconfigURL correctly set");
   });
 });
 
 add_task(async function test_proxy_boolean_settings() {
   // Tests that both false and true values are correctly set and locked
   await setupPolicyEngineWithJson({
     "policies": {
       "Proxy": {
--- a/browser/components/extensions/moz.build
+++ b/browser/components/extensions/moz.build
@@ -1,16 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files("**"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: Untriaged")
+    BUG_COMPONENT = ("WebExtensions", "Untriaged")
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXTRA_COMPONENTS += [
     'extensions-browser.manifest',
 ]
 
 EXTRA_JS_MODULES += [
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -113,18 +113,19 @@ export default class BasicCardForm exten
 
     this.cancelButton.textContent = this.dataset.cancelButtonLabel;
     this.backButton.textContent = this.dataset.backButtonLabel;
     this.saveButton.textContent = this.dataset.saveButtonLabel;
     this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
     this.addressAddLink.textContent = this.dataset.addressAddLinkLabel;
     this.addressEditLink.textContent = this.dataset.addressEditLinkLabel;
 
-    // The back button is temporarily hidden(See Bug 1462461).
-    this.backButton.hidden = !!page.onboardingWizard;
+    // The next line needs an onboarding check since we don't set previousId
+    // when navigating to add/edit directly from the summary page.
+    this.backButton.hidden = !page.previousId && page.onboardingWizard;
     this.cancelButton.hidden = !page.onboardingWizard;
 
     let record = {};
     let basicCards = paymentRequest.getBasicCards(state);
     let addresses = paymentRequest.getAddresses(state);
 
     this.genericErrorText.textContent = page.error;
 
@@ -205,21 +206,49 @@ export default class BasicCardForm exten
         if (evt.target == this.addressEditLink && selectedOption && selectedOption.value) {
           nextState["address-page"].title = this.dataset.billingAddressTitleEdit;
           nextState["address-page"].guid = selectedOption.value;
         }
         this.requestStore.setState(nextState);
         break;
       }
       case this.backButton: {
-        this.requestStore.setState({
+        let {
+          page,
+          request,
+          "address-page": addressPage,
+          "basic-card-page": basicCardPage,
+          selectedShippingAddress,
+        } = this.requestStore.getState();
+
+        let nextState = {
           page: {
-            id: "payment-summary",
+            id: page.previousId || "payment-summary",
+            onboardingWizard: page.onboardingWizard,
           },
-        });
+        };
+
+        let addressPageState;
+        if (page.onboardingWizard) {
+          if (request.paymentOptions.requestShipping) {
+            addressPageState = Object.assign({}, addressPage, {guid: selectedShippingAddress});
+          } else {
+            addressPageState =
+              Object.assign({}, addressPage, {guid: basicCardPage.billingAddressGUID});
+          }
+
+          let basicCardPageState = Object.assign({}, basicCardPage, {preserveFieldValues: true});
+
+          Object.assign(nextState, {
+            "address-page": addressPageState,
+            "basic-card-page": basicCardPageState,
+          });
+        }
+
+        this.requestStore.setState(nextState);
         break;
       }
       case this.saveButton: {
         this.saveRecord();
         break;
       }
       default: {
         throw new Error("Unexpected click target");
--- a/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
+++ b/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
@@ -401,8 +401,103 @@ add_task(async function test_on_boarding
 
     info("Closing the payment dialog");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
 
     cleanupFormAutofillStorage();
   });
 });
+
+add_task(async function test_back_button_on_basic_card_page_during_onboarding() {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    cleanupFormAutofillStorage();
+
+    info("Opening the payment dialog");
+    let {win, frame} =
+        await setupPaymentDialog(browser, {
+          methodData: [PTU.MethodData.basicCard],
+          details: PTU.Details.total60USD,
+          merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+        });
+
+    await spawnPaymentDialogTask(frame, async function() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page";
+      }, "Address page is shown first if there are saved addresses during on boarding");
+
+      info("Checking if the address page has been rendered");
+      let addressSaveButton = content.document.querySelector("address-form .save-button");
+      ok(content.isVisible(addressSaveButton), "Address save button is rendered");
+
+      for (let [key, val] of Object.entries(PTU.Addresses.TimBL2)) {
+        let field = content.document.getElementById(key);
+        if (!field) {
+          ok(false, `${key} field not found`);
+        }
+        field.value = val;
+        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+      }
+      content.document.querySelector("address-form .save-button").click();
+
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page";
+      }, "Basic card page is shown next");
+
+      info("Checking if basic card page is rendered");
+      let basicCardBackButton = content.document.querySelector("basic-card-form .back-button");
+      ok(content.isVisible(basicCardBackButton), "Back button is visible on the basic card page");
+
+      info("Partially fill basic card form");
+      let field = content.document.getElementById("cc-number");
+      field.value = PTU.BasicCards.JohnDoe["cc-number"];
+
+      info("Clicking on the back button to edit address saved in the previous step");
+      basicCardBackButton.click();
+
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page" &&
+               state["address-page"].guid == state["basic-card-page"].billingAddressGUID;
+      }, "Address page is shown again");
+
+      info("Checking if the address page has been rendered");
+      addressSaveButton = content.document.querySelector("address-form .save-button");
+      ok(content.isVisible(addressSaveButton), "Address save button is rendered");
+
+      info("Checking if the address saved in the last step is correctly loaded in the form");
+      field = content.document.getElementById("given-name");
+      ok(field.value, PTU.Addresses.TimBL2["given-name"],
+         "Given name field value is correctly loaded");
+
+      info("Editing the address and saving again");
+      field.value = "John";
+      addressSaveButton.click();
+
+      info("Checking if the address was correctly edited");
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page" &&
+               // eslint-disable-next-line max-len
+               state.savedAddresses[state["basic-card-page"].billingAddressGUID]["given-name"] == "John";
+      }, "Address was correctly edited and saved");
+
+      // eslint-disable-next-line max-len
+      info("Checking if the basic card form is now rendered and if the field values from before are preserved");
+      let basicCardCancelButton = content.document.querySelector("basic-card-form .cancel-button");
+      ok(content.isVisible(basicCardCancelButton),
+         "Cancel button is visible on the basic card page");
+      field = content.document.getElementById("cc-number");
+      ok(field.value, PTU.BasicCards.JohnDoe["cc-number"], "Values in the form are preserved");
+    });
+
+    info("Closing the payment dialog");
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+
+    cleanupFormAutofillStorage();
+  });
+});
--- a/browser/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -951,26 +951,26 @@ var gEditItemOverlay = {
       this._paneInfo.title = aNewTitle;
       this._initTextField(this._namePicker, aNewTitle);
     } else if (this._paneInfo.visibleRows.has("folderRow")) {
       // If the title of a folder which is listed within the folders
       // menulist has been changed, we need to update the label of its
       // representing element.
       let menupopup = this._folderMenuList.menupopup;
       for (let menuitem of menupopup.childNodes) {
-        if ("folderId" in menuitem && menuitem.folderId == aItemId) {
+        if ("folderGuid" in menuitem && menuitem.folderGuid == aGuid) {
           menuitem.label = aNewTitle;
           break;
         }
       }
     }
     // We need to also update title of recent folders.
     if (this._recentFolders) {
       for (let folder of this._recentFolders) {
-        if (folder.folderId == aItemId) {
+        if (folder.folderGuid == aGuid) {
           folder.title = aNewTitle;
           break;
         }
       }
     }
   },
 
   // nsINavBookmarkObserver
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -34,16 +34,17 @@ skip-if = (verify && debug)
 [browser_bookmarkProperties_addLivemark.js]
 skip-if = (verify && debug && (os == 'linux' || os == 'win'))
 [browser_bookmarkProperties_bookmarkAllTabs.js]
 skip-if = (verify && debug && (os == 'win' || os == 'mac'))
 [browser_bookmarkProperties_cancel.js]
 [browser_bookmarkProperties_editFolder.js]
 [browser_bookmarkProperties_editTagContainer.js]
 [browser_bookmarkProperties_no_user_actions.js]
+[browser_bookmarkProperties_newFolder.js]
 [browser_bookmarkProperties_readOnlyRoot.js]
 [browser_bookmarkProperties_remember_folders.js]
 [browser_bookmarksProperties.js]
 skip-if = (verify && debug && (os == 'win' || os == 'mac'))
 [browser_check_correct_controllers.js]
 [browser_click_bookmarks_on_toolbar.js]
 [browser_controller_onDrop_sidebar.js]
 [browser_controller_onDrop_tagFolder.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_URL = "about:robots";
+const bookmarkPanel = document.getElementById("editBookmarkPanel");
+let folders;
+
+add_task(async function setup() {
+  await PlacesUtils.bookmarks.eraseEverything();
+
+  bookmarkPanel.setAttribute("animate", false);
+
+  let oldTimeout = StarUI._autoCloseTimeout;
+  // Make the timeout something big, so it doesn't iteract badly with tests.
+  StarUI._autoCloseTimeout = 6000000;
+
+  let tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    opening: TEST_URL,
+    waitForStateStop: true
+  });
+
+  registerCleanupFunction(async () => {
+    StarUI._autoCloseTimeout = oldTimeout;
+    BrowserTestUtils.removeTab(tab);
+    bookmarkPanel.removeAttribute("animate");
+    await PlacesUtils.bookmarks.eraseEverything();
+  });
+});
+
+
+add_task(async function test_newFolder() {
+  await clickBookmarkStar();
+
+  // Open folder selector.
+  document.getElementById("editBMPanel_foldersExpander").click();
+
+  let folderTree = document.getElementById("editBMPanel_folderTree");
+
+  // Create new folder.
+  let newFolderButton = document.getElementById("editBMPanel_newFolderButton");
+  newFolderButton.click();
+
+  let newFolderGuid;
+  let newFolderObserver = PlacesTestUtils.waitForNotification("onItemAdded",
+    (id, parentId, index, type, uri, title, dateAdded, guid) => {
+      newFolderGuid = guid;
+      return type == PlacesUtils.bookmarks.TYPE_FOLDER;
+  });
+
+  let menulist = document.getElementById("editBMPanel_folderMenuList");
+
+  await newFolderObserver;
+
+  // Wait for the folder to be created and for editing to start.
+  await BrowserTestUtils.waitForCondition(() => folderTree.hasAttribute("editing"),
+     "Should be in edit mode for the new folder");
+
+  Assert.equal(menulist.selectedItem.label, newFolderButton.label,
+    "Should have the new folder selected by default");
+
+  let renameObserver = PlacesTestUtils.waitForNotification("onItemChanged",
+    (id, property, isAnno, aNewValue) => property == "title" && aNewValue == "f");
+
+  // Enter a new name.
+  EventUtils.synthesizeKey("f", {}, window);
+  EventUtils.synthesizeKey("VK_RETURN", {}, window);
+
+  await renameObserver;
+
+  await BrowserTestUtils.waitForCondition(() => !folderTree.hasAttribute("editing"),
+     "Should have stopped editing the new folder");
+
+  Assert.equal(menulist.selectedItem.label, "f",
+    "Should have the new folder title");
+
+  let bookmark = await PlacesUtils.bookmarks.fetch({url: TEST_URL});
+
+  Assert.equal(bookmark.parentGuid, newFolderGuid,
+    "The bookmark should be parented by the new folder");
+
+  await hideBookmarksPanel();
+});
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
@@ -6,29 +6,16 @@
 
 /**
  * Tests that multiple tags can be added to a bookmark using the star-shaped button, the library and the sidebar.
  */
 
 const bookmarkPanel = document.getElementById("editBookmarkPanel");
 let folders;
 
-async function clickBookmarkStar() {
-  let shownPromise = promisePopupShown(bookmarkPanel);
-  BookmarkingUI.star.click();
-  await shownPromise;
-}
-
-async function hideBookmarksPanel() {
-  let hiddenPromise = promisePopupHidden(bookmarkPanel);
-  // Confirm and close the dialog.
-  document.getElementById("editBookmarkPanelDoneButton").click();
-  await hiddenPromise;
-}
-
 async function openPopupAndSelectFolder(guid, newBookmark = false) {
   await clickBookmarkStar();
 
   let notificationPromise;
   if (!newBookmark) {
     notificationPromise = PlacesTestUtils.waitForNotification("onItemMoved",
       (id, oldParentId, oldIndex, newParentId, newIndex, type,
        itemGuid, oldParentGuid, newParentGuid) => guid == newParentGuid);
--- a/browser/components/places/tests/browser/head.js
+++ b/browser/components/places/tests/browser/head.js
@@ -399,8 +399,23 @@ function getToolbarNodeForItemGuid(itemG
   let children = document.getElementById("PlacesToolbarItems").childNodes;
   for (let child of children) {
     if (itemGuid === child._placesNode.bookmarkGuid) {
       return child;
     }
   }
   return null;
 }
+
+// Open the bookmarks Star UI by clicking the star button on the address bar.
+async function clickBookmarkStar() {
+  let shownPromise = promisePopupShown(document.getElementById("editBookmarkPanel"));
+  BookmarkingUI.star.click();
+  await shownPromise;
+}
+
+// Close the bookmarks Star UI by clicking the "Done" button.
+async function hideBookmarksPanel() {
+  let hiddenPromise = promisePopupHidden(document.getElementById("editBookmarkPanel"));
+  // Confirm and close the dialog.
+  document.getElementById("editBookmarkPanelDoneButton").click();
+  await hiddenPromise;
+}
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -882,16 +882,23 @@ userContextOpenLink.label = Open Link in
 
 muteTab.label = Mute Tab
 muteTab.accesskey = M
 unmuteTab.label = Unmute Tab
 unmuteTab.accesskey = m
 playTab.label = Play Tab
 playTab.accesskey = l
 
+muteSelectedTabs.label = Mute Tabs
+muteSelectedTabs.accesskey = u
+unmuteSelectedTabs.label = Unmute Tabs
+unmuteSelectedTabs.accesskey = b
+playTabs.label = Play Tabs
+playTabs.accesskey = y
+
 # LOCALIZATION NOTE (certErrorDetails*.label): These are text strings that
 # appear in the about:certerror page, so that the user can copy and send them to
 # the server administrators for troubleshooting.
 certErrorDetailsHSTS.label = HTTP Strict Transport Security: %S
 certErrorDetailsKeyPinning.label = HTTP Public Key Pinning: %S
 certErrorDetailsCertChain.label = Certificate chain:
 
 # LOCALIZATION NOTE (pendingCrashReports2.label): Semi-colon list of plural forms
--- a/browser/modules/BlockedSiteContent.jsm
+++ b/browser/modules/BlockedSiteContent.jsm
@@ -14,17 +14,17 @@ ChromeUtils.defineModuleGetter(this, "Sa
 function getSiteBlockedErrorDetails(docShell) {
   let blockedInfo = {};
   if (docShell.failedChannel) {
     let classifiedChannel = docShell.failedChannel.
                             QueryInterface(Ci.nsIClassifiedChannel);
     if (classifiedChannel) {
       let httpChannel = docShell.failedChannel.QueryInterface(Ci.nsIHttpChannel);
 
-      let reportUri = httpChannel.URI.clone();
+      let reportUri = httpChannel.URI;
 
       // Remove the query to avoid leaking sensitive data
       if (reportUri instanceof Ci.nsIURL) {
         reportUri = reportUri.mutate()
                              .setQuery("")
                              .finalize();
       }
 
--- a/browser/modules/ContextMenu.jsm
+++ b/browser/modules/ContextMenu.jsm
@@ -800,17 +800,17 @@ class ContextMenu {
     if (context.target.nodeType != context.target.ELEMENT_NODE) {
       return;
     }
 
     // See if the user clicked on an image. This check mirrors
     // nsDocumentViewer::GetInImage. Make sure to update both if this is
     // changed.
     if (context.target instanceof Ci.nsIImageLoadingContent &&
-        context.target.currentRequestFinalURI) {
+        (context.target.currentRequestFinalURI || context.target.currentURI)) {
       context.onImage = true;
 
       context.imageInfo = {
         currentSrc: context.target.currentSrc,
         width: context.target.width,
         height: context.target.height,
         imageText: context.target.title || context.target.alt
       };
@@ -824,18 +824,19 @@ class ContextMenu {
       if (request &&
           (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
           !(request.imageStatus & request.STATUS_ERROR)) {
         context.onCompletedImage = true;
       }
 
       // The actual URL the image was loaded from (after redirects) is the
       // currentRequestFinalURI.  We should use that as the URL for purposes of
-      // deciding on the filename.
-      context.mediaURL = context.target.currentRequestFinalURI.spec;
+      // deciding on the filename, if it is present. It might not be present
+      // if images are blocked.
+      context.mediaURL = (context.target.currentRequestFinalURI || context.target.currentURI).spec;
 
       const descURL = context.target.getAttribute("longdesc");
 
       if (descURL) {
         context.imageDescURL = this._makeURLAbsolute(context.target.ownerDocument.body.baseURI,
                                                     descURL);
       }
     } else if (context.target instanceof this.content.HTMLCanvasElement) {
--- a/browser/modules/Sanitizer.jsm
+++ b/browser/modules/Sanitizer.jsm
@@ -201,16 +201,17 @@ var Sanitizer = {
       case Sanitizer.TIMESPAN_4HOURS :
         startDate = endDate - 14400000000; // 4*60*60*1000000
         break;
       case Sanitizer.TIMESPAN_TODAY :
         var d = new Date(); // Start with today
         d.setHours(0); // zero us back to midnight...
         d.setMinutes(0);
         d.setSeconds(0);
+        d.setMilliseconds(0);
         startDate = d.valueOf() * 1000; // convert to epoch usec
         break;
       case Sanitizer.TIMESPAN_24HOURS :
         startDate = endDate - 86400000000; // 24*60*60*1000000
         break;
       default:
         throw "Invalid time span for clear private data: " + ts;
     }
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -60,17 +60,17 @@ with Files("ContentCrashHandlers.jsm"):
 
 with Files("ContentSearch.jsm"):
     BUG_COMPONENT = ("Firefox", "Search")
 
 with Files("ContentWebRTC.jsm"):
     BUG_COMPONENT = ("Firefox", "Device Permissions")
 
 with Files("ExtensionsUI.jsm"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: General")
+    BUG_COMPONENT = ("WebExtensions", "General")
 
 with Files("LaterRun.jsm"):
     BUG_COMPONENT = ("Firefox", "Tours")
 
 with Files("LightWeightThemeWebInstallListener.jsm"):
     BUG_COMPONENT = ("Firefox", "Theme")
 
 with Files("OpenInTabsUtils.jsm"):
@@ -102,17 +102,17 @@ with Files("SiteDataManager.jsm"):
 
 with Files("SitePermissions.jsm"):
     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
 
 with Files("TabsPopup.jsm"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browser")
 
 with Files("ThemeVariableMap.jsm"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: Themes")
+    BUG_COMPONENT = ("WebExtensions", "Themes")
 
 with Files("TransientPrefs.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 with Files("Windows8WindowFrameColor.jsm"):
     BUG_COMPONENT = ("Firefox", "Theme")
 
 with Files("WindowsJumpLists.jsm"):
--- a/browser/themes/shared/places/editBookmarkPanel.inc.css
+++ b/browser/themes/shared/places/editBookmarkPanel.inc.css
@@ -3,16 +3,38 @@
  * 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/. */
 %endif
 
 #editBookmarkPanel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
+html|img#editBookmarkPanelFavicon[src] {
+  box-sizing: content-box;
+  width: 32px;
+  height: 32px;
+  padding: 5px;
+  background-color: #F9F9FA;
+  box-shadow: inset 0 0 0 1px rgba(0,0,0,.1);
+  border-radius: 6px;
+  position: relative;
+  margin-top: 10px;
+  margin-inline-start: 10px;
+  margin-bottom: -52px; /* margin-top + paddings + height */
+}
+
+#editBookmarkPanelImage {
+  border-bottom: 1px solid var(--panel-separator-color);
+  height: 150px;
+  background-image: -moz-element(#editBookmarkPanelImageCanvas);
+  background-repeat: no-repeat;
+  background-size: cover;
+}
+
 #editBookmarkPanelRows {
   padding: var(--arrowpanel-padding);
 }
 
 /* Implements editBookmarkPanel resizing on folderTree un-collapse. */
 #editBMPanel_folderTree {
   min-width: 27em;
 }
--- a/browser/tools/mozscreenshots/moz.build
+++ b/browser/tools/mozscreenshots/moz.build
@@ -6,17 +6,17 @@
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "General")
 
 with Files("controlCenter/**"):
     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
 
 with Files("devtools/**"):
-    BUG_COMPONENT = ("Firefox", "Developer Tools")
+    BUG_COMPONENT = ("DevTools", "General")
 
 with Files("permissionPrompts/**"):
     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
 
 with Files("preferences/**"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 BROWSER_CHROME_MANIFESTS += [
--- a/caps/NullPrincipalURI.cpp
+++ b/caps/NullPrincipalURI.cpp
@@ -276,17 +276,17 @@ NullPrincipalURI::GetUserPass(nsACString
 }
 
 nsresult
 NullPrincipalURI::SetUserPass(const nsACString& aUserPass)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
-NS_IMETHODIMP
+nsresult
 NullPrincipalURI::Clone(nsIURI** _newURI)
 {
   nsCOMPtr<nsIURI> uri = new NullPrincipalURI(*this);
   uri.forget(_newURI);
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/caps/NullPrincipalURI.h
+++ b/caps/NullPrincipalURI.h
@@ -51,16 +51,17 @@ private:
   NullPrincipalURI(const NullPrincipalURI& aOther);
 
   ~NullPrincipalURI() {}
 
   nsresult Init();
 
   nsAutoCStringN<NSID_LENGTH> mPath;
 
+  nsresult Clone(nsIURI** aURI);
   nsresult SetSpecInternal(const nsACString &input);
   nsresult SetScheme(const nsACString &input);
   nsresult SetUserPass(const nsACString &input);
   nsresult SetUsername(const nsACString &input);
   nsresult SetPassword(const nsACString &input);
   nsresult SetHostPort(const nsACString &aValue);
   nsresult SetHost(const nsACString &input);
   nsresult SetPort(int32_t port);
--- a/devtools/.eslintrc.js
+++ b/devtools/.eslintrc.js
@@ -38,17 +38,16 @@ module.exports = {
     ],
     "rules": {
       "consistent-return": "off",
     }
   }, {
     "files": [
       "client/framework/**",
       "client/scratchpad/**",
-      "client/shared/AppCacheUtils.jsm",
       "client/webide/**",
     ],
     "rules": {
       "max-nested-callbacks": "off",
     }
   }, {
     "files": [
       "client/framework/**",
--- a/devtools/client/aboutdebugging/moz.build
+++ b/devtools/client/aboutdebugging/moz.build
@@ -9,10 +9,10 @@ DIRS += [
     'modules',
 ]
 
 BROWSER_CHROME_MANIFESTS += [
     'test/browser.ini'
 ]
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: about:debugging')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'about:debugging')
--- a/devtools/client/accessibility/moz.build
+++ b/devtools/client/accessibility/moz.build
@@ -18,9 +18,9 @@ DevToolsModules(
     'accessibility-view.js',
     'accessibility.css',
     'constants.js',
     'picker.js',
     'provider.js',
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Accessibility')
+    BUG_COMPONENT = ('DevTools', 'Accessibility Tools')
--- a/devtools/client/canvasdebugger/moz.build
+++ b/devtools/client/canvasdebugger/moz.build
@@ -4,10 +4,10 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'panel.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Canvas Debugger')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'Canvas Debugger')
--- a/devtools/client/debugger/moz.build
+++ b/devtools/client/debugger/moz.build
@@ -14,10 +14,10 @@ DevToolsModules(
 )
 
 BROWSER_CHROME_MANIFESTS += [
   'new/test/mochitest/browser.ini',
   'test/mochitest/browser.ini',
   'test/mochitest/browser2.ini'
 ]
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Debugger')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'Debugger')
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -735,24 +735,24 @@ AddonDebugger.prototype = {
       }
 
       groupmap.get(group).get(label).url = source.url.split(" -> ").pop();
     }
 
     return groups;
   }),
 
-  _onMessage: function (event) {
-    if (typeof(event.data) !== "string") {
+  _onMessage: function(event) {
+    if (!event.data) {
       return;
     }
-    let json = JSON.parse(event.data);
-    switch (json.name) {
+    const msg = event.data;
+    switch (msg.name) {
       case "toolbox-title":
-        this.title = json.data.value;
+        this.title = msg.data.value;
         break;
     }
   }
 };
 
 function initChromeDebugger(aOnClose) {
   info("Initializing a chrome debugger process.");
 
--- a/devtools/client/dom/moz.build
+++ b/devtools/client/dom/moz.build
@@ -8,10 +8,10 @@ BROWSER_CHROME_MANIFESTS += ['test/brows
 DIRS += [
     'content',
 ]
 
 DevToolsModules(
     'dom-panel.js',
 )
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: DOM')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'DOM')
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -28,9 +28,9 @@ DevToolsModules(
     'toolbox-hosts.js',
     'toolbox-options.js',
     'toolbox-tabs-order-manager.js',
     'toolbox.js',
     'ToolboxProcess.jsm',
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Framework')
+    BUG_COMPONENT = ('DevTools', 'Framework')
--- a/devtools/client/framework/test/browser_toolbox_custom_host.js
+++ b/devtools/client/framework/test/browser_toolbox_custom_host.js
@@ -1,57 +1,45 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URL = "data:text/html,test custom host";
 
-function test() {
-  const {Toolbox} = require("devtools/client/framework/toolbox");
-
-  let toolbox, iframe, target;
-
-  window.addEventListener("message", onMessage);
+add_task(async function() {
+  const { Toolbox } = require("devtools/client/framework/toolbox");
 
-  iframe = document.createElement("iframe");
-  document.documentElement.appendChild(iframe);
-
-  addTab(TEST_URL).then(function(tab) {
-    target = TargetFactory.forTab(tab);
-    const options = {customIframe: iframe};
-    gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options)
-             .then(testCustomHost, console.error)
-             .catch(console.error);
+  const messageReceived = new Promise(resolve => {
+    function onMessage(event) {
+      if (!event.data) {
+        return;
+      }
+      const msg = event.data;
+      info(`onMessage: ${JSON.stringify(msg)}`);
+      switch (msg.name) {
+        case "toolbox-close":
+          ok(true, "Got the `toolbox-close` message");
+          window.removeEventListener("message", onMessage);
+          resolve();
+          break;
+      }
+    }
+    window.addEventListener("message", onMessage);
   });
 
-  function onMessage(event) {
-    if (typeof (event.data) !== "string") {
-      return;
-    }
-    info("onMessage: " + event.data);
-    const json = JSON.parse(event.data);
-    if (json.name == "toolbox-close") {
-      ok("Got the `toolbox-close` message");
-      window.removeEventListener("message", onMessage);
-      cleanup();
-    }
-  }
+  let iframe = document.createElement("iframe");
+  document.documentElement.appendChild(iframe);
+
+  const tab = await addTab(TEST_URL);
+  let target = TargetFactory.forTab(tab);
+  const options = { customIframe: iframe };
+  let toolbox = await gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options);
 
-  function testCustomHost(t) {
-    toolbox = t;
-    is(toolbox.win.top, window, "Toolbox is included in browser.xul");
-    is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe");
-    executeSoon(() => gBrowser.removeCurrentTab());
-  }
-
-  function cleanup() {
-    iframe.remove();
+  is(toolbox.win.top, window, "Toolbox is included in browser.xul");
+  is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe");
 
-    // Even if we received "toolbox-close", the toolbox may still be destroying
-    // toolbox.destroy() returns a singleton promise that ensures
-    // everything is cleaned up before proceeding.
-    toolbox.destroy().then(() => {
-      toolbox = iframe = target = null;
-      finish();
-    });
-  }
-}
+  iframe.remove();
+  await toolbox.destroy();
+  await messageReceived;
+
+  iframe = toolbox = target = null;
+});
--- a/devtools/client/framework/toolbox-host-manager.js
+++ b/devtools/client/framework/toolbox-host-manager.js
@@ -99,30 +99,31 @@ ToolboxHostManager.prototype = {
         break;
     }
   },
 
   onMessage(event) {
     if (!event.data) {
       return;
     }
+    const msg = event.data;
     // Toolbox document is still chrome and disallow identifying message
     // origin via event.source as it is null. So use a custom id.
-    if (event.data.frameId != this.frameId) {
+    if (msg.frameId != this.frameId) {
       return;
     }
-    switch (event.data.name) {
+    switch (msg.name) {
       case "switch-host":
-        this.switchHost(event.data.hostType);
+        this.switchHost(msg.hostType);
         break;
       case "raise-host":
         this.host.raise();
         break;
       case "set-host-title":
-        this.host.setTitle(event.data.title);
+        this.host.setTitle(msg.title);
         break;
     }
   },
 
   postMessage(data) {
     const window = this.host.frame.contentWindow;
     window.postMessage(data, "*");
   },
--- a/devtools/client/framework/toolbox-hosts.js
+++ b/devtools/client/framework/toolbox-hosts.js
@@ -330,21 +330,22 @@ CustomHost.prototype = {
 
   _sendMessageToTopWindow: function(msg, data) {
     // It's up to the custom frame owner (parent window) to honor
     // "close" or "raise" instructions.
     const topWindow = this.frame.ownerDocument.defaultView;
     if (!topWindow) {
       return;
     }
-    const json = {name: "toolbox-" + msg, uid: this.uid};
-    if (data) {
-      json.data = data;
-    }
-    topWindow.postMessage(JSON.stringify(json), "*");
+    const message = {
+      name: "toolbox-" + msg,
+      uid: this.uid,
+      data,
+    };
+    topWindow.postMessage(message, "*");
   },
 
   /**
    * Create a new xul window to contain the toolbox.
    */
   create: function() {
     return promise.resolve(this.frame);
   },
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -224,28 +224,27 @@ function onUnload() {
   window.removeEventListener("unload", onUnload);
   window.removeEventListener("message", onMessage);
   const cmdClose = document.getElementById("toolbox-cmd-close");
   cmdClose.removeEventListener("command", onCloseCommand);
   gToolbox.destroy();
 }
 
 function onMessage(event) {
-  try {
-    const json = JSON.parse(event.data);
-    switch (json.name) {
-      case "toolbox-raise":
-        raise();
-        break;
-      case "toolbox-title":
-        setTitle(json.data.value);
-        break;
-    }
-  } catch (e) {
-    console.error(e);
+  if (!event.data) {
+    return;
+  }
+  const msg = event.data;
+  switch (msg.name) {
+    case "toolbox-raise":
+      raise();
+      break;
+    case "toolbox-title":
+      setTitle(msg.data.value);
+      break;
   }
 }
 
 window.addEventListener("message", onMessage);
 
 function raise() {
   window.focus();
 }
--- a/devtools/client/inspector/animation-old/moz.build
+++ b/devtools/client/inspector/animation-old/moz.build
@@ -12,9 +12,9 @@ DIRS += [
 ]
 
 DevToolsModules(
     'graph-helper.js',
     'utils.js'
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Animation Inspector')
+    BUG_COMPONENT = ('DevTools', 'Animation Inspector')
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -321,20 +321,17 @@ class AnimationInspector {
         animation.off("changed", this.onAnimationStateChanged);
       }
     }
 
     // Update existing other animations as well since the currentTime would be proceeded
     // sice the scrubber position is related the currentTime.
     // Also, don't update the state of removed animations since React components
     // may refer to the same instance still.
-    await this.updateAnimations(animations);
-
-    // Get rid of animations that were removed during async updateAnimations().
-    animations = animations.filter(animation => !!animation.state.type);
+    animations = await this.updateAnimations(animations);
 
     this.updateState(animations.concat(addedAnimations));
   }
 
   onElementPickerStarted() {
     this.inspector.store.dispatch(updateElementPickerEnabled(true));
   }
 
@@ -398,65 +395,65 @@ class AnimationInspector {
   async setAnimationsCurrentTime(currentTime, shouldRefresh) {
     this.stopAnimationsCurrentTimeTimer();
     this.onAnimationsCurrentTimeUpdated(currentTime);
 
     if (!shouldRefresh && this.isCurrentTimeSet) {
       return;
     }
 
-    const { animations } = this.state;
+    let animations = this.state.animations;
     this.isCurrentTimeSet = true;
 
     try {
       await this.doSetCurrentTimes(currentTime);
-      await this.updateAnimations(animations);
+      animations = await this.updateAnimations(animations);
     } catch (e) {
       // Expected if we've already been destroyed or other node have been selected
       // in the meantime.
       console.error(e);
       return;
     }
 
     this.isCurrentTimeSet = false;
 
     if (shouldRefresh) {
-      this.updateState([...animations]);
+      this.updateState(animations);
     }
   }
 
   async setAnimationsPlaybackRate(playbackRate) {
-    const animations = this.state.animations;
+    let animations = this.state.animations;
     // "changed" event on each animation will fire respectively when the playback
     // rate changed. Since for each occurrence of event, change of UI is urged.
     // To avoid this, disable the listeners once in order to not capture the event.
     this.setAnimationStateChangedListenerEnabled(false);
 
     try {
       await this.animationsFront.setPlaybackRates(animations, playbackRate);
-      await this.updateAnimations(animations);
+      animations = await this.updateAnimations(animations);
     } catch (e) {
       // Expected if we've already been destroyed or other node have been selected
       // in the meantime.
       console.error(e);
       return;
     } finally {
       this.setAnimationStateChangedListenerEnabled(true);
     }
 
-    await this.updateState([...animations]);
+    await this.updateState(animations);
   }
 
   async setAnimationsPlayState(doPlay) {
     if (typeof this.hasPausePlaySome === "undefined") {
       this.hasPausePlaySome =
         await this.inspector.target.actorHasMethod("animations", "pauseSome");
     }
 
-    const { animations, timeScale } = this.state;
+    let { animations, timeScale } = this.state;
 
     try {
       if (doPlay && animations.every(animation =>
                       timeScale.getEndTime(animation) <= animation.state.currentTime)) {
         await this.doSetCurrentTimes(0);
       }
 
       // If the server does not support pauseSome/playSome function, (which happens
@@ -469,25 +466,25 @@ class AnimationInspector {
           await this.animationsFront.pauseSome(animations);
         }
       } else if (doPlay) {
         await this.animationsFront.playAll();
       } else {
         await this.animationsFront.pauseAll();
       }
 
-      await this.updateAnimations(animations);
+      animations = await this.updateAnimations(animations);
     } catch (e) {
       // Expected if we've already been destroyed or other node have been selected
       // in the meantime.
       console.error(e);
       return;
     }
 
-    await this.updateState([...animations]);
+    await this.updateState(animations);
   }
 
   /**
    * Enable/disable the animation state change listener.
    * If set true, observe "changed" event on current animations.
    * Otherwise, quit observing the "changed" event.
    *
    * @param {Bool} isEnabled
@@ -624,41 +621,39 @@ class AnimationInspector {
       ? await this.animationsFront.getAnimationPlayersForNode(selection.nodeFront)
       : [];
     this.updateState(animations);
     this.setAnimationStateChangedListenerEnabled(true);
 
     done();
   }
 
-  updateAnimations(animations) {
-    if (!animations.length) {
-      return Promise.resolve();
-    }
+  async updateAnimations(animations) {
+    let error = null;
 
-    return new Promise((resolve, reject) => {
-      let count = 0;
-      let error = null;
-
-      for (const animation of animations) {
+    const promises = animations.map(animation => {
+      return new Promise(resolve => {
         animation.refreshState().catch(e => {
           error = e;
         }).finally(() => {
-          count += 1;
+          resolve();
+        });
+      });
+    });
+    await Promise.all(promises);
 
-          if (count === animations.length) {
-            if (error) {
-              reject(error);
-            } else {
-              resolve();
-            }
-          }
-        });
-      }
-    });
+    if (error) {
+      throw new Error(error);
+    }
+
+    // Even when removal animation on inspected document, updateAnimations
+    // might be called before onAnimationsMutation due to the async timing.
+    // Return the animations as result of updateAnimations after getting rid of
+    // the animations since they should not display.
+    return animations.filter(anim => !!anim.state.type);
   }
 
   updateState(animations) {
     // Animation inspector already destroyed
     if (!this.inspector) {
       return;
     }
 
--- a/devtools/client/inspector/animation/components/AnimationItem.js
+++ b/devtools/client/inspector/animation/components/AnimationItem.js
@@ -29,35 +29,37 @@ class AnimationItem extends Component {
       timeScale: PropTypes.object.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
-      isSelected: false,
+      isSelected: this.isSelected(props),
     };
   }
 
   componentWillReceiveProps(nextProps) {
-    const { animation } = this.props;
-
     this.setState({
-      isSelected: nextProps.selectedAnimation &&
-                  animation.actorID === nextProps.selectedAnimation.actorID
+      isSelected: this.isSelected(nextProps),
     });
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return this.state.isSelected !== nextState.isSelected ||
            this.props.animation !== nextProps.animation ||
            this.props.timeScale !== nextProps.timeScale;
   }
 
+  isSelected(props) {
+    return props.selectedAnimation &&
+           props.animation.actorID === props.selectedAnimation.actorID;
+  }
+
   render() {
     const {
       animation,
       emitEventForTest,
       getAnimatedPropertyMap,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
--- a/devtools/client/inspector/animation/reducers/animations.js
+++ b/devtools/client/inspector/animation/reducers/animations.js
@@ -43,18 +43,22 @@ const reducers = {
       animations,
       detailVisibility,
       selectedAnimation,
       timeScale: new TimeScale(animations),
     });
   },
 
   [UPDATE_DETAIL_VISIBILITY](state, { detailVisibility }) {
+    const selectedAnimation =
+      detailVisibility ? state.selectedAnimation : null;
+
     return Object.assign({}, state, {
-      detailVisibility
+      detailVisibility,
+      selectedAnimation,
     });
   },
 
   [UPDATE_ELEMENT_PICKER_ENABLED](state, { elementPickerEnabled }) {
     return Object.assign({}, state, {
       elementPickerEnabled
     });
   },
--- a/devtools/client/inspector/animation/test/browser.ini
+++ b/devtools/client/inspector/animation/test/browser.ini
@@ -21,16 +21,18 @@ support-files =
 
 [browser_animation_animated-property-list.js]
 [browser_animation_animated-property-list_unchanged-items.js]
 [browser_animation_animated-property-name.js]
 [browser_animation_animation-detail_close-button.js]
 [browser_animation_animation-detail_title.js]
 [browser_animation_animation-detail_visibility.js]
 [browser_animation_animation-list.js]
+[browser_animation_animation-list_one-animation-select.js]
+[browser_animation_animation-list_select.js]
 [browser_animation_animation-target.js]
 [browser_animation_animation-target_highlight.js]
 [browser_animation_animation-target_select.js]
 [browser_animation_animation-timeline-tick.js]
 [browser_animation_css-transition-with-playstate-idle.js]
 [browser_animation_current-time-label.js]
 [browser_animation_current-time-scrubber.js]
 [browser_animation_current-time-scrubber_each-different-creation-time-animations.js]
--- a/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js
@@ -33,22 +33,10 @@ add_task(async function() {
                                            panel, `.${ targetClass }`);
     ok(panel.querySelector(".animated-property-list"),
       `The animated-property-list should be in the DOM at ${ targetClass }`);
     const itemEls =
       panel.querySelectorAll(".animated-property-list .animated-property-item");
     is(itemEls.length, expectedNumber,
        `The number of animated-property-list should be ${ expectedNumber } ` +
        `at ${ targetClass }`);
-
-    if (itemEls.length < 2) {
-      continue;
-    }
-
-    info("Checking the background color for " +
-         `the animated property item at ${ targetClass }`);
-    const evenColor = panel.ownerGlobal.getComputedStyle(itemEls[0]).backgroundColor;
-    const oddColor = panel.ownerGlobal.getComputedStyle(itemEls[1]).backgroundColor;
-    isnot(evenColor, oddColor,
-          "Background color of an even animated property item " +
-          "should be different from odd");
   }
 });
--- a/devtools/client/inspector/animation/test/browser_animation_animation-list.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list.js
@@ -12,22 +12,13 @@ add_task(async function() {
 
   info("Checking animation list and items existence");
   ok(panel.querySelector(".animation-list"),
     "The animation-list is in the DOM");
   is(panel.querySelectorAll(".animation-list .animation-item").length,
      animationInspector.state.animations.length,
      "The number of animations displayed matches the number of animations");
 
-  info("Checking the background color for the animation list items");
-  const animationItemEls = panel.querySelectorAll(".animation-list .animation-item");
-  const evenColor =
-    panel.ownerGlobal.getComputedStyle(animationItemEls[0]).backgroundColor;
-  const oddColor =
-    panel.ownerGlobal.getComputedStyle(animationItemEls[1]).backgroundColor;
-  isnot(evenColor, oddColor,
-    "Background color of an even animation should be different from odd");
-
   info("Checking list and items existence after select a element which has an animation");
   await selectNodeAndWaitForAnimations(".animated", inspector);
   is(panel.querySelectorAll(".animation-list .animation-item").length, 1,
     "The number of animations displayed should be 1 for .animated element");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation item has been selected from first time
+// if count of the animations is one.
+
+add_task(async function() {
+  await addTab(URL_ROOT + "doc_simple_animation.html");
+  await removeAnimatedElementsExcept([".animated"]);
+  const { panel } = await openAnimationInspector();
+
+  info("Checking whether an item element has been selected");
+  is(panel.querySelector(".animation-item").classList.contains("selected"), true,
+     "The animation item should have 'selected' class");
+
+  info("Checking whether the element will be unselected after closing the detail pane");
+  clickOnDetailCloseButton(panel);
+  is(panel.querySelector(".animation-item").classList.contains("selected"), false,
+     "The animation item should not have 'selected' class");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list_select.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation items in the list were selectable.
+
+add_task(async function() {
+  await addTab(URL_ROOT + "doc_simple_animation.html");
+  await removeAnimatedElementsExcept([".animated", ".long"]);
+  const { animationInspector, panel } = await openAnimationInspector();
+
+  info("Checking whether 1st element will be selected");
+  await clickOnAnimation(animationInspector, panel, 0);
+  assertSelection(panel, [true, false]);
+
+  info("Checking whether 2nd element will be selected");
+  await clickOnAnimation(animationInspector, panel, 1);
+  assertSelection(panel, [false, true]);
+
+  info("Checking whether all elements will be unselected after closing the detail pane");
+  clickOnDetailCloseButton(panel);
+  assertSelection(panel, [false, false]);
+});
+
+function assertSelection(panel, expectedResult) {
+  panel.querySelectorAll(".animation-item").forEach((item, index) => {
+    const shouldSelected = expectedResult[index];
+    is(item.classList.contains("selected"), shouldSelected,
+       `Animation item[${ index }] should ` +
+       `${ shouldSelected ? "" : "not" } have 'selected' class`);
+  });
+}
--- a/devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js
@@ -5,17 +5,17 @@
 
 // Test that when animations displayed in the timeline are running on the
 // compositor, they get a special icon and information in the tooltip.
 
 add_task(async function() {
   await addTab(URL_ROOT + "doc_simple_animation.html");
   await removeAnimatedElementsExcept(
     [".compositor-all", ".compositor-notall", ".no-compositor"]);
-  const { inspector, panel } = await openAnimationInspector();
+  const { animationInspector, inspector, panel } = await openAnimationInspector();
 
   info("Select a test node we know has an animation running on the compositor");
   await selectNodeAndWaitForAnimations(".compositor-all", inspector);
 
   const summaryGraphEl = panel.querySelector(".animation-summary-graph");
   ok(summaryGraphEl.classList.contains("compositor"),
     "The element has the compositor css class");
   ok(hasTooltip(summaryGraphEl,
@@ -37,14 +37,41 @@ add_task(async function() {
   info("Select a node we know has animation on the compositor and not on the compositor");
   await selectNodeAndWaitForAnimations(".compositor-notall", inspector);
 
   ok(summaryGraphEl.classList.contains("compositor"),
     "The element has the compositor css class");
   ok(hasTooltip(summaryGraphEl,
                 ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")),
      "The element has the right tooltip content");
+
+  info("Check compositor sign after pausing");
+  await clickOnPauseResumeButton(animationInspector, panel);
+  ok(!summaryGraphEl.classList.contains("compositor"),
+    "The element should not have the compositor css class after pausing");
+
+  info("Check compositor sign after resuming");
+  await clickOnPauseResumeButton(animationInspector, panel);
+  ok(summaryGraphEl.classList.contains("compositor"),
+    "The element should have the compositor css class after resuming");
+
+  info("Check compositor sign after rewind");
+  await clickOnRewindButton(animationInspector, panel);
+  ok(!summaryGraphEl.classList.contains("compositor"),
+    "The element should not have the compositor css class after rewinding");
+  await clickOnPauseResumeButton(animationInspector, panel);
+  ok(summaryGraphEl.classList.contains("compositor"),
+    "The element should have the compositor css class after resuming");
+
+  info("Check compositor sign after setting the current time");
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+  ok(!summaryGraphEl.classList.contains("compositor"),
+    "The element should not have the compositor css class " +
+    "after setting the current time");
+  await clickOnPauseResumeButton(animationInspector, panel);
+  ok(summaryGraphEl.classList.contains("compositor"),
+    "The element should have the compositor css class after resuming");
 });
 
 function hasTooltip(summaryGraphEl, expected) {
   const tooltip = summaryGraphEl.getAttribute("title");
   return tooltip.includes(expected);
 }
--- a/devtools/client/inspector/fonts/actions/font-editor.js
+++ b/devtools/client/inspector/fonts/actions/font-editor.js
@@ -38,20 +38,21 @@ module.exports = {
   updateAxis(axis, value) {
     return {
       type: UPDATE_AXIS_VALUE,
       axis,
       value,
     };
   },
 
-  updateFontEditor(fonts, properties = {}) {
+  updateFontEditor(fonts, families = { used: [], notUsed: [] }, properties = {}) {
     return {
       type: UPDATE_EDITOR_STATE,
       fonts,
+      families,
       properties,
     };
   },
 
   updateFontProperty(property, value) {
     return {
       type: UPDATE_PROPERTY_VALUE,
       property,
--- a/devtools/client/inspector/fonts/components/FontEditor.js
+++ b/devtools/client/inspector/fonts/components/FontEditor.js
@@ -74,53 +74,77 @@ class FontEditor extends PureComponent {
         step: this.getAxisStep(axis.minValue, axis.maxValue),
         label: axis.name,
         name: axis.tag,
         onChange: this.props.onPropertyChange,
         unit: null
       });
     });
   }
+  /**
+   * Render font family, font name, and metadata for all fonts used on selected node.
+   *
+   * @param {Array} fonts
+   *        Fonts used on selected node.
+   * @param {Function} onToggleFontHighlight
+   *        Callback to trigger in-context highlighting of text that uses a font.
+   * @return {DOMNode}
+   */
+  renderFontFamily(fonts, onToggleFontHighlight) {
+    if (!fonts.length) {
+      return null;
+    }
 
-  renderFontFamily(font, onToggleFontHighlight) {
+    const fontList = dom.ul(
+      {
+        className: "fonts-list"
+      },
+      fonts.map(font => {
+        return dom.li(
+          {},
+          FontMeta({ font, onToggleFontHighlight })
+        );
+      })
+    );
+
     return dom.label(
       {
         className: "font-control font-control-family",
       },
       dom.span(
         {
           className: "font-control-label",
         },
         getStr("fontinspector.fontFamilyLabel")
       ),
       dom.div(
         {
           className: "font-control-box",
         },
-        FontMeta({ font, onToggleFontHighlight })
+        fontList
       )
     );
   }
 
   renderFontSize(value) {
-    return FontSize({
+    return value && FontSize({
       onChange: this.props.onPropertyChange,
       value,
     });
   }
 
   renderFontStyle(value) {
-    return FontStyle({
+    return value && FontStyle({
       onChange: this.props.onPropertyChange,
       value,
     });
   }
 
   renderFontWeight(value) {
-    return FontWeight({
+    return value && FontWeight({
       onChange: this.props.onPropertyChange,
       value,
     });
   }
 
   /**
    * Get a dropdown which allows selecting between variation instances defined by a font.
    *
@@ -175,36 +199,50 @@ class FontEditor extends PureComponent {
           className: "font-control-label",
         },
         getStr("fontinspector.fontInstanceLabel")
       ),
       instanceSelect
     );
   }
 
+  renderWarning() {
+    return dom.div(
+      {
+        className: "devtools-sidepanel-no-result"
+      },
+      getStr("fontinspector.noFontsOnSelectedElement")
+    );
+  }
+
   render() {
     const { fontEditor, onToggleFontHighlight } = this.props;
     const { fonts, axes, instance, properties } = fontEditor;
-    const usedFonts = fonts.filter(font => font.used);
-    // If no used fonts were found, pick the first available font.
-    // Else, pick the first used font regardless of how many there are.
-    const font = usedFonts.length === 0 ? fonts[0] : usedFonts[0];
+    // Pick the first font to show editor controls regardless of how many fonts are used.
+    const font = fonts[0];
     const hasFontAxes = font && font.variationAxes;
-    const hasFontInstances = font && font.variationInstances.length > 0;
+    const hasFontInstances = font && font.variationInstances
+      && font.variationInstances.length > 0;
     const hasSlantOrItalicAxis = hasFontAxes && font.variationAxes.find(axis => {
       return axis.tag === "slnt" || axis.tag === "ital";
     });
     const hasWeightAxis = hasFontAxes && font.variationAxes.find(axis => {
       return axis.tag === "wght";
     });
+    // Check for falsy font-weight value (undefined or empty string).
+    const hasWeight = properties["font-weight"] != null;
 
     return dom.div(
-      {},
+      {
+        id: "font-editor"
+      },
+      // Render empty state message for nodes that don't have font properties.
+      !hasWeight && this.renderWarning(),
       // Always render UI for font family, format and font file URL.
-      this.renderFontFamily(font, onToggleFontHighlight),
+      this.renderFontFamily(fonts, onToggleFontHighlight),
       // Render UI for font variation instances if they are defined.
       hasFontInstances && this.renderInstances(font.variationInstances, instance),
       // Always render UI for font size.
       this.renderFontSize(properties["font-size"]),
       // Render UI for font weight if no "wght" registered axis is defined.
       !hasWeightAxis && this.renderFontWeight(properties["font-weight"]),
       // Render UI for font style if no "slnt" or "ital" registered axis is defined.
       !hasSlantOrItalicAxis && this.renderFontStyle(properties["font-style"]),
--- a/devtools/client/inspector/fonts/components/FontOverview.js
+++ b/devtools/client/inspector/fonts/components/FontOverview.js
@@ -2,23 +2,26 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Services = require("Services");
 
 const Accordion = createFactory(require("devtools/client/inspector/layout/components/Accordion"));
 const FontList = createFactory(require("./FontList"));
 
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
+const PREF_FONT_EDITOR = "devtools.inspector.fonteditor.enabled";
+
 class FontOverview extends PureComponent {
   static get propTypes() {
     return {
       fontData: PropTypes.shape(Types.fontData).isRequired,
       fontOptions: PropTypes.shape(Types.fontOptions).isRequired,
       onPreviewFonts: PropTypes.func.isRequired,
       onToggleFontHighlight: PropTypes.func.isRequired,
     };
@@ -27,16 +30,22 @@ class FontOverview extends PureComponent
   constructor(props) {
     super(props);
     this.onToggleFontHighlightGlobal = (font, show) => {
       this.props.onToggleFontHighlight(font, show, false);
     };
   }
 
   renderElementFonts() {
+    // Do not show element fonts if the font editor is enabled.
+    // It handles this differently. Rendering twice is not desired.
+    if (Services.prefs.getBoolPref(PREF_FONT_EDITOR)) {
+      return null;
+    }
+
     const {
       fontData,
       fontOptions,
       onPreviewFonts,
       onToggleFontHighlight,
     } = this.props;
     const { fonts } = fontData;
 
--- a/devtools/client/inspector/fonts/components/FontsApp.js
+++ b/devtools/client/inspector/fonts/components/FontsApp.js
@@ -33,24 +33,22 @@ class FontsApp extends PureComponent {
       fontEditor,
       fontOptions,
       onInstanceChange,
       onPreviewFonts,
       onPropertyChange,
       onToggleFontHighlight,
     } = this.props;
 
-    const hasFonts = fontEditor.fonts.length > 0;
-
     return dom.div(
       {
         className: "theme-sidebar inspector-tabpanel",
         id: "sidebar-panel-fontinspector"
       },
-      hasFonts && FontEditor({
+      FontEditor({
         fontEditor,
         onInstanceChange,
         onPropertyChange,
         onToggleFontHighlight,
       }),
       FontOverview({
         fontData,
         fontOptions,
--- a/devtools/client/inspector/fonts/fonts.js
+++ b/devtools/client/inspector/fonts/fonts.js
@@ -69,18 +69,18 @@ class FontInspector {
     this.writers = new Map();
 
     this.snapshotChanges = debounce(this.snapshotChanges, 100, this);
     this.syncChanges = debounce(this.syncChanges, 100, this);
     this.onInstanceChange = this.onInstanceChange.bind(this);
     this.onNewNode = this.onNewNode.bind(this);
     this.onPreviewFonts = this.onPreviewFonts.bind(this);
     this.onPropertyChange = this.onPropertyChange.bind(this);
+    this.onRuleUpdated = this.onRuleUpdated.bind(this);
     this.onToggleFontHighlight = this.onToggleFontHighlight.bind(this);
-    this.onRuleUpdated = this.onRuleUpdated.bind(this);
     this.onThemeChanged = this.onThemeChanged.bind(this);
     this.update = this.update.bind(this);
     this.updateFontVariationSettings = this.updateFontVariationSettings.bind(this);
 
     this.init();
   }
 
   init() {
@@ -103,17 +103,16 @@ class FontInspector {
     }, fontsApp);
 
     // Expose the provider to let inspector.js use it in setupSidebar.
     this.provider = provider;
 
     this.inspector.selection.on("new-node-front", this.onNewNode);
     // @see ToolSidebar.onSidebarTabSelected()
     this.inspector.sidebar.on("fontinspector-selected", this.onNewNode);
-    this.ruleView.on("property-value-updated", this.onRuleUpdated);
 
     // Listen for theme changes as the color of the previews depend on the theme
     gDevTools.on("theme-switched", this.onThemeChanged);
   }
 
   /**
    * Given all fonts on the page, and given the fonts used in given node, return all fonts
    * not from the page not used in this node.
@@ -148,28 +147,76 @@ class FontInspector {
     this.ruleView = null;
     this.selectedRule = null;
     this.store = null;
     this.writers.clear();
     this.writers = null;
   }
 
   /**
+   * Get a subset of fonts used on a node whose font family names are found in the
+   * node's CSS font-family property value. The fonts will be sorted in the order their
+   * family names are declared in CSS font-family.
+   *
+   * Fonts returned by this.getFontsForNode() contain, among others, these attributes:
+   * - CSSFamilyName: a string of the font's family name (ex: "Times");
+   * - CSSGeneric: a string of the generic font family (ex: "serif", "sans-serif") if
+   * the font was resolved from a generic font family keyword, like serif, instead of
+   * an explicit font famly, like "Times". If the font is resolved from an
+   * explicit font family, CSSGeneric is null.
+   *
+   * For example:
+   * font-family: "Avenir", serif;
+   *
+   * If fonts from both families are used, it will yield:
+   * { CSSFamilyName: "Avenir", CSSGeneric: null, ... },
+   * { CSSFamilyName: "Times", CSSGeneric: "serif", ... },
+   *
+   * @param {Array} fonts
+   *        Fonts used on a node got from a call to this.getFontsForNode().
+   * @param {Array} fontFamilies
+   *        Strings of font families from a node's CSS font-family property value.
+   * @return {Array}
+   *         Subset of `fonts` whose font family names appear in `fontFamilies`.
+   */
+  filterFontsUsed(fonts = [], fontFamilies = []) {
+    return fontFamilies.reduce((acc, family) => {
+      const match = fonts.find(font => {
+        const generic = typeof font.CSSGeneric === "string"
+          ? font.CSSGeneric.toLowerCase()
+          : font.CSSGeneric;
+
+        return generic === family.toLowerCase()
+          || font.CSSFamilyName.toLowerCase() === family.toLowerCase();
+      });
+
+      if (match) {
+        acc.push(match);
+      }
+
+      return acc;
+    }, []);
+  }
+
+  /**
    * Get all expected CSS font properties and values from the node's matching rules and
    * fallback to computed style.
    *
    * @return {Object}
    */
   getFontProperties() {
     const KEYWORD_VALUES = ["initial", "inherit", "unset", "none"];
     const properties = {};
 
-    // First, get all expected font properties from computed styles.
+    // First, get all expected font properties from computed styles, if available.
     for (const prop of FONT_PROPERTIES) {
-      properties[prop] = this.nodeComputedStyle[prop].value;
+      properties[prop] =
+        (this.nodeComputedStyle[prop] && this.nodeComputedStyle[prop].value)
+          ? this.nodeComputedStyle[prop].value
+          : "";
     }
 
     // Then, replace with enabled font properties found on any of the rules that apply.
     for (const rule of this.ruleView.rules) {
       for (const textProp of rule.textProps) {
         if (FONT_PROPERTIES.includes(textProp.name) &&
             !KEYWORD_VALUES.includes(textProp.value) &&
             !textProp.value.includes("calc(") &&
@@ -338,31 +385,61 @@ class FontInspector {
     } else {
       this.writers.set(name, this.updateFontVariationSettings);
     }
 
     return this.writers.get(name);
   }
 
   /**
+   * Given a list of font families, return an object that groups them into sets of used
+   * and not used if they match families of fonts from the given list of fonts used on a
+   * node.
+   *
+   * @See this.filterFontsUsed() for an explanation of CSSFamilyName and CSSGeneric.
+   *
+   * @param {Array} fontsUsed
+   *        Fonts used on a node.
+   * @param {Array} fontFamilies
+   *        Strings of font families
+   * @return {Object}
+   */
+  groupFontFamilies(fontsUsed = [], fontFamilies = []) {
+    const families = {};
+    // Font family names declared and used.
+    families.used = fontsUsed.map(font =>
+      font.CSSGeneric ? font.CSSGeneric : font.CSSFamilyName
+    );
+    const familiesUsedLowercase = families.used.map(family => family.toLowerCase());
+    // Font family names declared but not used.
+    families.notUsed = fontFamilies
+      .map(family => family.toLowerCase())
+      .filter(family => !familiesUsedLowercase.includes(family));
+
+    return families;
+  }
+
+  /**
    * Check if the font inspector panel is visible.
    *
    * @return {Boolean}
    */
   isPanelVisible() {
-    return this.inspector.sidebar &&
+    return this.inspector &&
+           this.inspector.sidebar &&
            this.inspector.sidebar.getCurrentTabID() === "fontinspector";
   }
   /**
    * Check if a selected node exists and fonts can apply to it.
    *
    * @return {Boolean}
    */
   isSelectedNodeValid() {
-    return this.inspector.selection.nodeFront &&
+    return this.inspector &&
+           this.inspector.selection.nodeFront &&
            this.inspector.selection.isConnected() &&
            this.inspector.selection.isElementNode();
   }
 
   /**
    * Sync the Rule view with the latest styles from the page. Called in a debounced way
    * (see constructor) after property changes are applied directly to the CSS style rule
    * on the page circumventing direct TextProperty.setValue() which triggers expensive DOM
@@ -563,54 +640,61 @@ class FontInspector {
 
     const options = {};
     if (this.pageStyle.supportsFontVariations) {
       options.includeVariations = true;
     }
 
     const node = this.inspector.selection.nodeFront;
     const fonts = await this.getFontsForNode(node, options);
-    if (!fonts.length) {
-      this.store.dispatch(resetFontEditor());
-      return;
-    }
 
     // Get computed styles for the selected node, but filter by CSS font properties.
     this.nodeComputedStyle = await this.pageStyle.getComputed(node, {
       filterProperties: FONT_PROPERTIES
     });
-    // Clear any references to writer methods because the node's
+
+    if (!this.nodeComputedStyle) {
+      this.store.dispatch(resetFontEditor());
+      return;
+    }
+
+    // Clear any references to writer methods and CSS declarations because the node's
     // styles may have changed since the last font editor refresh.
     this.writers.clear();
     // Select the node's inline style as the rule where to write property value changes.
     this.selectedRule =
       this.ruleView.rules.find(rule => rule.domRule.type === ELEMENT_STYLE);
-    const fontEditor = this.store.getState().fontEditor;
+
     const properties = this.getFontProperties();
-    // Names of fonts declared in font-family property without quotes and space trimmed.
-    const declaredFontNames =
-      properties["font-family"].split(",").map(font => font.replace(/\"+/g, "").trim());
-
-    // Mark available fonts as used if their names appears in the font-family declaration.
-    // TODO: sort used fonts in order of font-family declaration.
-    for (const font of fonts) {
-      font.used = declaredFontNames.includes(font.CSSFamilyName);
-    }
-
+    const familiesDeclared =
+      properties["font-family"].split(",")
+      .map(font => font.replace(/["']+/g, "").trim());
+    // Subset of fonts used on the node whose family names exist in CSS font-family.
+    let fontsUsed = this.filterFontsUsed(fonts, familiesDeclared);
+    // Object with font families groupped by used and not used.
+    const families = this.groupFontFamilies(fontsUsed, familiesDeclared);
     // Assign writer methods to each axis defined in font-variation-settings.
     const axes = parseFontVariationAxes(properties["font-variation-settings"]);
     Object.keys(axes).map(axis => {
       this.writers.set(axis, this.getWriterForAxis(axis));
     });
 
-    // Update the font editor state only if property values differ from the ones in store.
-    // This can happen when a user makes manual changes in the Rule view.
-    if (JSON.stringify(properties) !== JSON.stringify(fontEditor.properties)) {
-      this.store.dispatch(updateFontEditor(fonts, properties));
+    // Pick fonts from descendants if no declared fonts were used on this node.
+    if (!fontsUsed.length && fonts.length) {
+      const otherVarFonts = fonts.filter(font => {
+        return (font.variationAxes && font.variationAxes.length);
+      });
+
+      // Prefer picking variable fonts if any were found on descendants of this node.
+      // The FontEditor component will render UI for the first font in the list.
+      fontsUsed = otherVarFonts.length ? otherVarFonts : fonts;
     }
+
+    this.store.dispatch(updateFontEditor(fontsUsed, families, properties));
+    this.inspector.emit("fonteditor-updated");
   }
 
   /**
    * Capture the state of all variation axes. Allows the user to return to this state with
    * the "Custom" instance after they've selected a font-defined named variation instance.
    * This method is debounced. See constructor.
    */
   snapshotChanges() {
--- a/devtools/client/inspector/fonts/reducers/font-editor.js
+++ b/devtools/client/inspector/fonts/reducers/font-editor.js
@@ -16,17 +16,25 @@ const {
   UPDATE_PROPERTY_VALUE,
 } = require("../actions/index");
 
 const INITIAL_STATE = {
   // Variable font axes.
   axes: {},
   // Copy of the most recent axes values. Used to revert from a named instance.
   customInstanceValues: [],
-  // Fonts applicable to selected element.
+  // Font families declared on the selected element
+  families: {
+    // Names of font families used
+    used: [],
+    // Names of font families declared but not used
+    notUsed: []
+  },
+  // Fonts whose family names are declared in CSS font-family and used
+  // on the selected element.
   fonts: [],
   // Current selected font variation instance.
   instance: {
     name: getStr("fontinspector.customInstanceName"),
     values: [],
   },
   // CSS font properties defined on the selected rule.
   properties: {},
@@ -65,17 +73,17 @@ const reducers = {
   [UPDATE_CUSTOM_INSTANCE](state) {
     const newState = { ...state };
     newState.customInstanceValues = Object.keys(state.axes).map(axis => {
       return { axis: [axis], value: state.axes[axis] };
     });
     return newState;
   },
 
-  [UPDATE_EDITOR_STATE](state, { fonts, properties }) {
+  [UPDATE_EDITOR_STATE](state, { fonts, families, properties }) {
     const axes = parseFontVariationAxes(properties["font-variation-settings"]);
 
     // If not defined in font-variation-settings, setup "wght" axis with the value of
     // "font-weight" if it is numeric and not a keyword.
     const weight = properties["font-weight"];
     if (axes.wght === undefined && parseFloat(weight).toString() === weight.toString()) {
       axes.wght = weight;
     }
@@ -85,17 +93,17 @@ const reducers = {
     const stretch = properties["font-stretch"];
     // Match the number part from values like: 10%, 10.55%, 0.2%
     // If there's a match, the number is the second item in the match array.
     const match = stretch.trim().match(/^(\d+(.\d+)?)%$/);
     if (axes.wdth === undefined && match && match[1]) {
       axes.wdth = match[1];
     }
 
-    return { ...state, axes, fonts, properties };
+    return { ...state, axes, fonts, families, properties };
   },
 
   [UPDATE_PROPERTY_VALUE](state, { property, value }) {
     const newState = { ...state };
     newState.properties[property] = value;
     return newState;
   }
 
--- a/devtools/client/inspector/fonts/test/browser_fontinspector.html
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector.html
@@ -16,17 +16,18 @@
     src: url(ostrich-black.ttf);
   }
   @font-face {
     font-family: bar;
     font-weight: 800;
     src: url(ostrich-black.ttf);
   }
   body{
-    font-family:Arial;
+    /* Arial doesn't exist on Linux. Liberation Sans is the default sans-serif there. */
+    font-family:Arial, "Liberation Sans";
     font-size: 36px;
   }
   div {
     font-family:Arial;
     font-family:bar;
   }
   .normal-text {
     font-family: barnormal;
--- a/devtools/client/inspector/fonts/test/browser_fontinspector.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector.js
@@ -1,81 +1,55 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global getURL */
 "use strict";
 
 requestLongerTimeout(2);
 
 const TEST_URI = URL_ROOT + "browser_fontinspector.html";
-const FONTS = [{
-  name: "Ostrich Sans Medium",
-  remote: true,
-  url: URL_ROOT + "ostrich-regular.ttf",
-  cssName: "bar"
-}, {
-  name: "Ostrich Sans Black",
-  remote: true,
-  url: URL_ROOT + "ostrich-black.ttf",
-  cssName: "bar"
-}, {
-  name: "Ostrich Sans Black",
-  remote: true,
-  url: URL_ROOT + "ostrich-black.ttf",
-  cssName: "bar"
-}, {
-  name: "Ostrich Sans Medium",
-  remote: true,
-  url: URL_ROOT + "ostrich-regular.ttf",
-  cssName: "barnormal"
-}];
 
 add_task(async function() {
+  await pushPref("devtools.inspector.fonteditor.enabled", true);
   const { inspector, view } = await openFontInspectorForURL(TEST_URI);
   ok(!!view, "Font inspector document is alive.");
 
   const viewDoc = view.document;
 
   await testBodyFonts(inspector, viewDoc);
   await testDivFonts(inspector, viewDoc);
 });
 
 function isRemote(fontLi) {
   return fontLi.querySelector(".font-origin").classList.contains("remote");
 }
 
-function testBodyFonts(inspector, viewDoc) {
-  const lis = getUsedFontsEls(viewDoc);
-  is(lis.length, 5, "Found 5 fonts");
-
-  for (let i = 0; i < FONTS.length; i++) {
-    const li = lis[i];
-    const font = FONTS[i];
+async function testBodyFonts(inspector, viewDoc) {
+  await selectNode("body", inspector);
 
-    is(getName(li), font.name, `font ${i} right font name`);
-    is(isRemote(li), font.remote, `font ${i} remote value correct`);
-    is(li.querySelector(".font-origin").textContent, font.url, `font ${i} url correct`);
-  }
-
-  // test that the bold and regular fonts have different previews
-  const regSrc = lis[0].querySelector(".font-preview").src;
-  const boldSrc = lis[1].querySelector(".font-preview").src;
-  isnot(regSrc, boldSrc, "preview for bold font is different from regular");
-
+  const lis = getUsedFontsEls(viewDoc);
   // test system font
-  const localFontName = getName(lis[4]);
-
+  const localFontName = getName(lis[0]);
   // On Linux test machines, the Arial font doesn't exist.
   // The fallback is "Liberation Sans"
+  is(lis.length, 1, "Found 1 font on BODY");
   ok((localFontName == "Arial") || (localFontName == "Liberation Sans"),
      "local font right font name");
-  ok(!isRemote(lis[4]), "local font is local");
+  ok(!isRemote(lis[0]), "local font is local");
 }
 
 async function testDivFonts(inspector, viewDoc) {
-  const updated = inspector.once("fontinspector-updated");
   await selectNode("div", inspector);
-  await updated;
+
+  const font = {
+    name: "Ostrich Sans Medium",
+    remote: true,
+    url: URL_ROOT + "ostrich-regular.ttf",
+  };
 
   const lis = getUsedFontsEls(viewDoc);
+  const li = lis[0];
   is(lis.length, 1, "Found 1 font on DIV");
-  is(getName(lis[0]), "Ostrich Sans Medium", "The DIV font has the right name");
+  is(getName(li), font.name, "The DIV font has the right name");
+  is(isRemote(li), font.remote, `font remote value correct`);
+  is(getURL(li), font.url, `font url correct`);
 }
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_copy-URL.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_copy-URL.js
@@ -5,18 +5,20 @@
 "use strict";
 
 // Test that an icon appears next to web font URLs, and that clicking it copies the URL
 // to the clipboard thanks to it.
 
 const TEST_URI = URL_ROOT + "browser_fontinspector.html";
 
 add_task(async function() {
-  const { view } = await openFontInspectorForURL(TEST_URI);
+  await pushPref("devtools.inspector.fonteditor.enabled", true);
+  const { view, inspector } = await openFontInspectorForURL(TEST_URI);
   const viewDoc = view.document;
+  await selectNode("div", inspector);
 
   const fontEl = getUsedFontsEls(viewDoc)[0];
   const linkEl = fontEl.querySelector(".font-origin");
   const iconEl = linkEl.querySelector(".copy-icon");
 
   ok(iconEl, "The icon is displayed");
   is(iconEl.getAttribute("title"), "Copy URL", "This is the right icon");
 
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js
@@ -5,18 +5,21 @@
 
 // Test that previews change when the preview text changes. It doesn't check the
 // exact preview images because they are drawn on a canvas causing them to vary
 // between systems, platforms and software versions.
 
 const TEST_URI = URL_ROOT + "browser_fontinspector.html";
 
 add_task(async function() {
-  const {view} = await openFontInspectorForURL(TEST_URI);
+  await pushPref("devtools.inspector.fonteditor.enabled", true);
+  const { view, inspector } = await openFontInspectorForURL(TEST_URI);
   const viewDoc = view.document;
+  await selectNode("div", inspector);
+  await expandOtherFontsAccordion(viewDoc);
 
   const previews = viewDoc.querySelectorAll("#font-container .font-preview");
   const initialPreviews = [...previews].map(p => p.src);
 
   info("Typing 'Abc' to check that the reference previews are correct.");
   await updatePreviewText(view, "Abc");
   checkPreviewImages(viewDoc, initialPreviews, true);
 
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_expand-css-code.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_expand-css-code.js
@@ -4,47 +4,51 @@
 
 "use strict";
 
 // Test that the font-face css rule code is collapsed by default, and can be expanded.
 
 const TEST_URI = URL_ROOT + "browser_fontinspector.html";
 
 add_task(async function() {
-  const { view } = await openFontInspectorForURL(TEST_URI);
+  await pushPref("devtools.inspector.fonteditor.enabled", true);
+  const { view, inspector } = await openFontInspectorForURL(TEST_URI);
   const viewDoc = view.document;
+  await selectNode("div", inspector);
 
+  await expandOtherFontsAccordion(viewDoc);
   info("Checking that the css font-face rule is collapsed by default");
-  let fontEl = getUsedFontsEls(viewDoc)[0];
+  let fontEl = getOtherFontsEls(viewDoc)[0];
   let codeEl = fontEl.querySelector(".font-css-code");
   is(codeEl.textContent, "@font-face {}", "The font-face rule is collapsed");
 
   info("Expanding the rule by clicking on the expander icon");
   let onExpanded = BrowserTestUtils.waitForCondition(() => {
     return codeEl.textContent === `@font-face {
   font-family: bar;
-  src: url("bad/font/name.ttf"), url("ostrich-regular.ttf") format("truetype");
+  src: url("ostrich-black.ttf");
+  font-weight: bold;
 }`;
   }, "Waiting for the font-face rule 1");
 
   let expander = fontEl.querySelector(".font-css-code .theme-twisty");
   expander.click();
   await onExpanded;
 
   ok(true, "Font-face rule is now expanded");
 
   info("Expanding another rule by clicking on the [...] icon instead");
-  fontEl = getUsedFontsEls(viewDoc)[1];
+  fontEl = getOtherFontsEls(viewDoc)[1];
   codeEl = fontEl.querySelector(".font-css-code");
 
   onExpanded = BrowserTestUtils.waitForCondition(() => {
     return codeEl.textContent === `@font-face {
   font-family: bar;
   src: url("ostrich-black.ttf");
-  font-weight: bold;
+  font-weight: 800;
 }`;
   }, "Waiting for the font-face rule 2");
 
   expander = fontEl.querySelector(".font-css-code .font-css-code-expander");
   expander.click();
   await onExpanded;
 
   ok(true, "Font-face rule is now expanded too");
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_other-fonts.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_other-fonts.js
@@ -6,16 +6,17 @@
 // Check that the font inspector has a section for "other fonts" and that section contains
 // the fonts *not* used in the currently selected element.
 // Check that it's collapsed by default, but can be expanded. That it does not contain
 // the fonts listed in the previous section.
 
 const TEST_URI = URL_ROOT + "browser_fontinspector.html";
 
 add_task(async function() {
+  await pushPref("devtools.inspector.fonteditor.enabled", true);
   const { inspector, view } = await openFontInspectorForURL(TEST_URI);
   const viewDoc = view.document;
 
   const otherFontsAccordion = viewDoc.querySelector("#font-container .accordion");
   ok(otherFontsAccordion, "There's an accordion in the panel");
   is(otherFontsAccordion.textContent, "Other fonts in page", "It has the right title");
 
   await expandOtherFontsAccordion(viewDoc);
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_reveal-in-page.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_reveal-in-page.js
@@ -3,16 +3,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that fonts usage can be revealed in the page using the FontsHighlighter.
 
 const TEST_URI = URL_ROOT + "browser_fontinspector.html";
 
 add_task(async function() {
+  await pushPref("devtools.inspector.fonteditor.enabled", true);
   // Enable the feature, since it's off by default for now.
   await pushPref("devtools.inspector.fonthighlighter.enabled", true);
 
   // Make sure the toolbox is tall enough to accomodate all fonts, otherwise mouseover
   // events simulation will fail.
   await pushPref("devtools.toolbox.footer.height", 500);
 
   const { tab, view } = await openFontInspectorForURL(TEST_URI);
@@ -35,17 +36,17 @@ add_task(async function() {
     let onEvents = waitForNSelectionEvents(tab, expectedSelectionChangeEvents[i]);
     EventUtils.synthesizeMouse(nameEl, 2, 2, {type: "mouseover"}, viewDoc.defaultView);
     await onEvents;
 
     ok(true,
       `${expectedSelectionChangeEvents[i]} selectionchange events detected on mouseover`);
 
     // Simulating a mouse out event on the font name and expecting a selectionchange.
-    const otherEl = fontEls[i].querySelector(".font-preview");
+    const otherEl = fontEls[i].querySelector(".font-origin");
     onEvents = waitForNSelectionEvents(tab, 1);
     EventUtils.synthesizeMouse(otherEl, 2, 2, {type: "mouseover"}, viewDoc.defaultView);
     await onEvents;
 
     ok(true, "1 selectionchange events detected on mouseout");
   }
 });
 
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js
@@ -13,33 +13,37 @@ const TEST_URI = URL_ROOT + "browser_fon
 const originalTheme = getTheme();
 
 registerCleanupFunction(() => {
   info(`Restoring theme to '${originalTheme}.`);
   setTheme(originalTheme);
 });
 
 add_task(async function() {
+  await pushPref("devtools.inspector.fonteditor.enabled", true);
   const { inspector, view } = await openFontInspectorForURL(TEST_URI);
-  const { document: doc } = view;
+  const viewDoc = view.document;
 
   await selectNode(".normal-text", inspector);
+  await expandOtherFontsAccordion(viewDoc);
+  const otherFontsEls = getOtherFontsEls(viewDoc);
+  const fontEl = otherFontsEls[0];
 
   // Store the original preview URI for later comparison.
-  const originalURI = doc.querySelector("#font-container .font-preview").src;
+  const originalURI = fontEl.querySelector(".font-preview").src;
   const newTheme = originalTheme === "light" ? "dark" : "light";
 
   info(`Original theme was '${originalTheme}'.`);
 
   await setThemeAndWaitForUpdate(newTheme, inspector);
-  isnot(doc.querySelector("#font-container .font-preview").src, originalURI,
+  isnot(fontEl.querySelector(".font-preview").src, originalURI,
     "The preview image changed with the theme.");
 
   await setThemeAndWaitForUpdate(originalTheme, inspector);
-  is(doc.querySelector("#font-container .font-preview").src, originalURI,
+  is(fontEl.querySelector(".font-preview").src, originalURI,
     "The preview image is correct after the original theme was restored.");
 });
 
 /**
  * Sets the current theme and waits for fontinspector-updated event.
  *
  * @param {String} theme - the new theme
  * @param {Object} inspector - the inspector panel
--- a/devtools/client/inspector/fonts/test/head.js
+++ b/devtools/client/inspector/fonts/test/head.js
@@ -19,19 +19,20 @@ registerCleanupFunction(() => {
 /**
  * The font-inspector doesn't participate in the inspector's update mechanism
  * (i.e. it doesn't call inspector.updating() when updating), so simply calling
  * the default selectNode isn't enough to guaranty that the panel has finished
  * updating. We also need to wait for the fontinspector-updated event.
  */
 var _selectNode = selectNode;
 selectNode = async function(node, inspector, reason) {
-  const onUpdated = inspector.once("fontinspector-updated");
+  const onInspectorUpdated = inspector.once("fontinspector-updated");
+  const onEditorUpdated = inspector.once("fonteditor-updated");
   await _selectNode(node, inspector, reason);
-  await onUpdated;
+  await Promise.all([onInspectorUpdated, onEditorUpdated]);
 };
 
 /**
  * Adds a new tab with the given URL, opens the inspector and selects the
  * font-inspector tab.
  * @return {Promise} resolves to a {toolbox, inspector, view} object
  */
 var openFontInspectorForURL = async function(url) {
@@ -91,17 +92,17 @@ async function updatePreviewText(view, t
 
 /**
  * Get all of the <li> elements for the fonts used on the currently selected element.
  *
  * @param  {document} viewDoc
  * @return {Array}
  */
 function getUsedFontsEls(viewDoc) {
-  return viewDoc.querySelectorAll("#font-container > .fonts-list > li");
+  return viewDoc.querySelectorAll("#font-editor .fonts-list li");
 }
 
 /**
  * Expand the other fonts accordion.
  */
 async function expandOtherFontsAccordion(viewDoc) {
   info("Expanding the other fonts section");
 
@@ -134,8 +135,20 @@ function getOtherFontsEls(viewDoc) {
  * @param  {DOMNode} fontEl
  *         The font element.
  * @return {String}
  *         The name of the font as shown in the UI.
  */
 function getName(fontEl) {
   return fontEl.querySelector(".font-name").textContent;
 }
+
+/**
+ * Given a font element, return the font's URL.
+ *
+ * @param  {DOMNode} fontEl
+ *         The font element.
+ * @return {String}
+ *         The URL where the font was loaded from as shown in the UI.
+ */
+function getURL(fontEl) {
+  return fontEl.querySelector(".font-origin").textContent;
+}
--- a/devtools/client/inspector/fonts/types.js
+++ b/devtools/client/inspector/fonts/types.js
@@ -74,25 +74,36 @@ const font = exports.font = {
   variationInstances: PropTypes.arrayOf(PropTypes.shape(fontVariationInstance))
 };
 
 exports.fontOptions = {
   // The current preview text
   previewText: PropTypes.string,
 };
 
+const fontFamilies = {
+  // Font family names used on the selected element
+  used: PropTypes.arrayOf(PropTypes.string),
+
+  // Font family names declared but not used on the selected element
+  notUsed: PropTypes.arrayOf(PropTypes.string),
+};
+
 exports.fontEditor = {
   // Variable font axes and their values
   axes: PropTypes.object,
 
   // Axes values changed at runtime structured like the "values" property
   // of a fontVariationInstance
   customInstanceValues: PropTypes.array,
 
-  // Fonts applicable to selected element
+  // Font families declared on this element
+  families: PropTypes.shape(fontFamilies),
+
+  // Fonts used on the selected element whose family names are declared in CSS font-family
   fonts: PropTypes.arrayOf(PropTypes.shape(font)),
 
   // Font variation instance currently selected
   instance: PropTypes.shape(fontVariationInstance),
 
   // CSS font properties defined on the element
   properties: PropTypes.object,
 };
--- a/devtools/client/inspector/fonts/utils/font-utils.js
+++ b/devtools/client/inspector/fonts/utils/font-utils.js
@@ -51,27 +51,32 @@ module.exports = {
    *        Its contents are expected to be stable having been already parsed by the
    *        browser.
    * @return {Object}
    */
   parseFontVariationAxes(string) {
     let axes = {};
     const keywords = ["initial", "normal", "inherit", "unset"];
 
-    if (keywords.includes(string.trim())) {
+    if (!string || keywords.includes(string.trim())) {
       return axes;
     }
 
     // Parse font-variation-settings CSS declaration into an object
     // with axis tags as keys and axis values as values.
     axes = string
       .split(",")
       .reduce((acc, pair) => {
         // Tags are always in quotes. Split by quote and filter excessive whitespace.
         pair = pair.split(/["']/).filter(part => part.trim() !== "");
+        // Guard against malformed input that may have slipped through.
+        if (pair.length === 0) {
+          return acc;
+        }
+
         const tag = pair[0];
         const value = pair[1].trim();
         // Axis tags shorter or longer than 4 characters are invalid. Whitespace is valid.
         if (tag.length === 4) {
           acc[tag] = value;
         }
         return acc;
       }, {});
--- a/devtools/client/inspector/moz.build
+++ b/devtools/client/inspector/moz.build
@@ -26,9 +26,9 @@ DevToolsModules(
     'reducers.js',
     'store.js',
     'toolsidebar.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Inspector')
+    BUG_COMPONENT = ('DevTools', 'Inspector')
--- a/devtools/client/jsonview/moz.build
+++ b/devtools/client/jsonview/moz.build
@@ -14,10 +14,10 @@ DevToolsModules(
     'converter-child.js',
     'converter-observer.js',
     'json-viewer.js',
     'viewer-config.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: JSON Viewer')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'JSON Viewer')
deleted file mode 100644
--- a/devtools/client/locales/en-US/appcacheutils.properties
+++ /dev/null
@@ -1,121 +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/.
-
-# LOCALIZATION NOTE These strings are used inside the Web Console
-# command line which is available from the Web Developer sub-menu
-# -> 'Web Console'.
-# These messages are displayed when an attempt is made to validate a
-# page or a cache manifest using AppCacheUtils.jsm
-
-# The correct localization of this file might be to keep it in
-# English, or another language commonly spoken among web developers.
-# You want to make that choice consistent across the developer tools.
-# A good criteria is the language in which you'd find the best
-# documentation on web development on the web.
-
-# LOCALIZATION NOTE (noManifest): the specified page has no cache manifest.
-noManifest=The specified page has no manifest.
-
-# LOCALIZATION NOTE (notUTF8): the associated cache manifest has a character
-# encoding that is not UTF-8. Parameters: %S is the current encoding.
-notUTF8=Manifest has a character encoding of %S. Manifests must have the utf-8 character encoding.
-
-# LOCALIZATION NOTE (badMimeType): the associated cache manifest has a
-# mimetype that is not text/cache-manifest. Parameters: %S is the current
-# mimetype.
-badMimeType=Manifest has a mimetype of %S. Manifests must have a mimetype of text/cache-manifest.
-
-# LOCALIZATION NOTE (duplicateURI): the associated cache manifest references
-# the same URI from multiple locations. Parameters: %1$S is the URI, %2$S is a
-# list of references to this URI.
-duplicateURI=URI %1$S is referenced in multiple locations. This is not allowed: %2$S.
-
-# LOCALIZATION NOTE (networkBlocksURI, fallbackBlocksURI): the associated
-# cache manifest references the same URI in the NETWORK (or FALLBACK) section
-# as it does in other sections. Parameters: %1$S is the line number, %2$S is
-# the resource name, %3$S is the line number, %4$S is the resource name, %5$S
-# is the section name.
-networkBlocksURI=NETWORK section line %1$S (%2$S) prevents caching of line %3$S (%4$S) in the %5$S section.
-fallbackBlocksURI=FALLBACK section line %1$S (%2$S) prevents caching of line %3$S (%4$S) in the %5$S section.
-
-# LOCALIZATION NOTE (fileChangedButNotManifest): the associated cache manifest
-# references a URI that has a file modified after the cache manifest.
-# Parameters: %1$S is the resource name, %2$S is the cache manifest, %3$S is
-# the line number.
-fileChangedButNotManifest=The file %1$S was modified after %2$S. Unless the text in the manifest file is changed the cached version will be used instead at line %3$S.
-
-# LOCALIZATION NOTE (cacheControlNoStore): the specified page has a header
-# preventing caching or storing information. Parameters: %1$S is the resource
-# name, %2$S is the line number.
-cacheControlNoStore=%1$S has cache-control set to no-store. This will prevent the application cache from storing the file at line %2$S.
-
-# LOCALIZATION NOTE (notAvailable): the specified resource is not available.
-# Parameters: %1$S is the resource name, %2$S is the line number.
-notAvailable=%1$S points to a resource that is not available at line %2$S.
-
-# LOCALIZATION NOTE (invalidURI): it's used when an invalid URI is passed to
-# the appcache.
-invalidURI=The URI passed to AppCacheUtils is invalid.
-
-# LOCALIZATION NOTE (noResults): it's used when a search returns no results.
-noResults=Your search returned no results.
-
-# LOCALIZATION NOTE (cacheDisabled): it's used when the cache is disabled and
-# an attempt is made to view offline data.
-cacheDisabled=Your disk cache is disabled. Please set browser.cache.disk.enable to true in about:config and try again.
-
-# LOCALIZATION NOTE (firstLineMustBeCacheManifest): the associated cache
-# manifest has a first line that is not "CACHE MANIFEST". Parameters: %S is
-# the line number.
-firstLineMustBeCacheManifest=The first line of the manifest must be “CACHE MANIFEST” at line %S.
-
-# LOCALIZATION NOTE (cacheManifestOnlyFirstLine2): the associated cache
-# manifest has "CACHE MANIFEST" on a line other than the first line.
-# Parameters: %S is the line number where "CACHE MANIFEST" appears.
-cacheManifestOnlyFirstLine2=“CACHE MANIFEST” is only valid on the first line but was found at line %S.
-
-# LOCALIZATION NOTE (asteriskInWrongSection2): the associated cache manifest
-# has an asterisk (*) in a section other than the NETWORK section. Parameters:
-# %1$S is the section name, %2$S is the line number.
-asteriskInWrongSection2=Asterisk (*) incorrectly used in the %1$S section at line %2$S. If a line in the NETWORK section contains only a single asterisk character, then any URI not listed in the manifest will be treated as if the URI was listed in the NETWORK section. Otherwise such URIs will be treated as unavailable. Other uses of the * character are prohibited.
-
-# LOCALIZATION NOTE (escapeSpaces1): the associated cache manifest has a space
-# in a URI. Spaces must be replaced with %20. Parameters: %S is the line
-# number where this error occurs.
-# %% will be displayed as a single % character (% is commonly used to define
-# format specifiers, so it needs to be escaped).
-escapeSpaces1=Spaces in URIs need to be replaced with %%20 at line %S.
-
-# LOCALIZATION NOTE (slashDotDotSlashBad): the associated cache manifest has a
-# URI containing /../, which is invalid. Parameters: %S is the line number
-# where this error occurs.
-slashDotDotSlashBad=/../ is not a valid URI prefix at line %S.
-
-# LOCALIZATION NOTE (tooManyDotDotSlashes): the associated cache manifest has
-# a URI containing too many ../ operators. Too many of these operators mean
-# that the file would be below the root of the site, which is not possible.
-# Parameters: %S is the line number where this error occurs.
-tooManyDotDotSlashes=Too many dot dot slash operators (../) at line %S.
-
-# LOCALIZATION NOTE (fallbackUseSpaces): the associated cache manifest has a
-# FALLBACK section containing more or less than the standard two URIs
-# separated by a single space. Parameters: %S is the line number where this
-# error occurs.
-fallbackUseSpaces=Only two URIs separated by spaces are allowed in the FALLBACK section at line %S.
-
-# LOCALIZATION NOTE (fallbackAsterisk2): the associated cache manifest has a
-# FALLBACK section that attempts to use an asterisk (*) as a wildcard. In this
-# section the URI is simply a path prefix. Parameters: %S is the line number
-# where this error occurs.
-fallbackAsterisk2=Asterisk (*) incorrectly used in the FALLBACK section at line %S. URIs in the FALLBACK section simply need to match a prefix of the request URI.
-
-# LOCALIZATION NOTE (settingsBadValue): the associated cache manifest has a
-# SETTINGS section containing something other than the valid "prefer-online"
-# or "fast". Parameters: %S is the line number where this error occurs.
-settingsBadValue=The SETTINGS section may only contain a single value, “prefer-online” or “fast” at line %S.
-
-# LOCALIZATION NOTE (invalidSectionName): the associated cache manifest
-# contains an invalid section name. Parameters: %1$S is the section name, %2$S
-# is the line number.
-invalidSectionName=Invalid section name (%1$S) at line %2$S.
--- a/devtools/client/memory/moz.build
+++ b/devtools/client/memory/moz.build
@@ -1,15 +1,15 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Memory')
+    BUG_COMPONENT = ('DevTools', 'Memory')
 
 DIRS += [
     'actions',
     'components',
     'reducers',
 ]
 
 DevToolsModules(
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -39,9 +39,9 @@ DIRS += [
 JAR_MANIFESTS += ['jar.mn']
 
 DevToolsModules(
     'definitions.js',
     'menus.js',
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools')
+    BUG_COMPONENT = ('DevTools', 'General')
--- a/devtools/client/netmonitor/moz.build
+++ b/devtools/client/netmonitor/moz.build
@@ -10,9 +10,9 @@ DevToolsModules(
     'panel.js'
 )
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Netmonitor')
+    BUG_COMPONENT = ('DevTools', 'Netmonitor')
--- a/devtools/client/performance-new/moz.build
+++ b/devtools/client/performance-new/moz.build
@@ -12,9 +12,9 @@ DevToolsModules(
     'browser.js',
     'panel.js',
     'utils.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
+    BUG_COMPONENT = ('DevTools', 'Performance Tools (Profiler/Timeline)')
--- a/devtools/client/performance/moz.build
+++ b/devtools/client/performance/moz.build
@@ -13,9 +13,9 @@ DevToolsModules(
     'events.js',
     'panel.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
+    BUG_COMPONENT = ('DevTools', 'Performance Tools (Profiler/Timeline)')
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -24,9 +24,9 @@ DevToolsModules(
     'store.js',
     'types.js',
 )
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Responsive Design Mode')
+    BUG_COMPONENT = ('DevTools', 'Responsive Design Mode')
--- a/devtools/client/scratchpad/moz.build
+++ b/devtools/client/scratchpad/moz.build
@@ -7,10 +7,10 @@
 DevToolsModules(
     'scratchpad-commands.js',
     'scratchpad-manager.jsm',
     'scratchpad-panel.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Scratchpad')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'Scratchpad')
--- a/devtools/client/shadereditor/moz.build
+++ b/devtools/client/shadereditor/moz.build
@@ -4,10 +4,10 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'panel.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: WebGL Shader Editor')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'WebGL Shader Editor')
deleted file mode 100644
--- a/devtools/client/shared/AppCacheUtils.jsm
+++ /dev/null
@@ -1,617 +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/. */
-
-/**
- * validateManifest() warns of the following errors:
- *  - No manifest specified in page
- *  - Manifest is not utf-8
- *  - Manifest mimetype not text/cache-manifest
- *  - Manifest does not begin with "CACHE MANIFEST"
- *  - Page modified since appcache last changed
- *  - Duplicate entries
- *  - Conflicting entries e.g. in both CACHE and NETWORK sections or in cache
- *    but blocked by FALLBACK namespace
- *  - Detect referenced files that are not available
- *  - Detect referenced files that have cache-control set to no-store
- *  - Wildcards used in a section other than NETWORK
- *  - Spaces in URI not replaced with %20
- *  - Completely invalid URIs
- *  - Too many dot dot slash operators
- *  - SETTINGS section is valid
- *  - Invalid section name
- *  - etc.
- */
-
-"use strict";
-
-var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
-var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
-var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
-
-var { gDevTools } = require("devtools/client/framework/devtools");
-var Services = require("Services");
-var { globals } = require("devtools/shared/builtin-modules");
-
-this.EXPORTED_SYMBOLS = ["AppCacheUtils"];
-
-function AppCacheUtils(documentOrUri) {
-  this._parseManifest = this._parseManifest.bind(this);
-
-  if (documentOrUri) {
-    if (typeof documentOrUri == "string") {
-      this.uri = documentOrUri;
-    }
-    if (/HTMLDocument/.test(documentOrUri.toString())) {
-      this.doc = documentOrUri;
-    }
-  }
-}
-
-AppCacheUtils.prototype = {
-  get cachePath() {
-    return "";
-  },
-
-  validateManifest: function ACU_validateManifest() {
-    return new Promise((resolve, reject) => {
-      this.errors = [];
-      // Check for missing manifest.
-      this._getManifestURI().then(manifestURI => {
-        this.manifestURI = manifestURI;
-
-        if (!this.manifestURI) {
-          this._addError(0, "noManifest");
-          resolve(this.errors);
-        }
-
-        this._getURIInfo(this.manifestURI).then(uriInfo => {
-          this._parseManifest(uriInfo).then(() => {
-            // Sort errors by line number.
-            this.errors.sort(function(a, b) {
-              return a.line - b.line;
-            });
-            resolve(this.errors);
-          });
-        });
-      });
-    });
-  },
-
-  _parseManifest: function ACU__parseManifest(uriInfo) {
-    return new Promise((resolve, reject) => {
-      const manifestName = uriInfo.name;
-      const manifestLastModified = new Date(uriInfo.responseHeaders["last-modified"]);
-
-      if (uriInfo.charset.toLowerCase() != "utf-8") {
-        this._addError(0, "notUTF8", uriInfo.charset);
-      }
-
-      if (uriInfo.mimeType != "text/cache-manifest") {
-        this._addError(0, "badMimeType", uriInfo.mimeType);
-      }
-
-      const parser = new ManifestParser(uriInfo.text, this.manifestURI);
-      const parsed = parser.parse();
-
-      if (parsed.errors.length > 0) {
-        this.errors.push.apply(this.errors, parsed.errors);
-      }
-
-      // Check for duplicate entries.
-      const dupes = {};
-      for (const parsedUri of parsed.uris) {
-        dupes[parsedUri.uri] = dupes[parsedUri.uri] || [];
-        dupes[parsedUri.uri].push({
-          line: parsedUri.line,
-          section: parsedUri.section,
-          original: parsedUri.original
-        });
-      }
-      for (const [uri, value] of Object.entries(dupes)) {
-        if (value.length > 1) {
-          this._addError(0, "duplicateURI", uri, JSON.stringify(value));
-        }
-      }
-
-      // Loop through network entries making sure that fallback and cache don't
-      // contain uris starting with the network uri.
-      for (const neturi of parsed.uris) {
-        if (neturi.section == "NETWORK") {
-          for (const parsedUri of parsed.uris) {
-            if (parsedUri.section !== "NETWORK" &&
-                parsedUri.uri.startsWith(neturi.uri)) {
-              this._addError(neturi.line, "networkBlocksURI", neturi.line,
-                             neturi.original, parsedUri.line, parsedUri.original,
-                             parsedUri.section);
-            }
-          }
-        }
-      }
-
-      // Loop through fallback entries making sure that fallback and cache don't
-      // contain uris starting with the network uri.
-      for (const fb of parsed.fallbacks) {
-        for (const parsedUri of parsed.uris) {
-          if (parsedUri.uri.startsWith(fb.namespace)) {
-            this._addError(fb.line, "fallbackBlocksURI", fb.line,
-                           fb.original, parsedUri.line, parsedUri.original,
-                           parsedUri.section);
-          }
-        }
-      }
-
-      // Check that all resources exist and that their cach-control headers are
-      // not set to no-store.
-      let current = -1;
-      for (let i = 0, len = parsed.uris.length; i < len; i++) {
-        const parsedUri = parsed.uris[i];
-        this._getURIInfo(parsedUri.uri).then(uriInfo => {
-          current++;
-
-          if (uriInfo.success) {
-            // Check that the resource was not modified after the manifest was last
-            // modified. If it was then the manifest file should be refreshed.
-            const resourceLastModified =
-              new Date(uriInfo.responseHeaders["last-modified"]);
-
-            if (manifestLastModified < resourceLastModified) {
-              this._addError(parsedUri.line, "fileChangedButNotManifest",
-                             uriInfo.name, manifestName, parsedUri.line);
-            }
-
-            // If cache-control: no-store the file will not be added to the
-            // appCache.
-            if (uriInfo.nocache) {
-              this._addError(parsedUri.line, "cacheControlNoStore",
-                             parsedUri.original, parsedUri.line);
-            }
-          } else if (parsedUri.original !== "*") {
-            this._addError(parsedUri.line, "notAvailable",
-                           parsedUri.original, parsedUri.line);
-          }
-
-          if (current == len - 1) {
-            resolve();
-          }
-        });
-      }
-    });
-  },
-
-  _getURIInfo: function ACU__getURIInfo(uri) {
-    return new Promise((resolve, reject) => {
-      const inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
-                          .createInstance(Ci.nsIScriptableInputStream);
-      let buffer = "";
-      const channel = NetUtil.newChannel({
-        uri: uri,
-        loadUsingSystemPrincipal: true,
-        securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
-      });
-
-      // Avoid the cache:
-      channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
-      channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
-
-      channel.asyncOpen2({
-        onStartRequest: function(request, context) {
-          // This empty method is needed in order for onDataAvailable to be
-          // called.
-        },
-
-        onDataAvailable: function(request, context, stream, offset, count) {
-          request.QueryInterface(Ci.nsIHttpChannel);
-          inputStream.init(stream);
-          buffer = buffer.concat(inputStream.read(count));
-        },
-
-        onStopRequest: function onStartRequest(request, context, statusCode) {
-          if (statusCode === 0) {
-            request.QueryInterface(Ci.nsIHttpChannel);
-
-            const result = {
-              name: request.name,
-              success: request.requestSucceeded,
-              status: request.responseStatus + " - " + request.responseStatusText,
-              charset: request.contentCharset || "utf-8",
-              mimeType: request.contentType,
-              contentLength: request.contentLength,
-              nocache: request.isNoCacheResponse() || request.isNoStoreResponse(),
-              prePath: request.URI.prePath + "/",
-              text: buffer
-            };
-
-            result.requestHeaders = {};
-            request.visitRequestHeaders(function(header, value) {
-              result.requestHeaders[header.toLowerCase()] = value;
-            });
-
-            result.responseHeaders = {};
-            request.visitResponseHeaders(function(header, value) {
-              result.responseHeaders[header.toLowerCase()] = value;
-            });
-
-            resolve(result);
-          } else {
-            resolve({
-              name: request.name,
-              success: false
-            });
-          }
-        }
-      });
-    });
-  },
-
-  listEntries: function ACU_show(searchTerm) {
-    if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
-      throw new Error(l10n.GetStringFromName("cacheDisabled"));
-    }
-
-    const entries = [];
-
-    const appCacheStorage = Services.cache2.appCacheStorage(Services.loadContextInfo.default, null);
-    appCacheStorage.asyncVisitStorage({
-      onCacheStorageInfo: function() {},
-
-      onCacheEntryInfo: function(aURI, aIdEnhance, aDataSize, aFetchCount, aLastModifiedTime, aExpirationTime) {
-        const lowerKey = aURI.asciiSpec.toLowerCase();
-
-        if (searchTerm && !lowerKey.includes(searchTerm.toLowerCase())) {
-          return;
-        }
-
-        if (aIdEnhance) {
-          aIdEnhance += ":";
-        }
-
-        const entry = {
-          "deviceID": "offline",
-          "key": aIdEnhance + aURI.asciiSpec,
-          "fetchCount": aFetchCount,
-          "lastFetched": null,
-          "lastModified": new Date(aLastModifiedTime * 1000),
-          "expirationTime": new Date(aExpirationTime * 1000),
-          "dataSize": aDataSize
-        };
-
-        entries.push(entry);
-        return true;
-      }
-    }, true);
-
-    if (entries.length === 0) {
-      throw new Error(l10n.GetStringFromName("noResults"));
-    }
-    return entries;
-  },
-
-  viewEntry: function ACU_viewEntry(key) {
-    const win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-    const url = "about:cache-entry?storage=appcache&context=&eid=&uri=" + key;
-    win.openTrustedLinkIn(url, "tab");
-  },
-
-  clearAll: function ACU_clearAll() {
-    if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
-      throw new Error(l10n.GetStringFromName("cacheDisabled"));
-    }
-
-    const appCacheStorage = Services.cache2.appCacheStorage(Services.loadContextInfo.default, null);
-    appCacheStorage.asyncEvictStorage({
-      onCacheEntryDoomed: function(result) {}
-    });
-  },
-
-  _getManifestURI: function ACU__getManifestURI() {
-    return new Promise((resolve, reject) => {
-      const getURI = () => {
-        const htmlNode = this.doc.querySelector("html[manifest]");
-        if (htmlNode) {
-          const pageUri = this.doc.location ? this.doc.location.href : this.uri;
-          const manifestURI = htmlNode.getAttribute("manifest");
-
-          const originRegExp = new RegExp(/([a-z]*:\/\/[^/]*\/)/);
-          if (originRegExp.test(manifestURI)) {
-            return manifestURI;
-          } else if (manifestURI.startsWith("/")) {
-            return pageUri.match(originRegExp)[0] + manifestURI.substring(1);
-          }
-
-          return pageUri.substring(0, pageUri.lastIndexOf("/") + 1) + manifestURI;
-        }
-      };
-
-      if (this.doc) {
-        const uri = getURI();
-        return resolve(uri);
-      }
-      this._getURIInfo(this.uri).then(uriInfo => {
-        if (uriInfo.success) {
-          const html = uriInfo.text;
-          const parser = _DOMParser;
-          this.doc = parser.parseFromString(html, "text/html");
-          const uri = getURI();
-          resolve(uri);
-        } else {
-          this.errors.push({
-              line: 0,
-              msg: l10n.GetStringFromName("invalidURI")
-          });
-        }
-      });
-    });
-  },
-
-  _addError: function ACU__addError(line, l10nString, ...params) {
-    let msg;
-
-    if (params) {
-      msg = l10n.formatStringFromName(l10nString, params, params.length);
-    } else {
-      msg = l10n.GetStringFromName(l10nString);
-    }
-
-    this.errors.push({
-      line: line,
-      msg: msg
-    });
-  },
-};
-
-/**
- * We use our own custom parser because we need far more detailed information
- * than the system manifest parser provides.
- *
- * @param {String} manifestText
- *        The text content of the manifest file.
- * @param {String} manifestURI
- *        The URI of the manifest file. This is used in calculating the path of
- *        relative URIs.
- */
-function ManifestParser(manifestText, manifestURI) {
-  this.manifestText = manifestText;
-  this.origin = manifestURI.substr(0, manifestURI.lastIndexOf("/") + 1)
-                           .replace(" ", "%20");
-}
-
-ManifestParser.prototype = {
-  parse: function OCIMP_parse() {
-    const lines = this.manifestText.split(/\r?\n/);
-    const fallbacks = this.fallbacks = [];
-    const settings = this.settings = [];
-    const errors = this.errors = [];
-    const uris = this.uris = [];
-
-    this.currSection = "CACHE";
-
-    for (let i = 0; i < lines.length; i++) {
-      const text = this.text = lines[i].trim();
-      this.currentLine = i + 1;
-
-      if (i === 0 && text !== "CACHE MANIFEST") {
-        this._addError(1, "firstLineMustBeCacheManifest", 1);
-      }
-
-      // Ignore comments
-      if (/^#/.test(text) || !text.length) {
-        continue;
-      }
-
-      if (text == "CACHE MANIFEST") {
-        if (this.currentLine != 1) {
-          this._addError(this.currentLine, "cacheManifestOnlyFirstLine2",
-                         this.currentLine);
-        }
-        continue;
-      }
-
-      if (this._maybeUpdateSectionName()) {
-        continue;
-      }
-
-      switch (this.currSection) {
-        case "CACHE":
-        case "NETWORK":
-          this.parseLine();
-          break;
-        case "FALLBACK":
-          this.parseFallbackLine();
-          break;
-        case "SETTINGS":
-          this.parseSettingsLine();
-          break;
-      }
-    }
-
-    return {
-      uris: uris,
-      fallbacks: fallbacks,
-      settings: settings,
-      errors: errors
-    };
-  },
-
-  parseLine: function OCIMP_parseLine() {
-    let text = this.text;
-
-    if (text.includes("*")) {
-      if (this.currSection != "NETWORK" || text.length != 1) {
-        this._addError(this.currentLine, "asteriskInWrongSection2",
-                       this.currSection, this.currentLine);
-        return;
-      }
-    }
-
-    if (/\s/.test(text)) {
-      this._addError(this.currentLine, "escapeSpaces1", this.currentLine);
-      text = text.replace(/\s/g, "%20");
-    }
-
-    if (text[0] == "/") {
-      if (text.substr(0, 4) == "/../") {
-        this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
-      } else {
-        this.uris.push(this._wrapURI(this.origin + text.substring(1)));
-      }
-    } else if (text.substr(0, 2) == "./") {
-      this.uris.push(this._wrapURI(this.origin + text.substring(2)));
-    } else if (text.substr(0, 4) == "http") {
-      this.uris.push(this._wrapURI(text));
-    } else {
-      let origin = this.origin;
-      let path = text;
-
-      while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
-        const trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
-        origin = origin.substr(0, trimIdx);
-        path = path.substr(3);
-      }
-
-      if (path.substr(0, 3) == "../") {
-        this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
-        return;
-      }
-
-      if (/^https?:\/\//.test(path)) {
-        this.uris.push(this._wrapURI(path));
-        return;
-      }
-      this.uris.push(this._wrapURI(origin + path));
-    }
-  },
-
-  parseFallbackLine: function OCIMP_parseFallbackLine() {
-    const split = this.text.split(/\s+/);
-    const origURI = this.text;
-
-    if (split.length != 2) {
-      this._addError(this.currentLine, "fallbackUseSpaces", this.currentLine);
-      return;
-    }
-
-    let [ namespace, fallback ] = split;
-
-    if (namespace.includes("*")) {
-      this._addError(this.currentLine, "fallbackAsterisk2", this.currentLine);
-    }
-
-    if (/\s/.test(namespace)) {
-      this._addError(this.currentLine, "escapeSpaces1", this.currentLine);
-      namespace = namespace.replace(/\s/g, "%20");
-    }
-
-    if (namespace.substr(0, 4) == "/../") {
-      this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
-    }
-
-    if (namespace.substr(0, 2) == "./") {
-      namespace = this.origin + namespace.substring(2);
-    }
-
-    if (namespace.substr(0, 4) != "http") {
-      let origin = this.origin;
-      let path = namespace;
-
-      while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
-        const trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
-        origin = origin.substr(0, trimIdx);
-        path = path.substr(3);
-      }
-
-      if (path.substr(0, 3) == "../") {
-        this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
-      }
-
-      if (/^https?:\/\//.test(path)) {
-        namespace = path;
-      } else {
-        if (path[0] == "/") {
-          path = path.substring(1);
-        }
-        namespace = origin + path;
-      }
-    }
-
-    this.text = fallback;
-    this.parseLine();
-
-    this.fallbacks.push({
-      line: this.currentLine,
-      original: origURI,
-      namespace: namespace,
-      fallback: fallback
-    });
-  },
-
-  parseSettingsLine: function OCIMP_parseSettingsLine() {
-    const text = this.text;
-
-    if (this.settings.length == 1 || !/prefer-online|fast/.test(text)) {
-      this._addError(this.currentLine, "settingsBadValue", this.currentLine);
-      return;
-    }
-
-    switch (text) {
-      case "prefer-online":
-        this.settings.push(this._wrapURI(text));
-        break;
-      case "fast":
-        this.settings.push(this._wrapURI(text));
-        break;
-    }
-  },
-
-  _wrapURI: function OCIMP__wrapURI(uri) {
-    return {
-      section: this.currSection,
-      line: this.currentLine,
-      uri: uri,
-      original: this.text
-    };
-  },
-
-  _addError: function OCIMP__addError(line, l10nString, ...params) {
-    let msg;
-
-    if (params) {
-      msg = l10n.formatStringFromName(l10nString, params, params.length);
-    } else {
-      msg = l10n.GetStringFromName(l10nString);
-    }
-
-    this.errors.push({
-      line: line,
-      msg: msg
-    });
-  },
-
-  _maybeUpdateSectionName: function OCIMP__maybeUpdateSectionName() {
-    let text = this.text;
-
-    if (text == text.toUpperCase() && text.charAt(text.length - 1) == ":") {
-      text = text.substr(0, text.length - 1);
-
-      switch (text) {
-        case "CACHE":
-        case "NETWORK":
-        case "FALLBACK":
-        case "SETTINGS":
-          this.currSection = text;
-          return true;
-        default:
-          this._addError(this.currentLine,
-                         "invalidSectionName", text, this.currentLine);
-          return false;
-      }
-    }
-  },
-};
-
-XPCOMUtils.defineLazyGetter(this, "l10n", () => Services.strings
-  .createBundle("chrome://devtools/locale/appcacheutils.properties"));
-
-XPCOMUtils.defineLazyGetter(this, "_DOMParser", function() {
-  return globals.DOMParser();
-});
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -15,17 +15,16 @@ DIRS += [
     'redux',
     'source-map',
     'vendor',
     'webpack',
     'widgets',
 ]
 
 DevToolsModules(
-    'AppCacheUtils.jsm',
     'autocomplete-popup.js',
     'browser-loader.js',
     'css-angle.js',
     'curl.js',
     'demangle.js',
     'developer-toolbar.js',
     'devices.js',
     'DOMHelpers.jsm',
@@ -53,12 +52,12 @@ DevToolsModules(
     'undo.js',
     'unicode-url.js',
     'view-source.js',
     'webgl-utils.js',
     'zoom-keys.js',
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools')
+    BUG_COMPONENT = ('DevTools', 'General')
 
 with Files('components/**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Shared Components')
+    BUG_COMPONENT = ('DevTools', 'Shared Components')
--- a/devtools/client/shared/test/browser_telemetry_sidebar.js
+++ b/devtools/client/shared/test/browser_telemetry_sidebar.js
@@ -6,25 +6,43 @@
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_sidebar.js</p>";
 const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
+var animationPanelId;
+if (Services.prefs.getBoolPref("devtools.new-animationinspector.enabled")) {
+  animationPanelId = "newanimationinspector";
+} else {
+  animationPanelId = "animationinspector";
+}
+
 const DATA = [
   {
     timestamp: null,
     category: "devtools.main",
     method: "sidepanel_changed",
     object: "inspector",
     value: null,
     extra: {
       oldpanel: "computedview",
+      newpanel: animationPanelId
+    }
+  },
+  {
+    timestamp: null,
+    category: "devtools.main",
+    method: "sidepanel_changed",
+    object: "inspector",
+    value: null,
+    extra: {
+      oldpanel: animationPanelId,
       newpanel: "fontinspector"
     }
   },
   {
     timestamp: null,
     category: "devtools.main",
     method: "sidepanel_changed",
     object: "inspector",
@@ -48,16 +66,27 @@ const DATA = [
   {
     timestamp: null,
     category: "devtools.main",
     method: "sidepanel_changed",
     object: "inspector",
     value: null,
     extra: {
       oldpanel: "computedview",
+      newpanel: animationPanelId
+    }
+  },
+  {
+    timestamp: null,
+    category: "devtools.main",
+    method: "sidepanel_changed",
+    object: "inspector",
+    value: null,
+    extra: {
+      oldpanel: animationPanelId,
       newpanel: "fontinspector"
     }
   },
   {
     timestamp: null,
     category: "devtools.main",
     method: "sidepanel_changed",
     object: "inspector",
@@ -103,17 +132,17 @@ add_task(async function() {
   gBrowser.removeCurrentTab();
 });
 
 function testSidebar(toolbox) {
   info("Testing sidebar");
 
   const inspector = toolbox.getCurrentPanel();
   let sidebarTools = ["computedview", "layoutview", "fontinspector",
-                      "animationinspector"];
+                      animationPanelId];
 
   // Concatenate the array with itself so that we can open each tool twice.
   sidebarTools = [...sidebarTools, ...sidebarTools];
 
   return new Promise(resolve => {
     // See TOOL_DELAY for why we need setTimeout here
     setTimeout(function selectSidebarTab() {
       const tool = sidebarTools.pop();
--- a/devtools/client/sourceeditor/moz.build
+++ b/devtools/client/sourceeditor/moz.build
@@ -15,9 +15,9 @@ DevToolsModules(
     'editor-commands-controller.js',
     'editor.js',
     'wasm.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Source Editor')
+    BUG_COMPONENT = ('DevTools', 'Source Editor')
--- a/devtools/client/storage/moz.build
+++ b/devtools/client/storage/moz.build
@@ -6,10 +6,10 @@
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 DevToolsModules(
     'panel.js',
     'ui.js'
 )
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Storage Inspector')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'Storage Inspector')
--- a/devtools/client/styleeditor/moz.build
+++ b/devtools/client/styleeditor/moz.build
@@ -10,10 +10,10 @@ DevToolsModules(
     'original-source.js',
     'styleeditor-commands.js',
     'styleeditor-panel.js',
     'StyleEditorUI.jsm',
     'StyleEditorUtil.jsm',
     'StyleSheetEditor.jsm',
 )
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Style Editor')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'Style Editor')
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -1,40 +1,42 @@
 /* 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/. */
 
 /* Animation-inspector specific theme variables */
 
 :root {
-  --animation-even-background-color: rgba(0, 0, 0, 0.05);
+  --animation-item-hover-color: var(--grey-30-a40);
+  --animation-item-selected-color: var(--grey-30-a90);
   --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
+  --cssanimation-color: var(--purple-50);
+  --csstransition-color: var(--blue-55);
   --devtools-toolbar-height: 24px;
   --fast-track-image: url("images/animation-fast-track.svg");
-  --fill-color-cssanimation: var(--theme-contrast-background);
-  --fill-color-csstransition: var(--theme-highlight-blue);
-  --fill-color-scriptanimation: var(--theme-graphs-green);
   --graph-height: 30px;
   --graph-right-offset: 10px;
   --keyframe-marker-shadow-color: #c4c4c4;
   --pause-image: url(chrome://devtools/skin/images/pause.svg);
-  --progress-bar-color: #909090;
+  --progress-bar-color: var(--grey-40);
   --resume-image: url(chrome://devtools/skin/images/play.svg);
   --rewind-image: url(chrome://devtools/skin/images/rewind.svg);
-  --scrubber-color: #dd00a9;
+  --scriptanimation-color: var(--green-60);
+  --scrubber-color: var(--magenta-65);
   --sidebar-width: 200px;
-  --stroke-color-cssanimation: var(--theme-highlight-lightorange);
-  --stroke-color-csstransition: var(--theme-highlight-bluegrey);
-  --stroke-color-scriptanimation: var(--theme-highlight-green);
-  --tick-line-style: 0.5px solid rgba(128, 136, 144, 0.5);
+  --tick-line-style: 0.5px solid var(--theme-splitter-color);
 }
 
 :root.theme-dark {
-  --animation-even-background-color: rgba(255, 255, 255, 0.05);
+  --animation-item-hover-color: var(--grey-60-a50);
+  --animation-item-selected-color: var(--grey-60);
+  --csstransition-color: var(--blue-50);
   --keyframe-marker-shadow-color: #818181;
+  --progress-bar-color: var(--grey-50);
+  --scrubber-color: var(--magenta-50);
 }
 
 /* Root element of animation inspector */
 #animation-container {
   cursor: default;
   display: flex;
   flex-direction: column;
   height: 100%;
@@ -127,37 +129,37 @@ select.playback-rate-selector.devtools-b
 }
 
 .indication-bar.current-time-scrubber::after {
   border-left-color: var(--scrubber-color);
   left: 5px;
 }
 
 /* Animation Item */
-.animation-item:nth-child(2n+1) {
-  background-color: var(--animation-even-background-color);
-}
-
 .animation-item.cssanimation {
-  --computed-timing-graph-color: var(--fill-color-cssanimation);
-  --effect-timing-graph-color: var(--stroke-color-cssanimation);
+  --graph-color: var(--cssanimation-color);
+  --graph-opacity: 0.7;
 }
 
 .animation-item.csstransition {
-  --computed-timing-graph-color: var(--fill-color-csstransition);
-  --effect-timing-graph-color: var(--stroke-color-csstransition);
+  --graph-color: var(--csstransition-color);
+  --graph-opacity: 0.8;
 }
 
 .animation-item.scriptanimation {
-  --computed-timing-graph-color: var(--fill-color-scriptanimation);
-  --effect-timing-graph-color: var(--stroke-color-scriptanimation);
+  --graph-color: var(--scriptanimation-color);
+  --graph-opacity: 0.5;
+}
+
+.animation-item:hover {
+  background-color: var(--animation-item-hover-color);
 }
 
 .animation-item.selected {
-  background-color: var(--theme-selection-background-hover);
+  background-color: var(--animation-item-selected-color);
 }
 
 /* Animation Target */
 .animation-target {
   align-items: center;
   display: flex;
   grid-column: 1 / 2;
   height: var(--graph-height);
@@ -186,17 +188,18 @@ select.playback-rate-selector.devtools-b
   background-color: var(--theme-highlight-blue);
 }
 
 /* Summary Graph */
 .animation-summary-graph {
   cursor: pointer;
   grid-column: 2 / 3;
   height: var(--graph-height);
-  padding-top: 5px;
+  padding-bottom: 3px;
+  padding-top: 3px;
   position: relative;
 }
 
 .animation-summary-graph.compositor::after {
   background-image: var(--fast-track-image);
   background-repeat: no-repeat;
   content: "";
   display: block;
@@ -210,28 +213,29 @@ select.playback-rate-selector.devtools-b
 }
 
 .animation-summary-graph-path {
   height: 100%;
   width: 100%;
 }
 
 .animation-computed-timing-path path {
-  fill: var(--computed-timing-graph-color);
+  fill: var(--graph-color);
+  fill-opacity: var(--graph-opacity);
   vector-effect: non-scaling-stroke;
   transform: scale(1, -1);
 }
 
 .animation-computed-timing-path path.infinity:nth-child(n+2) {
   opacity: 0.3;
 }
 
 .animation-effect-timing-path path {
   fill: none;
-  stroke: var(--effect-timing-graph-color);
+  stroke: var(--graph-color);
   stroke-dasharray: 2px 2px;
   transform: scale(1, -1);
   vector-effect: non-scaling-stroke;
 }
 
 .animation-effect-timing-path path.infinity:nth-child(n+2) {
   opacity: 0.3;
 }
@@ -245,33 +249,33 @@ select.playback-rate-selector.devtools-b
   vector-effect: non-scaling-stroke;
 }
 
 .animation-delay-sign,
 .animation-end-delay-sign {
   background-color: var(--theme-graphs-grey);
   height: 3px;
   position: absolute;
-  top: calc(100% - 1.5px);
+  bottom: 2px;
 }
 
 .animation-delay-sign::before,
 .animation-end-delay-sign::before {
   background-color: inherit;
   border-radius: 50%;
   content: "";
   height: 6px;
   position: absolute;
   top: -1.5px;
   width: 6px;
 }
 
 .animation-delay-sign.fill,
 .animation-end-delay-sign.fill {
-  background-color: var(--effect-timing-graph-color);
+  background-color: var(--graph-color);
 }
 
 .animation-delay-sign.negative::before {
   left: unset;
   right: -3px;
 }
 
 .animation-end-delay-sign::before {
@@ -300,17 +304,17 @@ select.playback-rate-selector.devtools-b
   stroke-linejoin: round;
   stroke-opacity: .5;
   stroke-width: 4;
   text-anchor: end;
 }
 
 /* Animation Detail */
 .animation-detail-container {
-  background-color: var(--theme-body-background);
+  background-color: var(--theme-sidebar-background);
   display: flex;
   flex-direction: column;
   height: 100%;
   overflow: hidden;
   width: 100%;
   z-index: 2;
 }
 
@@ -360,20 +364,16 @@ select.playback-rate-selector.devtools-b
   border-top-color: var(--progress-bar-color);
 }
 
 .indication-bar.keyframes-progress-bar::after {
   border-left-color: var(--progress-bar-color);
 }
 
 /* Animated Property Item */
-.animated-property-item:nth-child(2n+1) {
-  background-color: var(--animation-even-background-color);
-}
-
 .animated-property-item.unchanged {
   opacity: 0.6;
 }
 
 /* Animated Property Name */
 .animated-property-name {
   align-items: center;
   display: flex;
@@ -383,25 +383,25 @@ select.playback-rate-selector.devtools-b
 }
 
 .animated-property-name.compositor span {
   padding-left: 15px;
   position: relative;
 }
 
 .animated-property-list-container.cssanimation .animated-property-name.compositor {
-  --fast-track-color: var(--stroke-color-cssanimation);
+  --fast-track-color: var(--cssanimation-color);
 }
 
 .animated-property-list-container.csstransition .animated-property-name.compositor {
-  --fast-track-color: var(--stroke-color-csstransition);
+  --fast-track-color: var(--csstransition-color);
 }
 
 .animated-property-list-container.scriptanimation .animated-property-name.compositor {
-  --fast-track-color: var(--stroke-color-scriptanimation);
+  --fast-track-color: var(--scriptanimation-color);
 }
 
 .animated-property-name.compositor span::before {
   background-image: var(--fast-track-image);
   background-repeat: no-repeat;
   background-size: contain;
   content: "";
   fill: var(--fast-track-color);
@@ -425,30 +425,31 @@ select.playback-rate-selector.devtools-b
 }
 
 .keyframes-graph-path {
   height: 100%;
   width: 100%;
 }
 
 .keyframes-graph-path path {
-  fill: #00b0bd88;
-  stroke: #00b0bd;
+  fill: var(--teal-60);
+  fill-opacity: 0.5;
+  stroke: var(--teal-70);
   vector-effect: non-scaling-stroke;
   transform: scale(1, -1);
 }
 
 .keyframes-graph.opacity .keyframes-graph-path path {
-  fill: #df00a988;
-  stroke: #df00a9;
+  fill: var(--magenta-50);
+  stroke: var(--magenta-70);
 }
 
 .keyframes-graph.transform .keyframes-graph-path path {
-  fill: #ea800088;
-  stroke: #ea8000;
+  fill: var(--yellow-50);
+  stroke: var(--yellow-60);
 }
 
 .keyframes-graph-path .color-path path {
   stroke: none;
 }
 
 .keyframes-graph .keyframes-graph-path .hint path {
   fill: none;
@@ -491,25 +492,25 @@ select.playback-rate-selector.devtools-b
   position: absolute;
   top: 50%;
   height: 10px;
   transform: translate(-5px, -3px);
   width: 10px;
 }
 
 .animated-property-list-container.cssanimation .keyframe-marker-item {
-  background-color: var(--fill-color-cssanimation);
+  background-color: var(--cssanimation-color);
 }
 
 .animated-property-list-container.csstransition .keyframe-marker-item {
-  background-color: var(--fill-color-csstransition);
+  background-color: var(--csstransition-color);
 }
 
 .animated-property-list-container.scriptanimation .keyframe-marker-item {
-  background-color: var(--fill-color-scriptanimation);
+  background-color: var(--scriptanimation-color);
 }
 
 /* Common Components */
 /* Progress Inspection Panel */
 .progress-inspection-panel {
   height: 100%;
   overflow-y: auto;
   overflow-x: hidden;
--- a/devtools/client/themes/variables.css
+++ b/devtools/client/themes/variables.css
@@ -208,38 +208,49 @@
   --toolbarbutton-checked-border-color: var(--toolbarbutton-border-color);
   --toolbarbutton-checked-focus-background: var(--blue-60);
 
   /* The photon animation curve */
   --animation-curve: cubic-bezier(.07,.95,0,1);
 
   /* Firefox Colors CSS Variables v1.0.3
    * Colors are taken from: https://github.com/FirefoxUX/design-tokens */
+  --magenta-50: #ff1ad9;
   --magenta-65: #dd00a9;
+  --magenta-70: #b5007f;
 
+  --purple-50: #9400ff;
   --purple-60: #8000d7;
 
   --blue-40: #45a1ff;
   --blue-50: #0a84ff;
   --blue-55: #0074e8;
   --blue-60: #0060df;
   --blue-70: #003eaa;
 
+  --teal-60: #00c8d7;
+  --teal-70: #008ea4;
+
   --red-70: #a4000f;
 
   --green-50: #30e60b;
   --green-60: #12bc00;
   --green-70: #058b00;
 
+  --yellow-50: #ffe900;
+  --yellow-60: #d7b600;
   --yellow-80: #715100;
 
   --grey-10: #f9f9fa;
   --grey-20: #ededf0;
   --grey-30: #d7d7db;
+  --grey-30-a40: rgba(215, 215, 219, 0.4);
+  --grey-30-a90: rgba(215, 215, 219, 0.9);
   --grey-40: #b1b1b3;
   --grey-50: #737373;
   --grey-60: #4a4a4f;
+  --grey-60-a50: rgba(74, 74, 79, 0.5);
   --grey-70: #38383d;
   --grey-80: #2a2a2e;
   --grey-90: #0c0c0d;
   --grey-90-a10: rgba(12, 12, 13, 0.1);
   --grey-90-a80: rgba(12, 12, 13, 0.8);
 }
--- a/devtools/client/webaudioeditor/moz.build
+++ b/devtools/client/webaudioeditor/moz.build
@@ -4,10 +4,10 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'panel.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Web Audio Editor')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'Web Audio Editor')
--- a/devtools/client/webconsole/moz.build
+++ b/devtools/client/webconsole/moz.build
@@ -24,9 +24,9 @@ DevToolsModules(
     'types.js',
     'utils.js',
     'webconsole-connection-proxy.js',
     'webconsole-frame.js',
     'webconsole-l10n.js',
     'webconsole-output-wrapper.js',
 )
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Console')
+    BUG_COMPONENT = ('DevTools', 'Console')
--- a/devtools/client/webide/moz.build
+++ b/devtools/client/webide/moz.build
@@ -14,9 +14,9 @@ DIRS += [
 BROWSER_CHROME_MANIFESTS += [
     'test/browser.ini'
 ]
 MOCHITEST_CHROME_MANIFESTS += [
     'test/chrome.ini'
 ]
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: WebIDE')
+    BUG_COMPONENT = ('DevTools', 'WebIDE')
--- a/devtools/moz.build
+++ b/devtools/moz.build
@@ -23,27 +23,27 @@ DIRS += [
 # /browser uses DIST_SUBDIR.  We opt-in to this treatment when building
 # DevTools for the browser to keep the root omni.ja slim for use by external XUL
 # apps.  Mulet also uses this since it includes /browser.
 if CONFIG['MOZ_BUILD_APP'] == 'browser':
     DIST_SUBDIR = 'browser'
     export('DIST_SUBDIR')
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools')
+    BUG_COMPONENT = ('DevTools', 'General')
 
 with Files('docs/**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools')
+    BUG_COMPONENT = ('DevTools', 'General')
 
 with Files('docs/tools/memory-panel.md'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Memory')
+    BUG_COMPONENT = ('DevTools', 'Memory')
 
 with Files('docs/tools/debugger-panel.md'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Debugger')
+    BUG_COMPONENT = ('DevTools', 'Debugger')
 
 with Files('docs/backend/debugger-api.md'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Debugger')
+    BUG_COMPONENT = ('DevTools', 'Debugger')
 
 with Files('docs/tools/http-inspector.md'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Console')
+    BUG_COMPONENT = ('DevTools', 'Console')
 
 with Files('docs/tools/inspector-panel.md'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Inspector')
+    BUG_COMPONENT = ('DevTools', 'Inspector')
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -110,20 +110,22 @@ var AnimationPlayerActor = protocol.Acto
       return this.player.effect.target;
     }
 
     const pseudo = this.player.effect.target;
     const treeWalker = this.walker.getDocumentWalker(pseudo.parentElement);
     return pseudo.type === "::before" ? treeWalker.firstChild() : treeWalker.lastChild();
   },
 
+  get document() {
+    return this.node.ownerDocument;
+  },
+
   get window() {
-    // ownerGlobal doesn't exist in content privileged windows.
-    // eslint-disable-next-line mozilla/use-ownerGlobal
-    return this.node.ownerDocument.defaultView;
+    return this.document.defaultView;
   },
 
   /**
    * Release the actor, when it isn't needed anymore.
    * Protocol.js uses this release method to call the destroy method.
    */
   release: function() {},
 
@@ -861,27 +863,31 @@ exports.AnimationsActor = protocol.Actor
    * Pause given animations.
    *
    * @param {Array} actors A list of AnimationPlayerActor.
    */
   pauseSome: function(actors) {
     for (const { player } of actors) {
       this.pauseSync(player);
     }
+
+    return this.waitForNextFrame(actors);
   },
 
   /**
    * Play given animations.
    *
    * @param {Array} actors A list of AnimationPlayerActor.
    */
   playSome: function(actors) {
     for (const { player } of actors) {
       this.playSync(player);
     }
+
+    return this.waitForNextFrame(actors);
   },
 
   /**
    * Set the current time of several animations at the same time.
    * @param {Array} players A list of AnimationPlayerActor.
    * @param {Number} time The new currentTime.
    * @param {Boolean} shouldPause Should the players be paused too.
    * @param {Object} options
@@ -902,17 +908,17 @@ exports.AnimationsActor = protocol.Actor
 
       if (shouldPause) {
         player.startTime = null;
       }
 
       player.currentTime = (time - actor.createdTime) * player.playbackRate;
     }
 
-    return Promise.resolve();
+    return this.waitForNextFrame(players);
   },
 
   /**
    * Set the playback rate of several animations at the same time.
    * @param {Array} players A list of AnimationPlayerActor.
    * @param {Number} rate The new rate.
    */
   setPlaybackRates: function(players, rate) {
@@ -988,9 +994,35 @@ exports.AnimationsActor = protocol.Actor
   updateAnimationCreatedTime(animation) {
     if (!this.animationCreatedTimeMap.has(animation)) {
       const createdTime =
         animation.startTime ||
         animation.timeline.currentTime - animation.currentTime / animation.playbackRate;
       this.animationCreatedTimeMap.set(animation, createdTime);
     }
   },
+
+  /**
+   * Wait for next animation frame.
+   *
+   * @param {Array} actors
+   * @return {Promise} which waits for next frame
+   */
+  waitForNextFrame(actors) {
+    const promises = actors.map(actor => {
+      const doc = actor.document;
+      const win = actor.window;
+      const timeAtCurrent = doc.timeline.currentTime;
+
+      return new Promise(resolve => {
+        win.requestAnimationFrame(() => {
+          if (timeAtCurrent === doc.timeline.currentTime) {
+            win.requestAnimationFrame(resolve);
+          } else {
+            resolve();
+          }
+        });
+      });
+    });
+
+    return Promise.all(promises);
+  },
 });
--- a/devtools/server/actors/inspector/moz.build
+++ b/devtools/server/actors/inspector/moz.build
@@ -10,9 +10,9 @@ DevToolsModules(
   'event-parsers.js',
   'inspector.js',
   'node.js',
   'utils.js',
   'walker.js',
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Inspector')
+    BUG_COMPONENT = ('DevTools', 'Inspector')
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -63,42 +63,42 @@ DevToolsModules(
     'timeline.js',
     'webaudio.js',
     'webbrowser.js',
     'webconsole.js',
     'webgl.js',
 )
 
 with Files('animation.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Animation Inspector')
+    BUG_COMPONENT = ('DevTools', 'Animation Inspector')
 
 with Files('breakpoint.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Debugger')
+    BUG_COMPONENT = ('DevTools', 'Debugger')
 
 with Files('css-properties.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: CSS Rules Inspector')
+    BUG_COMPONENT = ('DevTools', 'CSS Rules Inspector')
 
 with Files('csscoverage.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Graphics Commandline and Toolbar')
+    BUG_COMPONENT = ('DevTools', 'Graphics Commandline and Toolbar')
 
 with Files('memory.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Memory')
+    BUG_COMPONENT = ('DevTools', 'Memory')
 
 with Files('performance*'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
+    BUG_COMPONENT = ('DevTools', 'Performance Tools (Profiler/Timeline)')
 
 with Files('source.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Debugger')
+    BUG_COMPONENT = ('DevTools', 'Debugger')
 
 with Files('storage.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Storage Inspector')
+    BUG_COMPONENT = ('DevTools', 'Storage Inspector')
 
 with Files('stylesheets.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Style Editor')
+    BUG_COMPONENT = ('DevTools', 'Style Editor')
 
 with Files('webaudio.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Web Audio Editor')
+    BUG_COMPONENT = ('DevTools', 'Web Audio Editor')
 
 with Files('webconsole.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Console')
+    BUG_COMPONENT = ('DevTools', 'Console')
 
 with Files('webgl.js'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: WebGL Shader Editor')
+    BUG_COMPONENT = ('DevTools', 'WebGL Shader Editor')
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -296,16 +296,17 @@ var PageStyleActor = protocol.ActorClass
     const fonts = InspectorUtils.getUsedFontFaces(rng);
     const fontsArray = [];
 
     for (let i = 0; i < fonts.length; i++) {
       const font = fonts[i];
       const fontFace = {
         name: font.name,
         CSSFamilyName: font.CSSFamilyName,
+        CSSGeneric: font.CSSGeneric || null,
         srcIndex: font.srcIndex,
         URI: font.URI,
         format: font.format,
         localName: font.localName,
         metadata: font.metadata
       };
 
       // If this font comes from a @font-face rule
--- a/devtools/server/moz.build
+++ b/devtools/server/moz.build
@@ -17,9 +17,9 @@ BROWSER_CHROME_MANIFESTS += ['tests/brow
 MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 
 DevToolsModules(
     'main.js',
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools')
+    BUG_COMPONENT = ('DevTools', 'General')
--- a/devtools/server/performance/moz.build
+++ b/devtools/server/performance/moz.build
@@ -7,10 +7,10 @@
 DevToolsModules(
     'framerate.js',
     'memory.js',
     'profiler.js',
     'recorder.js',
     'timeline.js',
 )
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'Performance Tools (Profiler/Timeline)')
--- a/devtools/shared/css/generated/properties-db.js
+++ b/devtools/shared/css/generated/properties-db.js
@@ -276,16 +276,17 @@ exports.CSS_PROPERTIES = {
       "scrollbarbutton-down",
       "scrollbarbutton-left",
       "scrollbarbutton-right",
       "scrollbarbutton-up",
       "scrollbarthumb-horizontal",
       "scrollbarthumb-vertical",
       "scrollbartrack-horizontal",
       "scrollbartrack-vertical",
+      "scrollcorner",
       "searchfield",
       "separator",
       "spinner",
       "spinner-downbutton",
       "spinner-textfield",
       "spinner-upbutton",
       "splitter",
       "statusbar",
@@ -2906,16 +2907,37 @@ exports.CSS_PROPERTIES = {
       "cursor",
       "pointer-events",
       "-moz-user-input",
       "-moz-user-modify",
       "-moz-user-focus",
       "caret-color",
       "scrollbar-face-color",
       "scrollbar-track-color",
+      "text-anchor",
+      "color-interpolation",
+      "color-interpolation-filters",
+      "fill",
+      "fill-opacity",
+      "fill-rule",
+      "shape-rendering",
+      "stroke",
+      "stroke-width",
+      "stroke-linecap",
+      "stroke-linejoin",
+      "stroke-miterlimit",
+      "stroke-opacity",
+      "stroke-dasharray",
+      "stroke-dashoffset",
+      "clip-rule",
+      "marker-start",
+      "marker-mid",
+      "marker-end",
+      "paint-order",
+      "-moz-context-properties",
       "list-style-position",
       "list-style-type",
       "list-style-image",
       "quotes",
       "-moz-image-region",
       "margin-top",
       "margin-right",
       "margin-bottom",
@@ -2975,37 +2997,16 @@ exports.CSS_PROPERTIES = {
       "text-decoration-line",
       "text-decoration-style",
       "text-decoration-color",
       "initial-letter",
       "ime-mode",
       "-moz-user-select",
       "-moz-window-dragging",
       "-moz-force-broken-image-icon",
-      "text-anchor",
-      "color-interpolation",
-      "color-interpolation-filters",
-      "fill",
-      "fill-opacity",
-      "fill-rule",
-      "shape-rendering",
-      "stroke",
-      "stroke-width",
-      "stroke-linecap",
-      "stroke-linejoin",
-      "stroke-miterlimit",
-      "stroke-opacity",
-      "stroke-dasharray",
-      "stroke-dashoffset",
-      "clip-rule",
-      "marker-start",
-      "marker-mid",
-      "marker-end",
-      "paint-order",
-      "-moz-context-properties",
       "dominant-baseline",
       "vector-effect",
       "stop-color",
       "stop-opacity",
       "flood-color",
       "flood-opacity",
       "lighting-color",
       "mask-type",
--- a/devtools/shared/heapsnapshot/AutoMemMap.h
+++ b/devtools/shared/heapsnapshot/AutoMemMap.h
@@ -40,17 +40,18 @@ class MOZ_RAII AutoMemMap
   PRFileMap*   fileMap;
   void*        addr;
 
   AutoMemMap(const AutoMemMap& aOther) = delete;
   void operator=(const AutoMemMap& aOther) = delete;
 
 public:
   explicit AutoMemMap(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM)
-      : fd(nullptr)
+      : fileInfo()
+      , fd(nullptr)
       , fileMap(nullptr)
       , addr(nullptr)
   {
       MOZ_GUARD_OBJECT_NOTIFIER_INIT;
   };
   ~AutoMemMap();
 
   // Initialize this AutoMemMap.
--- a/devtools/shared/heapsnapshot/HeapSnapshot.cpp
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
@@ -1354,17 +1354,19 @@ class MOZ_STACK_CLASS HeapSnapshotHandle
 public:
   // For telemetry.
   uint32_t nodeCount;
   uint32_t edgeCount;
 
   HeapSnapshotHandler(CoreDumpWriter& writer,
                       JS::CompartmentSet* compartments)
     : writer(writer),
-      compartments(compartments)
+      compartments(compartments),
+      nodeCount(0),
+      edgeCount(0)
   { }
 
   // JS::ubi::BreadthFirst handler interface.
 
   class NodeData { };
   typedef JS::ubi::BreadthFirst<HeapSnapshotHandler> Traversal;
   bool operator() (Traversal& traversal,
                    JS::ubi::Node origin,
--- a/devtools/shared/heapsnapshot/moz.build
+++ b/devtools/shared/heapsnapshot/moz.build
@@ -1,16 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Memory')
+    BUG_COMPONENT = ('DevTools', 'Memory')
 
 if CONFIG['ENABLE_TESTS']:
     DIRS += ['tests/gtest']
 
 XPCSHELL_TESTS_MANIFESTS += [ 'tests/unit/xpcshell.ini' ]
 MOCHITEST_MANIFESTS += [ 'tests/mochitest/mochitest.ini' ]
 MOCHITEST_CHROME_MANIFESTS += [ 'tests/mochitest/chrome.ini' ]
 
--- a/devtools/shared/moz.build
+++ b/devtools/shared/moz.build
@@ -71,9 +71,9 @@ DevToolsModules(
     'system.js',
     'task.js',
     'ThreadSafeDevToolsUtils.js',
     'throttle.js',
     'wasm-source-map.js',
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools')
+    BUG_COMPONENT = ('DevTools', 'General')
--- a/devtools/shared/test-helpers/moz.build
+++ b/devtools/shared/test-helpers/moz.build
@@ -7,9 +7,9 @@
 # This directory is only processed for local build
 # where MOZILLA_OFFICIAL isn't set
 
 DevToolsModules(
     'allocation-tracker.js',
 )
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools')
+    BUG_COMPONENT = ('DevTools', 'General')
--- a/docshell/base/nsDefaultURIFixup.cpp
+++ b/docshell/base/nsDefaultURIFixup.cpp
@@ -128,32 +128,41 @@ HasUserPassword(const nsACString& aStrin
         return true;
       }
     }
   }
 
   return false;
 }
 
+// Assume that 1 tab is accidental, but more than 1 implies this is
+// supposed to be tab-separated content.
+static bool
+MaybeTabSeparatedContent(const nsCString& aStringURI)
+{
+  auto firstTab = aStringURI.FindChar('\t');
+  return firstTab != kNotFound && aStringURI.RFindChar('\t') != firstTab;
+}
+
 NS_IMETHODIMP
 nsDefaultURIFixup::GetFixupURIInfo(const nsACString& aStringURI,
                                    uint32_t aFixupFlags,
                                    nsIInputStream** aPostData,
                                    nsIURIFixupInfo** aInfo)
 {
   NS_ENSURE_ARG(!aStringURI.IsEmpty());
 
   nsresult rv;
 
   nsAutoCString uriString(aStringURI);
 
   // Eliminate embedded newlines, which single-line text fields now allow:
   uriString.StripCRLF();
-  // Cleanup the empty spaces that might be on each end:
-  uriString.Trim(" ");
+  // Cleanup the empty spaces and tabs that might be on each end:
+  uriString.Trim(" \t");
 
   NS_ENSURE_TRUE(!uriString.IsEmpty(), NS_ERROR_FAILURE);
 
   RefPtr<nsDefaultURIFixupInfo> info = new nsDefaultURIFixupInfo(uriString);
   NS_ADDREF(*aInfo = info);
 
   nsCOMPtr<nsIIOService> ioService =
     do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
@@ -355,22 +364,26 @@ nsDefaultURIFixup::GetFixupURIInfo(const
   if (StringBeginsWith(uriString, NS_LITERAL_CSTRING("://"))) {
     uriString = StringTail(uriString, uriString.Length() - 3);
     inputHadDuffProtocol = true;
   } else if (StringBeginsWith(uriString, NS_LITERAL_CSTRING("//"))) {
     uriString = StringTail(uriString, uriString.Length() - 2);
     inputHadDuffProtocol = true;
   }
 
-  // NB: this rv gets returned at the end of this method if we never
-  // do a keyword fixup after this (because the pref or the flags passed
-  // might not let us).
-  rv = FixupURIProtocol(uriString, info, getter_AddRefs(uriWithProtocol));
-  if (uriWithProtocol) {
-    info->mFixedURI = uriWithProtocol;
+  // Note: this rv gets returned at the end of this method if we don't fix up
+  // the protocol and don't do a keyword fixup after this (because the pref
+  // or the flags passed might not let us).
+  rv = NS_OK;
+  // Avoid fixing up content that looks like tab-separated values
+  if (!MaybeTabSeparatedContent(uriString)) {
+    rv = FixupURIProtocol(uriString, info, getter_AddRefs(uriWithProtocol));
+    if (uriWithProtocol) {
+      info->mFixedURI = uriWithProtocol;
+    }
   }
 
   // See if it is a keyword
   // Test whether keywords need to be fixed up
   if (sFixupKeywords && (aFixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP) &&
       !inputHadDuffProtocol) {
     if (NS_SUCCEEDED(KeywordURIFixup(uriString, info, aPostData)) &&
         info->mPreferredURI) {
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -1023,82 +1023,16 @@ nsDocShell::LoadURI(nsIURI* aURI,
                       srcdoc,
                       sourceDocShell,
                       baseURI,
                       nullptr,  // No nsIDocShell
                       nullptr); // No nsIRequest
 }
 
 NS_IMETHODIMP
-nsDocShell::LoadStream(nsIInputStream* aStream, nsIURI* aURI,
-                       const nsACString& aContentType,
-                       const nsACString& aContentCharset,
-                       nsIDocShellLoadInfo* aLoadInfo)
-{
-  NS_ENSURE_ARG(aStream);
-
-  mAllowKeywordFixup = false;
-
-  // if the caller doesn't pass in a URI we need to create a dummy URI. necko
-  // currently requires a URI in various places during the load. Some consumers
-  // do as well.
-  nsCOMPtr<nsIURI> uri = aURI;
-  if (!uri) {
-    // HACK ALERT
-    nsresult rv = NS_OK;
-    // Make sure that the URI spec "looks" like a protocol and path...
-    // For now, just use a bogus protocol called "internal"
-    rv = NS_MutateURI(NS_SIMPLEURIMUTATOR_CONTRACTID)
-           .SetSpec(NS_LITERAL_CSTRING("internal:load-stream"))
-           .Finalize(uri);
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-  }
-
-  uint32_t loadType = LOAD_NORMAL;
-  nsCOMPtr<nsIPrincipal> triggeringPrincipal;
-  if (aLoadInfo) {
-    nsDocShellInfoLoadType lt = nsIDocShellLoadInfo::loadNormal;
-    (void)aLoadInfo->GetLoadType(&lt);
-    // Get the appropriate LoadType from nsIDocShellLoadInfo type
-    loadType = ConvertDocShellInfoLoadTypeToLoadType(lt);
-    aLoadInfo->GetTriggeringPrincipal(getter_AddRefs(triggeringPrincipal));
-  }
-
-  NS_ENSURE_SUCCESS(Stop(nsIWebNavigation::STOP_NETWORK), NS_ERROR_FAILURE);
-
-  mLoadType = loadType;
-
-  if (!triggeringPrincipal) {
-    triggeringPrincipal = nsContentUtils::GetSystemPrincipal();
-  }
-
-  // build up a channel for this stream.
-  nsCOMPtr<nsIChannel> channel;
-  nsCOMPtr<nsIInputStream> stream = aStream;
-  nsresult rv = NS_NewInputStreamChannel(getter_AddRefs(channel),
-                                         uri,
-                                         stream.forget(),
-                                         triggeringPrincipal,
-                                         nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                         nsIContentPolicy::TYPE_OTHER,
-                                         aContentType,
-                                         aContentCharset);
-  NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
-
-  nsCOMPtr<nsIURILoader> uriLoader(do_GetService(NS_URI_LOADER_CONTRACTID));
-  NS_ENSURE_TRUE(uriLoader, NS_ERROR_FAILURE);
-
-  NS_ENSURE_SUCCESS(DoChannelLoad(channel, uriLoader, false),
-                    NS_ERROR_FAILURE);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
 nsDocShell::CreateLoadInfo(nsIDocShellLoadInfo** aLoadInfo)
 {
   nsDocShellLoadInfo* loadInfo = new nsDocShellLoadInfo();
   nsCOMPtr<nsIDocShellLoadInfo> localRef(loadInfo);
 
   localRef.forget(aLoadInfo);
   return NS_OK;
 }
@@ -13537,42 +13471,33 @@ nsDocShell::OnLinkClickSync(nsIContent* 
   if (anchor) {
     anchor->GetType(typeHint);
     NS_ConvertUTF16toUTF8 utf8Hint(typeHint);
     nsAutoCString type, dummy;
     NS_ParseRequestContentType(utf8Hint, type, dummy);
     CopyUTF8toUTF16(type, typeHint);
   }
 
-  // Clone the URI now, in case a content policy or something messes
-  // with it under InternalLoad; we do _not_ want to change the URI
-  // our caller passed in.
-  nsCOMPtr<nsIURI> clonedURI;
-  aURI->Clone(getter_AddRefs(clonedURI));
-  if (!clonedURI) {
-    return NS_ERROR_OUT_OF_MEMORY;
-  }
-
   // if the triggeringPrincipal is not passed explicitly, then we
   // fall back to using doc->NodePrincipal() as the triggeringPrincipal.
   nsCOMPtr<nsIPrincipal> triggeringPrincipal =
     aTriggeringPrincipal ? aTriggeringPrincipal
                          : aContent->NodePrincipal();
 
   // Link click (or form submission) can be triggered inside an onload handler,
   // and we don't want to add history entry in this case.
   bool inOnLoadHandler = false;
   GetIsExecutingOnLoadHandler(&inOnLoadHandler);
   uint32_t loadType = inOnLoadHandler ? LOAD_NORMAL_REPLACE : LOAD_LINK;
 
   if (aIsUserTriggered) {
     flags |= INTERNAL_LOAD_FLAGS_IS_USER_TRIGGERED;
   }
 
-  nsresult rv = InternalLoad(clonedURI,                 // New URI
+  nsresult rv = InternalLoad(aURI,                      // New URI
                              nullptr,                   // Original URI
                              Nothing(),                 // Let the protocol handler assign it
                              false,                     // LoadReplace
                              referer,                   // Referer URI
                              refererPolicy,             // Referer policy
                              triggeringPrincipal,
                              aContent->NodePrincipal(),
                              flags,
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -86,41 +86,16 @@ interface nsIDocShell : nsIDocShellTreeI
    *                     non-null loadInfo.  And even some of those might not
    *                     be allowed.  Use at your own risk.
    */
   [noscript]void loadURI(in nsIURI uri,
                          in nsIDocShellLoadInfo loadInfo,
                          in unsigned long aLoadFlags,
                          in boolean firstParty);
 
-  /**
-   * Loads a given stream. This will give priority to loading the requested
-   * stream in the object implementing this interface. If it can't be loaded
-   * here however, the URL dispatched will go through its normal process of
-   * content loading.
-   *
-   * @param aStream         - The input stream that provides access to the data
-   *                          to be loaded.  This must be a blocking, threadsafe
-   *                          stream implementation.
-   * @param aURI            - The URI representing the stream, or null.
-   * @param aContentType    - The type (MIME) of data being loaded (empty if unknown).
-   * @param aContentCharset - The charset of the data being loaded (empty if unknown).
-   * @param aLoadInfo       - This is the extended load info for this load.  This
-   *                          most often will be null, but if you need to do
-   *                          additional setup for this load you can get a
-   *                          loadInfo object by calling createLoadInfo.  Once
-   *                          you have this object you can set the needed
-   *                          properties on it and then pass it to loadStream.
-   */
-  [noscript]void loadStream(in nsIInputStream aStream,
-                            in nsIURI aURI,
-                            in ACString aContentType,
-                            in ACString aContentCharset,
-                            in nsIDocShellLoadInfo aLoadInfo);
-
   const long INTERNAL_LOAD_FLAGS_NONE                    = 0x0;
   const long INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL       = 0x1;
   const long INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER      = 0x2;
   const long INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP = 0x4;
 
   // This flag marks the first load in this object
   // @see nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD
   const long INTERNAL_LOAD_FLAGS_FIRST_LOAD              = 0x8;
--- a/docshell/base/timeline/moz.build
+++ b/docshell/base/timeline/moz.build
@@ -1,16 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
+    BUG_COMPONENT = ('DevTools', 'Performance Tools (Profiler/Timeline)')
 
 EXPORTS.mozilla += [
     'AbstractTimelineMarker.h',
     'AutoGlobalTimelineMarker.h',
     'AutoRestyleTimelineMarker.h',
     'AutoTimelineMarker.h',
     'CompositeTimelineMarker.h',
     'ConsoleTimelineMarker.h',
--- a/docshell/test/moz.build
+++ b/docshell/test/moz.build
@@ -24,20 +24,20 @@ with Files('browser/*bug92473*'):
 
 with Files('browser/*loadDisallowInherit*'):
     BUG_COMPONENT = ('Firefox', 'Address Bar')
 
 with Files('browser/*tab_touch_events*'):
     BUG_COMPONENT = ('Core', 'DOM: Events')
 
 with Files('browser/*timelineMarkers*'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
+    BUG_COMPONENT = ('DevTools', 'Performance Tools (Profiler/Timeline)')
 
 with Files('browser/*ua_emulation*'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools')
+    BUG_COMPONENT = ('DevTools', 'General')
 
 with Files('chrome/*112564*'):
     BUG_COMPONENT = ('Core', 'Networking: HTTP')
 
 with Files('chrome/*303267*'):
     BUG_COMPONENT = ('Core', 'DOM: Core & HTML')
 
 with Files('chrome/*453650*'):
--- a/docshell/test/unit/test_nsDefaultURIFixup_info.js
+++ b/docshell/test/unit/test_nsDefaultURIFixup_info.js
@@ -488,16 +488,26 @@ var testcases = [ {
     fixedURI: "http://plonk:8080/",
     protocolChange: true,
   }, {
     input: "\u10E0\u10D4\u10D2\u10D8\u10E1\u10E2\u10E0\u10D0\u10EA\u10D8\u10D0.\u10D2\u10D4",
     fixedURI: "http://xn--lodaehvb5cdik4g.xn--node/",
     alternateURI: "http://www.xn--lodaehvb5cdik4g.xn--node/",
     protocolChange: true,
   },
+  {
+    input: " \t mozilla.org/\t \t ",
+    fixedURI: "http://mozilla.org/",
+    alternateURI: "http://www.mozilla.org/",
+    protocolChange: true,
+  },
+  {
+    input: " moz\ti\tlla.org ",
+    keywordLookup: true,
+  },
 ];
 
 if (Services.appinfo.OS.toLowerCase().startsWith("win")) {
   testcases.push({
     input: "C:\\some\\file.txt",
     fixedURI: "file:///C:/some/file.txt",
     protocolChange: true,
   });
@@ -584,55 +594,58 @@ function do_single_test_run() {
         continue;
       }
 
       info("Checking \"" + testInput + "\" with flags " + flags +
            " (host lookup for single words: " + (gSingleWordHostLookup ? "yes" : "no") + ")");
 
       // Both APIs should then also be using the same spec.
       Assert.equal(!!fixupURIOnly, !!URIInfo.preferredURI);
-      if (fixupURIOnly)
-        Assert.equal(fixupURIOnly.spec, URIInfo.preferredURI.spec);
+      if (fixupURIOnly) {
+        Assert.equal(fixupURIOnly.spec, URIInfo.preferredURI.spec, "Fixed and preferred URI should match");
+      }
 
       let isFileURL = expectedFixedURI && expectedFixedURI.startsWith("file");
 
       // Check the fixedURI:
       let makeAlternativeURI = flags & urifixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI;
       if (makeAlternativeURI && alternativeURI != null) {
-        Assert.equal(URIInfo.fixedURI.spec, alternativeURI);
+        Assert.equal(URIInfo.fixedURI.spec, alternativeURI, "should have gotten alternate URI");
       } else {
-        Assert.equal(URIInfo.fixedURI && URIInfo.fixedURI.spec, expectedFixedURI);
+        Assert.equal(URIInfo.fixedURI && URIInfo.fixedURI.spec, expectedFixedURI, "should get correct fixed URI");
       }
 
       // Check booleans on input:
       let couldDoKeywordLookup = flags & urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
-      Assert.equal(!!URIInfo.keywordProviderName, couldDoKeywordLookup && expectKeywordLookup);
-      Assert.equal(URIInfo.fixupChangedProtocol, expectProtocolChange);
-      Assert.equal(URIInfo.fixupCreatedAlternateURI, makeAlternativeURI && alternativeURI != null);
+      Assert.equal(!!URIInfo.keywordProviderName, couldDoKeywordLookup && expectKeywordLookup, "keyword lookup as expected");
+      Assert.equal(URIInfo.fixupChangedProtocol, expectProtocolChange, "protocol change as expected");
+      Assert.equal(URIInfo.fixupCreatedAlternateURI, makeAlternativeURI && alternativeURI != null, "alternative URI as expected");
 
       // Check the preferred URI
       if (couldDoKeywordLookup) {
         if (expectKeywordLookup) {
           if (!inWhitelist) {
             let urlparamInput = encodeURIComponent(sanitize(testInput)).replace(/%20/g, "+");
             // If the input starts with `?`, then URIInfo.preferredURI.spec will omit it
             // In order to test this behaviour, remove `?` only if it is the first character
             if (urlparamInput.startsWith("%3F")) {
               urlparamInput = urlparamInput.replace("%3F", "");
             }
             let searchURL = kSearchEngineURL.replace("{searchTerms}", urlparamInput);
             let spec = URIInfo.preferredURI.spec.replace(/%27/g, "'");
-            Assert.equal(spec, searchURL);
+            Assert.equal(spec, searchURL, "should get correct search URI");
           } else {
-            Assert.equal(URIInfo.preferredURI, null);
+            Assert.equal(URIInfo.preferredURI, null, "not expecting a preferred URI");
           }
         } else {
-          Assert.equal(URIInfo.preferredURI.spec, URIInfo.fixedURI.spec);
+          Assert.equal(URIInfo.preferredURI.spec, URIInfo.fixedURI.spec, "fixed URI should match");
         }
       } else {
         // In these cases, we should never be doing a keyword lookup and
         // the fixed URI should be preferred:
-        Assert.equal(URIInfo.preferredURI.spec, URIInfo.fixedURI.spec);
+        let prefURI = URIInfo.preferredURI && URIInfo.preferredURI.spec;
+        let fixedURI = URIInfo.fixedURI && URIInfo.fixedURI.spec;
+        Assert.equal(prefURI, fixedURI, "fixed URI should be same as expected");
       }
-      Assert.equal(sanitize(testInput), URIInfo.originalInput);
+      Assert.equal(sanitize(testInput), URIInfo.originalInput, "should mirror original input");
     }
   }
 }
--- a/dom/base/Link.cpp
+++ b/dom/base/Link.cpp
@@ -854,29 +854,16 @@ Link::UnregisterFromHistory()
       NS_ASSERTION(NS_SUCCEEDED(rv), "This should only fail if we misuse the API!");
       if (NS_SUCCEEDED(rv)) {
         mRegistered = false;
       }
     }
   }
 }
 
-already_AddRefed<nsIURI>
-Link::GetURIToMutate()
-{
-  MOZ_ASSERT(false, "TODO: REMOVE THIS METHOD");
-  nsCOMPtr<nsIURI> uri(GetURI());
-  if (!uri) {
-    return nullptr;
-  }
-  nsCOMPtr<nsIURI> clone;
-  (void)uri->Clone(getter_AddRefs(clone));
-  return clone.forget();
-}
-
 void
 Link::SetHrefAttribute(nsIURI *aURI)
 {
   NS_ASSERTION(aURI, "Null URI is illegal!");
 
   // if we change this code to not reserialize we need to do something smarter
   // in SetProtocol because changing the protocol of an URI can change the
   // "nature" of the nsIURL/nsIURI implementation.
--- a/dom/base/Link.h
+++ b/dom/base/Link.h
@@ -165,17 +165,16 @@ protected:
 
 private:
   /**
    * Unregisters from History so this node no longer gets notifications about
    * changes to visitedness.
    */
   void UnregisterFromHistory();
 
-  already_AddRefed<nsIURI> GetURIToMutate();
   void SetHrefAttribute(nsIURI *aURI);
 
   void GetContentPolicyMimeTypeMedia(nsAttrValue& aAsAttr,
                                      nsContentPolicyType& aPolicyType,
                                      nsString& aMimeType,
                                      nsAString& aMedia);
 
   mutable nsCOMPtr<nsIURI> mCachedURI;
--- a/dom/base/Location.cpp
+++ b/dom/base/Location.cpp
@@ -216,17 +216,18 @@ Location::GetWritableURI(nsIURI** aURI, 
   nsCOMPtr<nsIURI> uri;
 
   nsresult rv = GetURI(getter_AddRefs(uri));
   if (NS_FAILED(rv) || !uri) {
     return rv;
   }
 
   if (!aNewRef) {
-    return uri->Clone(aURI);
+    uri.forget(aURI);
+    return NS_OK;
   }
 
   return uri->CloneWithNewRef(*aNewRef, aURI);
 }
 
 nsresult
 Location::SetURI(nsIURI* aURI, bool aReplace)
 {
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -75,16 +75,18 @@
 #include "mozilla/IMEStateManager.h"
 #include "mozilla/InternalMutationEvent.h"
 #include "mozilla/Likely.h"
 #include "mozilla/ManualNAC.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/ResultExtensions.h"
 #include "mozilla/dom/Selection.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs.h"
 #include "mozilla/TextEvents.h"
 #include "nsArrayUtils.h"
 #include "nsAString.h"
 #include "nsAttrName.h"
 #include "nsAttrValue.h"
 #include "nsAttrValueInlines.h"
 #include "nsBindingManager.h"
 #include "nsCanvasFrame.h"
@@ -507,16 +509,51 @@ EventListenerManagerHashClearEntry(PLDHa
 {
   EventListenerManagerMapEntry *lm =
     static_cast<EventListenerManagerMapEntry *>(entry);
 
   // Let the EventListenerManagerMapEntry clean itself up...
   lm->~EventListenerManagerMapEntry();
 }
 
+static bool
+IsThirdPartyWindowOrChannel(nsPIDOMWindowInner* aWindow,
+                            nsIChannel* aChannel,
+                            nsIURI* aURI)
+{
+  MOZ_ASSERT(!aWindow || !aChannel,
+             "A window and channel should not both be provided.");
+
+  nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil = services::GetThirdPartyUtil();
+  if (!thirdPartyUtil) {
+    return false;
+  }
+
+  // In the absence of a window or channel, we assume that we are first-party.
+  bool thirdParty = false;
+
+  if (aWindow) {
+    Unused << thirdPartyUtil->IsThirdPartyWindow(aWindow->GetOuterWindow(),
+                                                 aURI,
+                                                 &thirdParty);
+  }
+
+  if (aChannel) {
+    // Note, we must call IsThirdPartyChannel() here and not just try to
+    // use nsILoadInfo.isThirdPartyContext.  That nsILoadInfo property only
+    // indicates if the parent loading window is third party or not.  We
+    // want to check the channel URI against the loading principal as well.
+    Unused << thirdPartyUtil->IsThirdPartyChannel(aChannel,
+                                                  nullptr,
+                                                  &thirdParty);
+  }
+
+  return thirdParty;
+}
+
 class SameOriginCheckerImpl final : public nsIChannelEventSink,
                                     public nsIInterfaceRequestor
 {
   ~SameOriginCheckerImpl() = default;
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSICHANNELEVENTSINK
   NS_DECL_NSIINTERFACEREQUESTOR
@@ -8810,16 +8847,46 @@ nsContentUtils::GetCookieBehaviorForPrin
       // prevent us from expressing BEHAVIOR_REJECT_FOREIGN/ACCEPT_SESSION for a
       // specific domain. As BEHAVIOR_LIMIT_FOREIGN isn't visible in our UI,
       // this is probably not an issue.
       *aLifetimePolicy = nsICookieService::ACCEPT_NORMALLY;
       break;
   }
 }
 
+// static public
+bool
+nsContentUtils::StorageDisabledByAntiTracking(nsPIDOMWindowInner* aWindow,
+                                              nsIChannel* aChannel,
+                                              nsIURI* aURI)
+{
+  if (!StaticPrefs::privacy_trackingprotection_storagerestriction_enabled()) {
+    return false;
+  }
+
+  // Let's check if this is a 3rd party context.
+  if (!IsThirdPartyWindowOrChannel(aWindow, aChannel, aURI)) {
+    return false;
+  }
+
+  nsCOMPtr<nsIChannel> channel;
+
+  // aChannel and aWindow are mutually exclusive.
+  channel = aChannel;
+  if (aWindow) {
+    nsIDocument* document = aWindow->GetExtantDoc();
+    if (document) {
+      channel = document->GetChannel();
+    }
+  }
+
+  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel);
+  return httpChannel && httpChannel->GetIsTrackingResource();
+}
+
 // static, private
 nsContentUtils::StorageAccess
 nsContentUtils::InternalStorageAllowedForPrincipal(nsIPrincipal* aPrincipal,
                                                    nsPIDOMWindowInner* aWindow,
                                                    nsIURI* aURI,
                                                    nsIChannel* aChannel)
 {
   MOZ_ASSERT(aPrincipal);
@@ -8827,16 +8894,20 @@ nsContentUtils::InternalStorageAllowedFo
   StorageAccess access = StorageAccess::eAllow;
 
   // We don't allow storage on the null principal, in general. Even if the
   // calling context is chrome.
   if (aPrincipal->GetIsNullPrincipal()) {
     return StorageAccess::eDeny;
   }
 
+  if (StorageDisabledByAntiTracking(aWindow, aChannel, aURI)) {
+    return StorageAccess::eDeny;
+  }
+
   if (aWindow) {
     // If the document is sandboxed, then it is not permitted to use storage
     nsIDocument* document = aWindow->GetExtantDoc();
     if (document && document->GetSandboxFlags() & SANDBOXED_ORIGIN) {
       return StorageAccess::eDeny;
     }
 
     // Check if we are in private browsing, and record that fact
@@ -8902,57 +8973,25 @@ nsContentUtils::InternalStorageAllowedFo
     }
   }
 
   // We don't want to prompt for every attempt to access permissions.
   if (behavior == nsICookieService::BEHAVIOR_REJECT) {
     return StorageAccess::eDeny;
   }
 
-  if (behavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN ||
-      behavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) {
-
-    // In the absence of a window or channel, we assume that we are first-party.
-    bool thirdParty = false;
-
-    MOZ_ASSERT(!aWindow || !aChannel,
-               "A window and channel should not both be provided.");
-
-    if (aWindow) {
-      nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
-        do_GetService(THIRDPARTYUTIL_CONTRACTID);
-      MOZ_ASSERT(thirdPartyUtil);
-
-      Unused << thirdPartyUtil->IsThirdPartyWindow(aWindow->GetOuterWindow(),
-                                                   aURI,
-                                                   &thirdParty);
-    }
-
-    if (aChannel) {
-      nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
-        do_GetService(THIRDPARTYUTIL_CONTRACTID);
-      MOZ_ASSERT(thirdPartyUtil);
-
-      // Note, we must call IsThirdPartyChannel() here and not just try to
-      // use nsILoadInfo.isThirdPartyContext.  That nsILoadInfo property only
-      // indicates if the parent loading window is third party or not.  We
-      // want to check the channel URI against the loading principal as well.
-      Unused << thirdPartyUtil->IsThirdPartyChannel(aChannel,
-                                                    nullptr,
-                                                    &thirdParty);
-    }
-
-    if (thirdParty) {
-      // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by
-      // simply rejecting the request to use the storage. In the future, if we
-      // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense
-      // for non-cookie storage types, this may change.
-
-      return StorageAccess::eDeny;
-    }
+  if ((behavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN ||
+       behavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) &&
+      IsThirdPartyWindowOrChannel(aWindow, aChannel, aURI)) {
+    // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by
+    // simply rejecting the request to use the storage. In the future, if we
+    // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense
+    // for non-cookie storage types, this may change.
+
+    return StorageAccess::eDeny;
   }
 
   return access;
 }
 
 namespace {
 
 // We put StringBuilder in the anonymous namespace to prevent anything outside
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -2950,16 +2950,24 @@ public:
   /*
    * Checks if storage for the given principal is permitted by the user's
    * preferences. The caller is assumed to not be a third-party iframe.
    * (if that is possible, the caller should use StorageAllowedForWindow)
    */
   static StorageAccess StorageAllowedForPrincipal(nsIPrincipal* aPrincipal);
 
   /*
+   * Returns true if this window/channel should disable storages because of the
+   * anti-tracking feature.
+   */
+  static bool StorageDisabledByAntiTracking(nsPIDOMWindowInner* aWindow,
+                                            nsIChannel* aChannel,
+                                            nsIURI* aURI);
+
+  /*
    * Serializes a HTML nsINode into its markup representation.
    */
   static bool SerializeNodeToMarkup(nsINode* aRoot,
                                     bool aDescendentsOnly,
                                     nsAString& aOut);
 
   /*
    * Returns true iff the provided JSObject is a global, and its URI matches
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -936,16 +936,20 @@ nsGlobalWindowInner::nsGlobalWindowInner
       nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
       if (os) {
         // Watch for online/offline status changes so we can fire events. Use
         // a strong reference.
         os->AddObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC,
                         false);
 
         os->AddObserver(mObserver, MEMORY_PRESSURE_OBSERVER_TOPIC, false);
+
+        if (aOuterWindow->IsTopLevelWindow()) {
+          os->AddObserver(mObserver, "clear-site-data-reload-needed", false);
+        }
       }
 
       Preferences::AddStrongObserver(mObserver, "intl.accept_languages");
 
       // Watch for storage notifications so we can fire storage events.
       RefPtr<StorageNotifierService> sns =
         StorageNotifierService::GetOrCreate();
       if (sns) {
@@ -1263,16 +1267,21 @@ nsGlobalWindowInner::FreeInnerObjects()
 
   DisconnectEventTargetObjects();
 
   if (mObserver) {
     nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
     if (os) {
       os->RemoveObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
       os->RemoveObserver(mObserver, MEMORY_PRESSURE_OBSERVER_TOPIC);
+
+      if (GetOuterWindowInternal() &&
+          GetOuterWindowInternal()->IsTopLevelWindow()) {
+        os->RemoveObserver(mObserver, "clear-site-data-reload-needed");
+      }
     }
 
     RefPtr<StorageNotifierService> sns = StorageNotifierService::GetOrCreate();
     if (sns) {
      sns->Unregister(mObserver);
     }
 
     if (mIdleService) {
@@ -5813,16 +5822,23 @@ nsGlobalWindowInner::Observe(nsISupports
 
   if (!nsCRT::strcmp(aTopic, MEMORY_PRESSURE_OBSERVER_TOPIC)) {
     if (mPerformance) {
       mPerformance->MemoryPressure();
     }
     return NS_OK;
   }
 
+  if (!nsCRT::strcmp(aTopic, "clear-site-data-reload-needed")) {
+    // The reload is propagated from the top-level window only.
+    NS_ConvertUTF16toUTF8 otherOrigin(aData);
+    PropagateClearSiteDataReload(otherOrigin);
+    return NS_OK;
+  }
+
   if (!nsCRT::strcmp(aTopic, OBSERVER_TOPIC_IDLE)) {
     mCurrentlyIdle = true;
     if (IsFrozen()) {
       // need to fire only one idle event while the window is frozen.
       mNotifyIdleObserversIdleOnThaw = true;
       mNotifyIdleObserversActiveOnThaw = false;
     } else if (IsCurrentInnerWindow()) {
       HandleIdleActiveEvent();
@@ -8048,16 +8064,49 @@ nsPIDOMWindowInner::MaybeCreateDoc()
     // Note that |document| here is the same thing as our mDoc, but we
     // don't have to explicitly set the member variable because the docshell
     // has already called SetNewDocument().
     nsCOMPtr<nsIDocument> document = docShell->GetDocument();
     Unused << document;
   }
 }
 
+void
+nsGlobalWindowInner::PropagateClearSiteDataReload(const nsACString& aOrigin)
+{
+  nsIPrincipal* principal = GetPrincipal();
+  if (!principal) {
+    return;
+  }
+
+  nsAutoCString origin;
+  nsresult rv = principal->GetOrigin(origin);
+  NS_ENSURE_SUCCESS_VOID(rv);
+
+  // If the URL of this window matches, let's refresh this window only.
+  // We don't need to traverse the DOM tree.
+  if (origin.Equals(aOrigin)) {
+    nsCOMPtr<nsIDocShell> docShell = GetDocShell();
+    nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(docShell));
+    if (NS_WARN_IF(!webNav)) {
+      return;
+    }
+
+    // We don't need any special reload flags, because this notification is
+    // dispatched by Clear-Site-Data header, which should have already cleaned
+    // up all the needed data.
+    rv = webNav->Reload(nsIWebNavigation::LOAD_FLAGS_NONE);
+    NS_ENSURE_SUCCESS_VOID(rv);
+
+    return;
+  }
+
+  CallOnChildren(&nsGlobalWindowInner::PropagateClearSiteDataReload, aOrigin);
+}
+
 mozilla::dom::DocGroup*
 nsPIDOMWindowInner::GetDocGroup() const
 {
   nsIDocument* doc = GetExtantDoc();
   if (doc) {
     return doc->GetDocGroup();
   }
   return nullptr;
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -974,16 +974,18 @@ public:
   void GetInterface(JSContext* aCx, nsIJSID* aIID,
                     JS::MutableHandle<JS::Value> aRetval,
                     mozilla::ErrorResult& aError);
 
   already_AddRefed<nsWindowRoot> GetWindowRoot(mozilla::ErrorResult& aError);
 
   bool ShouldReportForServiceWorkerScope(const nsAString& aScope);
 
+  void PropagateClearSiteDataReload(const nsACString& aOrigin);
+
   already_AddRefed<mozilla::dom::InstallTriggerImpl> GetInstallTrigger();
 
   void UpdateTopInnerWindow();
 
   virtual bool IsInSyncOperation() override
   {
     return GetExtantDoc() && GetExtantDoc()->IsInSyncOperation();
   }
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -971,16 +971,25 @@ MaybeWrapValue(JSContext* cx, JS::Mutabl
 {
   if (rval.isGCThing()) {
     if (rval.isString()) {
       return MaybeWrapStringValue(cx, rval);
     }
     if (rval.isObject()) {
       return MaybeWrapObjectValue(cx, rval);
     }
+#ifdef ENABLE_BIGINT
+    // This could be optimized by checking the zone first, similar to
+    // the way strings are handled. At present, this is used primarily
+    // for structured cloning, so avoiding the overhead of JS_WrapValue
+    // calls is less important than for other types.
+    if (rval.isBigInt()) {
+      return JS_WrapValue(cx, rval);
+    }
+#endif
     MOZ_ASSERT(rval.isSymbol());
     JS_MarkCrossZoneId(cx, SYMBOL_TO_JSID(rval.toSymbol()));
   }
   return true;
 }
 
 namespace binding_detail {
 enum GetOrCreateReflectorWrapBehavior {
--- a/dom/broadcastchannel/BroadcastChannel.cpp
+++ b/dom/broadcastchannel/BroadcastChannel.cpp
@@ -12,16 +12,17 @@
 #include "mozilla/dom/StructuredCloneHolder.h"
 #include "mozilla/dom/ipc/StructuredCloneData.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerRef.h"
 #include "mozilla/dom/WorkerRunnable.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/StaticPrefs.h"
 #include "nsContentUtils.h"
 
 #include "nsIBFCacheEntry.h"
 #include "nsIDocument.h"
 #include "nsISupportsPrimitives.h"
 
 #ifdef XP_WIN
 #undef PostMessage
@@ -294,22 +295,35 @@ BroadcastChannel::Constructor(const Glob
     if (NS_WARN_IF(aRv.Failed())) {
       return nullptr;
     }
 
     aRv = PrincipalToPrincipalInfo(principal, &principalInfo);
     if (NS_WARN_IF(aRv.Failed())) {
       return nullptr;
     }
+
+    if (StaticPrefs::privacy_trackingprotection_storagerestriction_enabled() &&
+        nsContentUtils::StorageAllowedForWindow(window) !=
+          nsContentUtils::StorageAccess::eAllow) {
+      aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+      return nullptr;
+    }
   } else {
     JSContext* cx = aGlobal.Context();
 
     WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
     MOZ_ASSERT(workerPrivate);
 
+    if (StaticPrefs::privacy_trackingprotection_storagerestriction_enabled() &&
+        !workerPrivate->IsStorageAllowed()) {
+      aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+      return nullptr;
+    }
+
     RefPtr<StrongWorkerRef> workerRef =
       StrongWorkerRef::Create(workerPrivate, "BroadcastChannel",
                               [bc] () { bc->Shutdown(); });
     // We are already shutting down the worker. Let's return a non-active
     // object.
     if (NS_WARN_IF(!workerRef)) {
       aRv.Throw(NS_ERROR_FAILURE);
       return nullptr;
--- a/dom/browser-element/BrowserElementChildPreload.js
+++ b/dom/browser-element/BrowserElementChildPreload.js
@@ -858,18 +858,20 @@ BrowserElementChild.prototype = {
       docShell.QueryInterface(Ci.nsIWebNavigation).currentURI.spec;
 
     if ((ChromeUtils.getClassName(elem) === "HTMLAnchorElement" && elem.href) ||
         (ChromeUtils.getClassName(elem) === "HTMLAreaElement" && elem.href)) {
       return {uri: elem.href,
               documentURI: documentURI,
               text: elem.textContent.substring(0, kLongestReturnedString)};
     }
-    if (elem instanceof Ci.nsIImageLoadingContent && elem.currentRequestFinalURI) {
-      return {uri: elem.currentRequestFinalURI.spec, documentURI: documentURI};
+    if (elem instanceof Ci.nsIImageLoadingContent &&
+        (elem.currentRequestFinalURI || elem.currentURI)) {
+      let uri = elem.currentRequestFinalURI || elem.currentURI;
+      return {uri: uri.spec, documentURI: documentURI};
     }
     if (ChromeUtils.getClassName(elem) === "HTMLImageElement") {
       return {uri: elem.src, documentURI: documentURI};
     }
     if (ChromeUtils.getClassName(elem) === "HTMLVideoElement" ||
         ChromeUtils.getClassName(elem) === "HTMLAudioElement") {
       let hasVideo = !(elem.readyState >= elem.HAVE_METADATA &&
                        (elem.videoWidth == 0 || elem.videoHeight == 0));
--- a/dom/chrome-webidl/moz.build
+++ b/dom/chrome-webidl/moz.build
@@ -3,32 +3,32 @@
 # 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/.
 
 with Files("**"):
     BUG_COMPONENT = ("Core", "DOM")
 
 with Files("ChannelWrapper.webidl"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: Request Handling")
+    BUG_COMPONENT = ("WebExtensions", "Request Handling")
 
 with Files("HeapSnapshot.webidl"):
-    BUG_COMPONENT = ("Firefox", "Developer Tools: Memory")
+    BUG_COMPONENT = ("DevTools", "Memory")
 
 with Files("InspectorUtils.webidl"):
-    BUG_COMPONENT = ("Firefox", "Developer Tools: Inspector")
+    BUG_COMPONENT = ("DevTools", "Inspector")
 
 with Files("MatchGlob.webidl"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: General")
+    BUG_COMPONENT = ("WebExtensions", "General")
 
 with Files("MatchPattern.webidl"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: General")
+    BUG_COMPONENT = ("WebExtensions", "General")
 
 with Files("WebExtension*.webidl"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: General")
+    BUG_COMPONENT = ("WebExtensions", "General")
 
 PREPROCESSED_WEBIDL_FILES = [
     'ChromeUtils.webidl',
 ]
 
 WEBIDL_FILES = [
     'ChannelWrapper.webidl',
     'DominatorTree.webidl',
--- a/dom/html/HTMLFormElement.cpp
+++ b/dom/html/HTMLFormElement.cpp
@@ -1665,18 +1665,17 @@ HTMLFormElement::GetActionURL(nsIURI** a
   if (action.IsEmpty()) {
     nsCOMPtr<nsIHTMLDocument> htmlDoc(do_QueryInterface(document));
     if (!htmlDoc) {
       // Must be a XML, XUL or other non-HTML document type
       // so do nothing.
       return NS_OK;
     }
 
-    rv = docURI->Clone(getter_AddRefs(actionURL));
-    NS_ENSURE_SUCCESS(rv, rv);
+    actionURL = docURI;
   } else {
     nsCOMPtr<nsIURI> baseURL = GetBaseURI();
     NS_ASSERTION(baseURL, "No Base URL found in Form Submit!\n");
     if (!baseURL) {
       return NS_OK; // No base URL -> exit early, see Bug 30721
     }
     rv = NS_NewURI(getter_AddRefs(actionURL), action, nullptr, baseURL);
     NS_ENSURE_SUCCESS(rv, rv);
--- a/dom/html/nsHTMLDocument.cpp
+++ b/dom/html/nsHTMLDocument.cpp
@@ -1101,16 +1101,21 @@ nsHTMLDocument::GetCookie(nsAString& aCo
 
   // If the document's sandboxed origin flag is set, access to read cookies
   // is prohibited.
   if (mSandboxFlags & SANDBOXED_ORIGIN) {
     rv.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return;
   }
 
+  if (nsContentUtils::StorageDisabledByAntiTracking(GetInnerWindow(), nullptr,
+                                                    nullptr)) {
+    return;
+  }
+
   // If the document is a cookie-averse Document... return the empty string.
   if (IsCookieAverse()) {
     return;
   }
 
   // not having a cookie service isn't an error
   nsCOMPtr<nsICookieService> service = do_GetService(NS_COOKIESERVICE_CONTRACTID);
   if (service) {
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -2585,16 +2585,28 @@ ContentChild::RecvUpdateAppLocales(nsTAr
 mozilla::ipc::IPCResult
 ContentChild::RecvUpdateRequestedLocales(nsTArray<nsCString>&& aRequestedLocales)
 {
   LocaleService::GetInstance()->AssignRequestedLocales(aRequestedLocales);
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+ContentChild::RecvClearSiteDataReloadNeeded(const nsString& aOrigin)
+{
+  // Rebroadcast "clear-site-data-reload-needed".
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->NotifyObservers(nullptr, "clear-site-data-reload-needed",
+                         aOrigin.get());
+  }
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 ContentChild::RecvAddPermission(const IPC::Permission& permission)
 {
   nsCOMPtr<nsIPermissionManager> permissionManagerIface =
     services::GetPermissionManager();
   nsPermissionManager* permissionManager =
     static_cast<nsPermissionManager*>(permissionManagerIface.get());
   MOZ_ASSERT(permissionManager,
          "We have no permissionManager in the Content process !");
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -400,16 +400,18 @@ public:
 
   virtual mozilla::ipc::IPCResult RecvUpdateDictionaryList(InfallibleTArray<nsString>&& aDictionaries) override;
 
   virtual mozilla::ipc::IPCResult RecvUpdateFontList(InfallibleTArray<SystemFontListEntry>&& aFontList) override;
 
   virtual mozilla::ipc::IPCResult RecvUpdateAppLocales(nsTArray<nsCString>&& aAppLocales) override;
   virtual mozilla::ipc::IPCResult RecvUpdateRequestedLocales(nsTArray<nsCString>&& aRequestedLocales) override;
 
+  virtual mozilla::ipc::IPCResult RecvClearSiteDataReloadNeeded(const nsString& aOrigin) override;
+
   virtual mozilla::ipc::IPCResult RecvAddPermission(const IPC::Permission& permission) override;
 
   virtual mozilla::ipc::IPCResult RecvFlushMemory(const nsString& reason) override;
 
   virtual mozilla::ipc::IPCResult RecvActivateA11y(const uint32_t& aMainChromeTid,
                                                    const uint32_t& aMsaaID) override;
   virtual mozilla::ipc::IPCResult RecvShutdownA11y() override;
 
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -589,16 +589,17 @@ static const char* sObserverTopics[] = {
 #ifdef ACCESSIBILITY
   "a11y-init-or-shutdown",
 #endif
   "cacheservice:empty-cache",
   "intl:app-locales-changed",
   "intl:requested-locales-changed",
   "cookie-changed",
   "private-cookie-changed",
+  "clear-site-data-reload-needed",
 };
 
 // PreallocateProcess is called by the PreallocatedProcessManager.
 // ContentParent then takes this process back within GetNewOrUsedBrowserProcess.
 /*static*/ already_AddRefed<ContentParent>
 ContentParent::PreallocateProcess()
 {
   RefPtr<ContentParent> process =
@@ -3007,16 +3008,19 @@ ContentParent::Observe(nsISupports* aSub
     nsCOMPtr<nsICookie> xpcCookie = do_QueryInterface(aSubject);
     NS_ASSERTION(xpcCookie, "couldn't get cookie");
     if (!nsCRT::strcmp(aData, u"deleted")) {
       cs->RemoveCookie(xpcCookie);
     } else if ((!nsCRT::strcmp(aData, u"added")) ||
                (!nsCRT::strcmp(aData, u"changed"))) {
       cs->AddCookie(xpcCookie);
     }
+  } else if (!strcmp(aTopic, "clear-site-data-reload-needed")) {
+    // Rebroadcast "clear-site-data-reload-needed".
+    Unused << SendClearSiteDataReloadNeeded(nsString(aData));
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 ContentParent::GetInterface(const nsIID& aIID, void** aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -450,16 +450,18 @@ child:
 
     async UpdateDictionaryList(nsString[] dictionaries);
 
     async UpdateFontList(SystemFontListEntry[] fontList);
 
     async UpdateAppLocales(nsCString[] appLocales);
     async UpdateRequestedLocales(nsCString[] requestedLocales);
 
+    async ClearSiteDataReloadNeeded(nsString origin);
+
     // nsIPermissionManager messages
     async AddPermission(Permission permission);
 
     async FlushMemory(nsString reason);
 
     async GarbageCollect();
     async CycleCollect();
     async UnlinkGhosts();
--- a/dom/jsurl/nsJSProtocolHandler.cpp
+++ b/dom/jsurl/nsJSProtocolHandler.cpp
@@ -1371,26 +1371,17 @@ nsJSURI::Deserialize(const mozilla::ipc:
     return true;
 }
 
 // nsSimpleURI methods:
 /* virtual */ mozilla::net::nsSimpleURI*
 nsJSURI::StartClone(mozilla::net::nsSimpleURI::RefHandlingEnum refHandlingMode,
                     const nsACString& newRef)
 {
-    nsCOMPtr<nsIURI> baseClone;
-    if (mBaseURI) {
-      // Note: We preserve ref on *base* URI, regardless of ref handling mode.
-      nsresult rv = mBaseURI->Clone(getter_AddRefs(baseClone));
-      if (NS_FAILED(rv)) {
-        return nullptr;
-      }
-    }
-
-    nsJSURI* url = new nsJSURI(baseClone);
+    nsJSURI* url = new nsJSURI(mBaseURI);
     SetRefOnClone(url, refHandlingMode, newRef);
     return url;
 }
 
 // Queries this list of interfaces. If none match, it queries mURI.
 NS_IMPL_NSIURIMUTATOR_ISUPPORTS(nsJSURI::Mutator,
                                 nsIURISetters,
                                 nsIURIMutator,
--- a/dom/locales/en-US/chrome/security/security.properties
+++ b/dom/locales/en-US/chrome/security/security.properties
@@ -89,8 +89,10 @@ BlockScriptWithWrongMimeType=Script from “%1$S” was blocked because of a disallowed MIME type.
 BlockTopLevelDataURINavigation=Navigation to toplevel data: URI not allowed (Blocked loading of: “%1$S”)
 BlockSubresourceRedirectToData=Redirecting to insecure data: URI not allowed (Blocked loading of: “%1$S”)
 
 BlockSubresourceFTP=Loading FTP subresource within http(s) page not allowed (Blocked loading of: “%1$S”)
 
 # LOCALIZATION NOTE (BrowserUpgradeInsecureDisplayRequest):
 # %1$S is the browser name "brandShortName"; %2$S is the URL of the upgraded request; %1$S is the upgraded scheme.
 BrowserUpgradeInsecureDisplayRequest = %1$S is upgrading an insecure display request ‘%2$S’ to use ‘%3$S’
+RunningClearSiteDataValue=Clear-Site-Data header forces the clean up of “%S” data.
+UnknownClearSiteDataValue=Clear-Site-Data header found. Unknown value “%S”.
--- a/dom/media/platforms/wrappers/H264Converter.cpp
+++ b/dom/media/platforms/wrappers/H264Converter.cpp
@@ -404,38 +404,36 @@ H264Converter::DecodeFirstSample(MediaRa
            })
     ->Track(mDecodePromiseRequest);
 }
 
 MediaResult
 H264Converter::CheckForSPSChange(MediaRawData* aSample)
 {
   RefPtr<MediaByteBuffer> extra_data =
-    H264::ExtractExtraData(aSample);
+    aSample->mKeyframe ? H264::ExtractExtraData(aSample) : nullptr;
   if (!H264::HasSPS(extra_data)) {
     MOZ_ASSERT(mCanRecycleDecoder.isSome());
     if (!*mCanRecycleDecoder) {
       // If the decoder can't be recycled, the out of band extradata will never
       // change as the H264Converter will be recreated by the MediaFormatReader
       // instead. So there's no point in testing for changes.
       return NS_OK;
     }
     // This sample doesn't contain inband SPS/PPS
     // We now check if the out of band one has changed.
     // This scenario can only occur on Android with devices that can recycle a
     // decoder.
     if (!H264::HasSPS(aSample->mExtraData) ||
-        H264::CompareExtraData(aSample->mExtraData,
-                                            mOriginalExtraData)) {
+        H264::CompareExtraData(aSample->mExtraData, mOriginalExtraData)) {
       return NS_OK;
     }
     extra_data = mOriginalExtraData = aSample->mExtraData;
   }
-  if (H264::CompareExtraData(extra_data,
-                                          mCurrentConfig.mExtraData)) {
+  if (H264::CompareExtraData(extra_data, mCurrentConfig.mExtraData)) {
     return NS_OK;
   }
 
   MOZ_ASSERT(mCanRecycleDecoder.isSome());
   if (*mCanRecycleDecoder) {
     // Do not recreate the decoder, reuse it.
     UpdateConfigFromExtraData(extra_data);
     if (!aSample->mTrackInfo) {
--- a/dom/media/systemservices/VideoEngine.cpp
+++ b/dom/media/systemservices/VideoEngine.cpp
@@ -63,19 +63,35 @@ VideoEngine::CreateVideoCapture(int32_t&
 
   CaptureEntry entry = {-1, nullptr};
 
   if (mCaptureDevInfo.type == webrtc::CaptureDeviceType::Camera) {
     entry = CaptureEntry(id,
 		         webrtc::VideoCaptureFactory::Create(deviceUniqueIdUTF8));
   } else {
 #ifndef WEBRTC_ANDROID
+#ifdef MOZ_X11
+    webrtc::VideoCaptureModule* captureModule;
+    auto type = mCaptureDevInfo.type;
+    nsresult result = NS_DispatchToMainThread(media::NewRunnableFrom(
+      [&captureModule, id, deviceUniqueIdUTF8, type]() -> nsresult {
+        captureModule = webrtc::DesktopCaptureImpl::Create(id, deviceUniqueIdUTF8, type);
+        return NS_OK;
+      }), nsIEventTarget::DISPATCH_SYNC);
+
+    if (result == NS_OK) {
+      entry = CaptureEntry(id, captureModule);
+    } else {
+      return;
+    }
+#else
     entry = CaptureEntry(
 	      id,
 	      webrtc::DesktopCaptureImpl::Create(id, deviceUniqueIdUTF8, mCaptureDevInfo.type));
+#endif
 #else
     MOZ_ASSERT("CreateVideoCapture NO DESKTOP CAPTURE IMPL ON ANDROID" == nullptr);
 #endif
   }
   mCaps.emplace(id, std::move(entry));
   mIdMap.emplace(id, id);
 }
 
--- a/dom/payments/PaymentRequest.cpp
+++ b/dom/payments/PaymentRequest.cpp
@@ -644,33 +644,31 @@ already_AddRefed<Promise>
 PaymentRequest::CanMakePayment(ErrorResult& aRv)
 {
   if (mState != eCreated) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
   if (mResultPromise) {
+    // XXX This doesn't match the spec but does match Chromium.
     aRv.Throw(NS_ERROR_DOM_NOT_ALLOWED_ERR);
     return nullptr;
   }
 
-  nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+  nsIGlobalObject* global = GetOwnerGlobal();
   ErrorResult result;
   RefPtr<Promise> promise = Promise::Create(global, result);
   if (result.Failed()) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
-  if (NS_WARN_IF(!manager)) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
+  MOZ_ASSERT(manager);
   nsresult rv = manager->CanMakePayment(this);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     promise->MaybeReject(NS_ERROR_FAILURE);
     return promise.forget();
   }
   mResultPromise = promise;
   return promise.forget();
 }
@@ -682,48 +680,51 @@ PaymentRequest::RespondCanMakePayment(bo
   mResultPromise->MaybeResolve(aResult);
   mResultPromise = nullptr;
 }
 
 already_AddRefed<Promise>
 PaymentRequest::Show(const Optional<OwningNonNull<Promise>>& aDetailsPromise,
                      ErrorResult& aRv)
 {
-  if (mState != eCreated) {
-    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
-    return nullptr;
-  }
-
   if (!EventStateManager::IsHandlingUserInput()) {
     aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return nullptr;
   }
 
-  nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+  nsIGlobalObject* global = GetOwnerGlobal();
+  nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global);
+  MOZ_ASSERT(win);
+  nsIDocument* doc = win->GetExtantDoc();
+  if (!doc || !doc->IsCurrentActiveDocument()) {
+    aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
+    return nullptr;
+  }
+
+  if (mState != eCreated) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return nullptr;
+  }
+
   ErrorResult result;
   RefPtr<Promise> promise = Promise::Create(global, result);
   if (result.Failed()) {
     mState = eClosed;
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
-  RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
-  if (NS_WARN_IF(!manager)) {
-    mState = eClosed;
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
-
   if (aDetailsPromise.WasPassed()) {
     aDetailsPromise.Value().AppendNativeHandler(this);
     mUpdating = true;
     mDeferredShow = true;
   }
 
+  RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
+  MOZ_ASSERT(manager);
   nsresult rv = manager->ShowPayment(this);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     if (rv == NS_ERROR_ABORT) {
       promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
     } else {
       promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
     }
     mState = eClosed;
@@ -789,35 +790,31 @@ already_AddRefed<Promise>
 PaymentRequest::Abort(ErrorResult& aRv)
 {
   if (mState != eInteractive) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
   if (mAbortPromise) {
-    aRv.Throw(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
-  nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+  nsIGlobalObject* global = GetOwnerGlobal();
   ErrorResult result;
   RefPtr<Promise> promise = Promise::Create(global, result);
   if (result.Failed()) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
-  if (NS_WARN_IF(!manager)) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
-
-  // It's possible for to call this between show and its promise resolving.
+  MOZ_ASSERT(manager);
+  // It's possible to be called between show and its promise resolving.
   nsresult rv = manager->AbortPayment(this, mDeferredShow);
   mDeferredShow = false;
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   mAbortPromise = promise;
@@ -1055,17 +1052,19 @@ PaymentRequest::RejectedCallback(JSConte
   mUpdating = false;
   AbortUpdate(NS_ERROR_DOM_ABORT_ERR, mDeferredShow);
   mDeferredShow = false;
 }
 
 PaymentRequest::~PaymentRequest()
 {
   if (mIPC) {
-    mIPC->MaybeDelete();
+    // If we're being destroyed, the PaymentRequestManager isn't holding any
+    // references to us and we can't be waiting for any replies.
+    mIPC->MaybeDelete(false);
   }
 }
 
 JSObject*
 PaymentRequest::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return PaymentRequestBinding::Wrap(aCx, this, aGivenProto);
 }
--- a/dom/payments/PaymentRequestManager.cpp
+++ b/dom/payments/PaymentRequestManager.cpp
@@ -304,16 +304,27 @@ PaymentRequestManager::NotifyRequestDone
   MOZ_ASSERT(entry.Data() > 0);
 
   uint32_t count = --entry.Data();
   if (count == 0) {
     entry.Remove();
   }
 }
 
+void
+PaymentRequestManager::RequestIPCOver(PaymentRequest* aRequest)
+{
+  // This must only be called from ActorDestroy or if we're sure we won't
+  // receive any more IPC for aRequest.
+  mActivePayments.Remove(aRequest);
+  if (aRequest == mShowingRequest) {
+    mShowingRequest = nullptr;
+  }
+}
+
 already_AddRefed<PaymentRequestManager>
 PaymentRequestManager::GetSingleton()
 {
   if (!gPaymentManager) {
     gPaymentManager = new PaymentRequestManager();
     ClearOnShutdown(&gPaymentManager);
   }
   RefPtr<PaymentRequestManager> manager = gPaymentManager;
--- a/dom/payments/PaymentRequestManager.h
+++ b/dom/payments/PaymentRequestManager.h
@@ -59,16 +59,20 @@ public:
 
   nsresult RespondPayment(PaymentRequest* aRequest,
                           const IPCPaymentActionResponse& aResponse);
   nsresult ChangeShippingAddress(PaymentRequest* aRequest,
                                  const IPCPaymentAddress& aAddress);
   nsresult ChangeShippingOption(PaymentRequest* aRequest,
                                 const nsAString& aOption);
 
+  // Called to ensure that we don't "leak" aRequest if we shut down while it had
+  // an active request to the parent.
+  void RequestIPCOver(PaymentRequest* aRequest);
+
 private:
   PaymentRequestManager() = default;
   ~PaymentRequestManager()
   {
     MOZ_ASSERT(mActivePayments.Count() == 0);
   }
 
   PaymentRequestChild* GetPaymentChild(PaymentRequest* aRequest);
--- a/dom/payments/ipc/PaymentRequestChild.cpp
+++ b/dom/payments/ipc/PaymentRequestChild.cpp
@@ -81,43 +81,40 @@ PaymentRequestChild::RecvChangeShippingO
   }
   return IPC_OK();
 }
 
 void
 PaymentRequestChild::ActorDestroy(ActorDestroyReason aWhy)
 {
   if (mRequest) {
-    DetachFromRequest();
+    DetachFromRequest(true);
   }
 }
 
 void
-PaymentRequestChild::MaybeDelete()
+PaymentRequestChild::MaybeDelete(bool aCanBeInManager)
 {
   if (mRequest) {
-    DetachFromRequest();
+    DetachFromRequest(aCanBeInManager);
     Send__delete__(this);
   }
 }
 
-bool
-PaymentRequestChild::SendRequestPayment(const IPCPaymentActionRequest& aAction)
-{
-  return PPaymentRequestChild::SendRequestPayment(aAction);
-}
-
 void
-PaymentRequestChild::DetachFromRequest()
+PaymentRequestChild::DetachFromRequest(bool aCanBeInManager)
 {
   MOZ_ASSERT(mRequest);
-  nsAutoString id;
-  mRequest->GetInternalId(id);
 
-  RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
-  MOZ_ASSERT(manager);
+  if (aCanBeInManager) {
+    RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
+    MOZ_ASSERT(manager);
+
+    RefPtr<PaymentRequest> request(mRequest);
+    manager->RequestIPCOver(request);
+  }
 
   mRequest->SetIPC(nullptr);
   mRequest = nullptr;
 }
 
 } // end of namespace dom
 } // end of namespace mozilla
--- a/dom/payments/ipc/PaymentRequestChild.h
+++ b/dom/payments/ipc/PaymentRequestChild.h
@@ -14,17 +14,17 @@ namespace dom {
 
 class PaymentRequest;
 
 class PaymentRequestChild final : public PPaymentRequestChild
 {
 public:
   explicit PaymentRequestChild(PaymentRequest* aRequest);
 
-  void MaybeDelete();
+  void MaybeDelete(bool aCanBeInManager);
 
   nsresult RequestPayment(const IPCPaymentActionRequest& aAction);
 
 protected:
   mozilla::ipc::IPCResult
   RecvRespondPayment(const IPCPaymentActionResponse& aResponse) override;
 
   mozilla::ipc::IPCResult
@@ -35,17 +35,17 @@ protected:
   RecvChangeShippingOption(const nsString& aRequestId,
                            const nsString& aOption) override;
 
   void ActorDestroy(ActorDestroyReason aWhy) override;
 
 private:
   ~PaymentRequestChild() = default;
 
-  bool SendRequestPayment(const IPCPaymentActionRequest& aAction);
-  void DetachFromRequest();
+  void DetachFromRequest(bool aCanBeInManager);
+
   PaymentRequest* MOZ_NON_OWNING_REF mRequest;
 };
 
 } // end of namespace dom
 } // end of namespace mozilla
 
 #endif
--- a/dom/plugins/base/nsNPAPIPlugin.cpp
+++ b/dom/plugins/base/nsNPAPIPlugin.cpp
@@ -108,18 +108,18 @@ static NPNetscapeFuncs sBrowserFuncs = {
   nullptr, // _write, unimplemented
   nullptr, // _destroystream, unimplemented
   _status,
   _useragent,
   _memalloc,
   _memfree,
   _memflush,
   _reloadplugins,
-  nullptr, // _getJavaEnv, unimplemented
-  nullptr, // _getJavaPeer, unimplemented
+  _getJavaEnv,
+  _getJavaPeer,
   _geturlnotify,
   _posturlnotify,
   _getvalue,
   _setvalue,
   _invalidaterect,
   _invalidateregion,
   _forceredraw,
   _getstringidentifier,
@@ -1698,16 +1698,24 @@ NPError
 }
 
 NPError
 _requestread(NPStream *pstream, NPByteRange *rangeList)
 {
   return NPERR_STREAM_NOT_SEEKABLE;
 }
 
+// Deprecated, only stubbed out
+void* /* OJI type: JRIEnv* */
+_getJavaEnv()
+{
+  NPN_PLUGIN_LOG(PLUGIN_LOG_NORMAL, ("NPN_GetJavaEnv\n"));
+  return nullptr;
+}
+
 const char *
 _useragent(NPP npp)
 {
   if (!NS_IsMainThread()) {
     NPN_PLUGIN_LOG(PLUGIN_LOG_ALWAYS,("NPN_useragent called from the wrong thread\n"));
     return nullptr;
   }
   NPN_PLUGIN_LOG(PLUGIN_LOG_NORMAL, ("NPN_UserAgent: npp=%p\n", (void*)npp));
@@ -1731,16 +1739,24 @@ void *
 {
   if (!NS_IsMainThread()) {
     NPN_PLUGIN_LOG(PLUGIN_LOG_NORMAL,("NPN_memalloc called from the wrong thread\n"));
   }
   NPN_PLUGIN_LOG(PLUGIN_LOG_NOISY, ("NPN_MemAlloc: size=%d\n", size));
   return moz_xmalloc(size);
 }
 
+// Deprecated, only stubbed out
+void* /* OJI type: jref */
+_getJavaPeer(NPP npp)
+{
+  NPN_PLUGIN_LOG(PLUGIN_LOG_NORMAL, ("NPN_GetJavaPeer: npp=%p\n", (void*)npp));
+  return nullptr;
+}
+
 void
 _pushpopupsenabledstate(NPP npp, NPBool enabled)
 {
   if (!NS_IsMainThread()) {
     NPN_PLUGIN_LOG(PLUGIN_LOG_ALWAYS,("NPN_pushpopupsenabledstate called from the wrong thread\n"));
     return;
   }
   nsNPAPIPluginInstance *inst = npp ? (nsNPAPIPluginInstance *)npp->ndata : nullptr;
--- a/dom/plugins/base/nsNPAPIPlugin.h
+++ b/dom/plugins/base/nsNPAPIPlugin.h
@@ -274,16 +274,23 @@ void
 _forceredraw(NPP npp);
 
 const char*
 _useragent(NPP npp);
 
 void*
 _memalloc (uint32_t size);
 
+// Deprecated entry points for the old Java plugin.
+void* /* OJI type: JRIEnv* */
+_getJavaEnv();
+
+void* /* OJI type: jref */
+_getJavaPeer(NPP npp);
+
 void
 _urlredirectresponse(NPP instance, void* notifyData, NPBool allow);
 
 NPError
 _initasyncsurface(NPP instance, NPSize *size, NPImageFormat format, void *initData, NPAsyncSurface *surface);
 
 NPError
 _finalizeasyncsurface(NPP instance, NPAsyncSurface *surface);
--- a/dom/plugins/ipc/PluginModuleChild.cpp
+++ b/dom/plugins/ipc/PluginModuleChild.cpp
@@ -870,16 +870,24 @@ static void
 _forceredraw(NPP aNPP);
 
 static const char*
 _useragent(NPP aNPP);
 
 static void*
 _memalloc (uint32_t size);
 
+// Deprecated entry points for the old Java plugin.
+static void* /* OJI type: JRIEnv* */
+_getjavaenv(void);
+
+// Deprecated entry points for the old Java plugin.
+static void* /* OJI type: jref */
+_getjavapeer(NPP aNPP);
+
 static bool
 _invoke(NPP aNPP, NPObject* npobj, NPIdentifier method, const NPVariant *args,
         uint32_t argCount, NPVariant *result);
 
 static bool
 _invokedefault(NPP aNPP, NPObject* npobj, const NPVariant *args,
                uint32_t argCount, NPVariant *result);
 
@@ -974,18 +982,18 @@ const NPNetscapeFuncs PluginModuleChild:
     nullptr, // _write, unimplemented
     nullptr, // _destroystream, unimplemented
     mozilla::plugins::child::_status,
     mozilla::plugins::child::_useragent,
     mozilla::plugins::child::_memalloc,
     mozilla::plugins::child::_memfree,
     mozilla::plugins::child::_memflush,
     mozilla::plugins::child::_reloadplugins,
-    nullptr, // _getjavaenv, unimplemented
-    nullptr, // _getjavapeer, unimplemented
+    mozilla::plugins::child::_getjavaenv,
+    mozilla::plugins::child::_getjavapeer,
     mozilla::plugins::child::_geturlnotify,
     mozilla::plugins::child::_posturlnotify,
     mozilla::plugins::child::_getvalue,
     mozilla::plugins::child::_setvalue,
     mozilla::plugins::child::_invalidaterect,
     mozilla::plugins::child::_invalidateregion,
     mozilla::plugins::child::_forceredraw,
     PluginModuleChild::NPN_GetStringIdentifier,
@@ -1291,16 +1299,31 @@ const char*
 
 void*
 _memalloc(uint32_t aSize)
 {
     PLUGIN_LOG_DEBUG_FUNCTION;
     return moz_xmalloc(aSize);
 }
 
+// Deprecated entry points for the old Java plugin.
+void* /* OJI type: JRIEnv* */
+_getjavaenv(void)
+{
+    PLUGIN_LOG_DEBUG_FUNCTION;
+    return 0;
+}
+
+void* /* OJI type: jref */
+_getjavapeer(NPP aNPP)
+{
+    PLUGIN_LOG_DEBUG_FUNCTION;
+    return 0;
+}
+
 bool
 _invoke(NPP aNPP,
         NPObject* aNPObj,
         NPIdentifier aMethod,
         const NPVariant* aArgs,
         uint32_t aArgCount,
         NPVariant* aResult)
 {
--- a/dom/security/nsContentSecurityManager.cpp
+++ b/dom/security/nsContentSecurityManager.cpp
@@ -175,30 +175,36 @@ nsContentSecurityManager::CheckFTPSubres
     return NS_OK;
   }
 
   nsContentPolicyType type = loadInfo->GetExternalContentPolicyType();
   if (type == nsIContentPolicy::TYPE_DOCUMENT) {
     return NS_OK;
   }
 
+  // Allow the system principal to load everything. This is meant to
+  // temporarily fix downloads and pdf.js.
+  nsIPrincipal* triggeringPrincipal = loadInfo->TriggeringPrincipal();
+  if (nsContentUtils::IsSystemPrincipal(triggeringPrincipal)) {
+    return NS_OK;
+  }
+
   nsCOMPtr<nsIURI> uri;
   nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
   NS_ENSURE_SUCCESS(rv, rv);
   if (!uri) {
     return NS_OK;
   }
 
   bool isFtpURI = (NS_SUCCEEDED(uri->SchemeIs("ftp", &isFtpURI)) && isFtpURI);
   if (!isFtpURI) {
     return NS_OK;
   }
 
   // Allow loading FTP subresources in FTP documents, like XML.
-  nsIPrincipal* triggeringPrincipal = loadInfo->TriggeringPrincipal();
   nsCOMPtr<nsIURI> triggeringURI;
   triggeringPrincipal->GetURI(getter_AddRefs(triggeringURI));
   if (triggeringURI && nsContentUtils::SchemeIs(triggeringURI, "ftp")) {
     return NS_OK;
   }
 
   nsCOMPtr<nsIDocument> doc;
   if (nsINode* node = loadInfo->LoadingNode()) {
--- a/dom/tests/moz.build
+++ b/dom/tests/moz.build
@@ -3,17 +3,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files("browser/**"):
     BUG_COMPONENT = ("Core", "DOM")
 
 with Files("browser/*onsole*"):
-    BUG_COMPONENT = ("Firefox", "Developer Tools")
+    BUG_COMPONENT = ("DevTools", "General")
 
 with Files("browser/*autofocus*"):
     BUG_COMPONENT = ("Core", "DOM: Core & HTML")
 
 with Files("browser/*unload*"):
     BUG_COMPONENT = ("Core", "DOM: Events")
 
 with Files("browser/*1238427*"):
--- a/dom/vr/VRDisplay.cpp
+++ b/dom/vr/VRDisplay.cpp
@@ -258,27 +258,25 @@ VRDisplayCapabilities::WrapObject(JSCont
 {
   return VRDisplayCapabilitiesBinding::Wrap(aCx, this, aGivenProto);
 }
 
 VRPose::VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState)
   : Pose(aParent)
   , mVRState(aState)
 {
-  mFrameId = aState.inputFrameID;
   mozilla::HoldJSObjects(this);
 }
 
 VRPose::VRPose(nsISupports* aParent)
   : Pose(aParent)
 {
   mVRState.inputFrameID = 0;
   mVRState.timestamp = 0.0;
   mVRState.flags = gfx::VRDisplayCapabilityFlags::Cap_None;
-  mFrameId = 0;
   mozilla::HoldJSObjects(this);
 }
 
 VRPose::~VRPose()
 {
   mozilla::DropJSObjects(this);
 }
 
--- a/dom/vr/VRDisplay.h
+++ b/dom/vr/VRDisplay.h
@@ -98,18 +98,16 @@ protected:
 
 class VRPose final : public Pose
 {
 
 public:
   VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState);
   explicit VRPose(nsISupports* aParent);
 
-  uint64_t FrameID() const { return mFrameId; }
-
   virtual void GetPosition(JSContext* aCx,
                            JS::MutableHandle<JSObject*> aRetval,
                            ErrorResult& aRv) override;
   virtual void GetLinearVelocity(JSContext* aCx,
                                  JS::MutableHandle<JSObject*> aRetval,
                                  ErrorResult& aRv) override;
   virtual void GetLinearAcceleration(JSContext* aCx,
                                      JS::MutableHandle<JSObject*> aRetval,
@@ -124,17 +122,16 @@ public:
                                       JS::MutableHandle<JSObject*> aRetval,
                                       ErrorResult& aRv) override;
 
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
 protected:
   ~VRPose();
 
-  uint64_t mFrameId;
   gfx::VRHMDSensorState mVRState;
 };
 
 struct VRFrameInfo
 {
   VRFrameInfo();
 
   void Update(const gfx::VRDisplayInfo& aInfo,
--- a/dom/webauthn/WebAuthnManager.cpp
+++ b/dom/webauthn/WebAuthnManager.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "hasht.h"
 #include "nsHTMLDocument.h"
+#include "nsIURIMutator.h"
 #include "nsThreadUtils.h"
 #include "WebAuthnCoseIdentifiers.h"
 #include "mozilla/dom/AuthenticatorAttestationResponse.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PWebAuthnTransaction.h"
 #include "mozilla/dom/WebAuthnManager.h"
 #include "mozilla/dom/WebAuthnTransactionChild.h"
 #include "mozilla/dom/WebAuthnUtil.h"
@@ -119,21 +120,37 @@ RelaxSameOrigin(nsPIDOMWindowInner* aPar
   if (NS_FAILED(uri->GetAsciiHost(originHost))) {
     return NS_ERROR_FAILURE;
   }
   nsCOMPtr<nsIDocument> document = aParent->GetDoc();
   if (!document || !document->IsHTMLDocument()) {
     return NS_ERROR_FAILURE;
   }
   nsHTMLDocument* html = document->AsHTMLDocument();
-  if (!html->IsRegistrableDomainSuffixOfOrEqualTo(aInputRpId, originHost)) {
+  // See if the given RP ID is a valid domain string.
+  // (We use the document's URI here as a template so we don't have to come up
+  // with our own scheme, etc. If we can successfully set the host as the given
+  // RP ID, then it should be a valid domain string.)
+  nsCOMPtr<nsIURI> inputRpIdURI;
+  nsresult rv = NS_MutateURI(uri)
+         .SetHost(NS_ConvertUTF16toUTF8(aInputRpId))
+         .Finalize(inputRpIdURI);
+  if (NS_FAILED(rv)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+  nsAutoCString inputRpId;
+  if (NS_FAILED(inputRpIdURI->GetAsciiHost(inputRpId))) {
+    return NS_ERROR_FAILURE;
+  }
+  if (!html->IsRegistrableDomainSuffixOfOrEqualTo(
+      NS_ConvertUTF8toUTF16(inputRpId), originHost)) {
     return NS_ERROR_DOM_SECURITY_ERR;
   }
 
-  aRelaxedRpId.Assign(NS_ConvertUTF16toUTF8(aInputRpId));
+  aRelaxedRpId.Assign(inputRpId);
   return NS_OK;
 }
 
 /***********************************************************************
  * WebAuthnManager Implementation
  **********************************************************************/
 
 void
--- a/dom/webauthn/tests/test_webauthn_make_credential.html
+++ b/dom/webauthn/tests/test_webauthn_make_credential.html
@@ -244,16 +244,38 @@
             rp: rp, user: user, challenge: gCredentialChallenge,
             pubKeyCredParams: [param, param, param]
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsGood)
                       .catch(arrivingHereIsBad);
         },
 
+        // Test with an RP ID that is not a valid domain string
+        function() {
+          let rp = { id: document.domain + ":somejunk", name: "none", icon: "none" };
+          let makeCredentialOptions = {
+            rp: rp, user: user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+          };
+          return credm.create({publicKey: makeCredentialOptions})
+                      .then(arrivingHereIsBad)
+                      .catch(arrivingHereIsGood);
+        },
+
+        // Test with another RP ID that is not a valid domain string
+        function() {
+          let rp = { id: document.domain + ":8888", name: "none", icon: "none" };
+          let makeCredentialOptions = {
+            rp: rp, user: user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+          };
+          return credm.create({publicKey: makeCredentialOptions})
+                      .then(arrivingHereIsBad)
+                      .catch(arrivingHereIsGood);
+        },
+
         // Test with missing rp
         function() {
           let makeCredentialOptions = {
             user: user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectTypeError);
--- a/dom/webauthn/tests/test_webauthn_sameorigin.html
+++ b/dom/webauthn/tests/test_webauthn_sameorigin.html
@@ -256,17 +256,39 @@
           let publicKeyCredentialRequestOptions = {
             challenge: chall,
             rpId: window.origin,
             allowCredentials: [gTrackedCredential["basic"]]
           };
           return credm.get({publicKey: publicKeyCredentialRequestOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
-        }
+        },
+        function () {
+          // Test with an rpId that is not a valid domain string
+          let publicKeyCredentialRequestOptions = {
+            challenge: chall,
+            rpId: document.domain + ":somejunk",
+            allowCredentials: [gTrackedCredential["basic"]]
+          };
+          return credm.get({publicKey: publicKeyCredentialRequestOptions})
+                      .then(arrivingHereIsBad)
+                      .catch(arrivingHereIsGood);
+        },
+        function () {
+          // Test with another rpId that is not a valid domain string
+          let publicKeyCredentialRequestOptions = {
+            challenge: chall,
+            rpId: document.domain + ":8888",
+            allowCredentials: [gTrackedCredential["basic"]]
+          };
+          return credm.get({publicKey: publicKeyCredentialRequestOptions})
+                      .then(arrivingHereIsBad)
+                      .catch(arrivingHereIsGood);
+        },
       ];
       var i = 0;
       var runNextTest = () => {
         if (i == testFuncs.length) {
           SimpleTest.finish();
           return;
         }
         console.log(i, testFuncs[i], testFuncs.length);
--- a/dom/webbrowserpersist/nsWebBrowserPersist.cpp
+++ b/dom/webbrowserpersist/nsWebBrowserPersist.cpp
@@ -614,21 +614,17 @@ nsWebBrowserPersist::SerializeNextFile()
             nsCOMPtr<nsIURI> uri;
             rv = NS_NewURI(getter_AddRefs(uri), iter.Key(),
                            data->mCharset.get());
             if (NS_WARN_IF(NS_FAILED(rv))) {
                 break;
             }
 
             // Make a URI to save the data to.
-            nsCOMPtr<nsIURI> fileAsURI;
-            rv = data->mDataPath->Clone(getter_AddRefs(fileAsURI));
-            if (NS_WARN_IF(NS_FAILED(rv))) {
-                break;
-            }
+            nsCOMPtr<nsIURI> fileAsURI = data->mDataPath;
             rv = AppendPathToURI(fileAsURI, data->mFilename, fileAsURI);
             if (NS_WARN_IF(NS_FAILED(rv))) {
                 break;
             }
 
             // The Referrer Policy doesn't matter here since the referrer is
             // nullptr.
             rv = SaveURIInternal(uri, 0, nullptr,
@@ -2523,21 +2519,19 @@ nsWebBrowserPersist::URIData::GetLocalUR
 {
     aSpecOut.SetIsVoid(true);
     if (!mNeedsFixup) {
         return NS_OK;
     }
     nsresult rv;
     nsCOMPtr<nsIURI> fileAsURI;
     if (mFile) {
-        rv = mFile->Clone(getter_AddRefs(fileAsURI));
-        NS_ENSURE_SUCCESS(rv, rv);
+        fileAsURI = mFile;
     } else {
-        rv = mDataPath->Clone(getter_AddRefs(fileAsURI));
-        NS_ENSURE_SUCCESS(rv, rv);
+        fileAsURI = mDataPath;
         rv = AppendPathToURI(fileAsURI, mFilename, fileAsURI);
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
     // remove username/password if present
     Unused << NS_MutateURI(fileAsURI)
                 .SetUserPass(EmptyCString())
                 .Finalize(fileAsURI);
@@ -2650,26 +2644,22 @@ nsWebBrowserPersist::SaveSubframeContent
         aData->mSubFrameExt.Assign(char16_t('.'));
         aData->mSubFrameExt.Append(ext);
     }
 
     nsString filenameWithExt = aData->mFilename;
     filenameWithExt.Append(aData->mSubFrameExt);
 
     // Work out the path for the subframe
-    nsCOMPtr<nsIURI> frameURI;
-    rv = mCurrentDataPath->Clone(getter_AddRefs(frameURI));
-    NS_ENSURE_SUCCESS(rv, rv);
+    nsCOMPtr<nsIURI> frameURI = mCurrentDataPath;
     rv = AppendPathToURI(frameURI, filenameWithExt, frameURI);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Work out the path for the subframe data
-    nsCOMPtr<nsIURI> frameDataURI;
-    rv = mCurrentDataPath->Clone(getter_AddRefs(frameDataURI));
-    NS_ENSURE_SUCCESS(rv, rv);
+    nsCOMPtr<nsIURI> frameDataURI = mCurrentDataPath;
     nsAutoString newFrameDataPath(aData->mFilename);
 
     // Append _data
     newFrameDataPath.AppendLiteral("_data");
     rv = AppendPathToURI(frameDataURI, newFrameDataPath, frameDataURI);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Make frame document & data path conformant and unique
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -237,17 +237,17 @@ with Files("PointerEvent.webidl"):
 
 with Files("PopStateEvent.webidl*"):
     BUG_COMPONENT = ("Core", "DOM: Events")
 
 with Files("Position*"):
     BUG_COMPONENT = ("Core", "Geolocation")
 
 with Files("ProfileTimelineMarker.webidl"):
-    BUG_COMPONENT = ("Firefox", "Developer Tools: Performance Tools (profiler/timeline)")
+    BUG_COMPONENT = ("DevTools", "Performance Tools (Profiler/Timeline)")
 
 with Files("ProgressEvent.webidl"):
     BUG_COMPONENT = ("Core", "DOM: Events")
 
 with Files("Push*"):
     BUG_COMPONENT = ("Core", "DOM: Push Notifications")
 
 with Files("RTC*"):
@@ -273,17 +273,17 @@ with Files("SocketCommon.webidl"):
 
 with Files("SourceBuffer*"):
     BUG_COMPONENT = ("Core", "Audio/Video")
 
 with Files("StereoPannerNode.webidl"):
     BUG_COMPONENT = ("Core", "Web Audio")
 
 with Files("StreamFilter*"):
-    BUG_COMPONENT = ("Toolkit", "WebExtensions: Request Handling")
+    BUG_COMPONENT = ("WebExtensions", "Request Handling")
 
 with Files("Style*"):
     BUG_COMPONENT = ("Core", "DOM: CSS Object Model")
 
 with Files("SubtleCrypto.webidl"):
     BUG_COMPONENT = ("Core", "DOM: Security")
 
 with Files("TCP*"):
--- a/dom/workers/RuntimeService.cpp
+++ b/dom/workers/RuntimeService.cpp
@@ -303,19 +303,17 @@ LoadContextOptions(const char* aPrefName
                 .setIon(GetWorkerPref<bool>(NS_LITERAL_CSTRING("ion")))
                 .setNativeRegExp(GetWorkerPref<bool>(NS_LITERAL_CSTRING("native_regexp")))
                 .setAsyncStack(GetWorkerPref<bool>(NS_LITERAL_CSTRING("asyncstack")))
                 .setWerror(GetWorkerPref<bool>(NS_LITERAL_CSTRING("werror")))
 #ifdef FUZZING
                 .setFuzzing(GetWorkerPref<bool>(NS_LITERAL_CSTRING("fuzzing.enabled")))
 #endif
                 .setStreams(GetWorkerPref<bool>(NS_LITERAL_CSTRING("streams")))
-                .setExtraWarnings(GetWorkerPref<bool>(NS_LITERAL_CSTRING("strict")))
-                .setArrayProtoValues(GetWorkerPref<bool>(
-                      NS_LITERAL_CSTRING("array_prototype_values")));
+                .setExtraWarnings(GetWorkerPref<bool>(NS_LITERAL_CSTRING("strict")));
 
   nsCOMPtr<nsIXULRuntime> xr = do_GetService("@mozilla.org/xre/runtime;1");
   if (xr) {
     bool safeMode = false;
     xr->GetInSafeMode(&safeMode);
     if (safeMode) {
       contextOptions.disableOptionsForSafeMode();
     }
--- a/dom/xbl/nsXBLPrototypeBinding.cpp
+++ b/dom/xbl/nsXBLPrototypeBinding.cpp
@@ -131,25 +131,24 @@ nsXBLPrototypeBinding::Init(const nsACSt
                             bool aFirstBinding)
 {
   nsresult rv;
   nsCOMPtr<nsIURI> bindingURI = aInfo->DocumentURI();
 
   // The binding URI might be an immutable URI (e.g. for about: URIs). In that case,
   // we'll fail in SetRef below, but that doesn't matter much for now.
   if (aFirstBinding) {
-    rv = bindingURI->Clone(getter_AddRefs(mAlternateBindingURI));
-    NS_ENSURE_SUCCESS(rv, rv);
+    mAlternateBindingURI = bindingURI;
   }
   rv = NS_MutateURI(bindingURI)
         .SetRef(aID)
         .Finalize(mBindingURI);
   if (NS_FAILED(rv)) {
     // If SetRef failed, mBindingURI should be a clone.
-    bindingURI->Clone(getter_AddRefs(mBindingURI));
+    mBindingURI = bindingURI;
   }
 
   mXBLDocInfoWeak = aInfo;
 
   // aElement will be null when reading from the cache, but the element will
   // still be set later.
   if (aElement) {
     SetBindingElement(aElement);
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -2038,25 +2038,16 @@ APZCTreeManager::SetTargetAPZC(uint64_t 
   for (size_t i = 1; i < aTargets.Length(); i++) {
     RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aTargets[i]);
     target = GetMultitouchTarget(target, apzc);
   }
   mInputQueue->SetConfirmedTargetApzc(aInputBlockId, target);
 }
 
 void
-APZCTreeManager::SetTargetAPZC(uint64_t aInputBlockId, const ScrollableLayerGuid& aTarget)
-{
-  APZThreadUtils::AssertOnControllerThread();
-
-  RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aTarget);
-  mInputQueue->SetConfirmedTargetApzc(aInputBlockId, apzc);
-}
-
-void
 APZCTreeManager::UpdateZoomConstraints(const ScrollableLayerGuid& aGuid,
                                        const Maybe<ZoomConstraints>& aConstraints)
 {
   if (!GetUpdater()->IsUpdaterThread()) {
     // This can happen if we're in the UI process and got a call directly from
     // nsBaseWidget or from a content process over PAPZCTreeManager. In that case
     // we get this call on the compositor thread, which may be different from
     // the updater thread. It can also happen in the GPU process if that is
--- a/gfx/layers/apz/src/APZCTreeManager.h
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -316,22 +316,16 @@ public:
    *       in the drag block may be handled as no-ops until the drag metrics
    *       arrive.
    */
   void SetTargetAPZC(
       uint64_t aInputBlockId,
       const nsTArray<ScrollableLayerGuid>& aTargets) override;
 
   /**
-   * Helper function for SetTargetAPZC when used with single-target events,
-   * such as mouse wheel events.
-   */
-  void SetTargetAPZC(uint64_t aInputBlockId, const ScrollableLayerGuid& aTarget);
-
-  /**
    * Updates any zoom constraints contained in the <meta name="viewport"> tag.
    * If the |aConstraints| is Nothing() then previously-provided constraints for
    * the given |aGuid| are cleared.
    */
   void UpdateZoomConstraints(
       const ScrollableLayerGuid& aGuid,
       const Maybe<ZoomConstraints>& aConstraints) override;
 
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>APZ hit-testing with nested inactive transforms (bug 1459696)</title>
+  <script type="application/javascript" src="apz_test_utils.js"></script>
+  <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <meta name="viewport" content="width=device-width"/>
+  <style>
+    .pane {
+        position: fixed;
+        top: 0;
+        bottom: 0;
+    }
+    .left {
+        left: 0;
+        right: 66vw;
+        overflow: auto;
+    }
+    .content {
+        width: 100%;
+        height: 200%;
+        background-image: linear-gradient(blue, green);
+    }
+    .right {
+        left: 34vw;
+        right: 0;
+    }
+    .list {
+        overflow: hidden;
+        transform: translate3d(0, 0, 0);
+        height: 100%;
+    }
+    .track {
+        height: 100%;
+        width: 2000px;
+        transform: translate3d(-856px, 0px, 0px);
+    }
+    .slide {
+        float: left;
+        height: 100%;
+        width: 856px;
+        background-image: linear-gradient(red, yellow);
+    }
+    </style>
+</head>
+<body>
+  <div class="left pane" id="left-pane">
+      <div class="content"></div>
+  </div>
+  <div class="right pane">
+      <div class="list">
+          <div class="track">
+              <div class="slide"></div>
+              <div class="slide"></div>
+          </div>
+      </div>
+  </div>
+</body>
+<script type="application/javascript">
+
+function* test(testDriver) {
+  var utils = getHitTestConfig().utils;
+
+  var leftPane = document.getElementById("left-pane");
+
+  checkHitResult(
+    hitTest(centerOf(leftPane)),
+    APZHitResultFlags.VISIBLE,
+    utils.getViewId(leftPane),
+    "left pane was successfully hit");
+
+  subtestDone();
+}
+
+waitUntilApzStable().then(runContinuation(test));
+
+</script>
+</html>
--- a/gfx/layers/apz/test/mochitest/test_group_hittest.html
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest.html
@@ -28,17 +28,18 @@ var prefs = [
 
 var subtests = [
   {'file': 'helper_hittest_basic.html', 'prefs': prefs},
   {'file': 'helper_hittest_fixed_in_scrolled_transform.html', 'prefs': prefs},
   {'file': 'helper_hittest_float_bug1434846.html', 'prefs': prefs},
   {'file': 'helper_hittest_float_bug1443518.html', 'prefs': prefs},
   {'file': 'helper_hittest_checkerboard.html', 'prefs': prefs},
   {'file': 'helper_hittest_backface_hidden.html', 'prefs': prefs},
-  {'file': 'helper_hittest_touchaction.html', 'prefs': prefs}
+  {'file': 'helper_hittest_touchaction.html', 'prefs': prefs},
+  {'file': 'helper_hittest_nested_transforms_bug1459696.html', 'prefs': prefs}
 ];
 
 if (isApzEnabled()) {
   SimpleTest.waitForExplicitFinish();
   window.onload = function() {
     runSubtestsSeriallyInFreshWindows(subtests)
     .then(SimpleTest.finish, SimpleTest.finish);
   };
--- a/gfx/layers/wr/ClipManager.cpp
+++ b/gfx/layers/wr/ClipManager.cpp
@@ -86,17 +86,17 @@ void
 ClipManager::PushOverrideForASR(const ActiveScrolledRoot* aASR,
                                 const Maybe<wr::WrClipId>& aClipId)
 {
   layers::FrameMetrics::ViewID viewId = aASR
       ? aASR->GetViewId() : layers::FrameMetrics::NULL_SCROLL_ID;
   Maybe<wr::WrClipId> scrollId = mBuilder->GetScrollIdForDefinedScrollLayer(viewId);
   MOZ_ASSERT(scrollId.isSome());
 
-  CLIP_LOG("Pushing override %" PRIu64 " -> %s\n", scrollId->id,
+  CLIP_LOG("Pushing override %zu -> %s\n", scrollId->id,
       aClipId ? Stringify(aClipId->id).c_str() : "(none)");
   auto it = mASROverride.insert({ *scrollId, std::stack<Maybe<wr::WrClipId>>() });
   it.first->second.push(aClipId);
 
   // Start a new cache
   mCacheStack.emplace();
 }
 
@@ -109,17 +109,17 @@ ClipManager::PopOverrideForASR(const Act
   layers::FrameMetrics::ViewID viewId = aASR
       ? aASR->GetViewId() : layers::FrameMetrics::NULL_SCROLL_ID;
   Maybe<wr::WrClipId> scrollId = mBuilder->GetScrollIdForDefinedScrollLayer(viewId);
   MOZ_ASSERT(scrollId.isSome());
 
   auto it = mASROverride.find(*scrollId);
   MOZ_ASSERT(it != mASROverride.end());
   MOZ_ASSERT(!(it->second.empty()));
-  CLIP_LOG("Popping override %" PRIu64 " -> %s\n", scrollId->id,
+  CLIP_LOG("Popping override %zu -> %s\n", scrollId->id,
       it->second.top() ? Stringify(it->second.top()->id).c_str() : "(none)");
   it->second.pop();
   if (it->second.empty()) {
     mASROverride.erase(it);
   }
 }
 
 Maybe<wr::WrClipId>
@@ -128,17 +128,17 @@ ClipManager::ClipIdAfterOverride(const M
   if (!aClipId) {
     return Nothing();
   }
   auto it = mASROverride.find(*aClipId);
   if (it == mASROverride.end()) {
     return aClipId;
   }
   MOZ_ASSERT(!it->second.empty());
-  CLIP_LOG("Overriding %" PRIu64 " with %s\n", aClipId->id,
+  CLIP_LOG("Overriding %zu with %s\n", aClipId->id,
       it->second.top() ? Stringify(it->second.top()->id).c_str() : "(none)");
   return it->second.top();
 }
 
 void
 ClipManager::BeginItem(nsDisplayItem* aItem,
                        const StackingContextHelper& aStackingContext)
 {
@@ -304,17 +304,17 @@ ClipManager::DefineClipChain(const Displ
   // Iterate through the clips in the current item's clip chain, define them
   // in WR, and put their IDs into |clipIds|.
   for (const DisplayItemClipChain* chain = aChain; chain; chain = chain->mParent) {
     ClipIdMap& cache = mCacheStack.top();
     auto it = cache.find(chain);
     if (it != cache.end()) {
       // Found it in the currently-active cache, so just use the id we have for
       // it.
-      CLIP_LOG("cache[%p] => %" PRIu64 "\n", chain, it->second.id);
+      CLIP_LOG("cache[%p] => %zu\n", chain, it->second.id);
       clipIds.AppendElement(it->second);
       continue;
     }
     if (!chain->mClip.HasClip()) {
       // This item in the chain is a no-op, skip over it
       continue;
     }
 
@@ -334,17 +334,17 @@ ClipManager::DefineClipChain(const Displ
 
     // Define the clip
     Maybe<wr::WrClipId> parent = ClipIdAfterOverride(scrollId);
     wr::WrClipId clipId = mBuilder->DefineClip(
         parent,
         wr::ToRoundedLayoutRect(clip), &wrRoundedRects);
     clipIds.AppendElement(clipId);
     cache[chain] = clipId;
-    CLIP_LOG("cache[%p] <= %" PRIu64 "\n", chain, clipId.id);
+    CLIP_LOG("cache[%p] <= %zu\n", chain, clipId.id);
   }
 
   // Now find the parent display item's clipchain id
   Maybe<wr::WrClipChainId> parentChainId;
   if (!mItemClipStack.empty()) {
     parentChainId = mItemClipStack.top().mClipChainId;
   }
 
--- a/gfx/qcms/iccread.c
+++ b/gfx/qcms/iccread.c
@@ -509,16 +509,17 @@ static void read_nested_curveType(struct
 	uint32_t channel_offset = 0;
 	int i;
 	for (i = 0; i < num_channels; i++) {
 		uint32_t tag_len;
 
 		(*curveArray)[i] = read_curveType(src, curve_offset + channel_offset, &tag_len);
 		if (!(*curveArray)[i]) {
 			invalid_source(src, "invalid nested curveType curve");
+			break;
 		}
 
 		channel_offset += tag_len;
 		// 4 byte aligned
 		if ((tag_len % 4) != 0)
 			channel_offset += 4 - (tag_len % 4);
 	}
 
--- a/gfx/src/nsThemeConstants.h
+++ b/gfx/src/nsThemeConstants.h
@@ -175,16 +175,19 @@ enum ThemeWidgetType : uint8_t {
 
   // The scrollbar thumb
   NS_THEME_SCROLLBARTHUMB_HORIZONTAL,
   NS_THEME_SCROLLBARTHUMB_VERTICAL,
 
   // A non-disappearing scrollbar.
   NS_THEME_SCROLLBAR_NON_DISAPPEARING,
 
+  // The scroll corner
+  NS_THEME_SCROLLCORNER,
+
   // A textfield or text area
   NS_THEME_TEXTFIELD,
 
   // The caret of a text area
   NS_THEME_CARET,
 
   // A multiline text field
   NS_THEME_TEXTFIELD_MULTILINE,
--- a/gfx/thebes/gfxBlur.cpp
+++ b/gfx/thebes/gfxBlur.cpp
@@ -11,16 +11,18 @@
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/Blur.h"
 #include "mozilla/gfx/PathHelpers.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/SystemGroup.h"
 #include "nsExpirationTracker.h"
 #include "nsClassHashtable.h"
 #include "gfxUtils.h"
+#include <limits>
+#include <cmath>
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 
 gfxAlphaBoxBlur::gfxAlphaBoxBlur()
   : mData(nullptr),
     mAccelerated(false)
 {
@@ -948,16 +950,26 @@ DrawMirroredMinBoxShadow(DrawTarget* aDe
 gfxAlphaBoxBlur::BlurRectangle(gfxContext* aDestinationCtx,
                                const gfxRect& aRect,
                                const RectCornerRadii* aCornerRadii,
                                const gfxPoint& aBlurStdDev,
                                const Color& aShadowColor,
                                const gfxRect& aDirtyRect,
                                const gfxRect& aSkipRect)
 {
+  const double maxSize = (double)gfxPlatform::MaxTextureSize();
+  const double maxPos = (double)std::numeric_limits<std::int16_t>::max();
+  if (aRect.width > maxSize || aRect.height > maxSize ||
+      std::abs(aRect.x) > maxPos || std::abs(aRect.y) > maxPos) {
+    // The rectangle is huge, perhaps due to a very strong perspective or some other
+    // transform. We won't be able to blur something this big so give up now before
+    // overflowing or running into texture size limits later.
+    return;
+  }
+
   IntSize blurRadius = CalculateBlurRadius(aBlurStdDev);
   bool mirrorCorners = !aCornerRadii || aCornerRadii->AreRadiiSame();
 
   IntRect rect = RoundedToInt(ToRect(aRect));
   IntMargin blurMargin;
   IntMargin slice;
   IntSize minSize;
   RefPtr<SourceSurface> boxShadow = GetBlur(aDestinationCtx,
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -1057,17 +1057,17 @@ gfxPlatform::InitLayersIPC()
   }
   sLayersIPCIsUp = true;
 
   if (XRE_IsContentProcess()) {
     if (gfxVars::UseOMTP()) {
       layers::PaintThread::Start();
     }
   } else if (XRE_IsParentProcess()) {
-    if (gfxVars::UseWebRender()) {
+    if (!gfxConfig::IsEnabled(Feature::GPU_PROCESS) && gfxVars::UseWebRender()) {
       wr::RenderThread::Start();
     }
 
     layers::CompositorThreadHolder::Start();
     gfx::VRListenerThreadHolder::Start();
   }
 }
 
--- a/gfx/webrender_bindings/RenderD3D11TextureHostOGL.cpp
+++ b/gfx/webrender_bindings/RenderD3D11TextureHostOGL.cpp
@@ -159,17 +159,17 @@ RenderDXGITextureHostOGL::Lock(uint8_t a
   }
 
   if (!EnsureLockable()) {
     return InvalidToWrExternalImage();
   }
 
   if (!mLocked) {
     if (mKeyedMutex) {
-      HRESULT hr = mKeyedMutex->AcquireSync(0, 100);
+      HRESULT hr = mKeyedMutex->AcquireSync(0, 10000);
       if (hr != S_OK) {
         gfxCriticalError() << "RenderDXGITextureHostOGL AcquireSync timeout, hr=" << gfx::hexa(hr);
         return InvalidToWrExternalImage();
       }
     }
     mLocked = true;
   }
 
@@ -352,17 +352,17 @@ RenderDXGIYCbCrTextureHostOGL::Lock(uint
 
   if (!EnsureLockable()) {
     return InvalidToWrExternalImage();
   }
 
   if (!mLocked) {
     if (mKeyedMutexs[0]) {
       for (const auto& mutex : mKeyedMutexs) {
-        HRESULT hr = mutex->AcquireSync(0, 100);
+        HRESULT hr = mutex->AcquireSync(0, 10000);
         if (hr != S_OK) {
           gfxCriticalError() << "RenderDXGIYCbCrTextureHostOGL AcquireSync timeout, hr=" << gfx::hexa(hr);
           return InvalidToWrExternalImage();
         }
       }
     }
     mLocked = true;
   }
--- a/image/ImageCacheKey.cpp
+++ b/image/ImageCacheKey.cpp
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ImageCacheKey.h"
 
 #include "mozilla/HashFunctions.h"
 #include "mozilla/Move.h"
+#include "nsContentUtils.h"
 #include "nsLayoutUtils.h"
 #include "nsString.h"
 #include "mozilla/dom/BlobURLProtocolHandler.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/ServiceWorkerManager.h"
 #include "nsIDocument.h"
 #include "nsPrintfCString.h"
 
@@ -37,17 +38,17 @@ BlobSerial(nsIURI* aURI)
 }
 
 ImageCacheKey::ImageCacheKey(nsIURI* aURI,
                              const OriginAttributes& aAttrs,
                              nsIDocument* aDocument,
                              nsresult& aRv)
   : mURI(aURI)
   , mOriginAttributes(aAttrs)
-  , mControlledDocument(GetControlledDocumentToken(aDocument))
+  , mControlledDocument(GetSpecialCaseDocumentToken(aDocument, aURI))
   , mHash(0)
   , mIsChrome(false)
 {
   if (SchemeIs("blob")) {
     mBlobSerial = BlobSerial(mURI);
   } else if (SchemeIs("chrome")) {
     mIsChrome = true;
   }
@@ -120,26 +121,35 @@ ImageCacheKey::operator==(const ImageCac
 bool
 ImageCacheKey::SchemeIs(const char* aScheme)
 {
   bool matches = false;
   return NS_SUCCEEDED(mURI->SchemeIs(aScheme, &matches)) && matches;
 }
 
 /* static */ void*
-ImageCacheKey::GetControlledDocumentToken(nsIDocument* aDocument)
+ImageCacheKey::GetSpecialCaseDocumentToken(nsIDocument* aDocument, nsIURI* aURI)
 {
-  // For non-controlled documents, we just return null.  For controlled
-  // documents, we cast the pointer into a void* to avoid dereferencing
-  // it (since we only use it for comparisons), and return it.
+  // For controlled documents, we cast the pointer into a void* to avoid
+  // dereferencing it (since we only use it for comparisons).
   void* pointer = nullptr;
   RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
   if (aDocument && swm) {
     ErrorResult rv;
     if (aDocument->GetController().isSome()) {
       pointer = aDocument;
     }
   }
+
+  // If this document has been marked as tracker, let's use its address to make
+  // a unique cache key.
+  if (!pointer && aDocument &&
+      nsContentUtils::StorageDisabledByAntiTracking(nullptr,
+                                                    aDocument->GetChannel(),
+                                                    aURI)) {
+    pointer = aDocument;
+  }
+
   return pointer;
 }
 
 } // namespace image
 } // namespace mozilla
--- a/image/ImageCacheKey.h
+++ b/image/ImageCacheKey.h
@@ -36,29 +36,35 @@ public:
                 nsIDocument* aDocument, nsresult& aRv);
 
   ImageCacheKey(const ImageCacheKey& aOther);
   ImageCacheKey(ImageCacheKey&& aOther);
 
   bool operator==(const ImageCacheKey& aOther) const;
   PLDHashNumber Hash() const { return mHash; }
 
-  /// A weak pointer to the URI. For logging only.
+  /// A weak pointer to the URI.
   nsIURI* URI() const { return mURI; }
 
+  const OriginAttributes& OriginAttributesRef() const { return mOriginAttributes; }
+
   /// Is this cache entry for a chrome image?
   bool IsChrome() const { return mIsChrome; }
 
   /// A token indicating which service worker controlled document this entry
   /// belongs to, if any.
   void* ControlledDocument() const { return mControlledDocument; }
 
 private:
   bool SchemeIs(const char* aScheme);
-  static void* GetControlledDocumentToken(nsIDocument* aDocument);
+
+  // For ServiceWorker and for anti-tracking we need to use the document as
+  // token for the key. All those exceptions are handled by this method.
+  static void* GetSpecialCaseDocumentToken(nsIDocument* aDocument,
+                                           nsIURI* aURI);
 
   nsCOMPtr<nsIURI> mURI;
   Maybe<uint64_t> mBlobSerial;
   nsCString mBlobRef;
   OriginAttributes mOriginAttributes;
   void* mControlledDocument;
   PLDHashNumber mHash;
   bool mIsChrome;
--- a/image/decoders/icon/nsIconURI.cpp
+++ b/image/decoders/icon/nsIconURI.cpp
@@ -504,41 +504,37 @@ nsMozIconURI::SchemeIs(const char* aSche
   if (!aScheme) {
     return NS_ERROR_INVALID_ARG;
   }
 
   *aEquals = PL_strcasecmp("moz-icon", aScheme) ? false : true;
   return NS_OK;
 }
 
-NS_IMETHODIMP
+nsresult
 nsMozIconURI::Clone(nsIURI** result)
 {
+  nsresult rv;
   nsCOMPtr<nsIURL> newIconURL;
   if (mIconURL) {
-    nsCOMPtr<nsIURI> newURI;
-    nsresult rv = mIconURL->Clone(getter_AddRefs(newURI));
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-    newIconURL = do_QueryInterface(newURI, &rv);
+    newIconURL = do_QueryInterface(mIconURL, &rv);
     if (NS_FAILED(rv)) {
       return rv;
     }
   }
 
-  nsMozIconURI* uri = new nsMozIconURI();
+  RefPtr<nsMozIconURI> uri = new nsMozIconURI();
   newIconURL.swap(uri->mIconURL);
   uri->mSize = mSize;
   uri->mContentType = mContentType;
   uri->mFileName = mFileName;
   uri->mStockIcon = mStockIcon;
   uri->mIconSize = mIconSize;
   uri->mIconState = mIconState;
-  NS_ADDREF(*result = uri);
+  uri.forget(result);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsMozIconURI::CloneIgnoringRef(nsIURI** result)
 {
   // GetRef/SetRef not supported by nsMozIconURI, so
--- a/image/decoders/icon/nsIconURI.h
+++ b/image/decoders/icon/nsIconURI.h
@@ -42,16 +42,17 @@ protected:
                        // given a filename with an extension
   nsCString mStockIcon;
   int32_t mIconSize;   // -1 if not specified, otherwise index into
                        // kSizeStrings
   int32_t mIconState;  // -1 if not specified, otherwise index into
                        // kStateStrings
 
 private:
+  nsresult Clone(nsIURI** aURI);
   nsresult SetSpecInternal(const nsACString &input);
   nsresult SetScheme(const nsACString &input);
   nsresult SetUserPass(const nsACString &input);
   nsresult SetUsername(const nsACString &input);
   nsresult SetPassword(const nsACString &input);
   nsresult SetHostPort(const nsACString &aValue);
   nsresult SetHost(const nsACString &input);
   nsresult SetPort(int32_t port);
--- a/image/imgICache.idl
+++ b/image/imgICache.idl
@@ -3,21 +3,30 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface imgIRequest;
 interface nsIDocument;
+interface nsIPrincipal;
 interface nsIProperties;
 interface nsIURI;
 
 webidl Document;
 
+%{ C++
+namespace mozilla {
+class OriginAttributes;
+} // mozilla namespace
+%}
+
+[ptr] native OriginAttributesPtr(mozilla::OriginAttributes);
+
 /**
  * imgICache interface
  *
  * @author Stuart Parmenter <pavlov@netscape.com>
  * @version 0.1
  * @see imagelib2
  */
 [scriptable, builtinclass, uuid(bfdf23ff-378e-402e-8a6c-840f0c82b6c3)]
@@ -37,16 +46,24 @@ interface imgICache : nsISupports
    * @param uri The URI to remove.
    * @param doc The document to remove the cache entry for.
    * @throws NS_ERROR_NOT_AVAILABLE if \a uri was unable to be removed from
    * the cache.
    */
   [noscript] void removeEntry(in nsIURI uri, [optional] in Document doc);
 
   /**
+   * Evict images from the cache with the same origin and the same
+   * originAttributes of the passed principal.
+   *
+   * @param aPrincipal The principal
+   */
+  void removeEntriesFromPrincipal(in nsIPrincipal aPrincipal);
+
+  /**
    * Find Properties
    * Used to get properties such as 'type' and 'content-disposition'
    * 'type' is a nsISupportsCString containing the images' mime type such as
    * 'image/png'
    * 'content-disposition' will be a nsISupportsCString containing the header
    * If you call this before any data has been loaded from a URI, it will
    * succeed, but come back empty.
    *
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -1465,16 +1465,55 @@ imgLoader::ClearCache(bool chrome)
   if (chrome) {
     return ClearChromeImageCache();
   }
   return ClearImageCache();
 
 }
 
 NS_IMETHODIMP
+imgLoader::RemoveEntriesFromPrincipal(nsIPrincipal* aPrincipal)
+{
+  nsAutoString origin;
+  nsresult rv = nsContentUtils::GetUTFOrigin(aPrincipal, origin);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  AutoTArray<RefPtr<imgCacheEntry>, 128> entriesToBeRemoved;
+
+  imgCacheTable& cache = GetCache(nsContentUtils::IsSystemPrincipal(aPrincipal));
+  for (auto iter = cache.Iter(); !iter.Done(); iter.Next()) {
+    auto& key = iter.Key();
+
+    if (key.OriginAttributesRef() != BasePrincipal::Cast(aPrincipal)->OriginAttributesRef()) {
+       continue;
+    }
+
+    nsAutoString imageOrigin;
+    nsresult rv = nsContentUtils::GetUTFOrigin(key.URI(), imageOrigin);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      continue;
+    }
+
+    if (imageOrigin == origin) {
+      entriesToBeRemoved.AppendElement(iter.Data());
+    }
+  }
+
+  for (auto& entry : entriesToBeRemoved) {
+    if (!RemoveFromCache(entry)) {
+      NS_WARNING("Couldn't remove an entry from the cache in RemoveEntriesFromPrincipal()\n");
+    }
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 imgLoader::RemoveEntry(nsIURI* aURI,
                        nsIDocument* aDoc)
 {
   if (aURI) {
     OriginAttributes attrs;
     if (aDoc) {
       nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal();
       if (principal) {
new file mode 100644
--- /dev/null
+++ b/intl/l10n/docs/fluent_migrations.rst
@@ -0,0 +1,712 @@
+.. role:: bash(code)
+   :language: bash
+
+.. role:: js(code)
+   :language: javascript
+
+.. role:: python(code)
+   :language: python
+
+==================================
+Migrating Legacy Strings to Fluent
+==================================
+
+Firefox is a project localized in over 100 languages. As the code for existing
+features moves away from the old localization systems and starts using
+`Fluent`_, we need to ensure that we don’t lose existing translations, which
+would have the adverse effect of forcing contributors to localize hundreds of
+strings from scratch.
+
+`Fluent Migration`_ is a Python library designed to solve this specific problem:
+it allows to migrate legacy translations from `.dtd` and `.properties` files,
+not only moving strings and transforming them as needed to adapt to the `FTL`
+syntax, but also replicating "blame" for each string in VCS.
+
+
+Migration Recipes and Their Lifecycle
+=====================================
+
+The actual migrations are performed running Python modules called **migration
+recipes**, which contain directives on how to migrate strings, which files are
+involved, transformations to apply, etc. These recipes are stored in
+`mozilla-central`__.
+
+__ https://hg.mozilla.org/mozilla-central/file/default/python/l10n/fluent_migrations
+
+When part of Firefox’s UI is migrated to Fluent, a migration recipe should be
+attached to the same patch that adds new strings to `.ftl` files.
+
+Migration recipes can quickly become obsolete, either because the syntax used in
+these recipes changes, or because the referenced legacy strings and files are
+removed from repositories. For these reasons, l10n-drivers periodically clean up
+the `fluent_migrations` folder in mozilla-central, keeping only recipes for 2
+shipping versions (Nightly and Beta).
+
+
+.. hint::
+
+  As a developer you don’t need to bother about updating migration recipes
+  already in `mozilla-central`: if a new patch removes a string or file that is
+  used in a migration recipe, simply ignore it, since the entire recipe will be
+  removed within a couple of cycles.
+
+  See also the `How Migrations Are Run on l10n Repositories`_ section.
+
+
+How to Write Migration Recipes
+==============================
+
+The migration recipe’s filename should start with a reference to the associated
+bug number, and include a brief description of the bug, e.g.
+:bash:`bug_1451992_preferences_applicationManager.py` is the migration recipe
+used to migrate the Application Manager window in preferences. It’s also
+possible to look at existing recipes in `mozilla-central`__ for inspiration.
+
+__ https://hg.mozilla.org/mozilla-central/file/default/python/l10n/fluent_migrations
+
+
+Basic Migration
+---------------
+
+Let’s consider a basic example: one string needs to be migrated, without
+any further change, from a DTD file to Fluent.
+
+The legacy string is stored in :bash:`toolkit/locales/en-US/chrome/global/findbar.dtd`:
+
+
+.. code-block:: dtd
+
+  <!ENTITY next.tooltip "Find the next occurrence of the phrase">
+
+
+The new Fluent string is stored in :bash:`toolkit/locales/en-US/toolkit/main-window/findbar.ftl`:
+
+
+.. code-block:: properties
+
+  findbar-next =
+      .tooltiptext = Find the next occurrence of the phrase
+
+
+This is how the migration recipe looks:
+
+
+.. code-block:: python
+
+  # coding=utf8
+
+  # Any copyright is dedicated to the Public Domain.
+  # http://creativecommons.org/publicdomain/zero/1.0/
+
+  from __future__ import absolute_import
+  import fluent.syntax.ast as FTL
+  from fluent.migrate.helpers import transforms_from
+
+  def migrate(ctx):
+      """Bug 1411707 - Migrate the findbar XBL binding to a Custom Element, part {index}."""
+
+      ctx.add_transforms(
+          "toolkit/toolkit/main-window/findbar.ftl",
+          "toolkit/toolkit/main-window/findbar.ftl",
+          transforms_from(
+  """
+  findbar-next =
+      .tooltiptext = { COPY(from_path, "next.tooltip") }
+  """, from_path="toolkit/chrome/global/findbar.dtd")
+
+
+The first important thing to notice is that the migration recipe needs file
+paths relative to a localization repository, losing :bash:`locales/en-US/`:
+
+ - :bash:`toolkit/locales/en-US/chrome/global/findbar.dtd` becomes
+   :bash:`toolkit/chrome/global/findbar.dtd`.
+ - :bash:`toolkit/locales/en-US/toolkit/main-window/findbar.ftl` becomes
+   :bash:`toolkit/toolkit/main-window/findbar.ftl`.
+
+The recipe includes a :python:`migrate` function, which can contain multiple
+:python:`add_transforms` calls. The *docstring* for this function will be used
+as a commit message in VCS, that’s why it’s important to make sure the bug
+reference is correct, and to keep the `part {index}` section: multiple strings
+could have multiple authors, and would be migrated in distinct commits (part 1,
+part 2, etc.).
+
+The :python:`context.add_transforms` function takes 3 arguments:
+
+ - Path to the target l10n file.
+ - Path to the source (en-US) file.
+ - An array of Transforms. Transforms are AST nodes which describe how legacy
+   translations should be migrated.
+
+In this case there is only one Transform that migrates the string with ID
+:js:`next.tooltip` from :bash:`toolkit/chrome/global/findbar.dtd`, and injects
+it in the FTL fragment. The :python:`COPY` Transform allows to copy the string
+from an existing file as is, while :python:`from_path` is used to avoid
+repeating the same path multiple times, making the recipe more readable. Without
+:python:`from_path`, this could be written as:
+
+
+.. code-block:: python
+
+  ctx.add_transforms(
+      "toolkit/toolkit/main-window/findbar.ftl",
+      "toolkit/toolkit/main-window/findbar.ftl",
+      transforms_from(
+  """
+  findbar-next =
+  .tooltiptext = { COPY("toolkit/chrome/global/findbar.dtd", "next.tooltip") }
+  """)
+
+
+This method of writing migration recipes allows to take the original FTL
+strings, and simply replace the value of each message with a :python:`COPY`
+Transform. :python:`transforms_from` takes care of converting the FTL syntax
+into an array of Transforms describing how the legacy translations should be
+migrated. This manner of defining migrations is only suitable to simple strings
+where a copy operation is sufficient. For more complex use-cases which require
+some additional logic in Python, it’s necessary to resort to the raw AST.
+
+
+The example above is equivalent to the following syntax, which requires a deeper
+understanding of the underlying AST structure:
+
+
+.. code-block:: python
+
+  ctx.add_transforms(
+      "toolkit/toolkit/main-window/findbar.ftl",
+      "toolkit/toolkit/main-window/findbar.ftl",
+      [
+          FTL.Message(
+              id=FTL.Identifier("findbar-next"),
+              attributes=[
+                  FTL.Attribute(
+                      id=FTL.Identifier("tooltiptext"),
+                      value=COPY(
+                          "toolkit/chrome/global/findbar.dtd",
+                          "next.tooltip"
+                      )
+                  )
+              ]
+          )
+      ]
+  )
+
+This creates a :python:`Message`, taking the value from the legacy string
+:js:`findbar-next`. A message can have an array of attributes, each with an ID
+and a value: in this case there is only one attribute, with ID :js:`tooltiptext`
+and :js:`value` copied from the legacy string.
+
+Notice how both the ID of the message and the ID of the attribute are
+defined as an :python:`FTL.Identifier`, not simply as a string.
+
+
+.. tip::
+
+  It’s possible to concatenate arrays of Transforms defined manually, like in
+  the last example, with those coming from :python:`transforms_from`, by using
+  the :python:`+` operator. Alternatively, it’s possible to use multiple
+  :python:`add_transforms`.
+
+  The order of Transforms provided in the recipe is not relevant, the reference
+  file is used for ordering messages.
+
+
+Replacing Content in Legacy Strings
+-----------------------------------
+
+While :python:`COPY` allows to copy a legacy string as is, :python:`REPLACE`
+(from `fluent.migrate`) allows to replace content while performing the
+migration. This is necessary, for example, when migrating strings that include
+placeholders or entities that need to be replaced to adapt to Fluent syntax.
+
+Consider for example the following string:
+
+
+.. code-block:: properties
+
+  siteUsage = %1$S %2$S (Persistent)
+
+
+Which needs to be migrated to:
+
+
+.. code-block:: properties
+
+  site-usage = { $value } { $unit } (Persistent)
+
+
+:js:`%1$S` (and :js:`%S`) don’t match the format used in Fluent for variables,
+so they need to be replaced within the migration process. This is how the
+Transform is defined:
+
+
+.. code-block:: python
+
+  FTL.Message(
+      id=FTL.Identifier("site-usage-pattern"),
+      value=REPLACE(
+          "browser/chrome/browser/preferences/preferences.properties",
+          "siteUsage",
+          {
+              "%1$S": EXTERNAL_ARGUMENT(
+                  "value"
+              ),
+              "%2$S": EXTERNAL_ARGUMENT(
+                  "unit"
+              )
+          }
+      )
+  )
+
+
+This creates an :python:`FTL.Message`, taking the value from the legacy string
+:js:`siteUsage`, but replacing specified placeholders with Fluent variables.
+
+It’s also possible to replace content with a specific text: in that case, it
+needs to be defined as a :python:`TextElement`. For example, to replace
+:js:`%S` with some HTML markup:
+
+
+.. code-block:: python
+
+  value=REPLACE(
+      "browser/chrome/browser/preferences/preferences.properties",
+      "searchResults.sorryMessageWin",
+      {
+          "%S": FTL.TextElement('<span data-l10n-name="query"></span>')
+      }
+  )
+
+
+.. note::
+
+  :python:`EXTERNAL_ARGUMENT` and :python:`MESSAGE_REFERENCE` are helper
+  Transforms which can be used to save keystrokes in common cases where using
+  the raw AST is too verbose.
+
+  :python:`EXTERNAL_ARGUMENT` is used to create a reference to a variable, e.g.
+  :js:`{ $variable }`.
+
+  :python:`MESSAGE_REFERENCE` is used to create a reference to another message,
+  e.g. :js:`{ another-string }`, or a `term`__, e.g. :js:`{ -brand-short-name }`.
+
+  Both Transforms need to be imported at the beginning of the recipe, e.g.
+  :python:`from fluent.migrate.helpers import EXTERNAL_ARGUMENT`
+
+  __ https://projectfluent.org/fluent/guide/terms.html
+
+
+Concatenating Strings
+---------------------
+
+It’s quite common to concatenate multiple strings coming from `DTD` and
+`properties`, for example to create sentences with HTML markup. It’s possible to
+concatenate strings and text elements in a migration recipe using the
+:python:`CONCAT` Transform. This allows to generate a single Fluent message from
+these fragments, avoiding run-time transformations as prescribed by
+:ref:`Fluent’s social contract <fluent-tutorial-social-contract>`.
+
+Note that, in case of simple migrations using :python:`transforms_from`, the
+concatenation is carried out implicitly by using the Fluent syntax interleaved
+with COPY() transform calls to define the migration recipe.
+
+Consider the following example:
+
+
+.. code-block:: properties
+
+  # %S is replaced by a link, using searchResults.needHelpSupportLink as text
+  searchResults.needHelp = Need help? Visit %S
+
+  # %S is replaced by "Firefox"
+  searchResults.needHelpSupportLink = %S Support
+
+
+In Fluent:
+
+
+.. code-block:: properties
+
+  searchResults.needHelpSupportLink = Need help? Visit <a data-l10n-name="url">{ -brand-short-name } Support</a>
+
+
+This is quite a complex migration: it requires to take 2 legacy strings, and
+concatenate their values with HTML markup. Here’s how the Transform is defined:
+
+
+.. code-block:: python
+
+  FTL.Message(
+      id=FTL.Identifier("search-results-help-link"),
+      value=REPLACE(
+          "browser/chrome/browser/preferences/preferences.properties",
+          "searchResults.needHelp",
+          {
+              "%S": CONCAT(
+                  FTL.TextElement('<a data-l10n-name="url">'),
+                  REPLACE(
+                      "browser/chrome/browser/preferences/preferences.properties",
+                      "searchResults.needHelpSupportLink",
+                      {
+                          "%S": MESSAGE_REFERENCE("-brand-short-name"),
+                      }
+                  ),
+                  FTL.TextElement("</a>")
+              )
+          }
+      )
+  ),
+
+
+:js:`%S` in :js:`searchResults.needHelpSupportLink` is replaced by a reference
+to the term :js:`-brand-short-name`, migrating from :js:`%S Support` to :js:`{
+-brand-short-name } Support`. The result of this operation is then inserted
+between two text elements to create the anchor markup. The resulting text is
+finally  used to replace :js:`%S` in :js:`searchResults.needHelp`, and used as
+value for the FTL message.
+
+
+.. important::
+
+  When concatenating existing strings, avoid introducing changes to the original
+  text, for example adding spaces or punctuation. Each language has its own
+  rules, and this might result in poor migrated strings. In case of doubt,
+  always ask for feedback.
+
+
+Plural Strings
+--------------
+
+Migrating plural strings from `.properties` files usually involves two
+Transforms from :python:`fluent.migrate.transforms`: the
+:python:`REPLACE_IN_TEXT` Transform takes TextElements as input, making it
+possible to pass it as the foreach function of the :python:`PLURALS` Transform.
+
+Consider the following legacy string:
+
+
+.. code-block:: properties
+
+  # LOCALIZATION NOTE (disableContainersOkButton): Semi-colon list of plural forms.
+  # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+  # #1 is the number of container tabs
+  disableContainersOkButton = Close #1 Container Tab;Close #1 Container Tabs
+
+
+In Fluent:
+
+
+.. code-block:: properties
+
+  containers-disable-alert-ok-button =
+      { $tabCount ->
+          [one] Close { $tabCount } Container Tab
+         *[other] Close { $tabCount } Container Tabs
+      }
+
+
+This is how the Transform for this string is defined:
+
+
+.. code-block:: python
+
+  FTL.Message(
+      id=FTL.Identifier("containers-disable-alert-ok-button"),
+      value=PLURALS(
+          "browser/chrome/browser/preferences/preferences.properties",
+          "disableContainersOkButton",
+          EXTERNAL_ARGUMENT("tabCount"),
+          lambda text: REPLACE_IN_TEXT(
+              text,
+              {
+                  "#1": EXTERNAL_ARGUMENT("tabCount")
+              }
+          )
+      )
+  )
+
+
+The `PLURALS` Transform will take care of creating the correct number of plural
+categories for each language. Notice how `#1` is replaced for each of these
+variants with :js:`{ $tabCount }`, using :python:`REPLACE_IN_TEXT` and
+:python:`EXTERNAL_ARGUMENT("tabCount")`.
+
+In this case it’s not possible to use :python:`REPLACE` because it takes a file
+path and a message ID as arguments, whereas here the recipe needs to operate on
+regular text. The replacement is performed on each plural form of the original
+string, where plural forms are separated by a semicolon.
+
+Complex Cases
+-------------
+
+It’s always possible to migrate strings by manually creating the underlying AST
+structure. Consider the following complex Fluent string:
+
+
+.. code-block:: properties
+
+  use-current-pages =
+      .label =
+          { $tabCount ->
+              [1] Use Current Page
+             *[other] Use Current Pages
+          }
+      .accesskey = C
+
+
+The migration for this string is quite complex: the :js:`label` attribute is
+created from 2 different legacy strings, and it’s not a proper plural form.
+Notice how the first string is associated to the :js:`1` case, not the :js:`one`
+category used in plural forms. For these reasons, it’s not possible to use
+:python:`PLURALS`, the Transform needs to be crafted recreating the AST.
+
+
+.. code-block:: python
+
+
+  FTL.Message(
+      id=FTL.Identifier("use-current-pages"),
+      attributes=[
+          FTL.Attribute(
+              id=FTL.Identifier("label"),
+              value=FTL.Pattern(
+                  elements=[
+                      FTL.Placeable(
+                          expression=FTL.SelectExpression(
+                              expression=EXTERNAL_ARGUMENT("tabCount"),
+                              variants=[
+                                  FTL.Variant(
+                                      key=FTL.NumberExpression("1"),
+                                      default=False,
+                                      value=COPY(
+                                          "browser/chrome/browser/preferences/main.dtd",
+                                          "useCurrentPage.label",
+                                      )
+                                  ),
+                                  FTL.Variant(
+                                      key=FTL.VariantName("other"),
+                                      default=True,
+                                      value=COPY(
+                                          "browser/chrome/browser/preferences/main.dtd",
+                                          "useMultiple.label",
+                                      )
+                                  )
+                              ]
+                          )
+                      )
+                  ]
+              )
+          ),
+          FTL.Attribute(
+              id=FTL.Identifier("accesskey"),
+              value=COPY(
+                  "browser/chrome/browser/preferences/main.dtd",
+                  "useCurrentPage.accesskey",
+              )
+          ),
+      ],
+  ),
+
+
+This Transform uses several concepts already described in this document. Notable
+new elements are:
+
+ - The fact that the `label` attribute is defined as a :python:`Pattern`. This
+   is because, in this example, we’re creating a new value from scratch and
+   migrating existing translations as its variants. Patterns are one of Fluent’s
+   value types and, under the hood, all Transforms like :python:`COPY` or
+   :python:`REPLACE` evaluate to Fluent Patterns.
+ - A :python:`SelectExpression` is defined, with an array of :python:`Variant`
+   objects.
+
+
+How to Test Migration Recipes
+=============================
+
+Unfortunately, testing migration recipes requires several manual steps. We plan
+to `introduce automated testing`__ for patches including migration recipes, in
+the meantime this is how it’s possible to test migration recipes.
+
+__ https://bugzilla.mozilla.org/show_bug.cgi?id=1353680
+
+
+1. Install Fluent Migration
+---------------------------
+
+The first step is to install the `Fluent Migration`_ Python library. It’s
+currently not available as a package, so the repository must be cloned locally
+and installed manually, e.g. with :bash:`pip install -e .`.
+
+Installing this package will make a :bash:`migrate-l10n` command available.
+
+
+2. Clone gecko-strings
+----------------------
+
+Migration recipes work against localization repositories, which means it’s not
+possible to test them directly against `mozilla-central`, unless the *source*
+path (the second argument) in :python:`ctx.add_transforms` is temporarily
+tweaked to match `mozilla-central` paths.
+
+To test the actual recipe that will land in the patch, it’s necessary to clone
+the `gecko-strings`_ repository on the system twice, in two separate folders.
+One will simulate the reference en-US repository after the patch has landed, and
+the other will simulate a target localization. For example, let’s call the two
+folders `en-US` and `test`.
+
+
+.. code-block:: bash
+
+  hg clone https://hg.mozilla.org/l10n/gecko-strings en-US
+  cp -r en-US test
+
+
+3. Add new FTL strings to the local en-US repository
+------------------------------------------------
+
+The changed (or brand new) FTL files from the patch need to be copied into the
+`en-US` repository. Remember that paths are slightly different, with
+localization repositories missing the :bash:`locales/en-US` portion. There’s no
+need to commit these changes locally.
+
+
+4. Run the migration recipe
+---------------------------
+
+The system is all set to run the recipe with the following commands:
+
+
+.. code-block:: bash
+
+  cd PATH/TO/recipes
+
+  migrate-l10n \
+    --lang test
+    --reference-dir PATH/TO/en-US \
+    --localization-dir PATH/TO/test \
+    --dry-run \
+    name_of_the_recipe
+
+
+The name of the recipe needs to be specified without the :bash:`.py` extension,
+since it’s imported as a module.
+
+Alternatively, before running :bash:`migrate-l10n`, it’s possible to update the
+value of :bash:`PYTHONPATH` to include the folder storing migration recipes.
+
+
+.. code-block:: bash
+
+  export PYTHONPATH="${PYTHONPATH}:PATH/TO/recipes/"
+
+
+The :bash:`--dry-run` option allows to run the recipe without making changes,
+and it’s useful to spot syntax errors in the recipe. If there are no errors,
+it’s possible to run the migration without :bash:`--dry-run` and actually commit
+the changes locally.
+
+This is the output of a migration:
+
+
+.. code-block:: bash
+
+  Running migration bug_1411707_findbar for test
+  WARNING:migrate:Plural rule for "'test'" is not defined in compare-locales
+  INFO:migrate:Localization file toolkit/toolkit/main-window/findbar.ftl does not exist and it will be created
+    Writing to test/toolkit/toolkit/main-window/findbar.ftl
+    Committing changeset: Bug 1411707 - Migrate the findbar XBL binding to a Custom Element, part 1.
+    Writing to test/toolkit/toolkit/main-window/findbar.ftl
+    Committing changeset: Bug 1411707 - Migrate the findbar XBL binding to a Custom Element, part 2.
+
+
+.. hint::
+
+  The warning about plural rules is expected, since `test` is not a valid locale
+  code. At this point, the result of migration is committed to the local `test`
+  folder.
+
+
+5. Compare the resulting files
+------------------------------
+
+Once the migration has run, the `test` repository includes the migrated files,
+and it’s possible to compare them with the files in `en-US`. Since the migration
+code strips empty line between strings, it’s recommended to use :bash:`diff -B`
+between the two files, or use a visual diff to compare their content.
+
+
+6. Caveats
+----------
+
+Be aware of hard-coded English context in migration. Consider for example:
+
+
+.. code-block:: python
+
+  ctx.add_transforms(
+          "browser/browser/preferences/siteDataSettings.ftl",
+          "browser/browser/preferences/siteDataSettings.ftl",
+          transforms_from(
+  """
+  site-usage-persistent = { site-usage-pattern } (Persistent)
+  """)
+  )
+
+
+This Transform will pass a manual comparison, since the two files are identical,
+but will result in :js:`(Persistent)` being hard-coded in English for all
+languages.
+
+
+How Migrations Are Run on l10n Repositories
+===========================================
+
+Once a patch including new FTL strings and a migration recipe lands in
+mozilla-central, l10n-drivers will perform a series of actions to migrate
+strings in all 100+ localization repositories:
+
+ - New Fluent strings land in `mozilla-central`, together with a migration
+   recipe.
+ - New strings are added to `gecko-strings-quarantine`_, a unified repository
+   including strings for all shipping versions of Firefox, and used as a buffer
+   before exposing strings to localizers.
+ - Migration recipes are run against all l10n repositories, migrating strings
+   from old to new files, and storing them in VCS.
+ - New en-US strings are pushed to the official `gecko-strings`_ repository
+   used by localization tools, and exposed to all localizers.
+
+Migration recipes could be run again within a release cycle, in order to migrate
+translations for legacy strings added after the first run. They’re usually
+removed from `mozilla-central` within 2 cycles, e.g. a migration recipe created
+for Firefox 59 would be removed when Firefox 61 is available in Nightly.
+
+
+.. tip::
+
+  A script to run migrations on all l10n repositories is available in `this
+  repository`__, automating part of the steps described for manual testing, and
+  it could be adapted to local testing.
+
+  __ https://github.com/flodolo/fluent-migrations
+
+
+How to Get Help
+===============
+
+Writing migration recipes can be challenging for non trivial cases, and it can
+require extensive l10n knowledge to avoid localizability issues.
+
+Don’t hesitate to reach out to the l10n-drivers for feedback, help to test or
+write the migration recipes:
+
+ - Francesco Lodolo (:flod)
+ - Staś Małolepszy (:stas)
+ - Zibi Braniecki (:gandalf)
+ - Axel Hecht (:pike)
+
+
+
+.. _Fluent: http://projectfluent.org/
+.. _Fluent Migration: https://hg.mozilla.org/l10n/fluent-migration/
+.. _gecko-strings-quarantine: https://hg.mozilla.org/users/axel_mozilla.com/gecko-strings-quarantine
+.. _gecko-strings: https://hg.mozilla.org/l10n/gecko-strings
--- a/intl/l10n/docs/fluent_tutorial.rst
+++ b/intl/l10n/docs/fluent_tutorial.rst
@@ -44,17 +44,17 @@ Getting a Review
 
 If you end up working on any patch which touches FTL files, we have a temporary
 hook in place that will reject your patch unless you get an r+ from one of the following
 L10n Drivers:
 
   - Francesco Lodolo (:flod)
   - Zibi Braniecki (:gandalf)
   - Axel Hecht (:pike)
-  - Stas Malolepszy (:stas)
+  - Staś Małolepszy (:stas)
 
 
 Major Benefits
 ==============
 
 Not only was the previous system designed over 20 years ago using file formats
 never intended for localization, but also the Web stack which Fluent ties into has
 completely changed over the same period, and the domain of internationalization
@@ -160,16 +160,18 @@ The above, of course, is a particular se
 the new features and concepts introduced by Fluent.
 
 In order to ensure the quality of the output, a lot of new checks and tooling
 has been added to the build system.
 `Pontoon`_, the main localization tool used to translate Firefox, has been rebuilding
 its user experience to support localizers in their work.
 
 
+.. _fluent-tutorial-social-contract:
+
 Social Contract
 ===============
 
 Fluent uses the concept of a `social contract` between developer and localizers.
 This contract is established by the selection of a unique identifier, called :js:`l10n-id`,
 which carries a promise of being used in a particular place to carry a particular meaning.
 
 The use of unique identifiers is not new for Firefox engineers, but it is important
--- a/intl/l10n/docs/index.rst
+++ b/intl/l10n/docs/index.rst
@@ -8,16 +8,17 @@ all existing localization models current
 In case of Firefox it directly supersedes DTD and StringBundle systems, providing
 a large number of new features and improvements over both of them, for developers
 and localizers.
 
 .. toctree::
    :maxdepth: 2
 
    fluent_tutorial
+   fluent_migrations
 
 Other resources:
 
  * `Fluent Syntax Guide <http://projectfluent.org/fluent/guide/>`_
  * `Fluent Wiki <https://github.com/projectfluent/fluent/wiki>`_
  * `Fluent.js Wiki <https://github.com/projectfluent/fluent.js/wiki>`_
 
 .. _Fluent: http://projectfluent.org/
--- a/js/src/builtin/Eval.cpp
+++ b/js/src/builtin/Eval.cpp
@@ -257,21 +257,24 @@ EvalKernel(JSContext* cx, HandleValue v,
         esg.lookupInEvalCache(linearStr, callerScript, pc);
 
     if (!esg.foundScript()) {
         RootedScript maybeScript(cx);
         unsigned lineno;
         const char* filename;
         bool mutedErrors;
         uint32_t pcOffset;
-        DescribeScriptedCallerForCompilation(cx, &maybeScript, &filename, &lineno, &pcOffset,
-                                             &mutedErrors,
-                                             evalType == DIRECT_EVAL
-                                             ? CALLED_FROM_JSOP_EVAL
-                                             : NOT_CALLED_FROM_JSOP_EVAL);
+        if (evalType == DIRECT_EVAL) {
+            DescribeScriptedCallerForDirectEval(cx, callerScript, pc, &filename, &lineno,
+                                                &pcOffset, &mutedErrors);
+            maybeScript = callerScript;
+        } else {
+            DescribeScriptedCallerForCompilation(cx, &maybeScript, &filename, &lineno, &pcOffset,
+                                                 &mutedErrors);
+        }
 
         const char* introducerFilename = filename;
         if (maybeScript && maybeScript->scriptSource()->introducerFilename())
             introducerFilename = maybeScript->scriptSource()->introducerFilename();
 
         RootedScope enclosing(cx);
         if (evalType == DIRECT_EVAL)
             enclosing = callerScript->innermostScope(pc);
@@ -339,39 +342,39 @@ js::DirectEvalStringFromIon(JSContext* c
     if (ejr != EvalJSON_NotJSON)
         return ejr == EvalJSON_Success;
 
     EvalScriptGuard esg(cx);
 
     esg.lookupInEvalCache(linearStr, callerScript, pc);
 
     if (!esg.foundScript()) {
-        RootedScript maybeScript(cx);
         const char* filename;
         unsigned lineno;
         bool mutedErrors;
         uint32_t pcOffset;
-        DescribeScriptedCallerForCompilation(cx, &maybeScript, &filename, &lineno, &pcOffset,
-                                             &mutedErrors, CALLED_FROM_JSOP_EVAL);
+        DescribeScriptedCallerForDirectEval(cx, callerScript, pc, &filename, &lineno, &pcOffset,
+                                            &mutedErrors);
 
         const char* introducerFilename = filename;
-        if (maybeScript && maybeScript->scriptSource()->introducerFilename())
-            introducerFilename = maybeScript->scriptSource()->introducerFilename();
+        if (callerScript->scriptSource()->introducerFilename())
+            introducerFilename = callerScript->scriptSource()->introducerFilename();
 
         RootedScope enclosing(cx, callerScript->innermostScope(pc));
 
         CompileOptions options(cx);
         options.setIsRunOnce(true)
                .setNoScriptRval(false)
                .setMutedErrors(mutedErrors)
                .maybeMakeStrictMode(IsStrictEvalPC(pc));
 
         if (introducerFilename) {
             options.setFileAndLine(filename, 1);
-            options.setIntroductionInfo(introducerFilename, "eval", lineno, maybeScript, pcOffset);
+            options.setIntroductionInfo(introducerFilename, "eval", lineno, callerScript,
+                                        pcOffset);
         } else {
             options.setFileAndLine("eval", 1);
             options.setIntroductionType("eval");
         }
 
         AutoStableStringChars linearChars(cx);
         if (!linearChars.initTwoByte(cx, linearStr))
             return false;
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1355,17 +1355,17 @@ CaptureFirstSubsumedFrame(JSContext* cx,
 
     RootedObject obj(cx, &args[0].toObject());
     obj = CheckedUnwrap(obj);
     if (!obj) {
         JS_ReportErrorASCII(cx, "Denied permission to object.");
         return false;
     }
 
-    JS::StackCapture capture(JS::FirstSubsumedFrame(cx, obj->realm()->principals()));
+    JS::StackCapture capture(JS::FirstSubsumedFrame(cx, obj->nonCCWRealm()->principals()));
     if (args.length() > 1)
         capture.as<JS::FirstSubsumedFrame>().ignoreSelfHosted = JS::ToBoolean(args[1]);
 
     JS::RootedObject capturedStack(cx);
     if (!JS::CaptureCurrentStack(cx, &capturedStack, std::move(capture)))
         return false;
 
     args.rval().setObjectOrNull(capturedStack);
--- a/js/src/builtin/TypedObject.cpp
+++ b/js/src/builtin/TypedObject.cpp
@@ -334,23 +334,23 @@ static const uint32_t ReferenceSizes[] =
     sizeof(_type),
     JS_FOR_EACH_REFERENCE_TYPE_REPR(REFERENCE_SIZE) 0
 #undef REFERENCE_SIZE
 };
 
 uint32_t
 ReferenceTypeDescr::size(Type t)
 {
-    return ReferenceSizes[t];
+    return ReferenceSizes[uint32_t(t)];
 }
 
 uint32_t
 ReferenceTypeDescr::alignment(Type t)
 {
-    return ReferenceSizes[t];
+    return ReferenceSizes[uint32_t(t)];
 }
 
 /*static*/ const char*
 ReferenceTypeDescr::typeName(Type type)
 {
     switch (type) {
 #define NUMERIC_TYPE_TO_STRING(constant_, type_, name_) \
         case constant_: return #name_;
@@ -370,30 +370,30 @@ js::ReferenceTypeDescr::call(JSContext* 
 
     if (args.length() < 1) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_MORE_ARGS_NEEDED,
                                   descr->typeName(), "0", "s");
         return false;
     }
 
     switch (descr->type()) {
-      case ReferenceTypeDescr::TYPE_ANY:
+      case ReferenceType::TYPE_ANY:
         args.rval().set(args[0]);
         return true;
 
-      case ReferenceTypeDescr::TYPE_OBJECT:
+      case ReferenceType::TYPE_OBJECT:
       {
         RootedObject obj(cx, ToObject(cx, args[0]));
         if (!obj)
             return false;
         args.rval().setObject(*obj);
         return true;
       }
 
-      case ReferenceTypeDescr::TYPE_STRING:
+      case ReferenceType::TYPE_STRING:
       {
         RootedString obj(cx, ToString<CanGC>(cx, args[0]));
         if (!obj)
             return false;
         args.rval().setString(&*obj);
         return true;
       }
     }
@@ -761,53 +761,74 @@ const JSFunctionSpec StructMetaTypeDescr
 const JSPropertySpec StructMetaTypeDescr::typedObjectProperties[] = {
     JS_PS_END
 };
 
 const JSFunctionSpec StructMetaTypeDescr::typedObjectMethods[] = {
     JS_FS_END
 };
 
-JSObject*
+CheckedInt32
+StructMetaTypeDescr::Layout::addField(int32_t fieldAlignment, int32_t fieldSize)
+{
+    // Alignment of the struct is the max of the alignment of its fields.
+    structAlignment = js::Max(structAlignment, fieldAlignment);
+
+    // Align the pointer.
+    CheckedInt32 offset = RoundUpToAlignment(sizeSoFar, fieldAlignment);
+    if (!offset.isValid())
+        return offset;
+
+    // Allocate space.
+    sizeSoFar = offset + fieldSize;
+    if (!sizeSoFar.isValid())
+        return sizeSoFar;
+
+    return offset;
+}
+
+CheckedInt32
+StructMetaTypeDescr::Layout::addScalar(Scalar::Type type) {
+    return addField(ScalarTypeDescr::alignment(type),
+                    ScalarTypeDescr::size(type));
+}
+
+CheckedInt32
+StructMetaTypeDescr::Layout::addReference(ReferenceType type) {
+    return addField(ReferenceTypeDescr::alignment(type),
+                    ReferenceTypeDescr::size(type));
+}
+
+CheckedInt32
+StructMetaTypeDescr::Layout::close(int32_t *alignment) {
+    if (alignment)
+        *alignment = structAlignment;
+    return RoundUpToAlignment(sizeSoFar, structAlignment);
+}
+
+/* static */ JSObject*
 StructMetaTypeDescr::create(JSContext* cx,
                             HandleObject metaTypeDescr,
                             HandleObject fields)
 {
     // Obtain names of fields, which are the own properties of `fields`
     AutoIdVector ids(cx);
     if (!GetPropertyKeys(cx, fields, JSITER_OWNONLY | JSITER_SYMBOLS, &ids))
         return nullptr;
 
     // Iterate through each field. Collect values for the various
     // vectors below and also track total size and alignment. Be wary
     // of overflow!
-    StringBuffer stringBuffer(cx);     // Canonical string repr
-    AutoValueVector fieldNames(cx);    // Name of each field.
     AutoValueVector fieldTypeObjs(cx); // Type descriptor of each field.
-    AutoValueVector fieldOffsets(cx);  // Offset of each field field.
-    RootedObject userFieldOffsets(cx); // User-exposed {f:offset} object
-    RootedObject userFieldTypes(cx);   // User-exposed {f:descr} object.
-    CheckedInt32 sizeSoFar(0);         // Size of struct thus far.
-    uint32_t alignment = 1;            // Alignment of struct.
     bool opaque = false;               // Opacity of struct.
 
-    userFieldOffsets = NewBuiltinClassInstance<PlainObject>(cx, TenuredObject);
-    if (!userFieldOffsets)
-        return nullptr;
-
-    userFieldTypes = NewBuiltinClassInstance<PlainObject>(cx, TenuredObject);
-    if (!userFieldTypes)
-        return nullptr;
-
-    if (!stringBuffer.append("new StructType({"))
-        return nullptr;
-
     RootedValue fieldTypeVal(cx);
     RootedId id(cx);
     Rooted<TypeDescr*> fieldType(cx);
+
     for (unsigned int i = 0; i < ids.length(); i++) {
         id = ids[i];
 
         // Check that all the property names are non-numeric strings.
         uint32_t unused;
         if (!JSID_IS_ATOM(id) || JSID_TO_ATOM(id)->isIndex(&unused)) {
             RootedValue idValue(cx, IdToValue(id));
             ReportCannotConvertTo(cx, idValue, "StructType field name");
@@ -819,22 +840,69 @@ StructMetaTypeDescr::create(JSContext* c
         if (!GetProperty(cx, fields, fields, id, &fieldTypeVal))
             return nullptr;
         fieldType = ToObjectIf<TypeDescr>(fieldTypeVal);
         if (!fieldType) {
             ReportCannotConvertTo(cx, fieldTypeVal, "StructType field specifier");
             return nullptr;
         }
 
-        // Collect field name and type object
+        // Collect field type object
+        if (!fieldTypeObjs.append(ObjectValue(*fieldType)))
+            return nullptr;
+
+        // Struct is opaque if any field is opaque
+        if (fieldType->opaque())
+            opaque = true;
+    }
+
+    RootedObject structTypePrototype(cx, GetPrototype(cx, metaTypeDescr));
+    if (!structTypePrototype)
+        return nullptr;
+
+    return createFromArrays(cx, structTypePrototype, opaque, ids, fieldTypeObjs);
+}
+
+/* static */ StructTypeDescr*
+StructMetaTypeDescr::createFromArrays(JSContext* cx,
+                                      HandleObject structTypePrototype,
+                                      bool opaque,
+                                      AutoIdVector& ids,
+                                      AutoValueVector& fieldTypeObjs)
+{
+    StringBuffer stringBuffer(cx);     // Canonical string repr
+    AutoValueVector fieldNames(cx);    // Name of each field.
+    AutoValueVector fieldOffsets(cx);  // Offset of each field field.
+    RootedObject userFieldOffsets(cx); // User-exposed {f:offset} object
+    RootedObject userFieldTypes(cx);   // User-exposed {f:descr} object.
+    Layout layout;                     // Field offsetter
+
+    userFieldOffsets = NewBuiltinClassInstance<PlainObject>(cx, TenuredObject);
+    if (!userFieldOffsets)
+        return nullptr;
+
+    userFieldTypes = NewBuiltinClassInstance<PlainObject>(cx, TenuredObject);
+    if (!userFieldTypes)
+        return nullptr;
+
+    if (!stringBuffer.append("new StructType({"))
+        return nullptr;
+
+    RootedId id(cx);
+    Rooted<TypeDescr*> fieldType(cx);
+
+    for (unsigned int i = 0; i < ids.length(); i++) {
+        id = ids[i];
+
+        // Collect field name
         RootedValue fieldName(cx, IdToValue(id));
         if (!fieldNames.append(fieldName))
             return nullptr;
-        if (!fieldTypeObjs.append(ObjectValue(*fieldType)))
-            return nullptr;
+
+        fieldType = ToObjectIf<TypeDescr>(fieldTypeObjs[i]);
 
         // userFieldTypes[id] = typeObj
         if (!DefineDataProperty(cx, userFieldTypes, id, fieldTypeObjs[i],
                                 JSPROP_READONLY | JSPROP_PERMANENT))
         {
             return nullptr;
         }
 
@@ -843,69 +911,50 @@ StructMetaTypeDescr::create(JSContext* c
             return nullptr;
         if (!stringBuffer.append(JSID_TO_ATOM(id)))
             return nullptr;
         if (!stringBuffer.append(": "))
             return nullptr;
         if (!stringBuffer.append(&fieldType->stringRepr()))
             return nullptr;
 
-        // Offset of this field is the current total size adjusted for
-        // the field's alignment.
-        CheckedInt32 offset = RoundUpToAlignment(sizeSoFar, fieldType->alignment());
+        CheckedInt32 offset = layout.addField(fieldType->alignment(), fieldType->size());
         if (!offset.isValid()) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPEDOBJECT_TOO_BIG);
             return nullptr;
         }
         MOZ_ASSERT(offset.value() >= 0);
         if (!fieldOffsets.append(Int32Value(offset.value())))
             return nullptr;
 
         // userFieldOffsets[id] = offset
         RootedValue offsetValue(cx, Int32Value(offset.value()));
         if (!DefineDataProperty(cx, userFieldOffsets, id, offsetValue,
                                 JSPROP_READONLY | JSPROP_PERMANENT))
         {
             return nullptr;
         }
-
-        // Add space for this field to the total struct size.
-        sizeSoFar = offset + fieldType->size();
-        if (!sizeSoFar.isValid()) {
-            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPEDOBJECT_TOO_BIG);
-            return nullptr;
-        }
-
-        // Struct is opaque if any field is opaque
-        if (fieldType->opaque())
-            opaque = true;
-
-        // Alignment of the struct is the max of the alignment of its fields.
-        alignment = js::Max(alignment, fieldType->alignment());
     }
 
     // Complete string representation.
     if (!stringBuffer.append("})"))
         return nullptr;
 
     RootedAtom stringRepr(cx, stringBuffer.finishAtom());
     if (!stringRepr)
         return nullptr;
 
-    // Adjust the total size to be a multiple of the final alignment.
-    CheckedInt32 totalSize = RoundUpToAlignment(sizeSoFar, alignment);
+    int32_t alignment;
+    CheckedInt32 totalSize = layout.close(&alignment);
     if (!totalSize.isValid()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPEDOBJECT_TOO_BIG);
         return nullptr;
     }
 
     // Now create the resulting type descriptor.
-    RootedObject structTypePrototype(cx, GetPrototype(cx, metaTypeDescr));
-    if (!structTypePrototype)
-        return nullptr;
 
     Rooted<StructTypeDescr*> descr(cx);
     descr = NewObjectWithGivenProto<StructTypeDescr>(cx, structTypePrototype, SingletonObject);
     if (!descr)
         return nullptr;
 
     descr->initReservedSlot(JS_DESCR_SLOT_KIND, Int32Value(type::Struct));
     descr->initReservedSlot(JS_DESCR_SLOT_STRING_REPR, StringValue(stringRepr));
@@ -1122,17 +1171,17 @@ DefineSimpleTypeDescr(JSContext* cx,
     if (!descr)
         return false;
 
     descr->initReservedSlot(JS_DESCR_SLOT_KIND, Int32Value(T::Kind));
     descr->initReservedSlot(JS_DESCR_SLOT_STRING_REPR, StringValue(className));
     descr->initReservedSlot(JS_DESCR_SLOT_ALIGNMENT, Int32Value(T::alignment(type)));
     descr->initReservedSlot(JS_DESCR_SLOT_SIZE, Int32Value(AssertedCast<int32_t>(T::size(type))));
     descr->initReservedSlot(JS_DESCR_SLOT_OPAQUE, BooleanValue(T::Opaque));
-    descr->initReservedSlot(JS_DESCR_SLOT_TYPE, Int32Value(type));
+    descr->initReservedSlot(JS_DESCR_SLOT_TYPE, Int32Value(int32_t(type)));
 
     if (!CreateUserSizeAndAlignmentProperties(cx, descr))
         return false;
 
     if (!JS_DefineFunctions(cx, descr, T::typeObjectMethods))
         return false;
 
     // Create the typed prototype for the scalar type. This winds up
@@ -2735,32 +2784,32 @@ class MemoryInitVisitor {
 };
 
 } // namespace
 
 void
 MemoryInitVisitor::visitReference(ReferenceTypeDescr& descr, uint8_t* mem)
 {
     switch (descr.type()) {
-      case ReferenceTypeDescr::TYPE_ANY:
+      case ReferenceType::TYPE_ANY:
       {
         js::GCPtrValue* heapValue = reinterpret_cast<js::GCPtrValue*>(mem);
         heapValue->init(UndefinedValue());
         return;
       }
 
-      case ReferenceTypeDescr::TYPE_OBJECT:
+      case ReferenceType::TYPE_OBJECT:
       {
         js::GCPtrObject* objectPtr =
             reinterpret_cast<js::GCPtrObject*>(mem);
         objectPtr->init(nullptr);
         return;
       }
 
-      case ReferenceTypeDescr::TYPE_STRING:
+      case ReferenceType::TYPE_STRING:
       {
         js::GCPtrString* stringPtr =
             reinterpret_cast<js::GCPtrString*>(mem);
         stringPtr->init(rt_->emptyString);
         return;
       }
     }
 
@@ -2805,31 +2854,31 @@ class MemoryTracingVisitor {
 };
 
 } // namespace
 
 void
 MemoryTracingVisitor::visitReference(ReferenceTypeDescr& descr, uint8_t* mem)
 {
     switch (descr.type()) {
-      case ReferenceTypeDescr::TYPE_ANY:
+      case ReferenceType::TYPE_ANY:
       {
         GCPtrValue* heapValue = reinterpret_cast<js::GCPtrValue*>(mem);
         TraceEdge(trace_, heapValue, "reference-val");
         return;
       }
 
-      case ReferenceTypeDescr::TYPE_OBJECT:
+      case ReferenceType::TYPE_OBJECT:
       {
         GCPtrObject* objectPtr = reinterpret_cast<js::GCPtrObject*>(mem);
         TraceNullableEdge(trace_, objectPtr, "reference-obj");
         return;
       }
 
-      case ReferenceTypeDescr::TYPE_STRING:
+      case ReferenceType::TYPE_STRING:
       {
         GCPtrString* stringPtr = reinterpret_cast<js::GCPtrString*>(mem);
         TraceNullableEdge(trace_, stringPtr, "reference-str");
         return;
       }
     }
 
     MOZ_CRASH("Invalid kind");
@@ -2859,19 +2908,19 @@ struct TraceListVisitor {
 
 } // namespace
 
 void
 TraceListVisitor::visitReference(ReferenceTypeDescr& descr, uint8_t* mem)
 {
     VectorType* offsets;
     switch (descr.type()) {
-      case ReferenceTypeDescr::TYPE_ANY: offsets = &valueOffsets; break;
-      case ReferenceTypeDescr::TYPE_OBJECT: offsets = &objectOffsets; break;
-      case ReferenceTypeDescr::TYPE_STRING: offsets = &stringOffsets; break;
+      case ReferenceType::TYPE_ANY: offsets = &valueOffsets; break;
+      case ReferenceType::TYPE_OBJECT: offsets = &objectOffsets; break;
+      case ReferenceType::TYPE_STRING: offsets = &stringOffsets; break;
       default: MOZ_CRASH("Invalid kind");
     }
 
     AutoEnterOOMUnsafeRegion oomUnsafe;
     if (!offsets->append((uintptr_t) mem))
         oomUnsafe.crash("TraceListVisitor::visitReference");
 }
 
--- a/js/src/builtin/TypedObject.h
+++ b/js/src/builtin/TypedObject.h
@@ -2,16 +2,18 @@
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef builtin_TypedObject_h
 #define builtin_TypedObject_h
 
+#include "mozilla/CheckedInt.h"
+
 #include "builtin/TypedObjectConstants.h"
 #include "gc/WeakMap.h"
 #include "js/Conversions.h"
 #include "vm/ArrayBufferObject.h"
 #include "vm/JSObject.h"
 #include "vm/ShapedObject.h"
 
 /*
@@ -281,53 +283,55 @@ class ScalarTypeDescr : public SimpleTyp
     macro_(Scalar::Float32, float,    float32)                  \
     macro_(Scalar::Float64, double,   float64)
 
 // Must be in same order as the enum ScalarTypeDescr::Type:
 #define JS_FOR_EACH_SCALAR_TYPE_REPR(macro_)                    \
     JS_FOR_EACH_UNIQUE_SCALAR_TYPE_REPR_CTYPE(macro_)           \
     macro_(Scalar::Uint8Clamped, uint8_t, uint8Clamped)
 
+enum class ReferenceType {
+    TYPE_ANY = JS_REFERENCETYPEREPR_ANY,
+    TYPE_OBJECT = JS_REFERENCETYPEREPR_OBJECT,
+    TYPE_STRING = JS_REFERENCETYPEREPR_STRING
+};
+
 // Type for reference type constructors like `Any`, `String`, and
 // `Object`. All such type constructors share a common js::Class and
 // JSFunctionSpec. All these types are opaque.
 class ReferenceTypeDescr : public SimpleTypeDescr
 {
   public:
     // Must match order of JS_FOR_EACH_REFERENCE_TYPE_REPR below
-    enum Type {
-        TYPE_ANY = JS_REFERENCETYPEREPR_ANY,
-        TYPE_OBJECT = JS_REFERENCETYPEREPR_OBJECT,
-        TYPE_STRING = JS_REFERENCETYPEREPR_STRING,
-    };
-    static const int32_t TYPE_MAX = TYPE_STRING + 1;
+    typedef ReferenceType Type;
     static const char* typeName(Type type);
 
+    static const int32_t TYPE_MAX = int32_t(ReferenceType::TYPE_STRING) + 1;
     static const type::Kind Kind = type::Reference;
     static const bool Opaque = true;
     static const Class class_;
     static uint32_t size(Type t);
     static uint32_t alignment(Type t);
     static const JSFunctionSpec typeObjectMethods[];
 
-    ReferenceTypeDescr::Type type() const {
-        return (ReferenceTypeDescr::Type) getReservedSlot(JS_DESCR_SLOT_TYPE).toInt32();
+    ReferenceType type() const {
+        return (ReferenceType) getReservedSlot(JS_DESCR_SLOT_TYPE).toInt32();
     }
 
     const char* typeName() const {
         return typeName(type());
     }
 
     static MOZ_MUST_USE bool call(JSContext* cx, unsigned argc, Value* vp);
 };
 
 #define JS_FOR_EACH_REFERENCE_TYPE_REPR(macro_) \
-    macro_(ReferenceTypeDescr::TYPE_ANY, GCPtrValue, Any) \
-    macro_(ReferenceTypeDescr::TYPE_OBJECT, GCPtrObject, Object) \
-    macro_(ReferenceTypeDescr::TYPE_STRING, GCPtrString, string)
+    macro_(ReferenceType::TYPE_ANY, GCPtrValue, Any) \
+    macro_(ReferenceType::TYPE_OBJECT, GCPtrObject, Object) \
+    macro_(ReferenceType::TYPE_STRING, GCPtrString, string)
 
 // Type descriptors whose instances are objects and hence which have
 // an associated `prototype` property.
 class ComplexTypeDescr : public TypeDescr
 {
   public:
     // Returns the prototype that instances of this type descriptor
     // will have.
@@ -430,29 +434,59 @@ class ArrayTypeDescr : public ComplexTyp
  */
 class StructMetaTypeDescr : public NativeObject
 {
   private:
     static JSObject* create(JSContext* cx, HandleObject structTypeGlobal,
                             HandleObject fields);
 
   public:
+    // The prototype cannot be null.
+    // The names in `ids` must all be non-numeric.
+    // The type objects in `fieldTypeObjs` must all be TypeDescr objects.
+    static StructTypeDescr* createFromArrays(JSContext* cx,
+                                             HandleObject structTypePrototype,
+                                             bool opaque,
+                                             AutoIdVector& ids,
+                                             AutoValueVector& fieldTypeObjs);
+
     // Properties and methods to be installed on StructType.prototype,
     // and hence inherited by all struct type objects:
     static const JSPropertySpec typeObjectProperties[];
     static const JSFunctionSpec typeObjectMethods[];
 
     // Properties and methods to be installed on StructType.prototype.prototype,
     // and hence inherited by all struct *typed* objects:
     static const JSPropertySpec typedObjectProperties[];
     static const JSFunctionSpec typedObjectMethods[];
 
     // This is the function that gets called when the user
     // does `new StructType(...)`. It produces a struct type object.
     static MOZ_MUST_USE bool construct(JSContext* cx, unsigned argc, Value* vp);
+
+    class Layout
+    {
+        // Can call addField() directly.
+        friend class StructMetaTypeDescr;
+
+        mozilla::CheckedInt32 sizeSoFar = 0;
+        int32_t structAlignment = 1;
+
+        mozilla::CheckedInt32 addField(int32_t fieldAlignment, int32_t fieldSize);
+
+      public:
+        // The field adders return the offset of the the field.
+        mozilla::CheckedInt32 addScalar(Scalar::Type type);
+        mozilla::CheckedInt32 addReference(ReferenceType type);
+
+        // The close method rounds up the structure size to the appropriate
+        // alignment and returns that size.  If `alignment` is not NULL then
+        // return the structure alignment through that pointer.
+        mozilla::CheckedInt32 close(int32_t* alignment = nullptr);
+    };
 };
 
 class StructTypeDescr : public ComplexTypeDescr
 {
   public:
     static const Class class_;
 
     // Returns the number of fields defined in this struct.
--- a/js/src/ds/LifoAlloc.cpp
+++ b/js/src/ds/LifoAlloc.cpp
@@ -3,35 +3,41 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ds/LifoAlloc.h"
 
 #include "mozilla/MathAlgorithms.h"
 
+#include "ds/MemoryProtectionExceptionHandler.h"
+
+#ifdef LIFO_CHUNK_PROTECT
+# include "gc/Memory.h"
+#endif
+
 using namespace js;
 
 using mozilla::RoundUpPow2;
 using mozilla::tl::BitSize;
 
 namespace js {
 namespace detail {
 
 /* static */
 UniquePtr<BumpChunk>
-BumpChunk::newWithCapacity(size_t size)
+BumpChunk::newWithCapacity(size_t size, bool protect)
 {
     MOZ_ASSERT(RoundUpPow2(size) == size);
     MOZ_ASSERT(size >= sizeof(BumpChunk));
     void* mem = js_malloc(size);
     if (!mem)
         return nullptr;
 
-    UniquePtr<BumpChunk> result(new (mem) BumpChunk(size));
+    UniquePtr<BumpChunk> result(new (mem) BumpChunk(size, protect));
 
     // We assume that the alignment of LIFO_ALLOC_ALIGN is less than that of the
     // underlying memory allocator -- creating a new BumpChunk should always
     // satisfy the LIFO_ALLOC_ALIGN alignment constraint.
     MOZ_ASSERT(AlignPtr(result->begin()) == result->begin());
     return result;
 }
 
@@ -39,27 +45,119 @@ bool
 BumpChunk::canAlloc(size_t n)
 {
     uint8_t* aligned = AlignPtr(bump_);
     uint8_t* newBump = aligned + n;
     // bump_ <= newBump, is necessary to catch overflow.
     return bump_ <= newBump && newBump <= capacity_;
 }
 
+#ifdef LIFO_CHUNK_PROTECT
+
+static const uint8_t*
+AlignPtrUp(const uint8_t* ptr, uintptr_t align) {
+    MOZ_ASSERT(mozilla::IsPowerOfTwo(align));
+    uintptr_t uptr = uintptr_t(ptr);
+    uintptr_t diff = uptr & (align - 1);
+    diff = (align - diff) & (align - 1);
+    uptr = uptr + diff;
+    return (uint8_t*) uptr;
+}
+
+static const uint8_t*
+AlignPtrDown(const uint8_t* ptr, uintptr_t align) {
+    MOZ_ASSERT(mozilla::IsPowerOfTwo(align));
+    uintptr_t uptr = uintptr_t(ptr);
+    uptr = uptr & ~(align - 1);
+    return (uint8_t*) uptr;
+}
+
+void
+BumpChunk::setRWUntil(Loc loc) const
+{
+    if (!protect_)
+        return;
+
+    uintptr_t pageSize = gc::SystemPageSize();
+    // The allocated chunks might not be aligned on page boundaries. This code
+    // is used to ensure that we are changing the memory protection of pointers
+    // which are within the range of the BumpChunk, or that the range formed by
+    // [b .. e] is empty.
+    const uint8_t* b = base();
+    const uint8_t* e = capacity_;
+    b = AlignPtrUp(b, pageSize);
+    e = AlignPtrDown(e, pageSize);
+    if (e < b)
+        e = b;
+    // The mid-point is aligned to the next page, and clamp to the end-point to
+    // ensure that it remains in the [b .. e] range.
+    const uint8_t* m = nullptr;
+    switch (loc) {
+      case Loc::Header:
+        m = b;
+        break;
+      case Loc::Allocated:
+        m = begin();
+        break;
+      case Loc::Reserved:
+        m = end();
+        break;
+      case Loc::End:
+        m = e;
+        break;
+    }
+    m = AlignPtrUp(m, pageSize);
+    if (e < m)
+        m = e;
+
+    if (b < m)
+        gc::UnprotectPages(const_cast<uint8_t*>(b), m - b);
+    // Note: We could use no-access protection for everything after begin(), but
+    // we need to read capabilities for reading the bump_ / capacity_ fields
+    // from this function to unprotect the memory later.
+    if (m < e)
+        gc::MakePagesReadOnly(const_cast<uint8_t*>(m), e - m);
+}
+
+// The memory protection handler is catching memory accesses error on the
+// regions registered into it. These method, instead of registering sub-ranges
+// of the BumpChunk within setRWUntil, we just register the full BumpChunk
+// ranges, and let the MemoryProtectionExceptionHandler catch bad memory
+// accesses when it is being protected by setRWUntil.
+void
+BumpChunk::addMProtectHandler() const
+{
+    if (!protect_)
+        return;
+    js::MemoryProtectionExceptionHandler::addRegion(const_cast<uint8_t*>(base()), capacity_ - base());
+}
+
+void
+BumpChunk::removeMProtectHandler() const
+{
+    if (!protect_)
+        return;
+    js::MemoryProtectionExceptionHandler::removeRegion(const_cast<uint8_t*>(base()));
+}
+
+#endif
+
 } // namespace detail
 } // namespace js
 
 void
 LifoAlloc::freeAll()
 {
     while (!chunks_.empty()) {
+        chunks_.begin()->setRWUntil(Loc::End);
         BumpChunk bc = chunks_.popFirst();
         decrementCurSize(bc->computedSizeOfIncludingThis());
     }
     while (!unused_.empty()) {
+        unused_.begin()->setRWUntil(Loc::End);
         BumpChunk bc = unused_.popFirst();
         decrementCurSize(bc->computedSizeOfIncludingThis());
     }
 
     // Nb: maintaining curSize_ correctly isn't easy.  Fortunately, this is an
     // excellent sanity check.
     MOZ_ASSERT(curSize_ == 0);
 }
@@ -84,54 +182,70 @@ LifoAlloc::newChunkWithCapacity(size_t n
             return nullptr;
         }
 
         chunkSize = RoundUpPow2(allocSizeWithCanaries);
     } else {
         chunkSize = defaultChunkSize_;
     }
 
+    bool protect = false;
+#ifdef LIFO_CHUNK_PROTECT
+    protect = protect_;
+#endif
+
     // Create a new BumpChunk, and allocate space for it.
-    BumpChunk result = detail::BumpChunk::newWithCapacity(chunkSize);
+    BumpChunk result = detail::BumpChunk::newWithCapacity(chunkSize, protect);
     if (!result)
         return nullptr;
     MOZ_ASSERT(result->computedSizeOfIncludingThis() == chunkSize);
     return result;
 }
 
 bool
 LifoAlloc::getOrCreateChunk(size_t n)
 {
+    // This function is adding a new BumpChunk in which all upcoming allocation
+    // would be made. Thus, we protect against out-of-bounds the last chunk in
+    // which we did our previous allocations.
+    if (!chunks_.empty())
+        chunks_.last()->setRWUntil(Loc::Reserved);
+
     // Look for existing unused BumpChunks to satisfy the request, and pick the
     // first one which is large enough, and move it into the list of used
     // chunks.
     if (!unused_.empty()) {
         if (unused_.begin()->canAlloc(n)) {
             chunks_.append(unused_.popFirst());
+            chunks_.last()->setRWUntil(Loc::End);
             return true;
         }
 
         BumpChunkList::Iterator e(unused_.end());
         for (BumpChunkList::Iterator i(unused_.begin()); i->next() != e.get(); ++i) {
             detail::BumpChunk* elem = i->next();
             MOZ_ASSERT(elem->empty());
             if (elem->canAlloc(n)) {
                 BumpChunkList temp = unused_.splitAfter(i.get());
                 chunks_.append(temp.popFirst());
                 unused_.appendAll(std::move(temp));
+                chunks_.last()->setRWUntil(Loc::End);
                 return true;
             }
         }
     }
 
     // Allocate a new BumpChunk with enough space for the next allocation.
     BumpChunk newChunk = newChunkWithCapacity(n);
     if (!newChunk)
         return false;
     size_t size = newChunk->computedSizeOfIncludingThis();
+    // The last chunk in which allocations are performed should be protected
+    // with setRWUntil(Loc::End), but this is not necessary here because any new
+    // allocation should be protected as RW already.
     chunks_.append(std::move(newChunk));
     incrementCurSize(size);
     return true;
 }
 
 void
 LifoAlloc::transferFrom(LifoAlloc* other)
 {
--- a/js/src/ds/LifoAlloc.h
+++ b/js/src/ds/LifoAlloc.h
@@ -211,24 +211,34 @@ AlignPtr(uint8_t* orig) {
 // and the deallocation.
 //
 // This structure is only move-able, but not copyable.
 class BumpChunk : public SingleLinkedListElement<BumpChunk>
 {
   private:
     // Pointer to the last byte allocated in this chunk.
     uint8_t* bump_;
-    // Pointer to the last byte available in this chunk.
-    const uint8_t* capacity_;
+    // Pointer to the first byte after this chunk.
+    uint8_t* const capacity_;
 
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
     // Magic number used to check against poisoned values.
-    const uintptr_t magic_;
-    static constexpr uintptr_t magicNumber =
-        sizeof(uintptr_t) == 4 ? uintptr_t(0x4c69666f) : uintptr_t(0x4c69666f42756d70);
+    const uintptr_t magic_ : 24;
+    static constexpr uintptr_t magicNumber = uintptr_t(0x4c6966);
+#endif
+