Merge inbound to mozilla-central a=merge
authorCoroiu Cristina <ccoroiu@mozilla.com>
Wed, 11 Apr 2018 00:56:08 +0300
changeset 412696 0528a414c2a86dad0623779abde5301d37337934
parent 412695 d42671c2e69d953785fe8acc1556007172a119d5 (current diff)
parent 412627 09c70b07e2e4f6df6b7ad750e8d46a00209648fb (diff)
child 412697 9ad2b8aabfae6a1c47599fc66d781d5a2a3aa38a
child 412737 14c7879f7eb7a4d7306ac73c08ef741f435e5db7
child 412749 d1db4d5030c827ada3c01ec9d2ab5d665de2cd59
push id101981
push useraiakab@mozilla.com
push dateTue, 10 Apr 2018 22:18:59 +0000
treeherdermozilla-inbound@9ad2b8aabfae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.0a1
first release with
nightly linux32
0528a414c2a8 / 61.0a1 / 20180410220129 / files
nightly linux64
0528a414c2a8 / 61.0a1 / 20180410220129 / files
nightly mac
0528a414c2a8 / 61.0a1 / 20180410220129 / files
nightly win32
0528a414c2a8 / 61.0a1 / 20180410220129 / files
nightly win64
0528a414c2a8 / 61.0a1 / 20180410220129 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central a=merge
accessible/tests/mochitest/textcaret/test_browserui.xul
--- a/accessible/tests/mochitest/name/test_general.xul
+++ b/accessible/tests/mochitest/name/test_general.xul
@@ -48,30 +48,30 @@
       testName("rememberAfter", "days");
 
       // Anonymous content (see name.xbl#third)
       var anonBtn = getAccessible("labelledby_box_anon").lastChild;
       testName(anonBtn, "It's a cool button");
 
       //////////////////////////////////////////////////////////////////////////
       // Name from subtree (single relation labelled_by).
-      
+
       // Gets the name from text nodes contained by nested elements.
       testName("btn_labelledby_mixed", "nomore text");
 
       // Gets the name from text nodes and selected item of menulist
       // (other items are ignored).
       testName("btn_labelledby_mixed_menulist",
                "nomore text selected item more text");
-      
+
       // Gets the name from text nodes contained by nested elements, ignores
       // hidden elements (bug 443081).
       testName("btn_labelledby_mixed_hidden_child", "nomore text2");
 
-      // Gets the name from hidden text nodes contained by nested elements, 
+      // Gets the name from hidden text nodes contained by nested elements,
       // (label element is hidden entirely), (bug 443081)
       testName("btn_labelledby_mixed_hidden", "lala more hidden text");
 
 
       //////////////////////////////////////////////////////////////////////////
       // Name for nsIDOMXULLabeledControlElement.
 
       // Gets the name from @label attribute.
@@ -229,17 +229,17 @@
 
   <!-- aria-labelledby, multiple relations -->
   <box class="third" id="labelledby_box_anon" role="group" />
 
   <!-- trick aria-labelledby -->
   <checkbox id="rememberHistoryDays"
             label="Remember "
             aria-labelledby="rememberHistoryDays historyDays rememberAfter"/>
-  <textbox id="historyDays" type="number" size="3" value="3"
+  <textbox id="historyDays" type="number" value="3"
            aria-labelledby="rememberHistoryDays historyDays rememberAfter"/>
   <label id="rememberAfter">days</label>
 
   <!-- the name from subtree, mixed content -->
   <description id="labelledby_mixed">
     no<description>more text</description>
   </description>
   <button id="btn_labelledby_mixed"
@@ -308,18 +308,18 @@
     <button id="btn_label_4"/>
   </hbox>
 
   <!-- label element, anonymous content -->
   <box id="box_label_anon1"
        class="first"
        role="group"/>
 
-  <box id="box_label_anon2" 
-       class="second" 
+  <box id="box_label_anon2"
+       class="second"
        role="group"/>
 
   <!-- tooltiptext -->
   <box id="box_tooltiptext"
        role="group"
        tooltiptext="tooltiptext label"/>
 
   <!-- the name from @title of toolbaritem -->
@@ -374,9 +374,8 @@
                   label="Send an E-mail"/>
       </menupopup>
     </menulist>
   </hbox>
 
   </vbox> <!-- close tests area -->
   </hbox> <!-- close main area -->
 </window>
-
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -441,16 +441,20 @@
         </menupopup>
       </menu>
       <menuseparator id="inspect-separator" hidden="true"/>
       <menuitem id="context-inspect"
                 hidden="true"
                 label="&inspectContextMenu.label;"
                 accesskey="&inspectContextMenu.accesskey;"
                 oncommand="gContextMenu.inspectNode();"/>
+      <menuitem id="context-inspect-a11y"
+                hidden="true"
+                label="&inspectA11YContextMenu.label;"
+                oncommand="gContextMenu.inspectA11Y();"/>
       <menuseparator id="context-media-eme-separator" hidden="true"/>
       <menuitem id="context-media-eme-learnmore"
                 class="menuitem-iconic"
                 hidden="true"
                 label="&emeLearnMoreContextMenu.label;"
                 accesskey="&emeLearnMoreContextMenu.accesskey;"
                 oncommand="gContextMenu.drmLearnMore(event);"
                 onclick="checkForMiddleClick(this, event);"/>
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -434,24 +434,34 @@ nsContextMenu.prototype = {
                        this.onImage || this.onCanvas ||
                        this.onVideo || this.onAudio ||
                        this.onLink || this.onTextInput);
 
     var showInspect = this.inTabBrowser &&
                       Services.prefs.getBoolPref("devtools.inspector.enabled", true) &&
                       !Services.prefs.getBoolPref("devtools.policy.disabled", false);
 
+    var showInspectA11Y = showInspect &&
+                          // Only when accessibility service started.
+                          Services.appinfo.accessibilityEnabled &&
+                          this.inTabBrowser &&
+                          Services.prefs.getBoolPref("devtools.enabled", true) &&
+                          Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
+                          !Services.prefs.getBoolPref("devtools.policy.disabled", false);
+
     this.showItem("context-viewsource", shouldShow);
     this.showItem("context-viewinfo", shouldShow);
     // The page info is broken for WebExtension popups, as the browser is
     // destroyed when the popup is closed.
     this.setItemAttr("context-viewinfo", "disabled", this.webExtBrowserType === "popup");
     this.showItem("inspect-separator", showInspect);
     this.showItem("context-inspect", showInspect);
 
