Merge autoland to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Thu, 31 Aug 2017 12:58:02 -0700
changeset 378068 13d241d08912
parent 378067 4984da222428 (current diff)
parent 377977 6d66d3b4226c (diff)
child 378105 14eea6bedcf3
push id50176
push userkwierso@gmail.com
push dateThu, 31 Aug 2017 23:57:10 +0000
treeherderautoland@d7cae06749f1 [default view] [failures only]
reviewersmerge
milestone57.0a1
first release with
nightly win32
13d241d08912 / 57.0a1 / 20170831220208 / files
nightly win64
13d241d08912 / 57.0a1 / 20170831220208 / files
nightly linux32
nightly linux64
nightly mac
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly win32
nightly win64
Merge autoland to central, a=merge MozReview-Commit-ID: DVKb0Vjn9FR
--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -59,20 +59,20 @@ var shutdown = Task.async(function* () {
   if (typeof AnimationsPanel !== "undefined") {
     yield AnimationsPanel.destroy();
   }
   gToolbox = gInspector = null;
 });
 
 // This is what makes the sidebar widget able to load/unload the panel.
 function setPanel(panel) {
-  return startup(panel).catch(e => console.error(e));
+  return startup(panel).catch(console.error);
 }
 function destroy() {
-  return shutdown().catch(e => console.error(e));
+  return shutdown().catch(console.error);
 }
 
 /**
  * Get all the server-side capabilities (traits) so the UI knows whether or not
  * features should be enabled/disabled.
  * @param {Target} target The current toolbox target.
  * @return {Object} An object with boolean properties.
  */
@@ -256,17 +256,17 @@ var AnimationsController = {
    */
   toggleAll: function () {
     if (!this.traits.hasToggleAll) {
       return promise.resolve();
     }
 
     return this.animationsFront.toggleAll()
       .then(() => this.emit(this.ALL_ANIMATIONS_TOGGLED_EVENT, this))
-      .catch(e => console.error(e));
+      .catch(console.error);
   },
 
   /**
    * Similar to toggleAll except that it only plays/pauses the currently known
    * animations (those listed in this.animationPlayers).
    * @param {Boolean} shouldPause True if the animations should be paused, false
    * if they should be played.
    * @return {Promise} Resolves when the playState has been changed.
--- a/devtools/client/animationinspector/animation-panel.js
+++ b/devtools/client/animationinspector/animation-panel.js
@@ -174,19 +174,19 @@ var AnimationsPanel = {
   },
 
   onKeyDown: function (event) {
     // If the space key is pressed, it should toggle the play state of
     // the animations displayed in the panel, or of all the animations on
     // the page if the selected node does not have any animation on it.
     if (event.keyCode === KeyCodes.DOM_VK_SPACE) {
       if (AnimationsController.animationPlayers.length > 0) {
-        this.playPauseTimeline().catch(ex => console.error(ex));
+        this.playPauseTimeline().catch(console.error);
       } else {
-        this.toggleAll().catch(ex => console.error(ex));
+        this.toggleAll().catch(console.error);
       }
       event.preventDefault();
     }
   },
 
   togglePlayers: function (isVisible) {
     if (isVisible) {
       document.body.removeAttribute("empty");
@@ -203,30 +203,30 @@ var AnimationsPanel = {
     this.pickerButtonEl.classList.add("checked");
   },
 
   onPickerStopped: function () {
     this.pickerButtonEl.classList.remove("checked");
   },
 
   onToggleAllClicked: function () {
-    this.toggleAll().catch(ex => console.error(ex));
+    this.toggleAll().catch(console.error);
   },
 
   /**
    * Toggle (pause/play) all animations in the current target
    * and update the UI the toggleAll button.
    */
   toggleAll: Task.async(function* () {
     this.toggleAllButtonEl.classList.toggle("paused");
     yield AnimationsController.toggleAll();
   }),
 
   onTimelinePlayClicked: function () {
-    this.playPauseTimeline().catch(ex => console.error(ex));
+    this.playPauseTimeline().catch(console.error);
   },
 
   /**
    * Depending on the state of the timeline either pause or play the animations
    * displayed in it.
    * If the animations are finished, this will play them from the start again.
    * If the animations are playing, this will pause them.
    * If the animations are paused, this will resume them.
@@ -236,17 +236,17 @@ var AnimationsPanel = {
    */
   playPauseTimeline: function () {
     return AnimationsController
       .toggleCurrentAnimations(this.timelineData.isMoving)
       .then(() => this.refreshAnimationsStateAndUI());
   },
 
   onTimelineRewindClicked: function () {
-    this.rewindTimeline().catch(ex => console.error(ex));
+    this.rewindTimeline().catch(console.error);
   },
 
   /**
    * Reset the startTime of all current animations shown in the timeline and
    * pause them.
    *
    * @return {Promise} Resolves when currentTime is set and the UI is refreshed
    */
@@ -258,17 +258,17 @@ var AnimationsPanel = {
 
   /**
    * Set the playback rate of all current animations shown in the timeline to
    * the value of this.rateSelectorEl.
    */
   onRateChanged: function (e, rate) {
     AnimationsController.setPlaybackRateAll(rate)
                         .then(() => this.refreshAnimationsStateAndUI())
-                        .catch(ex => console.error(ex));
+                        .catch(console.error);
   },
 
   onTabNavigated: function () {
     this.toggleAllButtonEl.classList.remove("paused");
   },
 
   onTimelineDataChanged: function (e, data) {
     this.timelineData = data;
@@ -284,17 +284,17 @@ var AnimationsPanel = {
 
     // If the timeline data changed as a result of the user dragging the
     // scrubber, then pause all animations and set their currentTimes.
     // (Note that we want server-side requests to be sequenced, so we only do
     // this after the previous currentTime setting was done).
     if (isUserDrag && !this.setCurrentTimeAllPromise) {
       this.setCurrentTimeAllPromise =
         AnimationsController.setCurrentTimeAll(time, true)
-                            .catch(error => console.error(error))
+                            .catch(console.error)
                             .then(() => {
                               this.setCurrentTimeAllPromise = null;
                             });
     }
 
     this.displayTimelineCurrentTime();
   },
 
--- a/devtools/client/canvasdebugger/callslist.js
+++ b/devtools/client/canvasdebugger/callslist.js
@@ -283,17 +283,17 @@ var CallsListView = Heritage.extend(Widg
     setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => {
       return !this._isSliding;
     }, () => {
       let frameSnapshot = SnapshotsListView.selectedItem.attachment.actor;
       let functionCall = callItem.attachment.actor;
       frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => {
         this.showScreenshot(screenshot);
         this.highlightedThumbnail = screenshot.index;
-      }).catch(e => console.error(e));
+      }).catch(console.error);
     });
   },
 
   /**
    * The mousedown listener for the call selection slider.
    */
   _onSlideMouseDown: function () {
     this._isSliding = true;
--- a/devtools/client/inspector/boxmodel/box-model.js
+++ b/devtools/client/inspector/boxmodel/box-model.js
@@ -297,37 +297,37 @@ BoxModel.prototype = {
             properties.push({ name: bprop, value: "solid" });
           }
         }
 
         if (property.substring(0, 9) == "position-") {
           properties[0].name = property.substring(9);
         }
 
