Merge inbound to mozilla-central. a=merge
authorshindli <shindli@mozilla.com>
Wed, 17 Oct 2018 03:55:26 +0300
changeset 500027 778427bb6353f85c1656f4ebcd2041560cd70c16
parent 499982 d4fe026dee75521ac39478591cb84d782eb0b189 (current diff)
parent 500026 ffba3ac1d2bb4064bc7eeb398ed119cb0a28210e (diff)
child 500028 12a1714386d4428690a48ac0d0856fee1aa9f31e
child 500052 89189817c169f830f2b880df11ac88569582a67e
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
dom/webidl/PaymentRequest.webidl
--- a/devtools/client/inspector/flexbox/components/Header.js
+++ b/devtools/client/inspector/flexbox/components/Header.js
@@ -47,16 +47,17 @@ class Header extends PureComponent {
     // selected element.
     if (this.props.flexContainer.isFlexItemContainer) {
       return null;
     }
 
     return createElement(Fragment, null,
       dom.div({ className: "devtools-separator" }),
       dom.input({
+        id: "flexbox-checkbox-toggle",
         className: "devtools-checkbox-toggle",
         checked: this.props.highlighted,
         onChange: this.onFlexboxCheckboxClick,
         type: "checkbox",
       })
     );
   }
 
--- a/devtools/client/inspector/flexbox/flexbox.js
+++ b/devtools/client/inspector/flexbox/flexbox.js
@@ -333,17 +333,17 @@ class FlexboxInspector {
    *         A hex string representing the color to use.
    */
   async onSetFlexboxOverlayColor(color) {
     this.store.dispatch(updateFlexboxColor(color));
 
     const { flexbox } = this.store.getState();
 
     if (flexbox.highlighted) {
-      this.highlighters.showFlexboxHighlighter(flexbox.nodeFront);
+      this.highlighters.showFlexboxHighlighter(flexbox.flexContainer.nodeFront);
     }
 
     const currentUrl = this.inspector.target.url;
     // Get the hostname, if there is no hostname, fall back on protocol
     // ex: `data:` uri, and `about:` pages
     const hostName = parseURL(currentUrl).hostName || parseURL(currentUrl).protocol;
     const customColors = await this.getCustomHostColors();
     customColors[hostName] = color;
--- a/devtools/client/inspector/flexbox/test/browser.ini
+++ b/devtools/client/inspector/flexbox/test/browser.ini
@@ -3,13 +3,15 @@ tags = devtools
 subsuite = devtools
 support-files =
   doc_flexbox_simple.html
   head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/shared-head.js
 
+[browser_flexbox_highlighter_color_picker_on_ESC.js]
+[browser_flexbox_highlighter_color_picker_on_RETURN.js]
 [browser_flexbox_item_outline_exists.js]
 [browser_flexbox_item_outline_has_correct_layout.js]
 [browser_flexbox_item_outline_rotates_for_column.js]
 [browser_flexbox_sizing_info_exists.js]
 [browser_flexbox_sizing_info_has_correct_sections.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_ESC.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the flexbox highlighter color change in the color picker is reverted when
+// ESCAPE is pressed.
+
+const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
+
+add_task(async function() {
+  await addTab(TEST_URI);
+  const { inspector, flexboxInspector, layoutView } = await openLayoutView();
+  const { document: doc } = flexboxInspector;
+  const { store } = inspector;
+  const cPicker = layoutView.swatchColorPickerTooltip;
+  const spectrum = cPicker.spectrum;
+
+  const onColorSwatchRendered = waitForDOM(doc,
+    "#layout-flexbox-container .layout-color-swatch");
+  await selectNode("#container", inspector);
+  const [swatch] = await onColorSwatchRendered;
+
+  info("Checking the initial state of the Flexbox Inspector color picker.");
+  is(swatch.style.backgroundColor, "rgb(148, 0, 255)",
+    "The color swatch's background is correct.");
+  is(store.getState().flexbox.color, "#9400FF", "The flexbox color state is correct.");
+
+  info("Opening the color picker by clicking on the color swatch.");
+  const onColorPickerReady = cPicker.once("ready");
+  swatch.click();
+  await onColorPickerReady;
+
+  await simulateColorPickerChange(cPicker, [0, 255, 0, .5]);
+
+  is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+    "The color swatch's background was updated.");
+
+  info("Pressing ESCAPE to close the tooltip.");
+  const onColorUpdate = waitUntilState(store, state => state.flexbox.color === "#9400FF");
+  const onColorPickerHidden = cPicker.tooltip.once("hidden");
+  focusAndSendKey(spectrum.element.ownerDocument.defaultView, "ESCAPE");
+  await onColorPickerHidden;
+  await onColorUpdate;
+
+  is(swatch.style.backgroundColor, "rgb(148, 0, 255)",
+    "The color swatch's background was reverted after ESCAPE.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_RETURN.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the flexbox highlighter color change in the color picker is committed when
+// RETURN is pressed.
+
+const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
+
+add_task(async function() {
+  await addTab(TEST_URI);
+  const { inspector, flexboxInspector, layoutView } = await openLayoutView();
+  const { document: doc } = flexboxInspector;
+  const { highlighters, store } = inspector;
+  const cPicker = layoutView.swatchColorPickerTooltip;
+  const spectrum = cPicker.spectrum;
+
+  const onColorSwatchRendered = waitForDOM(doc,
+    "#layout-flexbox-container .layout-color-swatch");
+  await selectNode("#container", inspector);
+  const [swatch] = await onColorSwatchRendered;
+
+  const checkbox = doc.getElementById("flexbox-checkbox-toggle");
+
+  info("Checking the initial state of the Flexbox Inspector color picker.");
+  ok(!checkbox.checked, "Flexbox highlighter toggle is unchecked.");
+  is(swatch.style.backgroundColor, "rgb(148, 0, 255)",
+    "The color swatch's background is correct.");
+  is(store.getState().flexbox.color, "#9400FF", "The flexbox color state is correct.");
+
+  info("Toggling ON the flexbox highlighter.");
+  const onHighlighterShown = highlighters.once("flexbox-highlighter-shown");
+  const onCheckboxChange = waitUntilState(store, state => state.flexbox.highlighted);
+  checkbox.click();
+  await onHighlighterShown;
+  await onCheckboxChange;
+
+  info("Opening the color picker by clicking on the color swatch.");
+  const onColorPickerReady = cPicker.once("ready");
+  swatch.click();
+  await onColorPickerReady;
+
+  await simulateColorPickerChange(cPicker, [0, 255, 0, .5]);
+
+  is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+    "The color swatch's background was updated.");
+
+  info("Pressing RETURN to commit the color change.");
+  const onColorUpdate = waitUntilState(store, state =>
+    state.flexbox.color === "#00FF0080");
+  const onColorPickerHidden = cPicker.tooltip.once("hidden");
+  focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+  await onColorPickerHidden;
+  await onColorUpdate;
+
+  is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+    "The color swatch's background was kept after RETURN.");
+
+  info("Toggling OFF the flexbox highlighter.");
+  const onHighlighterHidden = highlighters.once("flexbox-highlighter-hidden");
+  checkbox.click();
+  await onHighlighterHidden;
+});
--- a/devtools/client/inspector/flexbox/test/doc_flexbox_simple.html
+++ b/devtools/client/inspector/flexbox/test/doc_flexbox_simple.html
@@ -25,17 +25,17 @@
 .growing .item {
   flex-basis: 200px;
   flex-grow: 1;
 }
 .growing.is-clamped .item {
   max-width: 250px;
 }
 </style>
-<div class="container">
+<div id="container" class="container">
   <div class="item">flex item in a row flex container</div>
 </div>
 <div class="container column">
   <div class="item">flex item in a column flex container</div>
 </div>
 <div class="container shrinking">
   <div class="item">Shrinking flex item</div>
 </div>
--- a/devtools/client/inspector/flexbox/test/head.js
+++ b/devtools/client/inspector/flexbox/test/head.js
@@ -5,16 +5,21 @@
 /* import-globals-from ../../test/head.js */
 "use strict";
 
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
+// Load the shared Redux helpers into this compartment.
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-redux-head.js",
+  this);
+
 // Make sure the flexbox inspector is enabled before running the tests.
 Services.prefs.setBoolPref("devtools.flexboxinspector.enabled", true);
 
 // Make sure only the flexbox layout accordion is opened, and the others are closed.
 Services.prefs.setBoolPref("devtools.layout.flexbox.opened", true);
 Services.prefs.setBoolPref("devtools.layout.boxmodel.opened", false);
 Services.prefs.setBoolPref("devtools.layout.grid.opened", false);
 
--- a/devtools/client/inspector/grids/test/head.js
+++ b/devtools/client/inspector/grids/test/head.js
@@ -12,36 +12,15 @@ Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
 // Load the shared Redux helpers into this compartment.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-redux-head.js",
   this);
 
-Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
-registerCleanupFunction(() => {
-  Services.prefs.clearUserPref("devtools.toolbox.footer.height");
-});
-
 const asyncStorage = require("devtools/shared/async-storage");
 
-/**
- * Simulate a color change in a given color picker tooltip.
- *
- * @param  {Spectrum} colorPicker
- *         The color picker widget.
- * @param  {Array} newRgba
- *         Array of the new rgba values to be set in the color widget.
- */
-var simulateColorPickerChange = async function(colorPicker, newRgba) {
-  info("Getting the spectrum colorpicker object");
-  const spectrum = await colorPicker.spectrum;
-  info("Setting the new color");
-  spectrum.rgb = newRgba;
-  info("Applying the change");
-  spectrum.updateUI();
-  spectrum.onChange();
-};
-
+Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
 registerCleanupFunction(async function() {
+  Services.prefs.clearUserPref("devtools.toolbox.footer.height");
   await asyncStorage.removeItem("gridInspectorHostColors");
 });
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -1,25 +1,25 @@
 /* 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 { createFactory, createElement } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const FlexboxInspector = require("devtools/client/inspector/flexbox/flexbox");
+const GridInspector = require("devtools/client/inspector/grids/grid-inspector");
 
 const LayoutApp = createFactory(require("./components/LayoutApp"));
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
 
-loader.lazyRequireGetter(this, "FlexboxInspector", "devtools/client/inspector/flexbox/flexbox");
-loader.lazyRequireGetter(this, "GridInspector", "devtools/client/inspector/grids/grid-inspector");
 loader.lazyRequireGetter(this, "SwatchColorPickerTooltip", "devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip");
 
 class LayoutView {
   constructor(inspector, window) {
     this.document = window.document;
     this.inspector = inspector;
     this.store = inspector.store;
 
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -881,8 +881,26 @@ async function expandContainer(inspector
 async function expandContainerByClick(inspector, container) {
   const onChildren = waitForChildrenUpdated(inspector);
   const onUpdated = inspector.once("inspector-updated");
   EventUtils.synthesizeMouseAtCenter(container.expander, {},
     inspector.markup.doc.defaultView);
   await onChildren;
   await onUpdated;
 }
+
+/**
+ * Simulate a color change in a given color picker tooltip.
+ *
+ * @param  {Spectrum} colorPicker
+ *         The color picker widget.
+ * @param  {Array} newRgba
+ *         Array of the new rgba values to be set in the color widget.
+ */
+async function simulateColorPickerChange(colorPicker, newRgba) {
+  info("Getting the spectrum colorpicker object");
+  const spectrum = await colorPicker.spectrum;
+  info("Setting the new color");
+  spectrum.rgb = newRgba;
+  info("Applying the change");
+  spectrum.updateUI();
+  spectrum.onChange();
+}
--- a/dom/base/StructuredCloneTester.cpp
+++ b/dom/base/StructuredCloneTester.cpp
@@ -71,33 +71,34 @@ StructuredCloneTester::ReadStructuredClo
   }
 
   nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
 
   if (NS_WARN_IF(!global)) {
     return nullptr;
   }
 