+    this.showItem("context-inspect-a11y", showInspectA11Y);
+
     this.showItem("context-sep-viewsource", shouldShow);
 
     // Set as Desktop background depends on whether an image was clicked on,
     // and only works if we have a shell service.
     var haveSetDesktopBackground = false;
 
     if (AppConstants.HAVE_SHELL_SERVICE &&
         Services.policies.isAllowed("setDesktopBackground")) {
@@ -749,16 +759,20 @@ nsContextMenu.prototype = {
   openPasswordManager() {
     LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
   },
 
   inspectNode() {
     return DevToolsShim.inspectNode(gBrowser.selectedTab, this.targetSelectors);
   },
 
+  inspectA11Y() {
+    return DevToolsShim.inspectA11Y(gBrowser.selectedTab, this.targetSelectors);
+  },
+
   _openLinkInParameters(extra) {
     let params = { charset: gContextMenuContentData.charSet,
                    originPrincipal: this.principal,
                    triggeringPrincipal: this.principal,
                    referrerURI: gContextMenuContentData.documentURIObject,
                    referrerPolicy: gContextMenuContentData.referrerPolicy,
                    frameOuterWindowID: gContextMenuContentData.frameOuterWindowID,
                    noReferrer: this.linkHasNoReferrer || this.onPlainTextLink };
--- a/browser/base/content/test/general/contextmenu_common.js
+++ b/browser/base/content/test/general/contextmenu_common.js
@@ -316,16 +316,22 @@ async function test_contextmenu(selector
 
   if (menuItems) {
     if (Services.prefs.getBoolPref("devtools.inspector.enabled", true)) {
       let inspectItems = ["---", null,
                           "context-inspect", true];
       menuItems = menuItems.concat(inspectItems);
     }
 
+    if (Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
+        Services.appinfo.accessibilityEnabled) {
+      let inspectA11YItems = ["context-inspect-a11y", true];
+      menuItems = menuItems.concat(inspectA11YItems);
+    }
+
     if (options.maybeScreenshotsPresent &&
         !Services.prefs.getBoolPref("extensions.screenshots.disabled", false)) {
       let screenshotItems = [
         "---", null,
         "screenshots_mozilla_org_create-screenshot", true
       ];
 
       menuItems = menuItems.concat(screenshotItems);
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
@@ -35,16 +35,17 @@ let extData = {
 
 let contextMenuItems = {
   "context-navigation": "hidden",
   "context-sep-navigation": "hidden",
   "context-viewsource": "",
   "context-viewinfo": "disabled",
   "inspect-separator": "hidden",
   "context-inspect": "hidden",
+  "context-inspect-a11y": "hidden",
   "context-bookmarkpage": "hidden",
 };
 
 add_task(async function browseraction_popup_contextmenu() {
   let extension = ExtensionTestUtils.loadExtension(extData);
   await extension.startup();
 
   await clickBrowserAction(extension, window);
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_contextMenu.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_contextMenu.js
@@ -42,16 +42,17 @@ let extData = {
 
 let contextMenuItems = {
   "context-navigation": "hidden",
   "context-sep-navigation": "hidden",
   "context-viewsource": "",
   "context-viewinfo": "disabled",
   "inspect-separator": "hidden",
   "context-inspect": "hidden",
+  "context-inspect-a11y": "hidden",
   "context-bookmarkpage": "hidden",
 };
 
 add_task(async function pageaction_popup_contextmenu() {
   let extension = ExtensionTestUtils.loadExtension(extData);
   await extension.startup();
   await extension.awaitMessage("action-shown");
 
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js
@@ -45,16 +45,17 @@ let extData = {
 
 let contextMenuItems = {
   "context-navigation": "hidden",
   "context-sep-navigation": "hidden",
   "context-viewsource": "",
   "context-viewinfo": "",
   "inspect-separator": "hidden",
   "context-inspect": "hidden",
+  "context-inspect-a11y": "hidden",
   "context-bookmarkpage": "hidden",
 };
 
 add_task(async function sidebar_contextmenu() {
   let extension = ExtensionTestUtils.loadExtension(extData);
   await extension.startup();
   // Test sidebar is opened on install
   await extension.awaitMessage("sidebar");
--- a/browser/components/preferences/connection.xul
+++ b/browser/components/preferences/connection.xul
@@ -72,17 +72,17 @@
             <row align="center">
               <hbox pack="end">
                 <label accesskey="&http2.accesskey;" control="networkProxyHTTP">&http2.label;</label>
               </hbox>
               <hbox align="center">
                 <textbox id="networkProxyHTTP" flex="1"
                          preference="network.proxy.http" onsyncfrompreference="return gConnectionsDialog.readHTTPProxyServer();"/>
                 <label accesskey="&HTTPport.accesskey;" control="networkProxyHTTP_Port">&port2.label;</label>
-                <textbox id="networkProxyHTTP_Port" type="number" max="65535" size="5"
+                <textbox id="networkProxyHTTP_Port" class="proxy-port-input" type="number" max="65535" hidespinbuttons="true"
                          preference="network.proxy.http_port" onsyncfrompreference="return gConnectionsDialog.readHTTPProxyPort();"/>
               </hbox>
             </row>
             <row>
               <hbox/>
               <hbox>
                 <checkbox id="shareAllProxies" label="&shareproxy.label;" accesskey="&shareproxy.accesskey;"
                           preference="network.proxy.share_proxy_settings"
@@ -92,42 +92,42 @@
             <row align="center">
               <hbox pack="end">
                 <label accesskey="&ssl2.accesskey;" control="networkProxySSL">&ssl2.label;</label>
               </hbox>
               <hbox align="center">
                 <textbox id="networkProxySSL" flex="1" preference="network.proxy.ssl"
                          onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ssl', false);"/>
                 <label accesskey="&SSLport.accesskey;" control="networkProxySSL_Port">&port2.label;</label>
-                <textbox id="networkProxySSL_Port" type="number" max="65535" size="5" preference="network.proxy.ssl_port"
-                         onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ssl', true);"/>
+                <textbox id="networkProxySSL_Port" class="proxy-port-input" type="number" max="65535" size="5" preference="network.proxy.ssl_port"
+                         hidespinbuttons="true" onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ssl', true);"/>
               </hbox>
             </row>
             <row align="center">
               <hbox pack="end">
                 <label accesskey="&ftp2.accesskey;" control="networkProxyFTP">&ftp2.label;</label>
               </hbox>
               <hbox align="center">
                 <textbox id="networkProxyFTP" flex="1" preference="network.proxy.ftp"
                          onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ftp', false);"/>
                 <label accesskey="&FTPport.accesskey;" control="networkProxyFTP_Port">&port2.label;</label>
-                <textbox id="networkProxyFTP_Port" type="number" max="65535" size="5" preference="network.proxy.ftp_port"
-                         onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ftp', true);"/>
+                <textbox id="networkProxyFTP_Port" class="proxy-port-input" type="number" max="65535" size="5" preference="network.proxy.ftp_port"
+                         hidespinbuttons="true" onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ftp', true);"/>
               </hbox>
             </row>
             <row align="center">
               <hbox pack="end">
                 <label accesskey="&socks2.accesskey;" control="networkProxySOCKS">&socks2.label;</label>
               </hbox>
               <hbox align="center">
                 <textbox id="networkProxySOCKS" flex="1" preference="network.proxy.socks"
                          onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('socks', false);"/>
                 <label accesskey="&SOCKSport.accesskey;" control="networkProxySOCKS_Port">&port2.label;</label>
-                <textbox id="networkProxySOCKS_Port" type="number" max="65535" size="5" preference="network.proxy.socks_port"
-                         onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('socks', true);"/>
+                <textbox id="networkProxySOCKS_Port" class="proxy-port-input" type="number" max="65535" size="5" preference="network.proxy.socks_port"
+                         hidespinbuttons="true" onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('socks', true);"/>
               </hbox>
             </row>
             <row>
               <spacer/>
               <box pack="start">
               <radiogroup id="networkProxySOCKSVersion" orient="horizontal"
                           preference="network.proxy.socks_version">
                 <radio id="networkProxySOCKSVersion4" value="4" label="&socks4.label;" accesskey="&socks4.accesskey;" />
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -290,16 +290,18 @@ These should match what Safari and other
 <!ENTITY addons.commandkey            "A">
 
 <!ENTITY webDeveloperMenu.label       "Web Developer">
 <!ENTITY webDeveloperMenu.accesskey   "W">
 
 <!ENTITY inspectContextMenu.label     "Inspect Element">
 <!ENTITY inspectContextMenu.accesskey "Q">
 
+<!ENTITY inspectA11YContextMenu.label     "Inspect Accessibility Properties">
+
 <!ENTITY fileMenu.label         "File">
 <!ENTITY fileMenu.accesskey       "F">
 <!ENTITY newUserContext.label             "New Container Tab">
 <!ENTITY newUserContext.accesskey         "B">
 <!ENTITY newNavigatorCmd.label        "New Window">
 <!ENTITY newNavigatorCmd.key        "N">
 <!ENTITY newNavigatorCmd.accesskey      "N">
 <!ENTITY newPrivateWindow.label     "New Private Window">
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -688,17 +688,17 @@ var BrowserUITelemetry = {
     "keywordfield", "searchselect", "frame", "showonlythisframe",
     "openframeintab", "openframe", "reloadframe", "bookmarkframe", "saveframe",
     "printframe", "viewframesource", "viewframeinfo",
     "viewpartialsource-selection", "viewpartialsource-mathml",
     "viewsource", "viewinfo", "spell-check-enabled",
     "spell-add-dictionaries-main", "spell-dictionaries",
     "spell-dictionaries-menu", "spell-add-dictionaries",
     "bidi-text-direction-toggle", "bidi-page-direction-toggle", "inspect",
-    "media-eme-learn-more"
+    "inspect-a11y", "media-eme-learn-more"
   ]),
 
   _contextMenuInteractions: {},
 
   registerContextMenuInteraction(keys, itemID) {
     if (itemID) {
       if (itemID == "openlinkprivate") {
         // Don't record anything, not even an other-item count
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -884,8 +884,14 @@ menulist[indicator=true] > menupopup men
   background-size: contain;
   background-repeat: no-repeat;
   background-position: center;
 }
 
 .no-results-message[query*=🔥🦊] > .no-results-container {
   visibility: hidden;
 }
+
+/* Proxy port input */
+
+.proxy-port-input {
+  width: calc(5ch + 22px); /* 5 chars + 11px padding on both sides */
+}
--- a/build/mobile/remoteautomation.py
+++ b/build/mobile/remoteautomation.py
@@ -242,17 +242,19 @@ class RemoteAutomation(Automation):
                     args = args[1:]
                 url = args[-1:][0]
                 if url.startswith('/'):
                     # this is probably a reftest profile directory, not a url
                     url = None
                 else:
                     args = args[:-1]
                 if 'geckoview' in app:
-                    self.device.launch_geckoview_example(app, moz_env=env, extra_args=args, url=url)
+                    activity = "TestRunnerActivity"
+                    self.device.launch_activity(app, activity, e10s=True, moz_env=env,
+                                                extra_args=args, url=url)
                 else:
                     self.device.launch_fennec(app, moz_env=env, extra_args=args, url=url)
 
             # Setting timeout at 1 hour since on a remote device this takes much longer.
             # Temporarily increased to 90 minutes because no more chunks can be created.
             self.timeout = 5400
 
             # Used to buffer log messages until we meet a line break
--- a/devtools/client/accessibility/test/browser.ini
+++ b/devtools/client/accessibility/test/browser.ini
@@ -2,14 +2,15 @@
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/shared-redux-head.js
 
+[browser_accessibility_context_menu_browser.js]
 [browser_accessibility_context_menu_inspector.js]
 [browser_accessibility_mutations.js]
 [browser_accessibility_reload.js]
 [browser_accessibility_sidebar.js]
 [browser_accessibility_tree.js]
 [browser_accessibility_tree_nagivation.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/browser_accessibility_context_menu_browser.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "<h1 id=\"h1\">header</h1><p id=\"p\">paragraph</p>";
+
+add_task(async function testNoShowAccessibilityPropertiesContextMenu() {
+  let tab = await addTab(buildURL(TEST_URI));
+  let { linkedBrowser: browser } = tab;
+
+  let contextMenu = document.getElementById("contentAreaContextMenu");
+  let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+  await BrowserTestUtils.synthesizeMouse("#h1", 0, 0, {
+    type: "contextmenu",
+    button: 2,
+    centered: true,
+  }, browser);
+  await awaitPopupShown;
+
+  let inspectA11YPropsItem = contextMenu.querySelector("#context-inspect-a11y");
+  ok(inspectA11YPropsItem.hidden, "Accessibility tools are not enabled.");
+  contextMenu.hidePopup();
+  gBrowser.removeCurrentTab();
+});
+
+addA11YPanelTask("Test show accessibility properties context menu in browser.",
+  TEST_URI,
+  async function({ panel, toolbox, browser }) {
+    let headerSelector = "#h1";
+
+    let contextMenu = document.getElementById("contentAreaContextMenu");
+    let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+    await BrowserTestUtils.synthesizeMouse(headerSelector, 0, 0, {
+      type: "contextmenu",
+      button: 2,
+      centered: true,
+    }, browser);
+    await awaitPopupShown;
+
+    let inspectA11YPropsItem = contextMenu.querySelector("#context-inspect-a11y");
+
+    info("Triggering 'Inspect Accessibility Properties' and waiting for " +
+         "accessibility panel to open");
+    inspectA11YPropsItem.click();
+    contextMenu.hidePopup();
+
+    let selected = await panel.once("new-accessible-front-selected");
+    let expectedSelectedNode = await getNodeFront(headerSelector,
+                                                  toolbox.getPanel("inspector"));
+    let expectedSelected = await panel.walker.getAccessibleFor(expectedSelectedNode);
+    is(toolbox.getCurrentPanel(), panel, "Accessibility panel is currently selected");
+    is(selected, expectedSelected, "Accessible front selected correctly");
+  });
--- a/devtools/client/debugger/new/README.mozilla
+++ b/devtools/client/debugger/new/README.mozilla
@@ -1,13 +1,13 @@
 This is the debugger.html project output.
 See https://github.com/devtools-html/debugger.html
 
-Version 32.0
+Version 33.0
 
-Comparison: https://github.com/devtools-html/debugger.html/compare/release-31...release-32
+Comparison: https://github.com/devtools-html/debugger.html/compare/release-32...release-33
 
 Packages:
 - babel-plugin-transform-es2015-modules-commonjs @6.26.0
 - babel-preset-react @6.24.1
 - react @16.2.0
 - react-dom @16.2.0
 - webpack @3.11.0
--- a/devtools/client/debugger/new/debugger.css
+++ b/devtools/client/debugger/new/debugger.css
@@ -321,16 +321,28 @@ menuseparator {
   --object-color: var(--theme-body-color);
   --caption-color: var(--theme-highlight-blue);
   --location-color: var(--theme-content-color1);
   --source-link-color: var(--theme-highlight-blue);
   --node-color: var(--theme-highlight-bluegrey);
   --reference-color: var(--theme-highlight-purple);
 }
 
+.theme-firebug {
+  --number-color: #000088;
+  --string-color: #ff0000;
+  --null-color: #787878;
+  --object-color: DarkGreen;
+  --caption-color: #444444;
+  --location-color: #555555;
+  --source-link-color: blue;
+  --node-color: rgb(0, 0, 136);
+  --reference-color: rgb(102, 102, 255);
+}
+
 /******************************************************************************/
 
 .objectLink:hover {
   text-decoration: underline;
 }
 
 .inline {
   display: inline;
@@ -1907,16 +1919,28 @@ html .toggle-button.end.vertical svg {
   --location-color: var(--theme-comment);
   --source-link-color: var(--theme-highlight-blue);
   --node-color: var(--theme-highlight-purple);
   --reference-color: var(--theme-highlight-blue);
   --comment-node-color: var(--theme-comment);
   --stack-function-color: var(--theme-highlight-red);
 }
 
+.theme-firebug {
+  --number-color: #000088;
+  --string-color: #FF0000;
+  --null-color: #787878;
+  --object-color: DarkGreen;
+  --caption-color: #444444;
+  --location-color: #555555;
+  --source-link-color: blue;
+  --node-color: rgb(0, 0, 136);
+  --reference-color: rgb(102, 102, 255);
+}
+
 /******************************************************************************/
 
 .inline {
   display: inline;
   white-space: normal;
 }
 
 .objectBox-object {
@@ -2744,30 +2768,42 @@ debug-expression-error {
 .breakpoints-toggle {
   margin: 2px 3px;
 }
 
 .breakpoints-list * {
   user-select: none;
 }
 
+.breakpoints-list .breakpoint-heading {
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+
+.breakpoints-list .breakpoint-heading,
 .breakpoints-list .breakpoint {
   font-size: 12px;
   color: var(--theme-content-color1);
-  padding: 0.5em 1em 0.5em 0.5em;
   line-height: 1em;
   position: relative;
   transition: all 0.25s ease;
-}
-
-html[dir="rtl"] .breakpoints-list .breakpoint {
+  padding: 0.5em 1em 0.5em 0.5em;
+}
+
+.breakpoints-list .breakpoint {
+  display: flex;
+}
+
+html[dir="rtl"] .breakpoints-list .breakpoint,
+html[dir="rtl"] .breakpoints-list .breakpoint-heading {
   border-right: 4px solid transparent;
 }
 
-html:not([dir="rtl"]) .breakpoints-list .breakpoint {
+html:not([dir="rtl"]) .breakpoints-list .breakpoint,
+html:not([dir="rtl"]) .breakpoints-list .breakpoint-heading {
   border-left: 4px solid transparent;
 }
 
 .breakpoints-list .breakpoint:last-of-type {
   padding-bottom: 0.45em;
 }
 
 html:not([dir="rtl"]) .breakpoints-list .breakpoint.is-conditional {
@@ -2787,25 +2823,52 @@ html .breakpoints-list .breakpoint.pause
   color: var(--theme-comment);
   transition: color 0.5s linear;
 }
 
 .breakpoints-list .breakpoint:hover {
   background-color: var(--search-overlays-semitransparent);
 }
 
+.breakpoints-list .breakpoint .breakpoint-line,
+.breakpoints-list .breakpoint-label {
+  font-family: var(--monospace-font-family);
+}
+
+.breakpoints-list .breakpoint .breakpoint-line {
+  font-size: 11px;
+  color: var(--theme-comment);
+  padding-top: 3px;
+  padding-inline-end: 2px;
+  min-width: 14px;
+  text-align: right;
+}
+
+html[dir="rtl"] .breakpoints-list .breakpoint .breakpoint-line {
+  text-align: left;
+}
+
+.breakpoints-list .breakpoint:hover .breakpoint-line {
+  display: none;
+}
+
 .breakpoints-list .breakpoint.paused:hover {
   border-color: var(--breakpoint-active-color-hover);
 }
 
 .breakpoints-list .breakpoint-label {
   max-width: calc(100% - var(--breakpoint-expression-right-clear-space));
   display: inline-block;
   padding-inline-start: 2px;
+  padding-inline-end: 8px;
   cursor: default;
+  flex-grow: 1;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  padding-top: 3px;
 }
 
 .breakpoint-label .breakpoint-checkbox {
   margin-inline-start: 0;
   vertical-align: text-bottom;
 }
 
 .breakpoint-label .location {
@@ -2822,25 +2885,54 @@ html .breakpoints-list .breakpoint.pause
   order: 3;
 }
 
 .breakpoint .close-btn {
   position: absolute;
   offset-inline-end: 13px;
   offset-inline-start: auto;
   top: 9px;
+  display: none;
+}
+
+.breakpoint:hover .close-btn {
+  display: flex;
 }
 
 .breakpoint .close {
   visibility: hidden;
 }
 
 .breakpoint:hover .close {
   visibility: visible;
 }
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-lines {
+  padding: 0;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-sizer {
+  min-width: initial !important;
+}
+
+.breakpoints-list .breakpoint.disabled .CodeMirror.cm-s-mozilla-breakpoint {
+  transition: opacity 0.5s linear;
+  opacity: 0.5;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-line span[role=presentation] {
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: inline-block;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-code {
+  cursor: default;
+}
 /* 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/>. */
 
 .expression-input-form {
   width: 100%;
 }
 
@@ -2912,16 +3004,20 @@ html .breakpoints-list .breakpoint.pause
 :root.theme-light .expression-container:hover {
   background-color: var(--theme-selection-background-hover);
 }
 
 :root.theme-dark .expression-container:hover {
   background-color: var(--theme-selection-background-hover);
 }
 
+:root.theme-firebug .expression-container:hover {
+  background-color: var(--theme-selection-background-hover);
+}
+
 .tree  .tree-node:not(.focused):hover {
   background-color: transparent;
 }
 
 .expression-container__close-btn {
   position: absolute;
   offset-inline-end: 0px;
   top: 0px;
@@ -3057,17 +3153,18 @@ html .breakpoints-list .breakpoint.pause
   display: flex;
   justify-content: space-between;
   flex-direction: row;
   align-items: center;
   margin: 0;
   flex-shrink: 0;
 }
 
-.theme-light .frames .location {
+.theme-light .frames .location,
+.theme-firebug .frames .location {
   color: var(--theme-comment);
 }
 
 :root.theme-dark .frames .location {
   color: var(--theme-body-color);
   opacity: 0.6;
 }
 
@@ -3092,16 +3189,17 @@ html .breakpoints-list .breakpoint.pause
   color: white;
 }
 
 .frames ul li.selected i.annotation-logo svg path {
   fill: white;
 }
 
 :root.theme-light .frames ul li.selected .location,
+:root.theme-firebug .frames ul li.selected .location,
 :root.theme-dark .frames ul li.selected .location {
   color: white;
 }
 
 .show-more-container {
   display: flex;
 }
 
--- a/devtools/client/debugger/new/debugger.js
+++ b/devtools/client/debugger/new/debugger.js
@@ -3007,17 +3007,26 @@ module.exports = {
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.findScopeByName = exports.getASTLocation = undefined;
+exports.findScopeByName = exports.getASTLocation = exports.createEditor = undefined;
+
+var _createEditor = __webpack_require__(3628);
+
+Object.defineProperty(exports, "createEditor", {
+  enumerable: true,
+  get: function () {
+    return _createEditor.createEditor;
+  }
+});
 
 var _astBreakpointLocation = __webpack_require__(1416);
 
 Object.defineProperty(exports, "getASTLocation", {
   enumerable: true,
   get: function () {
     return _astBreakpointLocation.getASTLocation;
   }
@@ -5519,29 +5528,33 @@ function update(state = createPauseState
         });
       }
 
     case "MAP_FRAMES":
       {
         return _extends({}, state, { frames: action.frames });
       }
 
+    case "ADD_EXTRA":
+      {
+        return _extends({}, state, { extra: action.extra });
+      }
+
     case "ADD_SCOPES":
       {
-        const { frame, extra, status, value } = action;
+        const { frame, status, value } = action;
         const selectedFrameId = frame.id;
 
         const generated = _extends({}, state.frameScopes.generated, {
           [selectedFrameId]: {
             pending: status !== "done",
             scope: value
           }
         });
         return _extends({}, state, {
-          extra: extra,
           frameScopes: _extends({}, state.frameScopes, {
             generated
           })
         });
       }
 
     case "TRAVEL_TO":
       return _extends({}, state, action.data.paused);
@@ -6487,16 +6500,17 @@ function setSymbols(sourceId) {
 
     await dispatch({
       type: "SET_SYMBOLS",
       source: source.toJS(),
       [_promise.PROMISE]: (0, _parser.getSymbols)(source.id)
     });
 
     if ((0, _selectors.isPaused)(getState())) {
+      await dispatch((0, _pause.setExtra)());
       await dispatch((0, _pause.mapFrames)());
     }
 
     await dispatch(setPausePoints(sourceId));
     await dispatch(setSourceMetaData(sourceId));
   };
 }
 
@@ -7791,19 +7805,17 @@ var _ast = __webpack_require__(1638);
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 function getASTLocation(source, symbols, location) {
   if (source.isWasm || !symbols || symbols.loading) {
     return { name: undefined, offset: location };
   }
 
-  const functions = [...symbols.functions];
-
-  const scope = (0, _ast.findClosestFunction)(functions, location);
+  const scope = (0, _ast.findClosestFunction)(symbols, location);
   if (scope) {
     // we only record the line, but at some point we may
     // also do column offsets
     const line = location.line - scope.location.start.line;
     return {
       name: scope.name,
       offset: { line, column: undefined }
     };
@@ -8959,18 +8971,21 @@ function _interopRequireWildcard(obj) { 
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 const { Provider } = __webpack_require__(3592);
 
 function renderPanel(component, store) {
   const root = document.createElement("div");
   root.className = "launchpad-root theme-body";
-  root.style.setProperty("flex", 1);
+  root.style.setProperty("flex", "1");
   const mount = document.querySelector("#mount");
+  if (!mount) {
+    return;
+  }
   mount.appendChild(root);
 
   _reactDom2.default.render(_react2.default.createElement(Provider, { store }, _react2.default.createElement(component)), root);
 }
 
 function bootstrapStore(client, { services, toolboxActions }) {
   const createStore = (0, _createStore2.default)({
     log: (0, _devtoolsConfig.isTesting)() || (0, _devtoolsConfig.getValue)("logging.actions"),
@@ -9124,17 +9139,17 @@ exports.searchSources = searchSources;
 exports.searchSource = searchSource;
 
 var _search = __webpack_require__(1395);
 
 var _selectors = __webpack_require__(3590);
 
 var _source = __webpack_require__(1356);
 
-var _sources = __webpack_require__(1797);
+var _loadSourceText = __webpack_require__(1435);
 
 var _projectTextSearch = __webpack_require__(1424);
 
 function addSearchQuery(query) {
   return { type: "ADD_QUERY", query };
 } /* 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/>. */
@@ -9167,17 +9182,17 @@ function closeProjectSearch() {
 function searchSources(query) {
   return async ({ dispatch, getState }) => {
     await dispatch(clearSearchResults());
     await dispatch(addSearchQuery(query));
     dispatch(updateSearchStatus(_projectTextSearch.statusType.fetching));
     const sources = (0, _selectors.getSources)(getState());
     const validSources = sources.valueSeq().filter(source => !(0, _selectors.hasPrettySource)(getState(), source.id) && !(0, _source.isThirdParty)(source));
     for (const source of validSources) {
-      await dispatch((0, _sources.loadSourceText)(source));
+      await dispatch((0, _loadSourceText.loadSourceText)(source));
       await dispatch(searchSource(source.id, query));
     }
     dispatch(updateSearchStatus(_projectTextSearch.statusType.done));
   };
 }
 
 function searchSource(sourceId, query) {
   return async ({ dispatch, getState }) => {
@@ -22238,17 +22253,17 @@ const {
 exports.default = (0, _reactRedux.connect)(state => {
   const selectedSource = (0, _selectors.getSelectedSource)(state);
   return {
     selectedLocation: (0, _selectors.getSelectedLocation)(state),
     selectedSource,
     hasPrettyPrint: !!(0, _selectors.getPrettySource)(state, selectedSource.get("id")),
     contextMenu: (0, _selectors.getContextMenu)(state),
     getFunctionText: line => (0, _function.findFunctionText)(line, selectedSource.toJS(), (0, _selectors.getSymbols)(state, selectedSource)),
-    getFunctionLocation: line => (0, _ast.findClosestFunction)((0, _selectors.getSymbols)(state, selectedSource).functions, {
+    getFunctionLocation: line => (0, _ast.findClosestFunction)((0, _selectors.getSymbols)(state, selectedSource), {
       line,
       column: Infinity
     })
   };
 }, {
   addExpression,
   evaluateInConsole,
   flashLineRange,
@@ -22275,17 +22290,17 @@ var _ast = __webpack_require__(1638);
 
 var _indentation = __webpack_require__(1438);
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 function findFunctionText(line, source, symbols) {
-  const func = (0, _ast.findClosestFunction)(symbols.functions, {
+  const func = (0, _ast.findClosestFunction)(symbols, {
     line,
     column: Infinity
   });
   if (!func) {
     return null;
   }
 
   const { location: { start, end } } = func;
@@ -22894,36 +22909,28 @@ var _react2 = _interopRequireDefault(_re
 var _redux = __webpack_require__(3593);
 
 var _reactRedux = __webpack_require__(3592);
 
 var _immutable = __webpack_require__(3594);
 
 var I = _interopRequireWildcard(_immutable);
 
-var _classnames = __webpack_require__(175);
-
-var _classnames2 = _interopRequireDefault(_classnames);
-
 var _reselect = __webpack_require__(993);
 
 var _lodash = __webpack_require__(2);
 
+var _BreakpointItem = __webpack_require__(3630);
+
+var _BreakpointItem2 = _interopRequireDefault(_BreakpointItem);
+
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
-var _Close = __webpack_require__(1374);
-
-var _Close2 = _interopRequireDefault(_Close);
-
-var _utils = __webpack_require__(1366);
-
-var _prefs = __webpack_require__(226);
-
 var _source = __webpack_require__(1356);
 
 var _selectors = __webpack_require__(3590);
 
 var _pause = __webpack_require__(2419);
 
 var _breakpoint = __webpack_require__(1364);
 
@@ -22943,34 +22950,17 @@ function isCurrentlyPausedAtBreakpoint(f
   }
 
   const bpId = (0, _breakpoint.makeLocationId)(breakpoint.location);
   const pausedId = (0, _breakpoint.makeLocationId)(frame.location);
   return bpId === pausedId;
 }
 
 function getBreakpointFilename(source) {
-  return source && source.toJS ? (0, _source.getFilename)(source.toJS()) : "";
-}
-
-function renderSourceLocation(source, line, column) {
-  const filename = getBreakpointFilename(source);
-  const isWasm = source && source.isWasm;
-  const columnVal = _prefs.features.columnBreakpoints && column ? `:${column}` : "";
-  const bpLocation = isWasm ? `0x${line.toString(16).toUpperCase()}` : `${line}${columnVal}`;
-
-  if (!filename) {
-    return null;
-  }
-
-  return _react2.default.createElement(
-    "div",
-    { className: "location" },
-    `${(0, _utils.endTruncateStr)(filename, 30)}: ${bpLocation}`
-  );
+  return source ? (0, _source.getFilename)(source) : "";
 }
 
 class Breakpoints extends _react.Component {
   shouldComponentUpdate(nextProps, nextState) {
     const { breakpoints } = this.props;
     return breakpoints !== nextProps.breakpoints;
   }
 
@@ -22991,88 +22981,69 @@ class Breakpoints extends _react.Compone
   }
 
   removeBreakpoint(event, breakpoint) {
     event.stopPropagation();
     this.props.removeBreakpoint(breakpoint.location);
   }
 
   renderBreakpoint(breakpoint) {
-    const locationId = breakpoint.locationId;
-    const line = breakpoint.location.line;
-    const column = breakpoint.location.column;
-    const isCurrentlyPaused = breakpoint.isCurrentlyPaused;
-    const isDisabled = breakpoint.disabled;
-    const isConditional = !!breakpoint.condition;
-    const isHidden = breakpoint.hidden;
-
-    if (isHidden) {
-      return;
-    }
-
-    return _react2.default.createElement(
-      "div",
-      {
-        className: (0, _classnames2.default)({
-          breakpoint,
-          paused: isCurrentlyPaused,
-          disabled: isDisabled,
-          "is-conditional": isConditional
-        }),
-        key: locationId,
-        onClick: () => this.selectBreakpoint(breakpoint),
-        onContextMenu: e => (0, _BreakpointsContextMenu2.default)(_extends({}, this.props, { breakpoint, contextMenuEvent: e }))
-      },
-      _react2.default.createElement("input", {
-        type: "checkbox",
-        className: "breakpoint-checkbox",
-        checked: !isDisabled,
-        onChange: () => this.handleCheckbox(breakpoint),
-        onClick: ev => ev.stopPropagation()
-      }),
-      _react2.default.createElement(
-        "label",
-        { className: "breakpoint-label", title: breakpoint.text },
-        renderSourceLocation(breakpoint.location.source, line, column)
-      ),
-      _react2.default.createElement(_Close2.default, {
-        handleClick: ev => this.removeBreakpoint(ev, breakpoint),
-        tooltip: L10N.getStr("breakpoints.removeBreakpointTooltip")
-      })
-    );
+    return _react2.default.createElement(_BreakpointItem2.default, {
+      key: breakpoint.locationId,
+      breakpoint: breakpoint,
+      onClick: () => this.selectBreakpoint(breakpoint),
+      onContextMenu: e => (0, _BreakpointsContextMenu2.default)(_extends({}, this.props, { breakpoint, contextMenuEvent: e })),
+      onChange: () => this.handleCheckbox(breakpoint),
+      onCloseClick: ev => this.removeBreakpoint(ev, breakpoint)
+    });
+  }
+
+  renderEmpty() {
+    return _react2.default.createElement(
+      "div",
+      { className: "pane-info" },
+      L10N.getStr("breakpoints.none")
+    );
+  }
+
+  renderBreakpoints() {
+    const { breakpoints } = this.props;
+
+    const groupedBreakpoints = (0, _lodash.groupBy)((0, _lodash.sortBy)([...breakpoints.valueSeq()], bp => bp.location.line), bp => getBreakpointFilename(bp.source));
+
+    return [...Object.keys(groupedBreakpoints).map(filename => {
+      return [_react2.default.createElement(
+        "div",
+        { className: "breakpoint-heading", title: filename, key: filename },
+        filename
+      ), ...groupedBreakpoints[filename].filter(bp => !bp.hidden && bp.text).map((bp, i) => this.renderBreakpoint(bp))];
+    })];
   }
 
   render() {
     const { breakpoints } = this.props;
-    const children = breakpoints.size === 0 ? _react2.default.createElement(
-      "div",
-      { className: "pane-info" },
-      L10N.getStr("breakpoints.none")
-    ) : (0, _lodash.sortBy)([...breakpoints.valueSeq()], [bp => getBreakpointFilename(bp.location.source), bp => bp.location.line]).map(bp => this.renderBreakpoint(bp));
 
     return _react2.default.createElement(
       "div",
       { className: "pane breakpoints-list" },
-      children
+      breakpoints.size ? this.renderBreakpoints() : this.renderEmpty()
     );
   }
 }
 
 function updateLocation(sources, frame, why, bp) {
   const source = (0, _selectors.getSourceInSources)(sources, bp.location.sourceId);
   const isCurrentlyPaused = isCurrentlyPausedAtBreakpoint(frame, why, bp);
   const locationId = (0, _breakpoint.makeLocationId)(bp.location);
-
-  const location = _extends({}, bp.location, { source });
-  const localBP = _extends({}, bp, { location, locationId, isCurrentlyPaused });
+  const localBP = _extends({}, bp, { locationId, isCurrentlyPaused, source });
 
   return localBP;
 }
 
-const _getBreakpoints = (0, _reselect.createSelector)(_selectors.getBreakpoints, _selectors.getSources, _selectors.getTopFrame, _selectors.getPauseReason, (breakpoints, sources, frame, why) => breakpoints.map(bp => updateLocation(sources, frame, why, bp)).filter(bp => bp.location.source && !bp.location.source.isBlackBoxed));
+const _getBreakpoints = (0, _reselect.createSelector)(_selectors.getBreakpoints, _selectors.getSources, _selectors.getTopFrame, _selectors.getPauseReason, (breakpoints, sources, frame, why) => breakpoints.map(bp => updateLocation(sources, frame, why, bp)).filter(bp => bp.source && !bp.source.isBlackBoxed));
 
 exports.default = (0, _reactRedux.connect)((state, props) => ({ breakpoints: _getBreakpoints(state) }), dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Breakpoints);
 
 /***/ }),
 
 /***/ 1601:
 /***/ (function(module, exports, __webpack_require__) {
 
@@ -26504,16 +26475,17 @@ function astCommand(stepType) {
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.findBestMatchExpression = findBestMatchExpression;
 exports.findEmptyLines = findEmptyLines;
 exports.containsPosition = containsPosition;
 exports.findClosestFunction = findClosestFunction;
+exports.findClosestClass = findClosestClass;
 
 var _lodash = __webpack_require__(2);
 
 function findBestMatchExpression(symbols, tokenPos) {
   const { memberExpressions, identifiers, literals } = symbols;
   const { line, column } = tokenPos;
 
   const members = memberExpressions.filter(({ computed }) => !computed);
@@ -26549,18 +26521,22 @@ function findEmptyLines(selectedSource, 
 
 function containsPosition(a, b) {
   const startsBefore = a.start.line < b.line || a.start.line === b.line && a.start.column <= b.column;
   const endsAfter = a.end.line > b.line || a.end.line === b.line && a.end.column >= b.column;
 
   return startsBefore && endsAfter;
 }
 
-function findClosestFunction(functions, location) {
-  return functions.reduce((found, currNode) => {
+function findClosestofSymbol(declarations, location) {
+  if (!declarations) {
+    return null;
+  }
+
+  return declarations.reduce((found, currNode) => {
     if (currNode.name === "anonymous" || !containsPosition(currNode.location, {
       line: location.line,
       column: location.column || 0
     })) {
       return found;
     }
 
     if (!found) {
@@ -26573,16 +26549,26 @@ function findClosestFunction(functions, 
     if (found.location.start.line === currNode.location.start.line && found.location.start.column > currNode.location.start.column) {
       return found;
     }
 
     return currNode;
   }, null);
 }
 
+function findClosestFunction(symbols, location) {
+  const { functions } = symbols;
+  return findClosestofSymbol(functions, location);
+}
+
+function findClosestClass(symbols, location) {
+  const { classes } = symbols;
+  return findClosestofSymbol(classes, location);
+}
+
 /***/ }),
 
 /***/ 1639:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
@@ -26690,16 +26676,25 @@ var _mapFrames = __webpack_require__(180
 
 Object.defineProperty(exports, "mapFrames", {
   enumerable: true,
   get: function () {
     return _mapFrames.mapFrames;
   }
 });
 
+var _setExtra = __webpack_require__(3627);
+
+Object.defineProperty(exports, "setExtra", {
+  enumerable: true,
+  get: function () {
+    return _setExtra.setExtra;
+  }
+});
+
 var _setPopupObjectProperties = __webpack_require__(2008);
 
 Object.defineProperty(exports, "setPopupObjectProperties", {
   enumerable: true,
   get: function () {
     return _setPopupObjectProperties.setPopupObjectProperties;
   }
 });
@@ -26859,51 +26854,53 @@ var _selectors = __webpack_require__(359
 var _ = __webpack_require__(1639);
 
 var _breakpoints = __webpack_require__(1396);
 
 var _expressions = __webpack_require__(1398);
 
 var _sources = __webpack_require__(1797);
 
+var _loadSourceText = __webpack_require__(1435);
+
 var _ui = __webpack_require__(1385);
 
 var _commands = __webpack_require__(1637);
 
 var _pause = __webpack_require__(2419);
 
 var _mapFrames = __webpack_require__(1804);
 
 var _fetchScopes = __webpack_require__(1655);
 
+/* 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/>. */
+
 async function getOriginalSourceForFrame(state, frame) {
   return (0, _selectors.getSources)(state).get(frame.location.sourceId);
 }
 /**
  * Debugger has just paused
  *
  * @param {object} pauseInfo
  * @memberof actions/pause
  * @static
  */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
-
 function paused(pauseInfo) {
   return async function ({ dispatch, getState, client, sourceMaps }) {
     const { frames, why, loadedObjects } = pauseInfo;
     const rootFrame = frames.length > 0 ? frames[0] : null;
 
     if (rootFrame) {
       const mappedFrame = await (0, _mapFrames.updateFrameLocation)(rootFrame, sourceMaps);
       const source = await getOriginalSourceForFrame(getState(), mappedFrame);
 
       // Ensure that the original file has loaded if there is one.
-      await dispatch((0, _sources.loadSourceText)(source));
+      await dispatch((0, _loadSourceText.loadSourceText)(source));
 
       if (await (0, _pause.shouldStep)(mappedFrame, getState(), sourceMaps)) {
         dispatch((0, _commands.command)("stepOver"));
         return;
       }
     }
 
     dispatch({
@@ -27761,40 +27758,38 @@ Object.defineProperty(exports, "__esModu
   value: true
 });
 exports.fetchScopes = fetchScopes;
 
 var _selectors = __webpack_require__(3590);
 
 var _mapScopes = __webpack_require__(1634);
 
-var _preview = __webpack_require__(1786);
-
 var _promise = __webpack_require__(1653);
 
+var _setExtra = __webpack_require__(3627);
+
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 function fetchScopes() {
   return async function ({ dispatch, getState, client, sourceMaps }) {
     const frame = (0, _selectors.getSelectedFrame)(getState());
     if (!frame || (0, _selectors.getGeneratedFrameScope)(getState(), frame.id)) {
       return;
     }
 
-    const extra = await dispatch((0, _preview.getExtra)("this;", frame.this, frame));
-
     const scopes = dispatch({
       type: "ADD_SCOPES",
       frame,
-      extra,
       [_promise.PROMISE]: client.getFrameScopes(frame)
     });
 
+    await dispatch((0, _setExtra.setExtra)());
     await dispatch((0, _mapScopes.mapScopes)(scopes, frame));
   };
 }
 
 /***/ }),
 
 /***/ 1657:
 /***/ (function(module, exports, __webpack_require__) {
@@ -28247,17 +28242,17 @@ var _extends = Object.assign || function
                                                                                                                                                                                                                                                                    * 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/. */
 /* global window */
 
 exports.log = log;
 
 var _devtoolsConfig = __webpack_require__(1355);
 
-const blacklist = ["SET_POPUP_OBJECT_PROPERTIES", "SET_PAUSE_POINTS", "SET_SYMBOLS", "OUT_OF_SCOPE_LOCATIONS", "MAP_SCOPES", "MAP_FRAMES", "ADD_SCOPES", "IN_SCOPE_LINES"];
+const blacklist = ["SET_POPUP_OBJECT_PROPERTIES", "SET_PAUSE_POINTS", "SET_SYMBOLS", "OUT_OF_SCOPE_LOCATIONS", "MAP_SCOPES", "MAP_FRAMES", "ADD_SCOPES", "IN_SCOPE_LINES", "REMOVE_BREAKPOINT", "ADD_BREAKPOINT"];
 
 function cloneAction(action) {
   action = action || {};
   action = _extends({}, action);
 
   // ADD_TAB, ...
   if (action.source && action.source.text) {
     const source = _extends({}, action.source, { text: "" });
@@ -30380,20 +30375,34 @@ async function getImmutableProps(express
   const immutableType = await evaluate((exp => `${exp}.constructor.name`)(expression));
 
   return {
     type: immutableType.result,
     entries: immutableEntries.result
   };
 }
 
-async function getExtraProps(expression, result, evaluate) {
+async function getExtraProps(getState, expression, result, evaluate) {
   const props = {};
   if ((0, _preview.isReactComponent)(result)) {
-    props.react = await getReactProps(evaluate);
+    const selectedFrame = (0, _selectors.getSelectedFrame)(getState());
+    const source = (0, _selectors.getSource)(getState(), selectedFrame.location.sourceId);
+    const symbols = (0, _selectors.getSymbols)(getState(), source);
+
+    if (symbols && symbols.classes) {
+      const originalClass = (0, _ast.findClosestClass)(symbols, selectedFrame.location);
+
+      if (originalClass) {
+        props.react = { displayName: originalClass.name };
+      }
+    }
+
+    if (!props.react) {
+      props.react = await getReactProps(evaluate);
+    }
   }
 
   if ((0, _preview.isImmutable)(result)) {
     props.immutable = await getImmutableProps(expression, evaluate);
   }
 
   return props;
 }
@@ -30415,17 +30424,17 @@ function isInvalidTarget(target) {
   // exclude codemirror elements that are not tokens
   const invalidTarget = target.parentElement && !target.parentElement.closest(".CodeMirror-line") || cursorPos.top == 0;
 
   return invalidTarget || invalidToken || invalidType;
 }
 
 function getExtra(expression, result, selectedFrame) {
   return async ({ dispatch, getState, client, sourceMaps }) => {
-    const extra = await getExtraProps(expression, result, expr => client.evaluateInFrame(selectedFrame.id, expr));
+    const extra = await getExtraProps(getState, expression, result, expr => client.evaluateInFrame(selectedFrame.id, expr));
 
     return extra;
   };
 }
 
 function updatePreview(target, editor) {
   return ({ dispatch, getState, client, sourceMaps }) => {
     const tokenPos = (0, _editor.getTokenLocation)(editor.codeMirror, target);
@@ -31883,17 +31892,17 @@ function mapDisplayNames(frames, getStat
   return frames.map(frame => {
     const source = (0, _selectors.getSource)(getState(), frame.location.sourceId);
     const symbols = (0, _selectors.getSymbols)(getState(), source);
 
     if (!symbols || !symbols.functions) {
       return frame;
     }
 
-    const originalFunction = (0, _ast.findClosestFunction)(symbols.functions, frame.location);
+    const originalFunction = (0, _ast.findClosestFunction)(symbols, frame.location);
 
     if (!originalFunction) {
       return frame;
     }
 
     const originalDisplayName = originalFunction.name;
     return _extends({}, frame, { originalDisplayName });
   });
@@ -38776,23 +38785,278 @@ function getRelativeSources(state) {
 
 /***/ 3626:
 /***/ (function(module, exports) {
 
 module.exports = __WEBPACK_EXTERNAL_MODULE_3626__;
 
 /***/ }),
 
+/***/ 3627:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.setExtra = setExtra;
+
+var _selectors = __webpack_require__(3590);
+
+var _fetchExtra = __webpack_require__(3629);
+
+function setExtra() {
+  return async function ({ dispatch, getState, sourceMaps }) {
+    const frame = (0, _selectors.getSelectedFrame)(getState());
+    const source = (0, _selectors.getSource)(getState(), frame.location.sourceId);
+    const symbols = (0, _selectors.getSymbols)(getState(), source);
+
+    if (symbols && symbols.classes) {
+      dispatch((0, _fetchExtra.fetchExtra)());
+    }
+  };
+}
+
+/***/ }),
+
+/***/ 3628:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.createEditor = createEditor;
+
+var _sourceEditor = __webpack_require__(197);
+
+var _sourceEditor2 = _interopRequireDefault(_sourceEditor);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function createEditor(value) {
+  return new _sourceEditor2.default({
+    mode: "javascript",
+    foldGutter: false,
+    enableCodeFolding: false,
+    readOnly: "nocursor",
+    lineNumbers: false,
+    theme: "mozilla mozilla-breakpoint",
+    styleActiveLine: false,
+    lineWrapping: false,
+    matchBrackets: false,
+    showAnnotationRuler: false,
+    gutters: false,
+    value: value || "",
+    scrollbarStyle: null
+  });
+} /* 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/>. */
+
+/***/ }),
+
+/***/ 3629:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.fetchExtra = fetchExtra;
+
+var _selectors = __webpack_require__(3590);
+
+var _preview = __webpack_require__(1786);
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+function fetchExtra() {
+  return async function ({ dispatch, getState }) {
+    const frame = (0, _selectors.getSelectedFrame)(getState());
+    if (!frame) {
+      return;
+    }
+
+    const extra = await dispatch((0, _preview.getExtra)("this;", frame.this, frame));
+    dispatch({
+      type: "ADD_EXTRA",
+      extra: extra
+    });
+  };
+}
+
+/***/ }),
+
 /***/ 363:
 /***/ (function(module, exports) {
 
 module.exports = "<!-- This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --><svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:svg=\"http://www.w3.org/2000/svg\"><path fill=\"black\" id=\"svg_1\" fill-rule=\"evenodd\" d=\"m4.55195,12.97461l7.4,-5l-7.4,-5l0,10zm-0.925,0l0,-10c0,-0.785 0.8,-1.264 1.415,-0.848l7.4,5c0.58,0.392 0.58,1.304 0,1.696l-7.4,5c-0.615,0.416 -1.415,-0.063 -1.415,-0.848z\"></path></svg>"
 
 /***/ }),
 
+/***/ 3630:
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _react = __webpack_require__(0);
+
+var _react2 = _interopRequireDefault(_react);
+
+var _reactDom = __webpack_require__(4);
+
+var _reactDom2 = _interopRequireDefault(_reactDom);
+
+var _classnames = __webpack_require__(175);
+
+var _classnames2 = _interopRequireDefault(_classnames);
+
+var _Close = __webpack_require__(1374);
+
+var _Close2 = _interopRequireDefault(_Close);
+
+var _breakpoint = __webpack_require__(1364);
+
+var _prefs = __webpack_require__(226);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+function getBreakpointLocation(source, line, column) {
+  const isWasm = source && source.isWasm;
+  const columnVal = _prefs.features.columnBreakpoints && column ? `:${column}` : "";
+  const bpLocation = isWasm ? `0x${line.toString(16).toUpperCase()}` : `${line}${columnVal}`;
+
+  return bpLocation;
+}
+
+class BreakpointItem extends _react.Component {
+
+  componentDidMount() {
+    this.setupEditor();
+  }
+  componentDidUpdate() {
+    this.setupEditor();
+  }
+
+  componentWillUnmount() {
+    if (this.editor) {
+      this.editor.destroy();
+    }
+  }
+
+  shouldComponentUpdate(nextProps) {
+    const prevBreakpoint = this.props.breakpoint;
+    const nextBreakpoint = nextProps.breakpoint;
+
+    return !prevBreakpoint || prevBreakpoint.text != nextBreakpoint.text || prevBreakpoint.disabled != nextBreakpoint.disabled || prevBreakpoint.condition != nextBreakpoint.condition || prevBreakpoint.hidden != nextBreakpoint.hidden || prevBreakpoint.isCurrentlyPaused != nextBreakpoint.isCurrentlyPaused;
+  }
+
+  setupEditor() {
+    const { breakpoint } = this.props;
+    if (this.editor) {
+      return;
+    }
+
+    this.editor = (0, _breakpoint.createEditor)(breakpoint.text);
+
+    // disables the default search shortcuts
+    // $FlowIgnore
+    this.editor._initShortcuts = () => {};
+
+    const node = _reactDom2.default.findDOMNode(this);
+    if (node instanceof HTMLElement) {
+      const mountNode = node.querySelector(".breakpoint-label");
+      if (node instanceof HTMLElement) {
+        // $FlowIgnore
+        mountNode.innerHTML = "";
+        this.editor.appendToLocalElement(mountNode);
+      }
+    }
+  }
+
+  render() {
+    const {
+      breakpoint,
+      onClick,
+      onChange,
+      onContextMenu,
+      onCloseClick
+    } = this.props;
+
+    const locationId = breakpoint.locationId;
+    const line = breakpoint.location.line;
+    const column = breakpoint.location.column;
+    const isCurrentlyPaused = breakpoint.isCurrentlyPaused;
+    const isDisabled = breakpoint.disabled;
+    const isConditional = !!breakpoint.condition;
+
+    return _react2.default.createElement(
+      "div",
+      {
+        className: (0, _classnames2.default)({
+          breakpoint,
+          paused: isCurrentlyPaused,
+          disabled: isDisabled,
+          "is-conditional": isConditional
+        }),
+        key: locationId,
+        onClick: onClick,
+        onContextMenu: onContextMenu
+      },
+      _react2.default.createElement("input", {
+        type: "checkbox",
+        className: "breakpoint-checkbox",
+        checked: !isDisabled,
+        onChange: onChange,
+        onClick: ev => ev.stopPropagation()
+      }),
+      _react2.default.createElement(
+        "label",
+        { className: "breakpoint-label", title: breakpoint.text },
+        breakpoint.text
+      ),
+      _react2.default.createElement(
+        "div",
+        { className: "breakpoint-line-close" },
+        _react2.default.createElement(
+          "div",
+          { className: "breakpoint-line" },
+          getBreakpointLocation(breakpoint.source, line, column)
+        ),
+        _react2.default.createElement(_Close2.default, {
+          handleClick: onCloseClick,
+          tooltip: L10N.getStr("breakpoints.removeBreakpointTooltip")
+        })
+      )
+    );
+  }
+}
+
+exports.default = BreakpointItem;
+
+/***/ }),
+
 /***/ 364:
 /***/ (function(module, exports) {
 
 module.exports = "<!-- This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --><svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 33 12\"><path id=\"base-path\" d=\"M27.1,0H1C0.4,0,0,0.4,0,1v10c0,0.6,0.4,1,1,1h26.1 c0.6,0,1.2-0.3,1.5-0.7L33,6l-4.4-5.3C28.2,0.3,27.7,0,27.1,0z\"></path></svg>"
 
 /***/ }),
 
 /***/ 365:
--- a/devtools/client/debugger/new/parser-worker.js
+++ b/devtools/client/debugger/new/parser-worker.js
@@ -1244,25 +1244,40 @@ module.exports = isObjectLike;
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.isFunction = isFunction;
 exports.isAwaitExpression = isAwaitExpression;
 exports.isYieldExpression = isYieldExpression;
+exports.isObjectShorthand = isObjectShorthand;
+exports.getObjectExpressionValue = getObjectExpressionValue;
+exports.getVariableNames = getVariableNames;
+exports.getComments = getComments;
+exports.getSpecifiers = getSpecifiers;
 exports.isVariable = isVariable;
 exports.isComputedExpression = isComputedExpression;
 exports.getMemberExpression = getMemberExpression;
 exports.getVariables = getVariables;
 
 var _types = __webpack_require__(2268);
 
 var t = _interopRequireWildcard(_types);
 
+var _generator = __webpack_require__(2365);
+
+var _generator2 = _interopRequireDefault(_generator);
+
+var _flatten = __webpack_require__(706);
+
+var _flatten2 = _interopRequireDefault(_flatten);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
 function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
 
 function isFunction(node) {
   return t.isFunction(node) || t.isArrowFunctionExpression(node) || t.isObjectMethod(node) || t.isClassMethod(node);
 } /* 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/>. */
 
@@ -1271,16 +1286,85 @@ function isAwaitExpression(path) {
   return t.isAwaitExpression(node) || t.isAwaitExpression(parent.init) || t.isAwaitExpression(parent);
 }
 
 function isYieldExpression(path) {
   const { node, parent } = path;
   return t.isYieldExpression(node) || t.isYieldExpression(parent.init) || t.isYieldExpression(parent);
 }
 
+function isObjectShorthand(parent) {
+  return t.isProperty(parent) && parent.key.start == parent.value.start && parent.key.loc.identifierName === parent.value.loc.identifierName;
+}
+
+function getObjectExpressionValue(node) {
+  const { value } = node;
+
+  if (t.isIdentifier(value)) {
+    return value.name;
+  }
+
+  if (t.isCallExpression(value)) {
+    return "";
+  }
+  const code = (0, _generator2.default)(value).code;
+
+  const shouldWrap = t.isObjectExpression(value);
+  return shouldWrap ? `(${code})` : code;
+}
+
+function getVariableNames(path) {
+  if (t.isObjectProperty(path.node) && !isFunction(path.node.value)) {
+    if (path.node.key.type === "StringLiteral") {
+      return [{
+        name: path.node.key.value,
+        location: path.node.loc
+      }];
+    } else if (path.node.value.type === "Identifier") {
+      return [{ name: path.node.value.name, location: path.node.loc }];
+    } else if (path.node.value.type === "AssignmentPattern") {
+      return [{ name: path.node.value.left.name, location: path.node.loc }];
+    }
+
+    return [{
+      name: path.node.key.name,
+      location: path.node.loc
+    }];
+  }
+
+  if (!path.node.declarations) {
+    return path.node.params.map(dec => ({
+      name: dec.name,
+      location: dec.loc
+    }));
+  }
+
+  const declarations = path.node.declarations.filter(dec => dec.id.type !== "ObjectPattern").map(getVariables);
+
+  return (0, _flatten2.default)(declarations);
+}
+
+function getComments(ast) {
+  if (!ast || !ast.comments) {
+    return [];
+  }
+  return ast.comments.map(comment => ({
+    name: comment.location,
+    location: comment.loc
+  }));
+}
+
+function getSpecifiers(specifiers) {
+  if (!specifiers) {
+    return [];
+  }
+
+  return specifiers.map(specifier => specifier.local && specifier.local.name);
+}
+
 function isVariable(path) {
   const node = path.node;
   return t.isVariableDeclaration(node) || isFunction(path) && path.node.params != null && path.node.params.length || t.isObjectProperty(node) && !isFunction(path.node.value);
 }
 
 function isComputedExpression(expression) {
   return (/^\[/m.test(expression)
   );
@@ -1441,20 +1525,16 @@ Object.defineProperty(exports, "__esModu
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* 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/>. */
 
 exports.clearSymbols = clearSymbols;
 exports.getSymbols = getSymbols;
 
-var _flatten = __webpack_require__(706);
-
-var _flatten2 = _interopRequireDefault(_flatten);
-
 var _types = __webpack_require__(2268);
 
 var t = _interopRequireWildcard(_types);
 
 var _simplePath = __webpack_require__(3591);
 
 var _simplePath2 = _interopRequireDefault(_simplePath);
 
@@ -1463,20 +1543,20 @@ var _ast = __webpack_require__(1375);
 var _helpers = __webpack_require__(1411);
 
 var _inferClassName = __webpack_require__(1620);
 
 var _getFunctionName = __webpack_require__(1621);
 
 var _getFunctionName2 = _interopRequireDefault(_getFunctionName);
 
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
 function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
 
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
 let symbolDeclarations = new Map();
 
 function getFunctionParameterNames(path) {
   if (path.node.params != null) {
     return path.node.params.map(param => {
       if (param.type !== "AssignmentPattern") {
         return param.name;
       }
@@ -1493,69 +1573,20 @@ function getFunctionParameterNames(path)
       } else if (param.left.type === "Identifier" && param.right.type === "NullLiteral") {
         return `${param.left.name} = null`;
       }
     });
   }
   return [];
 }
 
-function getVariableNames(path) {
-  if (t.isObjectProperty(path.node) && !(0, _helpers.isFunction)(path.node.value)) {
-    if (path.node.key.type === "StringLiteral") {
-      return [{
-        name: path.node.key.value,
-        location: path.node.loc
-      }];
-    } else if (path.node.value.type === "Identifier") {
-      return [{ name: path.node.value.name, location: path.node.loc }];
-    } else if (path.node.value.type === "AssignmentPattern") {
-      return [{ name: path.node.value.left.name, location: path.node.loc }];
-    }
-
-    return [{
-      name: path.node.key.name,
-      location: path.node.loc
-    }];
-  }
-
-  if (!path.node.declarations) {
-    return path.node.params.map(dec => ({
-      name: dec.name,
-      location: dec.loc
-    }));
-  }
-
-  const declarations = path.node.declarations.filter(dec => dec.id.type !== "ObjectPattern").map(_helpers.getVariables);
-
-  return (0, _flatten2.default)(declarations);
-}
-
-function getComments(ast) {
-  if (!ast || !ast.comments) {
-    return [];
-  }
-  return ast.comments.map(comment => ({
-    name: comment.location,
-    location: comment.loc
-  }));
-}
-
-function getSpecifiers(specifiers) {
-  if (!specifiers) {
-    return [];
-  }
-
-  return specifiers.map(specifier => specifier.local && specifier.local.name);
-}
-
 /* eslint-disable complexity */
 function extractSymbol(path, symbols) {
   if ((0, _helpers.isVariable)(path)) {
-    symbols.variables.push(...getVariableNames(path));
+    symbols.variables.push(...(0, _helpers.getVariableNames)(path));
   }
 
   if ((0, _helpers.isFunction)(path)) {
     symbols.functions.push({
       name: (0, _getFunctionName2.default)(path.node, path.parent),
       klass: (0, _inferClassName.inferClassName)(path),
       location: path.node.loc,
       parameterNames: getFunctionParameterNames(path),
@@ -1578,17 +1609,17 @@ function extractSymbol(path, symbols) {
       location: path.node.loc
     });
   }
 
   if (t.isImportDeclaration(path)) {
     symbols.imports.push({
       source: path.node.source.value,
       location: path.node.loc,
-      specifiers: getSpecifiers(path.node.specifiers)
+      specifiers: (0, _helpers.getSpecifiers)(path.node.specifiers)
     });
   }
 
   if (t.isObjectProperty(path)) {
     const { start, end, identifierName } = path.node.key.loc;
     symbols.objectProperties.push({
       name: identifierName,
       location: { start, end },
@@ -1624,26 +1655,39 @@ function extractSymbol(path, symbols) {
       symbols.callExpressions.push({
         name: identifierName,
         values: args.filter(arg => arg.value).map(arg => arg.value),
         location: { start, end }
       });
     }
   }
 
+  if (t.isStringLiteral(path) && t.isProperty(path.parentPath)) {
+    const { start, end } = path.node.loc;
+    return symbols.identifiers.push({
+      name: path.node.value,
+      expression: (0, _helpers.getObjectExpressionValue)(path.parent),
+      location: { start, end }
+    });
+  }
+
   if (t.isIdentifier(path) && !t.isGenericTypeAnnotation(path.parent)) {
     let { start, end } = path.node.loc;
 
     // We want to include function params, but exclude the function name
     if (t.isClassMethod(path.parent) && !path.inList) {
       return;
     }
 
-    if (t.isProperty(path.parent)) {
-      return;
+    if (t.isProperty(path.parentPath) && !(0, _helpers.isObjectShorthand)(path.parent)) {
+      return symbols.identifiers.push({
+        name: path.node.name,
+        expression: (0, _helpers.getObjectExpressionValue)(path.parent),
+        location: { start, end }
+      });
     }
 
     if (path.node.typeAnnotation) {
       const column = path.node.typeAnnotation.loc.start.column;
       end = _extends({}, end, { column });
     }
 
     symbols.identifiers.push({
@@ -1703,17 +1747,17 @@ function extractSymbols(sourceId) {
         }
       } catch (e) {
         console.error(e);
       }
     }
   });
 
   // comments are extracted separately from the AST
-  symbols.comments = getComments(ast);
+  symbols.comments = (0, _helpers.getComments)(ast);
 
   return symbols;
 }
 
 function extendSnippet(name, expression, path, prevPath) {
   const computed = path && path.node.computed;
   const prevComputed = prevPath && prevPath.node.computed;
   const prevArray = t.isArrayExpression(prevPath);
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -95,34 +95,38 @@ support-files =
   examples/doc-script-switching.html
   examples/doc-exceptions.html
   examples/doc-iframes.html
   examples/doc-frames.html
   examples/doc-debugger-statements.html
   examples/doc-minified.html
   examples/big-sourcemap.html
   examples/doc-minified2.html
+  examples/doc-on-load.html
+  examples/doc-preview.html
   examples/doc-sourcemaps.html
   examples/doc-sourcemaps2.html
   examples/doc-sourcemaps3.html
   examples/doc-sourcemap-bogus.html
   examples/doc-sources.html
   examples/doc-pause-points.html
   examples/doc-return-values.html
   examples/doc-wasm-sourcemaps.html
   examples/asm.js
   examples/async.js
   examples/bogus-map.js
   examples/entry.js
   examples/exceptions.js
   examples/long.js
   examples/math.min.js
   examples/nested/nested-source.js
+  examples/top-level.js
   examples/opts.js
   examples/output.js
+  examples/preview.js
   examples/simple1.js
   examples/simple2.js
   examples/simple3.js
   examples/frames.js
   examples/pause-points.js
   examples/script-mutate.js
   examples/script-switching-02.js
   examples/script-switching-01.js
@@ -169,16 +173,18 @@ skip-if = os == "linux" # bug 1351952
 skip-if = os == "win"
 [browser_dbg-navigation.js]
 [browser_dbg-minified.js]
 [browser_dbg-pretty-print.js]
 [browser_dbg-pretty-print-console.js]
 [browser_dbg-pretty-print-paused.js]
 [browser_dbg-preview.js]
 skip-if = os == "win"
+[browser_dbg-preview-module.js]
+skip-if = os == "win"
 [browser_dbg-preview-source-maps.js]
 skip-if = os == "win"
 [browser_dbg-returnvalues.js]
 [browser_dbg-reload.js]
 [browser_dbg-replay.js]
 [browser_dbg-pause-points.js]
 [browser_dbg-scopes-mutations.js]
 [browser_dbg-search-file.js]
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-toggle.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-toggle.js
@@ -1,17 +1,17 @@
 function toggleBreakpoint(dbg, index) {
-  const bp = findElement(dbg, "breakpointItem", index);
+  const bp = findAllElements(dbg, "breakpointItems")[index];
   const input = bp.querySelector("input");
   input.click();
 }
 
 async function removeBreakpoint(dbg, index) {
   const removed = waitForDispatch(dbg, "REMOVE_BREAKPOINT");
-  const bp = findElement(dbg, "breakpointItem", index);
+  const bp = findAllElements(dbg, "breakpointItems")[index];
   bp.querySelector(".close-btn").click();
   await removed;
 }
 
 async function disableBreakpoint(dbg, index) {
   const disabled = waitForDispatch(dbg, "DISABLE_BREAKPOINT");
   toggleBreakpoint(dbg, index);
   await disabled;
@@ -75,15 +75,15 @@ add_task(async function() {
   await enableBreakpoints(dbg, 1);
   bp1 = findBreakpoint(dbg, "simple2", 3);
   bp2 = findBreakpoint(dbg, "simple2", 5);
 
   is(bp1.disabled, false, "first breakpoint is enabled");
   is(bp2.disabled, false, "second breakpoint is enabled");
 
   // Remove the breakpoints
-  await removeBreakpoint(dbg, 1);
-  await removeBreakpoint(dbg, 1);
+  await removeBreakpoint(dbg, 0);
+  await removeBreakpoint(dbg, 0);
 
   const bps = findBreakpoints(dbg);
 
   is(bps.size, 0, "breakpoints are removed");
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints.js
@@ -1,24 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function toggleBreakpoint(dbg, index) {
-  const bp = findElement(dbg, "breakpointItem", index);
+  const bp = findAllElements(dbg, "breakpointItems")[index];
   const input = bp.querySelector("input");
   input.click();
 }
 
-async function removeBreakpoint(dbg, index) {
-  const removed = waitForDispatch(dbg, "REMOVE_BREAKPOINT");
-  const bp = findElement(dbg, "breakpointItem", index);
-  bp.querySelector(".close-btn").click();
-  await removed;
-}
-
 async function disableBreakpoint(dbg, index) {
   const disabled = waitForDispatch(dbg, "DISABLE_BREAKPOINT");
   toggleBreakpoint(dbg, index);
   await disabled;
 }
 
 async function enableBreakpoint(dbg, index) {
   const enabled = waitForDispatch(dbg, "ENABLE_BREAKPOINT");
@@ -57,20 +50,20 @@ add_task(async function() {
   const dbg = await initDebugger("doc-scripts.html");
 
   // Create two breakpoints
   await selectSource(dbg, "simple2");
   await addBreakpoint(dbg, "simple2", 3);
   await addBreakpoint(dbg, "simple2", 5);
 
   // Disable the first one
-  await disableBreakpoint(dbg, 1);
+  await disableBreakpoint(dbg, 0);
   let bp1 = findBreakpoint(dbg, "simple2", 3);
   let bp2 = findBreakpoint(dbg, "simple2", 5);
   is(bp1.disabled, true, "first breakpoint is disabled");
   is(bp2.disabled, false, "second breakpoint is enabled");
 
   // Disable and Re-Enable the second one
-  await disableBreakpoint(dbg, 2);
-  await enableBreakpoint(dbg, 2);
+  await disableBreakpoint(dbg, 1);
+  await enableBreakpoint(dbg, 1);
   bp2 = findBreakpoint(dbg, "simple2", 5);
   is(bp2.disabled, false, "second breakpoint is enabled");
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-browser-content-toolbox.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-browser-content-toolbox.js
@@ -9,17 +9,17 @@
  * Tests that the debugger is succesfully loaded in the Browser Content Toolbox.
  */
 
 const {
   gDevToolsBrowser
 } = require("devtools/client/framework/devtools-browser");
 
 function toggleBreakpoint(dbg, index) {
-  const bp = findElement(dbg, "breakpointItem", index);
+  const bp = findAllElements(dbg, "breakpointItems")[index];
   const input = bp.querySelector("input");
   input.click();
 }
 
 async function disableBreakpoint(dbg, index) {
   const disabled = waitForDispatch(dbg, "DISABLE_BREAKPOINT");
   toggleBreakpoint(dbg, index);
   await disabled;
@@ -52,22 +52,22 @@ add_task(async function() {
   let dbg = createDebuggerContext(toolbox);
   ok(dbg, "Debugger context is available");
 
   info("Create a breakpoint");
   await selectSource(dbg, "simple2");
   await addBreakpoint(dbg, "simple2", 3);
 
   info("Disable the breakpoint");
-  await disableBreakpoint(dbg, 1);
+  await disableBreakpoint(dbg, 0);
   let bp = findBreakpoint(dbg, "simple2", 3);
   is(bp.disabled, true, "breakpoint is disabled");
 
   info("Enable the breakpoint");
-  await enableBreakpoint(dbg, 1);
+  await enableBreakpoint(dbg, 0);
   bp = findBreakpoint(dbg, "simple2", 3);
   is(bp.disabled, false, "breakpoint is enabled");
 
   info("Close the browser toolbox window");
   let onToolboxDestroyed = toolbox.once("destroyed");
   toolbox.win.top.close();
   await onToolboxDestroyed;
 
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions-error.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions-error.js
@@ -24,27 +24,16 @@ function getValue(dbg, index) {
 async function addExpression(dbg, input) {
   const evaluation = waitForDispatch(dbg, "EVALUATE_EXPRESSION");
   findElementWithSelector(dbg, expressionSelectors.input).focus();
   type(dbg, input);
   pressKey(dbg, "Enter");
   await evaluation;
 }
 
-async function editExpression(dbg, input) {
-  info("updating the expression");
-  dblClickElement(dbg, "expressionNode", 1);
-  // Position cursor reliably at the end of the text.
-  const evaluation = waitForDispatch(dbg, "EVALUATE_EXPRESSION");
-  pressKey(dbg, "End");
-  type(dbg, input);
-  pressKey(dbg, "Enter");
-  await evaluation;
-}
-
 add_task(async function() {
   const dbg = await initDebugger("doc-script-switching.html");
 
   await togglePauseOnExceptions(dbg, true, false);
 
   // add a good expression, 2 bad expressions, and another good one
   await addExpression(dbg, "location");
   await addExpression(dbg, "foo.bar");
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-preview-source-maps.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-preview-source-maps.js
@@ -16,27 +16,27 @@ function hoverAtPos(dbg, { line, ch }) {
       bubbles: true,
       cancelable: true,
       view: dbg.win
     })
   );
   return previewed;
 }
 
-function assertTooltip(dbg, { result, expression }) {
+function assertPreviewTooltip(dbg, { result, expression }) {
   const previewEl = findElement(dbg, "tooltip");
   is(previewEl.innerText, result, "Preview text shown to user");
 
   const preview = dbg.selectors.getPreview(dbg.getState());
   is(`${preview.result}`, result, "Preview.result");
   is(preview.updating, false, "Preview.updating");
   is(preview.expression, expression, "Preview.expression");
 }
 
-function assertPopup(dbg, { field, value, expression }) {
+function assertPreviewPopup(dbg, { field, value, expression }) {
   const previewEl = findElement(dbg, "popup");
   is(previewEl.innerText, "", "Preview text shown to user");
 
   const preview = dbg.selectors.getPreview(dbg.getState());
 
   is(
     `${preview.result.preview.ownProperties[field].value}`,
     value,
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-preview.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-preview.js
@@ -1,74 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function getCoordsFromPosition(cm, { line, ch }) {
-  return cm.charCoords({ line: ~~line, ch: ~~ch });
-}
+async function previews(dbg, fnName, previews) {
+  const invokeResult = invokeInTab(fnName);
+  await waitForPaused(dbg);
 
-function hoverAtPos(dbg, { line, ch }) {
-  const cm = getCM(dbg);
-  const coords = getCoordsFromPosition(cm, { line: line - 1, ch });
-  const tokenEl = dbg.win.document.elementFromPoint(coords.left, coords.top);
-  tokenEl.dispatchEvent(
-    new MouseEvent("mouseover", {
-      bubbles: true,
-      cancelable: true,
-      view: dbg.win
-    })
-  );
-}
+  await assertPreviews(dbg, previews);
+  await resume(dbg);
 
-async function assertTooltip(dbg, { result, expression }) {
-  const previewEl = await waitForElement(dbg, "tooltip");
-  is(previewEl.innerText, result, "Preview text shown to user");
-
-  const preview = dbg.selectors.getPreview(dbg.getState());
-  is(`${preview.result}`, result, "Preview.result");
-  is(preview.updating, false, "Preview.updating");
-  is(preview.expression, expression, "Preview.expression");
+  info(`Ran tests for ${fnName}`);
 }
 
-async function assertPreviewPopup(dbg, { field, value, expression }) {
-  const previewEl = await waitForElement(dbg, "popup");
-  const preview = dbg.selectors.getPreview(dbg.getState());
+// Test hovering on an object, which will show a popup and on a
+// simple value, which will show a tooltip.
+add_task(async function() {
+  const dbg = await initDebugger("doc-preview.html");
+  await selectSource(dbg, "preview.js");
 
-  is(
-    `${preview.result.preview.ownProperties[field].value}`,
-    value,
-    "Preview.result"
-  );
-  is(preview.updating, false, "Preview.updating");
-  is(preview.expression, expression, "Preview.expression");
-}
-
-add_task(async function() {
-  const dbg = await initDebugger("doc-scripts.html");
-  const { selectors: { getSelectedSource }, getState } = dbg;
-  const simple3 = findSource(dbg, "simple3.js");
-
-  await selectSource(dbg, "simple3");
-
-  await addBreakpoint(dbg, simple3, 5);
+  await previews(dbg, "empties", [
+    // { line: 2, column: 9, expression: "a", result: '""' },
+    // { line: 3, column: 9, expression: "b", result: "false" },
+    { line: 4, column: 9, expression: "c", result: "undefined" },
+    { line: 5, column: 9, expression: "d", result: "null" }
+  ]);
 
-  invokeInTab("simple");
-  await waitForPaused(dbg);
-
-  const tooltipPreviewed = waitForDispatch(dbg, "SET_PREVIEW");
-  hoverAtPos(dbg, { line: 5, ch: 12 });
-  await tooltipPreviewed;
-  await assertTooltip(dbg, { result: "3", expression: "result" });
-
-  const popupPreviewed = waitForDispatch(dbg, "SET_PREVIEW");
-  hoverAtPos(dbg, { line: 2, ch: 10 });
-  await popupPreviewed;
-  await assertPreviewPopup(dbg, {
-    field: "foo",
-    value: "1",
-    expression: "obj"
-  });
-  await assertPreviewPopup(dbg, {
-    field: "bar",
-    value: "2",
-    expression: "obj"
-  });
+  await previews(dbg, "smalls", [
+    { line: 10, column: 9, expression: "a", result: '"..."' },
+    { line: 11, column: 9, expression: "b", result: "true" },
+    { line: 12, column: 9, expression: "c", result: "1" },
+    {
+      line: 13,
+      column: 9,
+      expression: "d",
+      fields: [["length", "0"]]
+    }
+  ]);
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-reload.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-reload.js
@@ -14,16 +14,17 @@ async function waitForBreakpoint(dbg, lo
       return dbg.selectors.getBreakpoint(dbg.getState(), location);
     },
     "Waiting for breakpoint"
   );
 }
 
 add_task(async function() {
   const dbg = await initDebugger("reload/doc-reload.html");
+
   await waitForSource(dbg, "sjs_code_reload");
   await selectSource(dbg, "sjs_code_reload");
   await addBreakpoint(dbg, "sjs_code_reload", 2);
 
   await reload(dbg, "sjs_code_reload.sjs");
   await waitForSelectedSource(dbg, "sjs_code_reload.sjs")
 
   const source = findSource(dbg, "sjs_code_reload");
--- a/devtools/client/debugger/new/test/mochitest/examples/doc-preview.html
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-preview.html
@@ -11,10 +11,13 @@
   <body>
     <script src="preview.js"></script>
     <script>
       // This inline script allows this HTML page to show up as a
       // source. It also needs to introduce a new global variable so
       // it's not immediately garbage collected.
       inline_script = function () { var x = 5; };
     </script>
+
+    <button onclick="empties()">Empties</button>
+    <button onclick="smalls()">Smalls</button>
   </body>
 </html>
--- a/devtools/client/debugger/new/test/mochitest/head.js
+++ b/devtools/client/debugger/new/test/mochitest/head.js
@@ -172,17 +172,17 @@ function waitForThreadEvents(dbg, eventN
 function waitForState(dbg, predicate, msg) {
   return new Promise(resolve => {
     info(`Waiting for state change: ${msg || ""}`);
     if (predicate(dbg.store.getState())) {
       return resolve();
     }
 
     const unsubscribe = dbg.store.subscribe(() => {
-      const result = predicate(dbg.store.getState())
+      const result = predicate(dbg.store.getState());
       if (result) {
         info(`Finished waiting for state change: ${msg || ""}`);
         unsubscribe();
         resolve(result);
       }
     });
   });
 }
@@ -223,20 +223,24 @@ async function waitForSources(dbg, ...so
  *
  * @memberof mochitest/waits
  * @param {Object} dbg
  * @param {String} source
  * @return {Promise}
  * @static
  */
 function waitForSource(dbg, url) {
-  return waitForState(dbg, state => {
-    const sources = dbg.selectors.getSources(state);
-    return sources.find(s => (s.get("url") || "").includes(url));
-  }, `source exists`);
+  return waitForState(
+    dbg,
+    state => {
+      const sources = dbg.selectors.getSources(state);
+      return sources.find(s => (s.get("url") || "").includes(url));
+    },
+    `source exists`
+  );
 }
 
 async function waitForElement(dbg, name) {
   await waitUntil(() => findElement(dbg, name));
   return findElement(dbg, name);
 }
 
 async function waitForElementWithSelector(dbg, selector) {
@@ -260,17 +264,20 @@ function waitForSelectedSource(dbg, url)
 
       const newSource = findSource(dbg, url, { silent: true });
       if (newSource.id != source.id) {
         return false;
       }
 
       // wait for async work to be done
       const hasSymbols = dbg.selectors.hasSymbols(state, source);
-      const hasSourceMetaData = dbg.selectors.hasSourceMetaData(state, source.id);
+      const hasSourceMetaData = dbg.selectors.hasSourceMetaData(
+        state,
+        source.id
+      );
       const hasPausePoints = dbg.selectors.hasPausePoints(state, source.id);
       return hasSymbols && hasSourceMetaData && hasPausePoints;
     },
     "selected source"
   );
 }
 
 /**
@@ -684,18 +691,18 @@ function deleteExpression(dbg, input) {
  *
  * @memberof mochitest/actions
  * @param {Object} dbg
  * @param {Array} sources
  * @return {Promise}
  * @static
  */
 async function reload(dbg, ...sources) {
-  const navigated = waitForDispatch(dbg, "NAVIGATE")
-  await dbg.client.reload()
+  const navigated = waitForDispatch(dbg, "NAVIGATE");
+  await dbg.client.reload();
   await navigated;
   return waitForSources(dbg, ...sources);
 }
 
 /**
  * Navigates the debuggee to another url.
  *
  * @memberof mochitest/actions
@@ -931,17 +938,18 @@ const selectors = {
   expressionNode: i =>
     `.expressions-list .expression-container:nth-child(${i}) .object-label`,
   expressionValue: i =>
     `.expressions-list .expression-container:nth-child(${i}) .object-delimiter + *`,
   expressionClose: i =>
     `.expressions-list .expression-container:nth-child(${i}) .close`,
   expressionNodes: ".expressions-list .tree-node",
   scopesHeader: ".scopes-pane ._header",
-  breakpointItem: i => `.breakpoints-list .breakpoint:nth-child(${i})`,
+  breakpointItem: i => `.breakpoints-list .breakpoint:nth-of-type(${i})`,
+  breakpointItems: `.breakpoints-list .breakpoint`,
   scopes: ".scopes-list",
   scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`,
   scopeValue: i =>
     `.scopes-list .tree-node:nth-child(${i}) .object-delimiter + *`,
   frame: i => `.frames ul li:nth-child(${i})`,
   frames: ".frames ul li",
   gutter: i => `.CodeMirror-code *:nth-child(${i}) .CodeMirror-linenumber`,
   menuitem: i => `menupopup menuitem:nth-child(${i})`,
@@ -1108,36 +1116,37 @@ function getCM(dbg) {
   return el.CodeMirror;
 }
 
 function getCoordsFromPosition(cm, { line, ch }) {
   return cm.charCoords({ line: ~~line, ch: ~~ch });
 }
 
 function hoverAtPos(dbg, { line, ch }) {
+  info(`Hovering at ${line}, ${ch}`);
   const cm = getCM(dbg);
 
   // Ensure the line is visible with margin because the bar at the bottom of
   // the editor overlaps into what the editor things is its own space, blocking
   // the click event below.
-  cm.scrollIntoView({ line: line - 1, ch  }, 100);
+  cm.scrollIntoView({ line: line - 1, ch }, 100);
 
   const coords = getCoordsFromPosition(cm, { line: line - 1, ch });
   const tokenEl = dbg.win.document.elementFromPoint(coords.left, coords.top);
   tokenEl.dispatchEvent(
     new MouseEvent("mouseover", {
       bubbles: true,
       cancelable: true,
       view: dbg.win
     })
   );
 }
 
 async function assertPreviewTextValue(dbg, { text, expression }) {
-  const previewEl = await waitForElement(dbg, "previewPopup");;
+  const previewEl = await waitForElement(dbg, "previewPopup");
 
   is(previewEl.innerText, text, "Preview text shown to user");
 
   const preview = dbg.selectors.getPreview(dbg.getState());
   is(preview.updating, false, "Preview.updating");
   is(preview.expression, expression, "Preview.expression");
 }
 
@@ -1150,25 +1159,47 @@ async function assertPreviewTooltip(dbg,
   is(preview.updating, false, "Preview.updating");
   is(preview.expression, expression, "Preview.expression");
 }
 
 async function assertPreviewPopup(dbg, { field, value, expression }) {
   const previewEl = await waitForElement(dbg, "popup");
   const preview = dbg.selectors.getPreview(dbg.getState());
 
-  is(
-    `${preview.result.preview.ownProperties[field].value}`,
-    value,
-    "Preview.result"
-  );
+  const properties =
+    preview.result.preview.ownProperties || preview.result.preview.items;
+  const property = properties[field];
+
+  is(`${property.value || property}`, value, "Preview.result");
   is(preview.updating, false, "Preview.updating");
   is(preview.expression, expression, "Preview.expression");
 }
 
+async function assertPreviews(dbg, previews) {
+  for (const { line, column, expression, result, fields } of previews) {
+    hoverAtPos(dbg, { line, ch: column - 1 });
+
+    if (fields && result) {
+      throw new Error("Invalid test fixture");
+    }
+
+    if (fields) {
+      for (const [field, value] of fields) {
+        await assertPreviewPopup(dbg, { expression, field, value });
+      }
+    } else {
+      await assertPreviewTextValue(dbg, { expression, text: result });
+    }
+
+    // Move to column 0 after to make sure that the preview created by this
+    // test does not affect later attempts to hover and preview.
+    hoverAtPos(dbg, { line: line, ch: 0 });
+  }
+}
+
 // NOTE: still experimental, the screenshots might not be exactly correct
 async function takeScreenshot(dbg) {
   let canvas = dbg.win.document.createElementNS(
     "http://www.w3.org/1999/xhtml",
     "html:canvas"
   );
   let context = canvas.getContext("2d");
   canvas.width = dbg.win.innerWidth;
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -628,16 +628,44 @@ DevTools.prototype = {
    * toolkit/components/extensions/ext-c-toolkit.js
    */
   openBrowserConsole: function() {
     let {HUDService} = require("devtools/client/webconsole/hudservice");
     HUDService.openBrowserConsoleOrFocus();
   },
 
   /**
+   * Evaluate the cross iframes query selectors
+   * @oaram {Object} walker
+   * @param {Array} selectors
+   *        An array of CSS selectors to find the target accessible object.
+   *        Several selectors can be needed if the element is nested in frames
+   *        and not directly in the root document.
+   * @return {Promise} a promise that resolves when the node front is found for
+   *                   selection using inspector tools.
+   */
+  async findNodeFront(walker, nodeSelectors) {
+    async function querySelectors(nodeFront) {
+      let selector = nodeSelectors.shift();
+      if (!selector) {
+        return nodeFront;
+      }
+      nodeFront = await walker.querySelector(nodeFront, selector);
+      if (nodeSelectors.length > 0) {
+        let { nodes } = await walker.children(nodeFront);
+        // This is the NodeFront for the document node inside the iframe
+        nodeFront = nodes[0];
+      }
+      return querySelectors(nodeFront);
+    }
+    let nodeFront = await walker.getRootNode();
+    return querySelectors(nodeFront);
+  },
+
+  /**
    * Called from the DevToolsShim, used by nsContextMenu.js.
    *
    * @param {XULTab} tab
    *        The browser tab on which inspect node was used.
    * @param {Array} selectors
    *        An array of CSS selectors to find the target node. Several selectors can be
    *        needed if the element is nested in frames and not directly in the root
    *        document. The selectors are ordered starting with the root document and
@@ -657,44 +685,57 @@ DevTools.prototype = {
     // If the toolbox has been switched into a nested frame, we should first remove
     // selectors according to the frame depth.
     nodeSelectors.splice(0, toolbox.selectedFrameDepth);
 
     // new-node-front tells us when the node has been selected, whether the
     // browser is remote or not.
     let onNewNode = inspector.selection.once("new-node-front");
 
-    // Evaluate the cross iframes query selectors
-    async function querySelectors(nodeFront) {
-      let selector = nodeSelectors.shift();
-      if (!selector) {
-        return nodeFront;
-      }
-      nodeFront = await inspector.walker.querySelector(nodeFront, selector);
-      if (nodeSelectors.length > 0) {
-        let { nodes } = await inspector.walker.children(nodeFront);
-        // This is the NodeFront for the document node inside the iframe
-        nodeFront = nodes[0];
-      }
-      return querySelectors(nodeFront);
-    }
-    let nodeFront = await inspector.walker.getRootNode();
-    nodeFront = await querySelectors(nodeFront);
+    let nodeFront = await this.findNodeFront(inspector.walker, nodeSelectors);
     // Select the final node
     inspector.selection.setNodeFront(nodeFront, { reason: "browser-context-menu" });
 
     await onNewNode;
     // Now that the node has been selected, wait until the inspector is
     // fully updated.
     await inspector.once("inspector-updated");
   },
 
   /**
+   * Called from the DevToolsShim, used by nsContextMenu.js.
+   *
+   * @param {XULTab} tab
+   *        The browser tab on which inspect accessibility was used.
+   * @param {Array} selectors
+   *        An array of CSS selectors to find the target accessible object.
+   *        Several selectors can be needed if the element is nested in frames
+   *        and not directly in the root document.
+   * @param {Number} startTime
+   *        Optional, indicates the time at which the user event related to this
+   *        node inspection started. This is a `performance.now()` timing.
+   * @return {Promise} a promise that resolves when the accessible object is
+   *         selected in the accessibility inspector.
+   */
+  async inspectA11Y(tab, nodeSelectors, startTime) {
+    let target = TargetFactory.forTab(tab);
+
+    let toolbox = await gDevTools.showToolbox(
+      target, "accessibility", null, null, startTime);
+    let nodeFront = await this.findNodeFront(toolbox.walker, nodeSelectors);
+    // Select the accessible object in the panel and wait for the event that
+    // tells us it has been done.
+    let a11yPanel = toolbox.getCurrentPanel();
+    let onSelected = a11yPanel.once("new-accessible-front-selected");
+    a11yPanel.selectAccessibleForNode(nodeFront);
+    await onSelected;
+  },
+
+  /**
    * Either the DevTools Loader has been destroyed or firefox is shutting down.
-
    * @param {boolean} shuttingDown
    *        True if firefox is currently shutting down. We may prevent doing
    *        some cleanups to speed it up. Otherwise everything need to be
    *        cleaned up in order to be able to load devtools again.
    */
   destroy({ shuttingDown }) {
     // Do not cleanup everything during firefox shutdown.
     if (!shuttingDown) {
--- a/devtools/client/inspector/fonts/actions/font-editor.js
+++ b/devtools/client/inspector/fonts/actions/font-editor.js
@@ -1,21 +1,46 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
+  RESET_EDITOR,
+  UPDATE_AXIS_VALUE,
   UPDATE_EDITOR_VISIBILITY,
+  UPDATE_EDITOR_STATE,
 } = require("./index");
 
 module.exports = {
 
+  resetFontEditor() {
+    return {
+      type: RESET_EDITOR,
+    };
+  },
+
   toggleFontEditor(isVisible, selector = "") {
     return {
       type: UPDATE_EDITOR_VISIBILITY,
       isVisible,
       selector,
     };
   },
 
+  updateAxis(axis, value) {
+    return {
+      type: UPDATE_AXIS_VALUE,
+      axis,
+      value,
+    };
+  },
+
+  updateFontEditor(fonts, properties = {}) {
+    return {
+      type: UPDATE_EDITOR_STATE,
+      fonts,
+      properties,
+    };
+  },
+
 };
--- a/devtools/client/inspector/fonts/actions/index.js
+++ b/devtools/client/inspector/fonts/actions/index.js
@@ -3,16 +3,25 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createEnum } = require("devtools/client/shared/enum");
 
 createEnum([
 
+  // Reset font editor to intial state.
+  "RESET_EDITOR",
+
+  // Update the value of a variable font axis.
+  "UPDATE_AXIS_VALUE",
+
+  // Update font editor with applicable fonts and user-defined CSS font properties.
+  "UPDATE_EDITOR_STATE",
+
   // Toggle the visibiltiy of the font editor
   "UPDATE_EDITOR_VISIBILITY",
 
   // Update the list of fonts.
   "UPDATE_FONTS",
 
   // Update the preview text.
   "UPDATE_PREVIEW_TEXT",
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/FontAxis.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { 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");
+
+class FontAxis extends PureComponent {
+  static get propTypes() {
+    return {
+      defaultValue: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+      label: PropTypes.string.isRequired,
+      min: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+      max: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+      name: PropTypes.string.isRequired,
+      onChange: PropTypes.func.isRequired,
+      showInput: PropTypes.bool,
+      step: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+      value: PropTypes.string,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.onChange = this.onChange.bind(this);
+  }
+
+  onChange(e) {
+    this.props.onChange(this.props.name, e.target.value);
+  }
+
+  render() {
+    const defaults = {
+      min: this.props.min,
+      max: this.props.max,
+      onChange: this.onChange,
+      step: this.props.step || 1,
+      value: this.props.value || this.props.defaultValue,
+    };
+
+    const range = dom.input(
+      {
+        ...defaults,
+        className: "font-axis-slider",
+        title: this.props.label,
+        type: "range",
+      }
+    );
+
+    const input = dom.input(
+      {
+        ...defaults,
+        className: "font-axis-input",
+        type: "number",
+      }
+    );
+
+    return dom.label(
+      {
+        className: "font-axis",
+      },
+      dom.span(
+        {
+          className: "font-axis-label",
+        },
+        this.props.label
+      ),
+      range,
+      this.props.showInput ? input : null
+    );
+  }
+}
+
+module.exports = FontAxis;
--- a/devtools/client/inspector/fonts/components/FontEditor.js
+++ b/devtools/client/inspector/fonts/components/FontEditor.js
@@ -1,32 +1,101 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { PureComponent } = require("devtools/client/shared/vendor/react");
+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 FontAxis = createFactory(require("./FontAxis"));
+
 const Types = require("../types");
 
 class FontEditor extends PureComponent {
   static get propTypes() {
     return {
       fontEditor: PropTypes.shape(Types.fontEditor).isRequired,
+      onAxisUpdate: PropTypes.func.isRequired,
     };
   }
 
+  /**
+   * Naive implementation to get increment step for variable font axis that ensures
+   * a wide spectrum of precision based on range of values between min and max.
+   *
+   * @param  {Number|String} min
+   *         Minumum value for range.
+   * @param  {Number|String} max
+   *         Maximum value for range.
+   * @return {String}
+   *         Step value used in range input for font axis.
+   */
+  getAxisStep(min, max) {
+    let step = 1;
+    let delta = parseInt(max, 10) - parseInt(min, 10);
+
+    if (delta <= 1) {
+      step = 0.001;
+    } else if (delta <= 10) {
+      step = 0.01;
+    } else if (delta <= 100) {
+      step = 0.1;
+    }
+
+    return step.toString();
+  }
+
+  /**
+   * Get an array of FontAxis components for of the given variable font axis instances.
+   * If an axis is defined in the fontEditor store, use its value, else use the default.
+   *
+   * @param  {Array} fontAxes
+   *         Array of font axis instances
+   * @param  {Object} editedAxes
+   *         Object with axes and values edited by the user or predefined in the CSS
+   *         declaration for font-variation-settings.
+   * @return {Array}
+  *          Array of FontAxis components
+   */
+  renderAxes(fontAxes = [], editedAxes) {
+    return fontAxes.map(axis => {
+      return FontAxis({
+        min: axis.minValue,
+        max: axis.maxValue,
+        value: editedAxes[axis.tag] || axis.defaultValue,
+        step: this.getAxisStep(axis.minValue, axis.maxValue),
+        label: axis.name,
+        name: axis.tag,
+        onChange: this.props.onAxisUpdate,
+        showInput: true
+      });
+    });
+  }
+
+  // Placeholder for non-variable font UI.
+  // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1450695
+  renderPlaceholder() {
+    return dom.div({}, "No fonts with variation axes apply to this element.");
+  }
+
   render() {
-    const { selector } = this.props.fontEditor;
+    const { fonts, axes } = this.props.fontEditor;
+    // For MVP use ony first font to show axes if available.
+    // Future implementations will allow switching between multiple fonts.
+    const fontAxes = (fonts[0] && fonts[0].variationAxes) ? fonts[0].variationAxes : null;
 
     return dom.div(
       {
         className: "theme-sidebar inspector-tabpanel",
-        id: "sidebar-panel-fonteditor"
-      }, `Placeholder for Font Editor panel for selector: ${selector}`
+        id: "sidebar-panel-fontinspector"
+      },
+      fontAxes ?
+        this.renderAxes(fontAxes, axes)
+        :
+        this.renderPlaceholder()
     );
   }
 }
 
 module.exports = FontEditor;
--- a/devtools/client/inspector/fonts/components/FontsApp.js
+++ b/devtools/client/inspector/fonts/components/FontsApp.js
@@ -15,36 +15,39 @@ const FontOverview = createFactory(requi
 const Types = require("../types");
 
 class FontsApp extends PureComponent {
   static get propTypes() {
     return {
       fontData: PropTypes.shape(Types.fontData).isRequired,
       fontEditor: PropTypes.shape(Types.fontEditor).isRequired,
       fontOptions: PropTypes.shape(Types.fontOptions).isRequired,
+      onAxisUpdate: PropTypes.func.isRequired,
       onPreviewFonts: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
       fontData,
       fontEditor,
       fontOptions,
-      onPreviewFonts
+      onAxisUpdate,
+      onPreviewFonts,
     } = this.props;
 
     return dom.div(
       {
         className: "theme-sidebar inspector-tabpanel",
         id: "sidebar-panel-fontinspector"
       },
       fontEditor.isVisible ?
         FontEditor({
           fontEditor,
+          onAxisUpdate,
         })
         :
         FontOverview({
           fontData,
           fontOptions,
           onPreviewFonts,
         })
     );
--- a/devtools/client/inspector/fonts/components/moz.build
+++ b/devtools/client/inspector/fonts/components/moz.build
@@ -1,14 +1,15 @@
 # -*- 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/.
 
 DevToolsModules(
     'Font.js',
+    'FontAxis.js',
     'FontEditor.js',
     'FontList.js',
     'FontOverview.js',
     'FontPreview.js',
     'FontsApp.js',
 )
--- a/devtools/client/inspector/fonts/fonts.js
+++ b/devtools/client/inspector/fonts/fonts.js
@@ -5,55 +5,69 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { gDevTools } = require("devtools/client/framework/devtools");
 const { getColor } = require("devtools/client/shared/theme");
 const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const { throttle } = require("devtools/shared/throttle");
 
 const FontsApp = createFactory(require("./components/FontsApp"));
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 const { updateFonts } = require("./actions/fonts");
 const { updatePreviewText } = require("./actions/font-options");
-const { toggleFontEditor } = require("./actions/font-editor");
+const { resetFontEditor, toggleFontEditor, updateAxis, updateFontEditor } =
+  require("./actions/font-editor");
 
 const FONT_EDITOR_ID = "fonteditor";
+const FONT_PROPERTIES = [
+  "font-optical-sizing",
+  "font-size",
+  "font-stretch",
+  "font-style",
+  "font-variation-settings",
+  "font-weight",
+];
 
 class FontInspector {
   constructor(inspector, window) {
     this.document = window.document;
     this.inspector = inspector;
     this.pageStyle = this.inspector.pageStyle;
     this.ruleView = this.inspector.getPanel("ruleview").view;
     this.selectedRule = null;
     this.store = this.inspector.store;
 
-    this.update = this.update.bind(this);
+    this.syncChanges = throttle(this.syncChanges, 100, this);
+    this.onAxisUpdate = this.onAxisUpdate.bind(this);
     this.onNewNode = this.onNewNode.bind(this);
     this.onPreviewFonts = this.onPreviewFonts.bind(this);
     this.onRuleSelected = this.onRuleSelected.bind(this);
     this.onRuleUnselected = this.onRuleUnselected.bind(this);
+    this.onRuleUpdated = this.onRuleUpdated.bind(this);
     this.onThemeChanged = this.onThemeChanged.bind(this);
+    this.update = this.update.bind(this);
 
     this.init();
   }
 
   init() {
     if (!this.inspector) {
       return;
     }
 
     let fontsApp = FontsApp({
       onPreviewFonts: this.onPreviewFonts,
+      onAxisUpdate: this.onAxisUpdate,
     });
 
     let provider = createElement(Provider, {
       id: "fontinspector",
       key: "fontinspector",
       store: this.store,
       title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
     }, fontsApp);
@@ -141,16 +155,73 @@ class FontInspector {
    * Returns true if the font inspector panel is visible, and false otherwise.
    */
   isPanelVisible() {
     return this.inspector.sidebar &&
            this.inspector.sidebar.getCurrentTabID() === "fontinspector";
   }
 
   /**
+   * Live preview all CSS font property values from the fontEditor store on the page
+   * and sync the changes to the Rule view.
+   */
+  applyChanges() {
+    const fontEditor = this.store.getState().fontEditor;
+    // Until registered axis values are supported as font property values,
+    // write all axes and their values to font-variation-settings.
+    // Bug 1449891: https://bugzilla.mozilla.org/show_bug.cgi?id=1449891
+    const name = "font-variation-settings";
+    const value = Object.keys(fontEditor.axes)
+      .map(tag => `"${tag}" ${fontEditor.axes[tag]}`)
+      .join(", ");
+
+    let textProperty = this.selectedRule.textProps.filter(prop => prop.name === name)[0];
+    if (!textProperty) {
+      textProperty = this.selectedRule.editor.addProperty(name, value, "", true);
+    }
+
+    // Prevent reacting to changes we caused.
+    this.ruleView.off("property-value-updated", this.onRuleUpdated);
+    // Live preview font property changes on the page.
+    this.selectedRule.previewPropertyValue(textProperty, value, "");
+    // Sync Rule view with changes reflected on the page (throttled).
+    this.syncChanges(textProperty, value);
+  }
+
+  /**
+   * Sync the Rule view with the styles from the page. Called in a throttled way
+   * (see constructor) after property changes are applied directly to the CSS style rule
+   * on the page circumventing TextProperty.setValue() which triggers expensive DOM
+   * operations in TextPropertyEditor.update().
+   *
+   * @param  {TextProperty} textProperty
+   *         Model of CSS declaration for a property in used in the rule view.
+   * @param  {String} value
+   *         Value of the CSS property that should be reflected in the rule view.
+   */
+  syncChanges(textProperty, value) {
+    textProperty.updateValue(value);
+    this.ruleView.on("property-value-updated", this.onRuleUpdated);
+  }
+
+  /**
+   * Handler for changes of font axis value. Updates the value in the store and previews
+   * the change on the page.
+   *
+   * @param  {String} tag
+   *         Tag name of the font axis.
+   * @param  {String} value
+   *         Value of the font axis.
+   */
+  onAxisUpdate(tag, value) {
+    this.store.dispatch(updateAxis(tag, value));
+    this.applyChanges();
+  }
+
+  /**
    * Selection 'new-node' event handler.
    */
   onNewNode() {
     if (this.isPanelVisible()) {
       this.update();
     }
   }
 
@@ -163,58 +234,111 @@ class FontInspector {
   }
 
   /**
    * Handler for "ruleview-rule-selected" event emitted from the rule view when a rule is
    * marked as selected for an editor.
    * If selected for the font editor, hold a reference to the rule so we know where to
    * put property changes coming from the font editor and show the font editor panel.
    *
-   * @param {Object} eventData
-   *        Data payload for the event. Contains:
-   *        - {String} editorId - id of the editor for which the rule was selected
-   *        - {Rule} rule - reference to rule that was selected
+   * @param  {Object} eventData
+   *         Data payload for the event. Contains:
+   *         - {String} editorId - id of the editor for which the rule was selected
+   *         - {Rule} rule - reference to rule that was selected
    */
-  onRuleSelected(eventData) {
+  async onRuleSelected(eventData) {
     const { editorId, rule } = eventData;
     if (editorId === FONT_EDITOR_ID) {
       const selector = rule.matchedSelectors[0];
       this.selectedRule = rule;
+
+      await this.refreshFontEditor();
       this.store.dispatch(toggleFontEditor(true, selector));
+      this.ruleView.on("property-value-updated", this.onRuleUpdated);
     }
   }
 
   /**
+   * Handler for "property-value-updated" event emitted from the rule view whenever a
+   * property value changes.
+   */
+  async onRuleUpdated() {
+    await this.refreshFontEditor();
+  }
+
+  /**
    * Handler for "ruleview-rule-unselected" event emitted from the rule view when a rule
    * was released from being selected for an editor.
    * If previously selected for the font editor, release the reference to the rule and
    * hide the font editor panel.
    *
    * @param {Object} eventData
    *        Data payload for the event. Contains:
    *        - {String} editorId - id of the editor for which the rule was released
    *        - {Rule} rule - reference to rule that was released
    */
   onRuleUnselected(eventData) {
     const { editorId, rule } = eventData;
     if (editorId === FONT_EDITOR_ID && rule == this.selectedRule) {
       this.selectedRule = null;
       this.store.dispatch(toggleFontEditor(false));
+      this.store.dispatch(resetFontEditor());
+      this.ruleView.off("property-value-updated", this.onRuleUpdated);
     }
   }
 
   /**
    * Handler for the "theme-switched" event.
    */
   onThemeChanged(frame) {
     if (frame === this.document.defaultView) {
       this.update();
     }
   }
 
+  /**
+   * Update the state of the font editor with:
+   * - the fonts which apply to the current node;
+   * - the CSS font properties declared on the selected rule.
+   *
+   * This method is called during initial setup and as a result of any property
+   * values change in the Rule view. For the latter case, we do a deep compare between the
+   * font properties on the selected rule and the ones already store to decide if to
+   * update the font edtior to reflect a new external state.
+   */
+  async refreshFontEditor() {
+    if (!this.selectedRule || !this.inspector || !this.store) {
+      return;
+    }
+
+    const options = {};
+    if (this.pageStyle.supportsFontVariations) {
+      options.includeVariations = true;
+    }
+
+    const node = this.inspector.selection.nodeFront;
+    const fonts = await this.getFontsForNode(node, options);
+    // Collect any expected font properties and their values from the selected rule.
+    const properties = this.selectedRule.textProps.reduce((acc, prop) => {
+      if (FONT_PROPERTIES.includes(prop.name)) {
+        acc[prop.name] = prop.value;
+      }
+
+      return acc;
+    }, {});
+
+    const fontEditor = this.store.getState().fontEditor;
+
+    // Update the font editor state only if property values in rule differ from store.
+    // This can happen when a user makes manual edits to the values in the rule view.
+    if (JSON.stringify(properties) !== JSON.stringify(fontEditor.properties)) {
+      this.store.dispatch(updateFontEditor(fonts, properties));
+    }
+  }
+
   async update() {
     // Stop refreshing if the inspector or store is already destroyed.
     if (!this.inspector || !this.store) {
       return;
     }
 
     let node = this.inspector.selection.nodeFront;
 
--- a/devtools/client/inspector/fonts/reducers/font-editor.js
+++ b/devtools/client/inspector/fonts/reducers/font-editor.js
@@ -1,27 +1,67 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
+  RESET_EDITOR,
+  UPDATE_AXIS_VALUE,
+  UPDATE_EDITOR_STATE,
   UPDATE_EDITOR_VISIBILITY,
 } = require("../actions/index");
 
 const INITIAL_STATE = {
+  // Variable font axes.
+  axes: {},
+  // Fonts applicable to selected element.
+  fonts: [],
   // Whether or not the font editor is visible.
   isVisible: false,
-  // Selector text of the rule where font properties will be written.
+  // CSS font properties defined on the selected rule.
+  properties: {},
+  // Selector text of the selected rule where updated font properties will be written.
   selector: "",
 };
 
 let reducers = {
 
+  [RESET_EDITOR](state) {
+    return { ...INITIAL_STATE };
+  },
+
+  [UPDATE_AXIS_VALUE](state, { axis, value }) {
+    let newState = { ...state };
+    newState.axes[axis] = value;
+    return newState;
+  },
+
+  [UPDATE_EDITOR_STATE](state, { fonts, properties }) {
+    let axes = {};
+
+    if (properties["font-variation-settings"]) {
+      // Parse font-variation-settings CSS declaration into an object
+      // with axis tags as keys and axis values as values.
+      axes = properties["font-variation-settings"]
+        .split(",")
+        .reduce((acc, pair) => {
+          // Tags are always in quotes. Split by quote and filter excessive whitespace.
+          pair = pair.split(/["']/).filter(part => part.trim() !== "");
+          const tag = pair[0].trim();
+          const value = pair[1].trim();
+          acc[tag] = value;
+          return acc;
+        }, {});
+    }
+
+    return { ...state, axes, fonts, properties };
+  },
+
   [UPDATE_EDITOR_VISIBILITY](state, { isVisible, selector }) {
     return { ...state, isVisible, selector };
   },
 
 };
 
 module.exports = function(state = INITIAL_STATE, action) {
   let reducer = reducers[action.type];
--- a/devtools/client/inspector/fonts/types.js
+++ b/devtools/client/inspector/fonts/types.js
@@ -75,22 +75,28 @@ const font = exports.font = {
 };
 
 exports.fontOptions = {
   // The current preview text
   previewText: PropTypes.string,
 };
 
 exports.fontEditor = {
-  // Font currently being edited
-  font: PropTypes.shape(font),
+  // Variable font axes and their values
+  axes: PropTypes.object,
+
+  // Fonts applicable to selected element
+  fonts: PropTypes.arrayOf(PropTypes.shape(font)),
 
   // Whether or not the font editor is visible
   isVisible: PropTypes.bool,
 
+  // CSS font properties defined on the element
+  properties: PropTypes.object,
+
   // Selector text of the rule where font properties will be written
   selector: PropTypes.string,
 };
 
 /**
  * Font data.
  */
 exports.fontData = {
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -247,17 +247,17 @@ Rule.prototype = {
    */
   _applyPropertiesAuthored: function(modifications) {
     return modifications.apply().then(() => {
       // The rewriting may have required some other property values to
       // change, e.g., to insert some needed terminators.  Update the
       // relevant properties here.
       for (let index in modifications.changedDeclarations) {
         let newValue = modifications.changedDeclarations[index];
-        this.textProps[index].noticeNewValue(newValue);
+        this.textProps[index].updateValue(newValue);
       }
       // Recompute and redisplay the computed properties.
       for (let prop of this.textProps) {
         if (!prop.invisible && prop.enabled) {
           prop.updateComputed();
           prop.updateEditor();
         }
       }
--- a/devtools/client/inspector/rules/models/text-property.js
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -123,19 +123,22 @@ TextProperty.prototype = {
     }
 
     this.rule.setPropertyValue(this, value, priority);
     this.updateEditor();
   },
 
   /**
    * Called when the property's value has been updated externally, and
-   * the property and editor should update.
+   * the property and editor should update to reflect that value.
+   *
+   * @param {String} value
+   *        Property value
    */
-  noticeNewValue: function(value) {
+  updateValue: function(value) {
     if (value !== this.value) {
       this.value = value;
       this.updateEditor();
     }
   },
 
   setName: function(name) {
     let store = this.rule.elementStyle.store;
--- a/devtools/client/preferences/debugger.js
+++ b/devtools/client/preferences/debugger.js
@@ -46,17 +46,17 @@ pref("devtools.debugger.file-search-rege
 pref("devtools.debugger.features.async-stepping", true);
 pref("devtools.debugger.project-directory-root", "");
 
 pref("devtools.debugger.features.wasm", true);
 pref("devtools.debugger.features.shortcuts", true);
 pref("devtools.debugger.features.root", true);
 pref("devtools.debugger.features.column-breakpoints", false);
 pref("devtools.debugger.features.chrome-scopes", false);
-pref("devtools.debugger.features.map-scopes", false);
+pref("devtools.debugger.features.map-scopes", true);
 pref("devtools.debugger.features.breakpoints-dropdown", false);
 pref("devtools.debugger.features.remove-command-bar-options", false);
 pref("devtools.debugger.features.workers", true);
 pref("devtools.debugger.features.code-coverage", false);
 pref("devtools.debugger.features.event-listeners", false);
 pref("devtools.debugger.features.code-folding", false);
 pref("devtools.debugger.features.outline", true);
 pref("devtools.debugger.features.replay", false);
--- a/devtools/client/themes/fonts.css
+++ b/devtools/client/themes/fonts.css
@@ -3,24 +3,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #sidebar-panel-fontinspector {
   margin: 0;
   display: flex;
   flex-direction: column;
   width: 100%;
   height: 100%;
-}
-
-#sidebar-panel-fonteditor {
-  padding: 1em;
+  overflow: auto;
 }
 
 #font-container {
-  overflow: auto;
   flex: auto;
 }
 
 .fonts-list {
   padding: 0;
   margin: 0;
   list-style: none;
 }
@@ -108,16 +104,50 @@
   color: var(--theme-body-color-inactive);
   border-radius: 3px;
   border-style: solid;
   border-width: 1px;
   text-align: center;
   vertical-align: middle;
 }
 
+.font-axis {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  justify-content: space-between;
+  align-items: center;
+  padding: 5px 20px;
+}
+
+.font-axis-input {
+  width: 60px;
+}
+
+.font-axis-label {
+  width: 70px;
+}
+
+.font-axis-slider {
+  flex: 1;
+}
+
+.font-axis-slider::-moz-focus-outer {
+  border: 0;
+}
+
+.font-axis-slider::-moz-range-thumb {
+  background: var(--grey-50);
+  border: 0;
+}
+
+.font-axis-slider:focus::-moz-range-thumb {
+  background: var(--blue-55);
+}
+
 .font-origin {
   margin-top: .2em;
   color: var(--grey-50);
   justify-self: start;
 }
 
 .font-origin.system {
   text-transform: capitalize;
--- a/devtools/startup/DevToolsShim.jsm
+++ b/devtools/startup/DevToolsShim.jsm
@@ -170,16 +170,49 @@ this.DevToolsShim = {
       return;
     }
 
     this.initDevTools("SessionRestore");
     this._gDevTools.restoreDevToolsSession(session);
   },
 
   /**
+   * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility
+   * context menu item.
+   *
+   * @param {XULTab} tab
+   *        The browser tab on which inspect accessibility was used.
+   * @param {Array} selectors
+   *        An array of CSS selectors to find the target accessible object. Several
+   *        selectors can be needed if the element is nested in frames and not directly
+   *        in the root document.
+   * @return {Promise} a promise that resolves when the accessible node is selected in the
+   *         accessibility inspector or that resolves immediately if DevTools are not
+   *         enabled.
+   */
+  inspectA11Y: function(tab, selectors) {
+    if (!this.isEnabled()) {
+      if (!this.isDisabledByPolicy()) {
+        DevtoolsStartup.openInstallPage("ContextMenu");
+      }
+      return Promise.resolve();
+    }
+
+    // Record the timing at which this event started in order to compute later in
+    // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+    // i.e. especially take `DevtoolsStartup.initDevTools` into account.
+    let { performance } = Services.appShell.hiddenDOMWindow;
+    let startTime = performance.now();
+
+    this.initDevTools("ContextMenu");
+
+    return this._gDevTools.inspectA11Y(tab, selectors, startTime);
+  },
+
+  /**
    * Called from nsContextMenu.js in mozilla-central when using the Inspect Element
    * context menu item.
    *
    * @param {XULTab} tab
    *        The browser tab on which inspect node was used.
    * @param {Array} selectors
    *        An array of CSS selectors to find the target node. Several selectors can be
    *        needed if the element is nested in frames and not directly in the root
--- a/dom/file/ipc/IPCBlobInputStream.cpp
+++ b/dom/file/ipc/IPCBlobInputStream.cpp
@@ -128,16 +128,17 @@ NS_INTERFACE_MAP_BEGIN(IPCBlobInputStrea
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
 NS_INTERFACE_MAP_END
 
 IPCBlobInputStream::IPCBlobInputStream(IPCBlobInputStreamChild* aActor)
   : mActor(aActor)
   , mState(eInit)
   , mStart(0)
   , mLength(0)
+  , mMutex("IPCBlobInputStream::mMutex")
 {
   MOZ_ASSERT(aActor);
 
   mLength = aActor->Size();
 
   if (XRE_IsParentProcess()) {
     nsCOMPtr<nsIInputStream> stream;
     IPCBlobInputStreamStorage::Get()->GetStream(mActor->ID(),
@@ -251,18 +252,22 @@ IPCBlobInputStream::Close()
     mAsyncRemoteStream = nullptr;
   }
 
   if (mRemoteStream) {
     mRemoteStream->Close();
     mRemoteStream = nullptr;
   }
 
-  mInputStreamCallback = nullptr;
-  mInputStreamCallbackEventTarget = nullptr;
+  {
+    MutexAutoLock lock(mMutex);
+
+    mInputStreamCallback = nullptr;
+    mInputStreamCallbackEventTarget = nullptr;
+  }
 
   mFileMetadataCallback = nullptr;
   mFileMetadataCallbackEventTarget = nullptr;
 
   mState = eClosed;
   return NS_OK;
 }
 
@@ -356,28 +361,33 @@ IPCBlobInputStream::AsyncWait(nsIInputSt
     mInputStreamCallback = aCallback;
     mInputStreamCallbackEventTarget = aEventTarget;
     mState = ePending;
 
     mActor->StreamNeeded(this, aEventTarget);
     return NS_OK;
 
   // We are still waiting for the remote inputStream
-  case ePending:
+  case ePending: {
+    MutexAutoLock lock(mMutex);
+
     if (mInputStreamCallback && aCallback) {
       return NS_ERROR_FAILURE;
     }
 
     mInputStreamCallback = aCallback;
     mInputStreamCallbackEventTarget = aEventTarget;
     return NS_OK;
+  }
 
   // We have the remote inputStream, let's check if we can execute the callback.
-  case eRunning:
-    return MaybeExecuteInputStreamCallback(aCallback, aEventTarget);
+  case eRunning: {
+    MutexAutoLock lock(mMutex);
+    return MaybeExecuteInputStreamCallback(aCallback, aEventTarget, lock);
+  }
 
   // Stream is closed.
   default:
     MOZ_ASSERT(mState == eClosed);
     return NS_BASE_STREAM_CLOSED;
   }
 }
 
@@ -420,55 +430,74 @@ IPCBlobInputStream::StreamReady(already_
   fileMetadataCallbackEventTarget.swap(mFileMetadataCallbackEventTarget);
 
   if (fileMetadataCallback) {
     FileMetadataCallbackRunnable::Execute(fileMetadataCallback,
                                           fileMetadataCallbackEventTarget,
                                           this);
   }
 
-  nsCOMPtr<nsIInputStreamCallback> inputStreamCallback;
-  inputStreamCallback.swap(mInputStreamCallback);
+  {
+    MutexAutoLock lock(mMutex);
+
+    nsCOMPtr<nsIInputStreamCallback> inputStreamCallback;
+    inputStreamCallback.swap(mInputStreamCallback);
 
-  nsCOMPtr<nsIEventTarget> inputStreamCallbackEventTarget;
-  inputStreamCallbackEventTarget.swap(mInputStreamCallbackEventTarget);
+    nsCOMPtr<nsIEventTarget> inputStreamCallbackEventTarget;
+    inputStreamCallbackEventTarget.swap(mInputStreamCallbackEventTarget);
 
-  if (inputStreamCallback) {
-    MaybeExecuteInputStreamCallback(inputStreamCallback,
-                                    inputStreamCallbackEventTarget);
+    if (inputStreamCallback) {
+      MaybeExecuteInputStreamCallback(inputStreamCallback,
+                                      inputStreamCallbackEventTarget,
+                                      lock);
+    }
   }
 }
 
 nsresult
 IPCBlobInputStream::MaybeExecuteInputStreamCallback(nsIInputStreamCallback* aCallback,
-                                                    nsIEventTarget* aCallbackEventTarget)
+                                                    nsIEventTarget* aCallbackEventTarget,
+                                                    const MutexAutoLock& aProofOfLock)
 {
   MOZ_ASSERT(mState == eRunning);
   MOZ_ASSERT(mRemoteStream || mAsyncRemoteStream);
 
   // If the callback has been already set, we return an error.
   if (mInputStreamCallback && aCallback) {
     return NS_ERROR_FAILURE;
   }
 
+  bool hadCallback = !!mInputStreamCallback;
+
   mInputStreamCallback = aCallback;
   mInputStreamCallbackEventTarget = aCallbackEventTarget;
 
+  nsCOMPtr<nsIInputStreamCallback> callback = this;
+
   if (!mInputStreamCallback) {
-    return NS_OK;
+    if (!hadCallback) {
+      // Nothing was pending.
+      return NS_OK;
+    }
+
+    // Let's set a null callback in order to abort the current operation.
+    callback = nullptr;
   }
 
+  // We don't need to be locked anymore.
+  MutexAutoUnlock unlock(mMutex);
+
   nsresult rv = EnsureAsyncRemoteStream();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   MOZ_ASSERT(mAsyncRemoteStream);
 
-  return mAsyncRemoteStream->AsyncWait(this, 0, 0, aCallbackEventTarget);
+  return mAsyncRemoteStream->AsyncWait(callback, 0, 0, aCallbackEventTarget);
 }
 
 void
 IPCBlobInputStream::InitWithExistingRange(uint64_t aStart, uint64_t aLength)
 {
   MOZ_ASSERT(mActor->Size() >= aStart + aLength);
   mStart = aStart;
   mLength = aLength;
@@ -484,37 +513,41 @@ IPCBlobInputStream::InitWithExistingRang
   }
 }
 
 // nsIInputStreamCallback
 
 NS_IMETHODIMP
 IPCBlobInputStream::OnInputStreamReady(nsIAsyncInputStream* aStream)
 {
-  // We have been closed in the meantime.
-  if (mState == eClosed) {
-    return NS_OK;
-  }
+  nsCOMPtr<nsIInputStreamCallback> callback;
+  nsCOMPtr<nsIEventTarget> callbackEventTarget;
+  {
+    MutexAutoLock lock(mMutex);
 
-  MOZ_ASSERT(mState == eRunning);
-  MOZ_ASSERT(mAsyncRemoteStream == aStream);
+    // We have been closed in the meantime.
+    if (mState == eClosed) {
+      return NS_OK;
+    }
 
-  // The callback has been canceled in the meantime.
-  if (!mInputStreamCallback) {
-    return NS_OK;
-  }
+    MOZ_ASSERT(mState == eRunning);
+    MOZ_ASSERT(mAsyncRemoteStream == aStream);
 
-  nsCOMPtr<nsIInputStreamCallback> callback;
-  callback.swap(mInputStreamCallback);
+    // The callback has been canceled in the meantime.
+    if (!mInputStreamCallback) {
+      return NS_OK;
+    }
 
-  nsCOMPtr<nsIEventTarget> callbackEventTarget;
-  callbackEventTarget.swap(mInputStreamCallbackEventTarget);
+    callback.swap(mInputStreamCallback);
+    callbackEventTarget.swap(mInputStreamCallbackEventTarget);
+  }
 
   // This must be the last operation because the execution of the callback can
   // be synchronous.
+  MOZ_ASSERT(callback);
   InputStreamCallbackRunnable::Execute(callback, callbackEventTarget, this);
   return NS_OK;
 }
 
 // nsIIPCSerializableInputStream
 
 void
 IPCBlobInputStream::Serialize(mozilla::ipc::InputStreamParams& aParams,
--- a/dom/file/ipc/IPCBlobInputStream.h
+++ b/dom/file/ipc/IPCBlobInputStream.h
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_ipc_IPCBlobInputStream_h
 #define mozilla_dom_ipc_IPCBlobInputStream_h
 
+#include "mozilla/Mutex.h"
 #include "nsIAsyncInputStream.h"
 #include "nsICloneableInputStream.h"
 #include "nsIFileStreams.h"
 #include "nsIIPCSerializableInputStream.h"
 #include "nsCOMPtr.h"
 
 namespace mozilla {
 namespace dom {
@@ -40,17 +41,18 @@ public:
   void
   StreamReady(already_AddRefed<nsIInputStream> aInputStream);
 
 private:
   ~IPCBlobInputStream();
 
   nsresult
   MaybeExecuteInputStreamCallback(nsIInputStreamCallback* aCallback,
-                                  nsIEventTarget* aEventTarget);
+                                  nsIEventTarget* aEventTarget,
+                                  const MutexAutoLock& aProofOfLock);
 
   nsresult
   EnsureAsyncRemoteStream();
 
   void
   InitWithExistingRange(uint64_t aStart, uint64_t aLength);
 
   RefPtr<IPCBlobInputStreamChild> mActor;
@@ -78,20 +80,23 @@ private:
 
   uint64_t mStart;
   uint64_t mLength;
 
   nsCOMPtr<nsIInputStream> mRemoteStream;
   nsCOMPtr<nsIAsyncInputStream> mAsyncRemoteStream;
 
   // These 2 values are set only if mState is ePending.
+  // They are protected by mutex.
   nsCOMPtr<nsIInputStreamCallback> mInputStreamCallback;
   nsCOMPtr<nsIEventTarget> mInputStreamCallbackEventTarget;
 
   // These 2 values are set only if mState is ePending.
   nsCOMPtr<nsIFileMetadataCallback> mFileMetadataCallback;
   nsCOMPtr<nsIEventTarget> mFileMetadataCallbackEventTarget;
+
+  Mutex mMutex;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_ipc_IPCBlobInputStream_h
--- a/dom/html/HTMLStyleElement.cpp
+++ b/dom/html/HTMLStyleElement.cpp
@@ -168,16 +168,26 @@ HTMLStyleElement::SetInnerHTML(const nsA
   SetTextContentInternal(aInnerHTML, aScriptedPrincipal, aError);
 }
 
 void
 HTMLStyleElement::SetTextContentInternal(const nsAString& aTextContent,
                                          nsIPrincipal* aScriptedPrincipal,
                                          ErrorResult& aError)
 {
+  // Per spec, if we're setting text content to an empty string and don't
+  // already have any children, we should not trigger any mutation observers, or
+  // re-parse the stylesheet.
+  if (aTextContent.IsEmpty() && !GetFirstChild()) {
+    nsIPrincipal* principal = mTriggeringPrincipal ? mTriggeringPrincipal.get() : NodePrincipal();
+    if (principal == aScriptedPrincipal) {
+      return;
+    }
+  }
+
   SetEnableUpdates(false);
 
   aError = nsContentUtils::SetNodeTextContent(this, aTextContent, true);
 
   SetEnableUpdates(true);
 
   mTriggeringPrincipal = aScriptedPrincipal;
 
--- a/dom/indexedDB/FileSnapshot.cpp
+++ b/dom/indexedDB/FileSnapshot.cpp
@@ -5,16 +5,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "FileSnapshot.h"
 
 #include "IDBDatabase.h"
 #include "IDBFileHandle.h"
 #include "IDBMutableFile.h"
 #include "mozilla/Assertions.h"
+#include "mozilla/Mutex.h"
 #include "nsIAsyncInputStream.h"
 #include "nsICloneableInputStream.h"
 #include "nsIIPCSerializableInputStream.h"
 
 namespace mozilla {
 namespace dom {
 namespace indexedDB {
 
@@ -31,25 +32,29 @@ class StreamWrapper final
   class CloseRunnable;
 
   nsCOMPtr<nsIEventTarget> mOwningThread;
   nsCOMPtr<nsIInputStream> mInputStream;
   RefPtr<IDBFileHandle> mFileHandle;
   bool mFinished;
 
   // This is needed to call OnInputStreamReady() with the correct inputStream.
+  // It is protected by mutex.
   nsCOMPtr<nsIInputStreamCallback> mAsyncWaitCallback;
 
+  Mutex mMutex;
+
 public:
   StreamWrapper(nsIInputStream* aInputStream,
                 IDBFileHandle* aFileHandle)
     : mOwningThread(aFileHandle->GetMutableFile()->Database()->EventTarget())
     , mInputStream(aInputStream)
     , mFileHandle(aFileHandle)
     , mFinished(false)
+    , mMutex("StreamWrapper::mMutex")
   {
     AssertIsOnOwningThread();
     MOZ_ASSERT(aInputStream);
     MOZ_ASSERT(aFileHandle);
     aFileHandle->AssertIsOnOwningThread();
 
     mFileHandle->OnNewRequest();
   }
@@ -350,47 +355,53 @@ StreamWrapper::AsyncWait(nsIInputStreamC
                          uint32_t aRequestedCount,
                          nsIEventTarget* aEventTarget)
 {
   nsCOMPtr<nsIAsyncInputStream> stream = do_QueryInterface(mInputStream);
   if (!stream) {
     return NS_ERROR_NO_INTERFACE;
   }
 
-  if (mAsyncWaitCallback && aCallback) {
-    return NS_ERROR_FAILURE;
+  nsCOMPtr<nsIInputStreamCallback> callback = aCallback ? this : nullptr;
+  {
+    MutexAutoLock lock(mMutex);
+
+    if (mAsyncWaitCallback && aCallback) {
+      return NS_ERROR_FAILURE;
+    }
+
+    mAsyncWaitCallback = aCallback;
   }
 
-  mAsyncWaitCallback = aCallback;
-
-  if (!mAsyncWaitCallback) {
-    return NS_OK;
-  }
-
-  return stream->AsyncWait(this, aFlags, aRequestedCount, aEventTarget);
+  return stream->AsyncWait(callback, aFlags, aRequestedCount, aEventTarget);
 }
 
 // nsIInputStreamCallback
 
 NS_IMETHODIMP
 StreamWrapper::OnInputStreamReady(nsIAsyncInputStream* aStream)
 {
   nsCOMPtr<nsIAsyncInputStream> stream = do_QueryInterface(mInputStream);
   if (!stream) {
     return NS_ERROR_NO_INTERFACE;
   }
 
-  // We have been canceled in the meanwhile.
-  if (!mAsyncWaitCallback) {
-    return NS_OK;
+  nsCOMPtr<nsIInputStreamCallback> callback;
+  {
+    MutexAutoLock lock(mMutex);
+
+    // We have been canceled in the meanwhile.
+    if (!mAsyncWaitCallback) {
+      return NS_OK;
+    }
+
+    callback.swap(mAsyncWaitCallback);
   }
 
-  nsCOMPtr<nsIInputStreamCallback> callback;
-  callback.swap(mAsyncWaitCallback);
-
+  MOZ_ASSERT(callback);
   return callback->OnInputStreamReady(this);
 }
 
 // nsICloneableInputStream
 
 NS_IMETHODIMP
 StreamWrapper::GetCloneable(bool* aCloneable)
 {
--- a/dom/serviceworkers/ServiceWorker.cpp
+++ b/dom/serviceworkers/ServiceWorker.cpp
@@ -71,26 +71,29 @@ ServiceWorker::ServiceWorker(nsIGlobalOb
   : DOMEventTargetHelper(aGlobal)
   , mDescriptor(aDescriptor)
   , mInner(aInner)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_DIAGNOSTIC_ASSERT(aGlobal);
   MOZ_DIAGNOSTIC_ASSERT(mInner);
 
+  KeepAliveIfHasListenersFor(NS_LITERAL_STRING("statechange"));
+
+  // The error event handler is required by the spec currently, but is not used
+  // anywhere.  Don't keep the object alive in that case.
+
   // This will update our state too.
   mInner->AddServiceWorker(this);
 }
 
 ServiceWorker::~ServiceWorker()
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (mInner) {
-    mInner->RemoveServiceWorker(this);
-  }
+  mInner->RemoveServiceWorker(this);
 }
 
 NS_IMPL_ADDREF_INHERITED(ServiceWorker, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(ServiceWorker, DOMEventTargetHelper)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorker)
   NS_INTERFACE_MAP_ENTRY(ServiceWorker)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
@@ -109,33 +112,42 @@ ServiceWorker::State() const
   return mDescriptor.State();
 }
 
 void
 ServiceWorker::SetState(ServiceWorkerState aState)
 {
   ServiceWorkerState oldState = mDescriptor.State();
   mDescriptor.SetState(aState);
-  if (oldState != aState) {
-    DOMEventTargetHelper::DispatchTrustedEvent(NS_LITERAL_STRING("statechange"));
+  if (oldState == aState) {
+    return;
+  }
+
+  DOMEventTargetHelper::DispatchTrustedEvent(NS_LITERAL_STRING("statechange"));
+
+  // Once we have transitioned to the redundant state then no
+  // more statechange events will occur.  We can allow the DOM
+  // object to GC if script is not holding it alive.
+  if (mDescriptor.State() == ServiceWorkerState::Redundant) {
+    IgnoreKeepAliveIfHasListenersFor(NS_LITERAL_STRING("statechange"));
   }
 }
 
 void
 ServiceWorker::GetScriptURL(nsString& aURL) const
 {
   CopyUTF8toUTF16(mDescriptor.ScriptURL(), aURL);
 }
 
 void
 ServiceWorker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
                            const Sequence<JSObject*>& aTransferable,
                            ErrorResult& aRv)
 {
-  if (State() == ServiceWorkerState::Redundant || !mInner) {
+  if (State() == ServiceWorkerState::Redundant) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   mInner->PostMessage(GetParentObject(), aCx, aMessage, aTransferable, aRv);
 }
 
 
@@ -143,17 +155,13 @@ const ServiceWorkerDescriptor&
 ServiceWorker::Descriptor() const
 {
   return mDescriptor;
 }
 
 void
 ServiceWorker::DisconnectFromOwner()
 {
-  if (mInner) {
-    mInner->RemoveServiceWorker(this);
-    mInner = nullptr;
-  }
   DOMEventTargetHelper::DisconnectFromOwner();
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/serviceworkers/ServiceWorker.h
+++ b/dom/serviceworkers/ServiceWorker.h
@@ -98,20 +98,16 @@ private:
                 const ServiceWorkerDescriptor& aDescriptor,
                 Inner* aInner);
 
   // This class is reference-counted and will be destroyed from Release().
   ~ServiceWorker();
 
   ServiceWorkerDescriptor mDescriptor;
 
-  // Hold a strong reference to the inner service worker object.  This will
-  // create a ref-cycle.  The cycle is broken when either DisconnectFromOwner()
-  // is called due to the global tearing down or when the underlying service
-  // worker transitions to the redundant state.
   RefPtr<Inner> mInner;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(ServiceWorker, NS_DOM_SERVICEWORKER_IID)
 
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/serviceworkers/ServiceWorkerInfo.cpp
+++ b/dom/serviceworkers/ServiceWorkerInfo.cpp
@@ -113,17 +113,17 @@ ServiceWorkerInfo::DetachDebugger()
   return mServiceWorkerPrivate->DetachDebugger();
 }
 
 namespace {
 
 class ChangeStateUpdater final : public Runnable
 {
 public:
-  ChangeStateUpdater(const nsTArray<RefPtr<ServiceWorker>>& aInstances,
+  ChangeStateUpdater(const nsTArray<ServiceWorker*>& aInstances,
                      ServiceWorkerState aState)
     : Runnable("dom::ChangeStateUpdater")
     , mState(aState)
   {
     for (size_t i = 0; i < aInstances.Length(); ++i) {
       mInstances.AppendElement(aInstances[i]);
     }
   }
@@ -171,21 +171,16 @@ ServiceWorkerInfo::UpdateState(ServiceWo
   if (State() != aState) {
     mServiceWorkerPrivate->UpdateState(aState);
   }
   mDescriptor.SetState(aState);
   nsCOMPtr<nsIRunnable> r = new ChangeStateUpdater(mInstances, State());
   MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r.forget()));
   if (State() == ServiceWorkerState::Redundant) {
     serviceWorkerScriptCache::PurgeCache(mPrincipal, mCacheName);
-
-    // Break the ref-cycle with the binding objects.  We won't need to
-    // fire any more events and they should be able to GC once content
-    // script no longer references them.
-    mInstances.Clear();
   }
 }
 
 ServiceWorkerInfo::ServiceWorkerInfo(nsIPrincipal* aPrincipal,
                                      const nsACString& aScope,
                                      const nsACString& aScriptSpec,
                                      const nsAString& aCacheName,
                                      nsLoadFlags aImportsLoadFlags)
@@ -251,21 +246,18 @@ ServiceWorkerInfo::RemoveServiceWorker(S
   MOZ_DIAGNOSTIC_ASSERT(aWorker);
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
   nsAutoString workerURL;
   aWorker->GetScriptURL(workerURL);
   MOZ_DIAGNOSTIC_ASSERT(
     workerURL.Equals(NS_ConvertUTF8toUTF16(mDescriptor.ScriptURL())));
 #endif
 
-  // If the binding layer initiates this call by disconnecting the global,
-  // then we will find an entry in mInstances here.  If the worker transitions
-  // to redundant and we clear mInstances, then we will not find an entry
-  // here.
-  mInstances.RemoveElement(aWorker);
+  DebugOnly<bool> removed = mInstances.RemoveElement(aWorker);
+  MOZ_ASSERT(removed);
 }
 
 void
 ServiceWorkerInfo::PostMessage(nsIGlobalObject* aGlobal,
                                JSContext* aCx, JS::Handle<JS::Value> aMessage,
                                const Sequence<JSObject*>& aTransferable,
                                ErrorResult& aRv)
 {
--- a/dom/serviceworkers/ServiceWorkerInfo.h
+++ b/dom/serviceworkers/ServiceWorkerInfo.h
@@ -49,26 +49,22 @@ private:
   TimeStamp mCreationTimeStamp;
 
   // The time of states are 0, if SW has not reached that state yet. Besides, we
   // update each of them after UpdateState() is called in SWRegistrationInfo.
   PRTime mInstalledTime;
   PRTime mActivatedTime;
   PRTime mRedundantTime;
 
-  // Track the list of known binding objects so we can fire events on them
-  // when appropriate.  These are held using strong references so that they
-  // are not GC'd while an event handler is registered and could observe an
-  // event.  This reference will create a cycle with the binding object.  The
-  // cycle is broken when either the global is detached or the service worker
-  // transitions to the redundant state.
+  // We hold rawptrs since the ServiceWorker constructor and destructor ensure
+  // addition and removal.
   //
   // There is a high chance of there being at least one ServiceWorker
   // associated with this all the time.
-  AutoTArray<RefPtr<ServiceWorker>, 1> mInstances;
+  AutoTArray<ServiceWorker*, 1> mInstances;
 
   RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate;
   bool mSkipWaitingFlag;
 
   enum {
     Unknown,
     Enabled,
     Disabled
--- a/dom/serviceworkers/ServiceWorkerRegistration.cpp
+++ b/dom/serviceworkers/ServiceWorkerRegistration.cpp
@@ -36,25 +36,26 @@ NS_INTERFACE_MAP_END_INHERITING(DOMEvent
 ServiceWorkerRegistration::ServiceWorkerRegistration(nsIGlobalObject* aGlobal,
                                                      const ServiceWorkerRegistrationDescriptor& aDescriptor,
                                                      ServiceWorkerRegistration::Inner* aInner)
   : DOMEventTargetHelper(aGlobal)
   , mDescriptor(aDescriptor)
   , mInner(aInner)
 {
   MOZ_DIAGNOSTIC_ASSERT(mInner);
+
+  KeepAliveIfHasListenersFor(NS_LITERAL_STRING("updatefound"));
+
   UpdateState(mDescriptor);
   mInner->SetServiceWorkerRegistration(this);
 }
 
 ServiceWorkerRegistration::~ServiceWorkerRegistration()
 {
-  if (mInner) {
-    mInner->ClearServiceWorkerRegistration(this);
-  }
+  mInner->ClearServiceWorkerRegistration(this);
 }
 
 JSObject*
 ServiceWorkerRegistration::WrapObject(JSContext* aCx,
                                       JS::Handle<JSObject*> aGivenProto)
 {
   return ServiceWorkerRegistrationBinding::Wrap(aCx, this, aGivenProto);
 }
@@ -90,21 +91,28 @@ ServiceWorkerRegistration::CreateForWork
     new ServiceWorkerRegistration(aGlobal, aDescriptor, inner);
 
   return registration.forget();
 }
 
 void
 ServiceWorkerRegistration::DisconnectFromOwner()
 {
-  mInner->ClearServiceWorkerRegistration(this);
-  mInner = nullptr;
   DOMEventTargetHelper::DisconnectFromOwner();
 }
 
+void
+ServiceWorkerRegistration::RegistrationRemoved()
+{
+  // Our underlying registration was removed from SWM, so we
+  // will never get an updatefound event again.  We can let
+  // the object GC if content is not holding it alive.
+  IgnoreKeepAliveIfHasListenersFor(NS_LITERAL_STRING("updatefound"));
+}
+
 already_AddRefed<ServiceWorker>
 ServiceWorkerRegistration::GetInstalling() const
 {
   RefPtr<ServiceWorker> ref = mInstallingWorker;
   return ref.forget();
 }
 
 already_AddRefed<ServiceWorker>
--- a/dom/serviceworkers/ServiceWorkerRegistration.h
+++ b/dom/serviceworkers/ServiceWorkerRegistration.h
@@ -78,16 +78,19 @@ public:
                   nsIGlobalObject* aGlobal,
                   const ServiceWorkerRegistrationDescriptor& aDescriptor);
 
   JSObject*
   WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   void DisconnectFromOwner() override;
 
+  void
+  RegistrationRemoved();
+
   already_AddRefed<ServiceWorker>
   GetInstalling() const;
 
   already_AddRefed<ServiceWorker>
   GetWaiting() const;
 
   already_AddRefed<ServiceWorker>
   GetActive() const;
@@ -126,20 +129,16 @@ public:
 private:
   ServiceWorkerRegistration(nsIGlobalObject* aGlobal,
                             const ServiceWorkerRegistrationDescriptor& aDescriptor,
                             Inner* aInner);
 
   ~ServiceWorkerRegistration();
 
   ServiceWorkerRegistrationDescriptor mDescriptor;
-
-  // This forms a ref-cycle with the inner implementation object.  Its broken
-  // when either the global is torn down or the registration is removed from
-  // the ServiceWorkerManager.
   RefPtr<Inner> mInner;
 
   RefPtr<ServiceWorker> mInstallingWorker;
   RefPtr<ServiceWorker> mWaitingWorker;
   RefPtr<ServiceWorker> mActiveWorker;
   RefPtr<PushManager> mPushManager;
 };
 
--- a/dom/serviceworkers/ServiceWorkerRegistrationImpl.cpp
+++ b/dom/serviceworkers/ServiceWorkerRegistrationImpl.cpp
@@ -83,22 +83,23 @@ ServiceWorkerRegistrationMainThread::Sto
   }
   mListeningForEvents = false;
 }
 
 void
 ServiceWorkerRegistrationMainThread::RegistrationRemovedInternal()
 {
   MOZ_ASSERT(NS_IsMainThread());
-
+  // Its possible for the binding object to be collected while we the
+  // runnable to call this method is in the event queue.  Double check
+  // whether there is still anything to do here.
+  if (mOuter) {
+    mOuter->RegistrationRemoved();
+  }
   StopListeningForEvents();
-
-  // Since the registration is effectively dead in the SWM we can break
-  // the ref-cycle and let the binding object clean up.
-  mOuter = nullptr;
 }
 
 void
 ServiceWorkerRegistrationMainThread::UpdateFound()
 {
   mOuter->DispatchTrustedEvent(NS_LITERAL_STRING("updatefound"));
 }
 
@@ -619,20 +620,17 @@ public:
   }
 };
 } // namespace
 
 already_AddRefed<Promise>
 ServiceWorkerRegistrationMainThread::Update(ErrorResult& aRv)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (!mOuter) {
-    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
-    return nullptr;
-  }
+  MOZ_DIAGNOSTIC_ASSERT(mOuter);
 
   nsCOMPtr<nsIGlobalObject> go = mOuter->GetParentObject();
   if (!go) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   RefPtr<Promise> promise = Promise::Create(go, aRv);
@@ -649,20 +647,17 @@ ServiceWorkerRegistrationMainThread::Upd
 
   return promise.forget();
 }
 
 already_AddRefed<Promise>
 ServiceWorkerRegistrationMainThread::Unregister(ErrorResult& aRv)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (!mOuter) {
-    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
-    return nullptr;
-  }
+  MOZ_DIAGNOSTIC_ASSERT(mOuter);
 
   nsCOMPtr<nsIGlobalObject> go = mOuter->GetParentObject();
   if (!go) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   // Although the spec says that the same-origin checks should also be done
@@ -721,20 +716,17 @@ ServiceWorkerRegistrationMainThread::Unr
 // Notification API extension.
 already_AddRefed<Promise>
 ServiceWorkerRegistrationMainThread::ShowNotification(JSContext* aCx,
                                                       const nsAString& aTitle,
                                                       const NotificationOptions& aOptions,
                                                       ErrorResult& aRv)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (!mOuter) {
-    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
-    return nullptr;
-  }
+  MOZ_DIAGNOSTIC_ASSERT(mOuter);
 
   nsCOMPtr<nsPIDOMWindowInner> window = mOuter->GetOwner();
   if (NS_WARN_IF(!window)) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   nsCOMPtr<nsIDocument> doc = window->GetExtantDoc();
@@ -758,38 +750,32 @@ ServiceWorkerRegistrationMainThread::Sho
 
   return p.forget();
 }
 
 already_AddRefed<Promise>
 ServiceWorkerRegistrationMainThread::GetNotifications(const GetNotificationOptions& aOptions, ErrorResult& aRv)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (!mOuter) {
-    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
-    return nullptr;
-  }
+  MOZ_DIAGNOSTIC_ASSERT(mOuter);
+
   nsCOMPtr<nsPIDOMWindowInner> window = mOuter->GetOwner();
   if (NS_WARN_IF(!window)) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
   return Notification::Get(window, aOptions, mScope, aRv);
 }
 
 already_AddRefed<PushManager>
 ServiceWorkerRegistrationMainThread::GetPushManager(JSContext* aCx,
                                                     ErrorResult& aRv)
 {
   MOZ_ASSERT(NS_IsMainThread());
-
-  if (!mOuter) {
-    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
-    return nullptr;
-  }
+  MOZ_DIAGNOSTIC_ASSERT(mOuter);
 
   nsCOMPtr<nsIGlobalObject> globalObject = mOuter->GetParentObject();
 
   if (!globalObject) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
@@ -912,17 +898,22 @@ ServiceWorkerRegistrationWorkerThread::~
 {
   MOZ_DIAGNOSTIC_ASSERT(!mListener);
   MOZ_DIAGNOSTIC_ASSERT(!mOuter);
 }
 
 void
 ServiceWorkerRegistrationWorkerThread::RegistrationRemoved()
 {
-  mOuter = nullptr;
+  // The SWM notifying us that the registration was removed on the MT may
+  // race with ClearServiceWorkerRegistration() on the worker thread.  So
+  // double-check that mOuter is still valid.
+  if (mOuter) {
+    mOuter->RegistrationRemoved();
+  }
 }
 
 void
 ServiceWorkerRegistrationWorkerThread::SetServiceWorkerRegistration(ServiceWorkerRegistration* aReg)
 {
   MOZ_DIAGNOSTIC_ASSERT(aReg);
   MOZ_DIAGNOSTIC_ASSERT(!mOuter);
   mOuter = aReg;
--- a/dom/serviceworkers/ServiceWorkerRegistrationImpl.h
+++ b/dom/serviceworkers/ServiceWorkerRegistrationImpl.h
@@ -84,17 +84,17 @@ private:
   StartListeningForEvents();
 
   void
   StopListeningForEvents();
 
   void
   RegistrationRemovedInternal();
 
-  RefPtr<ServiceWorkerRegistration> mOuter;
+  ServiceWorkerRegistration* mOuter;
   const nsString mScope;
   bool mListeningForEvents;
 };
 
 ////////////////////////////////////////////////////
 // Worker Thread implementation
 
 class WorkerListener;
@@ -148,21 +148,16 @@ private:
 
   void
   ReleaseListener();
 
   // This can be called only by WorkerListener.
   WorkerPrivate*
   GetWorkerPrivate(const MutexAutoLock& aProofOfLock);
 
-  // Store a strong reference to the outer binding object.  This will create
-  // a ref-cycle.  We must hold it alive in case any events need to be fired
-  // on it.  The cycle is broken when the global becomes detached or the
-  // registration is removed in the ServiceWorkerManager.
-  RefPtr<ServiceWorkerRegistration> mOuter;
-
+  ServiceWorkerRegistration* mOuter;
   const nsString mScope;
   RefPtr<WorkerListener> mListener;
   RefPtr<WeakWorkerRef> mWorkerRef;
 };
 
 } // dom namespace
 } // mozilla namespace
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -3160,16 +3160,22 @@ WorkerPrivate::DoRunLoop(JSContext* aCx)
         // The state of the world may have changed, recheck it.
         normalRunnablesPending = NS_HasPendingEvents(mThread);
         // The debugger queue doesn't get cleared, so we can ignore that.
       }
 
       currentStatus = mStatus;
     }
 
+    if (currentStatus >= Terminating && previousStatus < Terminating) {
+      if (mScope) {
+        mScope->NoteTerminating();
+      }
+    }
+
     // if all holders are done then we can kill this thread.
     if (currentStatus != Running && !HasActiveHolders()) {
 
       // If we just changed status, we must schedule the current runnables.
       if (previousStatus != Running && currentStatus != Killing) {
         NotifyInternal(Killing);
 
 #ifdef DEBUG
--- a/dom/workers/WorkerScope.cpp
+++ b/dom/workers/WorkerScope.cpp
@@ -134,16 +134,22 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 JSObject*
 WorkerGlobalScope::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   MOZ_CRASH("We should never get here!");
 }
 
+void
+WorkerGlobalScope::NoteTerminating()
+{
+  DisconnectEventTargetObjects();
+}
+
 already_AddRefed<Console>
 WorkerGlobalScope::GetConsole(ErrorResult& aRv)
 {
   mWorkerPrivate->AssertIsOnWorkerThread();
 
   if (!mConsole) {
     mConsole = Console::Create(mWorkerPrivate->GetJSContext(),
                                nullptr, aRv);
--- a/dom/workers/WorkerScope.h
+++ b/dom/workers/WorkerScope.h
@@ -87,16 +87,19 @@ public:
                                                          DOMEventTargetHelper)
 
   WorkerGlobalScope*
   Self()
   {
     return this;
   }
 
+  void
+  NoteTerminating();
+
   already_AddRefed<Console>
   GetConsole(ErrorResult& aRv);
 
   Console*
   GetConsoleIfExists() const
   {
     return mConsole;
   }
--- a/layout/tools/reftest/remotereftest.py
+++ b/layout/tools/reftest/remotereftest.py
@@ -332,17 +332,18 @@ class RemoteReftest(RefTest):
             except Exception:
                 print "Automation Error: Failed to copy extra files to device"
                 raise
 
     def printDeviceInfo(self, printLogcat=False):
         try:
             if printLogcat:
                 logcat = self.device.get_logcat(filter_out_regexps=fennecLogcatFilters)
-                print ''.join(logcat)
+                for l in logcat:
+                    print "%s\n" % l.decode('utf-8', 'replace')
             print "Device info:"
             devinfo = self.device.get_info()
             for category in devinfo:
                 if type(devinfo[category]) is list:
                     print "  %s:" % category
                     for item in devinfo[category]:
                         print "     %s" % item
                 else:
--- a/mfbt/Maybe.h
+++ b/mfbt/Maybe.h
@@ -23,27 +23,85 @@
 #include <type_traits>
 
 namespace mozilla {
 
 struct Nothing { };
 
 namespace detail {
 
+// You would think that poisoning Maybe instances could just be a call
+// to mozWritePoison.  Unfortunately, using a simple call to
+// mozWritePoison generates poor code on MSVC for small structures.  The
+// generated code contains (always not-taken) branches and does a bunch
+// of setup for `rep stos{l,q}`, even though we know at compile time
+// exactly how many words we're poisoning.  Instead, we're going to
+// force MSVC to generate the code we want via recursive templates.
+
+// Write the given poisonValue into p at offset*sizeof(uintptr_t).
+template<size_t offset>
+inline void
+WritePoisonAtOffset(void* p, const uintptr_t poisonValue)
+{
+    memcpy(static_cast<char*>(p) + offset*sizeof(poisonValue),
+           &poisonValue, sizeof(poisonValue));
+}
+
+
+template<size_t Offset, size_t NOffsets>
+struct InlinePoisoner
+{
+    static void poison(void* p, const uintptr_t poisonValue)
+    {
+        WritePoisonAtOffset<Offset>(p, poisonValue);
+        InlinePoisoner<Offset+1, NOffsets>::poison(p, poisonValue);
+    }
+};
+
+template<size_t N>
+struct InlinePoisoner<N, N>
+{
+    static void poison(void*, const uintptr_t)
+    {
+        // All done!
+    }
+};
+
+// We can't generate inline code for large structures, though, because we'll
+// blow out recursive template instantiation limits, and the code would be
+// bloated to boot.  So provide a fallback to the out-of-line poisoner.
+template<size_t ObjectSize>
+struct OutOfLinePoisoner
+{
+  static void poison(void* p, const uintptr_t)
+  {
+    mozWritePoison(p, ObjectSize);
+  }
+};
+
+template<typename T>
+inline void
+PoisonObject(T* p)
+{
+  const uintptr_t POISON = mozPoisonValue();
+  Conditional<(sizeof(T) <= 8*sizeof(POISON)),
+    InlinePoisoner<0, sizeof(T) / sizeof(POISON)>,
+    OutOfLinePoisoner<sizeof(T)>>::Type::poison(p, POISON);
+}
+
 template<typename T>
 struct MaybePoisoner
 {
   static const size_t N = sizeof(T);
 
   static void poison(void* aPtr)
   {
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
-    // Avoid MOZ_ASSERT in mozWritePoison.
     if (N >= sizeof(uintptr_t)) {
-      mozWritePoison(aPtr, N);
+      PoisonObject(static_cast<typename RemoveCV<T>::Type*>(aPtr));
     }
 #endif
     MOZ_MAKE_MEM_UNDEFINED(aPtr, N);
   }
 };
 
 } // namespace detail
 
@@ -121,78 +179,68 @@ class MOZ_NON_PARAM MOZ_INHERIT_TYPE_ANN
     detail::MaybePoisoner<T>::poison(data());
   }
 
 public:
   using ValueType = T;
 
   Maybe() : mIsSome(false)
   {
-    poisonData();
   }
   ~Maybe() { reset(); }
 
   MOZ_IMPLICIT Maybe(Nothing) : mIsSome(false)
   {
-    poisonData();
   }
 
   Maybe(const Maybe& aOther)
     : mIsSome(false)
   {
     if (aOther.mIsSome) {
       emplace(*aOther);
-    } else {
-      poisonData();
     }
   }
 
   /**
    * Maybe<T> can be copy-constructed from a Maybe<U> if U is convertible to T.
    */
   template<typename U,
            typename =
              typename std::enable_if<std::is_convertible<U, T>::value>::type>
   MOZ_IMPLICIT
   Maybe(const Maybe<U>& aOther)
     : mIsSome(false)
   {
     if (aOther.isSome()) {
       emplace(*aOther);
-    } else {
-      poisonData();
     }
   }
 
   Maybe(Maybe&& aOther)
     : mIsSome(false)
   {
     if (aOther.mIsSome) {
       emplace(Move(*aOther));
       aOther.reset();
-    } else {
-      poisonData();
     }
   }
 
   /**
    * Maybe<T> can be move-constructed from a Maybe<U> if U is convertible to T.
    */
   template<typename U,
            typename =
              typename std::enable_if<std::is_convertible<U, T>::value>::type>
   MOZ_IMPLICIT
   Maybe(Maybe<U>&& aOther)
     : mIsSome(false)
   {
     if (aOther.isSome()) {
       emplace(Move(*aOther));
       aOther.reset();
-    } else {
-      poisonData();
     }
   }
 
   Maybe& operator=(const Maybe& aOther)
   {
     if (&aOther != this) {
       if (aOther.mIsSome) {
         if (mIsSome) {
--- a/netwerk/base/PartiallySeekableInputStream.cpp
+++ b/netwerk/base/PartiallySeekableInputStream.cpp
@@ -32,30 +32,32 @@ PartiallySeekableInputStream::PartiallyS
                                                            uint64_t aBufferSize)
   : mInputStream(Move(aInputStream))
   , mWeakCloneableInputStream(nullptr)
   , mWeakIPCSerializableInputStream(nullptr)
   , mWeakAsyncInputStream(nullptr)
   , mBufferSize(aBufferSize)
   , mPos(0)
   , mClosed(false)
+  , mMutex("PartiallySeekableInputStream::mMutex")
 {
   Init();
 }
 
 PartiallySeekableInputStream::PartiallySeekableInputStream(already_AddRefed<nsIInputStream> aClonedBaseStream,
                                                            PartiallySeekableInputStream* aClonedFrom)
   : mInputStream(Move(aClonedBaseStream))
   , mWeakCloneableInputStream(nullptr)
   , mWeakIPCSerializableInputStream(nullptr)
   , mWeakAsyncInputStream(nullptr)
   , mCachedBuffer(aClonedFrom->mCachedBuffer)
   , mBufferSize(aClonedFrom->mBufferSize)
   , mPos(aClonedFrom->mPos)
   , mClosed(aClonedFrom->mClosed)
+  , mMutex("PartiallySeekableInputStream::mMutex")
 {
   Init();
 }
 
 void
 PartiallySeekableInputStream::Init()
 {
   MOZ_ASSERT(mInputStream);
@@ -222,47 +224,52 @@ PartiallySeekableInputStream::AsyncWait(
                                         nsIEventTarget* aEventTarget)
 {
   if (mClosed) {
     return NS_BASE_STREAM_CLOSED;
   }
 
   NS_ENSURE_STATE(mWeakAsyncInputStream);
 
-  if (mAsyncWaitCallback && aCallback) {
-    return NS_ERROR_FAILURE;
+  nsCOMPtr<nsIInputStreamCallback> callback = aCallback ? this : nullptr;
+  {
+    MutexAutoLock lock(mMutex);
+    if (mAsyncWaitCallback && aCallback) {
+      return NS_ERROR_FAILURE;
+    }
+
+    mAsyncWaitCallback = aCallback;
   }
 
-  mAsyncWaitCallback = aCallback;
-
-  if (!mAsyncWaitCallback) {
-    return NS_OK;
-  }
-
-  return mWeakAsyncInputStream->AsyncWait(this, aFlags, aRequestedCount,
+  return mWeakAsyncInputStream->AsyncWait(callback, aFlags, aRequestedCount,
                                           aEventTarget);
 }
 
 // nsIInputStreamCallback
 
 NS_IMETHODIMP
 PartiallySeekableInputStream::OnInputStreamReady(nsIAsyncInputStream* aStream)
 {
   MOZ_ASSERT(mWeakAsyncInputStream);
   MOZ_ASSERT(mWeakAsyncInputStream == aStream);
 
-  // We have been canceled in the meanwhile.
-  if (!mAsyncWaitCallback) {
-    return NS_OK;
+  nsCOMPtr<nsIInputStreamCallback> callback;
+
+  {
+    MutexAutoLock lock(mMutex);
+
+    // We have been canceled in the meanwhile.
+    if (!mAsyncWaitCallback) {
+      return NS_OK;
+    }
+
+    callback.swap(mAsyncWaitCallback);
   }
 
-  nsCOMPtr<nsIInputStreamCallback> callback = mAsyncWaitCallback;
-
-  mAsyncWaitCallback = nullptr;
-
+  MOZ_ASSERT(callback);
   return callback->OnInputStreamReady(this);
 }
 
 // nsIIPCSerializableInputStream
 
 void
 PartiallySeekableInputStream::Serialize(mozilla::ipc::InputStreamParams& aParams,
                                         FileDescriptorArray& aFileDescriptors)
--- a/netwerk/base/PartiallySeekableInputStream.h
+++ b/netwerk/base/PartiallySeekableInputStream.h
@@ -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/. */
 
 #ifndef PartiallySeekableInputStream_h
 #define PartiallySeekableInputStream_h
 
 #include "mozilla/Attributes.h"
+#include "mozilla/Mutex.h"
 #include "nsCOMPtr.h"
 #include "nsIAsyncInputStream.h"
 #include "nsICloneableInputStream.h"
 #include "nsIIPCSerializableInputStream.h"
 #include "nsISeekableStream.h"
 
 namespace mozilla {
 namespace net {
@@ -48,21 +49,24 @@ private:
 
   nsCOMPtr<nsIInputStream> mInputStream;
 
   // Raw pointers because these are just QI of mInputStream.
   nsICloneableInputStream* mWeakCloneableInputStream;
   nsIIPCSerializableInputStream* mWeakIPCSerializableInputStream;
   nsIAsyncInputStream* mWeakAsyncInputStream;
 
+  // Protected by mutex.
   nsCOMPtr<nsIInputStreamCallback> mAsyncWaitCallback;
 
   nsTArray<char> mCachedBuffer;
 
   uint64_t mBufferSize;
   uint64_t mPos;
   bool mClosed;
+
+  Mutex mMutex;
 };
 
 } // net namespace
 } // mozilla namespace
 
 #endif // PartiallySeekableInputStream_h
--- a/netwerk/base/nsBufferedStreams.cpp
+++ b/netwerk/base/nsBufferedStreams.cpp
@@ -300,16 +300,17 @@ NS_INTERFACE_MAP_END_INHERITING(nsBuffer
 NS_IMPL_CI_INTERFACE_GETTER(nsBufferedInputStream,
                             nsIInputStream,
                             nsIBufferedInputStream,
                             nsISeekableStream,
                             nsIStreamBufferAccess)
 
 nsBufferedInputStream::nsBufferedInputStream()
    : nsBufferedStream()
+   , mMutex("nsBufferedInputStream::mMutex")
    , mIsIPCSerializable(true)
    , mIsAsyncInputStream(false)
    , mIsCloneableInputStream(false)
 {}
 
 nsresult
 nsBufferedInputStream::Create(nsISupports *aOuter, REFNSIID aIID, void **aResult)
 {
@@ -661,40 +662,46 @@ nsBufferedInputStream::AsyncWait(nsIInpu
                                 uint32_t aRequestedCount,
                                 nsIEventTarget* aEventTarget)
 {
     nsCOMPtr<nsIAsyncInputStream> stream = do_QueryInterface(mStream);
     if (!stream) {
         return NS_ERROR_FAILURE;
     }
 
-    if (mAsyncWaitCallback && aCallback) {
-        return NS_ERROR_FAILURE;
+    nsCOMPtr<nsIInputStreamCallback> callback = aCallback ? this : nullptr;
+    {
+        MutexAutoLock lock(mMutex);
+
+        if (mAsyncWaitCallback && aCallback) {
+            return NS_ERROR_FAILURE;
+        }
+
+        mAsyncWaitCallback = aCallback;
     }
 
-    mAsyncWaitCallback = aCallback;
-
-    if (!mAsyncWaitCallback) {
-        return NS_OK;
-    }
-
-    return stream->AsyncWait(this, aFlags, aRequestedCount, aEventTarget);
+    return stream->AsyncWait(callback, aFlags, aRequestedCount, aEventTarget);
 }
 
 NS_IMETHODIMP
 nsBufferedInputStream::OnInputStreamReady(nsIAsyncInputStream* aStream)
 {
-    // We have been canceled in the meanwhile.
-    if (!mAsyncWaitCallback) {
-        return NS_OK;
+    nsCOMPtr<nsIInputStreamCallback> callback;
+    {
+        MutexAutoLock lock(mMutex);
+
+        // We have been canceled in the meanwhile.
+        if (!mAsyncWaitCallback) {
+            return NS_OK;
+        }
+
+        callback.swap(mAsyncWaitCallback);
     }
 
-    nsCOMPtr<nsIInputStreamCallback> callback;
-    callback.swap(mAsyncWaitCallback);
-
+    MOZ_ASSERT(callback);
     return callback->OnInputStreamReady(this);
 }
 
 NS_IMETHODIMP
 nsBufferedInputStream::GetData(nsIInputStream **aResult)
 {
     nsCOMPtr<nsISupports> stream;
     nsBufferedStream::GetData(getter_AddRefs(stream));
--- a/netwerk/base/nsBufferedStreams.h
+++ b/netwerk/base/nsBufferedStreams.h
@@ -11,16 +11,17 @@
 #include "nsIOutputStream.h"
 #include "nsISafeOutputStream.h"
 #include "nsISeekableStream.h"
 #include "nsIStreamBufferAccess.h"
 #include "nsCOMPtr.h"
 #include "nsIIPCSerializableInputStream.h"
 #include "nsIAsyncInputStream.h"
 #include "nsICloneableInputStream.h"
+#include "mozilla/Mutex.h"
 
 ////////////////////////////////////////////////////////////////////////////////
 
 class nsBufferedStream : public nsISeekableStream
 {
 public:
     NS_DECL_THREADSAFE_ISUPPORTS
     NS_DECL_NSISEEKABLESTREAM
@@ -90,17 +91,21 @@ public:
     }
 
 protected:
     virtual ~nsBufferedInputStream() {}
 
     NS_IMETHOD Fill() override;
     NS_IMETHOD Flush() override { return NS_OK; } // no-op for input streams
 
+    mozilla::Mutex mMutex;
+
+    // This value is protected by mutex.
     nsCOMPtr<nsIInputStreamCallback> mAsyncWaitCallback;
+
     bool mIsIPCSerializable;
     bool mIsAsyncInputStream;
     bool mIsCloneableInputStream;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 
 class nsBufferedOutputStream  : public nsBufferedStream,
--- a/netwerk/base/nsMIMEInputStream.cpp
+++ b/netwerk/base/nsMIMEInputStream.cpp
@@ -16,16 +16,17 @@
 #include "nsIHttpHeaderVisitor.h"
 #include "nsIMIMEInputStream.h"
 #include "nsISeekableStream.h"
 #include "nsString.h"
 #include "nsMIMEInputStream.h"
 #include "nsIClassInfoImpl.h"
 #include "nsIIPCSerializableInputStream.h"
 #include "mozilla/Move.h"
+#include "mozilla/Mutex.h"
 #include "mozilla/ipc/InputStreamUtils.h"
 
 using namespace mozilla::ipc;
 using mozilla::Maybe;
 using mozilla::Move;
 
 class nsMIMEInputStream : public nsIMIMEInputStream,
                           public nsISeekableStream,
@@ -62,16 +63,19 @@ private:
     bool IsAsyncInputStream() const;
     bool IsIPCSerializable() const;
 
     nsTArray<HeaderEntry> mHeaders;
 
     nsCOMPtr<nsIInputStream> mStream;
     bool mStartedReading;
 
+    mozilla::Mutex mMutex;
+
+    // This is protected by mutex.
     nsCOMPtr<nsIInputStreamCallback> mAsyncWaitCallback;
 };
 
 NS_IMPL_ADDREF(nsMIMEInputStream)
 NS_IMPL_RELEASE(nsMIMEInputStream)
 
 NS_IMPL_CLASSINFO(nsMIMEInputStream, nullptr, nsIClassInfo::THREADSAFE,
                   NS_MIMEINPUTSTREAM_CID)
@@ -91,17 +95,19 @@ NS_INTERFACE_MAP_BEGIN(nsMIMEInputStream
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CI_INTERFACE_GETTER(nsMIMEInputStream,
                             nsIMIMEInputStream,
                             nsIAsyncInputStream,
                             nsIInputStream,
                             nsISeekableStream)
 
-nsMIMEInputStream::nsMIMEInputStream() : mStartedReading(false)
+nsMIMEInputStream::nsMIMEInputStream()
+  : mStartedReading(false)
+  , mMutex("nsMIMEInputStream::mMutex")
 {
 }
 
 nsMIMEInputStream::~nsMIMEInputStream()
 {
 }
 
 NS_IMETHODIMP
@@ -247,37 +253,49 @@ nsMIMEInputStream::AsyncWait(nsIInputStr
                              nsIEventTarget* aEventTarget)
 {
     INITSTREAMS;
     nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(mStream);
     if (NS_WARN_IF(!asyncStream)) {
       return NS_ERROR_FAILURE;
     }
 
-    if (mAsyncWaitCallback && aCallback) {
-      return NS_ERROR_FAILURE;
+    nsCOMPtr<nsIInputStreamCallback> callback = aCallback ? this : nullptr;
+    {
+        MutexAutoLock lock(mMutex);
+        if (mAsyncWaitCallback && aCallback) {
+            return NS_ERROR_FAILURE;
+        }
+
+        mAsyncWaitCallback = aCallback;
     }
 
-    mAsyncWaitCallback = aCallback;
-
-    return asyncStream->AsyncWait(this, aFlags, aRequestedCount, aEventTarget);
+    return asyncStream->AsyncWait(callback, aFlags, aRequestedCount,
+                                  aEventTarget);
 }
 
 // nsIInputStreamCallback
 
 NS_IMETHODIMP
 nsMIMEInputStream::OnInputStreamReady(nsIAsyncInputStream* aStream)
 {
-  // We have been canceled in the meanwhile.
-  if (!mAsyncWaitCallback) {
-    return NS_OK;
+    nsCOMPtr<nsIInputStreamCallback> callback;
+
+    {
+        MutexAutoLock lock(mMutex);
+
+        // We have been canceled in the meanwhile.
+        if (!mAsyncWaitCallback) {
+            return NS_OK;
+        }
+
+        callback.swap(mAsyncWaitCallback);
   }
 
-  nsCOMPtr<nsIInputStreamCallback> callback = mAsyncWaitCallback;
-  mAsyncWaitCallback = nullptr;
+  MOZ_ASSERT(callback);
   return callback->OnInputStreamReady(this);
 }
 
 // nsISeekableStream
 NS_IMETHODIMP nsMIMEInputStream::Tell(int64_t *_retval)
 {
     INITSTREAMS;
     nsCOMPtr<nsISeekableStream> stream = do_QueryInterface(mStream);
--- a/taskcluster/docs/kinds.rst
+++ b/taskcluster/docs/kinds.rst
@@ -157,25 +157,19 @@ Docker repository
 
 The task definition used to create the image-building tasks is given in
 ``image.yml`` in the kind directory, and is interpreted as a :doc:`YAML
 Template <yaml-templates>`.
 
 balrog
 ------
 
-Balrog is the Mozilla Update Server. Jobs of this kind are submitting information
-which assists in telling Firefox that an update is available for the related job.
-
-balrog-l10n
------------
-
-Balrog is the Mozilla Update Server. Jobs of this kind are submitting information
-which assists in telling Firefox that an update is available for the localized
-job involved.
+Balrog tasks are responsible for submitting metadata to our update server (Balrog).
+They are typically downstream of a beetmover job that moves signed MARs somewhere
+(eg: beetmover and beetmover-l10n for releases, beetmover-repackage for nightlies).
 
 beetmover
 ---------
 
 Beetmover, takes specific artifacts, "Beets", and pushes them to a location outside
 of Taskcluster's task artifacts, (archive.mozilla.org as one place) and in the
 process determines the final location and a "pretty" name (versioned product name)
 
@@ -224,30 +218,37 @@ Artifact is then used by push-apk.
 
 push-apk
 --------
 PushApk publishes Android packages onto Google Play Store. Jobs of this kind take
 all the signed multi-locales (aka "multi") APKs for a given release and upload them
 all at once.
 
 release-balrog-submit-toplevel
-----------------------
-Push a top-level release blob to Balrog.
+------------------------------
+Toplevel tasks are responsible for submitting metadata to Balrog that is not specific to any
+particular platform+locale. For example: fileUrl templates, versions, and platform aliases.
+
+Toplevel tasks are also responsible for updating test channel rules to point at the Release
+being generated.
 
 release-secondary-balrog-submit-toplevel
-----------------------
-Push a top-level RC release blob to Balrog.
+----------------------------------------
+Performs the same function as `release-balrog-submit-toplevel`, but against the beta channel
+during RC builds.
 
 release-balrog-scheduling
-----------------------
-Schedule a release to go live in Balrog.
+-------------------------
+Schedules a Release for shipping in Balrog. If a `release_eta` was provided when starting the Release,
+it will be scheduled to go live at that day and time.
 
 release-secondary-balrog-scheduling
-----------------------
-Schedule an RC release to go live in Balrog.
+-----------------------------------
+Performs the same function as `release-balrog-scheduling`, except for the beta channel as part of RC
+Releases.
 
 release-binary-transparency
 ---------------------------
 Binary transparency creates a publicly verifiable log of binary shas for downstream
 release auditing. https://wiki.mozilla.org/Security/Binary_Transparency
 
 release-snap-repackage
 ----------------------
@@ -304,20 +305,16 @@ Submit to S3 the artifacts produced by t
 release-final-verify
 --------------------
 Verifies the contents and package of release update MARs.
 
 release-secondary-final-verify
 ------------------------------
 Verifies the contents and package of release update MARs for RC releases.
 
-release-secondary-balrog-publishing
----------------------
-Schedule an RC release to go live in Balrog. Usually this will happen on the beta channel, to a smaller audience, before the RC goes live on the release channel.
-
 release-update-verify
 ---------------------
 Verifies the contents and package of release update MARs.
 
 release-secondary-update-verify
 ---------------------
 Verifies the contents and package of release update MARs.
 
--- a/taskcluster/docs/release-promotion.rst
+++ b/taskcluster/docs/release-promotion.rst
@@ -41,8 +41,9 @@ limited rollout percentage or are depend
 fully ship.
 
 In-depth relpro guide
 ---------------------
 
 .. toctree::
 
     release-promotion-action
+    balrog
--- a/testing/config/tooltool-manifests/linux64/hostutils.manifest
+++ b/testing/config/tooltool-manifests/linux64/hostutils.manifest
@@ -1,10 +1,10 @@
 [
   {
-    "size": 74983178,
+    "size": 75035382,
     "visibility": "public",
-    "digest": "800acc77951dbd47880ded6ebfb70b2b6a1e034a52cb0e84fda1931e1825152853c0b5cfa4d05afdc2038f30c1f077fd037d9a49751505d98cbeb2b7e0377328",
+    "digest": "3e16ca4a0dea9522a8e917b49ee700a162e83297a5c16485dd418c31b1b8bb977b2ca1ff16ccd05a1b0cf0d83d72bfbc92e12ba9f8e60c368cf1051639546472",
     "algorithm": "sha512",
-    "filename": "host-utils-60.0a2.en-US.linux-x86_64.tar.gz",
+    "filename": "host-utils-60.0a3.en-US.linux-x86_64.tar.gz",
     "unpack": true
   }
 ]
--- a/testing/mochitest/rungeckoview.py
+++ b/testing/mochitest/rungeckoview.py
@@ -97,18 +97,19 @@ class GeckoviewTestRunner:
             env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
             env["XPCOM_DEBUG_BREAK"] = "stack"
             env["DISABLE_UNSAFE_CPOW_WARNINGS"] = "1"
             env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
             env["MOZ_IN_AUTOMATION"] = "1"
             env["R_LOG_VERBOSE"] = "1"
             env["R_LOG_LEVEL"] = "6"
             env["R_LOG_DESTINATION"] = "stderr"
-            self.device.launch_geckoview_example("org.mozilla.geckoview_example",
-                                                 extra_args=args, moz_env=env)
+            self.device.launch_activity("org.mozilla.geckoview_example",
+                                        "GeckoViewActivity",
+                                        extra_args=args, moz_env=env)
         except Exception:
             return (False, "Exception during %s startup" % self.appname)
         return (True, "%s started" % self.appname)
 
     def started(self):
         """
         startup logcat messages
         """
--- a/testing/mochitest/runrobocop.py
+++ b/testing/mochitest/runrobocop.py
@@ -326,21 +326,18 @@ class RobocopTestRunner(MochitestDesktop
            Log remote device information and logcat (if requested).
 
            This is similar to printDeviceInfo in runtestsremote.py
         """
         try:
             if printLogcat:
                 logcat = self.device.get_logcat(
                     filter_out_regexps=fennecLogcatFilters)
-                self.log.info(
-                    '\n' +
-                    ''.join(logcat).decode(
-                        'utf-8',
-                        'replace'))
+                for l in logcat:
+                    self.log.info(l.decode('utf-8', 'replace'))
             self.log.info("Device info:")
             devinfo = self.device.get_info()
             for category in devinfo:
                 if type(devinfo[category]) is list:
                     self.log.info("  %s:" % category)
                     for item in devinfo[category]:
                         self.log.info("     %s" % item)
                 else:
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -266,21 +266,18 @@ class MochiRemote(MochitestDesktop):
     def getLogFilePath(self, logFile):
         return logFile
 
     def printDeviceInfo(self, printLogcat=False):
         try:
             if printLogcat:
                 logcat = self.device.get_logcat(
                     filter_out_regexps=fennecLogcatFilters)
-                self.log.info(
-                    '\n' +
-                    ''.join(logcat).decode(
-                        'utf-8',
-                        'replace'))
+                for l in logcat:
+                    self.log.info(l.decode('utf-8', 'replace'))
             self.log.info("Device info:")
             devinfo = self.device.get_info()
             for category in devinfo:
                 if type(devinfo[category]) is list:
                     self.log.info("  %s:" % category)
                     for item in devinfo[category]:
                         self.log.info("     %s" % item)
                 else:
--- a/testing/mozbase/mozdevice/mozdevice/adb_android.py
+++ b/testing/mozbase/mozdevice/mozdevice/adb_android.py
@@ -444,27 +444,30 @@ class ADBAndroid(ADBDevice):
         if extra_args:
             extras['args'] = " ".join(extra_args)
 
         self.launch_application(app_name, "org.mozilla.gecko.BrowserApp",
                                 intent, url=url, extras=extras,
                                 wait=wait, fail_if_running=fail_if_running,
                                 timeout=timeout)
 
-    def launch_geckoview_example(self, app_name, intent="android.intent.action.Main",
-                                 moz_env=None, extra_args=None, url=None, e10s=False,
-                                 wait=True, fail_if_running=True, timeout=None):
-        """Convenience method to launch geckoview_example on Android with various
-        debugging arguments
+    def launch_activity(self, app_name, activity_name=None,
+                        intent="android.intent.action.Main",
+                        moz_env=None, extra_args=None, url=None, e10s=False,
+                        wait=True, fail_if_running=True, timeout=None):
+        """Convenience method to launch an application on Android with various
+        debugging arguments; convenient for geckoview apps.
 
         :param str app_name: Name of application (e.g.
-            `org.mozilla.geckoview_example`)
+            `org.mozilla.geckoview_example` or `org.mozilla.geckoview.test`)
+        :param str activity_name: Activity name, like `GeckoViewActivity`, or
+            `TestRunnerActivity`.
         :param str intent: Intent to launch application.
         :type moz_env: str or None
-        :param extra_args: Extra arguments to be parsed by fennec.
+        :param extra_args: Extra arguments to be parsed by the app.
         :type extra_args: str or None
         :param url: URL to open
         :type url: str or None
         :param bool e10s: If True, run in multiprocess mode.
         :param bool wait: If True, wait for application to start before
             returning.
         :param bool fail_if_running: Raise an exception if instance of
             application is already running.
@@ -481,23 +484,23 @@ class ADBAndroid(ADBDevice):
         extras = {}
 
         if moz_env:
             # moz_env is expected to be a dictionary of environment variables:
             # geckoview_example itself will set them when launched
             for (env_count, (env_key, env_val)) in enumerate(moz_env.iteritems()):
                 extras["env" + str(env_count)] = env_key + "=" + env_val
 
-        # Additional command line arguments that geckoview_example will read and use (e.g.
+        # Additional command line arguments that the app will read and use (e.g.
         # with a custom profile)
         if extra_args:
             extras['args'] = " ".join(extra_args)
         extras['use_multiprocess'] = e10s
         self.launch_application(app_name,
-                                "%s.GeckoViewActivity" % app_name,
+                                "%s.%s" % (app_name, activity_name),
                                 intent, url=url, extras=extras,
                                 wait=wait, fail_if_running=fail_if_running,
                                 timeout=timeout)
 
     def stop_application(self, app_name, timeout=None, root=False):
         """Stops the specified application
 
         For Android 3.0+, we use the "am force-stop" to do this, which
--- a/testing/mozharness/scripts/android_emulator_unittest.py
+++ b/testing/mozharness/scripts/android_emulator_unittest.py
@@ -374,19 +374,21 @@ class AndroidEmulatorTest(TestingMixin, 
             os.close(tmpfd)
             self.info("Taking screenshot with %s; saving to %s" % (utility, filename))
             subprocess.call([utility, filename], env=self.query_env())
         except OSError, err:
             self.warning("Failed to take screenshot: %s" % err.strerror)
 
     def _query_package_name(self):
         if self.app_name is None:
-            # For convenience, assume geckoview_example when install target
-            # looks like geckoview.
-            if 'geckoview' in self.installer_path:
+            # For convenience, assume geckoview.test/geckoview_example when install
+            # target looks like geckoview.
+            if 'androidTest' in self.installer_path:
+                self.app_name = 'org.mozilla.geckoview.test'
+            elif 'geckoview' in self.installer_path:
                 self.app_name = 'org.mozilla.geckoview_example'
         if self.app_name is None:
             # Find appname from package-name.txt - assumes download-and-extract
             # has completed successfully.
             # The app/package name will typically be org.mozilla.fennec,
             # but org.mozilla.firefox for release builds, and there may be
             # other variations. 'aapt dump badging <apk>' could be used as an
             # alternative to package-name.txt, but introduces a dependency
--- a/testing/talos/talos/tests/devtools/addon/content/tests/debugger/custom.js
+++ b/testing/talos/talos/tests/devtools/addon/content/tests/debugger/custom.js
@@ -1,39 +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/. */
 
 "use strict";
 
+const Services = require("Services");
 const { closeToolboxAndLog, garbageCollect, runTest, testSetup,
         testTeardown, PAGES_BASE_URL } = require("../head");
 const { createContext, openDebuggerAndLog, pauseDebugger, reloadDebuggerAndLog,
         removeBreakpoints, resume, step } = require("./debugger-helpers");
 
 const EXPECTED = {
   sources: 7,
   file: "App.js",
   text: "import React, { Component } from 'react';"
 };
 
 const EXPECTED_FUNCTION = "window.hitBreakpoint()";
 
 module.exports = async function() {
   const tab = await testSetup(PAGES_BASE_URL + "custom/debugger/index.html");
+  Services.prefs.setBoolPref("devtools.debugger.features.map-scopes", false);
 
   const toolbox = await openDebuggerAndLog("custom", EXPECTED);
   await reloadDebuggerAndLog("custom", toolbox, EXPECTED);
 
   // these tests are only run on custom.jsdebugger
   await pauseDebuggerAndLog(tab, toolbox, EXPECTED_FUNCTION);
   await stepDebuggerAndLog(tab, toolbox, EXPECTED_FUNCTION);
 
   await closeToolboxAndLog("custom.jsdebugger", toolbox);
 
+  Services.prefs.clearUserPref("devtools.debugger.features.map-scopes");
   await testTeardown();
 };
 
 async function pauseDebuggerAndLog(tab, toolbox, testFunction) {
   dump("Waiting for debugger panel\n");
   const panel = await toolbox.getPanelWhenReady("jsdebugger");
 
   dump("Creating context\n");
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -318226,16 +318226,22 @@
     ]
    ],
    "css/cssom/css-style-declaration-modifications.html": [
     [
      "/css/cssom/css-style-declaration-modifications.html",
      {}
     ]
    ],
+   "css/cssom/css-style-reparse.html": [
+    [
+     "/css/cssom/css-style-reparse.html",
+     {}
+    ]
+   ],
    "css/cssom/cssimportrule.html": [
     [
      "/css/cssom/cssimportrule.html",
      {}
     ]
    ],
    "css/cssom/cssom-cssText-serialize.html": [
     [
@@ -531795,16 +531801,20 @@
   "css/cssom/css-style-attribute-modifications.html": [
    "9199534f3b6cc473832562b1701ade3a05dde172",
    "testharness"
   ],
   "css/cssom/css-style-declaration-modifications.html": [
    "c169d758c1d91b75697b04cf72750f8ac1650e1a",
    "testharness"
   ],
+  "css/cssom/css-style-reparse.html": [
+   "d83b1fde05df64628f67b7773757c12d213f566b",
+   "testharness"
+  ],
   "css/cssom/cssimportrule.html": [
    "c7a70c7836b5a31631b12cc47f280d507542571d",
    "testharness"
   ],
   "css/cssom/cssom-cssText-serialize.html": [
    "66ad91da39c1e1da9021f6443e9b6d34baf57dcb",
    "testharness"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/css-style-reparse.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset=utf-8>
+  <title>CSS Test: DOM modification re-parsing test</title>
+  <link rel="help" href="https://drafts.csswg.org/cssom/">
+  <link rel="help" href="http://www.w3.org/TR/cssom-1/#the-cssrule-interface">
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <style>div { min-width: 0px; }</style>
+  <style id="style-element"></style>
+</head>
+<body>
+<div id="test-div"></div>
+<script type="text/javascript">
+    var style = document.getElementById("style-element");
+    var div = document.getElementById("test-div");
+
+    function testProperty(prop) {
+      // Assigning an empty string to textContent or innerHTML should trigger a
+      // reparse only if the element is not empty.
+      style.sheet.insertRule("#test-div { min-width: 42px; }");
+      assert_equals(getComputedStyle(div).minWidth, "42px");
+
+      style[prop] = "";
+      assert_equals(getComputedStyle(div).minWidth, "42px");
+
+      style[prop] = " ";
+      assert_equals(getComputedStyle(div).minWidth, "0px");
+
+      style.sheet.insertRule("#test-div { min-width: 42px; }");
+      assert_equals(getComputedStyle(div).minWidth, "42px");
+
+      style[prop] = "";
+      assert_equals(getComputedStyle(div).minWidth, "0px");
+
+      style.sheet.insertRule("#test-div { min-width: 42px; }");
+      assert_equals(getComputedStyle(div).minWidth, "42px");
+
+      style.appendChild(document.createTextNode(""));
+      assert_equals(getComputedStyle(div).minWidth, "0px");
+
+      style.sheet.insertRule("#test-div { min-width: 42px; }");
+      assert_equals(getComputedStyle(div).minWidth, "42px");
+
+      style[prop] = "";
+      assert_equals(getComputedStyle(div).minWidth, "0px");
+    }
+
+    test(function() {
+      testProperty("textContent");
+    }, "style.textContent modification");
+
+    test(function() {
+      testProperty("innerHTML");
+    }, "style.innerHTML modification");
+</script>
+</body>
+</html>
--- a/testing/xpcshell/remotexpcshelltests.py
+++ b/testing/xpcshell/remotexpcshelltests.py
@@ -431,16 +431,22 @@ class XPCShellRemote(xpcshell.XPCShellTe
         if self.options['localAPK']:
             remoteFile = posixpath.join(self.remoteBinDir,
                                         os.path.basename(self.options['localAPK']))
             self.device.push(self.options['localAPK'], remoteFile)
 
         self.pushLibs()
 
     def pushLibs(self):
+        elfhack = None
+        xrePath = self.options.get('xrePath')
+        if xrePath:
+            elfhack = os.path.join(xrePath, 'elfhack')
+            if not os.path.exists(elfhack):
+                elfhack = None
         pushed_libs_count = 0
         if self.options['localAPK']:
             try:
                 dir = tempfile.mkdtemp()
                 for info in self.localAPKContents.infolist():
                     if info.filename.endswith(".so"):
                         print("Pushing %s.." % info.filename, file=sys.stderr)
                         remoteFile = posixpath.join(self.remoteBinDir,
@@ -449,16 +455,20 @@ class XPCShellRemote(xpcshell.XPCShellTe
                         localFile = os.path.join(dir, info.filename)
                         with open(localFile) as f:
                             # Decompress xz-compressed file.
                             if f.read(5)[1:] == '7zXZ':
                                 cmd = ['xz', '-df', '--suffix', '.so', localFile]
                                 subprocess.check_output(cmd)
                                 # xz strips the ".so" file suffix.
                                 os.rename(localFile[:-3], localFile)
+                                # elfhack -r should provide better crash reports
+                                if elfhack:
+                                    cmd = [elfhack, '-r', localFile]
+                                    subprocess.check_output(cmd)
                         self.device.push(localFile, remoteFile)
                         pushed_libs_count += 1
             finally:
                 shutil.rmtree(dir)
             return pushed_libs_count
 
         for file in os.listdir(self.localLib):
             if (file.endswith(".so")):
--- a/toolkit/components/printing/content/printPreviewBindings.xml
+++ b/toolkit/components/printing/content/printPreviewBindings.xml
@@ -30,17 +30,17 @@
       <xul:vbox align="center" pack="center">
         <xul:label value="&page.label;" accesskey="&page.accesskey;" control="pageNumber"/>
       </xul:vbox>
       <xul:toolbarbutton anonid="navigateHome" class="navigate-button tabbable"
         oncommand="parentNode.navigate(0, 0, 'home');" tooltiptext="&homearrow.tooltip;"/>
       <xul:toolbarbutton anonid="navigatePrevious" class="navigate-button tabbable"
         oncommand="parentNode.navigate(-1, 0, 0);" tooltiptext="&previousarrow.tooltip;"/>
       <xul:hbox align="center" pack="center">
-        <xul:textbox id="pageNumber" size="3" value="1" min="1" type="number"
+        <xul:textbox id="pageNumber" value="1" min="1" type="number"
           hidespinbuttons="true" onchange="navigate(0, this.valueNumber, 0);"/>
         <xul:label value="&of.label;"/>
         <xul:label value="1"/>
       </xul:hbox>
       <xul:toolbarbutton anonid="navigateNext" class="navigate-button tabbable"
         oncommand="parentNode.navigate(1, 0, 0);" tooltiptext="&nextarrow.tooltip;"/>
       <xul:toolbarbutton anonid="navigateEnd" class="navigate-button tabbable"
         oncommand="parentNode.navigate(0, 0, 'end');" tooltiptext="&endarrow.tooltip;"/>
--- a/toolkit/content/tests/chrome/test_textbox_number.xul
+++ b/toolkit/content/tests/chrome/test_textbox_number.xul
@@ -5,29 +5,29 @@
   XUL Widget Test for textbox type="number"
   -->
 <window title="Textbox type='number' test" width="500" height="600"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
 
 <hbox>
-  <textbox id="n1" type="number" size="4"/>
+  <textbox id="n1" type="number"/>
   <textbox id="n2" type="number" value="10" min="5" max="15"/>
 </hbox>
 <hbox>
-  <textbox id="n4" type="number" size="4" value="-2" min="-8" max="18"/>
+  <textbox id="n4" type="number" value="-2" min="-8" max="18"/>
   <textbox id="n5" type="number" value="-17" min="-10" max="-3"/>
 </hbox>
 <hbox>
-  <textbox id="n6" type="number" size="4" value="9" min="12" max="8"/>
+  <textbox id="n6" type="number" value="9" min="12" max="8"/>
   <textbox id="n8" type="number" hidespinbuttons="true"/>
 </hbox>
 <hbox>
-  <textbox id="n9" type="number" size="4" oninput="updateInputEventCount();"/>
+  <textbox id="n9" type="number" oninput="updateInputEventCount();"/>
 </hbox>
 
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
 
   <!-- test code goes here -->
   <script type="application/javascript"><![CDATA[
 SimpleTest.waitForExplicitFinish();
--- a/toolkit/themes/linux/global/jar.mn
+++ b/toolkit/themes/linux/global/jar.mn
@@ -14,17 +14,17 @@ toolkit.jar:
    skin/classic/global/findBar.css
 *  skin/classic/global/global.css
    skin/classic/global/groupbox.css
    skin/classic/global/listbox.css
    skin/classic/global/menu.css
    skin/classic/global/menulist.css
    skin/classic/global/netError.css
 *  skin/classic/global/notification.css
-   skin/classic/global/numberbox.css
+*  skin/classic/global/numberbox.css
    skin/classic/global/popup.css
    skin/classic/global/printPreview.css
    skin/classic/global/radio.css
    skin/classic/global/scrollbox.css
    skin/classic/global/splitter.css
    skin/classic/global/tabbox.css
    skin/classic/global/textbox.css
    skin/classic/global/toolbar.css
--- a/toolkit/themes/linux/global/numberbox.css
+++ b/toolkit/themes/linux/global/numberbox.css
@@ -1,18 +1,5 @@
 /* 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/. */
 
-/* ===== numberbox.css ==================================================
-  == Styles used by the XUL textbox type="number" element.
-  ======================================================================= */
-
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
-@namespace html url("http://www.w3.org/1999/xhtml");
-
-html|*.numberbox-input {
-  text-align: right;
-}
-
-textbox[type="number"][hidespinbuttons="true"] html|*.numberbox-input {
-  -moz-appearance: textfield !important;
-}
+%include ../../shared/numberbox.inc.css
--- a/toolkit/themes/osx/global/jar.mn
+++ b/toolkit/themes/osx/global/jar.mn
@@ -16,17 +16,17 @@ toolkit.jar:
 * skin/classic/global/findBar.css
 * skin/classic/global/global.css
   skin/classic/global/groupbox.css
   skin/classic/global/listbox.css
   skin/classic/global/menu.css
   skin/classic/global/menulist.css
 * skin/classic/global/notification.css
   skin/classic/global/netError.css
-  skin/classic/global/numberbox.css
+* skin/classic/global/numberbox.css
   skin/classic/global/popup.css
   skin/classic/global/progressmeter.css
   skin/classic/global/radio.css
   skin/classic/global/resizer.css
   skin/classic/global/richlistbox.css
   skin/classic/global/scrollbars.css                                 (nativescrollbars.css)
   skin/classic/global/scrollbox.css
   skin/classic/global/splitter.css
--- a/toolkit/themes/osx/global/numberbox.css
+++ b/toolkit/themes/osx/global/numberbox.css
@@ -1,15 +1,9 @@
 /* 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/. */
 
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
-@namespace html url("http://www.w3.org/1999/xhtml");
+%include ../../shared/numberbox.inc.css
 
 html|*.numberbox-input {
-  text-align: right;
   padding: 0 1px !important;
 }
-
-textbox[type="number"][hidespinbuttons="true"] html|*.numberbox-input {
-  -moz-appearance: textfield !important;
-}
copy from toolkit/themes/linux/global/numberbox.css
copy to toolkit/themes/shared/numberbox.inc.css
--- a/toolkit/themes/linux/global/numberbox.css
+++ b/toolkit/themes/shared/numberbox.inc.css
@@ -11,8 +11,32 @@
 
 html|*.numberbox-input {
   text-align: right;
 }
 
 textbox[type="number"][hidespinbuttons="true"] html|*.numberbox-input {
   -moz-appearance: textfield !important;
 }
+
+/* input[type=number] uses display: flex; by default which is incompatible with XUL flexbox
+   Forcing XUL flexbox allows changing the size of the input. */
+html|*.numberbox-input,
+html|*.numberbox-input::-moz-number-wrapper,
+html|*.numberbox-input::-moz-number-spin-box {
+  display: -moz-box;
+  -moz-box-align: center;
+}
+
+html|*.numberbox-input::-moz-number-spin-box {
+  -moz-box-orient: vertical;
+}
+
+html|*.numberbox-input::-moz-number-wrapper,
+html|*.numberbox-input::-moz-number-spin-up,
+html|*.numberbox-input::-moz-number-spin-down,
+html|*.numberbox-input::-moz-number-text {
+  -moz-box-flex: 1;
+}
+
+html|*.numberbox-input::-moz-number-wrapper {
+  width: 100%;
+}
--- a/toolkit/themes/windows/global/jar.mn
+++ b/toolkit/themes/windows/global/jar.mn
@@ -17,17 +17,17 @@ toolkit.jar:
   skin/classic/global/tabbox.css
   skin/classic/global/textbox.css
   skin/classic/global/colorpicker.css
   skin/classic/global/commonDialog.css
   skin/classic/global/findBar.css
 * skin/classic/global/global.css
   skin/classic/global/listbox.css
   skin/classic/global/netError.css
-  skin/classic/global/numberbox.css
+* skin/classic/global/numberbox.css
 * skin/classic/global/notification.css
   skin/classic/global/printPageSetup.css
   skin/classic/global/printPreview.css
   skin/classic/global/scrollbox.css
   skin/classic/global/splitter.css
   skin/classic/global/toolbar.css
   skin/classic/global/toolbarbutton.css
 * skin/classic/global/tree.css
--- a/toolkit/themes/windows/global/numberbox.css
+++ b/toolkit/themes/windows/global/numberbox.css
@@ -1,18 +1,5 @@
 /* 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/. */
 
-/* ===== numberbox.css ==================================================
-  == Styles used by the XUL textbox type="number" element.
-  ======================================================================= */
-
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
-@namespace html url("http://www.w3.org/1999/xhtml");
-
-html|*.numberbox-input {
-  text-align: right;
-}
-
-textbox[type="number"][hidespinbuttons="true"] html|*.numberbox-input {
-  -moz-appearance: textfield !important;
-}
+%include ../../shared/numberbox.inc.css
--- a/toolkit/themes/windows/global/printPreview.css
+++ b/toolkit/themes/windows/global/printPreview.css
@@ -14,8 +14,12 @@
   list-style-image: url("chrome://global/skin/icons/Print-preview.png");
   -moz-image-region: rect(0px 16px 16px 0px);
 }
 
 .toolbar-landscape-page {
   list-style-image: url("chrome://global/skin/icons/Print-preview.png");
   -moz-image-region: rect(0px 32px 16px 16px);
 }
+
+#pageNumber {
+  width: 3ch;
+}
--- a/widget/gtk/WindowSurfaceX11.cpp
+++ b/widget/gtk/WindowSurfaceX11.cpp
@@ -31,26 +31,20 @@ WindowSurfaceX11::GetVisualFormat(const 
   case 32:
     if (aVisual->red_mask == 0xff0000 &&
         aVisual->green_mask == 0xff00 &&
         aVisual->blue_mask == 0xff) {
       return gfx::SurfaceFormat::B8G8R8A8;
     }
     break;
   case 24:
-    // Only support the BGRX layout, and report it as BGRA to the compositor.
-    // The alpha channel will be discarded when we put the image.
-    // Cairo/pixman lacks some fast paths for compositing BGRX onto BGRA, so
-    // just report it as BGRX directly in that case.
     if (aVisual->red_mask == 0xff0000 &&
         aVisual->green_mask == 0xff00 &&
         aVisual->blue_mask == 0xff) {
-      gfx::BackendType backend = gfxPlatform::GetPlatform()->GetDefaultContentBackend();
-      return backend == gfx::BackendType::CAIRO ? gfx::SurfaceFormat::B8G8R8X8
-                                                : gfx::SurfaceFormat::B8G8R8A8;
+      return gfx::SurfaceFormat::B8G8R8X8;
     }
     break;
   case 16:
     if (aVisual->red_mask == 0xf800 &&
         aVisual->green_mask == 0x07e0 &&
         aVisual->blue_mask == 0x1f) {
       return gfx::SurfaceFormat::R5G6B5_UINT16;
     }
--- a/widget/gtk/WindowSurfaceX11Image.cpp
+++ b/widget/gtk/WindowSurfaceX11Image.cpp
@@ -53,16 +53,20 @@ WindowSurfaceX11Image::Lock(const Layout
     mImageSurface = new gfxImageSurface(size, format);
     if (mImageSurface->CairoStatus()) {
       return nullptr;
     }
   }
 
   gfxImageFormat format = mImageSurface->Format();
   // Cairo prefers compositing to BGRX instead of BGRA where possible.
+  // Cairo/pixman lacks some fast paths for compositing BGRX onto BGRA, so
+  // just report it as BGRX directly in that case.
+  // Otherwise, for Skia, report it as BGRA to the compositor. The alpha
+  // channel will be discarded when we put the image.
   if (format == gfx::SurfaceFormat::X8R8G8B8_UINT32) {
     gfx::BackendType backend = gfxVars::ContentBackend();
     if (!gfx::Factory::DoesBackendSupportDataDrawtarget(backend)) {
 #ifdef USE_SKIA
       backend = gfx::BackendType::SKIA;
 #else
       backend = gfx::BackendType::CAIRO;
 #endif
--- a/xpcom/io/SlicedInputStream.cpp
+++ b/xpcom/io/SlicedInputStream.cpp
@@ -38,32 +38,34 @@ SlicedInputStream::SlicedInputStream(alr
   , mWeakSeekableInputStream(nullptr)
   , mWeakAsyncInputStream(nullptr)
   , mStart(aStart)
   , mLength(aLength)
   , mCurPos(0)
   , mClosed(false)
   , mAsyncWaitFlags(0)
   , mAsyncWaitRequestedCount(0)
+  , mMutex("SlicedInputStream::mMutex")
 {
   nsCOMPtr<nsIInputStream> inputStream = mozilla::Move(aInputStream);
   SetSourceStream(inputStream.forget());
 }
 
 SlicedInputStream::SlicedInputStream()
   : mWeakCloneableInputStream(nullptr)
   , mWeakIPCSerializableInputStream(nullptr)
   , mWeakSeekableInputStream(nullptr)
   , mWeakAsyncInputStream(nullptr)
   , mStart(0)
   , mLength(0)
   , mCurPos(0)
   , mClosed(false)
   , mAsyncWaitFlags(0)
   , mAsyncWaitRequestedCount(0)
+  , mMutex("SlicedInputStream::mMutex")
 {}
 
 SlicedInputStream::~SlicedInputStream()
 {}
 
 void
 SlicedInputStream::SetSourceStream(already_AddRefed<nsIInputStream> aInputStream)
 {
@@ -265,26 +267,26 @@ NS_IMETHODIMP
 SlicedInputStream::AsyncWait(nsIInputStreamCallback* aCallback,
                              uint32_t aFlags,
                              uint32_t aRequestedCount,
                              nsIEventTarget* aEventTarget)
 {
   NS_ENSURE_STATE(mInputStream);
   NS_ENSURE_STATE(mWeakAsyncInputStream);
 
+  nsCOMPtr<nsIInputStreamCallback> callback = aCallback ? this : nullptr;
+
+  MutexAutoLock lock(mMutex);
+
   if (mAsyncWaitCallback && aCallback) {
     return NS_ERROR_FAILURE;
   }
 
   mAsyncWaitCallback = aCallback;
 
-  if (!mAsyncWaitCallback) {
-    return NS_OK;
-  }
-
   // If we haven't started retrieving data, let's see if we can seek.
   // If we cannot seek, we will do consecutive reads.
   if (mCurPos < mStart && mWeakSeekableInputStream) {
     nsresult rv =
       mWeakSeekableInputStream->Seek(nsISeekableStream::NS_SEEK_SET, mStart);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
@@ -293,78 +295,92 @@ SlicedInputStream::AsyncWait(nsIInputStr
   }
 
   mAsyncWaitFlags = aFlags;
   mAsyncWaitRequestedCount = aRequestedCount;
   mAsyncWaitEventTarget = aEventTarget;
 
   // If we are not at the right position, let's do an asyncWait just internal.
   if (mCurPos < mStart) {
-    return mWeakAsyncInputStream->AsyncWait(this, 0, mStart - mCurPos,
-                                            aEventTarget);
+    uint64_t diff = mStart - mCurPos;
+
+    MutexAutoUnlock unlock(mMutex);
+    return mWeakAsyncInputStream->AsyncWait(callback, 0, diff, aEventTarget);
   }
 
-  return mWeakAsyncInputStream->AsyncWait(this, aFlags, aRequestedCount,
+  MutexAutoUnlock unlock(mMutex);
+  return mWeakAsyncInputStream->AsyncWait(callback, aFlags, aRequestedCount,
                                           aEventTarget);
 }
 
 // nsIInputStreamCallback
 
 NS_IMETHODIMP
 SlicedInputStream::OnInputStreamReady(nsIAsyncInputStream* aStream)
 {
   MOZ_ASSERT(mInputStream);
   MOZ_ASSERT(mWeakAsyncInputStream);
   MOZ_ASSERT(mWeakAsyncInputStream == aStream);
 
+  MutexAutoLock lock(mMutex);
+
   // We have been canceled in the meanwhile.
   if (!mAsyncWaitCallback) {
     return NS_OK;
   }
 
   if (mCurPos < mStart) {
     char buf[4096];
     while (mCurPos < mStart) {
       uint32_t bytesRead;
       uint64_t bufCount = XPCOM_MIN(mStart - mCurPos, (uint64_t)sizeof(buf));
       nsresult rv = mInputStream->Read(buf, bufCount, &bytesRead);
       if (NS_SUCCEEDED(rv) && bytesRead == 0) {
         mClosed = true;
-        return RunAsyncWaitCallback();
+        return RunAsyncWaitCallback(lock);
       }
 
       if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
-        return mWeakAsyncInputStream->AsyncWait(this, 0, mStart - mCurPos,
+        uint64_t diff = mStart - mCurPos;
+        MutexAutoUnlock unlock(mMutex);
+        return mWeakAsyncInputStream->AsyncWait(this, 0, diff,
                                                 mAsyncWaitEventTarget);
       }
 
       if (NS_WARN_IF(NS_FAILED(rv))) {
-        return RunAsyncWaitCallback();
+        return RunAsyncWaitCallback(lock);
       }
 
       mCurPos += bytesRead;
     }
 
+    uint32_t asyncWaitFlags = mAsyncWaitFlags;
+    uint32_t asyncWaitRequestedCount = mAsyncWaitRequestedCount;
+    nsCOMPtr<nsIEventTarget> asyncWaitEventTarget = mAsyncWaitEventTarget;
+
+    MutexAutoUnlock unlock(mMutex);
+
     // Now we are ready to do the 'real' asyncWait.
-    return mWeakAsyncInputStream->AsyncWait(this, mAsyncWaitFlags,
-                                            mAsyncWaitRequestedCount,
-                                            mAsyncWaitEventTarget);
+    return mWeakAsyncInputStream->AsyncWait(this, asyncWaitFlags,
+                                            asyncWaitRequestedCount,
+                                            asyncWaitEventTarget);
   }
 
-  return RunAsyncWaitCallback();
+  return RunAsyncWaitCallback(lock);
 }
 
 nsresult
-SlicedInputStream::RunAsyncWaitCallback()
+SlicedInputStream::RunAsyncWaitCallback(const MutexAutoLock& aProofOfLock)
 {
   nsCOMPtr<nsIInputStreamCallback> callback = mAsyncWaitCallback;
 
   mAsyncWaitCallback = nullptr;
   mAsyncWaitEventTarget = nullptr;
 
+  MutexAutoUnlock unlock(mMutex);
   return callback->OnInputStreamReady(this);
 }
 
 // nsIIPCSerializableInputStream
 
 void
 SlicedInputStream::Serialize(mozilla::ipc::InputStreamParams& aParams,
                              FileDescriptorArray& aFileDescriptors)
--- a/xpcom/io/SlicedInputStream.h
+++ b/xpcom/io/SlicedInputStream.h
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef SlicedInputStream_h
 #define SlicedInputStream_h
 
 #include "mozilla/Attributes.h"
+#include "mozilla/Mutex.h"
 #include "nsCOMPtr.h"
 #include "nsIAsyncInputStream.h"
 #include "nsICloneableInputStream.h"
 #include "nsIIPCSerializableInputStream.h"
 #include "nsISeekableStream.h"
 
 namespace mozilla {
 
@@ -52,34 +53,37 @@ public:
 
 private:
   ~SlicedInputStream();
 
   void
   SetSourceStream(already_AddRefed<nsIInputStream> aInputStream);
 
   nsresult
-  RunAsyncWaitCallback();
+  RunAsyncWaitCallback(const MutexAutoLock& aProofOfLock);
 
   nsCOMPtr<nsIInputStream> mInputStream;
 
   // Raw pointers because these are just QI of mInputStream.
   nsICloneableInputStream* mWeakCloneableInputStream;
   nsIIPCSerializableInputStream* mWeakIPCSerializableInputStream;
   nsISeekableStream* mWeakSeekableInputStream;
   nsIAsyncInputStream* mWeakAsyncInputStream;
 
   uint64_t mStart;
   uint64_t mLength;
   uint64_t mCurPos;
 
   bool mClosed;
 
-  // These four are used for AsyncWait.
+  // These four are used for AsyncWait. They are protected by mutex because
+  // touched on multiple threads.
   nsCOMPtr<nsIInputStreamCallback> mAsyncWaitCallback;
   nsCOMPtr<nsIEventTarget> mAsyncWaitEventTarget;
   uint32_t mAsyncWaitFlags;
   uint32_t mAsyncWaitRequestedCount;
+
+  Mutex mMutex;
 };
 
 } // mozilla namespace
 
 #endif // SlicedInputStream_h