-        session.setProperties(properties).catch(e => console.error(e));
+        session.setProperties(properties).catch(console.error);
       },
       done: (value, commit) => {
         editor.elt.parentNode.classList.remove("boxmodel-editing");
         if (!commit) {
           session.revert().then(() => {
             session.destroy();
-          }, e => console.error(e));
+          }, console.error);
           return;
         }
 
         if (!this.inspector) {
           return;
         }
 
         let node = this.inspector.selection.nodeFront;
         this.inspector.pageStyle.getLayout(node, {
           autoMargins: true,
         }).then(layout => {
           this.store.dispatch(updateLayout(layout));
-        }, e => console.error(e));
+        }, console.error);
       },
       cssProperties: getCssProperties(this.inspector.toolbox)
     }, event);
   },
 
   /**
    * Shows the box-model highlighter on the currently selected element.
    *
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -504,17 +504,17 @@ CssComputedView.prototype = {
 
               this.inspector.emit("computed-view-refreshed");
               resolve(undefined);
             }
           }
         );
         this._refreshProcess.schedule();
       });
-    }).catch((err) => console.error(err));
+    }).catch(console.error);
   },
 
   /**
    * Handle the shortcut events in the computed view.
    */
   _onShortcut: function (name, event) {
     if (!event.target.closest("#sidebar-panel-computedview")) {
       return;
--- a/devtools/client/inspector/inspector-commands.js
+++ b/devtools/client/inspector/inspector-commands.js
@@ -53,33 +53,33 @@ exports.items = [{
     }, {
       name: "hide",
       type: "boolean",
       hidden: true
     }]
   }],
   exec: function* (args, context) {
     if (args.hide) {
-      context.updateExec("eyedropper_server_hide").catch(e => console.error(e));
+      context.updateExec("eyedropper_server_hide").catch(console.error);
       return;
     }
 
     // If the inspector is already picking a color from the page, cancel it.
     let target = context.environment.target;
     let toolbox = gDevTools.getToolbox(target);
     if (toolbox) {
       let inspector = toolbox.getPanel("inspector");
       if (inspector) {
         yield inspector.hideEyeDropper();
       }
     }
 
     let telemetry = new Telemetry();
     telemetry.toolOpened(args.frommenu ? "menueyedropper" : "eyedropper");
-    context.updateExec("eyedropper_server").catch(e => console.error(e));
+    context.updateExec("eyedropper_server").catch(console.error);
   }
 }, {
   item: "command",
   runAt: "server",
   name: "eyedropper_server",
   hidden: true,
   exec: function (args, {environment}) {
     let eyeDropper = windowEyeDroppers.get(environment.window);
--- a/devtools/client/inspector/inspector-search.js
+++ b/devtools/client/inspector/inspector-search.js
@@ -67,17 +67,17 @@ InspectorSearch.prototype = {
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
     this.searchBox = null;
     this.searchClearButton = null;
     this.autocompleter.destroy();
   },
 
   _onSearch: function (reverse = false) {
     this.doFullTextSearch(this.searchBox.value, reverse)
-        .catch(e => console.error(e));
+        .catch(console.error);
   },
 
   doFullTextSearch: Task.async(function* (query, reverse) {
     let lastSearched = this._lastSearched;
     this._lastSearched = query;
 
     if (query.length === 0) {
       this.searchBox.classList.remove("devtools-style-searchbox-no-match");
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -219,23 +219,23 @@ Inspector.prototype = {
     this._supportsResolveRelativeURL = false;
 
     // Use getActorDescription first so that all actorHasMethod calls use
     // a cached response from the server.
     return this._target.getActorDescription("domwalker").then(desc => {
       return promise.all([
         this._target.actorHasMethod("domwalker", "duplicateNode").then(value => {
           this._supportsDuplicateNode = value;
-        }).catch(e => console.error(e)),
+        }).catch(console.error),
         this._target.actorHasMethod("domnode", "scrollIntoView").then(value => {
           this._supportsScrollIntoView = value;
-        }).catch(e => console.error(e)),
+        }).catch(console.error),
         this._target.actorHasMethod("inspector", "resolveRelativeURL").then(value => {
           this._supportsResolveRelativeURL = value;
-        }).catch(e => console.error(e)),
+        }).catch(console.error),
       ]);
     });
   },
 
   _deferredOpen: function (defaultSelection) {
     this.breadcrumbs = new HTMLBreadcrumbs(this);
 
     this.walker.on("new-root", this.onNewRoot);
@@ -1622,34 +1622,34 @@ Inspector.prototype = {
     if (!this.eyeDropperButton) {
       return null;
     }
 
     this.telemetry.toolOpened("toolbareyedropper");
     this.eyeDropperButton.classList.add("checked");
     this.startEyeDropperListeners();
     return this.inspector.pickColorFromPage(this.toolbox, {copyOnSelect: true})
-                         .catch(e => console.error(e));
+                         .catch(console.error);
   },
 
   /**
    * Hide the eyedropper.
    * @return {Promise} resolves when the eyedropper is hidden.
    */
   hideEyeDropper: function () {
     // The eyedropper button doesn't exist, most probably because the actor doesn't
     // support the pickColorFromPage, or because the page isn't HTML.
     if (!this.eyeDropperButton) {
       return null;
     }
 
     this.eyeDropperButton.classList.remove("checked");
     this.stopEyeDropperListeners();
     return this.inspector.cancelPickColorFromPage()
-                         .catch(e => console.error(e));
+                         .catch(console.error);
   },
 
   /**
    * Create a new node as the last child of the current selection, expand the
    * parent and select the new node.
    */
   addNode: Task.async(function* () {
     if (!this.canAddHTMLChild()) {
@@ -1834,74 +1834,74 @@ Inspector.prototype = {
    * @param  {Promise} longStringActorPromise
    *         promise expected to resolve a LongStringActor instance
    * @return {Promise} promise resolving (with no argument) when the
    *         string is sent to the clipboard
    */
   _copyLongString: function (longStringActorPromise) {
     return this._getLongString(longStringActorPromise).then(string => {
       clipboardHelper.copyString(string);
-    }).catch(e => console.error(e));
+    }).catch(console.error);
   },
 
   /**
    * Retrieve the content of a longString (via a promise resolving a LongStringActor)
    * @param  {Promise} longStringActorPromise
    *         promise expected to resolve a LongStringActor instance
    * @return {Promise} promise resolving with the retrieved string as argument
    */
   _getLongString: function (longStringActorPromise) {
     return longStringActorPromise.then(longStringActor => {
       return longStringActor.string().then(string => {
-        longStringActor.release().catch(e => console.error(e));
+        longStringActor.release().catch(console.error);
         return string;
       });
-    }).catch(e => console.error(e));
+    }).catch(console.error);
   },
 
   /**
    * Copy a unique selector of the selected Node to the clipboard.
    */
   copyUniqueSelector: function () {
     if (!this.selection.isNode()) {
       return;
     }
 
     this.telemetry.toolOpened("copyuniquecssselector");
     this.selection.nodeFront.getUniqueSelector().then(selector => {
       clipboardHelper.copyString(selector);
-    }).catch(e => console.error);
+    }).catch(console.error);
   },
 
   /**
    * Copy the full CSS Path of the selected Node to the clipboard.
    */
   copyCssPath: function () {
     if (!this.selection.isNode()) {
       return;
     }
 
     this.telemetry.toolOpened("copyfullcssselector");
     this.selection.nodeFront.getCssPath().then(path => {
       clipboardHelper.copyString(path);
-    }).catch(e => console.error);
+    }).catch(console.error);
   },
 
   /**
    * Copy the XPath of the selected Node to the clipboard.
    */
   copyXPath: function () {
     if (!this.selection.isNode()) {
       return;
     }
 
     this.telemetry.toolOpened("copyxpath");
     this.selection.nodeFront.getXPath().then(path => {
       clipboardHelper.copyString(path);
-    }).catch(e => console.error);
+    }).catch(console.error);
   },
 
   /**
    * Initiate gcli screenshot command on selected node.
    */
   screenshotNode: Task.async(function* () {
     const command = Services.prefs.getBoolPref("devtools.screenshot.clipboard.enabled") ?
       "screenshot --file --clipboard --selector" :
@@ -1937,17 +1937,17 @@ Inspector.prototype = {
   duplicateNode: function () {
     let selection = this.selection;
     if (!selection.isElementNode() ||
         selection.isRoot() ||
         selection.isAnonymousNode() ||
         selection.isPseudoElementNode()) {
       return;
     }
-    this.walker.duplicateNode(selection.nodeFront).catch(e => console.error(e));
+    this.walker.duplicateNode(selection.nodeFront).catch(console.error);
   },
 
   /**
    * Delete the selected node.
    */
   deleteNode: function () {
     if (!this.selection.isNode() ||
          this.selection.isRoot()) {
@@ -2038,28 +2038,28 @@ Inspector.prototype = {
             let browserWin = this.target.tab.ownerDocument.defaultView;
             browserWin.openUILinkIn(url, "tab");
           } else if (type === "cssresource") {
             return this.toolbox.viewSourceInStyleEditor(url);
           } else if (type === "jsresource") {
             return this.toolbox.viewSourceInDebugger(url);
           }
           return null;
-        }).catch(e => console.error(e));
+        }).catch(console.error);
     } else if (type == "idref") {
       // Select the node in the same document.
       this.walker.document(this.selection.nodeFront).then(doc => {
         return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
           if (!node) {
             this.emit("idref-attribute-link-failed");
             return;
           }
           this.selection.setNodeFront(node);
         });
-      }).catch(e => console.error(e));
+      }).catch(console.error);
     }
   },
 
   /**
    * This method is here for the benefit of the node-menu-link-copy menu item
    * in the inspector contextual-menu.
    */
   onCopyLink: function () {
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -179,17 +179,17 @@ MarkupView.prototype = {
 
   _disableImagePreviewTooltip: function () {
     this.imagePreviewTooltip.stopTogglingOnHover();
   },
 
   _onToolboxPickerHover: function (event, nodeFront) {
     this.showNode(nodeFront).then(() => {
       this._showContainerAsHovered(nodeFront);
-    }, e => console.error(e));
+    }, console.error);
   },
 
   /**
    * If the element picker gets canceled, make sure and re-center the view on the
    * current selected element.
    */
   _onToolboxPickerCanceled: function () {
     if (this._selectedContainer) {
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -892,17 +892,17 @@ CssRuleView.prototype = {
 
       this._clearRules();
       let onEditorsReady = this._createEditors();
       this.refreshPseudoClassPanel();
 
       // Notify anyone that cares that we refreshed.
       return onEditorsReady.then(() => {
         this.emit("ruleview-refreshed");
-      }, e => console.error(e));
+      }, console.error);
     }).catch(promiseWarn);
   },
 
   /**
    * Show the user that the rule view has no node selected.
    */
   _showEmpty: function () {
     if (this.styleDocument.getElementById("ruleview-no-results")) {
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -270,17 +270,17 @@ RuleEditor.prototype = {
     let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     if (showOrig && !this.rule.isSystem &&
         this.rule.domRule.type !== ELEMENT_STYLE) {
       // Only get the original source link if the right pref is set, if the rule
       // isn't a system rule and if it isn't an inline rule.
       this.rule.getOriginalSourceStrings().then((strings) => {
         sourceLabel.textContent = strings.short;
         sourceLabel.setAttribute("title", strings.full);
-      }, e => console.error(e)).then(() => {
+      }, console.error).then(() => {
         this.emit("source-link-updated");
       });
     } else {
       // If we're not getting the original source link, then we can emit the
       // event immediately (but still asynchronously to give consumers a chance
       // to register it after having instantiated the RuleEditor).
       promise.resolve().then(() => {
         this.emit("source-link-updated");
--- a/devtools/client/inspector/shared/dom-node-preview.js
+++ b/devtools/client/inspector/shared/dom-node-preview.js
@@ -191,17 +191,17 @@ DomNodePreview.prototype = {
     this.inspector.off("markupmutation", this.onMarkupMutations);
     this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
     this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
     this.previewEl.removeEventListener("click", this.onSelectElClick);
     this.highlightNodeEl.removeEventListener("click", this.onHighlightElClick);
   },
 
   destroy: function () {
-    HighlighterLock.unhighlight().catch(e => console.error(e));
+    HighlighterLock.unhighlight().catch(console.error);
 
     this.stopListeners();
 
     this.el.remove();
     this.el = this.tagNameEl = this.idEl = this.classEl = this.pseudoEl = null;
     this.highlightNodeEl = this.previewEl = null;
     this.nodeFront = this.inspector = null;
   },
@@ -213,25 +213,25 @@ DomNodePreview.prototype = {
     return null;
   },
 
   onPreviewMouseOver: function () {
     if (!this.nodeFront || !this.highlighterUtils) {
       return;
     }
     this.highlighterUtils.highlightNodeFront(this.nodeFront)
-                         .catch(e => console.error(e));
+                         .catch(console.error);
   },
 
   onPreviewMouseOut: function () {
     if (!this.nodeFront || !this.highlighterUtils) {
       return;
     }
     this.highlighterUtils.unhighlight()
-                         .catch(e => console.error(e));
+                         .catch(console.error);
   },
 
   onSelectElClick: function () {
     if (!this.nodeFront) {
       return;
     }
     this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview");
   },
@@ -241,22 +241,22 @@ DomNodePreview.prototype = {
 
     let classList = this.highlightNodeEl.classList;
     let isHighlighted = classList.contains("selected");
 
     if (isHighlighted) {
       classList.remove("selected");
       HighlighterLock.unhighlight().then(() => {
         this.emit("target-highlighter-unlocked");
-      }, error => console.error(error));
+      }, console.error);
     } else {
       classList.add("selected");
       HighlighterLock.highlight(this).then(() => {
         this.emit("target-highlighter-locked");
-      }, error => console.error(error));
+      }, console.error);
     }
   },
 
   onHighlighterLocked: function (e, domNodePreview) {
     if (domNodePreview !== this) {
       this.highlightNodeEl.classList.remove("selected");
     }
   },
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -530,17 +530,17 @@ HighlightersOverlay.prototype = {
     }
 
     // For some reason, the call to highlighter.hide doesn't always return a
     // promise. This causes some tests to fail when trying to install a
     // rejection handler on the result of the call. To avoid this, check
     // whether the result is truthy before installing the handler.
     let onHidden = this.highlighters[this.hoveredHighlighterShown].hide();
     if (onHidden) {
-      onHidden.catch(e => console.error(e));
+      onHidden.catch(console.error);
     }
 
     this.hoveredHighlighterShown = null;
     this.emit("highlighter-hidden");
   },
 
   /**
    * Is the current hovered node a css transform property value in the
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -184,17 +184,17 @@ const ResponsiveUIManager = exports.Resp
       case "resize off":
         completed = this.closeIfNeeded(window, tab, { command: true });
         break;
       case "resize toggle":
         completed = this.toggle(window, tab, { command: true });
         break;
       default:
     }
-    completed.catch(e => console.error(e));
+    completed.catch(console.error);
   },
 
   handleMenuCheck({target}) {
     ResponsiveUIManager.setMenuCheckFor(target);
   },
 
   initMenuCheckListenerFor(window) {
     let { tabContainer } = window.gBrowser;
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -1731,17 +1731,17 @@ var Scratchpad = {
       if (state)
         this.dirty = !state.saved;
 
       this.initialized = true;
       this._triggerObservers("Ready");
       this.populateRecentFilesMenu();
       PreferenceObserver.init();
       CloseObserver.init();
-    }).catch((err) => console.error(err));
+    }).catch(console.error);
     this._setupCommandListeners();
     this._updateViewMenuItems();
     this._setupPopupShowingListeners();
 
     // Change the accesskey for the help menu as it can be specific on Windows
     // some localizations of Windows (ex:french, german) use "?"
     //  for the help button in the menubar but Gnome does not.
     if (Services.appinfo.OS == "WINNT") {
--- a/devtools/client/shadereditor/shadereditor.js
+++ b/devtools/client/shadereditor/shadereditor.js
@@ -309,17 +309,17 @@ var ShadersListView = Heritage.extend(Wi
         vs: vertexShaderText,
         fs: fragmentShaderText
       });
     }
 
     getShaders()
       .then(getSources)
       .then(showSources)
-      .catch(e => console.error(e));
+      .catch(console.error);
   },
 
   /**
    * The check listener for the programs container.
    */
   _onProgramCheck: function ({ detail: { checked }, target }) {
     let sourceItem = this.getItemForElement(target);
     let attachment = sourceItem.attachment;
--- a/devtools/client/shared/widgets/FilterWidget.js
+++ b/devtools/client/shared/widgets/FilterWidget.js
@@ -570,25 +570,25 @@ CSSFilterEditorWidget.prototype = {
 
     let id = +preset.dataset.id;
 
     this.getPresets().then(presets => {
       if (el.classList.contains("remove-button")) {
         // If the click happened on the remove button.
         presets.splice(id, 1);
         this.setPresets(presets).then(this.renderPresets,
-                                      ex => console.error(ex));
+                                      console.error);
       } else {
         // Or if the click happened on a preset.
         let p = presets[id];
 
         this.setCssValue(p.value);
         this.addPresetInput.value = p.name;
       }
-    }, ex => console.error(ex));
+    }, console.error);
   },
 
   _togglePresets: function () {
     this.el.classList.toggle("show-presets");
     this.emit("render");
   },
 
   _savePreset: function (e) {
@@ -607,18 +607,18 @@ CSSFilterEditorWidget.prototype = {
 
       if (index > -1) {
         presets[index].value = value;
       } else {
         presets.push({name, value});
       }
 
       this.setPresets(presets).then(this.renderPresets,
-                                    ex => console.error(ex));
-    }, ex => console.error(ex));
+                                    console.error);
+    }, console.error);
   },
 
   /**
    * Workaround needed to reset the focus when using a HTML select inside a XUL panel.
    * See Bug 1294366.
    */
   _resetFocus: function () {
     this.filterSelect.ownerDocument.defaultView.focus();
@@ -947,22 +947,22 @@ CSSFilterEditorWidget.prototype = {
 
   getPresets: function () {
     return asyncStorage.getItem("cssFilterPresets").then(presets => {
       if (!presets) {
         return [];
       }
 
       return presets;
-    }, e => console.error(e));
+    }, console.error);
   },
 
   setPresets: function (presets) {
     return asyncStorage.setItem("cssFilterPresets", presets)
-      .catch(e => console.error(e));
+      .catch(console.error);
   }
 };
 
 // Fixes JavaScript's float precision
 function fixFloat(a, number) {
   let fixed = parseFloat(a).toFixed(1);
   return number ? parseFloat(fixed) : fixed;
 }
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -166,17 +166,17 @@ SwatchColorPickerTooltip.prototype = ext
     telemetry.toolOpened("pickereyedropper");
     inspector.pickColorFromPage(toolbox, {copyOnSelect: false}).then(() => {
       this.eyedropperOpen = true;
 
       // close the colorpicker tooltip so that only the eyedropper is open.
       this.hide();
 
       this.tooltip.emit("eyedropper-opened");
-    }, e => console.error(e));
+    }, console.error);
 
     inspector.once("color-picked", color => {
       toolbox.win.focus();
       this._selectColor(color);
       this._onEyeDropperDone();
     });
 
     inspector.once("color-pick-canceled", () => {
--- a/devtools/client/sourceeditor/autocomplete.js
+++ b/devtools/client/sourceeditor/autocomplete.js
@@ -232,17 +232,17 @@ function autoComplete({ ed, cm }) {
     popup.setItems(suggestions);
 
     popup.once("popup-opened", () => {
       // This event is used in tests.
       ed.emit("after-suggest");
     });
     popup.openPopup(cursorElement, -1 * left, 0);
     autocompleteOpts.suggestionInsertedOnce = false;
-  }).catch(e => console.error(e));
+  }).catch(console.error);
 }
 
 /**
  * Inserts a popup item into the current cursor location
  * in the editor.
  */
 function insertPopupItem(ed, popupItem) {
   let {preLabel, text} = popupItem;
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -229,17 +229,17 @@ StyleEditorUI.prototype = {
    * @param {string} event
    *        Event name
    * @param {StyleSheet} styleSheet
    *        StyleSheet object for new sheet
    */
   _onNewDocument: function () {
     this._debuggee.getStyleSheets().then((styleSheets) => {
       return this._resetStyleSheetList(styleSheets);
-    }).catch(e => console.error(e));
+    }).catch(console.error);
   },
 
   /**
    * Add editors for all the given stylesheets to the UI.
    *
    * @param  {array} styleSheets
    *         Array of StyleSheetFront
    */