-  // Prevent the return value from being trashed by a GC during ~nsRefPtr
-  JS::RootedObject result(aCx);
+  // Prevent the return value from being trashed by a GC during ~RefPtr
+  JS::Rooted<JSObject*> result(aCx);
+  {
+    RefPtr<StructuredCloneTester> sct = new StructuredCloneTester(
+      global,
+      static_cast<bool>(serializable),
+      static_cast<bool>(deserializable)
+    );
 
-  RefPtr<StructuredCloneTester> sct = new StructuredCloneTester(
-    global,
-    static_cast<bool>(serializable),
-    static_cast<bool>(deserializable)
-  );
+    // "Fail" deserialization
+    if (!sct->Deserializable()) {
+      xpc::Throw(aCx, NS_ERROR_DOM_DATA_CLONE_ERR);
+      return nullptr;
+    }
 
-  // "Fail" deserialization
-  if (!sct->Deserializable()) {
-    xpc::Throw(aCx, NS_ERROR_DOM_DATA_CLONE_ERR);
-    return nullptr;
+    result = sct->WrapObject(aCx, nullptr);
   }
 
-  result = sct->WrapObject(aCx, nullptr);
-
   return result;
 }
 
 bool
 StructuredCloneTester::WriteStructuredClone(JSStructuredCloneWriter* aWriter) const
 {
   return JS_WriteUint32Pair(aWriter, SCTAG_DOM_STRUCTURED_CLONE_TESTER, 0) &&
          JS_WriteUint32Pair(aWriter, static_cast<uint32_t>(Serializable()),
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -3800,32 +3800,20 @@ ContentChild::RecvShareCodeCoverageMutex
   CodeCoverageHandler::Init(aHandle);
   return IPC_OK();
 #else
   MOZ_CRASH("Shouldn't receive this message in non-code coverage builds!");
 #endif
 }
 
 mozilla::ipc::IPCResult
-ContentChild::RecvDumpCodeCoverageCounters(DumpCodeCoverageCountersResolver&& aResolver)
+ContentChild::RecvFlushCodeCoverageCounters(FlushCodeCoverageCountersResolver&& aResolver)
 {
 #ifdef MOZ_CODE_COVERAGE
-  CodeCoverageHandler::DumpCounters();
-  aResolver(/* unused */ true);
-  return IPC_OK();
-#else
-  MOZ_CRASH("Shouldn't receive this message in non-code coverage builds!");
-#endif
-}
-
-mozilla::ipc::IPCResult
-ContentChild::RecvResetCodeCoverageCounters(ResetCodeCoverageCountersResolver&& aResolver)
-{
-#ifdef MOZ_CODE_COVERAGE
-  CodeCoverageHandler::ResetCounters();
+  CodeCoverageHandler::FlushCounters();
   aResolver(/* unused */ true);
   return IPC_OK();
 #else
   MOZ_CRASH("Shouldn't receive this message in non-code coverage builds!");
 #endif
 }
 
 mozilla::ipc::IPCResult
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -634,20 +634,17 @@ public:
   mozilla::ipc::IPCResult
   RecvSetPermissionsWithKey(const nsCString& aPermissionKey,
                             nsTArray<IPC::Permission>&& aPerms) override;
 
   virtual mozilla::ipc::IPCResult
   RecvShareCodeCoverageMutex(const CrossProcessMutexHandle& aHandle) override;
 
   virtual mozilla::ipc::IPCResult
-  RecvDumpCodeCoverageCounters(DumpCodeCoverageCountersResolver&& aResolver) override;
-
-  virtual mozilla::ipc::IPCResult
-  RecvResetCodeCoverageCounters(ResetCodeCoverageCountersResolver&& aResolver) override;
+  RecvFlushCodeCoverageCounters(FlushCodeCoverageCountersResolver&& aResolver) override;
 
   virtual mozilla::ipc::IPCResult
   RecvSetInputEventQueueEnabled() override;
 
   virtual mozilla::ipc::IPCResult
   RecvFlushInputEventQueue() override;
 
   virtual mozilla::ipc::IPCResult
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -643,18 +643,17 @@ child:
      * plugins changes. The chrome process sends up the last epoch it observed.
      * If the epoch last seen by the content process is the same, the content
      * process ignores the update. Otherwise the content process updates its
      * list and reloads its plugins.
      **/
     async SetPluginList(uint32_t pluginEpoch, PluginTag[] plugins, FakePluginTag[] fakePlugins);
 
     async ShareCodeCoverageMutex(CrossProcessMutexHandle handle);
-    async DumpCodeCoverageCounters() returns (bool unused);
-    async ResetCodeCoverageCounters() returns (bool unused);
+    async FlushCodeCoverageCounters() returns (bool unused);
 
     /*
      * IPC message to enable the input event queue on the main thread of the
      * content process.
      */
     async SetInputEventQueueEnabled();
 
     /*
--- a/dom/media/mediasource/test/mochitest.ini
+++ b/dom/media/mediasource/test/mochitest.ini
@@ -155,10 +155,10 @@ skip-if = android_version == '22' # bug 
 skip-if = toolkit == 'android' # Not supported on android
 [test_WaitingOnMissingData.html]
 skip-if = (toolkit == 'android') #timeout android only bug 1101187
 [test_WaitingOnMissingData_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_WaitingOnMissingDataEnded_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_WaitingToEndedTransition_mp4.html]
-skip-if = toolkit == 'android' # Not supported on android
+skip-if = (toolkit == 'android') || (os == 'linux') # Not supported on android, Bug 1495167
 [test_WebMTagsBeforeCluster.html]
--- a/dom/payments/PaymentRequest.cpp
+++ b/dom/payments/PaymentRequest.cpp
@@ -398,25 +398,16 @@ PaymentRequest::IsValidCurrency(const ns
 
 nsresult
 PaymentRequest::IsValidCurrencyAmount(const nsAString& aItem,
                                       const PaymentCurrencyAmount& aAmount,
                                       const bool aIsTotalItem,
                                       nsAString& aErrorMsg)
 {
   nsresult rv;
-  // currencySystem must equal urn:iso:std:iso:4217
-  if (!aAmount.mCurrencySystem.EqualsASCII("urn:iso:std:iso:4217")) {
-    aErrorMsg.AssignLiteral("The amount.currencySystem of \"");
-    aErrorMsg.Append(aItem);
-    aErrorMsg.AppendLiteral("\"(");
-    aErrorMsg.Append(aAmount.mCurrencySystem);
-    aErrorMsg.AppendLiteral(") must equal urn:iso:std:iso:4217.");
-    return NS_ERROR_RANGE_ERR;
-  }
   rv = IsValidCurrency(aItem, aAmount.mCurrency, aErrorMsg);
   if (NS_FAILED(rv)) {
     return rv;
   }
   if (aIsTotalItem) {
     rv = IsNonNegativeNumber(aItem, aAmount.mValue, aErrorMsg);
     if (NS_FAILED(rv)) {
       return rv;
--- a/dom/webidl/PaymentRequest.webidl
+++ b/dom/webidl/PaymentRequest.webidl
@@ -13,17 +13,16 @@
 dictionary PaymentMethodData {
   required DOMString           supportedMethods;
            object              data;
 };
 
 dictionary PaymentCurrencyAmount {
   required DOMString currency;
   required DOMString value;
-           DOMString currencySystem = "urn:iso:std:iso:4217";
 };
 
 dictionary PaymentItem {
   required DOMString             label;
   required PaymentCurrencyAmount amount;
            boolean               pending = false;
 };
 
--- a/gfx/skia/skia/src/core/SkGeometry.cpp
+++ b/gfx/skia/skia/src/core/SkGeometry.cpp
@@ -53,53 +53,61 @@ static int valid_unit_divide(SkScalar nu
     SkASSERTF(r >= 0 && r < SK_Scalar1, "numer %f, denom %f, r %f", numer, denom, r);
     if (r == 0) { // catch underflow if numer <<<< denom
         return 0;
     }
     *ratio = r;
     return 1;
 }
 
+// Just returns its argument, but makes it easy to set a break-point to know when
+// SkFindUnitQuadRoots is going to return 0 (an error).
+static int return_check_zero(int value) {
+    if (value == 0) {
+        return 0;
+    }
+    return value;
+}
+
 /** From Numerical Recipes in C.
 
     Q = -1/2 (B + sign(B) sqrt[B*B - 4*A*C])
     x1 = Q / A
     x2 = C / Q
 */
 int SkFindUnitQuadRoots(SkScalar A, SkScalar B, SkScalar C, SkScalar roots[2]) {
     SkASSERT(roots);
 
     if (A == 0) {
-        return valid_unit_divide(-C, B, roots);
+        return return_check_zero(valid_unit_divide(-C, B, roots));
     }
 
     SkScalar* r = roots;
 
-    SkScalar R = B*B - 4*A*C;
-    if (R < 0 || !SkScalarIsFinite(R)) {  // complex roots
-        // if R is infinite, it's possible that it may still produce
-        // useful results if the operation was repeated in doubles
-        // the flipside is determining if the more precise answer
-        // isn't useful because surrounding machinery (e.g., subtracting
-        // the axis offset from C) already discards the extra precision
-        // more investigation and unit tests required...
-        return 0;
+    // use doubles so we don't overflow temporarily trying to compute R
+    double dr = (double)B * B - 4 * (double)A * C;
+    if (dr < 0) {
+        return return_check_zero(0);
     }
-    R = SkScalarSqrt(R);
+    dr = sqrt(dr);
+    SkScalar R = SkDoubleToScalar(dr);
+    if (!SkScalarIsFinite(R)) {
+        return return_check_zero(0);
+    }
 
     SkScalar Q = (B < 0) ? -(B-R)/2 : -(B+R)/2;
     r += valid_unit_divide(Q, A, r);
     r += valid_unit_divide(C, Q, r);
     if (r - roots == 2) {
         if (roots[0] > roots[1])
             SkTSwap<SkScalar>(roots[0], roots[1]);
         else if (roots[0] == roots[1])  // nearly-equal?
             r -= 1; // skip the double root
     }
-    return (int)(r - roots);
+    return return_check_zero((int)(r - roots));
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 ///////////////////////////////////////////////////////////////////////////////
 
 void SkEvalQuadAt(const SkPoint src[3], SkScalar t, SkPoint* pt, SkVector* tangent) {
     SkASSERT(src);
     SkASSERT(t >= 0 && t <= SK_Scalar1);
--- a/gfx/skia/skia/src/core/SkPath.cpp
+++ b/gfx/skia/skia/src/core/SkPath.cpp
@@ -12,16 +12,17 @@
 #include "SkGeometry.h"
 #include "SkMath.h"
 #include "SkMatrixPriv.h"
 #include "SkPathPriv.h"
 #include "SkPathRef.h"
 #include "SkPointPriv.h"
 #include "SkRRect.h"
 #include "SkSafeMath.h"
+#include "SkTLazy.h"
 
 static float poly_eval(float A, float B, float C, float t) {
     return (A * t + B) * t + C;
 }
 
 static float poly_eval(float A, float B, float C, float D, float t) {
     return ((A * t + B) * t + C) * t + D;
 }
@@ -1550,20 +1551,27 @@ void SkPath::arcTo(SkScalar x1, SkScalar
 
 void SkPath::addPath(const SkPath& path, SkScalar dx, SkScalar dy, AddPathMode mode) {
     SkMatrix matrix;
 
     matrix.setTranslate(dx, dy);
     this->addPath(path, matrix, mode);
 }
 
-void SkPath::addPath(const SkPath& path, const SkMatrix& matrix, AddPathMode mode) {
-    SkPathRef::Editor(&fPathRef, path.countVerbs(), path.countPoints());
-
-    RawIter iter(path);
+void SkPath::addPath(const SkPath& srcPath, const SkMatrix& matrix, AddPathMode mode) {
+    // Detect if we're trying to add ourself
+    const SkPath* src = &srcPath;
+    SkTLazy<SkPath> tmp;
+    if (this == src) {
+        src = tmp.set(srcPath);
+    }
+
+    SkPathRef::Editor(&fPathRef, src->countVerbs(), src->countPoints());
+
+    RawIter iter(*src);
     SkPoint pts[4];
     Verb    verb;
 
     SkMatrixPriv::MapPtsProc proc = SkMatrixPriv::GetMapPtsProc(matrix);
     bool firstVerb = true;
     while ((verb = iter.next(pts)) != kDone_Verb) {
         switch (verb) {
             case kMove_Verb:
@@ -1653,24 +1661,31 @@ void SkPath::reversePathTo(const SkPath&
                 break;
             default:
                 SkDEBUGFAIL("bad verb");
                 break;
         }
     }
 }
 
-void SkPath::reverseAddPath(const SkPath& src) {
-    SkPathRef::Editor ed(&fPathRef, src.fPathRef->countPoints(), src.fPathRef->countVerbs());
-
-    const SkPoint* pts = src.fPathRef->pointsEnd();
+void SkPath::reverseAddPath(const SkPath& srcPath) {
+    // Detect if we're trying to add ourself
+    const SkPath* src = &srcPath;
+    SkTLazy<SkPath> tmp;
+    if (this == src) {
+        src = tmp.set(srcPath);
+    }
+
+    SkPathRef::Editor ed(&fPathRef, src->fPathRef->countPoints(), src->fPathRef->countVerbs());
+
+    const SkPoint* pts = src->fPathRef->pointsEnd();
     // we will iterator through src's verbs backwards
-    const uint8_t* verbs = src.fPathRef->verbsMemBegin(); // points at the last verb
-    const uint8_t* verbsEnd = src.fPathRef->verbs(); // points just past the first verb
-    const SkScalar* conicWeights = src.fPathRef->conicWeightsEnd();
+    const uint8_t* verbs = src->fPathRef->verbsMemBegin(); // points at the last verb
+    const uint8_t* verbsEnd = src->fPathRef->verbs(); // points just past the first verb
+    const SkScalar* conicWeights = src->fPathRef->conicWeightsEnd();
 
     bool needMove = true;
     bool needClose = false;
     while (verbs < verbsEnd) {
         uint8_t v = *(verbs++);
         int n = pts_in_verb(v);
 
         if (needMove) {
--- a/gfx/skia/skia/src/core/SkRRect.cpp
+++ b/gfx/skia/skia/src/core/SkRRect.cpp
@@ -162,16 +162,29 @@ bool SkRRect::initializeRect(const SkRec
     if (fRect.isEmpty()) {
         memset(fRadii, 0, sizeof(fRadii));
         fType = kEmpty_Type;
         return false;
     }
     return true;
 }
 
+// If we can't distinguish one of the radii relative to the other, force it to zero so it
+// doesn't confuse us later. See crbug.com/850350
+//
+static void flush_to_zero(SkScalar& a, SkScalar& b) {
+    SkASSERT(a >= 0);
+    SkASSERT(b >= 0);
+    if (a + b == a) {
+        b = 0;
+    } else if (a + b == b) {
+        a = 0;
+    }
+}
+
 void SkRRect::scaleRadii() {
 
     // Proportionally scale down all radii to fit. Find the minimum ratio
     // of a side and the radii on that side (for all four sides) and use
     // that to scale down _all_ the radii. This algorithm is from the
     // W3 spec (http://www.w3.org/TR/css3-background/) section 5.5 - Overlapping
     // Curves:
     // "Let f = min(Li/Si), where i is one of { top, right, bottom, left },
@@ -184,16 +197,21 @@ void SkRRect::scaleRadii() {
     // The sides of the rectangle may be larger than a float.
     double width = (double)fRect.fRight - (double)fRect.fLeft;
     double height = (double)fRect.fBottom - (double)fRect.fTop;
     scale = compute_min_scale(fRadii[0].fX, fRadii[1].fX, width,  scale);
     scale = compute_min_scale(fRadii[1].fY, fRadii[2].fY, height, scale);
     scale = compute_min_scale(fRadii[2].fX, fRadii[3].fX, width,  scale);
     scale = compute_min_scale(fRadii[3].fY, fRadii[0].fY, height, scale);
 
+    flush_to_zero(fRadii[0].fX, fRadii[1].fX);
+    flush_to_zero(fRadii[1].fY, fRadii[2].fY);
+    flush_to_zero(fRadii[2].fX, fRadii[3].fX);
+    flush_to_zero(fRadii[3].fY, fRadii[0].fY);
+
     if (scale < 1.0) {
         SkScaleToSides::AdjustRadii(width,  scale, &fRadii[0].fX, &fRadii[1].fX);
         SkScaleToSides::AdjustRadii(height, scale, &fRadii[1].fY, &fRadii[2].fY);
         SkScaleToSides::AdjustRadii(width,  scale, &fRadii[2].fX, &fRadii[3].fX);
         SkScaleToSides::AdjustRadii(height, scale, &fRadii[3].fY, &fRadii[0].fY);
     }
 
     // At this point we're either oval, simple, or complex (not empty or rect).
--- a/gfx/skia/skia/src/gpu/GrTessellator.cpp
+++ b/gfx/skia/skia/src/gpu/GrTessellator.cpp
@@ -2269,28 +2269,28 @@ int get_contour_count(const SkPath& path
     }
     if (maxPts > ((int)SK_MaxU16 + 1)) {
         SkDebugf("Path not rendered, too many verts (%d)\n", maxPts);
         return 0;
     }
     return contourCnt;
 }
 
-int count_points(Poly* polys, SkPath::FillType fillType) {
-    int count = 0;
+int64_t count_points(Poly* polys, SkPath::FillType fillType) {
+    int64_t count = 0;
     for (Poly* poly = polys; poly; poly = poly->fNext) {
         if (apply_fill_type(fillType, poly) && poly->fCount >= 3) {
             count += (poly->fCount - 2) * (TESSELLATOR_WIREFRAME ? 6 : 3);
         }
     }
     return count;
 }
 
-int count_outer_mesh_points(const VertexList& outerMesh) {
-    int count = 0;
+int64_t count_outer_mesh_points(const VertexList& outerMesh) {
+    int64_t count = 0;
     for (Vertex* v = outerMesh.fHead; v; v = v->fNext) {
         for (Edge* e = v->fFirstEdgeBelow; e; e = e->fNextEdgeBelow) {
             count += TESSELLATOR_WIREFRAME ? 12 : 6;
         }
     }
     return count;
 }
 
@@ -2322,23 +2322,24 @@ int PathToTriangles(const SkPath& path, 
         *isLinear = true;
         return 0;
     }
     SkArenaAlloc alloc(kArenaChunkSize);
     VertexList outerMesh;
     Poly* polys = path_to_polys(path, tolerance, clipBounds, contourCnt, alloc, antialias,
                                 isLinear, &outerMesh);
     SkPath::FillType fillType = antialias ? SkPath::kWinding_FillType : path.getFillType();
-    int count = count_points(polys, fillType);
+    int64_t count64 = count_points(polys, fillType);
     if (antialias) {
-        count += count_outer_mesh_points(outerMesh);
+        count64 += count_outer_mesh_points(outerMesh);
     }
-    if (0 == count) {
+    if (0 == count64 || count64 > SK_MaxS32) {
         return 0;
     }
+    int count = count64;
 
     void* verts = vertexAllocator->lock(count);
     if (!verts) {
         SkDebugf("Could not allocate vertices\n");
         return 0;
     }
 
     LOG("emitting %d verts\n", count);
@@ -2362,21 +2363,22 @@ int PathToVertices(const SkPath& path, S
         *verts = nullptr;
         return 0;
     }
     SkArenaAlloc alloc(kArenaChunkSize);
     bool isLinear;
     Poly* polys = path_to_polys(path, tolerance, clipBounds, contourCnt, alloc, false, &isLinear,
                                 nullptr);
     SkPath::FillType fillType = path.getFillType();
-    int count = count_points(polys, fillType);
-    if (0 == count) {
+    int64_t count64 = count_points(polys, fillType);
+    if (0 == count64 || count64 > SK_MaxS32) {
         *verts = nullptr;
         return 0;
     }
+    int count = count64;
 
     *verts = new GrTessellator::WindingVertex[count];
     GrTessellator::WindingVertex* vertsEnd = *verts;
     SkPoint* points = new SkPoint[count];
     SkPoint* pointsEnd = points;
     for (Poly* poly = polys; poly; poly = poly->fNext) {
         if (apply_fill_type(fillType, poly)) {
             SkPoint* start = pointsEnd;
--- a/gfx/skia/skia/src/gpu/ops/GrAAConvexPathRenderer.cpp
+++ b/gfx/skia/skia/src/gpu/ops/GrAAConvexPathRenderer.cpp
@@ -17,16 +17,17 @@
 #include "GrPathUtils.h"
 #include "GrProcessor.h"
 #include "GrSimpleMeshDrawOpHelper.h"
 #include "SkGeometry.h"
 #include "SkPathPriv.h"
 #include "SkPointPriv.h"
 #include "SkString.h"
 #include "SkTraceEvent.h"
+#include "SkTypes.h"
 #include "glsl/GrGLSLFragmentShaderBuilder.h"
 #include "glsl/GrGLSLGeometryProcessor.h"
 #include "glsl/GrGLSLProgramDataManager.h"
 #include "glsl/GrGLSLUniformHandler.h"
 #include "glsl/GrGLSLVarying.h"
 #include "glsl/GrGLSLVertexGeoBuilder.h"
 #include "ops/GrMeshDrawOp.h"
 
@@ -125,53 +126,58 @@ static void compute_vectors(SegmentArray
     // Make the normals point towards the outside
     SkPointPriv::Side normSide;
     if (dir == SkPathPriv::kCCW_FirstDirection) {
         normSide = SkPointPriv::kRight_Side;
     } else {
         normSide = SkPointPriv::kLeft_Side;
     }
 
-    *vCount = 0;
-    *iCount = 0;
+    int64_t vCount64 = 0;
+    int64_t iCount64 = 0;
     // compute normals at all points
     for (int a = 0; a < count; ++a) {
         Segment& sega = (*segments)[a];
         int b = (a + 1) % count;
         Segment& segb = (*segments)[b];
 
         const SkPoint* prevPt = &sega.endPt();
         int n = segb.countPoints();
         for (int p = 0; p < n; ++p) {
             segb.fNorms[p] = segb.fPts[p] - *prevPt;
             segb.fNorms[p].normalize();
             SkPointPriv::SetOrthog(&segb.fNorms[p], segb.fNorms[p], normSide);
             prevPt = &segb.fPts[p];
         }
         if (Segment::kLine == segb.fType) {
-            *vCount += 5;
-            *iCount += 9;
+            vCount64 += 5;
+            iCount64 += 9;
         } else {
-            *vCount += 6;
-            *iCount += 12;
+            vCount64 += 6;
+            iCount64 += 12;
         }
     }
 
     // compute mid-vectors where segments meet. TODO: Detect shallow corners
     // and leave out the wedges and close gaps by stitching segments together.
     for (int a = 0; a < count; ++a) {
         const Segment& sega = (*segments)[a];
         int b = (a + 1) % count;
         Segment& segb = (*segments)[b];
         segb.fMid = segb.fNorms[0] + sega.endNorm();
         segb.fMid.normalize();
         // corner wedges
-        *vCount += 4;
-        *iCount += 6;
+        vCount64 += 4;
+        iCount64 += 6;
     }
+    if (vCount64 > SK_MaxS32 || iCount64 > SK_MaxS32) {
+        return;
+    }
+    *vCount = vCount64;
+    *iCount = iCount64;
 }
 
 struct DegenerateTestData {
     DegenerateTestData() { fStage = kInitial; }
     bool isDegenerate() const { return kNonDegenerate != fStage; }
     enum {
         kInitial,
         kPoint,
--- a/gfx/skia/skia/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp
+++ b/gfx/skia/skia/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp
@@ -250,58 +250,70 @@ private:
 
         SkASSERT(fHelper.compatibleWithAlphaAsCoverage()
                          ? vertexStride == sizeof(GrDefaultGeoProcFactory::PositionColorAttr)
                          : vertexStride ==
                                    sizeof(GrDefaultGeoProcFactory::PositionColorCoverageAttr));
 
         int instanceCount = fPaths.count();
 
-        int vertexCount = 0;
-        int indexCount = 0;
-        int maxVertices = DEFAULT_BUFFER_SIZE;
-        int maxIndices = DEFAULT_BUFFER_SIZE;
+        int64_t vertexCount = 0;
+        int64_t indexCount = 0;
+        int64_t maxVertices = DEFAULT_BUFFER_SIZE;
+        int64_t maxIndices = DEFAULT_BUFFER_SIZE;
         uint8_t* vertices = (uint8_t*) sk_malloc_throw(maxVertices * vertexStride);
         uint16_t* indices = (uint16_t*) sk_malloc_throw(maxIndices * sizeof(uint16_t));
         for (int i = 0; i < instanceCount; i++) {
             const PathData& args = fPaths[i];
             GrAAConvexTessellator tess(args.fStyle, args.fStrokeWidth,
                                        args.fJoin, args.fMiterLimit);
 
             if (!tess.tessellate(args.fViewMatrix, args.fPath)) {
                 continue;
             }
 
-            int currentIndices = tess.numIndices();
-            if (indexCount + currentIndices > UINT16_MAX) {
+            int currentVertices = tess.numPts();
+            if (vertexCount + currentVertices > static_cast<int>(UINT16_MAX)) {
                 // if we added the current instance, we would overflow the indices we can store in a
                 // uint16_t. Draw what we've got so far and reset.
                 this->draw(target, gp.get(), pipeline, vertexCount, vertexStride, vertices,
                            indexCount, indices);
                 vertexCount = 0;
                 indexCount = 0;
             }
-            int currentVertices = tess.numPts();
             if (vertexCount + currentVertices > maxVertices) {
                 maxVertices = SkTMax(vertexCount + currentVertices, maxVertices * 2);
+                if (maxVertices * vertexStride > SK_MaxS32) {
+                    sk_free(vertices);
+                    sk_free(indices);
+                    return;
+                }
                 vertices = (uint8_t*) sk_realloc_throw(vertices, maxVertices * vertexStride);
             }
+            int currentIndices = tess.numIndices();
             if (indexCount + currentIndices > maxIndices) {
                 maxIndices = SkTMax(indexCount + currentIndices, maxIndices * 2);
+                if (maxIndices * sizeof(uint16_t) > SK_MaxS32) {
+                    sk_free(vertices);
+                    sk_free(indices);
+                    return;
+                }
                 indices = (uint16_t*) sk_realloc_throw(indices, maxIndices * sizeof(uint16_t));
             }
 
             extract_verts(tess, vertices + vertexStride * vertexCount, vertexStride, args.fColor,
                           vertexCount, indices + indexCount,
                           fHelper.compatibleWithAlphaAsCoverage());
             vertexCount += currentVertices;
             indexCount += currentIndices;
         }
-        this->draw(target, gp.get(), pipeline, vertexCount, vertexStride, vertices, indexCount,
-                   indices);
+        if (vertexCount <= SK_MaxS32 && indexCount <= SK_MaxS32) {
+            this->draw(target, gp.get(), pipeline, vertexCount, vertexStride, vertices, indexCount,
+                       indices);
+        }
         sk_free(vertices);
         sk_free(indices);
     }
 
     bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAFlatteningConvexPathOp* that = t->cast<AAFlatteningConvexPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
             return false;
--- a/gfx/skia/skia/src/gpu/ops/GrSmallPathRenderer.cpp
+++ b/gfx/skia/skia/src/gpu/ops/GrSmallPathRenderer.cpp
@@ -264,16 +264,22 @@ private:
                     kA8_GrMaskFormat, invert, fHelper.usesLocalCoords());
         }
 
         // allocate vertices
         size_t vertexStride = flushInfo.fGeometryProcessor->getVertexStride();
         SkASSERT(vertexStride == sizeof(SkPoint) + sizeof(GrColor) + 2*sizeof(uint16_t));
 
         const GrBuffer* vertexBuffer;
+
+        // We need to make sure we don't overflow a 32 bit int when we request space in the
+        // makeVertexSpace call below.
+        if (instanceCount > SK_MaxS32 / kVerticesPerQuad) {
+            return;
+        }
         void* vertices = target->makeVertexSpace(vertexStride,
                                                  kVerticesPerQuad * instanceCount,
                                                  &vertexBuffer,
                                                  &flushInfo.fVertexOffset);
         flushInfo.fVertexBuffer.reset(SkRef(vertexBuffer));
         flushInfo.fIndexBuffer = target->resourceProvider()->refQuadIndexBuffer();
         if (!vertices || !flushInfo.fIndexBuffer) {
             SkDebugf("Could not allocate vertices\n");
--- a/gfx/webrender/Cargo.toml
+++ b/gfx/webrender/Cargo.toml
@@ -15,17 +15,17 @@ debugger = ["ws", "serde_json", "serde",
 capture = ["webrender_api/serialize", "ron", "serde", "debug_renderer"]
 replay = ["webrender_api/deserialize", "ron", "serde"]
 debug_renderer = []
 pathfinder = ["pathfinder_font_renderer", "pathfinder_gfx_utils", "pathfinder_partitioner", "pathfinder_path_utils"]
 serialize_program = ["serde"]
 
 [dependencies]
 app_units = "0.7"
-base64 = { optional = true, version = "0.6" }
+base64 = { optional = true, version = "0.9" }
 bincode = "1.0"
 bitflags = "1.0"
 byteorder = "1.0"
 cfg-if = "0.1.2"
 euclid = "0.19"
 fxhash = "0.2.1"
 gleam = "0.6.3"
 image = { optional = true, version = "0.19" }
--- a/gfx/webrender/res/brush.glsl
+++ b/gfx/webrender/res/brush.glsl
@@ -4,17 +4,17 @@
 
 #ifdef WR_VERTEX_SHADER
 
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize local_rect,
     RectWithSize segment_rect,
-    ivec3 user_data,
+    ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 segment_data
 );
 
 #define VECS_PER_SEGMENT                    2
 
@@ -25,16 +25,17 @@ void brush_vs(
 
 void main(void) {
     // Load the brush instance from vertex attributes.
     int prim_header_address = aData.x;
     int clip_address = aData.y;
     int segment_index = aData.z & 0xffff;
     int edge_flags = (aData.z >> 16) & 0xff;
     int brush_flags = (aData.z >> 24) & 0xff;
+    int segment_user_data = aData.w;
     PrimitiveHeader ph = fetch_prim_header(prim_header_address);
 
     // Fetch the segment of this brush primitive we are drawing.
     int segment_address = ph.specific_prim_address +
                           VECS_PER_SPECIFIC_BRUSH +
                           segment_index * VECS_PER_SEGMENT;
 
     vec4[2] segment_data = fetch_from_gpu_cache_2(segment_address);
@@ -97,17 +98,17 @@ void main(void) {
 #endif
 
     // Run the specific brush VS code to write interpolators.
     brush_vs(
         vi,
         ph.specific_prim_address,
         ph.local_rect,
         local_segment_rect,
-        ph.user_data,
+        ivec4(ph.user_data, segment_user_data),
         transform.m,
         pic_task,
         brush_flags,
         segment_data[1]
     );
 }
 #endif
 
--- a/gfx/webrender/res/brush_blend.glsl
+++ b/gfx/webrender/res/brush_blend.glsl
@@ -16,17 +16,17 @@ flat varying vec4 vUvClipBounds;
 
 #ifdef WR_VERTEX_SHADER
 
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize local_rect,
     RectWithSize segment_rect,
-    ivec3 user_data,
+    ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 unused
 ) {
     ImageResource res = fetch_image_resource(user_data.x);
     vec2 uv0 = res.uv_rect.p0;
     vec2 uv1 = res.uv_rect.p1;
--- a/gfx/webrender/res/brush_image.glsl
+++ b/gfx/webrender/res/brush_image.glsl
@@ -42,33 +42,33 @@ ImageBrushData fetch_image_data(int addr
     return data;
 }
 
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize prim_rect,
     RectWithSize segment_rect,
-    ivec3 user_data,
+    ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 texel_rect
 ) {
     ImageBrushData image_data = fetch_image_data(prim_address);
 
     // If this is in WR_FEATURE_TEXTURE_RECT mode, the rect and size use
     // non-normalized texture coordinates.
 #ifdef WR_FEATURE_TEXTURE_RECT
     vec2 texture_size = vec2(1, 1);
 #else
     vec2 texture_size = vec2(textureSize(sColor0, 0));
 #endif
 
-    ImageResource res = fetch_image_resource(user_data.x);
+    ImageResource res = fetch_image_resource(user_data.w);
     vec2 uv0 = res.uv_rect.p0;
     vec2 uv1 = res.uv_rect.p1;
 
     RectWithSize local_rect = prim_rect;
     vec2 stretch_size = image_data.stretch_size;
 
     // If this segment should interpolate relative to the
     // segment, modify the parameters for that.
@@ -101,32 +101,32 @@ void brush_vs(
     vUvSampleBounds = vec4(
         min_uv + vec2(0.5),
         max_uv - vec2(0.5)
     ) / texture_size.xyxy;
 
     vec2 f = (vi.local_pos - local_rect.p0) / local_rect.size;
 
 #ifdef WR_FEATURE_ALPHA_PASS
-    int color_mode = user_data.y >> 16;
-    int raster_space = user_data.y & 0xffff;
+    int color_mode = user_data.x;
+    int raster_space = user_data.y;
 
     if (color_mode == COLOR_MODE_FROM_PASS) {
         color_mode = uMode;
     }
 
     // Derive the texture coordinates for this image, based on
     // whether the source image is a local-space or screen-space
     // image.
     switch (raster_space) {
         case RASTER_SCREEN: {
             // Since the screen space UVs specify an arbitrary quad, do
             // a bilinear interpolation to get the correct UV for this
             // local position.
-            ImageResourceExtra extra_data = fetch_image_resource_extra(user_data.x);
+            ImageResourceExtra extra_data = fetch_image_resource_extra(user_data.w);
             vec2 x = mix(extra_data.st_tl, extra_data.st_tr, f.x);
             vec2 y = mix(extra_data.st_bl, extra_data.st_br, f.x);
             f = mix(x, y, f.y);
             break;
         }
         default:
             break;
     }
--- a/gfx/webrender/res/brush_linear_gradient.glsl
+++ b/gfx/webrender/res/brush_linear_gradient.glsl
@@ -40,17 +40,17 @@ Gradient fetch_gradient(int address) {
     );
 }
 
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize local_rect,
     RectWithSize segment_rect,
-    ivec3 user_data,
+    ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 texel_rect
 ) {
     Gradient gradient = fetch_gradient(prim_address);
 
     if ((brush_flags & BRUSH_FLAG_SEGMENT_RELATIVE) != 0) {
--- a/gfx/webrender/res/brush_mix_blend.glsl
+++ b/gfx/webrender/res/brush_mix_blend.glsl
@@ -17,17 +17,17 @@ vec2 snap_device_pos(VertexInfo vi, floa
     return vi.world_pos.xy * device_pixel_scale / max(0.0, vi.world_pos.w) + vi.snap_offset;
 }
 
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize local_rect,
     RectWithSize segment_rect,
-    ivec3 user_data,
+    ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 unused
 ) {
     vec2 snapped_device_pos = snap_device_pos(vi, pic_task.common_data.device_pixel_scale);
     vec2 texture_size = vec2(textureSize(sPrevPassColor, 0));
     vOp = user_data.x;
--- a/gfx/webrender/res/brush_radial_gradient.glsl
+++ b/gfx/webrender/res/brush_radial_gradient.glsl
@@ -40,17 +40,17 @@ RadialGradient fetch_radial_gradient(int
     );
 }
 
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize local_rect,
     RectWithSize segment_rect,
-    ivec3 user_data,
+    ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 texel_rect
 ) {
     RadialGradient gradient = fetch_radial_gradient(prim_address);
 
     if ((brush_flags & BRUSH_FLAG_SEGMENT_RELATIVE) != 0) {
--- a/gfx/webrender/res/brush_solid.glsl
+++ b/gfx/webrender/res/brush_solid.glsl
@@ -23,17 +23,17 @@ SolidBrush fetch_solid_primitive(int add
     return SolidBrush(data);
 }
 
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize local_rect,
     RectWithSize segment_rect,
-    ivec3 user_data,
+    ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 unused
 ) {
     SolidBrush prim = fetch_solid_primitive(prim_address);
     vColor = prim.color;
 
--- a/gfx/webrender/res/brush_yuv_image.glsl
+++ b/gfx/webrender/res/brush_yuv_image.glsl
@@ -81,17 +81,17 @@ YuvPrimitive fetch_yuv_primitive(int add
     return YuvPrimitive(data.x);
 }
 
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize local_rect,
     RectWithSize segment_rect,
-    ivec3 user_data,
+    ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 unused
 ) {
     vec2 f = (vi.local_pos - local_rect.p0) / local_rect.size;
 
     YuvPrimitive prim = fetch_yuv_primitive(prim_address);
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -13,25 +13,25 @@ use gpu_cache::{GpuCache, GpuCacheHandle
 use gpu_types::{BrushFlags, BrushInstance, PrimitiveHeaders};
 use gpu_types::{ClipMaskInstance, SplitCompositeInstance};
 use gpu_types::{PrimitiveInstanceData, RasterizationSpace, GlyphInstance};
 use gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
 use internal_types::{FastHashMap, SavedTargetIndex, TextureSource};
 use picture::{PictureCompositeMode, PicturePrimitive, PictureSurface};
 use plane_split::{BspSplitter, Clipper, Polygon, Splitter};
 use prim_store::{BrushKind, BrushPrimitive, BrushSegmentTaskId, DeferredResolve};
-use prim_store::{EdgeAaSegmentMask, ImageSource};
+use prim_store::{EdgeAaSegmentMask, ImageSource, PrimitiveIndex};
 use prim_store::{VisibleGradientTile, PrimitiveInstance};
 use prim_store::{BorderSource, Primitive, PrimitiveDetails};
 use render_task::{RenderTaskAddress, RenderTaskId, RenderTaskTree};
 use renderer::{BlendMode, ImageBufferKind, ShaderColorMode};
 use renderer::BLOCKS_PER_UV_RECT;
 use resource_cache::{CacheItem, GlyphFetchResult, ImageRequest, ResourceCache, ImageProperties};
 use scene::FilterOpHelpers;
-use std::{f32, i32};
+use std::{f32, i32, usize};
 use tiling::{RenderTargetContext};
 use util::{MatrixHelpers, TransformedRectKind};
 
 // Special sentinel value recognized by the shader. It is considered to be
 // a dummy task that doesn't mask out anything.
 const OPAQUE_TASK_ADDRESS: RenderTaskAddress = RenderTaskAddress(0x7fff);
 
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
@@ -122,135 +122,151 @@ impl BatchKey {
 #[inline]
 fn textures_compatible(t1: TextureSource, t2: TextureSource) -> bool {
     t1 == TextureSource::Invalid || t2 == TextureSource::Invalid || t1 == t2
 }
 
 pub struct AlphaBatchList {
     pub batches: Vec<PrimitiveBatch>,
     pub item_rects: Vec<Vec<WorldRect>>,
+    current_batch_index: usize,
+    current_prim_index: PrimitiveIndex,
 }
 
 impl AlphaBatchList {
     fn new() -> Self {
         AlphaBatchList {
             batches: Vec::new(),
             item_rects: Vec::new(),
+            current_prim_index: PrimitiveIndex(usize::MAX),
+            current_batch_index: usize::MAX,
         }
     }
 
-    pub fn get_suitable_batch(
+    pub fn set_params_and_get_batch(
         &mut self,
         key: BatchKey,
         bounding_rect: &WorldRect,
+        prim_index: PrimitiveIndex,
     ) -> &mut Vec<PrimitiveInstanceData> {
-        let mut selected_batch_index = None;
+        if prim_index != self.current_prim_index ||
+           self.current_batch_index == usize::MAX ||
+           !self.batches[self.current_batch_index].key.is_compatible_with(&key) {
+            let mut selected_batch_index = None;
 
-        match key.blend_mode {
-            BlendMode::SubpixelWithBgColor => {
-                'outer_multipass: for (batch_index, batch) in self.batches.iter().enumerate().rev().take(10) {
-                    // Some subpixel batches are drawn in two passes. Because of this, we need
-                    // to check for overlaps with every batch (which is a bit different
-                    // than the normal batching below).
-                    for item_rect in &self.item_rects[batch_index] {
-                        if item_rect.intersects(bounding_rect) {
-                            break 'outer_multipass;
+            match key.blend_mode {
+                BlendMode::SubpixelWithBgColor => {
+                    'outer_multipass: for (batch_index, batch) in self.batches.iter().enumerate().rev().take(10) {
+                        // Some subpixel batches are drawn in two passes. Because of this, we need
+                        // to check for overlaps with every batch (which is a bit different
+                        // than the normal batching below).
+                        for item_rect in &self.item_rects[batch_index] {
+                            if item_rect.intersects(bounding_rect) {
+                                break 'outer_multipass;
+                            }
+                        }
+
+                        if batch.key.is_compatible_with(&key) {
+                            selected_batch_index = Some(batch_index);
+                            break;
                         }
                     }
+                }
+                _ => {
+                    'outer_default: for (batch_index, batch) in self.batches.iter().enumerate().rev().take(10) {
+                        // For normal batches, we only need to check for overlaps for batches
+                        // other than the first batch we consider. If the first batch
+                        // is compatible, then we know there isn't any potential overlap
+                        // issues to worry about.
+                        if batch.key.is_compatible_with(&key) {
+                            selected_batch_index = Some(batch_index);
+                            break;
+                        }
 
-                    if batch.key.is_compatible_with(&key) {
-                        selected_batch_index = Some(batch_index);
-                        break;
-                    }
-                }
-            }
-            _ => {
-                'outer_default: for (batch_index, batch) in self.batches.iter().enumerate().rev().take(10) {
-                    // For normal batches, we only need to check for overlaps for batches
-                    // other than the first batch we consider. If the first batch
-                    // is compatible, then we know there isn't any potential overlap
-                    // issues to worry about.
-                    if batch.key.is_compatible_with(&key) {
-                        selected_batch_index = Some(batch_index);
-                        break;
-                    }
-
-                    // check for intersections
-                    for item_rect in &self.item_rects[batch_index] {
-                        if item_rect.intersects(bounding_rect) {
-                            break 'outer_default;
+                        // check for intersections
+                        for item_rect in &self.item_rects[batch_index] {
+                            if item_rect.intersects(bounding_rect) {
+                                break 'outer_default;
+                            }
                         }
                     }
                 }
             }
+
+            if selected_batch_index.is_none() {
+                let new_batch = PrimitiveBatch::new(key);
+                selected_batch_index = Some(self.batches.len());
+                self.batches.push(new_batch);
+                self.item_rects.push(Vec::new());
+            }
+
+            self.current_batch_index = selected_batch_index.unwrap();
+            self.item_rects[self.current_batch_index].push(*bounding_rect);
+            self.current_prim_index = prim_index;
         }
 
-        if selected_batch_index.is_none() {
-            let new_batch = PrimitiveBatch::new(key);
-            selected_batch_index = Some(self.batches.len());
-            self.batches.push(new_batch);
-            self.item_rects.push(Vec::new());
-        }
-
-        let selected_batch_index = selected_batch_index.unwrap();
-        self.item_rects[selected_batch_index].push(*bounding_rect);
-        &mut self.batches[selected_batch_index].instances
+        &mut self.batches[self.current_batch_index].instances
     }
 }
 
 pub struct OpaqueBatchList {
     pub pixel_area_threshold_for_new_batch: f32,
     pub batches: Vec<PrimitiveBatch>,
+    pub current_batch_index: usize,
 }
 
 impl OpaqueBatchList {
     fn new(pixel_area_threshold_for_new_batch: f32) -> Self {
         OpaqueBatchList {
             batches: Vec::new(),
             pixel_area_threshold_for_new_batch,
+            current_batch_index: usize::MAX,
         }
     }
 
-    pub fn get_suitable_batch(
+    pub fn set_params_and_get_batch(
         &mut self,
         key: BatchKey,
         bounding_rect: &WorldRect,
     ) -> &mut Vec<PrimitiveInstanceData> {
-        let mut selected_batch_index = None;
-        let item_area = bounding_rect.size.area();
+        if self.current_batch_index == usize::MAX ||
+           !self.batches[self.current_batch_index].key.is_compatible_with(&key) {
+            let mut selected_batch_index = None;
+            let item_area = bounding_rect.size.area();
 
-        // If the area of this primitive is larger than the given threshold,
-        // then it is large enough to warrant breaking a batch for. In this
-        // case we just see if it can be added to the existing batch or
-        // create a new one.
-        if item_area > self.pixel_area_threshold_for_new_batch {
-            if let Some(batch) = self.batches.last() {
-                if batch.key.is_compatible_with(&key) {
-                    selected_batch_index = Some(self.batches.len() - 1);
+            // If the area of this primitive is larger than the given threshold,
+            // then it is large enough to warrant breaking a batch for. In this
+            // case we just see if it can be added to the existing batch or
+            // create a new one.
+            if item_area > self.pixel_area_threshold_for_new_batch {
+                if let Some(batch) = self.batches.last() {
+                    if batch.key.is_compatible_with(&key) {
+                        selected_batch_index = Some(self.batches.len() - 1);
+                    }
+                }
+            } else {
+                // Otherwise, look back through a reasonable number of batches.
+                for (batch_index, batch) in self.batches.iter().enumerate().rev().take(10) {
+                    if batch.key.is_compatible_with(&key) {
+                        selected_batch_index = Some(batch_index);
+                        break;
+                    }
                 }
             }
-        } else {
-            // Otherwise, look back through a reasonable number of batches.
-            for (batch_index, batch) in self.batches.iter().enumerate().rev().take(10) {
-                if batch.key.is_compatible_with(&key) {
-                    selected_batch_index = Some(batch_index);
-                    break;
-                }
+
+            if selected_batch_index.is_none() {
+                let new_batch = PrimitiveBatch::new(key);
+                selected_batch_index = Some(self.batches.len());
+                self.batches.push(new_batch);
             }
+
+            self.current_batch_index = selected_batch_index.unwrap();
         }
 
-        if selected_batch_index.is_none() {
-            let new_batch = PrimitiveBatch::new(key);
-            selected_batch_index = Some(self.batches.len());
-            self.batches.push(new_batch);
-        }
-
-        let batch = &mut self.batches[selected_batch_index.unwrap()];
-
-        &mut batch.instances
+        &mut self.batches[self.current_batch_index].instances
     }
 
     fn finalize(&mut self) {
         // Reverse the instance arrays in the opaque batches
         // to get maximum z-buffer efficiency by drawing
         // front-to-back.
         // TODO(gw): Maybe we can change the batch code to
         //           build these in reverse and avoid having
@@ -273,53 +289,62 @@ impl BatchList {
         let batch_area_threshold = (screen_size.width * screen_size.height) as f32 / 4.0;
 
         BatchList {
             alpha_batch_list: AlphaBatchList::new(),
             opaque_batch_list: OpaqueBatchList::new(batch_area_threshold),
         }
     }
 
-    pub fn get_suitable_batch(
+    pub fn push_single_instance(
         &mut self,
         key: BatchKey,
         bounding_rect: &WorldRect,
-    ) -> &mut Vec<PrimitiveInstanceData> {
+        prim_index: PrimitiveIndex,
+        instance: PrimitiveInstanceData,
+    ) {
         match key.blend_mode {
             BlendMode::None => {
                 self.opaque_batch_list
-                    .get_suitable_batch(key, bounding_rect)
+                    .set_params_and_get_batch(key, bounding_rect)
+                    .push(instance);
             }
             BlendMode::Alpha |
             BlendMode::PremultipliedAlpha |
             BlendMode::PremultipliedDestOut |
             BlendMode::SubpixelConstantTextColor(..) |
             BlendMode::SubpixelWithBgColor |
             BlendMode::SubpixelDualSource => {
                 self.alpha_batch_list
-                    .get_suitable_batch(key, bounding_rect)
+                    .set_params_and_get_batch(key, bounding_rect, prim_index)
+                    .push(instance);
             }
         }
     }
 
-    // Remove any batches that were added but didn't get any instances
-    // added to them.
-    fn remove_unused_batches(&mut self) {
-        if self.opaque_batch_list
-               .batches
-               .last()
-               .map_or(false, |batch| batch.instances.is_empty()) {
-            self.opaque_batch_list.batches.pop().unwrap();
-        }
-
-        if self.alpha_batch_list
-               .batches
-               .last()
-               .map_or(false, |batch| batch.instances.is_empty()) {
-            self.alpha_batch_list.batches.pop().unwrap();
+    pub fn set_params_and_get_batch(
+        &mut self,
+        key: BatchKey,
+        bounding_rect: &WorldRect,
+        prim_index: PrimitiveIndex,
+    ) -> &mut Vec<PrimitiveInstanceData> {
+        match key.blend_mode {
+            BlendMode::None => {
+                self.opaque_batch_list
+                    .set_params_and_get_batch(key, bounding_rect)
+            }
+            BlendMode::Alpha |
+            BlendMode::PremultipliedAlpha |
+            BlendMode::PremultipliedDestOut |
+            BlendMode::SubpixelConstantTextColor(..) |
+            BlendMode::SubpixelWithBgColor |
+            BlendMode::SubpixelDualSource => {
+                self.alpha_batch_list
+                    .set_params_and_get_batch(key, bounding_rect, prim_index)
+            }
         }
     }
 
     fn finalize(&mut self) {
         self.opaque_batch_list.finalize()
     }
 }
 
@@ -530,31 +555,31 @@ impl AlphaBatchBuilder {
             ];
 
             let gpu_handle = gpu_cache.push_per_frame_blocks(&gpu_blocks);
             let key = BatchKey::new(
                 BatchKind::SplitComposite,
                 BlendMode::PremultipliedAlpha,
                 BatchTextures::no_texture(),
             );
-            let batch = self.batch_list
-                            .get_suitable_batch(
-                                key,
-                                &prim_instance.clipped_world_rect.as_ref().expect("bug"),
-                            );
 
             let gpu_address = gpu_cache.get_address(&gpu_handle);
 
             let instance = SplitCompositeInstance::new(
                 prim_header_index,
                 gpu_address,
                 prim_headers.z_generator.next(),
             );
 
-            batch.push(PrimitiveInstanceData::from(instance));
+            self.batch_list.push_single_instance(
+                key,
+                &prim_instance.clipped_world_rect.as_ref().expect("bug"),
+                prim_instance.prim_index,
+                PrimitiveInstanceData::from(instance),
+            );
         }
     }
 
     // Adds a primitive to a batch.
     // It can recursively call itself in some situations, for
     // example if it encounters a picture where the items
     // in that picture are being drawn into the same target.
     fn add_prim_to_batch(
@@ -721,32 +746,37 @@ impl AlphaBatchBuilder {
                                                         ctx.resource_cache,
                                                         gpu_cache,
                                                     );
                                                 let key = BatchKey::new(
                                                     kind,
                                                     non_segmented_blend_mode,
                                                     textures,
                                                 );
-                                                let batch = self.batch_list.get_suitable_batch(key, bounding_rect);
                                                 let prim_header_index = prim_headers.push(&prim_header, [
-                                                    uv_rect_address.as_int(),
-                                                    (ShaderColorMode::Image as i32) << 16 |
+                                                    ShaderColorMode::Image as i32,
                                                     RasterizationSpace::Screen as i32,
                                                     0,
                                                 ]);
 
                                                 let instance = BrushInstance {
                                                     prim_header_index,
                                                     segment_index: 0,
                                                     edge_flags: EdgeAaSegmentMask::empty(),
                                                     brush_flags: BrushFlags::empty(),
                                                     clip_task_address,
+                                                    user_data: uv_rect_address.as_int(),
                                                 };
-                                                batch.push(PrimitiveInstanceData::from(instance));
+
+                                                self.batch_list.push_single_instance(
+                                                    key,
+                                                    bounding_rect,
+                                                    prim_instance.prim_index,
+                                                    PrimitiveInstanceData::from(instance),
+                                                );
                                             }
                                             FilterOp::DropShadow(offset, ..) => {
                                                 // Draw an instance of the shadow first, following by the content.
 
                                                 // Both the shadow and the content get drawn as a brush image.
                                                 let kind = BatchKind::Brush(
                                                     BrushBatchKind::Image(ImageBufferKind::Texture2DArray),
                                                 );
@@ -779,62 +809,68 @@ impl AlphaBatchBuilder {
                                                 let content_uv_rect_address = render_tasks[secondary_id]
                                                     .get_texture_address(gpu_cache)
                                                     .as_int();
 
                                                 // Get the GPU cache address of the extra data handle.
                                                 let shadow_prim_address = gpu_cache.get_address(&picture.extra_gpu_data_handle);
 
                                                 let content_prim_header_index = prim_headers.push(&prim_header, [
-                                                    content_uv_rect_address,
-                                                    (ShaderColorMode::Image as i32) << 16 |
+                                                    ShaderColorMode::Image as i32,
                                                     RasterizationSpace::Screen as i32,
                                                     0,
                                                 ]);
 
                                                 let shadow_rect = prim_metadata.local_rect.translate(&offset);
                                                 let shadow_clip_rect = prim_metadata.local_clip_rect.translate(&offset);
 
                                                 let shadow_prim_header = PrimitiveHeader {
                                                     local_rect: shadow_rect,
                                                     local_clip_rect: shadow_clip_rect,
                                                     specific_prim_address: shadow_prim_address,
                                                     ..prim_header
                                                 };
 
                                                 let shadow_prim_header_index = prim_headers.push(&shadow_prim_header, [
-                                                    shadow_uv_rect_address,
-                                                    (ShaderColorMode::Alpha as i32) << 16 |
+                                                    ShaderColorMode::Alpha as i32,
                                                     RasterizationSpace::Screen as i32,
                                                     0,
                                                 ]);
 
                                                 let shadow_instance = BrushInstance {
                                                     prim_header_index: shadow_prim_header_index,
                                                     clip_task_address,
                                                     segment_index: 0,
                                                     edge_flags: EdgeAaSegmentMask::empty(),
                                                     brush_flags: BrushFlags::empty(),
+                                                    user_data: shadow_uv_rect_address,
                                                 };
 
                                                 let content_instance = BrushInstance {
                                                     prim_header_index: content_prim_header_index,
                                                     clip_task_address,
                                                     segment_index: 0,
                                                     edge_flags: EdgeAaSegmentMask::empty(),
                                                     brush_flags: BrushFlags::empty(),
+                                                    user_data: content_uv_rect_address,
                                                 };
 
-                                                self.batch_list
-                                                    .get_suitable_batch(shadow_key, bounding_rect)
-                                                    .push(PrimitiveInstanceData::from(shadow_instance));
+                                                self.batch_list.push_single_instance(
+                                                    shadow_key,
+                                                    bounding_rect,
+                                                    prim_instance.prim_index,
+                                                    PrimitiveInstanceData::from(shadow_instance),
+                                                );
 
-                                                self.batch_list
-                                                    .get_suitable_batch(content_key, bounding_rect)
-                                                    .push(PrimitiveInstanceData::from(content_instance));
+                                                self.batch_list.push_single_instance(
+                                                    content_key,
+                                                    bounding_rect,
+                                                    prim_instance.prim_index,
+                                                    PrimitiveInstanceData::from(content_instance),
+                                                );
                                             }
                                             _ => {
                                                 let filter_mode = match filter {
                                                     FilterOp::Identity => 1, // matches `Contrast(1)`
                                                     FilterOp::Blur(..) => 0,
                                                     FilterOp::Contrast(..) => 1,
                                                     FilterOp::Grayscale(..) => 2,
                                                     FilterOp::HueRotate(..) => 3,
@@ -894,20 +930,25 @@ impl AlphaBatchBuilder {
                                                 ]);
 
                                                 let instance = BrushInstance {
                                                     prim_header_index,
                                                     clip_task_address,
                                                     segment_index: 0,
                                                     edge_flags: EdgeAaSegmentMask::empty(),
                                                     brush_flags: BrushFlags::empty(),
+                                                    user_data: 0,
                                                 };
 
-                                                let batch = self.batch_list.get_suitable_batch(key, bounding_rect);
-                                                batch.push(PrimitiveInstanceData::from(instance));
+                                                self.batch_list.push_single_instance(
+                                                    key,
+                                                    bounding_rect,
+                                                    prim_instance.prim_index,
+                                                    PrimitiveInstanceData::from(instance),
+                                                );
                                             }
                                         }
                                     }
                                     PictureCompositeMode::MixBlend(mode) => {
                                         let cache_task_id = surface.resolve_render_task_id();
                                         let backdrop_id = picture.secondary_render_task_id.expect("no backdrop!?");
 
                                         let key = BatchKey::new(
@@ -916,68 +957,75 @@ impl AlphaBatchBuilder {
                                                     task_id,
                                                     source_id: cache_task_id,
                                                     backdrop_id,
                                                 },
                                             ),
                                             BlendMode::PremultipliedAlpha,
                                             BatchTextures::no_texture(),
                                         );
-                                        let batch = self.batch_list.get_suitable_batch(key, bounding_rect);
                                         let backdrop_task_address = render_tasks.get_task_address(backdrop_id);
                                         let source_task_address = render_tasks.get_task_address(cache_task_id);
                                         let prim_header_index = prim_headers.push(&prim_header, [
                                             mode as u32 as i32,
                                             backdrop_task_address.0 as i32,
                                             source_task_address.0 as i32,
                                         ]);
 
                                         let instance = BrushInstance {
                                             prim_header_index,
                                             clip_task_address,
                                             segment_index: 0,
                                             edge_flags: EdgeAaSegmentMask::empty(),
                                             brush_flags: BrushFlags::empty(),
+                                            user_data: 0,
                                         };
 
-                                        batch.push(PrimitiveInstanceData::from(instance));
+                                        self.batch_list.push_single_instance(
+                                            key,
+                                            bounding_rect,
+                                            prim_instance.prim_index,
+                                            PrimitiveInstanceData::from(instance),
+                                        );
                                     }
                                     PictureCompositeMode::Blit => {
                                         let cache_task_id = surface.resolve_render_task_id();
                                         let kind = BatchKind::Brush(
                                             BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
                                         );
                                         let key = BatchKey::new(
                                             kind,
                                             non_segmented_blend_mode,
                                             BatchTextures::render_target_cache(),
                                         );
-                                        let batch = self.batch_list.get_suitable_batch(
-                                            key,
-                                            bounding_rect,
-                                        );
 
                                         let uv_rect_address = render_tasks[cache_task_id]
                                             .get_texture_address(gpu_cache)
                                             .as_int();
                                         let prim_header_index = prim_headers.push(&prim_header, [
-                                            uv_rect_address,
-                                            (ShaderColorMode::Image as i32) << 16 |
+                                            ShaderColorMode::Image as i32,
                                             RasterizationSpace::Screen as i32,
                                             0,
                                         ]);
 
                                         let instance = BrushInstance {
                                             prim_header_index,
                                             clip_task_address,
                                             segment_index: 0,
                                             edge_flags: EdgeAaSegmentMask::empty(),
                                             brush_flags: BrushFlags::empty(),
+                                            user_data: uv_rect_address,
                                         };
-                                        batch.push(PrimitiveInstanceData::from(instance));
+
+                                        self.batch_list.push_single_instance(
+                                            key,
+                                            bounding_rect,
+                                            prim_instance.prim_index,
+                                            PrimitiveInstanceData::from(instance),
+                                        );
                                     }
                                 }
                             }
                             None => {
                                 // If this picture is being drawn into an existing target (i.e. with
                                 // no composition operation), recurse and add to the current batch list.
                                 self.add_pic_to_batch(
                                     picture,
@@ -990,73 +1038,83 @@ impl AlphaBatchBuilder {
                                     transforms,
                                     root_spatial_node_index,
                                 );
                             }
                         }
                     }
                     BrushKind::Image { request, ref visible_tiles, .. } if !visible_tiles.is_empty() => {
                         for tile in visible_tiles {
-                            if let Some((batch_kind, textures, user_data)) = get_image_tile_params(
+                            if let Some((batch_kind, textures, user_data, uv_rect_address)) = get_image_tile_params(
                                     ctx.resource_cache,
                                     gpu_cache,
                                     deferred_resolves,
                                     request.with_tile(tile.tile_offset),
                             ) {
                                 let prim_cache_address = gpu_cache.get_address(&tile.handle);
                                 let prim_header = PrimitiveHeader {
                                     specific_prim_address: prim_cache_address,
                                     local_rect: tile.local_rect,
                                     local_clip_rect: tile.local_clip_rect,
                                     ..prim_header
                                 };
                                 let prim_header_index = prim_headers.push(&prim_header, user_data);
 
                                 self.add_image_tile_to_batch(
+                                    prim_instance,
                                     batch_kind,
                                     specified_blend_mode,
                                     textures,
                                     prim_header_index,
                                     clip_task_address,
                                     bounding_rect,
-                                    tile.edge_flags
+                                    tile.edge_flags,
+                                    uv_rect_address,
                                 );
                             }
                         }
                     }
                     BrushKind::LinearGradient { ref stops_handle, ref visible_tiles, .. } if !visible_tiles.is_empty() => {
                         add_gradient_tiles(
+                            prim_instance,
                             visible_tiles,
                             stops_handle,
                             BrushBatchKind::LinearGradient,
                             specified_blend_mode,
                             bounding_rect,
                             clip_task_address,
                             gpu_cache,
                             &mut self.batch_list,
                             &prim_header,
                             prim_headers,
                         );
                     }
                     BrushKind::RadialGradient { ref stops_handle, ref visible_tiles, .. } if !visible_tiles.is_empty() => {
                         add_gradient_tiles(
+                            prim_instance,
                             visible_tiles,
                             stops_handle,
                             BrushBatchKind::RadialGradient,
                             specified_blend_mode,
                             bounding_rect,
                             clip_task_address,
                             gpu_cache,
                             &mut self.batch_list,
                             &prim_header,
                             prim_headers,
                         );
                     }
                     _ => {
-                        if let Some((batch_kind, textures, user_data)) = brush.get_batch_params(
+                        // TODO(gw): As an interim step, just return one value for the
+                        //           per-segment user data. In the future, this method
+                        //           will be expanded to optionally return a list of
+                        //           (BatchTextures, user_data) per segment, which will
+                        //           allow a different texture / render task to be used
+                        //           per segment.
+                        if let Some((batch_kind, textures, user_data, segment_user_data)) = brush.get_batch_params(
                                 ctx.resource_cache,
                                 gpu_cache,
                                 deferred_resolves,
                                 ctx.prim_store.chase_id == Some(prim_instance.prim_index),
                         ) {
                             let prim_header_index = prim_headers.push(&prim_header, user_data);
                             if cfg!(debug_assertions) && ctx.prim_store.chase_id == Some(prim_instance.prim_index) {
                                 println!("\t{:?} {:?}, task relative bounds {:?}",
@@ -1070,26 +1128,27 @@ impl AlphaBatchBuilder {
                                 specified_blend_mode,
                                 non_segmented_blend_mode,
                                 textures,
                                 prim_header_index,
                                 clip_task_address,
                                 bounding_rect,
                                 transform_kind,
                                 render_tasks,
+                                segment_user_data,
                             );
                         }
                     }
                 }
             }
             PrimitiveDetails::TextRun(ref text_cpu) => {
                 let subpx_dir = text_cpu.used_font.get_subpx_dir();
 
                 let glyph_fetch_buffer = &mut self.glyph_fetch_buffer;
-                let batch_list = &mut self.batch_list;
+                let alpha_batch_list = &mut self.batch_list.alpha_batch_list;
 
                 ctx.resource_cache.fetch_glyphs(
                     text_cpu.used_font.clone(),
                     &text_cpu.glyph_keys,
                     glyph_fetch_buffer,
                     gpu_cache,
                     |texture_id, mut glyph_format, glyphs| {
                         debug_assert_ne!(texture_id, TextureSource::Invalid);
@@ -1149,20 +1208,24 @@ impl AlphaBatchBuilder {
                                     BlendMode::PremultipliedAlpha,
                                     ShaderColorMode::ColorBitmap,
                                 )
                             }
                         };
 
                         let prim_header_index = prim_headers.push(&prim_header, [0; 3]);
                         let key = BatchKey::new(kind, blend_mode, textures);
-                        let batch = batch_list.get_suitable_batch(key, bounding_rect);
                         let base_instance = GlyphInstance::new(
                             prim_header_index,
                         );
+                        let batch = alpha_batch_list.set_params_and_get_batch(
+                            key,
+                            bounding_rect,
+                            prim_instance.prim_index,
+                        );
 
                         for glyph in glyphs {
                             batch.push(base_instance.build(
                                 glyph.index_in_text_run,
                                 glyph.uv_rect_address.as_int(),
                                 (subpx_dir as u32 as i32) << 16 |
                                 (color_mode as u32 as i32),
                             ));
@@ -1170,87 +1233,74 @@ impl AlphaBatchBuilder {
                     },
                 );
             }
         }
     }
 
     fn add_image_tile_to_batch(
         &mut self,
+        prim_instance: &PrimitiveInstance,
         batch_kind: BrushBatchKind,
         blend_mode: BlendMode,
         textures: BatchTextures,
         prim_header_index: PrimitiveHeaderIndex,
         clip_task_address: RenderTaskAddress,
         bounding_rect: &WorldRect,
         edge_flags: EdgeAaSegmentMask,
+        uv_rect_address: GpuCacheAddress,
     ) {
         let base_instance = BrushInstance {
             prim_header_index,
             clip_task_address,
             segment_index: 0,
             edge_flags,
             brush_flags: BrushFlags::PERSPECTIVE_INTERPOLATION,
+            user_data: uv_rect_address.as_int(),
         };
 
         let batch_key = BatchKey {
             blend_mode,
             kind: BatchKind::Brush(batch_kind),
             textures,
         };
-        let batch = self.batch_list.get_suitable_batch(batch_key, bounding_rect);
-        batch.push(PrimitiveInstanceData::from(base_instance));
+        self.batch_list.push_single_instance(
+            batch_key,
+            bounding_rect,
+            prim_instance.prim_index,
+            PrimitiveInstanceData::from(base_instance),
+        );
     }
 
     fn add_brush_to_batch(
         &mut self,
         brush: &BrushPrimitive,
         prim_instance: &PrimitiveInstance,
         batch_kind: BrushBatchKind,
         alpha_blend_mode: BlendMode,
         non_segmented_blend_mode: BlendMode,
         textures: BatchTextures,
         prim_header_index: PrimitiveHeaderIndex,
         clip_task_address: RenderTaskAddress,
         bounding_rect: &WorldRect,
         transform_kind: TransformedRectKind,
         render_tasks: &RenderTaskTree,
+        user_data: i32,
     ) {
         let base_instance = BrushInstance {
             prim_header_index,
             clip_task_address,
             segment_index: 0,
             edge_flags: EdgeAaSegmentMask::all(),
             brush_flags: BrushFlags::PERSPECTIVE_INTERPOLATION,
+            user_data,
         };
 
         match brush.segment_desc {
             Some(ref segment_desc) => {
-                let alpha_batch_key = BatchKey {
-                    blend_mode: alpha_blend_mode,
-                    kind: BatchKind::Brush(batch_kind),
-                    textures,
-                };
-
-                let alpha_batch = self.batch_list.alpha_batch_list.get_suitable_batch(
-                    alpha_batch_key,
-                    bounding_rect,
-                );
-
-                let opaque_batch_key = BatchKey {
-                    blend_mode: BlendMode::None,
-                    kind: BatchKind::Brush(batch_kind),
-                    textures,
-                };
-
-                let opaque_batch = self.batch_list.opaque_batch_list.get_suitable_batch(
-                    opaque_batch_key,
-                    bounding_rect,
-                );
-
                 for (i, segment) in segment_desc.segments.iter().enumerate() {
                     let is_inner = segment.edge_flags.is_empty();
                     let needs_blending = !prim_instance.opacity.is_opaque ||
                                          segment.clip_task_id.needs_blending() ||
                                          (!is_inner && transform_kind == TransformedRectKind::Complex);
 
                     let clip_task_address = match segment.clip_task_id {
                         BrushSegmentTaskId::RenderTaskId(id) =>
@@ -1262,57 +1312,68 @@ impl AlphaBatchBuilder {
                     let instance = PrimitiveInstanceData::from(BrushInstance {
                         segment_index: i as i32,
                         edge_flags: segment.edge_flags,
                         clip_task_address,
                         brush_flags: base_instance.brush_flags | segment.brush_flags,
                         ..base_instance
                     });
 
-                    if needs_blending {
-                        alpha_batch.push(instance);
-                    } else {
-                        opaque_batch.push(instance);
-                    }
+                    let batch_key = BatchKey {
+                        blend_mode: if needs_blending { alpha_blend_mode } else { BlendMode::None },
+                        kind: BatchKind::Brush(batch_kind),
+                        textures,
+                    };
+
+                    self.batch_list.push_single_instance(
+                        batch_key,
+                        bounding_rect,
+                        prim_instance.prim_index,
+                        instance,
+                    );
                 }
             }
             None => {
                 let batch_key = BatchKey {
                     blend_mode: non_segmented_blend_mode,
                     kind: BatchKind::Brush(batch_kind),
                     textures,
                 };
-                let batch = self.batch_list.get_suitable_batch(batch_key, bounding_rect);
-                batch.push(PrimitiveInstanceData::from(base_instance));
+                self.batch_list.push_single_instance(
+                    batch_key,
+                    bounding_rect,
+                    prim_instance.prim_index,
+                    PrimitiveInstanceData::from(base_instance),
+                );
             }
         }
-
-        self.batch_list.remove_unused_batches();
     }
 }
 
 fn add_gradient_tiles(
+    prim_instance: &PrimitiveInstance,
     visible_tiles: &[VisibleGradientTile],
     stops_handle: &GpuCacheHandle,
     kind: BrushBatchKind,
     blend_mode: BlendMode,
     bounding_rect: &WorldRect,
     clip_task_address: RenderTaskAddress,
     gpu_cache: &GpuCache,
     batch_list: &mut BatchList,
     base_prim_header: &PrimitiveHeader,
     prim_headers: &mut PrimitiveHeaders,
 ) {
-    let batch = batch_list.get_suitable_batch(
+    let batch = batch_list.set_params_and_get_batch(
         BatchKey {
             blend_mode: blend_mode,
             kind: BatchKind::Brush(kind),
             textures: BatchTextures::no_texture(),
         },
         bounding_rect,
+        prim_instance.prim_index,
     );
 
     let user_data = [stops_handle.as_int(gpu_cache), 0, 0];
 
     for tile in visible_tiles {
         let prim_header = PrimitiveHeader {
             specific_prim_address: gpu_cache.get_address(&tile.handle),
             local_rect: tile.local_rect,
@@ -1323,60 +1384,61 @@ fn add_gradient_tiles(
 
         batch.push(PrimitiveInstanceData::from(
             BrushInstance {
                 prim_header_index,
                 clip_task_address,
                 segment_index: 0,
                 edge_flags: EdgeAaSegmentMask::all(),
                 brush_flags: BrushFlags::PERSPECTIVE_INTERPOLATION,
+                user_data: 0,
             }
         ));
     }
 }
 
 fn get_image_tile_params(
     resource_cache: &ResourceCache,
     gpu_cache: &mut GpuCache,
     deferred_resolves: &mut Vec<DeferredResolve>,
     request: ImageRequest,
-) -> Option<(BrushBatchKind, BatchTextures, [i32; 3])> {
+) -> Option<(BrushBatchKind, BatchTextures, [i32; 3], GpuCacheAddress)> {
 
     let cache_item = resolve_image(
         request,
         resource_cache,
         gpu_cache,
         deferred_resolves,
     );
 
     if cache_item.texture_id == TextureSource::Invalid {
         None
     } else {
         let textures = BatchTextures::color(cache_item.texture_id);
         Some((
             BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
             textures,
             [
-                cache_item.uv_rect_handle.as_int(gpu_cache),
-                (ShaderColorMode::Image as i32) << 16 |
-                     RasterizationSpace::Local as i32,
+                ShaderColorMode::Image as i32,
+                RasterizationSpace::Local as i32,
                 0,
             ],
+            gpu_cache.get_address(&cache_item.uv_rect_handle),
         ))
     }
 }
 
 impl BrushPrimitive {
     fn get_batch_params(
         &self,
         resource_cache: &ResourceCache,
         gpu_cache: &mut GpuCache,
         deferred_resolves: &mut Vec<DeferredResolve>,
         is_chased: bool,
-    ) -> Option<(BrushBatchKind, BatchTextures, [i32; 3])> {
+    ) -> Option<(BrushBatchKind, BatchTextures, [i32; 3], i32)> {
         match self.kind {
             BrushKind::Image { request, ref source, .. } => {
                 let cache_item = match *source {
                     ImageSource::Default => {
                         resolve_image(
                             request,
                             resource_cache,
                             gpu_cache,
@@ -1400,49 +1462,50 @@ impl BrushPrimitive {
                     None
                 } else {
                     let textures = BatchTextures::color(cache_item.texture_id);
 
                     Some((
                         BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
                         textures,
                         [
-                            cache_item.uv_rect_handle.as_int(gpu_cache),
-                            (ShaderColorMode::Image as i32) << 16|
-                             RasterizationSpace::Local as i32,
+                            ShaderColorMode::Image as i32,
+                            RasterizationSpace::Local as i32,
                             0,
                         ],
+                        cache_item.uv_rect_handle.as_int(gpu_cache),
                     ))
                 }
             }
             BrushKind::LineDecoration { ref handle, style, .. } => {
                 match style {
                     LineStyle::Solid => {
                         Some((
                             BrushBatchKind::Solid,
                             BatchTextures::no_texture(),
                             [0; 3],
+                            0,
                         ))
                     }
                     LineStyle::Dotted |
                     LineStyle::Dashed |
                     LineStyle::Wavy => {
                         let rt_cache_entry = resource_cache
                             .get_cached_render_task(handle.as_ref().unwrap());
                         let cache_item = resource_cache.get_texture_cache_item(&rt_cache_entry.handle);
                         let textures = BatchTextures::color(cache_item.texture_id);
                         Some((
                             BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
                             textures,
                             [
-                                cache_item.uv_rect_handle.as_int(gpu_cache),
-                                (ShaderColorMode::Image as i32) << 16|
-                                 RasterizationSpace::Local as i32,
+                                ShaderColorMode::Image as i32,
+                                RasterizationSpace::Local as i32,
                                 0,
                             ],
+                            cache_item.uv_rect_handle.as_int(gpu_cache),
                         ))
                     }
                 }
             }
             BrushKind::Border { ref source, .. } => {
                 let cache_item = match *source {
                     BorderSource::Image(request) => {
                         resolve_image(
@@ -1467,61 +1530,65 @@ impl BrushPrimitive {
                     None
                 } else {
                     let textures = BatchTextures::color(cache_item.texture_id);
 
                     Some((
                         BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
                         textures,
                         [
-                            cache_item.uv_rect_handle.as_int(gpu_cache),
-                            (ShaderColorMode::Image as i32) << 16|
-                             RasterizationSpace::Local as i32,
+                            ShaderColorMode::Image as i32,
+                            RasterizationSpace::Local as i32,
                             0,
                         ],
+                        cache_item.uv_rect_handle.as_int(gpu_cache),
                     ))
                 }
             }
             BrushKind::Picture { .. } => {
                 panic!("bug: get_batch_key is handled at higher level for pictures");
             }
             BrushKind::Solid { .. } => {
                 Some((
                     BrushBatchKind::Solid,
                     BatchTextures::no_texture(),
                     [0; 3],
+                    0,
                 ))
             }
             BrushKind::Clear => {
                 Some((
                     BrushBatchKind::Solid,
                     BatchTextures::no_texture(),
                     [0; 3],
+                    0,
                 ))
             }
             BrushKind::RadialGradient { ref stops_handle, .. } => {
                 Some((
                     BrushBatchKind::RadialGradient,
                     BatchTextures::no_texture(),
                     [
                         stops_handle.as_int(gpu_cache),
                         0,
                         0,
                     ],
+                    0,
                 ))
             }
             BrushKind::LinearGradient { ref stops_handle, .. } => {
                 Some((
                     BrushBatchKind::LinearGradient,
                     BatchTextures::no_texture(),
                     [
                         stops_handle.as_int(gpu_cache),
                         0,
                         0,
                     ],
+                    0,
                 ))
             }
             BrushKind::YuvImage { format, yuv_key, image_rendering, color_depth, color_space } => {
                 let mut textures = BatchTextures::no_texture();
                 let mut uv_rect_addresses = [0; 3];
 
                 //yuv channel
                 let channel_count = format.get_plane_num();
@@ -1567,16 +1634,17 @@ impl BrushPrimitive {
                 Some((
                     kind,
                     textures,
                     [
                         uv_rect_addresses[0],
                         uv_rect_addresses[1],
                         uv_rect_addresses[2],
                     ],
+                    0,
                 ))
             }
         }
     }
 }
 
 impl Primitive {
     fn get_blend_mode(&self) -> BlendMode {
--- a/gfx/webrender/src/gpu_types.rs
+++ b/gfx/webrender/src/gpu_types.rs
@@ -326,28 +326,29 @@ bitflags! {
 //           compressed to a smaller size.
 #[repr(C)]
 pub struct BrushInstance {
     pub prim_header_index: PrimitiveHeaderIndex,
     pub clip_task_address: RenderTaskAddress,
     pub segment_index: i32,
     pub edge_flags: EdgeAaSegmentMask,
     pub brush_flags: BrushFlags,
+    pub user_data: i32,
 }
 
 impl From<BrushInstance> for PrimitiveInstanceData {
     fn from(instance: BrushInstance) -> Self {
         PrimitiveInstanceData {
             data: [
                 instance.prim_header_index.0,
                 instance.clip_task_address.0 as i32,
                 instance.segment_index |
                 ((instance.edge_flags.bits() as i32) << 16) |
                 ((instance.brush_flags.bits() as i32) << 24),
-                0,
+                instance.user_data,
             ]
         }
     }
 }
 
 // Represents the information about a transform palette
 // entry that is passed to shaders. It includes an index
 // into the transform palette, and a set of flags. The
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -2663,16 +2663,17 @@ impl Renderer {
                 }
             }
         }
 
         if self.debug_flags.contains(DebugFlags::ECHO_DRIVER_MESSAGES) {
             self.device.echo_driver_messages();
         }
 
+        stats.texture_upload_kb = self.profile_counters.texture_data_uploaded.get();
         self.backend_profile_counters.reset();
         self.profile_counters.reset();
         self.profile_counters.frame_counter.inc();
 
         profile_timers.cpu_time.profile(|| {
             let _gm = self.gpu_profile.start_marker("end frame");
             self.gpu_profile.end_frame();
             #[cfg(feature = "debug_renderer")]
@@ -4601,24 +4602,26 @@ impl DebugServer {
 // Some basic statistics about the rendered scene
 // that we can use in wrench reftests to ensure that
 // tests are batching and/or allocating on render
 // targets as we expect them to.
 pub struct RendererStats {
     pub total_draw_calls: usize,
     pub alpha_target_count: usize,
     pub color_target_count: usize,
+    pub texture_upload_kb: usize,
 }
 
 impl RendererStats {
     pub fn empty() -> Self {
         RendererStats {
             total_draw_calls: 0,
             alpha_target_count: 0,
             color_target_count: 0,
+            texture_upload_kb: 0,
         }
     }
 }
 
 
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-5adc86c19cbef6697975ea078fa0d10635e5d660
+98d507003c07c003ef0e0297dc4d29ee896a5868
--- a/gfx/wrench/Cargo.toml
+++ b/gfx/wrench/Cargo.toml
@@ -1,17 +1,17 @@
 [package]
 name = "wrench"
 version = "0.3.0"
 authors = ["Vladimir Vukicevic <vladimir@pobox.com>"]
 build = "build.rs"
 license = "MPL-2.0"
 
 [dependencies]
-base64 = "0.6"
+base64 = "0.9"
 bincode = "1.0"
 byteorder = "1.0"
 env_logger = { version = "0.5", optional = true }
 euclid = "0.19"
 gleam = "0.6.2"
 glutin = "0.17"
 app_units = "0.7"
 image = "0.19"
--- a/js/public/GCAnnotations.h
+++ b/js/public/GCAnnotations.h
@@ -7,55 +7,55 @@
 #ifndef js_GCAnnotations_h
 #define js_GCAnnotations_h
 
 // Set of annotations for the rooting hazard analysis, used to categorize types
 // and functions.
 #ifdef XGILL_PLUGIN
 
 // Mark a type as being a GC thing (eg js::gc::Cell has this annotation).
-# define JS_HAZ_GC_THING __attribute__((tag("GC Thing")))
+# define JS_HAZ_GC_THING __attribute__((annotate("GC Thing")))
 
 // Mark a type as holding a pointer to a GC thing (eg JS::Value has this
 // annotation.)
-# define JS_HAZ_GC_POINTER __attribute__((tag("GC Pointer")))
+# define JS_HAZ_GC_POINTER __attribute__((annotate("GC Pointer")))
 
 // Mark a type as a rooted pointer, suitable for use on the stack (eg all
 // Rooted<T> instantiations should have this.)
-# define JS_HAZ_ROOTED __attribute__((tag("Rooted Pointer")))
+# define JS_HAZ_ROOTED __attribute__((annotate("Rooted Pointer")))
 
 // Mark a type as something that should not be held live across a GC, but which
 // is not itself a GC pointer.
-# define JS_HAZ_GC_INVALIDATED __attribute__((tag("Invalidated by GC")))
+# define JS_HAZ_GC_INVALIDATED __attribute__((annotate("Invalidated by GC")))
 
 // Mark a class as a base class of rooted types, eg CustomAutoRooter. All
 // descendants of this class will be considered rooted, though classes that
 // merely contain these as a field member will not be. "Inherited" by
 // templatized types with MOZ_INHERIT_TYPE_ANNOTATIONS_FROM_TEMPLATE_ARGS
-# define JS_HAZ_ROOTED_BASE __attribute__((tag("Rooted Base")))
+# define JS_HAZ_ROOTED_BASE __attribute__((annotate("Rooted Base")))
 
 // Mark a type that would otherwise be considered a GC Pointer (eg because it
 // contains a JS::Value field) as a non-GC pointer. It is handled almost the
 // same in the analysis as a rooted pointer, except it will not be reported as
 // an unnecessary root if used across a GC call. This should rarely be used,
 // but makes sense for something like ErrorResult, which only contains a GC
 // pointer when it holds an exception (and it does its own rooting,
 // conditionally.)
-# define JS_HAZ_NON_GC_POINTER __attribute__((tag("Suppressed GC Pointer")))
+# define JS_HAZ_NON_GC_POINTER __attribute__((annotate("Suppressed GC Pointer")))
 
 // Mark a function as something that runs a garbage collection, potentially
 // invalidating GC pointers.
-# define JS_HAZ_GC_CALL __attribute__((tag("GC Call")))
+# define JS_HAZ_GC_CALL __attribute__((annotate("GC Call")))
 
 // Mark an RAII class as suppressing GC within its scope.
-# define JS_HAZ_GC_SUPPRESSED __attribute__((tag("Suppress GC")))
+# define JS_HAZ_GC_SUPPRESSED __attribute__((annotate("Suppress GC")))
 
 // Mark a function as one that can run script if called.  This obviously
 // subsumes JS_HAZ_GC_CALL, since anything that can run script can GC.`
-# define JS_HAZ_CAN_RUN_SCRIPT __attribute__((tag("Can run script")))
+# define JS_HAZ_CAN_RUN_SCRIPT __attribute__((annotate("Can run script")))
 
 #else
 
 # define JS_HAZ_GC_THING
 # define JS_HAZ_GC_POINTER
 # define JS_HAZ_ROOTED
 # define JS_HAZ_GC_INVALIDATED
 # define JS_HAZ_ROOTED_BASE
--- a/js/public/Utility.h
+++ b/js/public/Utility.h
@@ -67,16 +67,17 @@ enum ThreadType {
     THREAD_TYPE_PARSE,          // 4
     THREAD_TYPE_COMPRESS,       // 5
     THREAD_TYPE_GCHELPER,       // 6
     THREAD_TYPE_GCPARALLEL,     // 7
     THREAD_TYPE_PROMISE_TASK,   // 8
     THREAD_TYPE_ION_FREE,       // 9
     THREAD_TYPE_WASM_TIER2,     // 10
     THREAD_TYPE_WORKER,         // 11
+    THREAD_TYPE_CURRENT,        // 12 -- Special code to only track the current thread.
     THREAD_TYPE_MAX             // Used to check shell function arguments
 };
 
 namespace oom {
 
 /*
  * Theads are tagged only in certain debug contexts.  Notably, to make testing
  * OOM in certain helper threads more effective, we allow restricting the OOM
@@ -87,26 +88,34 @@ namespace oom {
  * is in jsutil.cpp.
  */
 # if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
 
 // Define the range of threads tested by simulated OOM testing and the
 // like. Testing worker threads is not supported.
 const ThreadType FirstThreadTypeToTest = THREAD_TYPE_MAIN;
 const ThreadType LastThreadTypeToTest = THREAD_TYPE_WASM_TIER2;
+const ThreadType WorkerFirstThreadTypeToTest = THREAD_TYPE_CURRENT;
+const ThreadType WorkerLastThreadTypeToTest = THREAD_TYPE_CURRENT;
 
 extern bool InitThreadType(void);
 extern void SetThreadType(ThreadType);
 extern JS_FRIEND_API(uint32_t) GetThreadType(void);
+extern JS_FRIEND_API(uint32_t) GetAllocationThreadType(void);
+extern JS_FRIEND_API(uint32_t) GetStackCheckThreadType(void);
+extern JS_FRIEND_API(uint32_t) GetInterruptCheckThreadType(void);
 
 # else
 
 inline bool InitThreadType(void) { return true; }
 inline void SetThreadType(ThreadType t) {};
 inline uint32_t GetThreadType(void) { return 0; }
+inline uint32_t GetAllocationThreadType(void) { return 0; }
+inline uint32_t GetStackCheckThreadType(void) { return 0; }
+inline uint32_t GetInterruptCheckThreadType(void) { return 0; }
 
 # endif
 
 } /* namespace oom */
 } /* namespace js */
 
 # if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
 
@@ -138,17 +147,18 @@ extern void
 SimulateOOMAfter(uint64_t allocations, uint32_t thread, bool always);
 
 extern void
 ResetSimulatedOOM();
 
 inline bool
 IsThreadSimulatingOOM()
 {
-    return js::oom::targetThread && js::oom::targetThread == js::oom::GetThreadType();
+    return js::oom::targetThread &&
+        js::oom::targetThread == js::oom::GetAllocationThreadType();
 }
 
 inline bool
 IsSimulatedOOMAllocation()
 {
     return IsThreadSimulatingOOM() &&
            (counter == maxAllocations || (counter > maxAllocations && failAlways));
 }
@@ -186,17 +196,18 @@ extern void
 SimulateStackOOMAfter(uint64_t checks, uint32_t thread, bool always);
 
 extern void
 ResetSimulatedStackOOM();
 
 inline bool
 IsThreadSimulatingStackOOM()
 {
-    return js::oom::stackTargetThread && js::oom::stackTargetThread == js::oom::GetThreadType();
+    return js::oom::stackTargetThread &&
+        js::oom::stackTargetThread == js::oom::GetStackCheckThreadType();
 }
 
 inline bool
 IsSimulatedStackOOMCheck()
 {
     return IsThreadSimulatingStackOOM() &&
            (stackCheckCounter == maxStackChecks || (stackCheckCounter > maxStackChecks && stackCheckFailAlways));
 }
@@ -235,17 +246,18 @@ extern void
 SimulateInterruptAfter(uint64_t checks, uint32_t thread, bool always);
 
 extern void
 ResetSimulatedInterrupt();
 
 inline bool
 IsThreadSimulatingInterrupt()
 {
-    return js::oom::interruptTargetThread && js::oom::interruptTargetThread == js::oom::GetThreadType();
+    return js::oom::interruptTargetThread &&
+        js::oom::interruptTargetThread == js::oom::GetInterruptCheckThreadType();
 }
 
 inline bool
 IsSimulatedInterruptCheck()
 {
     return IsThreadSimulatingInterrupt() &&
            (interruptCheckCounter == maxInterruptChecks || (interruptCheckCounter > maxInterruptChecks && interruptCheckFailAlways));
 }
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1889,17 +1889,17 @@ RunIterativeFailureTest(JSContext* cx, c
 #endif
 
     size_t compartmentCount = CountCompartments(cx);
 
     RootedValue exception(cx);
 
     simulator.setup(cx);
 
-    for (unsigned thread = params.threadStart; thread < params.threadEnd; thread++) {
+    for (unsigned thread = params.threadStart; thread <= params.threadEnd; thread++) {
         if (params.verbose) {
             fprintf(stderr, "thread %d\n", thread);
         }
 
         unsigned iteration = 1;
         bool failureWasSimulated;
         do {
             if (params.verbose) {
@@ -2030,30 +2030,39 @@ ParseIterativeFailureTestParams(JSContex
     }
 
     // There are some places where we do fail without raising an exception, so
     // we can't expose this to the fuzzers by default.
     if (fuzzingSafe) {
         params->expectExceptionOnFailure = false;
     }
 
-    // Test all threads by default.
-    params->threadStart = oom::FirstThreadTypeToTest;
-    params->threadEnd = oom::LastThreadTypeToTest;
+    // Test all threads by default except worker threads, except if we are
+    // running in a worker thread in which case only the worker thread which
+    // requested the simulation is tested.
+    if (js::oom::GetThreadType() == js::THREAD_TYPE_WORKER) {
+        params->threadStart = oom::WorkerFirstThreadTypeToTest;
+        params->threadEnd = oom::WorkerLastThreadTypeToTest;
+    } else {
+        params->threadStart = oom::FirstThreadTypeToTest;
+        params->threadEnd = oom::LastThreadTypeToTest;
+    }
 
     // Test a single thread type if specified by the OOM_THREAD environment variable.
     int threadOption = 0;
     if (EnvVarAsInt("OOM_THREAD", &threadOption)) {
-        if (threadOption < oom::FirstThreadTypeToTest || threadOption > oom::LastThreadTypeToTest) {
+        if (threadOption < oom::FirstThreadTypeToTest || threadOption > oom::LastThreadTypeToTest ||
+            threadOption != js::THREAD_TYPE_CURRENT)
+        {
             JS_ReportErrorASCII(cx, "OOM_THREAD value out of range.");
             return false;
         }
 
         params->threadStart = threadOption;
-        params->threadEnd = threadOption + 1;
+        params->threadEnd = threadOption;
     }
 
     params->verbose = EnvVarIsDefined("OOM_VERBOSE");
 
     return true;
 }
 
 struct OOMSimulator : public IterativeFailureSimulator
--- a/js/src/devtools/rootAnalysis/CFG.js
+++ b/js/src/devtools/rootAnalysis/CFG.js
@@ -1,33 +1,33 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
 
 "use strict";
 
 var functionBodies;
 
-function findAllPoints(bodies, blockId)
+function findAllPoints(bodies, blockId, limits)
 {
     var points = [];
     var body;
 
     for (var xbody of bodies) {
         if (sameBlockId(xbody.BlockId, blockId)) {
             assert(!body);
             body = xbody;
         }
     }
     assert(body);
 
     if (!("PEdge" in body))
         return;
     for (var edge of body.PEdge) {
-        points.push([body, edge.Index[0]]);
+        points.push([body, edge.Index[0], limits]);
         if (edge.Kind == "Loop")
-            Array.prototype.push.apply(points, findAllPoints(bodies, edge.BlockId));
+            points.push(...findAllPoints(bodies, edge.BlockId, limits));
     }
 
     return points;
 }
 
 function isMatchingDestructor(constructor, edge)
 {
     if (edge.Kind != "Call")
@@ -73,24 +73,25 @@ function allRAIIGuardedCallPoints(typeIn
     for (var edge of body.PEdge) {
         if (edge.Kind != "Call")
             continue;
         var callee = edge.Exp[0];
         if (callee.Kind != "Var")
             continue;
         var variable = callee.Variable;
         assert(variable.Kind == "Func");
-        if (!isConstructor(typeInfo, edge.Type, variable.Name))
+        const limits = isConstructor(typeInfo, edge.Type, variable.Name);
+        if (!limits)
             continue;
         if (!("PEdgeCallInstance" in edge))
             continue;
         if (edge.PEdgeCallInstance.Exp.Kind != "Var")
             continue;
 
-        Array.prototype.push.apply(points, pointsInRAIIScope(bodies, body, edge));
+        points.push(...pointsInRAIIScope(bodies, body, edge, limits));
     }
 
     return points;
 }
 
 // Test whether the given edge is the constructor corresponding to the given
 // destructor edge
 function isMatchingConstructor(destructor, edge)
@@ -136,32 +137,32 @@ function findMatchingConstructor(destruc
             for (var e of predecessors[edge.Index[0]])
                 worklist.push(e);
         }
     }
     printErr("Could not find matching constructor!");
     debugger;
 }
 
-function pointsInRAIIScope(bodies, body, constructorEdge) {
+function pointsInRAIIScope(bodies, body, constructorEdge, limits) {
     var seen = {};
     var worklist = [constructorEdge.Index[1]];
     var points = [];
     while (worklist.length) {
         var point = worklist.pop();
         if (point in seen)
             continue;
         seen[point] = true;
-        points.push([body, point]);
+        points.push([body, point, limits]);
         var successors = getSuccessors(body);
         if (!(point in successors))
             continue;
         for (var nedge of successors[point]) {
             if (isMatchingDestructor(constructorEdge, nedge))
                 continue;
             if (nedge.Kind == "Loop")
-                Array.prototype.push.apply(points, findAllPoints(bodies, nedge.BlockId));
+                points.push(...findAllPoints(bodies, nedge.BlockId, limits));
             worklist.push(nedge.Index[1]);
         }
     }
 
     return points;
 }
--- a/js/src/devtools/rootAnalysis/analyze.py
+++ b/js/src/devtools/rootAnalysis/analyze.py
@@ -83,17 +83,17 @@ def print_command(command, outfile=None,
 
 def generate_hazards(config, outfilename):
     jobs = []
     for i in range(int(config['jobs'])):
         command = fill(('%(js)s',
                         '%(analysis_scriptdir)s/analyzeRoots.js',
                         '%(gcFunctions_list)s',
                         '%(gcEdges)s',
-                        '%(suppressedFunctions_list)s',
+                        '%(limitedFunctions_list)s',
                         '%(gcTypes)s',
                         '%(typeInfo)s',
                         str(i+1), '%(jobs)s',
                         'tmp.%s' % (i+1,)),
                        config)
         outfile = 'rootingHazards.%s' % (i+1,)
         output = open(outfile, 'w')
         if config['verbose']:
@@ -128,23 +128,24 @@ JOBS = {'dbs':
           '.'),
          ()),
 
         'list-dbs':
         (('ls', '-l'),
          ()),
 
         'callgraph':
-        (('%(js)s', '%(analysis_scriptdir)s/computeCallgraph.js', '%(typeInfo)s'),
-         'callgraph.txt'),
+        (('%(js)s', '%(analysis_scriptdir)s/computeCallgraph.js', '%(typeInfo)s',
+          '[callgraph]'),
+         ('callgraph.txt',)),
 
         'gcFunctions':
         (('%(js)s', '%(analysis_scriptdir)s/computeGCFunctions.js', '%(callgraph)s',
-          '[gcFunctions]', '[gcFunctions_list]', '[gcEdges]', '[suppressedFunctions_list]'),
-         ('gcFunctions.txt', 'gcFunctions.lst', 'gcEdges.txt', 'suppressedFunctions.lst')),
+          '[gcFunctions]', '[gcFunctions_list]', '[gcEdges]', '[limitedFunctions_list]'),
+         ('gcFunctions.txt', 'gcFunctions.lst', 'gcEdges.txt', 'limitedFunctions.lst')),
 
         'gcTypes':
         (('%(js)s', '%(analysis_scriptdir)s/computeGCTypes.js',
           '[gcTypes]', '[typeInfo]'),
          ('gcTypes.txt', 'typeInfo.txt')),
 
         'allFunctions':
         (('%(sixgill_bin)s/xdbkeys', 'src_body.xdb',),
--- a/js/src/devtools/rootAnalysis/analyzeRoots.js
+++ b/js/src/devtools/rootAnalysis/analyzeRoots.js
@@ -8,44 +8,46 @@ loadRelativeToScript('CFG.js');
 loadRelativeToScript('dumpCFG.js');
 
 var sourceRoot = (os.getenv('SOURCE') || '') + '/'
 
 var functionName;
 var functionBodies;
 
 if (typeof scriptArgs[0] != 'string' || typeof scriptArgs[1] != 'string')
-    throw "Usage: analyzeRoots.js [-f function_name] <gcFunctions.lst> <gcEdges.txt> <suppressedFunctions.lst> <gcTypes.txt> <typeInfo.txt> [start end [tmpfile]]";
+    throw "Usage: analyzeRoots.js [-f function_name] <gcFunctions.lst> <gcEdges.txt> <limitedFunctions.lst> <gcTypes.txt> <typeInfo.txt> [start end [tmpfile]]";
 
 var theFunctionNameToFind;
 if (scriptArgs[0] == '--function' || scriptArgs[0] == '-f') {
     theFunctionNameToFind = scriptArgs[1];
     scriptArgs = scriptArgs.slice(2);
 }
 
 var gcFunctionsFile = scriptArgs[0] || "gcFunctions.lst";
 var gcEdgesFile = scriptArgs[1] || "gcEdges.txt";
-var suppressedFunctionsFile = scriptArgs[2] || "suppressedFunctions.lst";
+var limitedFunctionsFile = scriptArgs[2] || "limitedFunctions.lst";
 var gcTypesFile = scriptArgs[3] || "gcTypes.txt";
 var typeInfoFile = scriptArgs[4] || "typeInfo.txt";
 var batch = (scriptArgs[5]|0) || 1;
 var numBatches = (scriptArgs[6]|0) || 1;
 var tmpfile = scriptArgs[7] || "tmp.txt";
 
 var gcFunctions = {};
 var text = snarf("gcFunctions.lst").split("\n");
 assert(text.pop().length == 0);
 for (var line of text)
     gcFunctions[mangled(line)] = true;
 
-var suppressedFunctions = {};
-var text = snarf(suppressedFunctionsFile).split("\n");
+var limitedFunctions = {};
+var text = snarf(limitedFunctionsFile).split("\n");
 assert(text.pop().length == 0);
 for (var line of text) {
-    suppressedFunctions[line] = true;
+    const [_, limits, func] = line.match(/(.*?) (.*)/);
+    assert(limits !== undefined);
+    limitedFunctions[func] = limits | 0;
 }
 text = null;
 
 var typeInfo = loadTypeInfo(typeInfoFile);
 
 var gcEdges = {};
 text = snarf(gcEdgesFile).split('\n');
 assert(text.pop().length == 0);
@@ -348,26 +350,30 @@ function edgeCanGC(edge)
         if (variable.Kind == "Func") {
             var callee = mangled(variable.Name[0]);
             if ((callee in gcFunctions) || ((callee + internalMarker) in gcFunctions))
                 return "'" + variable.Name[0] + "'";
             return null;
         }
 
         var varName = variable.Name[0];
-        return indirectCallCannotGC(functionName, varName) ? null : "*" + varName;
+        return indirectCallCannotGC(functionName, varName) ? null : "'*" + varName + "'";
     }
 
     if (callee.Kind == "Fld") {
         var field = callee.Field;
         var csuName = field.FieldCSU.Type.Name;
         var fullFieldName = csuName + "." + field.Name[0];
         if (fieldCallCannotGC(csuName, fullFieldName))
             return null;
-        return (fullFieldName in suppressedFunctions) ? null : fullFieldName;
+
+        if (fullFieldName in gcFunctions)
+            return "'" + fullFieldName + "'";
+
+        return null;
     }
 }
 
 // Search recursively through predecessors from the use of a variable's value,
 // returning whether a GC call is reachable (in the reverse direction; this
 // means that the variable use is reachable from the GC call, and therefore the
 // variable is live after the GC call), along with some additional information.
 // What info we want depends on whether the variable turns out to be live
@@ -496,17 +502,17 @@ function findGCBeforeValueUse(start_body
 
                 // Otherwise, keep searching through the graph, but truncate
                 // this particular branch of the search at this edge.
                 continue;
             }
 
             var src_gcInfo = gcInfo;
             var src_preGCLive = preGCLive;
-            if (!gcInfo && !(source in body.suppressed) && !suppressed) {
+            if (!gcInfo && !(body.limits[source] & LIMIT_CANNOT_GC) && !suppressed) {
                 var gcName = edgeCanGC(edge, body);
                 if (gcName)
                     src_gcInfo = {name:gcName, body:body, ppoint:source};
             }
 
             if (edge_uses) {
                 // The live range starts at least this far back, so we're done
                 // for the same reason as with edge_kills. The only difference
@@ -778,17 +784,17 @@ function typeDesc(type)
         return '???';
     }
 }
 
 function processBodies(functionName)
 {
     if (!("DefineVariable" in functionBodies[0]))
         return;
-    var suppressed = (mangled(functionName) in suppressedFunctions);
+    var suppressed = Boolean(limitedFunctions[mangled(functionName)] & LIMIT_CANNOT_GC);
     for (var variable of functionBodies[0].DefineVariable) {
         var name;
         if (variable.Variable.Kind == "This")
             name = "this";
         else if (variable.Variable.Kind == "Return")
             name = "<returnvalue>";
         else
             name = variable.Variable.Name[0];
@@ -843,21 +849,29 @@ var N = (maxStream - minStream) + 1;
 var start = Math.floor((batch - 1) / numBatches * N) + minStream;
 var start_next = Math.floor(batch / numBatches * N) + minStream;
 var end = start_next - 1;
 
 function process(name, json) {
     functionName = name;
     functionBodies = JSON.parse(json);
 
+    // Annotate body with a table of all points within the body that may be in
+    // a limited scope (eg within the scope of a GC suppression RAII class.)
+    // body.limits is a plain object indexed by point, with the value being a
+    // bit set stored in an integer of the limit bits.
     for (var body of functionBodies)
-        body.suppressed = [];
+        body.limits = [];
+
     for (var body of functionBodies) {
-        for (var [pbody, id] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isSuppressConstructor))
-            pbody.suppressed[id] = true;
+        for (var [pbody, id, limits] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isLimitConstructor))
+        {
+            if (limits)
+                pbody.limits[id] = limits;
+        }
     }
     processBodies(functionName);
 }
 
 if (theFunctionNameToFind) {
     var data = xdb.read_entry(theFunctionNameToFind);
     var json = data.readString();
     process(theFunctionNameToFind, json);
--- a/js/src/devtools/rootAnalysis/annotations.js
+++ b/js/src/devtools/rootAnalysis/annotations.js
@@ -258,18 +258,21 @@ function isHeapSnapshotMockClass(name)
 
 function isGTest(name)
 {
     return name.match(/\btesting::/);
 }
 
 function ignoreGCFunction(mangled)
 {
-    assert(mangled in readableNames, mangled + " not in readableNames");
-    var fun = readableNames[mangled][0];
+    // Field calls will not be in readableNames
+    if (!(mangled in readableNames))
+        return false;
+
+    const fun = readableNames[mangled][0];
 
     if (fun in ignoreFunctions)
         return true;
 
     // The protobuf library, and [de]serialization code generated by the
     // protobuf compiler, uses a _ton_ of function pointers but they are all
     // internal. Easiest to just ignore that mess here.
     if (isProtobuf(fun))
@@ -335,41 +338,47 @@ function isRootedGCPointerTypeName(name)
 }
 
 function isUnsafeStorage(typeName)
 {
     typeName = stripUCSAndNamespace(typeName);
     return typeName.startsWith('UniquePtr<');
 }
 
-function isSuppressConstructor(typeInfo, edgeType, varName)
+// If edgeType is a constructor type, return whatever limits it implies for its
+// scope (or zero if not matching).
+function isLimitConstructor(typeInfo, edgeType, varName)
 {
     // Check whether this could be a constructor
     if (edgeType.Kind != 'Function')
-        return false;
+        return 0;
     if (!('TypeFunctionCSU' in edgeType))
-        return false;
+        return 0;
     if (edgeType.Type.Kind != 'Void')
-        return false;
+        return 0;
 
     // Check whether the type is a known suppression type.
     var type = edgeType.TypeFunctionCSU.Type.Name;
-    if (!(type in typeInfo.GCSuppressors))
-        return false;
+    let limit = 0;
+    if (type in typeInfo.GCSuppressors)
+        limit = limit | LIMIT_CANNOT_GC;
 
     // And now make sure this is the constructor, not some other method on a
     // suppression type. varName[0] contains the qualified name.
     var [ mangled, unmangled ] = splitFunction(varName[0]);
-    if (mangled.search(/C\dE/) == -1)
-        return false; // Mangled names of constructors have C<num>E
+    if (mangled.search(/C\d[EI]/) == -1)
+        return 0; // Mangled names of constructors have C<num>E or C<num>I
     var m = unmangled.match(/([~\w]+)(?:<.*>)?\(/);
     if (!m)
-        return false;
+        return 0;
     var type_stem = type.replace(/\w+::/g, '').replace(/\<.*\>/g, '');
-    return m[1] == type_stem;
+    if (m[1] != type_stem)
+        return 0;
+
+    return limit;
 }
 
 // nsISupports subclasses' methods may be scriptable (or overridden
 // via binary XPCOM), and so may GC. But some fields just aren't going
 // to get overridden with something that can GC.
 function isOverridableField(initialCSU, csu, field)
 {
     if (csu != 'nsISupports')
@@ -384,16 +393,18 @@ function isOverridableField(initialCSU, 
     if (field == 'GetNativeContext')
         return false;
     if (field == "GetGlobalJSObject")
         return false;
     if (field == "GetIsMainThread")
         return false;
     if (field == "GetThreadFromPRThread")
         return false;
+    if (field == "DocAddSizeOfIncludingThis")
+        return false;
     if (field == "ConstructUbiNode")
         return false;
     if (initialCSU == 'nsIXPCScriptable' && field == "GetScriptableFlags")
         return false;
     if (initialCSU == 'nsIXPConnectJSObjectHolder' && field == 'GetJSObject')
         return false;
     if (initialCSU == 'nsIXPConnect' && field == 'GetSafeJSContext')
         return false;
--- a/js/src/devtools/rootAnalysis/callgraph.js
+++ b/js/src/devtools/rootAnalysis/callgraph.js
@@ -13,20 +13,22 @@ function addToNamedSet(map, name, entry)
 {
     if (!map.has(name))
         map.set(name, new Set());
     map.get(name).add(entry);
 }
 
 function fieldKey(csuName, field)
 {
-    // Note: not dealing with overloading correctly.
+    // This makes a minimal attempt at dealing with overloading: it will not
+    // conflate two virtual methods with differing numbers of arguments. So
+    // far, that is all that has been needed.
     var nargs = 0;
     if (field.Type.Kind == "Function" && "TypeFunctionArguments" in field.Type)
-	nargs = field.Type.TypeFunctionArguments.length;
+        nargs = field.Type.TypeFunctionArguments.Type.length;
     return csuName + ":" + field.Name[0] + ":" + nargs;
 }
 
 // CSU is "Class/Struct/Union"
 function processCSU(csuName, csu)
 {
     if (!("FunctionField" in csu))
         return;
@@ -59,20 +61,21 @@ function nearestAncestorMethods(csu, fie
     if (superclasses.has(csu)) {
         for (const parent of superclasses.get(csu))
             functions.update(nearestAncestorMethods(parent, field));
     }
 
     return functions;
 }
 
-// Return [ instantations, suppressed ], where instantiations is a Set of all
+// Return [ instantiations, limits ], where instantiations is a Set of all
 // possible implementations of 'field' given static type 'initialCSU', plus
-// null if arbitrary other implementations are possible, and suppressed is true
-// if we the method is assumed to be non-GC'ing by annotation.
+// null if arbitrary other implementations are possible, and limits gives
+// information about what things are not possible within it (currently, that it
+// cannot GC).
 function findVirtualFunctions(initialCSU, field)
 {
     const fieldName = field.Name[0];
     const worklist = [initialCSU];
     const functions = new Set();
 
     // Loop through all methods of initialCSU (by looking at all methods of ancestor csus).
     //
@@ -82,17 +85,17 @@ function findVirtualFunctions(initialCSU
     // If this is a method that is annotated to be dangerous (eg, it could be
     // overridden with an implementation that could GC), then use null as a
     // signal value that it should be considered to GC, even though we'll also
     // collect all of the instantiations for other purposes.
 
     while (worklist.length) {
         const csu = worklist.pop();
         if (isSuppressedVirtualMethod(csu, fieldName))
-            return [ new Set(), true ];
+            return [ new Set(), LIMIT_CANNOT_GC ];
         if (isOverridableField(initialCSU, csu, fieldName)) {
             // We will still resolve the virtual function call, because it's
             // nice to have as complete a callgraph as possible for other uses.
             // But push a token saying that we can run arbitrary code.
             functions.add(null);
         }
 
         if (superclasses.has(csu))
@@ -115,17 +118,17 @@ function findVirtualFunctions(initialCSU
 
         if (classFunctions.has(key))
             functions.update(classFunctions.get(key));
 
         if (subclasses.has(csu))
             worklist.push(...subclasses.get(csu));
     }
 
-    return [ functions, false ];
+    return [ functions, LIMIT_NONE ];
 }
 
 // Return a list of all callees that the given edge might be a call to. Each
 // one is represented by an object with a 'kind' field that is one of
 // ('direct', 'field', 'resolved-field', 'indirect', 'unknown'), though note
 // that 'resolved-field' is really a global record of virtual method
 // resolutions, indepedent of this particular edge.
 function getCallees(edge)
@@ -136,17 +139,17 @@ function getCallees(edge)
     const callee = edge.Exp[0];
     if (callee.Kind == "Var") {
         assert(callee.Variable.Kind == "Func");
         return [{'kind': 'direct', 'name': callee.Variable.Name[0]}];
     }
 
     if (callee.Kind == "Int")
         return []; // Intentional crash
-  
+
     assert(callee.Kind == "Drf");
     const called = callee.Exp[0];
     if (called.Kind == "Var") {
         // indirect call through a variable.
         return [{'kind': "indirect", 'variable': callee.Exp[0].Variable.Name[0]}];
     }
 
     if (called.Kind != "Fld") {
@@ -154,25 +157,21 @@ function getCallees(edge)
         return [{'kind': "unknown"}];
     }
 
     const callees = [];
     const field = callee.Exp[0].Field;
     const fieldName = field.Name[0];
     const csuName = field.FieldCSU.Type.Name;
     let functions;
+    let limits = LIMIT_NONE;
     if ("FieldInstanceFunction" in field) {
-        let suppressed;
-        [ functions, suppressed ] = findVirtualFunctions(csuName, field, suppressed);
-        if (suppressed) {
-            // Field call known to not GC; mark it as suppressed so direct
-            // invocations will be ignored
-            callees.push({'kind': "field", 'csu': csuName, 'field': fieldName,
-                          'suppressed': true, 'isVirtual': true});
-        }
+        [ functions, limits ] = findVirtualFunctions(csuName, field);
+        callees.push({'kind': "field", 'csu': csuName, 'field': fieldName,
+                      'limits': limits, 'isVirtual': true});
     } else {
         functions = new Set([null]); // field call
     }
 
     // Known set of virtual call targets. Treat them as direct calls to all
     // possible resolved types, but also record edges from this field call to
     // each final callee. When the analysis is checking whether an edge can GC
     // and it sees an unrooted pointer held live across this field call, it
@@ -181,21 +180,21 @@ function getCallees(edge)
     let fullyResolved = true;
     for (const name of functions) {
         if (name === null) {
             // Unknown set of call targets, meaning either a function pointer
             // call ("field call") or a virtual method that can be overridden
             // in extensions. Use the isVirtual property so that callers can
             // tell which case holds.
             callees.push({'kind': "field", 'csu': csuName, 'field': fieldName,
+                          'limits': limits,
 			  'isVirtual': "FieldInstanceFunction" in field});
             fullyResolved = false;
         } else {
-            callees.push({'kind': "direct", 'name': name});
-            targets.push({'kind': "direct", 'name': name});
+            targets.push({'kind': "direct", name, limits });
         }
     }
     if (fullyResolved)
         callees.push({'kind': "resolved-field", 'csu': csuName, 'field': fieldName, 'callees': targets});
 
     return callees;
 }
 
--- a/js/src/devtools/rootAnalysis/computeCallgraph.js
+++ b/js/src/devtools/rootAnalysis/computeCallgraph.js
@@ -6,120 +6,163 @@ loadRelativeToScript('callgraph.js');
 
 var theFunctionNameToFind;
 if (scriptArgs[0] == '--function') {
     theFunctionNameToFind = scriptArgs[1];
     scriptArgs = scriptArgs.slice(2);
 }
 
 var typeInfo_filename = scriptArgs[0] || "typeInfo.txt";
+var callgraphOut_filename = scriptArgs[1] || "callgraph.txt";
+
+var origOut = os.file.redirect(callgraphOut_filename);
 
 var memoized = new Map();
 var memoizedCount = 0;
 
-function memo(name)
+var unmangled2id = new Set();
+
+function getId(name)
 {
-    if (!memoized.has(name)) {
-        let id = memoized.size + 1;
-        memoized.set(name, "" + id);
-        print(`#${id} ${name}`);
-    }
-    return memoized.get(name);
+    let id = memoized.get(name);
+    if (id !== undefined)
+        return id;
+
+    id = memoized.size + 1;
+    memoized.set(name, id);
+    print(`#${id} ${name}`);
+
+    return id;
+}
+
+function functionId(name)
+{
+    const [mangled, unmangled] = splitFunction(name);
+    const id = getId(mangled);
+
+    // Only produce a mangled -> unmangled mapping once, unless there are
+    // multiple unmangled names for the same mangled name.
+    if (unmangled2id.has(unmangled))
+        return id;
+
+    print(`= ${id} ${unmangled}`);
+    unmangled2id.add(unmangled);
+    return id;
 }
 
 var lastline;
 function printOnce(line)
 {
     if (line != lastline) {
         print(line);
         lastline = line;
     }
 }
 
-// Returns a table mapping function name to lists of [annotation-name,
-// annotation-value] pairs: { function-name => [ [annotation-name, annotation-value] ] }
-function getAnnotations(body)
+// Returns a table mapping function name to lists of
+// [annotation-name, annotation-value] pairs:
+//   { function-name => [ [annotation-name, annotation-value] ] }
+//
+// Note that sixgill will only store certain attributes (annotation-names), so
+// this won't be *all* the attributes in the source, just the ones that sixgill
+// watches for.
+function getAllAttributes(body)
 {
     var all_annotations = {};
     for (var v of (body.DefineVariable || [])) {
         if (v.Variable.Kind != 'Func')
             continue;
         var name = v.Variable.Name[0];
         var annotations = all_annotations[name] = [];
 
         for (var ann of (v.Type.Annotation || [])) {
             annotations.push(ann.Name);
         }
     }
 
     return all_annotations;
 }
 
-function getTags(functionName, body) {
+// Get just the annotations understood by the hazard analysis.
+function getAnnotations(functionName, body) {
     var tags = new Set();
-    var annotations = getAnnotations(body);
-    if (functionName in annotations) {
-        for (var [ annName, annValue ] of annotations[functionName]) {
-            if (annName == 'Tag')
+    var attributes = getAllAttributes(body);
+    if (functionName in attributes) {
+        for (var [ annName, annValue ] of attributes[functionName]) {
+            if (annName == 'annotate')
                 tags.add(annValue);
         }
     }
     return tags;
 }
 
+// Scan through a function body, pulling out all annotations and calls and
+// recording them in callgraph.txt.
 function processBody(functionName, body)
 {
     if (!('PEdge' in body))
         return;
 
-    for (var tag of getTags(functionName, body).values())
-        print("T " + memo(functionName) + " " + tag);
+
+    for (var tag of getAnnotations(functionName, body).values())
+        print("T " + functionId(functionName) + " " + tag);
 
     // Set of all callees that have been output so far, in order to suppress
-    // repeated callgraph edges from being recorded. Use a separate set for
-    // suppressed callees, since we don't want a suppressed edge (within one
-    // RAII scope) to prevent an unsuppressed edge from being recorded. The
-    // seen array is indexed by a boolean 'suppressed' variable.
-    var seen = [ new Set(), new Set() ];
+    // repeated callgraph edges from being recorded. This uses a Map from
+    // callees to limit sets, because we don't want a limited edge to prevent
+    // an unlimited edge from being recorded later. (So an edge will be skipped
+    // if it exists and is at least as limited as the previously seen edge.)
+    //
+    // Limit sets are implemented as integers interpreted as bitfields.
+    //
+    var seen = new Map();
 
     lastline = null;
     for (var edge of body.PEdge) {
         if (edge.Kind != "Call")
             continue;
 
-        // Whether this call is within the RAII scope of a GC suppression class
-        var edgeSuppressed = (edge.Index[0] in body.suppressed);
+        // The limits (eg LIMIT_CANNOT_GC) are determined by whatever RAII
+        // scopes might be active, which have been computed previously for all
+        // points in the body.
+        var edgeLimited = body.limits[edge.Index[0]] | 0;
 
         for (var callee of getCallees(edge)) {
-            var suppressed = Boolean(edgeSuppressed || callee.suppressed);
-            var prologue = suppressed ? "SUPPRESS_GC " : "";
-            prologue += memo(functionName) + " ";
+            // Individual callees may have additional limits. The only such
+            // limit currently is that nsISupports.{AddRef,Release} are assumed
+            // to never GC.
+            const limits = edgeLimited | callee.limits;
+            let prologue = limits ? `/${limits} ` : "";
+            prologue += functionId(functionName) + " ";
             if (callee.kind == 'direct') {
-                if (!seen[+suppressed].has(callee.name)) {
-                    seen[+suppressed].add(callee.name);
-                    printOnce("D " + prologue + memo(callee.name));
+                const prev_limits = seen.has(callee.name) ? seen.get(callee.name) : LIMIT_UNVISITED;
+                if (prev_limits & ~limits) {
+                    // Only output an edge if it loosens a limit.
+                    seen.set(callee.name, prev_limits & limits);
+                    printOnce("D " + prologue + functionId(callee.name));
                 }
             } else if (callee.kind == 'field') {
                 var { csu, field, isVirtual } = callee;
                 const tag = isVirtual ? 'V' : 'F';
-                printOnce(tag + " " + prologue + "CLASS " + csu + " FIELD " + field);
+                const fullfield = `${csu}.${field}`;
+                printOnce(`${tag} ${prologue}${getId(fullfield)} CLASS ${csu} FIELD ${field}`);
             } else if (callee.kind == 'resolved-field') {
                 // Fully-resolved field (virtual method) call. Record the
-                // callgraph edges. Do not consider suppression, since it is
+                // callgraph edges. Do not consider limits, since they are
                 // local to this callsite and we are writing out a global
                 // record here.
                 //
                 // Any field call that does *not* have an R entry must be
                 // assumed to call anything.
                 var { csu, field, callees } = callee;
                 var fullFieldName = csu + "." + field;
                 if (!virtualResolutionsSeen.has(fullFieldName)) {
                     virtualResolutionsSeen.add(fullFieldName);
                     for (var target of callees)
-                        printOnce("R " + memo(fullFieldName) + " " + memo(target.name));
+                        printOnce("R " + getId(fullFieldName) + " " + functionId(target.name));
                 }
             } else if (callee.kind == 'indirect') {
                 printOnce("I " + prologue + "VARIABLE " + callee.variable);
             } else if (callee.kind == 'unknown') {
                 printOnce("I " + prologue + "VARIABLE UNKNOWN");
             } else {
                 printErr("invalid " + callee.kind + " callee");
                 debugger;
@@ -147,21 +190,22 @@ if (theFunctionNameToFind) {
         quit(1);
     }
     minStream = maxStream = index;
 }
 
 function process(functionName, functionBodies)
 {
     for (var body of functionBodies)
-        body.suppressed = [];
+        body.limits = [];
 
     for (var body of functionBodies) {
-        for (var [pbody, id] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isSuppressConstructor))
-            pbody.suppressed[id] = true;
+        for (var [pbody, id, limits] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isLimitConstructor)) {
+            pbody.limits[id] = limits;
+        }
     }
 
     for (var body of functionBodies)
         processBody(functionName, body);
 
     // GCC generates multiple constructors and destructors ("in-charge" and
     // "not-in-charge") to handle virtual base classes. They are normally
     // identical, and it appears that GCC does some magic to alias them to the
@@ -170,17 +214,17 @@ function process(functionName, functionB
     // will show up as called but only "foo *INTERNAL* " will be emitted in the
     // case where the constructors are identical.
     //
     // This is slightly conservative in the case where they are *not*
     // identical, but that should be rare enough that we don't care.
     var markerPos = functionName.indexOf(internalMarker);
     if (markerPos > 0) {
         var inChargeXTor = functionName.replace(internalMarker, "");
-        print("D " + memo(inChargeXTor) + " " + memo(functionName));
+        printOnce("D " + functionId(inChargeXTor) + " " + functionId(functionName));
     }
 
     // Further note: from https://itanium-cxx-abi.github.io/cxx-abi/abi.html the
     // different kinds of constructors/destructors are:
     // C1	# complete object constructor
     // C2	# base object constructor
     // C3	# complete object allocating constructor
     // D0	# deleting destructor
@@ -216,30 +260,30 @@ function process(functionName, functionB
         // E terminates the method name (and precedes the method parameters).
         // If eg "C4E" shows up in the mangled name for another reason, this
         // will create bogus edges in the callgraph. But it will affect little
         // and is somewhat difficult to avoid, so we will live with it.
         //
         // Another possibility! A templatized constructor will contain C4I...E
         // for template arguments.
         //
-        for (let [synthetic, variant] of [
-            ['C4E', 'C1E'],
-            ['C4E', 'C2E'],
-            ['C4E', 'C3E'],
-            ['C4I', 'C1I'],
-            ['C4I', 'C2I'],
-            ['C4I', 'C3I']])
+        for (let [synthetic, variant, desc] of [
+            ['C4E', 'C1E', 'complete_ctor'],
+            ['C4E', 'C2E', 'base_ctor'],
+            ['C4E', 'C3E', 'complete_alloc_ctor'],
+            ['C4I', 'C1I', 'complete_ctor'],
+            ['C4I', 'C2I', 'base_ctor'],
+            ['C4I', 'C3I', 'complete_alloc_ctor']])
         {
             if (mangled.indexOf(synthetic) == -1)
                 continue;
 
             let variant_mangled = mangled.replace(synthetic, variant);
-            let variant_full = variant_mangled + "$" + unmangled;
-            print("D " + memo(variant_full) + " " + memo(functionName));
+            let variant_full = `${variant_mangled}$${unmangled} [[${desc}]]`;
+            printOnce("D " + functionId(variant_full) + " " + functionId(functionName));
         }
     }
 
     // For destructors:
     //
     // I've never seen D4Ev() + D4Ev(int32), only one or the other. So
     // for a D4Ev of any sort, create:
     //
@@ -249,24 +293,26 @@ function process(functionName, functionB
     //                 # use whichever one is defined, in-charge or not.
     //                 # ('?') means either () or (int32).
     //
     // Note that this doesn't actually make sense -- D0 and D1 should be
     // in-charge, but gcc doesn't seem to give them the in-charge parameter?!
     //
     if (functionName.indexOf("D4Ev") != -1 && functionName.indexOf("::~") != -1) {
         const not_in_charge_dtor = functionName.replace("(int32)", "()");
-        const D0 = not_in_charge_dtor.replace("D4Ev", "D0Ev");
-        const D1 = not_in_charge_dtor.replace("D4Ev", "D1Ev");
-        const D2 = not_in_charge_dtor.replace("D4Ev", "D2Ev");
-        print("D " + memo(D0) + " " + memo(D1));
-        print("D " + memo(D1) + " " + memo(D2));
-        print("D " + memo(D2) + " " + memo(functionName));
+        const D0 = not_in_charge_dtor.replace("D4Ev", "D0Ev") + " [[deleting_dtor]]";
+        const D1 = not_in_charge_dtor.replace("D4Ev", "D1Ev") + " [[complete_dtor]]";
+        const D2 = not_in_charge_dtor.replace("D4Ev", "D2Ev") + " [[base_dtor]]";
+        printOnce("D " + functionId(D0) + " " + functionId(D1));
+        printOnce("D " + functionId(D1) + " " + functionId(D2));
+        printOnce("D " + functionId(D2) + " " + functionId(functionName));
     }
 }
 
 for (var nameIndex = minStream; nameIndex <= maxStream; nameIndex++) {
     var name = xdb.read_key(nameIndex);
     var data = xdb.read_entry(name);
     process(name.readString(), JSON.parse(data.readString()));
     xdb.free_string(name);
     xdb.free_string(data);
 }
+
+os.file.close(os.file.redirect(origOut));
--- a/js/src/devtools/rootAnalysis/computeGCFunctions.js
+++ b/js/src/devtools/rootAnalysis/computeGCFunctions.js
@@ -2,51 +2,55 @@
 
 "use strict";
 
 loadRelativeToScript('utility.js');
 loadRelativeToScript('annotations.js');
 loadRelativeToScript('loadCallgraph.js');
 
 if (typeof scriptArgs[0] != 'string')
-    throw "Usage: computeGCFunctions.js <callgraph.txt> <out:gcFunctions.txt> <out:gcFunctions.lst> <out:gcEdges.txt> <out:suppressedFunctions.lst>";
+    throw "Usage: computeGCFunctions.js <callgraph.txt> <out:gcFunctions.txt> <out:gcFunctions.lst> <out:gcEdges.txt> <out:limitedFunctions.lst>";
 
 var start = "Time: " + new Date;
 
 var callgraph_filename = scriptArgs[0];
 var gcFunctions_filename = scriptArgs[1] || "gcFunctions.txt";
 var gcFunctionsList_filename = scriptArgs[2] || "gcFunctions.lst";
 var gcEdges_filename = scriptArgs[3] || "gcEdges.txt";
-var suppressedFunctionsList_filename = scriptArgs[4] || "suppressedFunctions.lst";
+var limitedFunctionsList_filename = scriptArgs[4] || "limitedFunctions.lst";
 
 loadCallgraph(callgraph_filename);
 
 printErr("Writing " + gcFunctions_filename);
 redirect(gcFunctions_filename);
 
 for (var name in gcFunctions) {
-    for (let readable of readableNames[name]) {
+    for (let readable of (readableNames[name] || [])) {
         print("");
         print("GC Function: " + name + "$" + readable);
         let current = name;
         do {
             current = gcFunctions[current];
             if (current in readableNames)
                 print("    " + readableNames[current][0]);
             else
                 print("    " + current);
         } while (current in gcFunctions);
     }
 }
 
 printErr("Writing " + gcFunctionsList_filename);
 redirect(gcFunctionsList_filename);
 for (var name in gcFunctions) {
-    for (var readable of readableNames[name])
-        print(name + "$" + readable);
+    if (name in readableNames) {
+        for (var readable of readableNames[name])
+            print(name + "$" + readable);
+    } else {
+        print(name);
+    }
 }
 
 // gcEdges is a list of edges that can GC for more specific reasons than just
 // calling a function that is in gcFunctions.txt.
 //
 // Right now, it is unused. It was meant for ~AutoRealm when it might
 // wrap an exception, but anything held live across ~AC will have to be held
 // live across the corresponding constructor (and hence the whole scope of the
@@ -57,13 +61,12 @@ printErr("Writing " + gcEdges_filename);
 redirect(gcEdges_filename);
 for (var block in gcEdges) {
   for (var edge in gcEdges[block]) {
       var func = gcEdges[block][edge];
     print([ block, edge, func ].join(" || "));
   }
 }
 
-printErr("Writing " + suppressedFunctionsList_filename);
-redirect(suppressedFunctionsList_filename);
-for (var name in suppressedFunctions) {
-    print(name);
-}
+printErr("Writing " + limitedFunctionsList_filename);
+redirect(limitedFunctionsList_filename);
+for (const [name, limits] of Object.entries(limitedFunctions))
+    print(`${limits} ${name}`);
--- a/js/src/devtools/rootAnalysis/computeGCTypes.js
+++ b/js/src/devtools/rootAnalysis/computeGCTypes.js
@@ -33,17 +33,17 @@ var gcTypes = {}; // map from parent str
 var gcPointers = {}; // map from parent struct => Set of GC typed children
 var gcFields = new Map;
 
 var rootedPointers = {};
 
 function processCSU(csu, body)
 {
     for (let { 'Name': [ annType, tag ] } of (body.Annotation || [])) {
-        if (annType != 'Tag')
+        if (annType != 'annotate')
             continue;
 
         if (tag == 'GC Pointer')
             typeInfo.GCPointers.push(csu);
         else if (tag == 'Invalidated by GC')
             typeInfo.GCPointers.push(csu);
         else if (tag == 'GC Thing')
             typeInfo.GCThings.push(csu);
--- a/js/src/devtools/rootAnalysis/dumpCFG.js
+++ b/js/src/devtools/rootAnalysis/dumpCFG.js
@@ -102,17 +102,17 @@ function str_value(val, env, options) {
     const exp = str_value(Exp[0], env);
     if (options && options.noderef)
       return exp;
     return "*" + exp;
   } else if (Kind == 'Fld') {
     const {Exp, Field} = val;
     const name = Field.Name[0];
     if ("FieldInstanceFunction" in Field) {
-      return Field.FieldCSU.Type.Name + "::" + name;
+      return Field.FieldCSU.Type.Name + "." + name;
     }
     const container = str_value(Exp[0]);
     if (container.startsWith("*"))
       return container.substring(1) + "->" + name;
     return container + "." + name;
   } else if (Kind == 'Empty') {
     return '<unknown>';
   } else if (Kind == 'Binop') {
--- a/js/src/devtools/rootAnalysis/loadCallgraph.js
+++ b/js/src/devtools/rootAnalysis/loadCallgraph.js
@@ -23,182 +23,401 @@ loadRelativeToScript('utility.js');
 //
 // Note that callgraph.txt uses a compressed representation -- each name is
 // mapped to an integer, and those integers are what is recorded in the edges.
 // But the integers depend on the full name, whereas the true edge should only
 // consider the mangled name. And some of the names encoded in callgraph.txt
 // are FieldCalls, not just function names.
 
 var readableNames = {}; // map from mangled name => list of readable names
-var mangledName = {}; // map from demangled names => mangled names. Could be eliminated.
-var calleeGraph = {}; // map from mangled => list of tuples of {'callee':mangled, 'suppressed':bool}
-var callerGraph = {}; // map from mangled => list of tuples of {'caller':mangled, 'suppressed':bool}
+var calleesOf = {}; // map from mangled => list of tuples of {'callee':mangled, 'limits':intset}
+var callersOf; // map from mangled => list of tuples of {'caller':mangled, 'limits':intset}
 var gcFunctions = {}; // map from mangled callee => reason
-var suppressedFunctions = {}; // set of mangled names (map from mangled name => true)
+var limitedFunctions = {}; // set of mangled names (map from mangled name => limit intset)
 var gcEdges = {};
 
-function addGCFunction(caller, reason)
+// "Map" from identifier to mangled name, or sometimes to a Class.Field name.
+var functionNames = [""];
+
+var mangledToId = {};
+
+// Returns whether the function was added. (It will be refused if it was
+// already there, or if limits or annotations say it shouldn't be added.)
+function addGCFunction(caller, reason, functionLimits)
 {
-    if (caller in suppressedFunctions)
+    if (functionLimits[caller] & LIMIT_CANNOT_GC)
         return false;
 
-    if (ignoreGCFunction(caller))
+    if (ignoreGCFunction(functionNames[caller]))
         return false;
 
     if (!(caller in gcFunctions)) {
         gcFunctions[caller] = reason;
         return true;
     }
 
     return false;
 }
 
-function addCallEdge(caller, callee, suppressed)
-{
-    addToKeyedList(calleeGraph, caller, {callee:callee, suppressed:suppressed});
-    addToKeyedList(callerGraph, callee, {caller:caller, suppressed:suppressed});
-}
+// Every caller->callee callsite is associated with a limit saying what is
+// allowed at that callsite (eg if it's in a GC suppression zone, it would have
+// LIMIT_CANNOT_GC set.) A given caller might call the same callee multiple
+// times, with different limits, so we want to associate the <caller,callee>
+// edge with the intersection ('AND') of all of the callsites' limits.
+//
+// Scan through all call edges and intersect the limits for all matching
+// <caller,callee> edges (so that the result is the least limiting of all
+// matching edges.) Preserve the original order.
+//
+// During the same scan, build callersOf from calleesOf.
+function merge_repeated_calls(calleesOf) {
+    const callersOf = Object.create(null);
+
+    for (const [caller, callee_limits] of Object.entries(calleesOf)) {
+        const ordered_callees = [];
 
-// Map from identifier to full "mangled|readable" name. Or sometimes to a
-// Class.Field name.
-var functionNames = [""];
+        // callee_limits is a list of {callee,limit} objects.
+        const callee2limit = new Map();
+        for (const {callee, limits} of callee_limits) {
+            const prev_limits = callee2limit.get(callee);
+            if (prev_limits === undefined) {
+                callee2limit.set(callee, limits);
+                ordered_callees.push(callee);
+            } else {
+                callee2limit.set(callee, prev_limits & limits);
+            }
+        }
 
-// Map from identifier to mangled name (or to a Class.Field)
-var idToMangled = [""];
+        // Update the contents of callee_limits to contain a single entry for
+        // each callee, with its limits set to the AND of the limits observed
+        // at all callsites within this caller function.
+        callee_limits.length = 0;
+        for (const callee of ordered_callees) {
+            const limits = callee2limit.get(callee);
+            callee_limits.push({callee, limits});
+            if (!(callee in callersOf))
+                callersOf[callee] = [];
+            callersOf[callee].push({caller, limits});
+        }
+    }
+
+    return callersOf;
+}
 
 function loadCallgraph(file)
 {
-    var suppressedFieldCalls = {};
-    var resolvedFunctions = {};
+    const fieldCallLimits = {};
+    const fieldCallCSU = new Map(); // map from full field name id => csu name
+    const resolvedFieldCalls = new Set();
 
-    var numGCCalls = 0;
+    // set of mangled names (map from mangled name => limit intset)
+    var functionLimits = {};
 
-    for (var line of readFileLines_gen(file)) {
+    let numGCCalls = 0;
+
+    for (let line of readFileLines_gen(file)) {
         line = line.replace(/\n/, "");
 
-        var match;
+        let match;
         if (match = line.charAt(0) == "#" && /^\#(\d+) (.*)/.exec(line)) {
-            assert(functionNames.length == match[1]);
-            functionNames.push(match[2]);
-            var [ mangled, readable ] = splitFunction(match[2]);
+            const [ _, id, mangled ] = match;
+            assert(functionNames.length == id);
+            functionNames.push(mangled);
+            mangledToId[mangled] = id;
+            continue;
+        }
+        if (match = line.charAt(0) == "=" && /^= (\d+) (.*)/.exec(line)) {
+            const [ _, id, readable ] = match;
+            const mangled = functionNames[id];
             if (mangled in readableNames)
                 readableNames[mangled].push(readable);
             else
                 readableNames[mangled] = [ readable ];
-            mangledName[readable] = mangled;
-            idToMangled.push(mangled);
             continue;
         }
-        var suppressed = false;
-        if (line.indexOf("SUPPRESS_GC") != -1) {
-            match = /^(..)SUPPRESS_GC (.*)/.exec(line);
-            line = match[1] + match[2];
-            suppressed = true;
+
+        let limits = 0;
+        // Example line: D /17 6 7
+        //
+        // This means a direct call from 6 -> 7, but within a scope that
+        // applies limits 0x1 and 0x10 to the callee.
+        //
+        // Look for a limit and remove it from the line if found.
+        if (line.indexOf("/") != -1) {
+            match = /^(..)\/(\d+) (.*)/.exec(line);
+            line = match[1] + match[3];
+            limits = match[2]|0;
         }
-        var tag = line.charAt(0);
+        const tag = line.charAt(0);
         if (match = tag == 'I' && /^I (\d+) VARIABLE ([^\,]*)/.exec(line)) {
-            var mangledCaller = idToMangled[match[1]];
-            var name = match[2];
-            if (!indirectCallCannotGC(functionNames[match[1]], name) && !suppressed)
-                addGCFunction(mangledCaller, "IndirectCall: " + name);
-        } else if (match = (tag == 'F' || tag == 'V') && /^[FV] (\d+) CLASS (.*?) FIELD (.*)/.exec(line)) {
-            var caller = idToMangled[match[1]];
-            var csu = match[2];
-            var fullfield = csu + "." + match[3];
-            if (suppressed)
-                suppressedFieldCalls[fullfield] = true;
-            else if (!fieldCallCannotGC(csu, fullfield))
-                addGCFunction(caller, "FieldCall: " + fullfield);
+            const caller = match[1]|0;
+            const name = match[2];
+            if (!indirectCallCannotGC(functionNames[caller], name) &&
+                !(limits & LIMIT_CANNOT_GC))
+            {
+                addGCFunction(caller, "IndirectCall: " + name, functionLimits);
+            }
+        } else if (match = (tag == 'F' || tag == 'V') && /^[FV] (\d+) (\d+) CLASS (.*?) FIELD (.*)/.exec(line)) {
+            const caller = match[1]|0;
+            const fullfield = match[2]|0;
+            const csu = match[3];
+            const fullfield_str = csu + "." + match[4];
+            assert(functionNames[fullfield] == fullfield_str);
+            if (limits)
+                fieldCallLimits[fullfield] = limits;
+            addToKeyedList(calleesOf, caller, {callee:fullfield, limits});
+            fieldCallCSU.set(fullfield, csu);
         } else if (match = tag == 'D' && /^D (\d+) (\d+)/.exec(line)) {
-            var caller = idToMangled[match[1]];
-            var callee = idToMangled[match[2]];
-            addCallEdge(caller, callee, suppressed);
+            const caller = match[1]|0;
+            const callee = match[2]|0;
+            addToKeyedList(calleesOf, caller, {callee:callee, limits:limits});
         } else if (match = tag == 'R' && /^R (\d+) (\d+)/.exec(line)) {
-            var callerField = idToMangled[match[1]];
-            var callee = idToMangled[match[2]];
-            addCallEdge(callerField, callee, false);
-            resolvedFunctions[callerField] = true;
+            const callerField = match[1]|0;
+            const callee = match[2]|0;
+            // Resolved virtual functions create a dummy node for the field
+            // call, and callers call it. It will then call all possible
+            // instantiations. No additional limits are placed on the callees;
+            // it's as if there were a function named BaseClass.foo:
+            //
+            //     void BaseClass.foo() {
+            //         Subclass1::foo();
+            //         Subclass2::foo();
+            //     }
+            //
+            addToKeyedList(calleesOf, callerField, {callee:callee, limits:0});
+            // Mark that we resolved this virtual method, so that it isn't
+            // assumed to call some random function that might do anything.
+            resolvedFieldCalls.add(callerField);
         } else if (match = tag == 'T' && /^T (\d+) (.*)/.exec(line)) {
-            var mangled = idToMangled[match[1]];
-            var tag = match[2];
+            const id = match[1]|0;
+            let tag = match[2];
             if (tag == 'GC Call') {
-                addGCFunction(mangled, "GC");
+                addGCFunction(id, "GC", functionLimits);
                 numGCCalls++;
             }
+        } else {
+            assert(false, "Invalid format in callgraph line: " + line);
         }
     }
 
+    // Callers have a list of callees, with duplicates (if the same function is
+    // called more than once.) Merge the repeated calls, only keeping limits
+    // that are in force for *every* callsite of that callee. Also, generate
+    // the callersOf table at the same time.
+    callersOf = merge_repeated_calls(calleesOf);
+
     // Add in any extra functions at the end. (If we did this early, it would
     // mess up the id <-> name correspondence. Also, we need to know if the
     // functions even exist in the first place.)
     for (var func of extraGCFunctions()) {
-        addGCFunction(func, "annotation");
+        addGCFunction(mangledToId[func], "annotation", functionLimits);
     }
 
-    // Initialize suppressedFunctions to the set of all functions, and the
-    // worklist to all toplevel callers.
-    var worklist = [];
-    for (var callee in callerGraph)
-        suppressedFunctions[callee] = true;
-    for (var caller in calleeGraph) {
-        if (!(caller in callerGraph)) {
-            suppressedFunctions[caller] = true;
-            worklist.push(caller);
-        }
+    // Compute functionLimits: it should contain the set of functions that
+    // are *always* called within some sort of limited context (eg GC
+    // suppression).
+
+    // Initialize to limited field calls.
+    for (var [name, limits] of Object.entries(fieldCallLimits)) {
+        if (limits)
+            functionLimits[name] = limits;
     }
 
-    // Find all functions reachable via an unsuppressed call chain, and remove
-    // them from the suppressedFunctions set. Everything remaining is only
-    // reachable when GC is suppressed.
-    var top = worklist.length;
-    while (top > 0) {
-        name = worklist[--top];
-        if (!(name in suppressedFunctions))
-            continue;
-        delete suppressedFunctions[name];
-        if (!(name in calleeGraph))
-            continue;
-        for (var entry of calleeGraph[name]) {
-            if (!entry.suppressed)
-                worklist[top++] = entry.callee;
-        }
-    }
+    // Initialize functionLimits to the set of all functions, where each one is
+    // maximally limited, and return a worklist containing all simple roots
+    // (nodes with no callers).
+    var roots = gather_simple_roots(functionLimits, callersOf);
+
+    // Traverse the graph, spreading the limits down from the roots.
+    propagate_limits(roots, functionLimits, calleesOf);
 
-    // Such functions are known to not GC.
+    // There are a surprising number of "recursive roots", where there is a
+    // cycle of functions calling each other but not called by anything else,
+    // and these roots may also have descendants. Now that the above traversal
+    // has eliminated everything reachable from simple roots, traverse the
+    // remaining graph to gather up a representative function from each root
+    // cycle.
+    roots = gather_recursive_roots(roots, functionLimits, callersOf);
+
+    // And do a final traversal starting with the recursive roots.
+    propagate_limits(roots, functionLimits, calleesOf);
+
+    // Eliminate GC-limited functions from the set of functions known to GC.
     for (var name in gcFunctions) {
-        if (name in suppressedFunctions)
+        if (functionLimits[name] & LIMIT_CANNOT_GC)
             delete gcFunctions[name];
     }
 
-    for (var name in suppressedFieldCalls) {
-        suppressedFunctions[name] = true;
-    }
+    // functionLimits should now contain all functions that are always called
+    // in a limited context.
 
     // Sanity check to make sure the callgraph has some functions annotated as
     // GC Calls. This is mostly a check to be sure the earlier processing
     // succeeded (as opposed to, say, running on empty xdb files because you
     // didn't actually compile anything interesting.)
     assert(numGCCalls > 0, "No GC functions found!");
 
     // Initialize the worklist to all known gcFunctions.
     var worklist = [];
-    for (var name in gcFunctions)
+    for (const name in gcFunctions)
         worklist.push(name);
 
-    // Recursively find all callers and add them to the set of gcFunctions.
+    // Include all field calls and unresolved virtual method calls.
+    for (const [name, csuName] of fieldCallCSU) {
+        if (resolvedFieldCalls.has(name))
+            continue; // Skip resolved virtual functions.
+        const fullFieldName = functionNames[name];
+        if (!fieldCallCannotGC(csuName, fullFieldName)) {
+            gcFunctions[name] = 'unresolved ' + fullFieldName;
+            worklist.push(name);
+        }
+    }
+
+    // Recursively find all callers not always called in a GC suppression
+    // context, and add them to the set of gcFunctions.
     while (worklist.length) {
         name = worklist.shift();
         assert(name in gcFunctions);
-        if (!(name in callerGraph))
+        if (!(name in callersOf))
             continue;
-        for (var entry of callerGraph[name]) {
-            if (!entry.suppressed && addGCFunction(entry.caller, name))
-                worklist.push(entry.caller);
+        for (const {caller, limits} of callersOf[name]) {
+            if (!(limits & LIMIT_CANNOT_GC)) {
+                if (addGCFunction(caller, name, functionLimits))
+                    worklist.push(caller);
+            }
+        }
+    }
+
+    // Convert functionLimits to limitedFunctions (using mangled names instead
+    // of ids.)
+
+    for (const [id, limits] of Object.entries(functionLimits))
+        limitedFunctions[functionNames[id]] = limits;
+
+    // The above code uses integer ids for efficiency. External code uses
+    // mangled names. Rewrite the various data structures to convert ids to
+    // mangled names.
+    remap_ids_to_mangled_names();
+}
+
+// Return a worklist of functions with no callers, and also initialize
+// functionLimits to the set of all functions, each mapped to LIMIT_UNVISTED.
+function gather_simple_roots(functionLimits, callersOf) {
+    const roots = [];
+    for (let callee in callersOf)
+        functionLimits[callee] = LIMIT_UNVISITED;
+    for (let caller in calleesOf) {
+        if (!(caller in callersOf)) {
+            functionLimits[caller] = LIMIT_UNVISITED;
+            roots.push([caller, LIMIT_NONE, 'root']);
         }
     }
 
-    // Any field call that has been resolved to all possible callees can be
-    // trusted to not GC if all of those callees are known to not GC.
-    for (var name in resolvedFunctions) {
-        if (!(name in gcFunctions))
-            suppressedFunctions[name] = true;
+    return roots;
+}
+
+// Recursively traverse the callgraph from the roots. Recurse through every
+// edge that weakens the limits. (Limits that entirely disappear, aka go to a
+// zero intset, will be removed from functionLimits.)
+function propagate_limits(worklist, functionLimits, calleesOf) {
+    let top = worklist.length;
+    while (top > 0) {
+        // Consider caller where (graph) -> caller -> (0 or more callees)
+        // 'callercaller' is for debugging.
+        const [caller, edge_limits, callercaller] = worklist[--top];
+        const prev_limits = functionLimits[caller];
+        if (prev_limits & ~edge_limits) {
+            // Turning off a limit (or unvisited marker). Must recurse to the
+            // children. But first, update this caller's limits: we just found
+            // out it is reachable by an unlimited path, so it must be treated
+            // as unlimited (with respect to that bit).
+            const new_limits = prev_limits & edge_limits;
+            if (new_limits)
+                functionLimits[caller] = new_limits;
+            else
+                delete functionLimits[caller];
+            for (const {callee, limits} of (calleesOf[caller] || []))
+                worklist[top++] = [callee, limits | edge_limits, caller];
+        }
     }
 }
+
+// Mutually-recursive roots and their descendants will not have been visited,
+// and will still be set to LIMIT_UNVISITED. Scan through and gather them.
+function gather_recursive_roots(functionLimits, callersOf) {
+    const roots = [];
+
+    // 'seen' maps functions to the most recent starting function that each was
+    // first reachable from, to distinguish between the current pass and passes
+    // for preceding functions.
+    //
+    // Consider:
+    //
+    //   A <--> B --> C <-- D <--> E
+    //                C --> F
+    //                C --> G
+    //
+    // So there are two root cycles AB and DE, both calling C that in turn
+    // calls F and G. If we start at F and scan up through callers, we will
+    // keep going until A loops back to B and E loops back to D, and will add B
+    // and D as roots. Then if we scan from G, we encounter C and see that it
+    // was already been seen on an earlier pass. So C and everything reachable
+    // from it is already reachable by some root. (We need to label nodes with
+    // their pass because otherwise we couldn't distinguish "already seen C,
+    // done" from "already seen B, must be a root".)
+    //
+    const seen = new Map();
+    for (var func in functionLimits) {
+        if (functionLimits[func] != LIMIT_UNVISITED)
+            continue;
+
+        // We should only be looking at nodes with callers, since otherwise
+        // they would have been handled in the previous pass!
+        assert(callersOf[func].length > 0);
+
+        const work = [func];
+        while (work.length > 0) {
+            const f = work.pop();
+            if (seen.has(f)) {
+                if (seen.get(f) == func) {
+                    // We have traversed a cycle and reached an already-seen
+                    // node. Treat it as a root.
+                    roots.push([f, LIMIT_NONE, 'root']);
+                    print(`recursive root? ${f} = ${functionNames[f]}`);
+                } else {
+                    // Otherwise we hit the portion of the graph that is
+                    // reachable from a past root.
+                    seen.set(f, func);
+                }
+            } else {
+                print(`retained by recursive root? ${f} = ${functionNames[f]}`);
+                work.push(...callersOf[f]);
+                seen.set(f, func);
+            }
+        }
+    }
+
+    return roots;
+}
+
+function remap_ids_to_mangled_names() {
+    var tmp = gcFunctions;
+    gcFunctions = {};
+    for (const [caller, reason] of Object.entries(tmp))
+        gcFunctions[functionNames[caller]] = functionNames[reason] || reason;
+
+    tmp = calleesOf;
+    calleesOf = {};
+    for (const [callerId, callees] of Object.entries(calleesOf)) {
+        const caller = functionNames[callerId];
+        for (const {calleeId, limits} of callees)
+            calleesOf[caller][functionNames[calleeId]] = limits;
+    }
+
+    tmp = callersOf;
+    callersOf = {};
+    for (const [calleeId, callers] of Object.entries(callersOf)) {
+        const callee = functionNames[calleeId];
+        callersOf[callee] = {};
+        for (const {callerId, limits} of callers)
+            callersOf[callee][functionNames[caller]] = limits;
+    }
+}
--- a/js/src/devtools/rootAnalysis/run-test.py
+++ b/js/src/devtools/rootAnalysis/run-test.py
@@ -3,22 +3,24 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import site
 import subprocess
 import argparse
 
-testdir = os.path.abspath(os.path.join(os.path.dirname(__file__), 't'))
+from glob import glob
+
+scriptdir = os.path.abspath(os.path.dirname(__file__))
+testdir = os.path.join(scriptdir, 't')
+
 site.addsitedir(testdir)
 from testlib import Test, equal
 
-scriptdir = os.path.abspath(os.path.dirname(__file__))
-
 parser = argparse.ArgumentParser(description='run hazard analysis tests')
 parser.add_argument(
     '--js', default=os.environ.get('JS'),
     help='JS binary to run the tests with')
 parser.add_argument(
     '--sixgill', default=os.environ.get('SIXGILL', os.path.join(testdir, "sixgill")),
     help='Path to root of sixgill installation')
 parser.add_argument(
@@ -35,17 +37,22 @@ parser.add_argument(
     help='Path to gcc')
 parser.add_argument(
     '--cxx', default=os.environ.get('CXX'),
     help='Path to g++')
 parser.add_argument(
     '--verbose', '-v', action='store_true',
     help='Display verbose output, including commands executed')
 parser.add_argument(
-    'tests', nargs='*', default=['sixgill-tree', 'suppression', 'hazards', 'exceptions'],
+    'tests', nargs='*', default=[
+        'sixgill-tree',
+        'suppression',
+        'hazards',
+        'exceptions',
+        'virtual'],
     help='tests to run')
 
 cfg = parser.parse_args()
 
 if not cfg.js:
     exit('Must specify JS binary through environment variable or --js option')
 if not cfg.cc:
     if cfg.gccdir:
@@ -64,33 +71,46 @@ if not cfg.sixgill_plugin:
 
 subprocess.check_call([cfg.js, '-e', 'if (!getBuildConfiguration()["has-ctypes"]) quit(1)'])
 
 
 def binpath(prog):
     return os.path.join(cfg.sixgill_bin, prog)
 
 
-try:
-    os.mkdir(os.path.join('t', 'out'))
-except OSError:
-    pass
+def make_dir(dirname, exist_ok=True):
+    try:
+        os.mkdir(dirname)
+    except OSError as e:
+        if exist_ok and e.strerror == 'File exists':
+            pass
+        else:
+            raise
+
+
+outroot = os.path.join(testdir, 'out')
+make_dir(outroot)
 
 for name in cfg.tests:
     name = os.path.basename(name)
     indir = os.path.join(testdir, name)
-    outdir = os.path.join(testdir, 'out', name)
-    try:
-        os.mkdir(outdir)
-    except OSError:
-        pass
+    outdir = os.path.join(outroot, name)
+    make_dir(outdir)
 
     test = Test(indir, outdir, cfg, verbose=cfg.verbose)
 
     os.chdir(outdir)
-    subprocess.call(["sh", "-c", "rm *.xdb"])
+    for xdb in glob("*.xdb"):
+        os.unlink(xdb)
     if cfg.verbose:
         print("Running test %s" % name)
     testpath = os.path.join(indir, "test.py")
     testscript = open(testpath).read()
     testcode = compile(testscript, testpath, 'exec')
-    exec(testcode, {'test': test, 'equal': equal})
-    print("TEST-PASSED: %s" % name)
+    try:
+        exec(testcode, {'test': test, 'equal': equal})
+    except subprocess.CalledProcessError:
+        print("TEST-FAILED: %s" % name)
+    except StandardError:
+        print("TEST-FAILED: %s" % name)
+        raise
+    else:
+        print("TEST-PASSED: %s" % name)
--- a/js/src/devtools/rootAnalysis/t/exceptions/source.cpp
+++ b/js/src/devtools/rootAnalysis/t/exceptions/source.cpp
@@ -1,42 +1,45 @@
-#define ANNOTATE(property) __attribute__((tag(property)))
+#define ANNOTATE(property) __attribute__((annotate(property)))
 
 struct Cell { int f; } ANNOTATE("GC Thing");
 
 extern void GC() ANNOTATE("GC Call");
 
 void GC()
 {
     // If the implementation is too trivial, the function body won't be emitted at all.
     asm("");
 }
 
 class RAII_GC {
   public:
-    RAII_GC() {}
+    RAII_GC() { }
     ~RAII_GC() { GC(); }
 };
 
 // ~AutoSomething calls GC because of the RAII_GC field. The constructor,
 // though, should *not* GC -- unless it throws an exception. Which is not
-// possible when compiled with -fno-exceptions.
+// possible when compiled with -fno-exceptions. This test will try it both
+// ways.
 class AutoSomething {
     RAII_GC gc;
   public:
     AutoSomething() : gc() {
         asm(""); // Ooh, scary, this might throw an exception
     }
     ~AutoSomething() {
         asm("");
     }
 };
 
 extern void usevar(Cell* cell);
 
 void f() {
     Cell* thing = nullptr; // Live range starts here
 
+    // When compiling with -fexceptions, there should be a hazard below. With
+    // -fno-exceptions, there should not be one. We will check both.
     {
         AutoSomething smth; // Constructor can GC only if exceptions are enabled
         usevar(thing); // Live range ends here
     } // In particular, 'thing' is dead at the destructor, so no hazard
 }
--- a/js/src/devtools/rootAnalysis/t/hazards/source.cpp
+++ b/js/src/devtools/rootAnalysis/t/hazards/source.cpp
@@ -1,9 +1,9 @@
-#define ANNOTATE(property) __attribute__((tag(property)))
+#define ANNOTATE(property) __attribute__((annotate(property)))
 
 struct Cell { int f; } ANNOTATE("GC Thing");
 
 struct RootedCell { RootedCell(Cell*) {} } ANNOTATE("Rooted Pointer");
 
 class AutoSuppressGC_Base {
   public:
     AutoSuppressGC_Base() {}
--- a/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp
+++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp
@@ -1,9 +1,9 @@
-#define ANNOTATE(property) __attribute__((tag(property)))
+#define ANNOTATE(property) __attribute__((annotate(property)))
 
 namespace js {
 namespace gc {
 struct Cell { int f; } ANNOTATE("GC Thing");
 }
 }
 
 struct Bogon {
--- a/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py
+++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py
@@ -17,32 +17,32 @@ assert('obj' in body['Variables'])
 assert('random' in body['Variables'])
 assert('other1' in body['Variables'])
 assert('other2' in body['Variables'])
 
 # Test function annotations
 js_GC = test.process_body(test.load_db_entry("src_body", re.compile(r'js_GC'))[0])
 annotations = js_GC['Variables']['void js_GC()']['Annotation']
 assert(annotations)
-found_call_tag = False
+found_call_annotate = False
 for annotation in annotations:
     (annType, value) = annotation['Name']
-    if annType == 'Tag' and value == 'GC Call':
-        found_call_tag = True
-assert(found_call_tag)
+    if annType == 'annotate' and value == 'GC Call':
+        found_call_annotate = True
+assert(found_call_annotate)
 
 # Test type annotations
 
 # js::gc::Cell first
 cell = test.load_db_entry("src_comp", 'js::gc::Cell')[0]
 assert(cell['Kind'] == 'Struct')
 annotations = cell['Annotation']
 assert(len(annotations) == 1)
 (tag, value) = annotations[0]['Name']
-assert(tag == 'Tag')
+assert(tag == 'annotate')
 assert(value == 'GC Thing')
 
 # Check JSObject inheritance.
 JSObject = test.load_db_entry("src_comp", 'JSObject')[0]
 bases = [b['Base'] for b in JSObject['CSUBaseClass']]
 assert('js::gc::Cell' in bases)
 assert('Bogon' in bases)
 assert(len(bases) == 2)
--- a/js/src/devtools/rootAnalysis/t/suppression/source.cpp
+++ b/js/src/devtools/rootAnalysis/t/suppression/source.cpp
@@ -1,9 +1,9 @@
-#define ANNOTATE(property) __attribute__((tag(property)))
+#define ANNOTATE(property) __attribute__((annotate(property)))
 
 struct Cell { int f; } ANNOTATE("GC Thing");
 
 class AutoSuppressGC_Base {
   public:
     AutoSuppressGC_Base() {}
     ~AutoSuppressGC_Base() {}
 } ANNOTATE("Suppress GC");
--- a/js/src/devtools/rootAnalysis/t/testlib.py
+++ b/js/src/devtools/rootAnalysis/t/testlib.py
@@ -3,18 +3,33 @@ import os
 import re
 import subprocess
 
 from sixgill import Body
 from collections import defaultdict, namedtuple
 
 scriptdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
 
-HazardSummary = namedtuple(
-    'HazardSummary', ['function', 'variable', 'type', 'GCFunction', 'location'])
+HazardSummary = namedtuple('HazardSummary', [
+    'function',
+    'variable',
+    'type',
+    'GCFunction',
+    'location'])
+
+Callgraph = namedtuple('Callgraph', [
+    'functionNames',
+    'nameToId',
+    'mangledToUnmangled',
+    'unmangledToMangled',
+    'calleesOf',
+    'callersOf',
+    'tags',
+    'calleeGraph',
+    'callerGraph'])
 
 
 def equal(got, expected):
     if got != expected:
         print("Got '%s', expected '%s'" % (got, expected))
 
 
 def extract_unmangled(func):
@@ -85,33 +100,95 @@ sixgill_bin = '{bindir}'
         self.run_analysis_script("gcTypes")
 
     def load_text_file(self, filename, extract=lambda l: l):
         fullpath = os.path.join(self.outdir, filename)
         values = (extract(line.strip()) for line in open(fullpath, "r"))
         return list(filter(lambda _: _ is not None, values))
 
     def load_suppressed_functions(self):
-        return set(self.load_text_file("suppressedFunctions.lst"))
+        return set(self.load_text_file("limitedFunctions.lst", extract=lambda l: l.split(' ')[1]))
 
     def load_gcTypes(self):
         def grab_type(line):
             m = re.match(r'^(GC\w+): (.*)', line)
             if m:
                 return (m.group(1) + 's', m.group(2))
             return None
 
         gctypes = defaultdict(list)
         for collection, typename in self.load_text_file('gcTypes.txt', extract=grab_type):
             gctypes[collection].append(typename)
         return gctypes
 
     def load_gcFunctions(self):
         return self.load_text_file('gcFunctions.lst', extract=extract_unmangled)
 
+    def load_callgraph(self):
+        data = Callgraph(
+            functionNames=['dummy'],
+            nameToId={},
+            mangledToUnmangled={},
+            unmangledToMangled={},
+            calleesOf=defaultdict(list),
+            callersOf=defaultdict(list),
+            tags=defaultdict(set),
+            calleeGraph=defaultdict(dict),
+            callerGraph=defaultdict(dict),
+        )
+
+        def lookup(id):
+            mangled = data.functionNames[int(id)]
+            return data.mangledToUnmangled.get(mangled, mangled)
+
+        def add_call(caller, callee, limit):
+            data.calleesOf[caller].append(callee)
+            data.callersOf[callee].append(caller)
+            data.calleeGraph[caller][callee] = True
+            data.callerGraph[callee][caller] = True
+
+        def process(line):
+            if line.startswith('#'):
+                name = line.split(" ", 1)[1]
+                data.nameToId[name] = len(data.functionNames)
+                data.functionNames.append(name)
+                return
+
+            if line.startswith('='):
+                m = re.match(r'^= (\d+) (.*)', line)
+                mangled = data.functionNames[int(m.group(1))]
+                unmangled = m.group(2)
+                data.nameToId[unmangled] = id
+                data.mangledToUnmangled[mangled] = unmangled
+                data.unmangledToMangled[unmangled] = mangled
+                return
+
+            limit = 0
+            m = re.match(r'^\w (?:/(\d+))? ', line)
+            if m:
+                limit = int(m[1])
+
+            tokens = line.split(' ')
+            if tokens[0] in ('D', 'R'):
+                _, caller, callee = tokens
+                add_call(lookup(caller), lookup(callee), limit)
+            elif tokens[0] == 'T':
+                data.tags[tokens[1]].add(line.split(' ', 2)[2])
+            elif tokens[0] in ('F', 'V'):
+                m = re.match(r'^[FV] (\d+) (\d+) CLASS (.*?) FIELD (.*)', line)
+                caller, callee, csu, field = m.groups()
+                add_call(lookup(caller), lookup(callee), limit)
+
+            elif tokens[0] == 'I':
+                m = re.match(r'^I (\d+) VARIABLE ([^\,]*)', line)
+                pass
+
+        self.load_text_file('callgraph.txt', extract=process)
+        return data
+
     def load_hazards(self):
         def grab_hazard(line):
             m = re.match(
                 r"Function '(.*?)' has unrooted '(.*?)' of type '(.*?)' live across GC call '(.*?)' at (.*)", line)  # NOQA: E501
             if m:
                 info = list(m.groups())
                 info[0] = info[0].split("$")[-1]
                 info[3] = info[3].split("$")[-1]
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/source.cpp
@@ -0,0 +1,102 @@
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC()
+{
+    // If the implementation is too trivial, the function body won't be emitted at all.
+    asm("");
+}
+
+struct Cell { int f; } ANNOTATE("GC Thing");
+
+extern void foo();
+
+typedef void (*func_t)();
+
+class Base {
+  public:
+    int ANNOTATE("field annotation") dummy;
+    virtual void someGC() = 0;
+    func_t functionField;
+
+    // For now, this is just to verify that the plugin doesn't crash. The
+    // analysis code does not yet look at this annotation or output it anywhere
+    // (though it *is* being recorded.)
+    static float testAnnotations() ANNOTATE("static func");
+
+    // Similar, though sixgill currently completely ignores parameter annotations.
+    static double testParamAnnotations(Cell& ANNOTATE("param annotation") ANNOTATE("second param annot") cell) ANNOTATE("static func") ANNOTATE("second func");
+};
+
+float Base::testAnnotations()
+{
+    asm("");
+}
+
+double Base::testParamAnnotations(Cell& cell)
+{
+    asm("");
+}
+
+class Super : public Base {
+  public:
+    virtual void noneGC() = 0;
+    virtual void allGC() = 0;
+};
+
+void bar() {
+    GC();
+}
+
+class Sub1 : public Super {
+  public:
+    void noneGC() override { foo(); }
+    void someGC() override { foo(); }
+    void allGC() override { foo(); bar(); }
+};
+
+class Sub2 : public Super {
+  public:
+    void noneGC() override { foo(); }
+    void someGC() override { foo(); bar(); }
+    void allGC() override { foo(); bar(); }
+};
+
+class Sibling : public Base {
+  public:
+    virtual void noneGC() { foo(); }
+    void someGC() override { foo(); bar(); }
+    virtual void allGC() { foo(); bar(); }
+};
+
+class AutoSuppressGC {
+  public:
+    AutoSuppressGC() {}
+    ~AutoSuppressGC() {}
+} ANNOTATE("Suppress GC");
+
+void use(Cell*) {
+    asm("");
+}
+
+void f() {
+    Sub1 s1;
+    Sub2 s2;
+
+    Cell cell;
+    { Cell* c1 = &cell; s1.noneGC(); use(c1); }
+    { Cell* c2 = &cell; s2.someGC(); use(c2); }
+    { Cell* c3 = &cell; s1.allGC(); use(c3); }
+    { Cell* c4 = &cell; s2.noneGC(); use(c4); }
+    { Cell* c5 = &cell; s2.someGC(); use(c5); }
+    { Cell* c6 = &cell; s2.allGC(); use(c6); }
+
+    Super* super = &s2;
+    { Cell* c7 = &cell; super->noneGC(); use(c7); }
+    { Cell* c8 = &cell; super->someGC(); use(c8); }
+    { Cell* c9 = &cell; super->allGC(); use(c9); }
+
+    { Cell* c10 = &cell; s1.functionField(); use(c10); }
+    { Cell* c11 = &cell; super->functionField(); use(c11); }
+}
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/test.py
@@ -0,0 +1,41 @@
+# 'test' is provided by the calling script.
+# flake8: noqa: F821
+
+test.compile("source.cpp")
+test.run_analysis_script('gcTypes')
+
+# The suppressions file uses only mangled names since it's for internal use,
+# though I may change that soon given (1) the unfortunate non-uniqueness of
+# mangled constructor names, and (2) the usefulness of this file for
+# mrgiggles's reporting.
+suppressed = test.load_suppressed_functions()
+
+# gcFunctions should be the inverse, but we get to rely on unmangled names here.
+gcFunctions = test.load_gcFunctions()
+
+assert 'void Sub1::noneGC()' not in gcFunctions
+assert 'void Sub1::someGC()' not in gcFunctions
+assert 'void Sub1::allGC()' in gcFunctions
+assert 'void Sub2::noneGC()' not in gcFunctions
+assert 'void Sub2::someGC()' in gcFunctions
+assert 'void Sub2::allGC()' in gcFunctions
+
+callgraph = test.load_callgraph()
+assert callgraph.calleeGraph['void f()']['Super.noneGC']
+assert callgraph.calleeGraph['Super.noneGC']['void Sub1::noneGC()']
+assert callgraph.calleeGraph['Super.noneGC']['void Sub2::noneGC()']
+assert 'void Sibling::noneGC()' not in callgraph.calleeGraph['Super.noneGC']
+
+hazards = test.load_hazards()
+hazmap = {haz.variable: haz for haz in hazards}
+assert 'c1' not in hazmap
+assert 'c2' in hazmap
+assert 'c3' in hazmap
+assert 'c4' not in hazmap
+assert 'c5' in hazmap
+assert 'c6' in hazmap
+assert 'c7' not in hazmap
+assert 'c8' in hazmap
+assert 'c9' in hazmap
+assert 'c10' in hazmap
+assert 'c11' in hazmap
--- a/js/src/devtools/rootAnalysis/utility.js
+++ b/js/src/devtools/rootAnalysis/utility.js
@@ -1,12 +1,28 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
 
 "use strict";
 
+loadRelativeToScript('dumpCFG.js');
+
+// Limit inset bits - each call edge may carry a set of 'limit' bits, saying eg
+// that the edge takes place within a scope where GC is suppressed, for
+// example.
+var LIMIT_NONE = 0;
+var LIMIT_CANNOT_GC = 1;
+var LIMIT_ALL = 1;
+
+// The traversal algorithms we run will recurse into children if you change any
+// limit bit to zero. Use all bits set to maximally limited, including
+// additional bits that all just mean "unvisited", so that the first time we
+// see a node with this limit, we're guaranteed to turn at least one bit off
+// and thereby keep going.
+var LIMIT_UNVISITED = 0xffff;
+
 // gcc appends this to mangled function names for "not in charge"
 // constructors/destructors.
 var internalMarker = " *INTERNAL* ";
 
 if (! Set.prototype.hasOwnProperty("update")) {
     Object.defineProperty(Set.prototype, "update", {
         value: function (collection) {
             for (let elt of collection)
--- a/js/src/frontend/BinSource-auto.cpp
+++ b/js/src/frontend/BinSource-auto.cpp
@@ -2323,21 +2323,20 @@ BinASTParser<Tok>::parseInterfaceAsserte
     RootedAtom name(cx_);
     MOZ_TRY_VAR(name, tokenizer_->readIdentifierName());
     // `positionalParams` vector can be shorter than the actual
     // parameter length. Resize on demand.
     // (see also ListOfAssertedMaybePositionalParameterName)
     size_t prevLength = positionalParams.get().length();
     if (index >= prevLength) {
         // This is implementation limit, which is not in the spec.
-        size_t newLength = index + 1;
-        if (newLength >= ARGNO_LIMIT) {
+        if (index >= ARGNO_LIMIT - 1) {
             return raiseError("AssertedPositionalParameterName.index is too big");
         }
-
+        size_t newLength = index + 1;
         BINJS_TRY(positionalParams.get().resize(newLength));
         for (uint32_t i = prevLength; i < newLength; i++) {
             positionalParams.get()[i] = nullptr;
         }
     }
 
     if (positionalParams.get()[index]) {
         return raiseError("AssertedPositionalParameterName has duplicate entry for the same index");
--- a/js/src/frontend/BinSource.yaml
+++ b/js/src/frontend/BinSource.yaml
@@ -283,21 +283,20 @@ AssertedPositionalParameterName:
         name:
             after: |
                 // `positionalParams` vector can be shorter than the actual
                 // parameter length. Resize on demand.
                 // (see also ListOfAssertedMaybePositionalParameterName)
                 size_t prevLength = positionalParams.get().length();
                 if (index >= prevLength) {
                     // This is implementation limit, which is not in the spec.
-                    size_t newLength = index + 1;
-                    if (newLength >= ARGNO_LIMIT) {
+                    if (index >= ARGNO_LIMIT - 1) {
                         return raiseError("AssertedPositionalParameterName.index is too big");
                     }
-
+                    size_t newLength = index + 1;
                     BINJS_TRY(positionalParams.get().resize(newLength));
                     for (uint32_t i = prevLength; i < newLength; i++) {
                         positionalParams.get()[i] = nullptr;
                     }
                 }
 
                 if (positionalParams.get()[index]) {
                     return raiseError("AssertedPositionalParameterName has duplicate entry for the same index");
--- a/js/src/frontend/BinTokenReaderMultipart.cpp
+++ b/js/src/frontend/BinTokenReaderMultipart.cpp
@@ -491,16 +491,20 @@ BinTokenReaderMultipart::readInternalUin
         }
 
         result = newResult;
         shift += 7;
 
         if ((byte & 1) == 0) {
             return result;
         }
+
+        if (shift >= 32) {
+            return raiseError("Overflow during readInternalUint32");
+        }
     }
 }
 
 
 BinTokenReaderMultipart::AutoTaggedTuple::AutoTaggedTuple(BinTokenReaderMultipart& reader)
     : AutoBase(reader)
 { }
 
--- a/js/src/gc/GC.cpp
+++ b/js/src/gc/GC.cpp
@@ -1886,16 +1886,17 @@ GCRuntime::setObjectsTenuredCallback(JSO
 {
     tenuredCallback.op = callback;
     tenuredCallback.data = data;
 }
 
 void
 GCRuntime::callObjectsTenuredCallback()
 {
+    JS::AutoSuppressGCAnalysis nogc;
     if (tenuredCallback.op) {
         tenuredCallback.op(rt->mainContextFromOwnThread(), tenuredCallback.data);
     }
 }
 
 bool
 GCRuntime::addFinalizeCallback(JSFinalizeCallback callback, void* data)
 {
--- a/js/src/gc/RootMarking.cpp
+++ b/js/src/gc/RootMarking.cpp
@@ -77,16 +77,20 @@ static inline void
 TraceStackRoots(JSTracer* trc, JS::RootedListHeads& stackRoots)
 {
 #define TRACE_ROOTS(name, type, _) \
     TraceExactStackRootList<type*>(trc, stackRoots[JS::RootKind::name], "exact-" #name);
 JS_FOR_EACH_TRACEKIND(TRACE_ROOTS)
 #undef TRACE_ROOTS
     TraceExactStackRootList<jsid>(trc, stackRoots[JS::RootKind::Id], "exact-id");
     TraceExactStackRootList<Value>(trc, stackRoots[JS::RootKind::Value], "exact-value");
+
+    // ConcreteTraceable calls through a function pointer.
+    JS::AutoSuppressGCAnalysis nogc;
+
     TraceExactStackRootList<ConcreteTraceable>(
         trc, stackRoots[JS::RootKind::Traceable], "Traceable");
 }
 
 void
 JS::RootingContext::traceStackRoots(JSTracer* trc)
 {
     TraceStackRoots(trc, stackRoots_);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/self-test/oom-test-bug1497906.js
@@ -0,0 +1,27 @@
+// |jit-test| skip-if: !('oomTest' in this && 'stackTest' in this) || helperThreadCount() === 0
+
+// Check the we can run a stack test on all threads except worker threads while
+// a worker thread is running a OOM test at the same time.
+//
+// This test case uses setSharedObject to "synchronize" between a worker thread
+// and the main thread. This worker thread enters the oomTest section, while the
+// main thread waits. Then the worker thread remains in the oomTest section
+// until the main thread finish entering and existing its stackTest section.
+
+setSharedObject(0);
+evalInWorker(`
+    oomTest(() => {
+        if (getSharedObject() < 2) {
+            setSharedObject(1);
+            while (getSharedObject() != 2) {
+            }
+        }
+    });
+`);
+
+while (getSharedObject() != 1) {
+    // poor-man wait condition.
+}
+
+stackTest(() => 42);
+setSharedObject(2);
--- a/js/src/jit/arm64/Lowering-arm64.cpp
+++ b/js/src/jit/arm64/Lowering-arm64.cpp
@@ -14,35 +14,38 @@
 using namespace js;
 using namespace js::jit;
 
 using mozilla::FloorLog2;
 
 LBoxAllocation
 LIRGeneratorARM64::useBoxFixed(MDefinition* mir, Register reg1, Register, bool useAtStart)
 {
-    MOZ_CRASH("useBoxFixed");
+    MOZ_ASSERT(mir->type() == MIRType::Value);
+
+    ensureDefined(mir);
+    return LBoxAllocation(LUse(reg1, mir->virtualRegister(), useAtStart));
 }
 
 LAllocation
 LIRGeneratorARM64::useByteOpRegister(MDefinition* mir)
 {
-    MOZ_CRASH("useByteOpRegister");
+    return useRegister(mir);
 }
 
 LAllocation
 LIRGeneratorARM64::useByteOpRegisterAtStart(MDefinition* mir)
 {
-    MOZ_CRASH("useByteOpRegister");
+    return useRegisterAtStart(mir);
 }
 
 LAllocation
 LIRGeneratorARM64::useByteOpRegisterOrNonDoubleConstant(MDefinition* mir)
 {
-    MOZ_CRASH("useByteOpRegisterOrNonDoubleConstant");
+    return useRegisterOrNonDoubleConstant(mir);
 }
 
 void
 LIRGenerator::visitBox(MBox* box)
 {
     MDefinition* opd = box->getOperand(0);
 
     // If the operand is a constant, emit near its uses.
--- a/js/src/jsutil.cpp
+++ b/js/src/jsutil.cpp
@@ -33,134 +33,178 @@ mozilla::Atomic<AutoEnterOOMUnsafeRegion
 
 namespace oom {
 
 JS_PUBLIC_DATA(uint32_t) targetThread = 0;
 MOZ_THREAD_LOCAL(uint32_t) threadType;
 JS_PUBLIC_DATA(uint64_t) maxAllocations = UINT64_MAX;
 JS_PUBLIC_DATA(uint64_t) counter = 0;
 JS_PUBLIC_DATA(bool) failAlways = true;
+MOZ_THREAD_LOCAL(bool) isAllocationThread;
 
 JS_PUBLIC_DATA(uint32_t) stackTargetThread = 0;
 JS_PUBLIC_DATA(uint64_t) maxStackChecks = UINT64_MAX;
 JS_PUBLIC_DATA(uint64_t) stackCheckCounter = 0;
 JS_PUBLIC_DATA(bool) stackCheckFailAlways = true;
+MOZ_THREAD_LOCAL(bool) isStackCheckThread;
 
 JS_PUBLIC_DATA(uint32_t) interruptTargetThread = 0;
 JS_PUBLIC_DATA(uint64_t) maxInterruptChecks = UINT64_MAX;
 JS_PUBLIC_DATA(uint64_t) interruptCheckCounter = 0;
 JS_PUBLIC_DATA(bool) interruptCheckFailAlways = true;
+MOZ_THREAD_LOCAL(bool) isInterruptCheckThread;
 
 bool
 InitThreadType(void) {
-    return threadType.init();
+    return threadType.init() && isAllocationThread.init() &&
+        isStackCheckThread.init() && isInterruptCheckThread.init();
 }
 
 void
 SetThreadType(ThreadType type) {
     threadType.set(type);
+    isAllocationThread.set(false);
+    isStackCheckThread.set(false);
+    isInterruptCheckThread.set(false);
 }
 
 uint32_t
 GetThreadType(void) {
     return threadType.get();
 }
 
+uint32_t
+GetAllocationThreadType(void) {
+    if (isAllocationThread.get()) {
+        return js::THREAD_TYPE_CURRENT;
+    }
+    return threadType.get();
+}
+
+uint32_t
+GetStackCheckThreadType(void) {
+    if (isStackCheckThread.get()) {
+        return js::THREAD_TYPE_CURRENT;
+    }
+    return threadType.get();
+}
+
+uint32_t
+GetInterruptCheckThreadType(void) {
+    if (isInterruptCheckThread.get()) {
+        return js::THREAD_TYPE_CURRENT;
+    }
+    return threadType.get();
+}
+
 static inline bool
 IsHelperThreadType(uint32_t thread)
 {
-    return thread != THREAD_TYPE_NONE && thread != THREAD_TYPE_MAIN;
+    return thread != THREAD_TYPE_NONE && thread != THREAD_TYPE_MAIN &&
+        thread != THREAD_TYPE_CURRENT;
 }
 
 void
 SimulateOOMAfter(uint64_t allocations, uint32_t thread, bool always)
 {
     Maybe<AutoLockHelperThreadState> lock;
     if (IsHelperThreadType(targetThread) || IsHelperThreadType(thread)) {
         lock.emplace();
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     MOZ_ASSERT(counter + allocations > counter);
     MOZ_ASSERT(thread > js::THREAD_TYPE_NONE && thread < js::THREAD_TYPE_MAX);
     targetThread = thread;
+    if (thread == js::THREAD_TYPE_CURRENT) {
+        isAllocationThread.set(true);
+    }
     maxAllocations = counter + allocations;
     failAlways = always;
 }
 
 void
 ResetSimulatedOOM()
 {
     Maybe<AutoLockHelperThreadState> lock;
     if (IsHelperThreadType(targetThread)) {
         lock.emplace();
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     targetThread = THREAD_TYPE_NONE;
+    isAllocationThread.set(false);
     maxAllocations = UINT64_MAX;
     failAlways = false;
 }
 
 void
 SimulateStackOOMAfter(uint64_t checks, uint32_t thread, bool always)
 {
     Maybe<AutoLockHelperThreadState> lock;
     if (IsHelperThreadType(stackTargetThread) || IsHelperThreadType(thread)) {
         lock.emplace();
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     MOZ_ASSERT(stackCheckCounter + checks > stackCheckCounter);
     MOZ_ASSERT(thread > js::THREAD_TYPE_NONE && thread < js::THREAD_TYPE_MAX);
     stackTargetThread = thread;
+    if (thread == js::THREAD_TYPE_CURRENT) {
+        isStackCheckThread.set(true);
+    }
     maxStackChecks = stackCheckCounter + checks;
     stackCheckFailAlways = always;
 }
 
 void
 ResetSimulatedStackOOM()
 {
     Maybe<AutoLockHelperThreadState> lock;
     if (IsHelperThreadType(stackTargetThread)) {
         lock.emplace();
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     stackTargetThread = THREAD_TYPE_NONE;
+    isStackCheckThread.set(false);
     maxStackChecks = UINT64_MAX;
     stackCheckFailAlways = false;
 }
 
 void
 SimulateInterruptAfter(uint64_t checks, uint32_t thread, bool always)
 {
     Maybe<AutoLockHelperThreadState> lock;
     if (IsHelperThreadType(interruptTargetThread) || IsHelperThreadType(thread)) {
         lock.emplace();
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     MOZ_ASSERT(interruptCheckCounter + checks > interruptCheckCounter);
     MOZ_ASSERT(thread > js::THREAD_TYPE_NONE && thread < js::THREAD_TYPE_MAX);
     interruptTargetThread = thread;
+    if (thread == js::THREAD_TYPE_CURRENT) {
+        isInterruptCheckThread.set(true);
+    }
     maxInterruptChecks = interruptCheckCounter + checks;
     interruptCheckFailAlways = always;
 }
 
 void
 ResetSimulatedInterrupt()
 {
     Maybe<AutoLockHelperThreadState> lock;
     if (IsHelperThreadType(interruptTargetThread)) {
         lock.emplace();
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     interruptTargetThread = THREAD_TYPE_NONE;
+    isInterruptCheckThread.set(false);
     maxInterruptChecks = UINT64_MAX;
     interruptCheckFailAlways = false;
 }
 
 } // namespace oom
 } // namespace js
 #endif // defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
 
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -6396,27 +6396,29 @@ DisableGeckoProfiling(JSContext* cx, uns
 // objects on each read.  The alternatives are to cache created objects locally,
 // but this retains storage we don't need to retain, or to somehow clear the
 // mailbox locally, but this creates a coordination headache.  Buyer beware.
 
 enum class MailboxTag {
     Empty,
     SharedArrayBuffer,
     WasmMemory,
-    WasmModule
+    WasmModule,
+    Number,
 };
 
 struct SharedObjectMailbox
 {
     union Value {
         struct {
             SharedArrayRawBuffer* buffer;
             uint32_t              length;
         } sarb;
         const wasm::Module*       module;
+        double                    number;
     };
 
     SharedObjectMailbox() : tag(MailboxTag::Empty) {}
 
     MailboxTag tag;
     Value      val;
 };
 
@@ -6436,16 +6438,17 @@ static void
 DestructSharedObjectMailbox()
 {
     // All workers need to have terminated at this point.
 
     {
         auto mbx = sharedObjectMailbox->lock();
         switch (mbx->tag) {
           case MailboxTag::Empty:
+          case MailboxTag::Number:
             break;
           case MailboxTag::SharedArrayBuffer:
           case MailboxTag::WasmMemory:
             mbx->val.sarb.buffer->dropReference();
             break;
           case MailboxTag::WasmModule:
             mbx->val.module->Release();
             break;
@@ -6465,16 +6468,20 @@ GetSharedObject(JSContext* cx, unsigned 
     RootedObject newObj(cx);
 
     {
         auto mbx = sharedObjectMailbox->lock();
         switch (mbx->tag) {
           case MailboxTag::Empty: {
             break;
           }
+          case MailboxTag::Number: {
+            args.rval().setNumber(mbx->val.number);
+            return true;
+          }
           case MailboxTag::SharedArrayBuffer:
           case MailboxTag::WasmMemory: {
             // Flag was set in the sender; ensure it is set in the receiver.
             MOZ_ASSERT(cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled());
 
             // The protocol for creating a SAB requires the refcount to be
             // incremented prior to the SAB creation.
 
@@ -6584,28 +6591,33 @@ SetSharedObject(JSContext* cx, unsigned 
         } else if (obj->is<WasmModuleObject>()) {
             tag = MailboxTag::WasmModule;
             value.module = &obj->as<WasmModuleObject>().module();
             value.module->AddRef();
         } else {
             JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject");
             return false;
         }
+    } else if (args.get(0).isNumber()) {
+        tag = MailboxTag::Number;
+        value.number = args.get(0).toNumber();
+        // Nothing
     } else if (args.get(0).isNullOrUndefined()) {
         // Nothing
     } else {
         JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject");
         return false;
     }
 
     {
         auto mbx = sharedObjectMailbox->lock();
 
         switch (mbx->tag) {
           case MailboxTag::Empty:
+          case MailboxTag::Number:
             break;
           case MailboxTag::SharedArrayBuffer:
           case MailboxTag::WasmMemory:
             mbx->val.sarb.buffer->dropReference();
             break;
           case MailboxTag::WasmModule:
             mbx->val.module->Release();
             break;
--- a/js/src/tests/shell/mailbox.js
+++ b/js/src/tests/shell/mailbox.js
@@ -28,16 +28,19 @@ assertEq(getSharedObject() == null, fals
 
 var v = getSharedObject();
 assertEq(v.byteLength, mem.buffer.byteLength); // Looks like what we put in?
 var w = new Int32Array(v);
 mem[0] = 314159;
 assertEq(w[0], 314159);		// Shares memory (locally) with what we put in?
 mem[0] = 0;
 
+setSharedObject(3.14);	// Share numbers
+assertEq(getSharedObject(), 3.14);
+
 setSharedObject(null);	// Setting to null clears to null
 assertEq(getSharedObject(), null);
 
 setSharedObject(mem.buffer);
 setSharedObject(undefined); // Setting to undefined clears to null
 assertEq(getSharedObject(), null);
 
 setSharedObject(mem.buffer);
@@ -46,17 +49,16 @@ assertEq(getSharedObject(), null);
 
 // Non-shared objects cannot be stored in the mbx
 
 assertThrowsInstanceOf(() => setSharedObject({x:10, y:20}), Error);
 assertThrowsInstanceOf(() => setSharedObject([1,2]), Error);
 assertThrowsInstanceOf(() => setSharedObject(new ArrayBuffer(10)), Error);
 assertThrowsInstanceOf(() => setSharedObject(new Int32Array(10)), Error);
 assertThrowsInstanceOf(() => setSharedObject(false), Error);
-assertThrowsInstanceOf(() => setSharedObject(3.14), Error);
 assertThrowsInstanceOf(() => setSharedObject(mem), Error);
 assertThrowsInstanceOf(() => setSharedObject("abracadabra"), Error);
 assertThrowsInstanceOf(() => setSharedObject(() => 37), Error);
 
 // We can store wasm shared memories, too
 
 if (!this.WebAssembly || !wasmThreadsSupported()) {
     reportCompare(true, true);
--- a/js/src/wasm/WasmBaselineCompile.cpp
+++ b/js/src/wasm/WasmBaselineCompile.cpp
@@ -9774,17 +9774,17 @@ BaseCompiler::emitStructNew()
     const StructType& structType = env_.types[typeIndex].structType();
 
     pushI32(structType.moduleIndex_);
     emitInstanceCall(lineOrBytecode, SigPI_, ExprType::AnyRef, SymbolicAddress::StructNew);
 
     // Null pointer check.
 
     Label ok;
-    masm.branchTestPtr(Assembler::NotEqual, ReturnReg, ReturnReg, &ok);
+    masm.branchTestPtr(Assembler::NonZero, ReturnReg, ReturnReg, &ok);
     trap(Trap::ThrowReported);
     masm.bind(&ok);
 
     // As many arguments as there are fields.
 
     MOZ_ASSERT(args.length() == structType.fields_.length());
 
     // Optimization opportunity: Iterate backward to pop arguments off the
@@ -9903,17 +9903,17 @@ BaseCompiler::emitStructGet()
         return true;
     }
 
     const StructType& structType = env_.types[typeIndex].structType();
 
     RegPtr rp = popRef();
 
     Label ok;
-    masm.branchTestPtr(Assembler::NotEqual, rp, rp, &ok);
+    masm.branchTestPtr(Assembler::NonZero, rp, rp, &ok);
     trap(Trap::NullPointerDereference);
     masm.bind(&ok);
 
     if (!structType.isInline_) {
         masm.loadPtr(Address(rp, OutlineTypedObject::offsetOfData()), rp);
     }
 
     uint32_t offs = structType.fields_[fieldIndex].offset;
@@ -10008,17 +10008,17 @@ BaseCompiler::emitStructSet()
         break;
       default:
         MOZ_CRASH("Unexpected field type");
     }
 
     RegPtr rp = popRef();
 
     Label ok;
-    masm.branchTestPtr(Assembler::NotEqual, rp, rp, &ok);
+    masm.branchTestPtr(Assembler::NonZero, rp, rp, &ok);
     trap(Trap::NullPointerDereference);
     masm.bind(&ok);
 
     if (!structType.isInline_) {
         masm.loadPtr(Address(rp, OutlineTypedObject::offsetOfData()), rp);
     }
 
     uint32_t offs = structType.fields_[fieldIndex].offset;
@@ -10080,17 +10080,17 @@ BaseCompiler::emitStructNarrow()
         return true;
     }
 
     // Null pointers are just passed through.
 
     Label done;
     Label doTest;
     RegPtr rp = popRef();
-    masm.branchTestPtr(Assembler::NotEqual, rp, rp, &doTest);
+    masm.branchTestPtr(Assembler::NonZero, rp, rp, &doTest);
     pushRef(NULLREF_VALUE);
     masm.jump(&done);
 
     // AnyRef -> (ref T) must first unbox; leaves rp or null
 
     bool mustUnboxAnyref = inputType == ValType::AnyRef;
 
     // Dynamic downcast (ref T) -> (ref U), leaves rp or null
--- a/layout/forms/nsListControlFrame.cpp
+++ b/layout/forms/nsListControlFrame.cpp
@@ -31,16 +31,17 @@
 #include "mozilla/EventStates.h"
 #include "mozilla/LookAndFeel.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/TextEvents.h"
 #include <algorithm>
 
 using namespace mozilla;
+using namespace mozilla::dom;
 
 // Constants
 const uint32_t kMaxDropDownRows         = 20; // This matches the setting for 4.x browsers
 const int32_t kNothingSelected          = -1;
 
 // Static members
 nsListControlFrame * nsListControlFrame::mFocused = nullptr;
 nsString * nsListControlFrame::sIncrementalString = nullptr;
@@ -274,60 +275,64 @@ NS_QUERYFRAME_TAIL_INHERITING(nsHTMLScro
 #ifdef ACCESSIBILITY
 a11y::AccType
 nsListControlFrame::AccessibleType()
 {
   return a11y::eHTMLSelectListType;
 }
 #endif
 
-static nscoord
-GetMaxOptionBSize(nsIFrame* aContainer, WritingMode aWM)
+// Return true if we found at least one <option> or non-empty <optgroup> label
+// that has a frame.  aResult will be the maximum BSize of those.
+static bool
+GetMaxRowBSize(nsIFrame* aContainer, WritingMode aWM, nscoord* aResult)
 {
-  nscoord result = 0;
-  for (nsIFrame* option : aContainer->PrincipalChildList()) {
-    nscoord optionBSize;
-    if (HTMLOptGroupElement::FromNode(option->GetContent())) {
-      // An optgroup; drill through any scroll frame and recurse.  |frame| might
-      // be null here though if |option| is an anonymous leaf frame of some sort.
-      auto frame = option->GetContentInsertionFrame();
-      optionBSize = frame ? GetMaxOptionBSize(frame, aWM) : 0;
+  bool found = false;
+  for (nsIFrame* child : aContainer->PrincipalChildList()) {
+    if (child->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) {
+      // An optgroup; drill through any scroll frame and recurse.  |inner| might
+      // be null here though if |inner| is an anonymous leaf frame of some sort.
+      auto inner = child->GetContentInsertionFrame();
+      if (inner && GetMaxRowBSize(inner, aWM, aResult)) {
+        found = true;
+      }
     } else {
-      // an option
-      optionBSize = option->BSize(aWM);
+      // an option or optgroup label
+      bool isOptGroupLabel = child->Style()->IsPseudoElement() &&
+        aContainer->GetContent()->IsHTMLElement(nsGkAtoms::optgroup);
+      nscoord childBSize = child->BSize(aWM);
+      // XXX bug 1499176: skip empty <optgroup> labels (zero bsize) for now
+      if (!isOptGroupLabel || childBSize > nscoord(0)) {
+        found = true;
+        *aResult = std::max(childBSize, *aResult);
+      }
     }
-    if (result < optionBSize)
-      result = optionBSize;
   }
-  return result;
+  return found;
 }
 
 //-----------------------------------------------------------------
 // Main Reflow for ListBox/Dropdown
 //-----------------------------------------------------------------
 
 nscoord
 nsListControlFrame::CalcBSizeOfARow()
 {
   // Calculate the block size in our writing mode of a single row in the
   // listbox or dropdown list by using the tallest thing in the subtree,
   // since there may be option groups in addition to option elements,
   // either of which may be visible or invisible, may use different
   // fonts, etc.
-  int32_t blockSizeOfARow = GetMaxOptionBSize(GetOptionsContainer(),
-                                              GetWritingMode());
-
-  // Check to see if we have zero items (and optimize by checking
-  // blockSizeOfARow first)
-  if (blockSizeOfARow == 0 && GetNumberOfOptions() == 0) {
+  nscoord rowBSize(0);
+  if (!GetMaxRowBSize(GetOptionsContainer(), GetWritingMode(), &rowBSize)) {
+    // We don't have any <option>s or <optgroup> labels with a frame.
     float inflation = nsLayoutUtils::FontSizeInflationFor(this);
-    blockSizeOfARow = CalcFallbackRowBSize(inflation);
+    rowBSize = CalcFallbackRowBSize(inflation);
   }
-
-  return blockSizeOfARow;
+  return rowBSize;
 }
 
 nscoord
 nsListControlFrame::GetPrefISize(gfxContext *aRenderingContext)
 {
   nscoord result;
   DISPLAY_PREF_INLINE_SIZE(this, result);
 
@@ -1138,17 +1143,17 @@ nsListControlFrame::GetNonDisabledOption
     dom::HTMLSelectElement::FromNode(mContent);
 
   const uint32_t length = selectElement->Length();
   for (uint32_t i = std::max(aFromIndex, 0); i < length; ++i) {
     HTMLOptionElement* node = selectElement->Item(i);
     if (!node) {
       break;
     }
-    if (!selectElement->IsOptionDisabled(node)) {
+    if (IsOptionInteractivelySelectable(selectElement, node)) {
       if (aFoundIndex) {
         *aFoundIndex = i;
       }
       return node;
     }
   }
   return nullptr;
 }
@@ -1540,26 +1545,33 @@ nsListControlFrame::GetFrameName(nsAStri
 #endif
 
 nscoord
 nsListControlFrame::GetBSizeOfARow()
 {
   return BSizeOfARow();
 }
 
-nsresult
-nsListControlFrame::IsOptionDisabled(int32_t anIndex, bool &aIsDisabled)
+bool
+nsListControlFrame::IsOptionInteractivelySelectable(int32_t aIndex) const
 {
-  RefPtr<dom::HTMLSelectElement> sel =
-    dom::HTMLSelectElement::FromNode(mContent);
-  if (sel) {
-    sel->IsOptionDisabled(anIndex, &aIsDisabled);
-    return NS_OK;
+  if (HTMLSelectElement* sel = HTMLSelectElement::FromNode(mContent)) {
+    if (HTMLOptionElement* item = sel->Item(aIndex)) {
+      return IsOptionInteractivelySelectable(sel, item);
+    }
   }
-  return NS_ERROR_FAILURE;
+  return false;
+}
+
+bool
+nsListControlFrame::IsOptionInteractivelySelectable(HTMLSelectElement* aSelect,
+                                                    HTMLOptionElement* aOption)
+{
+  return !aSelect->IsOptionDisabled(aOption) &&
+         aOption->GetPrimaryFrame();
 }
 
 //----------------------------------------------------------------------
 // helper
 //----------------------------------------------------------------------
 bool
 nsListControlFrame::IsLeftButton(dom::Event* aMouseEvent)
 {
@@ -1655,20 +1667,18 @@ nsListControlFrame::MouseUp(dom::Event* 
     // depeneding on whether the clickCount is non-zero.
     // So we cheat here by either setting or unsetting the clcikCount in the native event
     // so the right thing happens for the onclick event
     WidgetMouseEvent* mouseEvent =
       aMouseEvent->WidgetEventPtr()->AsMouseEvent();
 
     int32_t selectedIndex;
     if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
-      // If it's disabled, disallow the click and leave.
-      bool isDisabled = false;
-      IsOptionDisabled(selectedIndex, isDisabled);
-      if (isDisabled) {
+      // If it's not selectable, disallow the click and leave.
+      if (!IsOptionInteractivelySelectable(selectedIndex)) {
         aMouseEvent->PreventDefault();
         aMouseEvent->StopPropagation();
         CaptureMouseEvents(false);
         return NS_ERROR_FAILURE;
       }
 
       if (kNothingSelected != selectedIndex) {
         AutoWeakFrame weakFrame(this);
@@ -2027,19 +2037,18 @@ nsListControlFrame::AdjustIndexForDisabl
   // make sure we start off in the range
   if (newIndex < bottom) {
     newIndex = 0;
   } else if (newIndex >= top) {
     newIndex = aNumOptions-1;
   }
 
   while (1) {
-    // if the newIndex isn't disabled, we are golden, bail out
-    bool isDisabled = true;
-    if (NS_SUCCEEDED(IsOptionDisabled(newIndex, isDisabled)) && !isDisabled) {
+    // if the newIndex is selectable, we are golden, bail out
+    if (IsOptionInteractivelySelectable(newIndex)) {
       break;
     }
 
     // it WAS disabled, so sart looking ahead for the next enabled option
     newIndex += aDoAdjustIncNext;
 
     // well, if we reach end reverse the search
     if (newIndex < bottom) {
--- a/layout/forms/nsListControlFrame.h
+++ b/layout/forms/nsListControlFrame.h
@@ -31,16 +31,17 @@
 class nsComboboxControlFrame;
 class nsPresContext;
 class nsListEventListener;
 
 namespace mozilla {
 namespace dom {
 class Event;
 class HTMLOptionElement;
+class HTMLSelectElement;
 class HTMLOptionsCollection;
 } // namespace dom
 } // namespace mozilla
 
 /**
  * Frame-based listbox.
  */
 
@@ -267,17 +268,27 @@ protected:
 
 
   /**
    * Toggles (show/hide) the combobox dropdown menu.
    * @note This method might destroy the frame, pres shell and other objects.
    */
   void DropDownToggleKey(mozilla::dom::Event* aKeyEvent);
 
-  nsresult   IsOptionDisabled(int32_t anIndex, bool &aIsDisabled);
+  /**
+   * @return true if the <option> at aIndex is selectable by the user.
+   */
+  bool IsOptionInteractivelySelectable(int32_t aIndex) const;
+  /**
+   * @return true if aOption in aSelect is selectable by the user.
+   */
+  static bool
+  IsOptionInteractivelySelectable(mozilla::dom::HTMLSelectElement* aSelect,
+                                  mozilla::dom::HTMLOptionElement* aOption);
+
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   void ScrollToFrame(HTMLOptionElement& aOptElement);
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   void ScrollToIndex(int32_t anIndex);
--- a/layout/forms/test/mochitest.ini
+++ b/layout/forms/test/mochitest.ini
@@ -61,8 +61,9 @@ skip-if = toolkit == 'android'
 skip-if = toolkit == 'android' #select elements don't use an in-page popup on Android
 [test_select_prevent_default.html]
 [test_select_vertical.html]
 skip-if = e10s || toolkit == 'android' # Bug 1170129 - vertical <select> popup not implemented for e10s # <select> elements don't use an in-page popup on Android
 [test_textarea_resize.html]
 skip-if = toolkit == 'android'
 [test_bug1327129.html]
 [test_readonly.html]
+[test_select_key_navigation_bug1498769.html]
new file mode 100644
--- /dev/null
+++ b/layout/forms/test/test_select_key_navigation_bug1498769.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1498769
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 1498769</title>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+  /** Test for Bug 1498769 **/
+
+  SimpleTest.waitForExplicitFinish();
+
+  function test() {
+    const kIsMac = navigator.platform.indexOf("Mac") == 0;
+    SimpleTest.waitForFocus(function() {
+      [...document.querySelectorAll('select')].forEach(function(e) {
+        e.focus();
+        is(e.selectedIndex, 1, "the 'selected' attribute is respected");
+        if (kIsMac && e.size == "1") {
+          // On OSX, UP/DOWN opens the dropdown menu rather than changing
+          // the value so we skip the rest of this test there in this case.
+          return;
+        }
+        synthesizeKey("VK_DOWN", {});
+        is(e.selectedIndex, 2, "VK_DOWN selected the first option below");
+        synthesizeKey("VK_UP", {});
+        is(e.selectedIndex, 0, "VK_UP skips the display:none/contents option");
+        synthesizeKey("VK_DOWN", {});
+        is(e.selectedIndex, 2, "VK_DOWN skips the display:none/contents option");
+      });
+      SimpleTest.finish();
+    });
+  }
+</script>
+</head>
+<body onload="test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1498769">Mozilla Bug 1498769</a>
+<div>
+  <select size="4">
+    <option>0</option>
+    <option selected style="display:none">1</option>
+    <option>2</option>
+    <option>3</option>
+  </select>
+  <select size="4">
+    <option>0</option>
+    <option selected style="display:contents">1</option>
+    <option>2</option>
+    <option>3</option>
+  </select>
+  <select size="4">
+    <option>0</option>
+    <optgroup label="group" style="display:none">
+      <option selected>1</option>
+    </optgroup>
+    <option>2</option>
+    <option>3</option>
+  </select>
+  <select size="4">
+    <option>0</option>
+    <optgroup label="group" style="display:contents">
+      <option selected>1</option>
+    </optgroup>
+    <option>2</option>
+    <option>3</option>
+  </select>
+  <select size="4">
+    <option>0</option>
+    <optgroup label="group" style="display:contents">
+      <option selected style="display:none">1</option>
+    </optgroup>
+    <option>2</option>
+    <option>3</option>
+  </select>
+
+<!-- Same as above but with size="1" -->
+
+  <select size="1">
+    <option>0</option>
+    <option selected style="display:none">1</option>
+    <option>2</option>
+    <option>3</option>
+  </select>
+  <select size="1">
+    <option>0</option>
+    <option selected style="display:contents">1</option>
+    <option>2</option>
+    <option>3</option>
+  </select>
+  <select size="1">
+    <option>0</option>
+    <optgroup label="group" style="display:none">
+      <option selected>1</option>
+    </optgroup>
+    <option>2</option>
+    <option>3</option>
+  </select>
+  <select size="1">
+    <option>0</option>
+    <optgroup label="group" style="display:contents">
+      <option selected>1</option>
+    </optgroup>
+    <option>2</option>
+    <option>3</option>
+  </select>
+  <select size="1">
+    <option>0</option>
+    <optgroup label="group" style="display:contents">
+      <option selected style="display:none">1</option>
+    </optgroup>
+    <option>2</option>
+    <option>3</option>
+  </select>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -51,16 +51,17 @@ import org.mozilla.gecko.text.TextSelect
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.ColorUtil;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.PackageUtil;
 import org.mozilla.gecko.webapps.WebApps;
 import org.mozilla.gecko.widget.ActionModePresenter;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
+import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.GeckoView;
 
 import java.util.List;
 
 public class CustomTabsActivity extends AppCompatActivity
@@ -578,28 +579,28 @@ public class CustomTabsActivity extends 
 
     @Override
     public void onCanGoForward(GeckoSession session, boolean canGoForward) {
         mCanGoForward = canGoForward;
         updateMenuItemForward();
     }
 
     @Override
-    public GeckoResult<Boolean> onLoadRequest(final GeckoSession session, final String urlStr,
-                                              final int target,
-                                              final int flags) {
+    public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session, final String urlStr,
+                                                  final int target,
+                                                  final int flags) {
         if (target != GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW) {
-            return GeckoResult.fromValue(false);
+            return GeckoResult.fromValue(AllowOrDeny.ALLOW);
         }
 
         final Uri uri = Uri.parse(urlStr);
         if (uri == null) {
             // We can't handle this, so deny it.
             Log.w(LOGTAG, "Failed to parse URL for navigation: " + urlStr);
-            return GeckoResult.fromValue(true);
+            return GeckoResult.fromValue(AllowOrDeny.DENY);
         }
 
         // Always use Fennec for these schemes.
         if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme()) ||
             "data".equals(uri.getScheme()) || "blob".equals(uri.getScheme())) {
             final Intent intent = new Intent(this, BrowserApp.class);
             intent.setAction(Intent.ACTION_VIEW);
             intent.setData(uri);
@@ -614,17 +615,17 @@ public class CustomTabsActivity extends 
             intent.setData(uri);
             try {
                 startActivity(intent);
             } catch (ActivityNotFoundException e) {
                 Log.w(LOGTAG, "No activity handler found for: " + urlStr);
             }
         }
 
-        return GeckoResult.fromValue(true);
+        return GeckoResult.fromValue(AllowOrDeny.DENY);
     }
 
     @Override
     public GeckoResult<GeckoSession> onNewSession(final GeckoSession session, final String uri) {
         // We should never get here because we abort loads that need a new session in onLoadRequest()
         throw new IllegalStateException("Unexpected new session");
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -33,16 +33,17 @@ import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.customtabs.CustomTabsActivity;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.prompts.PromptService;
 import org.mozilla.gecko.text.TextSelection;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.ColorUtil;
 import org.mozilla.gecko.widget.ActionModePresenter;
+import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.GeckoView;
 
 public class WebAppActivity extends AppCompatActivity
                             implements ActionModePresenter,
                                        GeckoSession.ContentDelegate,
@@ -372,30 +373,30 @@ public class WebAppActivity extends AppC
     }
 
     @Override // GeckoSession.ContentDelegate
     public void onFullScreen(GeckoSession session, boolean fullScreen) {
         updateFullScreenContent(fullScreen);
     }
 
     @Override
-    public GeckoResult<Boolean> onLoadRequest(final GeckoSession session, final String urlStr,
-                                              final int target,
-                                              final int flags) {
+    public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session, final String urlStr,
+                                                  final int target,
+                                                  final int flags) {
         final Uri uri = Uri.parse(urlStr);
         if (uri == null) {
             // We can't really handle this, so deny it?
             Log.w(LOGTAG, "Failed to parse URL for navigation: " + urlStr);
-            return GeckoResult.fromValue(true);
+            return GeckoResult.fromValue(AllowOrDeny.DENY);
         }
 
         if (mManifest.isInScope(uri) && target != TARGET_WINDOW_NEW) {
             // This is in scope and wants to load in the same frame, so
             // let Gecko handle it.
-            return GeckoResult.fromValue(false);
+            return GeckoResult.fromValue(AllowOrDeny.ALLOW);
         }
 
         if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme()) ||
             "data".equals(uri.getScheme()) || "blob".equals(uri.getScheme())) {
             final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder()
                 .addDefaultShareMenuItem()
                 .setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left)
                 .setExitAnimations(this, R.anim.slide_in_left, R.anim.slide_out_right);
@@ -414,17 +415,17 @@ public class WebAppActivity extends AppC
             intent.setData(uri);
             try {
                 startActivity(intent);
             } catch (ActivityNotFoundException e) {
                 Log.w(LOGTAG, "No activity handler found for: " + urlStr);
             }
         }
 
-        return GeckoResult.fromValue(true);
+        return GeckoResult.fromValue(AllowOrDeny.DENY);
     }
 
     @Override
     public GeckoResult<GeckoSession> onNewSession(final GeckoSession session, final String uri) {
         // We should never get here because we abort loads that need a new session in onLoadRequest()
         throw new IllegalStateException("Unexpected new session");
     }
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -1,16 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
 import android.app.assist.AssistStructure
 import android.os.Build
+import org.mozilla.geckoview.AllowOrDeny
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.util.Callbacks
@@ -50,17 +51,17 @@ class ContentDelegateTest : BaseSessionT
 
     @Test fun download() {
         sessionRule.session.loadTestPath(DOWNLOAD_HTML_PATH)
 
         sessionRule.waitUntilCalled(object : Callbacks.NavigationDelegate, Callbacks.ContentDelegate {
 
             @AssertCalled(count = 2)
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 return null
             }
 
             @AssertCalled(false)
             override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
                 return null
             }
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
 import org.mozilla.gecko.util.GeckoBundle
+import org.mozilla.geckoview.AllowOrDeny
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.GeckoSessionSettings
 import org.mozilla.geckoview.GeckoSession.TrackingProtectionDelegate;
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.NullDelegate
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.Setting
@@ -31,17 +32,17 @@ class NavigationDelegateTest : BaseSessi
 
     fun testLoadErrorWithErrorPage(testUri: String, expectedCategory: Int,
                                    expectedError: Int,
                                    errorPageUrl: String?) {
         sessionRule.delegateDuringNextWait(
                 object : Callbacks.ProgressDelegate, Callbacks.NavigationDelegate, Callbacks.ContentDelegate {
                     @AssertCalled(count = 1, order = [1])
                     override fun onLoadRequest(session: GeckoSession, uri: String,
-                                               where: Int, flags: Int): GeckoResult<Boolean>? {
+                                               where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                         assertThat("URI should be " + testUri, uri, equalTo(testUri))
                         return null
                     }
 
                     @AssertCalled(count = 1, order = [2])
                     override fun onPageStart(session: GeckoSession, url: String) {
                         assertThat("URI should be " + testUri, url, equalTo(testUri))
                     }
@@ -216,17 +217,17 @@ class NavigationDelegateTest : BaseSessi
         }
 
         sessionRule.session.loadUri(uri)
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
             @AssertCalled(count = 2, order = [1, 2])
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URI should not be null", uri, notNullValue())
                 assertThat("URL should match", uri,
                         equalTo(forEachCall(uri, redirectUri)))
                 assertThat("Where should not be null", where, notNullValue())
                 assertThat("Where should match", where,
                         equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 return null
@@ -395,17 +396,17 @@ class NavigationDelegateTest : BaseSessi
 
     @Test fun load() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URI should not be null", uri, notNullValue())
                 assertThat("URI should match", uri, endsWith(HELLO_HTML_PATH))
                 assertThat("Where should not be null", where, notNullValue())
                 assertThat("Where should match", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 return null
             }
@@ -588,17 +589,17 @@ class NavigationDelegateTest : BaseSessi
         sessionRule.waitForPageStop()
 
         sessionRule.session.reload()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("URI should match", uri, endsWith(HELLO_HTML_PATH))
                 assertThat("Where should match", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 return null
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onLocationChange(session: GeckoSession, url: String) {
@@ -637,17 +638,17 @@ class NavigationDelegateTest : BaseSessi
         })
 
         sessionRule.session.goBack()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("URI should match", uri, endsWith(HELLO_HTML_PATH))
                 assertThat("Where should match", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 return null
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onLocationChange(session: GeckoSession, url: String) {
@@ -671,17 +672,17 @@ class NavigationDelegateTest : BaseSessi
         })
 
         sessionRule.session.goForward()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("URI should match", uri, endsWith(HELLO2_HTML_PATH))
                 assertThat("Where should match", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 return null
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onLocationChange(session: GeckoSession, url: String) {
@@ -704,18 +705,24 @@ class NavigationDelegateTest : BaseSessi
             }
         })
     }
 
     @Test fun onLoadUri_returnTrueCancelsLoad() {
         sessionRule.delegateDuringNextWait(object : Callbacks.NavigationDelegate {
             @AssertCalled(count = 2)
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean> {
-                return GeckoResult.fromValue(uri.endsWith(HELLO_HTML_PATH))
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny> {
+                val res : AllowOrDeny
+                if (uri.endsWith(HELLO_HTML_PATH)) {
+                    res = AllowOrDeny.DENY
+                } else {
+                    res = AllowOrDeny.ALLOW
+                }
+                return GeckoResult.fromValue(res)
             }
         })
 
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.session.loadTestPath(HELLO2_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
@@ -739,17 +746,17 @@ class NavigationDelegateTest : BaseSessi
         sessionRule.session.loadTestPath(NEW_SESSION_HTML_PATH)
         sessionRule.session.waitForPageStop()
 
         sessionRule.session.evaluateJS("window.open('newSession_child.html', '_blank')")
 
         sessionRule.session.waitUntilCalled(object : Callbacks.NavigationDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("URI should be correct", uri, endsWith(NEW_SESSION_CHILD_HTML_PATH))
                 assertThat("Where should be correct", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW))
                 return null
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
@@ -769,17 +776,17 @@ class NavigationDelegateTest : BaseSessi
 
         sessionRule.session.evaluateJS("$('#targetBlankLink').click()")
 
         sessionRule.session.waitUntilCalled(object : Callbacks.NavigationDelegate {
             // We get two onLoadRequest calls for the link click,
             // one when loading the URL and one when opening a new window.
             @AssertCalled(count = 2, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("URI should be correct", uri, endsWith(NEW_SESSION_CHILD_HTML_PATH))
                 assertThat("Where should be correct", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW))
                 return null
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
@@ -865,32 +872,38 @@ class NavigationDelegateTest : BaseSessi
         // Disable popup blocker.
         sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false))
 
         sessionRule.session.loadTestPath(NEW_SESSION_HTML_PATH)
         sessionRule.session.waitForPageStop()
 
         sessionRule.session.delegateDuringNextWait(object : Callbacks.NavigationDelegate {
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean> {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny> {
                 // Pretend we handled the target="_blank" link click.
-                return GeckoResult.fromValue(uri.endsWith(NEW_SESSION_CHILD_HTML_PATH))
+                val res : AllowOrDeny
+                if (uri.endsWith(NEW_SESSION_CHILD_HTML_PATH)) {
+                    res = AllowOrDeny.DENY
+                } else {
+                    res = AllowOrDeny.ALLOW
+                }
+                return GeckoResult.fromValue(res)
             }
         })
 
         sessionRule.session.evaluateJS("$('#targetBlankLink').click()")
 
         sessionRule.session.reload()
         sessionRule.session.waitForPageStop()
 
         // Assert that onNewSession was not called for the link click.
         sessionRule.session.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
             @AssertCalled(count = 2)
             override fun onLoadRequest(session: GeckoSession, uri: String,
-                                       where: Int, flags: Int): GeckoResult<Boolean>? {
+                                       where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("URI must match", uri,
                            endsWith(forEachCall(NEW_SESSION_CHILD_HTML_PATH, NEW_SESSION_HTML_PATH)))
                 return null
             }
 
             @AssertCalled(count = 0)
             override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
                 return null
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PromptDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PromptDelegateTest.kt
@@ -1,10 +1,11 @@
 package org.mozilla.geckoview.test
 
+import org.mozilla.geckoview.AllowOrDeny
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 import org.mozilla.geckoview.test.util.Callbacks
 
 import android.support.test.filters.MediumTest
@@ -20,40 +21,40 @@ import org.junit.runner.RunWith
 class PromptDelegateTest : BaseSessionTest() {
     @Test fun popupTest() {
         // Ensure popup blocking is enabled for this test.
         sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to true))
         sessionRule.session.loadTestPath(POPUP_HTML_PATH)
 
         sessionRule.waitUntilCalled(object : Callbacks.PromptDelegate {
             @AssertCalled(count = 1)
-            override fun onPopupRequest(session: GeckoSession, targetUri: String): GeckoResult<Boolean>? {
+            override fun onPopupRequest(session: GeckoSession, targetUri: String): GeckoResult<AllowOrDeny>? {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URL should not be null", targetUri, notNullValue())
                 assertThat("URL should match", targetUri, endsWith(HELLO_HTML_PATH))
                 return null
             }
         })
     }
 
     @Test fun popupTestAllow() {
         // Ensure popup blocking is enabled for this test.
         sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to true))
 
         sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate, Callbacks.NavigationDelegate {
             @AssertCalled(count = 1)
-            override fun onPopupRequest(session: GeckoSession, targetUri: String): GeckoResult<Boolean>? {
+            override fun onPopupRequest(session: GeckoSession, targetUri: String): GeckoResult<AllowOrDeny>? {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URL should not be null", targetUri, notNullValue())
                 assertThat("URL should match", targetUri, endsWith(HELLO_HTML_PATH))
-                return GeckoResult.fromValue(true)
+                return GeckoResult.fromValue(AllowOrDeny.ALLOW)
             }
 
             @AssertCalled(count = 2)
-            override fun onLoadRequest(session: GeckoSession, uri: String, where: Int, flags: Int): GeckoResult<Boolean>? {
+            override fun onLoadRequest(session: GeckoSession, uri: String, where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URL should not be null", uri, notNullValue())
                 assertThat("URL should match", uri, endsWith(forEachCall(POPUP_HTML_PATH, HELLO_HTML_PATH)))
                 return null
             }
         })
 
         sessionRule.session.loadTestPath(POPUP_HTML_PATH)
@@ -61,25 +62,25 @@ class PromptDelegateTest : BaseSessionTe
     }
 
     @Test fun popupTestBlock() {
         // Ensure popup blocking is enabled for this test.
         sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to true))
 
         sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate, Callbacks.NavigationDelegate {
             @AssertCalled(count = 1)
-            override fun onPopupRequest(session: GeckoSession, targetUri: String): GeckoResult<Boolean>? {
+            override fun onPopupRequest(session: GeckoSession, targetUri: String): GeckoResult<AllowOrDeny>? {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URL should not be null", targetUri, notNullValue())
                 assertThat("URL should match", targetUri, endsWith(HELLO_HTML_PATH))
-                return GeckoResult.fromValue(false)
+                return GeckoResult.fromValue(AllowOrDeny.DENY)
             }
 
             @AssertCalled(count = 1)
-            override fun onLoadRequest(session: GeckoSession, uri: String, where: Int, flags: Int): GeckoResult<Boolean>? {
+            override fun onLoadRequest(session: GeckoSession, uri: String, where: Int, flags: Int): GeckoResult<AllowOrDeny>? {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URL should not be null", uri, notNullValue())
                 assertThat("URL should match", uri, endsWith(POPUP_HTML_PATH))
                 return null
             }
 
             @AssertCalled(count = 0)
             override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.geckoview.test;
 
+import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.GeckoDisplay;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.GeckoView;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 
@@ -45,20 +46,20 @@ public class TestRunnerActivity extends 
         }
 
         @Override
         public void onCanGoForward(GeckoSession session, boolean canGoForward) {
 
         }
 
         @Override
-        public GeckoResult<Boolean> onLoadRequest(GeckoSession session, String uri, int target,
-                                                  int flags) {
+        public GeckoResult<AllowOrDeny> onLoadRequest(GeckoSession session, String uri,
+                                                      int target, int flags) {
             // Allow Gecko to load all URIs
-            return GeckoResult.fromValue(false);
+            return GeckoResult.fromValue(AllowOrDeny.ALLOW);
         }
 
         @Override
         public GeckoResult<GeckoSession> onNewSession(GeckoSession session, String uri) {
             return GeckoResult.fromValue(createBackgroundSession(session.getSettings()));
         }
 
         @Override
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.geckoview.test.util
 
+import org.mozilla.geckoview.AllowOrDeny
 import org.mozilla.geckoview.GeckoResponse
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 
 import android.view.inputmethod.CursorAnchorInfo
 import android.view.inputmethod.ExtractedText
 import android.view.inputmethod.ExtractedTextRequest
 
@@ -49,17 +50,17 @@ class Callbacks private constructor() {
 
         override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
         }
 
         override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
         }
 
         override fun onLoadRequest(session: GeckoSession, uri: String, where: Int,
-                                   flags: Int): GeckoResult<Boolean>? {
+                                   flags: Int): GeckoResult<AllowOrDeny>? {
             return null
         }
 
         override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
             return null
         }
 
         override fun onLoadError(session: GeckoSession, uri: String?,
@@ -124,17 +125,17 @@ class Callbacks private constructor() {
         override fun onDateTimePrompt(session: GeckoSession, title: String, type: Int, value: String, min: String, max: String, callback: GeckoSession.PromptDelegate.TextCallback) {
             callback.dismiss()
         }
 
         override fun onFilePrompt(session: GeckoSession, title: String, type: Int, mimeTypes: Array<out String>, callback: GeckoSession.PromptDelegate.FileCallback) {
             callback.dismiss()
         }
 
-        override fun onPopupRequest(session: GeckoSession, targetUri: String): GeckoResult<Boolean>? {
+        override fun onPopupRequest(session: GeckoSession, targetUri: String): GeckoResult<AllowOrDeny>? {
             return null
         }
     }
 
     interface ScrollDelegate : GeckoSession.ScrollDelegate {
         override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
         }
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AllowOrDeny.java
@@ -0,0 +1,14 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * 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/. */
+
+package org.mozilla.geckoview;
+
+/**
+ * This represents a decision to allow or deny a request.
+ */
+public enum AllowOrDeny {
+    ALLOW, DENY;
+}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResult.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResult.java
@@ -144,16 +144,26 @@ public class GeckoResult<T> {
     private static final String LOGTAG = "GeckoResult";
 
     public static final class UncaughtException extends RuntimeException {
         public UncaughtException(final Throwable cause) {
             super(cause);
         }
     }
 
+    /**
+     * A GeckoResult that resolves to AllowOrDeny.ALLOW
+     */
+    public static final GeckoResult<AllowOrDeny> ALLOW = GeckoResult.fromValue(AllowOrDeny.ALLOW);
+
+    /**
+     * A GeckoResult that resolves to AllowOrDeny.DENY
+     */
+    public static final GeckoResult<AllowOrDeny> DENY = GeckoResult.fromValue(AllowOrDeny.DENY);
+
     private Handler mHandler;
     private boolean mComplete;
     private T mValue;
     private Throwable mError;
     private boolean mIsUncaughtError;
     private ArrayList<Runnable> mListeners;
 
     /**
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -313,29 +313,35 @@ public class GeckoSession extends LayerS
 
                         delegate.onLoadError(GeckoSession.this, uri,
                                              NavigationDelegate.ERROR_CATEGORY_URI,
                                              NavigationDelegate.ERROR_MALFORMED_URI);
 
                         return;
                     }
 
-                    final GeckoResult<Boolean> result =
+                    final GeckoResult<AllowOrDeny> result =
                         delegate.onLoadRequest(GeckoSession.this, uri, where, flags);
 
                     if (result == null) {
                         callback.sendSuccess(null);
                         return;
                     }
 
-                    result.then(new GeckoResult.OnValueListener<Boolean, Void>() {
+                    result.then(new GeckoResult.OnValueListener<AllowOrDeny, Void>() {
                         @Override
-                        public GeckoResult<Void> onValue(Boolean value) throws Throwable {
+                        public GeckoResult<Void> onValue(AllowOrDeny value) throws Throwable {
                             ThreadUtils.assertOnUiThread();
-                            callback.sendSuccess(value);
+                            if (value == AllowOrDeny.ALLOW) {
+                                callback.sendSuccess(false);
+                            } else  if (value == AllowOrDeny.DENY) {
+                                callback.sendSuccess(true);
+                            } else {
+                                callback.sendError("Invalid response");
+                            }
                             return null;
                         }
                     }, new GeckoResult.OnExceptionListener<Void>() {
                         @Override
                         public GeckoResult<Void> onException(Throwable exception) throws Throwable {
                             callback.sendError(exception.getMessage());
                             return null;
                         }
@@ -2036,28 +2042,34 @@ public class GeckoSession extends LayerS
                         }
                     }
                     mimeTypes = combined.toArray(new String[combined.size()]);
                 }
                 delegate.onFilePrompt(session, title, intMode, mimeTypes, cb);
                 break;
             }
             case "popup": {
-                GeckoResult<Boolean> res = delegate.onPopupRequest(session, message.getString("targetUri"));
+                GeckoResult<AllowOrDeny> res = delegate.onPopupRequest(session, message.getString("targetUri"));
 
                 if (res == null) {
                     // Keep the popup blocked if the delegate returns null
                     callback.sendSuccess(false);
                     return;
                 }
 
-                res.then(new GeckoResult.OnValueListener<Boolean, Void>() {
+                res.then(new GeckoResult.OnValueListener<AllowOrDeny, Void>() {
                     @Override
-                    public GeckoResult<Void> onValue(Boolean value) throws Throwable {
-                        callback.sendSuccess(value);
+                    public GeckoResult<Void> onValue(AllowOrDeny value) throws Throwable {
+                        if (value == AllowOrDeny.ALLOW) {
+                            callback.sendSuccess(true);
+                        } else if (value == AllowOrDeny.DENY) {
+                            callback.sendSuccess(false);
+                        } else {
+                            callback.sendError("Invalid response");
+                        }
                         return null;
                     }
                 }, new GeckoResult.OnExceptionListener<Void>() {
                     @Override
                     public GeckoResult<Void> onException(Throwable exception) throws Throwable {
                         callback.sendError("Failed to get popup-blocking decision");
                         return null;
                     }
@@ -2564,25 +2576,25 @@ public class GeckoSession extends LayerS
          * @param session The GeckoSession that initiated the callback.
          * @param uri The URI to be loaded.
          * @param target The target where the window has requested to open.
          *               One of {@link #TARGET_WINDOW_NONE TARGET_WINDOW_*}.
          * @param flags The load request flags.
          *              One or more of {@link #LOAD_REQUEST_IS_USER_TRIGGERED
          *              LOAD_REQUEST_*}.
          *
-         * @return A {@link GeckoResult} with a boolean value which indicates whether or
-         *         not the load was handled. If unhandled, Gecko will continue the
+         * @return A {@link GeckoResult} with a AllowOrDeny value which indicates whether
+         *         or not the load was handled. If unhandled, Gecko will continue the
          *         load as normal. If handled (true value), Gecko will abandon the load.
          *         A null return value is interpreted as false (unhandled).
          */
-        @Nullable GeckoResult<Boolean> onLoadRequest(@NonNull GeckoSession session,
-                                                     @NonNull String uri,
-                                                     @TargetWindow int target,
-                                                     @LoadRequestFlags int flags);
+        @Nullable GeckoResult<AllowOrDeny> onLoadRequest(@NonNull GeckoSession session,
+                                                         @NonNull String uri,
+                                                         @TargetWindow int target,
+                                                         @LoadRequestFlags int flags);
 
         /**
         * A request has been made to open a new session. The URI is provided only for
         * informational purposes. Do not call GeckoSession.loadUri() here. Additionally, the
         * returned GeckoSession must be a newly-created one.
         *
         * @param session The GeckoSession that initiated the callback.
         * @param uri The URI to be loaded.
@@ -3137,20 +3149,20 @@ public class GeckoSession extends LayerS
 
         /**
          * Display a popup request prompt; this occurs when content attempts to open
          * a new window in a way that doesn't appear to be the result of user input.
          *
          * @param session GeckoSession that triggered the prompt
          * @param targetUri The target URI for the popup
          *
-         * @return A {@link GeckoResult} resolving to a Boolean which indicates
+         * @return A {@link GeckoResult} resolving to a AllowOrDeny which indicates
          *         whether or not the popup should be allowed to open.
          */
-        GeckoResult<Boolean> onPopupRequest(GeckoSession session, String targetUri);
+        GeckoResult<AllowOrDeny> onPopupRequest(GeckoSession session, String targetUri);
     }
 
     /**
      * GeckoSession applications implement this interface to handle content scroll
      * events.
      **/
     public interface ScrollDelegate {
         /**
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
@@ -43,16 +43,17 @@ import android.widget.TimePicker;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
 
+import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource;
 
 final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
     protected static final String LOGTAG = "BasicGeckoViewPrompt";
 
     private final Activity mActivity;
@@ -911,12 +912,12 @@ final class BasicGeckoViewPrompt impleme
 
     public void onMediaPrompt(final GeckoSession session, final String title,
                                final MediaSource[] video, final MediaSource[] audio,
                                final GeckoSession.PermissionDelegate.MediaCallback callback) {
         onMediaPrompt(session, title, video, audio, null, null, callback);
     }
 
     @Override
-    public GeckoResult<Boolean> onPopupRequest(final GeckoSession session, final String targetUri) {
-        return GeckoResult.fromValue(true);
+    public GeckoResult<AllowOrDeny> onPopupRequest(final GeckoSession session, final String targetUri) {
+        return GeckoResult.fromValue(AllowOrDeny.ALLOW);
     }
 }
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.geckoview_example;
 
+import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.BasicSelectionActionDelegate;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSession.TrackingProtectionDelegate;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.GeckoView;
@@ -626,21 +627,21 @@ public class GeckoViewActivity extends A
         }
 
         @Override
         public void onCanGoForward(GeckoSession session, boolean canGoForward) {
             mCanGoForward = canGoForward;
         }
 
         @Override
-        public GeckoResult<Boolean> onLoadRequest(final GeckoSession session, final String uri,
-                                                  final int target, final int flags) {
+        public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session, final String uri,
+                                                       final int target, final int flags) {
             Log.d(LOGTAG, "onLoadRequest=" + uri + " where=" + target +
                   " flags=" + flags);
-            return GeckoResult.fromValue(false);
+            return GeckoResult.fromValue(AllowOrDeny.ALLOW);
         }
 
         @Override
         public GeckoResult<GeckoSession> onNewSession(final GeckoSession session, final String uri) {
             GeckoSession newSession = new GeckoSession(session.getSettings());
 
             Intent intent = new Intent(GeckoViewActivity.this, SessionActivity.class);
             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
--- a/servo/components/style/build_gecko.rs
+++ b/servo/components/style/build_gecko.rs
@@ -12,17 +12,17 @@ mod common {
     }
 }
 
 #[cfg(feature = "bindgen")]
 mod bindings {
     use bindgen::{Builder, CodegenConfig};
     use regex::Regex;
     use std::cmp;
-    use std::collections::{HashMap, HashSet};
+    use std::collections::HashSet;
     use std::env;
     use std::fs::{self, File};
     use std::io::{Read, Write};
     use std::path::{Path, PathBuf};
     use std::process::{exit, Command};
     use std::slice;
     use std::sync::Mutex;
     use std::time::SystemTime;
@@ -38,17 +38,17 @@ mod bindings {
         println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
         update_last_modified(&path);
 
         let mut contents = String::new();
         File::open(path)
             .expect("Failed to open config file")
             .read_to_string(&mut contents)
             .expect("Failed to read config file");
-        match toml::from_str::<toml::value::Table>(&contents) {
+        match toml::from_str::<Table>(&contents) {
             Ok(result) => result,
             Err(e) => panic!("Failed to parse config file: {}", e),
         }
     }
 
     lazy_static! {
         static ref CONFIG: Table = {
             // Load Gecko's binding generator config from the source tree.
@@ -255,18 +255,17 @@ mod bindings {
             .unwrap()
             .write_all(&bytes)
             .expect("Unable to write output");
     }
 
     fn get_types(filename: &str, macro_pat: &str) -> Vec<(String, String)> {
         // Read the file
         let path = DISTDIR_PATH.join("include/mozilla/").join(filename);
-        let mut list_file = File::open(path)
-            .expect(&format!("Unable to open {}", filename));
+        let mut list_file = File::open(path).expect(&format!("Unable to open {}", filename));
         let mut content = String::new();
         list_file
             .read_to_string(&mut content)
             .expect(&format!("Failed to read {}", filename));
         // Remove comments
         let block_comment_re = Regex::new(r#"(?s)/\*.*?\*/"#).unwrap();
         let line_comment_re = Regex::new(r#"//.*"#).unwrap();
         let content = block_comment_re.replace_all(&content, "");
@@ -274,34 +273,29 @@ mod bindings {
         // Extract the list
         let re_string = format!(r#"^({})\(.+,\s*(\w+)\)$"#, macro_pat);
         let re = Regex::new(&re_string).unwrap();
         content
             .lines()
             .map(|line| line.trim())
             .filter(|line| !line.is_empty())
             .map(|line| {
-                let captures = re.captures(&line)
-                    .expect(&format!(
-                        "Unrecognized line in {}: '{}'",
-                        filename,
-                        line
-                    ));
+                let captures = re
+                    .captures(&line)
+                    .expect(&format!("Unrecognized line in {}: '{}'", filename, line));
                 let macro_name = captures.get(1).unwrap().as_str().to_string();
                 let type_name = captures.get(2).unwrap().as_str().to_string();
                 (macro_name, type_name)
             }).collect()
     }
 
     fn get_borrowed_types() -> Vec<(bool, String)> {
         get_types("BorrowedTypeList.h", "GECKO_BORROWED_TYPE(?:_MUT)?")
             .into_iter()
-            .map(|(macro_name, type_name)| {
-                (macro_name.ends_with("MUT"), type_name)
-            })
+            .map(|(macro_name, type_name)| (macro_name.ends_with("MUT"), type_name))
             .collect()
     }
 
     fn get_arc_types() -> Vec<String> {
         get_types("ServoArcTypeList.h", "SERVO_ARC_TYPE")
             .into_iter()
             .map(|(_, type_name)| type_name)
             .collect()
@@ -530,21 +524,27 @@ mod bindings {
                     "pub type {0}Strong = ::gecko_bindings::sugar::ownership::Strong<{0}>;",
                     ty
                 )).borrowed_type(ty)
                 .zero_size_type(ty, &structs_types);
         }
         for ty in get_boxed_types().iter() {
             builder = builder
                 .blacklist_type(format!("{}Owned", ty))
-                .raw_line(format!("pub type {0}Owned = ::gecko_bindings::sugar::ownership::Owned<{0}>;", ty))
-                .blacklist_type(format!("{}OwnedOrNull", ty))
-                .raw_line(format!(concat!("pub type {0}OwnedOrNull = ",
-                                          "::gecko_bindings::sugar::ownership::OwnedOrNull<{0}>;"), ty))
-                .mutable_borrowed_type(ty)
+                .raw_line(format!(
+                    "pub type {0}Owned = ::gecko_bindings::sugar::ownership::Owned<{0}>;",
+                    ty
+                )).blacklist_type(format!("{}OwnedOrNull", ty))
+                .raw_line(format!(
+                    concat!(
+                        "pub type {0}OwnedOrNull = ",
+                        "::gecko_bindings::sugar::ownership::OwnedOrNull<{0}>;"
+                    ),
+                    ty
+                )).mutable_borrowed_type(ty)
                 .zero_size_type(ty, &structs_types);
         }
         write_binding_file(builder, BINDINGS_FILE, &fixups);
     }
 
     fn generate_atoms() {
         let script = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap())
             .join("gecko")
--- a/taskcluster/scripts/builder/build-haz-linux.sh
+++ b/taskcluster/scripts/builder/build-haz-linux.sh
@@ -22,61 +22,66 @@ while [[ $# -gt 0 ]]; do
         DO_TOOLTOOL=
     elif [[ -z "$WORKSPACE" ]]; then
         WORKSPACE=$( cd "$1" && pwd )
         shift
         break
     fi
 done
 
-SCRIPT_FLAGS="$@"
+SCRIPT_FLAGS=$*
 
 # Ensure all the scripts in this dir are on the path....
 DIRNAME=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
 PATH=$DIRNAME:$PATH
 
 # Use GECKO_BASE_REPOSITORY as a signal for whether we are running in automation.
 export AUTOMATION=${GECKO_BASE_REPOSITORY:+1}
 
-: ${GECKO_DIR:=$WORKSPACE/gecko}
-: ${TOOLTOOL_CACHE:=$WORKSPACE/tt-cache}
+: "${GECKO_DIR:="$DIRNAME"/../../..}"
+: "${TOOLTOOL_CACHE:=$WORKSPACE/tt-cache}"
 
-if ! [ -d $GECKO_DIR ]; then
+if ! [ -d "$GECKO_DIR" ]; then
     echo "GECKO_DIR must be set to a directory containing a gecko source checkout" >&2
     exit 1
 fi
-GECKO_DIR=$( cd "$GECKO_DIR" && pwd )
+GECKO_DIR="$( cd "$GECKO_DIR" && pwd )"
 
 # Directory to populate with tooltool-installed tools
 export TOOLTOOL_DIR="$WORKSPACE"
 
 # Directory to hold the (useless) object files generated by the analysis.
 export MOZ_OBJDIR="$WORKSPACE/obj-analyzed"
 mkdir -p "$MOZ_OBJDIR"
 
-if [ -n "$DO_TOOLTOOL" ]; then
-  ( cd $TOOLTOOL_DIR; $GECKO_DIR/mach artifact toolchain -v${TOOLTOOL_MANIFEST:+ --tooltool-url https://tooltool.mozilla-releng.net/ --tooltool-manifest $GECKO_DIR/$TOOLTOOL_MANIFEST} --cache-dir $TOOLTOOL_CACHE${MOZ_TOOLCHAINS:+ ${MOZ_TOOLCHAINS}} )
-fi
+if [ -n "$DO_TOOLTOOL" ]; then (
+    cd "$TOOLTOOL_DIR"
+    "$GECKO_DIR"/mach artifact toolchain -v\
+                ${TOOLTOOL_MANIFEST:+ --tooltool-url https://tooltool.mozilla-releng.net/ \
+                                      --tooltool-manifest "$GECKO_DIR/$TOOLTOOL_MANIFEST"} \
+                --cache-dir "$TOOLTOOL_CACHE"${MOZ_TOOLCHAINS:+ ${MOZ_TOOLCHAINS}}
+) fi
 
 export NO_MERCURIAL_SETUP_CHECK=1
 
 if [[ "$PROJECT" = "browser" ]]; then (
     cd "$WORKSPACE"
     set "$WORKSPACE"
     # Mozbuild config:
     export MOZBUILD_STATE_PATH=$WORKSPACE/mozbuild/
     # Create .mozbuild so mach doesn't complain about this
-    mkdir -p $MOZBUILD_STATE_PATH
+    mkdir -p "$MOZBUILD_STATE_PATH"
 ) fi
 . hazard-analysis.sh
 
 build_js_shell
+analysis_self_test
 
 # Artifacts folder is outside of the cache.
-mkdir -p $HOME/artifacts/ || true
+mkdir -p "$HOME"/artifacts/ || true
 
 function onexit () {
     grab_artifacts "$WORKSPACE/analysis" "$HOME/artifacts"
 }
 
 trap onexit EXIT
 
 configure_analysis "$WORKSPACE/analysis"
--- a/taskcluster/scripts/builder/hazard-analysis.sh
+++ b/taskcluster/scripts/builder/hazard-analysis.sh
@@ -1,32 +1,33 @@
 #!/bin/bash -ex
 
 [ -n "$WORKSPACE" ]
 [ -n "$MOZ_OBJDIR" ]
 [ -n "$GECKO_DIR" ]
 
 HAZARD_SHELL_OBJDIR=$WORKSPACE/obj-haz-shell
+JSBIN="$HAZARD_SHELL_OBJDIR/dist/bin/js"
 JS_SRCDIR=$GECKO_DIR/js/src
 ANALYSIS_SRCDIR=$JS_SRCDIR/devtools/rootAnalysis
+GCCDIR="$TOOLTOOL_DIR/gcc"
 
-export CC="$TOOLTOOL_DIR/gcc/bin/gcc"
-export CXX="$TOOLTOOL_DIR/gcc/bin/g++"
-export PATH="$TOOLTOOL_DIR/gcc/bin:$PATH"
-export LD_LIBRARY_PATH="$TOOLTOOL_DIR/gcc/lib64"
+export CC="$GCCDIR/bin/gcc"
+export CXX="$GCCDIR/bin/g++"
+export PATH="$GCCDIR/bin:$PATH"
+export LD_LIBRARY_PATH="$GCCDIR/lib64"
 export RUSTC="$TOOLTOOL_DIR/rustc/bin/rustc"
 export CARGO="$TOOLTOOL_DIR/rustc/bin/cargo"
 export LLVM_CONFIG="$TOOLTOOL_DIR/clang/bin/llvm-config"
 
 PYTHON=python2.7
 if ! which $PYTHON; then
     PYTHON=python
 fi
 
-
 function check_commit_msg () {
     ( set +e;
     if [[ -n "$AUTOMATION" ]]; then
         hg --cwd "$GECKO_DIR" log -r. --template '{desc}\n' | grep -F -q -- "$1"
     else
         echo -- "$SCRIPT_FLAGS" | grep -F -q -- "$1"
     fi
     )
@@ -63,17 +64,17 @@ function configure_analysis () {
     if [[ -z "$HAZ_DEP" ]]; then
         [ -d "$analysis_dir" ] && rm -rf "$analysis_dir"
     fi
 
     mkdir -p "$analysis_dir" || true
     (
         cd "$analysis_dir"
         cat > defaults.py <<EOF
-js = "$HAZARD_SHELL_OBJDIR/dist/bin/js"
+js = "$JSBIN"
 analysis_scriptdir = "$ANALYSIS_SRCDIR"
 objdir = "$MOZ_OBJDIR"
 source = "$GECKO_DIR"
 sixgill = "$TOOLTOOL_DIR/sixgill/usr/libexec/sixgill"
 sixgill_bin = "$TOOLTOOL_DIR/sixgill/usr/bin"
 EOF
 
         local rev
@@ -102,16 +103,20 @@ function run_analysis () {
     fi
 
     (
         cd "$analysis_dir"
         $PYTHON "$ANALYSIS_SRCDIR/analyze.py" -v --buildcommand="$GECKO_DIR/taskcluster/scripts/builder/hazard-${build_type}.sh"
     )
 }
 
+function analysis_self_test () {
+    LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$(dirname "$JSBIN")" $PYTHON "$ANALYSIS_SRCDIR/run-test.py" -v --js "$JSBIN" --sixgill "$TOOLTOOL_DIR/sixgill" --gccdir "$GCCDIR"
+}
+
 function grab_artifacts () {
     local analysis_dir
     analysis_dir="$1"
     local artifacts
     artifacts="$2"
 
     (
         cd "$analysis_dir"
@@ -156,33 +161,37 @@ function check_hazards () {
     NUM_WRITE_HAZARDS=$(perl -lne 'print $1 if m!found (\d+)/\d+ allowed errors!' "$1"/heapWriteHazards.txt)
 
     set +x
     echo "TinderboxPrint: rooting hazards<br/>$NUM_HAZARDS"
     echo "TinderboxPrint: (unsafe references to unrooted GC pointers)<br/>$NUM_UNSAFE"
     echo "TinderboxPrint: (unnecessary roots)<br/>$NUM_UNNECESSARY"
     echo "TinderboxPrint: heap write hazards<br/>$NUM_WRITE_HAZARDS"
 
+    # Display errors in a way that will get picked up by the taskcluster scraper.
+    perl -le 'print "TEST-UNEXPECTED-FAIL | hazards | $ENV{NUM_HAZARDS} rooting hazards" if $ENV{NUM_HAZARDS}'
+    perl -lne 'print "TEST-UNEXPECTED-FAIL | hazards | $1 $2" if /^Function.* has (unrooted .*live across GC call).* (at .*)$/' "$1"/hazards.txt
+
     exit_status=0
 
     if [ $NUM_HAZARDS -gt 0 ]; then
-        echo "TEST-UNEXPECTED-FAIL $NUM_HAZARDS rooting hazards detected" >&2
+        echo "TEST-UNEXPECTED-FAIL | hazards | $NUM_HAZARDS rooting hazards detected" >&2
         echo "TinderboxPrint: documentation<br/><a href='https://wiki.mozilla.org/Javascript:Hazard_Builds#Diagnosing_a_rooting_hazards_failure'>static rooting hazard analysis failures</a>, visit \"Inspect Task\" link for hazard details"
         exit_status=1
     fi
 
     NUM_ALLOWED_WRITE_HAZARDS=0
     if [ $NUM_WRITE_HAZARDS -gt $NUM_ALLOWED_WRITE_HAZARDS ]; then
-        echo "TEST-UNEXPECTED-FAIL $NUM_WRITE_HAZARDS heap write hazards detected out of $NUM_ALLOWED_WRITE_HAZARDS allowed" >&2
+        echo "TEST-UNEXPECTED-FAIL | heap-write-hazards | $NUM_WRITE_HAZARDS heap write hazards detected out of $NUM_ALLOWED_WRITE_HAZARDS allowed" >&2
         echo "TinderboxPrint: documentation<br/><a href='https://wiki.mozilla.org/Javascript:Hazard_Builds#Diagnosing_a_heap_write_hazard_failure'>heap write hazard analysis failures</a>, visit \"Inspect Task\" link for hazard details"
         exit_status = 1
     fi
 
     if [ $NUM_DROPPED -gt 0 ]; then
-        echo "TEST-UNEXPECTED-FAIL $NUM_DROPPED CFGs dropped" >&2
+        echo "TEST-UNEXPECTED-FAIL | hazards | $NUM_DROPPED CFGs dropped" >&2
         echo "TinderboxPrint: sixgill unable to handle constructs<br/>$NUM_DROPPED"
         exit_status=1
     fi
 
     if [ $exit_status -ne 0 ]; then
         exit $exit_status
     fi
     )
--- a/taskcluster/scripts/misc/build-gcc-sixgill-plugin-linux.sh
+++ b/taskcluster/scripts/misc/build-gcc-sixgill-plugin-linux.sh
@@ -17,17 +17,17 @@ data_dir=$HOME_DIR/src/build/unix/build-
 
 # Download and unpack upstream toolchain artifacts (ie, the gcc binary).
 . $(dirname $0)/tooltool-download.sh
 
 gcc_version=6.4.0
 gcc_ext=xz
 binutils_version=2.28.1
 binutils_ext=xz
-sixgill_rev=ab06fc42cf0f
+sixgill_rev=bc0ef9258470
 sixgill_repo=https://hg.mozilla.org/users/sfink_mozilla.com/sixgill
 
 . $data_dir/build-gcc.sh
 
 pushd $root_dir/gcc-$gcc_version
 ln -sf ../binutils-2.28.1 binutils
 ln -sf ../gmp-5.1.3 gmp
 ln -sf ../isl-0.15 isl
--- a/testing/talos/talos/talos-powers/api.js
+++ b/testing/talos/talos/talos-powers/api.js
@@ -4,16 +4,17 @@
 
 /* globals ExtensionAPI */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   Services: "resource://gre/modules/Services.jsm",
+  PerTestCoverageUtils: "resource://testing-common/PerTestCoverageUtils.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "resProto",
                                    "@mozilla.org/network/protocol;1?name=resource",
                                    "nsISubstitutingProtocolHandler");
 
 Cu.importGlobalProperties(["TextEncoder"]);
 
@@ -315,27 +316,21 @@ TalosPowersService.prototype = {
 
     // arg: handle from startFrameTimeRecording. return: array with composition intervals
     stopFrameTimeRecording(arg, callback, win) {
       var rv = win.windowUtils.stopFrameTimeRecording(arg);
       callback(rv);
     },
 
     requestDumpCoverageCounters(arg, callback, win) {
-      let codeCoverage = Cc["@mozilla.org/tools/code-coverage;1"].
-                         getService(Ci.nsICodeCoverage);
-      codeCoverage.dumpCounters();
-      callback();
+      PerTestCoverageUtils.afterTest().then(callback);
     },
 
     requestResetCoverageCounters(arg, callback, win) {
-      let codeCoverage = Cc["@mozilla.org/tools/code-coverage;1"].
-                         getService(Ci.nsICodeCoverage);
-      codeCoverage.resetCounters();
-      callback();
+      PerTestCoverageUtils.beforeTest().then(callback);
     },
 
     dumpAboutSupport(arg, callback, win) {
       ChromeUtils.import("resource://gre/modules/Troubleshoot.jsm");
       Troubleshoot.snapshot(function(snapshot) {
         dump("about:support\t" + JSON.stringify(snapshot));
       });
       callback();
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css/css-display/select-4-option-optgroup-display-none.html.ini
@@ -0,0 +1,2 @@
+[select-4-option-optgroup-display-none.html]
+  expected: FAIL
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/wake-lock/__dir__.ini
@@ -0,0 +1,1 @@
+prefs: [dom.security.featurePolicy.enabled:true]
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/webusb/__dir__.ini
@@ -0,0 +1,1 @@
+prefs: [dom.security.featurePolicy.enabled:true]
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-display/select-4-option-optgroup-display-none-ref.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html><head>
+  <meta charset="utf-8">
+  <title>Reference: display:none on OPTION and OPTGROUP</title>
+  <link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
+  <style>
+
+.none { display:none; }
+.contents { display:contents; }
+.red { color: red; }
+.green { color: green; }
+
+select { -webkit-appearance: none; }
+
+  </style>
+</head>
+<body>
+
+<pre>FAIL if there is any red color</pre>
+
+<optgroup></optgroup>
+<optgroup class="contents red"></optgroup>
+<optgroup class="contents green" label="optgroup"></optgroup>
+
+<br>
+
+<select class="red" size="4"></select>
+<select size="4" class="red"></select>
+<select size="4" class="red"></select>
+<select size="4" class="red"><optgroup></select>
+<select size="4"></select>
+<select size="4" class="red"></select>
+<select size="4" class="red"></select>
+<select size="4" class="red"></select>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-display/select-4-option-optgroup-display-none.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html><head>
+  <meta charset="utf-8">
+  <title>CSS Test: display:none on OPTION and OPTGROUP</title>
+  <link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
+  <link rel="help" href="https://drafts.csswg.org/css-display-3/#valdef-display-none">
+  <link rel="match" href="select-4-option-optgroup-display-none-ref.html">
+  <style>
+
+.none { display:none; }
+.contents { display:contents; }
+.red { color: red; }
+.green { color: green; }
+
+select { -webkit-appearance: none; }
+
+  </style>
+</head>
+<body>
+
+<pre>FAIL if there is any red color</pre>
+
+<option class="none red">text</option>
+<optgroup class="none red">text</optgroup>
+
+<optgroup class="none red"><option>option</option></optgroup>
+<optgroup><option class="none red">option</option></optgroup>
+<optgroup class="contents red"><option class="none">option</option></optgroup>
+<optgroup class="contents green" label="optgroup"><option class="none red">option</option></optgroup>
+<optgroup class="none red" label="optgroup"><option class="red">option</option></optgroup>
+
+<br>
+
+<select class="red" size="4">select</select>
+<select size="4" class="red"><optgroup class="none" label="optgroup"></select>
+<select size="4" class="red"><option class="none">option</select>
+<select size="4" class="red"><optgroup><option class="none">option</select>
+<select size="4"><optgroup class="none"><option class="green">option</select>
+<select size="4" class="red"><optgroup class="none green" label="optgroup"><option>option</select>
+<select size="4" class="red"><optgroup class="none"><option class="none">option</select>
+<select size="4" class="red"><optgroup class="none green" label="optgroup"><option class="none">option</select>
+
+</body>
+</html>
--- a/tools/code-coverage/CodeCoverageHandler.cpp
+++ b/tools/code-coverage/CodeCoverageHandler.cpp
@@ -13,93 +13,50 @@
 #endif
 #include "mozilla/CodeCoverageHandler.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/DebugOnly.h"
 #include "nsAppRunner.h"
 
 using namespace mozilla;
 
-// The __gcov_dump function writes the coverage counters to gcda files.
-// The __gcov_reset function resets the coverage counters to zero.
-// They are defined at https://github.com/gcc-mirror/gcc/blob/aad93da1a579b9ae23ede6b9cf8523360f0a08b4/libgcc/libgcov-interface.c.
-// __gcov_flush is protected by a mutex, __gcov_dump and __gcov_reset aren't.
-// So we are using a CrossProcessMutex to protect them.
-
-#if defined(__GNUC__) && !defined(__clang__)
-extern "C" void __gcov_dump();
-extern "C" void __gcov_reset();
+// The __gcov_flush function writes the coverage counters to gcda files and then resets them to zero.
+// It is defined at https://github.com/gcc-mirror/gcc/blob/aad93da1a579b9ae23ede6b9cf8523360f0a08b4/libgcc/libgcov-interface.c.
+// __gcov_flush is protected by a mutex in GCC, but not in LLVM, so we are using a CrossProcessMutex to protect it.
 
-void counters_dump() {
-  __gcov_dump();
-}
-
-void counters_reset() {
-  __gcov_reset();
-}
-#else
-void counters_dump() {
-  /* Do nothing */
-}
-
-void counters_reset() {
-  /* Do nothing */
-}
-#endif
+extern "C" void __gcov_flush();
 
 StaticAutoPtr<CodeCoverageHandler> CodeCoverageHandler::instance;
 
-void CodeCoverageHandler::DumpCounters()
+void CodeCoverageHandler::FlushCounters()
 {
-  printf_stderr("[CodeCoverage] Requested dump for %d.\n", getpid());
+  printf_stderr("[CodeCoverage] Requested flush for %d.\n", getpid());
 
   CrossProcessMutexAutoLock lock(*CodeCoverageHandler::Get()->GetMutex());
 
-  counters_dump();
-  printf_stderr("[CodeCoverage] Dump completed.\n");
-}
-
-void CodeCoverageHandler::DumpCountersSignalHandler(int)
-{
-  DumpCounters();
+  __gcov_flush();
+  printf_stderr("[CodeCoverage] flush completed.\n");
 }
 
-void CodeCoverageHandler::ResetCounters()
+void CodeCoverageHandler::FlushCountersSignalHandler(int)
 {
-  printf_stderr("[CodeCoverage] Requested reset for %d.\n", getpid());
-
-  CrossProcessMutexAutoLock lock(*CodeCoverageHandler::Get()->GetMutex());
-
-  counters_reset();
-  printf_stderr("[CodeCoverage] Reset completed.\n");
-}
-
-void CodeCoverageHandler::ResetCountersSignalHandler(int)
-{
-  ResetCounters();
+  FlushCounters();
 }
 
 void CodeCoverageHandler::SetSignalHandlers()
 {
 #ifndef XP_WIN
   printf_stderr("[CodeCoverage] Setting handlers for process %d.\n", getpid());
 
   struct sigaction dump_sa;
-  dump_sa.sa_handler = CodeCoverageHandler::DumpCountersSignalHandler;
+  dump_sa.sa_handler = CodeCoverageHandler::FlushCountersSignalHandler;
   dump_sa.sa_flags = SA_RESTART;
   sigemptyset(&dump_sa.sa_mask);
   DebugOnly<int> r1 = sigaction(SIGUSR1, &dump_sa, nullptr);
   MOZ_ASSERT(r1 == 0, "Failed to install GCOV SIGUSR1 handler");
-
-  struct sigaction reset_sa;
-  reset_sa.sa_handler = CodeCoverageHandler::ResetCountersSignalHandler;
-  reset_sa.sa_flags = SA_RESTART;
-  sigemptyset(&reset_sa.sa_mask);
-  DebugOnly<int> r2 = sigaction(SIGUSR2, &reset_sa, nullptr);
-  MOZ_ASSERT(r2 == 0, "Failed to install GCOV SIGUSR2 handler");
 #endif
 }
 
 CodeCoverageHandler::CodeCoverageHandler()
   : mGcovLock("GcovLock")
 {
   SetSignalHandlers();
 }
--- a/tools/code-coverage/CodeCoverageHandler.h
+++ b/tools/code-coverage/CodeCoverageHandler.h
@@ -13,20 +13,18 @@ namespace mozilla {
 
 class CodeCoverageHandler {
 public:
   static void Init();
   static void Init(const CrossProcessMutexHandle& aHandle);
   static CodeCoverageHandler* Get();
   CrossProcessMutex* GetMutex();
   CrossProcessMutexHandle GetMutexHandle(int aProcId);
-  static void DumpCounters();
-  static void DumpCountersSignalHandler(int);
-  static void ResetCounters();
-  static void ResetCountersSignalHandler(int);
+  static void FlushCounters();
+  static void FlushCountersSignalHandler(int);
 
 private:
   CodeCoverageHandler();
   explicit CodeCoverageHandler(const CrossProcessMutexHandle& aHandle);
 
   static StaticAutoPtr<CodeCoverageHandler> instance;
   CrossProcessMutex mGcovLock;
 
--- a/tools/code-coverage/PerTestCoverageUtils.jsm
+++ b/tools/code-coverage/PerTestCoverageUtils.jsm
@@ -57,40 +57,40 @@ function moveDirectoryContents(src, dst)
 
 var PerTestCoverageUtils = class PerTestCoverageUtilsClass {
   // Resets the counters to 0.
   static async beforeTest() {
     if (!PerTestCoverageUtils.enabled) {
       return;
     }
 
-    // Reset the counters.
+    // Flush the counters.
     let codeCoverageService = Cc["@mozilla.org/tools/code-coverage;1"].getService(Ci.nsICodeCoverage);
-    await codeCoverageService.resetCounters();
+    await codeCoverageService.flushCounters();
 
-    // Remove any gcda file that might have been created between the end of a previous test and the beginning of the next one (e.g. some tests can create a new content process for every sub-test).
+    // Remove gcda files created by the flush, and those that might have been created between the end of a previous test and the beginning of the next one (e.g. some tests can create a new content process for every sub-test).
     removeDirectoryContents(gcovPrefixDir);
 
     // Move gcda files from the GCOV_RESULTS_DIR directory, so we can accumulate the counters.
     moveDirectoryContents(gcovResultsDir, gcovPrefixDir);
   }
 
   static beforeTestSync() {
     awaitPromise(this.beforeTest());
   }
 
   // Dumps counters and moves the gcda files in the directory expected by codecoverage.py.
   static async afterTest() {
     if (!PerTestCoverageUtils.enabled) {
       return;
     }
 
-    // Dump the counters.
+    // Flush the counters.
     let codeCoverageService = Cc["@mozilla.org/tools/code-coverage;1"].getService(Ci.nsICodeCoverage);
-    await codeCoverageService.dumpCounters();
+    await codeCoverageService.flushCounters();
 
     // Move the gcda files in the GCOV_RESULTS_DIR, so that the execution from now to shutdown (or next test) is not counted.
     moveDirectoryContents(gcovPrefixDir, gcovResultsDir);
   }
 
   static afterTestSync() {
     awaitPromise(this.afterTest());
   }
--- a/tools/code-coverage/nsCodeCoverage.cpp
+++ b/tools/code-coverage/nsCodeCoverage.cpp
@@ -19,17 +19,17 @@ NS_IMPL_ISUPPORTS(nsCodeCoverage,
 nsCodeCoverage::nsCodeCoverage()
 {
 }
 
 nsCodeCoverage::~nsCodeCoverage()
 {
 }
 
-enum RequestType { Dump, Reset };
+enum RequestType { Flush };
 
 class ProcessCount final {
   NS_INLINE_DECL_REFCOUNTING(ProcessCount);
 
 public:
   explicit ProcessCount(uint32_t c) : mCount(c) {}
   operator uint32_t() const { return mCount; }
   ProcessCount& operator--() { mCount--; return *this; }
@@ -56,20 +56,18 @@ nsresult Request(JSContext* cx, Promise*
   }
 
   uint32_t processCount = 0;
   for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) {
     Unused << cp;
     ++processCount;
   }
 
-  if (requestType == RequestType::Dump) {
-    CodeCoverageHandler::DumpCounters();
-  } else if (requestType == RequestType::Reset) {
-    CodeCoverageHandler::ResetCounters();
+  if (requestType == RequestType::Flush) {
+    CodeCoverageHandler::FlushCounters();
   }
 
   if (processCount == 0) {
     promise->MaybeResolveWithUndefined();
   } else {
     RefPtr<ProcessCount> processCountHolder(new ProcessCount(processCount));
 
     auto resolve = [processCountHolder, promise](bool unused) {
@@ -78,29 +76,22 @@ nsresult Request(JSContext* cx, Promise*
       }
     };
 
     auto reject = [promise](ResponseRejectReason aReason) {
       promise->MaybeReject(NS_ERROR_FAILURE);
     };
 
     for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) {
-      if (requestType == RequestType::Dump) {
-        cp->SendDumpCodeCoverageCounters(resolve, reject);
-      } else if (requestType == RequestType::Reset) {
-        cp->SendResetCodeCoverageCounters(resolve, reject);
+      if (requestType == RequestType::Flush) {
+        cp->SendFlushCodeCoverageCounters(resolve, reject);
       }
     }
   }
 
   promise.forget(aPromise);
   return NS_OK;
 }
 
-NS_IMETHODIMP nsCodeCoverage::DumpCounters(JSContext *cx, Promise** aPromise)
+NS_IMETHODIMP nsCodeCoverage::FlushCounters(JSContext *cx, Promise** aPromise)
 {
-  return Request(cx, aPromise, RequestType::Dump);
+  return Request(cx, aPromise, RequestType::Flush);
 }
-
-NS_IMETHODIMP nsCodeCoverage::ResetCounters(JSContext *cx, Promise** aPromise)
-{
-  return Request(cx, aPromise, RequestType::Reset);
-}
--- a/tools/code-coverage/nsICodeCoverage.idl
+++ b/tools/code-coverage/nsICodeCoverage.idl
@@ -3,28 +3,22 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 /**
  * The nsICodeCoverage component allows controlling the code coverage counters
  * collected by Firefox during execution.
- * By resetting and dumping the counters, one can analyze the coverage information
+ * By flushing the counters, one can analyze the coverage information
  * for a subset of the program execution (e.g. startup code coverage).
  *
  */
 
 [scriptable, uuid(57d92056-37b4-4d0a-a52f-deb8f6dac8bc)]
 interface nsICodeCoverage : nsISupports
 {
   /**
-   * Write the coverage counters to disk.
+   * Write the coverage counters to disk, and reset them in memory to 0.
    */
   [implicit_jscontext]
-  Promise dumpCounters();
-
-  /**
-   * Reset the coverage counters to 0 (as if nothing was executed).
-   */
-  [implicit_jscontext]
-  Promise resetCounters();
+  Promise flushCounters();
 };
--- a/tools/code-coverage/tests/xpcshell/test_basic.js
+++ b/tools/code-coverage/tests/xpcshell/test_basic.js
@@ -8,14 +8,12 @@ async function run_test() {
   Assert.ok("@mozilla.org/tools/code-coverage;1" in Cc);
 
   let codeCoverageCc = Cc["@mozilla.org/tools/code-coverage;1"];
   Assert.ok(!!codeCoverageCc);
 
   let codeCoverage = codeCoverageCc.getService(Ci.nsICodeCoverage);
   Assert.ok(!!codeCoverage);
 
-  await codeCoverage.dumpCounters();
-
-  await codeCoverage.resetCounters();
+  await codeCoverage.flushCounters();
 
   do_test_finished();
 }
--- a/tools/code-coverage/tests/xpcshell/test_basic_child_and_parent.js
+++ b/tools/code-coverage/tests/xpcshell/test_basic_child_and_parent.js
@@ -4,14 +4,13 @@
 
 function run_test() {
   do_load_child_test_harness();
   do_test_pending();
 
   sendCommand("let v = 'test';", async function() {
       let codeCoverage = Cc["@mozilla.org/tools/code-coverage;1"].getService(Ci.nsICodeCoverage);
 
-      await codeCoverage.dumpCounters();
-      await codeCoverage.resetCounters();
+      await codeCoverage.flushCounters();
 
       do_test_finished();
   });
 }