@@ -629,17 +629,17 @@ StyleEditorUI.prototype = {
             let lineCount = editorText.split("\n").length;
             let ruleCount = showEditor.styleSheet.ruleCount;
             if (lineCount >= ruleCount) {
               showEditor.addUnusedRegions(reports);
             } else {
               this.emit("error", { key: "error-compressed", level: "info" });
             }
           }
-        }.bind(this)).catch(e => console.error(e));
+        }.bind(this)).catch(console.error);
       }
     });
   },
 
   /**
    * Switch to the editor that has been marked to be selected.
    *
    * @return {Promise}
@@ -914,17 +914,17 @@ StyleEditorUI.prototype = {
         div.appendChild(link);
 
         list.appendChild(div);
       }
 
       sidebar.hidden = !showSidebar || !inSource;
 
       this.emit("media-list-changed", editor);
-    }.bind(this)).catch(e => console.error(e));
+    }.bind(this)).catch(console.error);
   },
 
   /**
    * Used to safely inject media query links
    *
    * @param {HTMLElement} element
    *        The element corresponding to the media sidebar condition
    * @param {String} rawText
--- a/devtools/client/styleeditor/StyleSheetEditor.jsm
+++ b/devtools/client/styleeditor/StyleSheetEditor.jsm
@@ -122,17 +122,17 @@ function StyleSheetEditor(styleSheet, wi
   this._onMouseMove = this._onMouseMove.bind(this);
 
   this._focusOnSourceEditorReady = false;
   this.cssSheet.on("property-change", this._onPropertyChange);
   this.styleSheet.on("error", this._onError);
   this.mediaRules = [];
   if (this.cssSheet.getMediaRules) {
     this.cssSheet.getMediaRules().then(this._onMediaRulesChanged,
-                                       e => console.error(e));
+                                       console.error);
   }
   this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged);
   this.cssSheet.on("style-applied", this._onStyleApplied);
   this.savedFile = file;
   this.linkCSSFile();
 }
 this.StyleSheetEditor = StyleSheetEditor;
 
@@ -513,17 +513,17 @@ StyleSheetEditor.prototype = {
     }
     this.focus();
   },
 
   /**
    * Toggled the disabled state of the underlying stylesheet.
    */
   toggleDisabled: function () {
-    this.styleSheet.toggleDisabled().catch(e => console.error(e));
+    this.styleSheet.toggleDisabled().catch(console.error);
   },
 
   /**
    * Queue a throttled task to update the live style sheet.
    */
   updateStyleSheet: function () {
     if (this._updateTask) {
       // cancel previous queued task not executed within throttle delay
@@ -555,17 +555,17 @@ StyleSheetEditor.prototype = {
     this._updateTask = null;
 
     if (this.sourceEditor) {
       this._state.text = this.sourceEditor.getText();
     }
 
     this._isUpdating = true;
     this.styleSheet.update(this._state.text, this.transitionsEnabled)
-      .catch(e => console.error(e));
+      .catch(console.error);
   },
 
   /**
    * Handle mousemove events, calling _highlightSelectorAt after a delay only
    * and reseting the delay everytime.
    */
   _onMouseMove: function (e) {
     this.highlighter.hide();
--- a/devtools/client/webconsole/console-output.js
+++ b/devtools/client/webconsole/console-output.js
@@ -3067,17 +3067,17 @@ Widgets.ObjectRenderers.add({
     }
 
     this._text(">");
 
     // Register this widget in the owner message so that it gets destroyed when
     // the message is destroyed.
     this.message.widgets.add(this);
 
-    this.linkToInspector().catch(e => console.error(e));
+    this.linkToInspector().catch(console.error);
   },
 
   /**
    * If the DOMNode being rendered can be highlit in the page, this function
    * will attach mouseover/out event listeners to do so, and the inspector icon
    * to open the node in the inspector.
    * @return a promise that resolves when the node has been linked to the
    * inspector, or rejects if it wasn't (either if no toolbox could be found to
@@ -3155,17 +3155,17 @@ Widgets.ObjectRenderers.add({
   /**
    * Unhighlight a previously highlit node
    * @see highlightDomNode
    * @return a promise that resolves when the highlighter has been hidden
    */
   unhighlightDomNode: function () {
     return this.linkToInspector().then(() => {
       return this.toolbox.highlighterUtils.unhighlight();
-    }).catch(e => console.error(e));
+    }).catch(console.error);
   },
 
   /**
    * Open the DOMNode corresponding to the ObjectActor in the inspector panel
    * @return a promise that resolves when the inspector has been switched to
    * and the node has been selected, or rejects if the node cannot be selected
    * (detached from the DOM). Note that in any case, the inspector panel will
    * be switched to.
--- a/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
+++ b/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
@@ -92,17 +92,17 @@ function mixedContentOverrideTest2(hud, 
         text: "Loading mixed (insecure) display content" +
           " \u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d" +
           " on a secure page",
         category: CATEGORY_SECURITY,
         severity: SEVERITY_WARNING,
         objects: true,
       },
     ],
-  }).then(msgs => deferred.resolve(msgs), e => console.error(e));
+  }).then(msgs => deferred.resolve(msgs), console.error);
 
   return deferred.promise;
 }
 
 function testClickOpenNewTab(hud, match) {
   let warningNode = match.clickableElements[0];
   ok(warningNode, "link element");
   ok(warningNode.classList.contains("learn-more-link"), "link class name");
--- a/devtools/client/webide/content/runtimedetails.js
+++ b/devtools/client/webide/content/runtimedetails.js
@@ -97,17 +97,17 @@ function CheckLockState() {
         device.isRoot().then(isRoot => {
           if (isRoot) {
             adbCheckResult.textContent = sYes;
             flipCertPerfButton.removeAttribute("disabled");
           } else {
             adbCheckResult.textContent = sNo;
             adbRootAction.removeAttribute("hidden");
           }
-        }, e => console.error(e));
+        }, console.error);
       } else {
         adbCheckResult.textContent = sUnknown;
       }
     } else {
       adbCheckResult.textContent = sNotUSB;
     }
 
     // forbid-certified-apps check
@@ -115,17 +115,17 @@ function CheckLockState() {
       let prefFront = AppManager.preferenceFront;
       prefFront.getBoolPref("devtools.debugger.forbid-certified-apps").then(isForbidden => {
         if (isForbidden) {
           devtoolsCheckResult.textContent = sNo;
           flipCertPerfAction.removeAttribute("hidden");
         } else {
           devtoolsCheckResult.textContent = sYes;
         }
-      }, e => console.error(e));
+      }, console.error);
     } catch (e) {
       // Exception. pref actor is only accessible if forbird-certified-apps is false
       devtoolsCheckResult.textContent = sNo;
       flipCertPerfAction.removeAttribute("hidden");
     }
 
   }
 
@@ -142,10 +142,10 @@ function EnableCertApps() {
     "echo 'user_pref(\"network.disable.ipc.security\", true);' >> prefs.js && " +
     "echo 'user_pref(\"dom.webcomponents.enabled\", true);' >> prefs.js && " +
     "start b2g"
   );
 }
 
 function RootADB() {
   let device = AppManager.selectedRuntime.device;
-  device.summonRoot().then(CheckLockState, (e) => console.error(e));
+  device.summonRoot().then(CheckLockState, console.error);
 }
--- a/devtools/docs/backend/backward-compatibility.md
+++ b/devtools/docs/backend/backward-compatibility.md
@@ -38,17 +38,17 @@ let hasProfilerActor = toolbox.target.ha
 
 The `hasActor` method returns a boolean synchronously.
 
 2. Detecting if an actor has a given method: same thing here, you need access to the toolbox:
 
 ```js
 toolbox.target.actorHasMethod("domwalker", "duplicateNode").then(hasMethod => {
 
-}).catch(e => console.error(e));
+}).catch(console.error);
 ```
 
 The `actorHasMethod` returns a promise that resolves to a boolean.
 
 ## Removing old backward compatibility code
 
 We used to support old Firefox OS servers (back to Gecko 28), we don't anymore. Does this mean we can remove compatibility traits for it?
 
--- a/devtools/server/actors/highlighters/eye-dropper.js
+++ b/devtools/server/actors/highlighters/eye-dropper.js
@@ -378,17 +378,17 @@ EyeDropper.prototype = {
    */
   selectColor() {
     let onColorSelected = Promise.resolve();
     if (this.options.copyOnSelect) {
       onColorSelected = this.copyColor();
     }
 
     this.emit("selected", toColorString(this.centerColor, this.format));
-    onColorSelected.then(() => this.hide(), e => console.error(e));
+    onColorSelected.then(() => this.hide(), console.error);
   },
 
   /**
    * Handler for the keydown event. Either select the color or move the panel in a
    * direction depending on the key pressed.
    */
   handleKeyDown(e) {
     // Bail out early if any unsupported modifier is used, so that we let
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -1246,17 +1246,17 @@ Front.prototype = extend(Pool.prototype,
    */
   send: function (packet) {
     if (packet.to) {
       this.conn._transport.send(packet);
     } else {
       this.actor().then(actorID => {
         packet.to = actorID;
         this.conn._transport.send(packet);
-      }).catch(e => console.error(e));
+      }).catch(console.error);
     }
   },
 
   /**
    * Send a two-way request on the connection.
    */
   request: function (packet) {
     let deferred = defer();
--- a/gfx/layers/wr/ScrollingLayersHelper.cpp
+++ b/gfx/layers/wr/ScrollingLayersHelper.cpp
@@ -46,17 +46,17 @@ ScrollingLayersHelper::ScrollingLayersHe
     if (layer->GetIsFixedPosition() &&
         layer->GetFixedPositionScrollContainerId() == fm.GetScrollId()) {
       // If the layer contents are fixed for this metadata onwards, we need
       // to insert the layer's local clip at this point in the clip tree,
       // as a child of whatever's on the stack.
       PushLayerLocalClip(aStackingContext);
     }
 
-    PushScrollLayer(fm, aStackingContext);
+    DefineAndPushScrollLayer(fm, aStackingContext);
   }
 
   // The scrolled clip on the layer is "inside" all of the scrollable metadatas
   // on that layer. That is, the clip scrolls along with the content in
   // child layers. So we need to apply this after pushing all the scroll layers,
   // which we do above.
   if (const Maybe<LayerClip>& scrolledClip = layer->GetScrolledClip()) {
     PushLayerClip(scrolledClip.ref(), aStackingContext);
@@ -141,20 +141,17 @@ ScrollingLayersHelper::DefineAndPushScro
                                                  wr::DisplayListBuilder& aBuilder,
                                                  int32_t aAppUnitsPerDevPixel,
                                                  const StackingContextHelper& aStackingContext,
                                                  WebRenderLayerManager::ClipIdMap& aCache)
 {
   if (!aAsr) {
     return;
   }
-  Maybe<ScrollMetadata> metadata = aAsr->mScrollableFrame->ComputeScrollMetadata(
-      nullptr, aItem->ReferenceFrame(), ContainerLayerParameters(), nullptr);
-  MOZ_ASSERT(metadata);
-  FrameMetrics::ViewID scrollId = metadata->GetMetrics().GetScrollId();
+  FrameMetrics::ViewID scrollId = nsLayoutUtils::ViewIDForASR(aAsr);
   if (aBuilder.TopmostScrollId() == scrollId) {
     // it's already been pushed, so we don't need to recurse any further.
     return;
   }
 
   // Find the first clip up the chain that's "outside" aAsr. Any clips
   // that are "inside" aAsr (i.e. that are scrolled by aAsr) will need to be
   // pushed onto the stack after aAsr has been pushed. On the recursive call
@@ -171,21 +168,29 @@ ScrollingLayersHelper::DefineAndPushScro
       aAppUnitsPerDevPixel, aStackingContext, aCache);
 
   // Once the ancestors are dealt with, we want to make sure all the clips
   // enclosing aAsr are pushed. All the clips enclosing aAsr->mParent were
   // already taken care of in the recursive call, so DefineAndPushChain will
   // push exactly what we want.
   DefineAndPushChain(asrClippedBy, aBuilder, aStackingContext,
       aAppUnitsPerDevPixel, aCache);
-  // Finally, push the ASR itself as a scroll layer. Note that the
-  // implementation of wr_push_scroll_layer in bindings.rs makes sure the
-  // scroll layer doesn't get defined multiple times so we don't need to worry
-  // about that here.
-  if (PushScrollLayer(metadata->GetMetrics(), aStackingContext)) {
+  // Finally, push the ASR itself as a scroll layer. If it's already defined
+  // we can skip the expensive step of computing the ScrollMetadata.
+  bool pushed = false;
+  if (mBuilder->IsScrollLayerDefined(scrollId)) {
+    mBuilder->PushScrollLayer(scrollId);
+    pushed = true;
+  } else {
+    Maybe<ScrollMetadata> metadata = aAsr->mScrollableFrame->ComputeScrollMetadata(
+        nullptr, aItem->ReferenceFrame(), ContainerLayerParameters(), nullptr);
+    MOZ_ASSERT(metadata);
+    pushed = DefineAndPushScrollLayer(metadata->GetMetrics(), aStackingContext);
+  }
+  if (pushed) {
     mPushedClips.push_back(wr::ScrollOrClipId(scrollId));
   }
 }
 
 void
 ScrollingLayersHelper::DefineAndPushChain(const DisplayItemClipChain* aChain,
                                           wr::DisplayListBuilder& aBuilder,
                                           const StackingContextHelper& aStackingContext,
@@ -223,18 +228,18 @@ ScrollingLayersHelper::DefineAndPushChai
   }
   // Finally, push the clip onto the WR stack
   MOZ_ASSERT(clipId);
   aBuilder.PushClip(clipId.value());
   mPushedClips.push_back(wr::ScrollOrClipId(clipId.value()));
 }
 
 bool
-ScrollingLayersHelper::PushScrollLayer(const FrameMetrics& aMetrics,
-                                       const StackingContextHelper& aStackingContext)
+ScrollingLayersHelper::DefineAndPushScrollLayer(const FrameMetrics& aMetrics,
+                                                const StackingContextHelper& aStackingContext)
 {
   if (!aMetrics.IsScrollable()) {
     return false;
   }
   LayerRect contentRect = ViewAs<LayerPixel>(
       aMetrics.GetExpandedScrollableRect() * aMetrics.GetDevPixelsPerCSSPixel(),
       PixelCastJustification::WebRenderHasUnitResolution);
   // TODO: check coordinate systems are sane here
@@ -246,19 +251,20 @@ ScrollingLayersHelper::PushScrollLayer(c
   // is, both of them should be relative to the stacking context `aStackingContext`.
   // However, when we get the scrollable rect from the FrameMetrics, the origin
   // has nothing to do with the position of the frame but instead represents
   // the minimum allowed scroll offset of the scrollable content. While APZ
   // uses this to clamp the scroll position, we don't need to send this to
   // WebRender at all. Instead, we take the position from the composition
   // bounds.
   contentRect.MoveTo(clipBounds.TopLeft());
-  mBuilder->PushScrollLayer(aMetrics.GetScrollId(),
+  mBuilder->DefineScrollLayer(aMetrics.GetScrollId(),
       aStackingContext.ToRelativeLayoutRect(contentRect),
       aStackingContext.ToRelativeLayoutRect(clipBounds));
+  mBuilder->PushScrollLayer(aMetrics.GetScrollId());
   return true;
 }
 
 void
 ScrollingLayersHelper::PushLayerLocalClip(const StackingContextHelper& aStackingContext)
 {
   Layer* layer = mLayer->GetLayer();
   Maybe<ParentLayerRect> clip;
--- a/gfx/layers/wr/ScrollingLayersHelper.h
+++ b/gfx/layers/wr/ScrollingLayersHelper.h
@@ -44,18 +44,18 @@ private:
                                  int32_t aAppUnitsPerDevPixel,
                                  const StackingContextHelper& aStackingContext,
                                  WebRenderLayerManager::ClipIdMap& aCache);
   void DefineAndPushChain(const DisplayItemClipChain* aChain,
                           wr::DisplayListBuilder& aBuilder,
                           const StackingContextHelper& aStackingContext,
                           int32_t aAppUnitsPerDevPixel,
                           WebRenderLayerManager::ClipIdMap& aCache);
-  bool PushScrollLayer(const FrameMetrics& aMetrics,
-                       const StackingContextHelper& aStackingContext);
+  bool DefineAndPushScrollLayer(const FrameMetrics& aMetrics,
+                                const StackingContextHelper& aStackingContext);
   void PushLayerLocalClip(const StackingContextHelper& aStackingContext);
   void PushLayerClip(const LayerClip& aClip,
                      const StackingContextHelper& aSc);
 
   WebRenderLayer* mLayer;
   wr::DisplayListBuilder* mBuilder;
   bool mPushedLayerLocalClip;
   bool mPushedClipAndScroll;
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -673,31 +673,49 @@ void
 DisplayListBuilder::PushBuiltDisplayList(BuiltDisplayList &dl)
 {
   WRDL_LOG("PushBuiltDisplayList\n", mWrState);
   wr_dp_push_built_display_list(mWrState,
                                 dl.dl_desc,
                                 &dl.dl.inner);
 }
 
+bool
+DisplayListBuilder::IsScrollLayerDefined(layers::FrameMetrics::ViewID aScrollId) const
+{
+  return mScrollParents.find(aScrollId) != mScrollParents.end();
+}
+
 void
-DisplayListBuilder::PushScrollLayer(const layers::FrameMetrics::ViewID& aScrollId,
-                                    const wr::LayoutRect& aContentRect,
-                                    const wr::LayoutRect& aClipRect)
+DisplayListBuilder::DefineScrollLayer(const layers::FrameMetrics::ViewID& aScrollId,
+                                      const wr::LayoutRect& aContentRect,
+                                      const wr::LayoutRect& aClipRect)
 {
-  WRDL_LOG("PushScrollLayer id=%" PRIu64 " co=%s cl=%s\n", mWrState,
+  WRDL_LOG("DefineScrollLayer id=%" PRIu64 " co=%s cl=%s\n", mWrState,
       aScrollId, Stringify(aContentRect).c_str(), Stringify(aClipRect).c_str());
-  wr_dp_push_scroll_layer(mWrState, aScrollId, aContentRect, aClipRect);
-  if (!mScrollIdStack.empty()) {
-    auto it = mScrollParents.insert({aScrollId, mScrollIdStack.back()});
-    if (!it.second) { // aScrollId was already a key in mScrollParents
-                      // so check that the parent value is the same.
-      MOZ_ASSERT(it.first->second == mScrollIdStack.back());
-    }
+
+  Maybe<layers::FrameMetrics::ViewID> parent =
+      mScrollIdStack.empty() ? Nothing() : Some(mScrollIdStack.back());
+  auto it = mScrollParents.insert({aScrollId, parent});
+  if (it.second) {
+    // An insertion took place, which means we haven't defined aScrollId before.
+    // So let's define it now.
+    wr_dp_define_scroll_layer(mWrState, aScrollId, aContentRect, aClipRect);
+  } else {
+    // aScrollId was already a key in mScrollParents so check that the parent
+    // value is the same.
+    MOZ_ASSERT(it.first->second == parent);
   }
+}
+
+void
+DisplayListBuilder::PushScrollLayer(const layers::FrameMetrics::ViewID& aScrollId)
+{
+  WRDL_LOG("PushScrollLayer id=%" PRIu64 "\n", mWrState, aScrollId);
+  wr_dp_push_scroll_layer(mWrState, aScrollId);
   mScrollIdStack.push_back(aScrollId);
 }
 
 void
 DisplayListBuilder::PopScrollLayer()
 {
   WRDL_LOG("PopScrollLayer id=%" PRIu64 "\n", mWrState, mScrollIdStack.back());
   mScrollIdStack.pop_back();
@@ -1006,13 +1024,13 @@ DisplayListBuilder::TopmostScrollId()
   }
   return mScrollIdStack.back();
 }
 
 Maybe<layers::FrameMetrics::ViewID>
 DisplayListBuilder::ParentScrollIdFor(layers::FrameMetrics::ViewID aScrollId)
 {
   auto it = mScrollParents.find(aScrollId);
-  return (it == mScrollParents.end() ? Nothing() : Some(it->second));
+  return (it == mScrollParents.end() ? Nothing() : it->second);
 }
 
 } // namespace wr
 } // namespace mozilla
--- a/gfx/webrender_bindings/WebRenderAPI.h
+++ b/gfx/webrender_bindings/WebRenderAPI.h
@@ -190,19 +190,21 @@ public:
   wr::WrClipId DefineClip(const wr::LayoutRect& aClipRect,
                           const nsTArray<wr::WrComplexClipRegion>* aComplex = nullptr,
                           const wr::WrImageMask* aMask = nullptr);
   void PushClip(const wr::WrClipId& aClipId, bool aRecordInStack = true);
   void PopClip(bool aRecordInStack = true);
 
   void PushBuiltDisplayList(wr::BuiltDisplayList &dl);
 
-  void PushScrollLayer(const layers::FrameMetrics::ViewID& aScrollId,
-                       const wr::LayoutRect& aContentRect, // TODO: We should work with strongly typed rects
-                       const wr::LayoutRect& aClipRect);
+  bool IsScrollLayerDefined(layers::FrameMetrics::ViewID aScrollId) const;
+  void DefineScrollLayer(const layers::FrameMetrics::ViewID& aScrollId,
+                         const wr::LayoutRect& aContentRect, // TODO: We should work with strongly typed rects
+                         const wr::LayoutRect& aClipRect);
+  void PushScrollLayer(const layers::FrameMetrics::ViewID& aScrollId);
   void PopScrollLayer();
 
   void PushClipAndScrollInfo(const layers::FrameMetrics::ViewID& aScrollId,
                              const wr::WrClipId* aClipId);
   void PopClipAndScrollInfo();
 
   void PushRect(const wr::LayoutRect& aBounds,
                 const wr::LayoutRect& aClip,
@@ -343,18 +345,20 @@ protected:
 
   // Track the stack of clip ids and scroll layer ids that have been pushed
   // (by PushClip and PushScrollLayer, respectively) and are still active.
   // This is helpful for knowing e.g. what the ancestor scroll id of a particular
   // scroll id is, and doing other "queries" of current state.
   std::vector<wr::WrClipId> mClipIdStack;
   std::vector<layers::FrameMetrics::ViewID> mScrollIdStack;
 
-  // Track the parent scroll id of each scroll id that we encountered.
-  std::unordered_map<layers::FrameMetrics::ViewID, layers::FrameMetrics::ViewID> mScrollParents;
+  // Track the parent scroll id of each scroll id that we encountered. A
+  // Nothing() value indicates a root scroll id. We also use this structure to
+  // ensure that we don't define a particular scroll layer multiple times.
+  std::unordered_map<layers::FrameMetrics::ViewID, Maybe<layers::FrameMetrics::ViewID>> mScrollParents;
 
   friend class WebRenderAPI;
 };
 
 Maybe<wr::ImageFormat>
 SurfaceFormatToImageFormat(gfx::SurfaceFormat aFormat);
 
 } // namespace wr
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -1,9 +1,8 @@
-use std::collections::HashSet;
 use std::ffi::CString;
 use std::{mem, slice};
 use std::path::PathBuf;
 use std::sync::Arc;
 use std::os::raw::{c_void, c_char, c_float};
 use gleam::gl;
 
 use webrender_api::*;
@@ -921,26 +920,24 @@ pub unsafe extern "C" fn wr_api_get_name
 // and moving more of the logic in C++ land.
 // This work is tracked by bug 1328602.
 //
 // See RenderThread.h for some notes about how the pieces fit together.
 
 pub struct WebRenderFrameBuilder {
     pub root_pipeline_id: WrPipelineId,
     pub dl_builder: webrender_api::DisplayListBuilder,
-    pub scroll_clips_defined: HashSet<ClipId>,
 }
 
 impl WebRenderFrameBuilder {
     pub fn new(root_pipeline_id: WrPipelineId,
                content_size: LayoutSize) -> WebRenderFrameBuilder {
         WebRenderFrameBuilder {
             root_pipeline_id: root_pipeline_id,
             dl_builder: webrender_api::DisplayListBuilder::new(root_pipeline_id, content_size),
-            scroll_clips_defined: HashSet::new(),
         }
     }
 }
 
 pub struct WrState {
     pipeline_id: WrPipelineId,
     frame_builder: WebRenderFrameBuilder,
 }
@@ -1097,31 +1094,32 @@ pub extern "C" fn wr_dp_push_clip(state:
 
 #[no_mangle]
 pub extern "C" fn wr_dp_pop_clip(state: &mut WrState) {
     assert!(unsafe { !is_in_render_thread() });
     state.frame_builder.dl_builder.pop_clip_id();
 }
 
 #[no_mangle]
-pub extern "C" fn wr_dp_push_scroll_layer(state: &mut WrState,
-                                          scroll_id: u64,
-                                          content_rect: LayoutRect,
-                                          clip_rect: LayoutRect) {
+pub extern "C" fn wr_dp_define_scroll_layer(state: &mut WrState,
+                                            scroll_id: u64,
+                                            content_rect: LayoutRect,
+                                            clip_rect: LayoutRect) {
     assert!(unsafe { is_in_main_thread() });
     let clip_id = ClipId::new(scroll_id, state.pipeline_id);
-    // Avoid defining multiple scroll clips with the same clip id, as that
-    // results in undefined behaviour or assertion failures.
-    if !state.frame_builder.scroll_clips_defined.contains(&clip_id) {
+    state.frame_builder.dl_builder.define_scroll_frame(
+        Some(clip_id), content_rect, clip_rect, vec![], None,
+        ScrollSensitivity::Script);
+}
 
-        state.frame_builder.dl_builder.define_scroll_frame(
-            Some(clip_id), content_rect, clip_rect, vec![], None,
-            ScrollSensitivity::Script);
-        state.frame_builder.scroll_clips_defined.insert(clip_id);
-    }
+#[no_mangle]
+pub extern "C" fn wr_dp_push_scroll_layer(state: &mut WrState,
+                                          scroll_id: u64) {
+    assert!(unsafe { is_in_main_thread() });
+    let clip_id = ClipId::new(scroll_id, state.pipeline_id);
     state.frame_builder.dl_builder.push_clip_id(clip_id);
 }
 
 #[no_mangle]
 pub extern "C" fn wr_dp_pop_scroll_layer(state: &mut WrState) {
     assert!(unsafe { is_in_main_thread() });
     state.frame_builder.dl_builder.pop_clip_id();
 }
--- a/gfx/webrender_bindings/webrender_ffi_generated.h
+++ b/gfx/webrender_bindings/webrender_ffi_generated.h
@@ -856,16 +856,23 @@ WR_INLINE
 uint64_t wr_dp_define_clip(WrState *aState,
                            LayoutRect aClipRect,
                            const WrComplexClipRegion *aComplex,
                            size_t aComplexCount,
                            const WrImageMask *aMask)
 WR_FUNC;
 
 WR_INLINE
+void wr_dp_define_scroll_layer(WrState *aState,
+                               uint64_t aScrollId,
+                               LayoutRect aContentRect,
+                               LayoutRect aClipRect)
+WR_FUNC;
+
+WR_INLINE
 void wr_dp_end(WrState *aState)
 WR_FUNC;
 
 WR_INLINE
 void wr_dp_pop_clip(WrState *aState)
 WR_FUNC;
 
 WR_INLINE
@@ -1022,19 +1029,17 @@ WR_INLINE
 void wr_dp_push_rect(WrState *aState,
                      LayoutRect aRect,
                      LayoutRect aClip,
                      ColorF aColor)
 WR_FUNC;
 
 WR_INLINE
 void wr_dp_push_scroll_layer(WrState *aState,
-                             uint64_t aScrollId,
-                             LayoutRect aContentRect,
-                             LayoutRect aClipRect)
+                             uint64_t aScrollId)
 WR_FUNC;
 
 WR_INLINE
 void wr_dp_push_stacking_context(WrState *aState,
                                  LayoutRect aBounds,
                                  uint64_t aAnimationId,
                                  const float *aOpacity,
                                  const LayoutTransform *aTransform,
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -392,21 +392,17 @@ var Addons = {
     // newly selected extension.
     optionsBox.innerHTML = "";
 
     switch (parseInt(addon.optionsType)) {
       case AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
         // Allow the options to use all the available width space.
         optionsBox.classList.remove("inner");
 
-        // WebExtensions are loaded asynchronously and the optionsURL
-        // may not be available via listitem when the add-on has just been
-        // installed, but it is available on the addon if one is set.
-        detailItem.setAttribute("optionsURL", addon.optionsURL);
-        this.createWebExtensionOptions(optionsBox, addon.optionsURL, addon.optionsBrowserStyle);
+        this.createWebExtensionOptions(optionsBox, addon);
         break;
       case AddonManager.OPTIONS_TYPE_TAB:
         // Keep the usual layout for any options related the legacy (or system) add-ons
         // when the options are opened in a new tab from a single button in the addon
         // details page.
         optionsBox.classList.add("inner");
 
         this.createOptionsInTabButton(optionsBox, addon);
@@ -436,54 +432,68 @@ var Addons = {
     if (!button) {
       button = document.createElement("button");
       button.setAttribute("id", "open-addon-options");
       button.textContent = gStringBundle.GetStringFromName("addon.options");
       destination.appendChild(button);
     }
 
     button.onclick = async () => {
+      if (addon.isWebExtension) {
+        // WebExtensions are loaded asynchronously and the optionsURL
+        // may not be available until the addon has been started.
+        await addon.startupPromise;
+      }
+
       const {optionsURL} = addon;
       openOptionsInTab(optionsURL);
     };
   },
 
-  createWebExtensionOptions: async function(destination, optionsURL, browserStyle) {
-    let originalHeight;
-    let frame = document.createElement("iframe");
-    frame.setAttribute("id", "addon-options");
-    frame.setAttribute("mozbrowser", "true");
-    frame.setAttribute("style", "width: 100%; overflow: hidden;");
+  createWebExtensionOptions: async function(destination, addon) {
+    // WebExtensions are loaded asynchronously and the optionsURL
+    // may not be available until the addon has been started.
+    await addon.startupPromise;
 
-    // Adjust iframe height to the iframe content (also between navigation of multiple options
-    // files).
-    frame.onload = (evt) => {
-      if (evt.target !== frame) {
-        return;
-      }
+    const {optionsURL, optionsBrowserStyle} = addon;
+    let frame = destination.querySelector("iframe#addon-options");
+
+    if (!frame) {
+      let originalHeight;
+      frame = document.createElement("iframe");
+      frame.setAttribute("id", "addon-options");
+      frame.setAttribute("mozbrowser", "true");
+      frame.setAttribute("style", "width: 100%; overflow: hidden;");
 
-      const {document} = frame.contentWindow;
-      const bodyScrollHeight = document.body && document.body.scrollHeight;
-      const documentScrollHeight = document.documentElement.scrollHeight;
+      // Adjust iframe height to the iframe content (also between navigation of multiple options
+      // files).
+      frame.onload = (evt) => {
+        if (evt.target !== frame) {
+          return;
+        }
 
-      // Set the iframe height to the maximum between the body and the document
-      // scrollHeight values.
-      frame.style.height = Math.max(bodyScrollHeight, documentScrollHeight) + "px";
+        const {document} = frame.contentWindow;
+        const bodyScrollHeight = document.body && document.body.scrollHeight;
+        const documentScrollHeight = document.documentElement.scrollHeight;
 
-      // Restore the original iframe height between option page loads,
-      // so that we don't force the new document to have the same size
-      // of the previosuly loaded option page.
-      frame.contentWindow.addEventListener("unload", () => {
-        frame.style.height = originalHeight + "px";
-      }, {once: true});
-    };
+        // Set the iframe height to the maximum between the body and the document
+        // scrollHeight values.
+        frame.style.height = Math.max(bodyScrollHeight, documentScrollHeight) + "px";
 
-    destination.appendChild(frame);
+        // Restore the original iframe height between option page loads,
+        // so that we don't force the new document to have the same size
+        // of the previosuly loaded option page.
+        frame.contentWindow.addEventListener("unload", () => {
+          frame.style.height = originalHeight + "px";
+        }, {once: true});
+      };
 
-    originalHeight = frame.getBoundingClientRect().height;
+      destination.appendChild(frame);
+      originalHeight = frame.getBoundingClientRect().height;
+    }
 
     // Loading the URL this way prevents the native back
     // button from applying to the iframe.
     frame.contentWindow.location.replace(optionsURL);
   },
 
   createInlineOptions(destination, optionsURL, aListItem) {
     // This function removes and returns the text content of aNode without
@@ -580,16 +590,24 @@ var Addons = {
     }
 
     if (addon == detailItem.addon) {
       detailItem.setAttribute("isDisabled", !aValue);
       if (opType)
         detailItem.setAttribute("opType", opType);
       else
         detailItem.removeAttribute("opType");
+
+      // Remove any addon options iframe if the currently selected addon has been disabled.
+      if (!aValue) {
+        const addonOptionsIframe = document.querySelector("#addon-options");
+        if (addonOptionsIframe) {
+          addonOptionsIframe.remove();
+        }
+      }
     }
 
     // Sync to the list item
     if (listItem) {
       listItem.setAttribute("isDisabled", !aValue);
       if (opType)
         listItem.setAttribute("opType", opType);
       else
--- a/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
@@ -79,16 +79,24 @@ function waitDOMContentLoaded(checkUrlCb
     BrowserApp.deck.addEventListener("DOMContentLoaded", listener);
   });
 }
 
 function waitAboutAddonsLoaded() {
   return waitDOMContentLoaded(url => url === "about:addons");
 }
 
+function clickAddonDisable() {
+  content.document.querySelector("#disable-btn").click();
+}
+
+function clickAddonEnable() {
+  content.document.querySelector("#enable-btn").click();
+}
+
 add_task(async function test_options_ui_iframe_height() {
   let addonID = "test-options-ui@mozilla.org";
 
   let extension = ExtensionTestUtils.loadExtension({
     useAddonManager: "temporary",
     manifest: {
       applications: {
         gecko: {id: addonID},
@@ -401,12 +409,92 @@ add_task(async function test_options_ui_
   ok(BrowserApp.selectedTab !== oldSelectedTab,
      "runtime.openOptionsPage has opened a new tab");
 
   BrowserApp.closeTab(BrowserApp.selectedTab);
 
   await extension.unload();
 });
 
+add_task(async function test_options_ui_on_disable_and_enable() {
+  let addonID = "test-options-ui-disable-enable@mozilla.org";
+
+  function optionsScript() {
+    browser.test.sendMessage("options-page-loaded", window.location.href);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary",
+    manifest: {
+      applications: {
+        gecko: {id: addonID},
+      },
+      name: "Options UI open addon details Extension",
+      description: "Longer addon description",
+      options_ui: {
+        page: "options.html",
+      },
+    },
+    files: {
+      "options.js": optionsScript,
+      "options.html": `<!DOCTYPE html>
+       <html>
+         <head>
+           <meta charset="utf-8">
+         </head>
+         <body>
+           <h1>Options page</h1>
+           <script src="options.js"><\/script>
+         </body>
+       </html>
+     `,
+    },
+  });
+
+  await extension.startup();
+
+  const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
+
+  let onceAboutAddonsLoaded = waitAboutAddonsLoaded();
+
+  BrowserApp.addTab("about:addons", {
+    selected: true,
+    parentId: BrowserApp.selectedTab.id,
+  });
+
+  await onceAboutAddonsLoaded;
+
+  const aboutAddonsTab = BrowserApp.selectedTab;
+
+  is(aboutAddonsTab.currentURI.spec, "about:addons",
+     "about:addons is the currently selected tab");
+
+  info("Wait the addon details to have been loaded");
+  await ContentTask.spawn(aboutAddonsTab.browser, addonID, waitAboutAddonsRendered);
+  await ContentTask.spawn(aboutAddonsTab.browser, addonID, navigateToAddonDetails);
+
+  info("Wait the addon options page to have been loaded");
+  await extension.awaitMessage("options-page-loaded");
+
+  info("Click the addon disable button");
+  await ContentTask.spawn(aboutAddonsTab.browser, null, clickAddonDisable);
+
+  // NOTE: Currently after disabling the addon the extension.awaitMessage seems
+  // to fail be able to receive events coming from the browser.test.sendMessage API
+  // (nevertheless `await extension.unload()` seems to be able to remove the extension),
+  // falling back to wait for the options page to be loaded here.
+  const onceAddonOptionsLoaded = waitDOMContentLoaded(url => url.endsWith("options.html"));
+
+  info("Click the addon enable button");
+  await ContentTask.spawn(aboutAddonsTab.browser, null, clickAddonEnable);
+
+  info("Wait the addon options page to have been loaded after clicking the addon enable button");
+  await onceAddonOptionsLoaded;
+
+  BrowserApp.closeTab(BrowserApp.selectedTab);
+
+  await extension.unload();
+});
+
 </script>
 
 </body>
 </html>
--- a/servo/components/script/dom/bindings/codegen/parser/WebIDL.py
+++ b/servo/components/script/dom/bindings/codegen/parser/WebIDL.py
@@ -2059,16 +2059,19 @@ class IDLType(IDLObject):
         return self.name == "Void"
 
     def isSequence(self):
         return False
 
     def isRecord(self):
         return False
 
+    def isReadableStream(self):
+        return False
+
     def isArrayBuffer(self):
         return False
 
     def isArrayBufferView(self):
         return False
 
     def isSharedArrayBuffer(self):
         return False
@@ -2086,22 +2089,22 @@ class IDLType(IDLObject):
         """ Returns a boolean indicating whether this type is an 'interface'
             type that is implemented in Gecko. At the moment, this returns
             true for all interface types that are not types from the TypedArray
             spec."""
         return self.isInterface() and not self.isSpiderMonkeyInterface()
 
     def isSpiderMonkeyInterface(self):
         """ Returns a boolean indicating whether this type is an 'interface'
-            type that is implemented in Spidermonkey.  At the moment, this
-            only returns true for the types from the TypedArray spec. """
+            type that is implemented in SpiderMonkey. """
         return self.isInterface() and (self.isArrayBuffer() or
                                        self.isArrayBufferView() or
                                        self.isSharedArrayBuffer() or
-                                       self.isTypedArray())
+                                       self.isTypedArray() or
+                                       self.isReadableStream())
 
     def isDictionary(self):
         return False
 
     def isInterface(self):
         return False
 
     def isAny(self):
@@ -2284,16 +2287,19 @@ class IDLNullableType(IDLParametrizedTyp
         return False
 
     def isSequence(self):
         return self.inner.isSequence()
 
     def isRecord(self):
         return self.inner.isRecord()
 
+    def isReadableStream(self):
+        return self.inner.isReadableStream()
+
     def isArrayBuffer(self):
         return self.inner.isArrayBuffer()
 
     def isArrayBufferView(self):
         return self.inner.isArrayBufferView()
 
     def isSharedArrayBuffer(self):
         return self.inner.isSharedArrayBuffer()
@@ -2651,16 +2657,19 @@ class IDLTypedefType(IDLType):
         return self.inner.isVoid()
 
     def isSequence(self):
         return self.inner.isSequence()
 
     def isRecord(self):
         return self.inner.isRecord()
 
+    def isReadableStream(self):
+        return self.inner.isReadableStream()
+
     def isDictionary(self):
         return self.inner.isDictionary()
 
     def isArrayBuffer(self):
         return self.inner.isArrayBuffer()
 
     def isArrayBufferView(self):
         return self.inner.isArrayBufferView()
@@ -2965,17 +2974,18 @@ class IDLBuiltinType(IDLType):
         'Int8Array',
         'Uint8Array',
         'Uint8ClampedArray',
         'Int16Array',
         'Uint16Array',
         'Int32Array',
         'Uint32Array',
         'Float32Array',
-        'Float64Array'
+        'Float64Array',
+        'ReadableStream',
         )
 
     TagLookup = {
         Types.byte: IDLType.Tags.int8,
         Types.octet: IDLType.Tags.uint8,
         Types.short: IDLType.Tags.int16,
         Types.unsigned_short: IDLType.Tags.uint16,
         Types.long: IDLType.Tags.int32,
@@ -3000,17 +3010,18 @@ class IDLBuiltinType(IDLType):
         Types.Int8Array: IDLType.Tags.interface,
         Types.Uint8Array: IDLType.Tags.interface,
         Types.Uint8ClampedArray: IDLType.Tags.interface,
         Types.Int16Array: IDLType.Tags.interface,
         Types.Uint16Array: IDLType.Tags.interface,
         Types.Int32Array: IDLType.Tags.interface,
         Types.Uint32Array: IDLType.Tags.interface,
         Types.Float32Array: IDLType.Tags.interface,
-        Types.Float64Array: IDLType.Tags.interface
+        Types.Float64Array: IDLType.Tags.interface,
+        Types.ReadableStream: IDLType.Tags.interface,
     }
 
     def __init__(self, location, name, type):
         IDLType.__init__(self, location, name)
         self.builtin = True
         self._typeTag = type
 
     def isPrimitive(self):
@@ -3047,24 +3058,28 @@ class IDLBuiltinType(IDLType):
 
     def isSharedArrayBuffer(self):
         return self._typeTag == IDLBuiltinType.Types.SharedArrayBuffer
 
     def isTypedArray(self):
         return (self._typeTag >= IDLBuiltinType.Types.Int8Array and
                 self._typeTag <= IDLBuiltinType.Types.Float64Array)
 
+    def isReadableStream(self):
+        return self._typeTag == IDLBuiltinType.Types.ReadableStream
+
     def isInterface(self):
         # TypedArray things are interface types per the TypedArray spec,
         # but we handle them as builtins because SpiderMonkey implements
         # all of it internally.
         return (self.isArrayBuffer() or
                 self.isArrayBufferView() or
                 self.isSharedArrayBuffer() or
-                self.isTypedArray())
+                self.isTypedArray() or
+                self.isReadableStream())
 
     def isNonCallbackInterface(self):
         # All the interfaces we can be are non-callback
         return self.isInterface()
 
     def isFloat(self):
         return (self._typeTag == IDLBuiltinType.Types.float or
                 self._typeTag == IDLBuiltinType.Types.double or
@@ -3124,16 +3139,17 @@ class IDLBuiltinType(IDLType):
         return (other.isPrimitive() or other.isString() or other.isEnum() or
                 other.isCallback() or other.isDictionary() or
                 other.isSequence() or other.isRecord() or other.isDate() or
                 (other.isInterface() and (
                  # ArrayBuffer is distinguishable from everything
                  # that's not an ArrayBuffer or a callback interface
                  (self.isArrayBuffer() and not other.isArrayBuffer()) or
                  (self.isSharedArrayBuffer() and not other.isSharedArrayBuffer()) or
+                 (self.isReadableStream() and not other.isReadableStream()) or
                  # ArrayBufferView is distinguishable from everything
                  # that's not an ArrayBufferView or typed array.
                  (self.isArrayBufferView() and not other.isArrayBufferView() and
                   not other.isTypedArray()) or
                  # Typed arrays are distinguishable from everything
                  # except ArrayBufferView and the same type of typed
                  # array
                  (self.isTypedArray() and not other.isArrayBufferView() and not
@@ -3233,17 +3249,20 @@ BuiltinTypes = {
     IDLBuiltinType.Types.Uint32Array:
         IDLBuiltinType(BuiltinLocation("<builtin type>"), "Uint32Array",
                        IDLBuiltinType.Types.Uint32Array),
     IDLBuiltinType.Types.Float32Array:
         IDLBuiltinType(BuiltinLocation("<builtin type>"), "Float32Array",
                        IDLBuiltinType.Types.Float32Array),
     IDLBuiltinType.Types.Float64Array:
         IDLBuiltinType(BuiltinLocation("<builtin type>"), "Float64Array",
-                       IDLBuiltinType.Types.Float64Array)
+                       IDLBuiltinType.Types.Float64Array),
+    IDLBuiltinType.Types.ReadableStream:
+        IDLBuiltinType(BuiltinLocation("<builtin type>"), "ReadableStream",
+                       IDLBuiltinType.Types.ReadableStream),
 }
 
 
 integerTypeSizes = {
     IDLBuiltinType.Types.byte: (-128, 127),
     IDLBuiltinType.Types.octet:  (0, 255),
     IDLBuiltinType.Types.short: (-32768, 32767),
     IDLBuiltinType.Types.unsigned_short: (0, 65535),
@@ -5282,17 +5301,18 @@ class Tokenizer(object):
         "<": "LT",
         ">": "GT",
         "ArrayBuffer": "ARRAYBUFFER",
         "SharedArrayBuffer": "SHAREDARRAYBUFFER",
         "or": "OR",
         "maplike": "MAPLIKE",
         "setlike": "SETLIKE",
         "iterable": "ITERABLE",
-        "namespace": "NAMESPACE"
+        "namespace": "NAMESPACE",
+        "ReadableStream": "READABLESTREAM",
         }
 
     tokens.extend(keywords.values())
 
     def t_error(self, t):
         raise WebIDLError("Unrecognized Input",
                           [Location(lexer=self.lexer,
                                     lineno=self.lexer.lineno,
@@ -6470,24 +6490,27 @@ class Parser(Tokenizer):
         """
         p[0] = []
 
     def p_NonAnyType(self, p):
         """
             NonAnyType : PrimitiveType Null
                        | ARRAYBUFFER Null
                        | SHAREDARRAYBUFFER Null
+                       | READABLESTREAM Null
                        | OBJECT Null
         """
         if p[1] == "object":
             type = BuiltinTypes[IDLBuiltinType.Types.object]
         elif p[1] == "ArrayBuffer":
             type = BuiltinTypes[IDLBuiltinType.Types.ArrayBuffer]
         elif p[1] == "SharedArrayBuffer":
             type = BuiltinTypes[IDLBuiltinType.Types.SharedArrayBuffer]
+        elif p[1] == "ReadableStream":
+            type = BuiltinTypes[IDLBuiltinType.Types.ReadableStream]
         else:
             type = BuiltinTypes[p[1]]
 
         p[0] = self.handleNullable(type, p[2])
 
     def p_NonAnyTypeStringType(self, p):
         """
             NonAnyType : StringType Null
--- a/testing/marionette/action.js
+++ b/testing/marionette/action.js
@@ -6,28 +6,26 @@
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/element.js");
 const {
-  error,
+  pprint,
   InvalidArgumentError,
   MoveTargetOutOfBoundsError,
   UnsupportedOperationError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
 
 this.EXPORTED_SYMBOLS = ["action"];
 
-const {pprint} = error;
-
 // TODO? With ES 2016 and Symbol you can make a safer approximation
 // to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689
 /**
  * Implements WebDriver Actions API: a low-level interface for providing
  * virtualised device input to the web browser.
  *
  * @namespace
  */
--- a/testing/marionette/assert.js
+++ b/testing/marionette/assert.js
@@ -6,20 +6,20 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const {
-  error,
   InvalidArgumentError,
   InvalidSessionIDError,
   NoSuchWindowError,
+  pprint,
   UnexpectedAlertOpenError,
   UnsupportedOperationError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 
 this.EXPORTED_SYMBOLS = ["assert"];
 
 const isFennec = () => AppConstants.platform == "android";
 const isFirefox = () => Services.appinfo.name == "Firefox";
@@ -169,17 +169,17 @@ assert.noUserPrompt = function(dialog, m
  *
  * @return {?}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not defined.
  */
 assert.defined = function(obj, msg = "") {
-  msg = msg || error.pprint`Expected ${obj} to be defined`;
+  msg = msg || pprint`Expected ${obj} to be defined`;
   return assert.that(o => typeof o != "undefined", msg)(obj);
 };
 
 /**
  * Asserts that |obj| is a finite number.
  *
  * @param {?} obj
  *     Value to test.
@@ -188,17 +188,17 @@ assert.defined = function(obj, msg = "")
  *
  * @return {number}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not a number.
  */
 assert.number = function(obj, msg = "") {
-  msg = msg || error.pprint`Expected ${obj} to be finite number`;
+  msg = msg || pprint`Expected ${obj} to be finite number`;
   return assert.that(Number.isFinite, msg)(obj);
 };
 
 /**
  * Asserts that |obj| is callable.
  *
  * @param {?} obj
  *     Value to test.
@@ -207,17 +207,17 @@ assert.number = function(obj, msg = "") 
  *
  * @return {Function}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not callable.
  */
 assert.callable = function(obj, msg = "") {
-  msg = msg || error.pprint`${obj} is not callable`;
+  msg = msg || pprint`${obj} is not callable`;
   return assert.that(o => typeof o == "function", msg)(obj);
 };
 
 /**
  * Asserts that |obj| is an integer.
  *
  * @param {?} obj
  *     Value to test.
@@ -226,17 +226,17 @@ assert.callable = function(obj, msg = ""
  *
  * @return {number}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not an integer.
  */
 assert.integer = function(obj, msg = "") {
-  msg = msg || error.pprint`Expected ${obj} to be an integer`;
+  msg = msg || pprint`Expected ${obj} to be an integer`;
   return assert.that(Number.isInteger, msg)(obj);
 };
 
 /**
  * Asserts that |obj| is a positive integer.
  *
  * @param {?} obj
  *     Value to test.
@@ -246,17 +246,17 @@ assert.integer = function(obj, msg = "")
  * @return {number}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not a positive integer.
  */
 assert.positiveInteger = function(obj, msg = "") {
   assert.integer(obj, msg);
-  msg = msg || error.pprint`Expected ${obj} to be >= 0`;
+  msg = msg || pprint`Expected ${obj} to be >= 0`;
   return assert.that(n => n >= 0, msg)(obj);
 };
 
 /**
  * Asserts that |obj| is a boolean.
  *
  * @param {?} obj
  *     Value to test.
@@ -265,17 +265,17 @@ assert.positiveInteger = function(obj, m
  *
  * @return {boolean}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not a boolean.
  */
 assert.boolean = function(obj, msg = "") {
-  msg = msg || error.pprint`Expected ${obj} to be boolean`;
+  msg = msg || pprint`Expected ${obj} to be boolean`;
   return assert.that(b => typeof b == "boolean", msg)(obj);
 };
 
 /**
  * Asserts that |obj| is a string.
  *
  * @param {?} obj
  *     Value to test.
@@ -284,17 +284,17 @@ assert.boolean = function(obj, msg = "")
  *
  * @return {string}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not a string.
  */
 assert.string = function(obj, msg = "") {
-  msg = msg || error.pprint`Expected ${obj} to be a string`;
+  msg = msg || pprint`Expected ${obj} to be a string`;
   return assert.that(s => typeof s == "string", msg)(obj);
 };
 
 /**
  * Asserts that |obj| is an object.
  *
  * @param {?} obj
  *     Value to test.
@@ -303,17 +303,17 @@ assert.string = function(obj, msg = "") 
  *
  * @return {Object}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not an object.
  */
 assert.object = function(obj, msg = "") {
-  msg = msg || error.pprint`Expected ${obj} to be an object`;
+  msg = msg || pprint`Expected ${obj} to be an object`;
   return assert.that(o => {
     // unable to use instanceof because LHS and RHS may come from
     // different globals
     let s = Object.prototype.toString.call(o);
     return s == "[object Object]" || s == "[object nsJSIID]";
   }, msg)(obj);
 };
 
@@ -330,17 +330,17 @@ assert.object = function(obj, msg = "") 
  * @return {?}
  *     Value of |obj|'s own property |prop|.
  *
  * @throws {InvalidArgumentError}
  *     If |prop| is not in |obj|, or |obj| is not an object.
  */
 assert.in = function(prop, obj, msg = "") {
   assert.object(obj, msg);
-  msg = msg || error.pprint`Expected ${prop} in ${obj}`;
+  msg = msg || pprint`Expected ${prop} in ${obj}`;
   assert.that(p => obj.hasOwnProperty(p), msg)(prop);
   return obj[prop];
 };
 
 /**
  * Asserts that |obj| is an Array.
  *
  * @param {?} obj
@@ -350,17 +350,17 @@ assert.in = function(prop, obj, msg = ""
  *
  * @return {Object}
  *     |obj| is returned unaltered.
  *
  * @throws {InvalidArgumentError}
  *     If |obj| is not an Array.
  */
 assert.array = function(obj, msg = "") {
-  msg = msg || error.pprint`Expected ${obj} to be an Array`;
+  msg = msg || pprint`Expected ${obj} to be an Array`;
   return assert.that(Array.isArray, msg)(obj);
 };
 
 /**
  * Returns a function that is used to assert the |predicate|.
  *
  * @param {function(?): boolean} predicate
  *     Evaluated on calling the return value of this function.  If its
--- a/testing/marionette/cookie.js
+++ b/testing/marionette/cookie.js
@@ -5,18 +5,18 @@
 "use strict";
 
 const {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.import("chrome://marionette/content/assert.js");
 const {
-  error,
   InvalidCookieDomainError,
+  pprint,
 } = Cu.import("chrome://marionette/content/error.js", {});
 
 this.EXPORTED_SYMBOLS = ["cookie"];
 
 const IPV4_PORT_EXPR = /:\d+$/;
 
 /** @namespace */
 this.cookie = {
@@ -48,17 +48,17 @@ this.cookie = {
  *     Valid cookie object.
  *
  * @throws {InvalidArgumentError}
  *     If any of the properties are invalid.
  */
 cookie.fromJSON = function(json) {
   let newCookie = {};
 
-  assert.object(json, error.pprint`Expected cookie object, got ${json}`);
+  assert.object(json, pprint`Expected cookie object, got ${json}`);
 
   newCookie.name = assert.string(json.name, "Cookie name must be string");
   newCookie.value = assert.string(json.value, "Cookie value must be string");
 
   if (typeof json.path != "undefined") {
     newCookie.path = assert.string(json.path, "Cookie path must be string");
   }
   if (typeof json.secure != "undefined") {
--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -7,20 +7,20 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Log.jsm");
 
 Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/atom.js");
 const {
-  error,
   InvalidSelectorError,
   JavaScriptError,
   NoSuchElementError,
+  pprint,
   StaleElementReferenceError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 Cu.import("chrome://marionette/content/wait.js");
 
 const logger = Log.repository.getLogger("Marionette");
 
 this.EXPORTED_SYMBOLS = ["element"];
 
@@ -175,17 +175,17 @@ element.Store = class {
       el = el.get();
     } catch (e) {
       el = null;
       delete this.els[uuid];
     }
 
     if (element.isStale(el)) {
       throw new StaleElementReferenceError(
-          error.pprint`The element reference of ${el} stale; ` +
+          pprint`The element reference of ${el} stale; ` +
               "either the element is no longer attached to the DOM " +
               "or the document has been refreshed");
     }
 
     return el;
   }
 };
 
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -40,17 +40,21 @@ const BUILTIN_ERRORS = new Set([
   "InternalError",
   "RangeError",
   "ReferenceError",
   "SyntaxError",
   "TypeError",
   "URIError",
 ]);
 
-this.EXPORTED_SYMBOLS = ["error", "error.pprint"].concat(Array.from(ERRORS));
+this.EXPORTED_SYMBOLS = [
+  "error",
+  "pprint",
+  "stack",
+].concat(Array.from(ERRORS));
 
 /** @namespace */
 this.error = {};
 
 /**
  * Check if |val| is an instance of the |Error| prototype.
  *
  * Because error objects may originate from different globals, comparing
@@ -153,17 +157,17 @@ error.stringify = function(err) {
  *     let bool = {value: true};
  *     pprint`Expected boolean, got ${bool}`;
  *     => 'Expected boolean, got [object Object] {"value": true}'
  *
  *     let htmlElement = document.querySelector("input#foo");
  *     pprint`Expected element ${htmlElement}`;
  *     => 'Expected element <input id="foo" class="bar baz">'
  */
-error.pprint = function(ss, ...values) {
+this.pprint = function(ss, ...values) {
   function prettyObject(obj) {
     let proto = Object.prototype.toString.call(obj);
     let s = "";
     try {
       s = JSON.stringify(obj);
     } catch (e) {
       if (e instanceof TypeError) {
         s = `<${e.message}>`;
@@ -207,16 +211,24 @@ error.pprint = function(ss, ...values) {
         s = typeof val;
       }
       res.push(s);
     }
   }
   return res.join("");
 };
 
+/** Create a stacktrace to the current line in the program. */
+this.stack = function() {
+  let trace = new Error().stack;
+  let sa = trace.split("\n");
+  sa = sa.slice(1);
+  return "stacktrace:\n" + sa.join("\n");
+};
+
 /**
  * WebDriverError is the prototypal parent of all WebDriver errors.
  * It should not be used directly, as it does not correspond to a real
  * error in the specification.
  */
 class WebDriverError extends Error {
   /**
    * @param {(string|Error)=} x
@@ -300,27 +312,27 @@ class ElementClickInterceptedError exten
   constructor(obscuredEl = undefined, coords = undefined) {
     let msg = "";
     if (obscuredEl && coords) {
       const doc = obscuredEl.ownerDocument;
       const overlayingEl = doc.elementFromPoint(coords.x, coords.y);
 
       switch (obscuredEl.style.pointerEvents) {
         case "none":
-          msg = error.pprint`Element ${obscuredEl} is not clickable ` +
+          msg = pprint`Element ${obscuredEl} is not clickable ` +
               `at point (${coords.x},${coords.y}) ` +
               `because it does not have pointer events enabled, ` +
-              error.pprint`and element ${overlayingEl} ` +
+              pprint`and element ${overlayingEl} ` +
               `would receive the click instead`;
           break;
 
         default:
-          msg = error.pprint`Element ${obscuredEl} is not clickable ` +
+          msg = pprint`Element ${obscuredEl} is not clickable ` +
               `at point (${coords.x},${coords.y}) ` +
-              error.pprint`because another element ${overlayingEl} ` +
+              pprint`because another element ${overlayingEl} ` +
               `obscures it`;
           break;
       }
     }
 
     super(msg);
     this.status = "element click intercepted";
   }
--- a/testing/marionette/interaction.js
+++ b/testing/marionette/interaction.js
@@ -6,17 +6,16 @@
 
 const {utils: Cu} = Components;
 
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/atom.js");
 const {
   ElementClickInterceptedError,
   ElementNotInteractableError,
-  error,
   InvalidArgument,
   InvalidArgumentError,
   InvalidElementStateError,
   pprint,
 } = Cu.import("chrome://marionette/content/error.js", {});
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/event.js");
 
@@ -173,17 +172,17 @@ async function webdriverClickElement(el,
   // step 5
   // TODO(ato): wait for containerEl to be in view
 
   // step 6
   // if we cannot bring the container element into the viewport
   // there is no point in checking if it is pointer-interactable
   if (!element.isInView(containerEl)) {
     throw new ElementNotInteractableError(
-        error.pprint`Element ${el} could not be scrolled into view`);
+        pprint`Element ${el} could not be scrolled into view`);
   }
 
   // step 7
   let rects = containerEl.getClientRects();
   let clickPoint = element.getInViewCentrePoint(rects[0], win);
 
   if (element.isObscured(containerEl)) {
     throw new ElementClickInterceptedError(containerEl, clickPoint);
--- a/testing/marionette/test_error.js
+++ b/testing/marionette/test_error.js
@@ -1,15 +1,44 @@
 /* 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/. */
 
 const {utils: Cu} = Components;
 
-Cu.import("chrome://marionette/content/error.js");
+const {
+  ElementClickInterceptedError,
+  ElementNotAccessibleError,
+  ElementNotInteractableError,
+  error,
+  InsecureCertificateError,
+  InvalidArgumentError,
+  InvalidCookieDomainError,
+  InvalidElementStateError,
+  InvalidSelectorError,
+  InvalidSessionIDError,
+  JavaScriptError,
+  MoveTargetOutOfBoundsError,
+  NoAlertOpenError,
+  NoSuchElementError,
+  NoSuchFrameError,
+  NoSuchWindowError,
+  pprint,
+  ScriptTimeoutError,
+  SessionNotCreatedError,
+  stack,
+  StaleElementReferenceError,
+  TimeoutError,
+  UnableToSetCookieError,
+  UnexpectedAlertOpenError,
+  UnknownCommandError,
+  UnknownError,
+  UnsupportedOperationError,
+  WebDriverError,
+} = Cu.import("chrome://marionette/content/error.js", {});
 
 function notok(condition) {
   ok(!(condition));
 }
 
 add_test(function test_isError() {
   notok(error.isError(null));
   notok(error.isError([]));
@@ -85,38 +114,46 @@ add_test(function test_stringify() {
       error.stringify(new WebDriverError("foo")).split("\n")[0]);
   equal("InvalidArgumentError: foo",
       error.stringify(new InvalidArgumentError("foo")).split("\n")[0]);
 
   run_next_test();
 });
 
 add_test(function test_pprint() {
-  equal('[object Object] {"foo":"bar"}', error.pprint`${{foo: "bar"}}`);
+  equal('[object Object] {"foo":"bar"}', pprint`${{foo: "bar"}}`);
 
-  equal("[object Number] 42", error.pprint`${42}`);
-  equal("[object Boolean] true", error.pprint`${true}`);
-  equal("[object Undefined] undefined", error.pprint`${undefined}`);
-  equal("[object Null] null", error.pprint`${null}`);
+  equal("[object Number] 42", pprint`${42}`);
+  equal("[object Boolean] true", pprint`${true}`);
+  equal("[object Undefined] undefined", pprint`${undefined}`);
+  equal("[object Null] null", pprint`${null}`);
 
   let complexObj = {toJSON: () => "foo"};
-  equal('[object Object] "foo"', error.pprint`${complexObj}`);
+  equal('[object Object] "foo"', pprint`${complexObj}`);
 
   let cyclic = {};
   cyclic.me = cyclic;
-  equal("[object Object] <cyclic object value>", error.pprint`${cyclic}`);
+  equal("[object Object] <cyclic object value>", pprint`${cyclic}`);
 
   let el = {
     nodeType: 1,
     localName: "input",
     id: "foo",
     classList: {length: 1},
     className: "bar baz",
   };
-  equal('<input id="foo" class="bar baz">', error.pprint`${el}`);
+  equal('<input id="foo" class="bar baz">', pprint`${el}`);
+
+  run_next_test();
+});
+
+add_test(function test_stack() {
+  equal("string", typeof stack());
+  ok(stack().includes("test_stack"));
+  ok(!stack().includes("add_test"));
 
   run_next_test();
 });
 
 add_test(function test_toJSON() {
   let e0 = new WebDriverError();
   let e0s = e0.toJSON();
   equal(e0s.error, "webdriver error");
--- a/testing/mozharness/mozharness/mozilla/testing/talos.py
+++ b/testing/mozharness/mozharness/mozilla/testing/talos.py
@@ -212,29 +212,34 @@ class Talos(TestingMixin, MercurialScrip
             # now let's see if we added GeckoProfile specs in the commit message
             try:
                 junk, junk, opts = self.buildbot_config['sourcestamp']['changes'][-1]['comments'].partition('mozharness:')
             except IndexError:
                 # when we don't have comments on changes (bug 1255187)
                 opts = None
 
             if opts:
-              # In the case of a multi-line commit message, only examine
-              # the first line for mozharness options
-              opts = opts.split('\n')[0]
-              opts = re.sub(r'\w+:.*', '', opts).strip().split(' ')
-              if "--geckoProfile" in opts:
-                  # overwrite whatever was set here.
-                  self.gecko_profile = True
-              try:
-                  idx = opts.index('--geckoProfileInterval')
-                  if len(opts) > idx + 1:
-                      self.gecko_profile_interval = opts[idx + 1]
-              except ValueError:
-                  pass
+                # In the case of a multi-line commit message, only examine
+                # the first line for mozharness options
+                opts = opts.split('\n')[0]
+                opts = re.sub(r'\w+:.*', '', opts).strip().split(' ')
+                if "--geckoProfile" in opts:
+                    # overwrite whatever was set here.
+                    self.gecko_profile = True
+                try:
+                    idx = opts.index('--geckoProfileInterval')
+                    if len(opts) > idx + 1:
+                        self.gecko_profile_interval = opts[idx + 1]
+                except ValueError:
+                    pass
+            else:
+                # no opts, check for '--geckoProfile' in try message text directly
+                if self.try_message_has_flag('geckoProfile'):
+                    self.gecko_profile = True
+
         # finally, if gecko_profile is set, we add that to the talos options
         if self.gecko_profile:
             gecko_results.append('--geckoProfile')
             if self.gecko_profile_interval:
                 gecko_results.extend(
                     ['--geckoProfileInterval', str(self.gecko_profile_interval)]
                 )
         return gecko_results
--- a/testing/mozharness/mozharness/mozilla/testing/try_tools.py
+++ b/testing/mozharness/mozharness/mozilla/testing/try_tools.py
@@ -158,18 +158,24 @@ class TryToolsMixin(TransferMixin):
         msg_list = self._extract_try_args(message)
         args, _ = parser.parse_known_args(msg_list)
         return getattr(args, flag, False)
 
     def _is_try(self):
         repo_path = None
         if self.buildbot_config and 'properties' in self.buildbot_config:
             repo_path = self.buildbot_config['properties'].get('branch')
-        return (self.config.get('branch', repo_path) == 'try' or
-                'TRY_COMMIT_MSG' in os.environ)
+        get_branch = self.config.get('branch', repo_path)
+        if get_branch is not None:
+            on_try = ('try' in get_branch or 'Try' in get_branch)
+        elif os.environ is not None:
+            on_try = ('TRY_COMMIT_MSG' in os.environ)
+        else:
+            on_try = False
+        return on_try
 
     @PostScriptAction('download-and-extract')
     def set_extra_try_arguments(self, action, success=None):
         """Finds a commit message and parses it for extra arguments to pass to the test
         harness command line and test paths used to filter manifests.
 
         Extracting arguments from a commit message taken directly from the try_parser.
         """
--- a/toolkit/components/places/tests/PlacesTestUtils.jsm
+++ b/toolkit/components/places/tests/PlacesTestUtils.jsm
@@ -202,24 +202,25 @@ this.PlacesTestUtils = Object.freeze({
    * Asynchronously returns the required DB field for a specified page.
    * @param aURI
    *        nsIURI or address to look for.
    *
    * @return {Promise}
    * @resolves Returns the field value.
    * @rejects JavaScript exception.
    */
-  async fieldInDB(aURI, field) {
+  fieldInDB(aURI, field) {
     let url = aURI instanceof Ci.nsIURI ? new URL(aURI.spec) : new URL(aURI);
-    let db = await PlacesUtils.promiseDBConnection();
-    let rows = await db.executeCached(
-      `SELECT ${field} FROM moz_places
-       WHERE url_hash = hash(:url) AND url = :url`,
-      { url: url.href });
-    return rows[0].getResultByIndex(0);
+    return PlacesUtils.withConnectionWrapper("PlacesTestUtils.jsm: fieldInDb", async db => {
+      let rows = await db.executeCached(
+        `SELECT ${field} FROM moz_places
+        WHERE url_hash = hash(:url) AND url = :url`,
+        { url: url.href });
+      return rows[0].getResultByIndex(0);
+    });
   },
 
   /**
    * Marks all syncable bookmarks as synced by setting their sync statuses to
    * "NORMAL", resetting their change counters, and removing all tombstones.
    * Used by tests to avoid calling `PlacesSyncUtils.bookmarks.pullChanges`
    * and `PlacesSyncUtils.bookmarks.pushChanges`.
    *
--- a/tools/profiler/core/platform.cpp
+++ b/tools/profiler/core/platform.cpp
@@ -2635,16 +2635,56 @@ profiler_get_buffer_info_helper(uint32_t
   }
 
   *aCurrentPosition = ActivePS::Buffer(lock).mWritePos;
   *aEntries = ActivePS::Entries(lock);
   *aGeneration = ActivePS::Buffer(lock).mGeneration;
 }
 
 static void
+PollJSSamplingForCurrentThread()
+{
+  MOZ_RELEASE_ASSERT(CorePS::Exists());
+
+  PSAutoLock lock(gPSMutex);
+
+  ThreadInfo* info = TLSInfo::Info(lock);
+  if (!info) {
+    return;
+  }
+
+  info->PollJSSampling();
+}
+
+// When the profiler is started on a background thread, we can't synchronously
+// call PollJSSampling on the main thread's ThreadInfo. And the next regular
+// call to PollJSSampling on the main thread would only happen once the main
+// thread triggers a JS interrupt callback.
+// This means that all the JS execution between profiler_start() and the first
+// JS interrupt would happen with JS sampling disabled, and we wouldn't get any
+// JS function information for that period of time.
+// So in order to start JS sampling as soon as possible, we dispatch a runnable
+// to the main thread which manually calls PollJSSamplingForCurrentThread().
+// In some cases this runnable will lose the race with the next JS interrupt.
+// That's fine; PollJSSamplingForCurrentThread() is immune to redundant calls.
+static void
+TriggerPollJSSamplingOnMainThread()
+{
+  nsCOMPtr<nsIThread> mainThread;
+  nsresult rv = NS_GetMainThread(getter_AddRefs(mainThread));
+  if (NS_SUCCEEDED(rv) && mainThread) {
+    nsCOMPtr<nsIRunnable> task =
+      NS_NewRunnableFunction("TriggerPollJSSamplingOnMainThread", []() {
+        PollJSSamplingForCurrentThread();
+      });
+    SystemGroup::Dispatch(TaskCategory::Other, task.forget());
+  }
+}
+
+static void
 locked_profiler_start(PSLockRef aLock, int aEntries, double aInterval,
                       uint32_t aFeatures,
                       const char** aFilters, uint32_t aFilterCount)
 {
   if (LOG_TEST) {
     LOG("locked_profiler_start");
     LOG("- entries  = %d", aEntries);
     LOG("- interval = %.2f", aInterval);
@@ -2684,16 +2724,21 @@ locked_profiler_start(PSLockRef aLock, i
     if (ActivePS::ShouldProfileThread(aLock, info)) {
       info->StartProfiling();
       if (ActivePS::FeatureJS(aLock)) {
         info->StartJSSampling();
         if (info->ThreadId() == tid) {
           // We can manually poll the current thread so it starts sampling
           // immediately.
           info->PollJSSampling();
+        } else if (info->IsMainThread()) {
+          // Dispatch a runnable to the main thread to call PollJSSampling(),
+          // so that we don't have wait for the next JS interrupt callback in
+          // order to start profiling JS.
+          TriggerPollJSSamplingOnMainThread();
         }
       }
     }
   }
 
   // Dead ThreadInfos are deleted in profiler_stop(), and dead ThreadInfos
   // aren't saved when the profiler is inactive. Therefore the dead threads
   // vector should be empty here.
@@ -3071,27 +3116,17 @@ profiler_thread_is_sleeping()
   }
   return racyInfo->IsSleeping();
 }
 
 void
 profiler_js_interrupt_callback()
 {
   // This function runs on JS threads being sampled.
-
-  MOZ_RELEASE_ASSERT(CorePS::Exists());
-
-  PSAutoLock lock(gPSMutex);
-
-  ThreadInfo* info = TLSInfo::Info(lock);
-  if (!info) {
-    return;
-  }
-
-  info->PollJSSampling();
+  PollJSSamplingForCurrentThread();
 }
 
 double
 profiler_time()
 {
   MOZ_RELEASE_ASSERT(CorePS::Exists());
 
   TimeDuration delta = TimeStamp::Now() - CorePS::ProcessStartTime();