Merge autoland to mozilla-central a=merge
authorarthur.iakab <aiakab@mozilla.com>
Tue, 01 May 2018 00:49:30 +0300
changeset 469911 4b248e2264b84f8b357c7149136663dfe8d37866
parent 469882 2bbe101d3eda4a50dd3f670ec32cde81ce84aca9 (current diff)
parent 469910 8480289cd4393653c55b23b0099347751d01dcd8 (diff)
child 469912 7ef8450810693ab08e79ab0d4702de6f479e678c
push id9179
push userarchaeopteryx@coole-files.de
push dateThu, 03 May 2018 15:28:18 +0000
treeherdermozilla-beta@e6f9ade8bca7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge autoland to mozilla-central a=merge
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1084,17 +1084,18 @@ pref("security.sandbox.gmp.win32k-disabl
 // 3 -> "no global read/write access, read access permitted to
 //       $PROFILE/{extensions,chrome}"
 // This setting is read when the content process is started. On Mac the content
 // process is killed when all windows are closed, so a change will take effect
 // when the 1st window is opened.
 pref("security.sandbox.content.level", 3);
 #endif
 
-#if defined(NIGHTLY_BUILD) && defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+// Enable the Mac Flash sandbox on Nightly and Beta, not Release
+#if defined(EARLY_BETA_OR_EARLIER) && defined(XP_MACOSX) && defined(MOZ_SANDBOX)
 // Controls whether and how the Mac NPAPI Flash plugin process is sandboxed.
 // On Mac these levels are:
 // 0 - "no sandbox"
 // 1 - "write access to some Flash-specific directories and global
 //      read access triggered by file dialog activity"
 // 2 - "no global read access, read and write access to some
 //      Flash-specific directories"
 pref("dom.ipc.plugins.sandbox-level.flash", 1);
@@ -1530,19 +1531,17 @@ pref("browser.tabs.remote.desktopbehavio
 // performance while using the async tab switcher.
 //
 // This feature is enabled by default on Windows and Linux
 // on all channels.
 //
 // This feature is enabled on macOS only on the Nightly channel
 // until bug 1453080 is fixed.
 //
-#if defined(XP_LINUX) || defined(XP_WIN)
-pref("browser.tabs.remote.warmup.enabled", true);
-#elif defined(NIGHTLY_BUILD) && defined(XP_MACOSX)
+#if !defined(XP_MACOSX) || defined(NIGHTLY_BUILD)
 pref("browser.tabs.remote.warmup.enabled", true);
 #else
 pref("browser.tabs.remote.warmup.enabled", false);
 #endif
 
 pref("browser.tabs.remote.warmup.maxTabs", 3);
 pref("browser.tabs.remote.warmup.unloadDelayMs", 2000);
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -859,20 +859,31 @@
       </method>
 
       <method name="_updateNewTabVisibility">
         <body><![CDATA[
           // Helper functions to help deal with customize mode wrapping some items
           let wrap = n => n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
           let unwrap = n => n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
 
+          // Starting from the tabs element, find the next sibling that:
+          // - isn't hidden; and
+          // - isn't one of the titlebar placeholder elements; and
+          // - isn't the all-tabs button.
+          // If it's the new tab button, consider the new tab button adjacent to the tabs.
+          // If the new tab button is marked as adjacent and the tabstrip doesn't
+          // overflow, we'll display the 'new tab' button inline in the tabstrip.
+          // In all other cases, the separate new tab button is displayed in its
+          // customized location.
           let sib = this;
           do {
             sib = unwrap(wrap(sib).nextElementSibling);
-          } while (sib && sib.hidden);
+          } while (sib && (sib.hidden ||
+                           sib.getAttribute("skipintoolbarset") == "true" ||
+                           sib.id == "alltabs-button"));
 
           const kAttr = "hasadjacentnewtabbutton";
           if (sib && sib.id == "new-tab-button") {
             this.setAttribute(kAttr, "true");
           } else {
             this.removeAttribute(kAttr);
           }
         ]]></body>
--- a/browser/base/content/test/about/browser.ini
+++ b/browser/base/content/test/about/browser.ini
@@ -11,17 +11,16 @@ support-files =
 [browser_aboutCertError.js]
 support-files =
   dummy_page.html
 [browser_aboutHome_imitate.js]
 [browser_aboutHome_input.js]
 skip-if = true # Bug 1409054 to remove; previously skipped for intermittents, e.g., Bug 1399648
 [browser_aboutHome_search_POST.js]
 [browser_aboutHome_search_composing.js]
-skip-if = !debug && (os == "mac" || (os == "linux" && bits == 32)) # Bug 1400491, bug 1399648
 [browser_aboutHome_search_searchbar.js]
 [browser_aboutHome_search_suggestion.js]
 skip-if = os == "mac" || (os == "linux" && (!debug || bits == 64)) # Bug 1399648, bug 1402502
 [browser_aboutHome_search_telemetry.js]
 [browser_aboutHome_snippets.js]
 skip-if = true # Bug 1409054 to remove
 [browser_aboutHome_wrapsCorrectly.js]
 skip-if = true # Bug 1409054 to remove; previously skipped for intermittents, e.g., Bug 1395602
--- a/browser/base/content/test/about/browser_aboutHome_search_composing.js
+++ b/browser/base/content/test/about/browser_aboutHome_search_composing.js
@@ -16,28 +16,18 @@ add_task(async function() {
     await p;
 
     await ContentTask.spawn(browser, null, async function() {
       // Start composition and type "x"
       let input = content.document.querySelector(["#searchText", "#newtab-search-text"]);
       input.focus();
     });
 
-    // FYI: "compositionstart" will be dispatched automatically.
-    await BrowserTestUtils.synthesizeCompositionChange({
-      composition: {
-        string: "x",
-        clauses: [
-          { length: 1, attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE }
-        ]
-      },
-      caret: { start: 1, length: 0 }
-    }, browser);
-
-    await ContentTask.spawn(browser, null, async function() {
+    info("Setting up the mutation observer before synthesizing composition");
+    let mutationPromise = ContentTask.spawn(browser, null, async function() {
       let searchController = content.wrappedJSObject.gContentSearchController;
 
       // Wait for the search suggestions to become visible.
       let table = searchController._suggestionsList;
       let input = content.document.querySelector(["#searchText", "#newtab-search-text"]);
 
       await new Promise(resolve => {
         let observer = new content.MutationObserver(() => {
@@ -58,16 +48,30 @@ add_task(async function() {
 
       // ContentSearchUIController looks at the current selectedIndex when
       // performing a search. Synthesizing the mouse event on the suggestion
       // doesn't actually mouseover the suggestion and trigger it to be flagged
       // as selected, so we manually select it first.
       searchController.selectedIndex = 1;
     });
 
+    // FYI: "compositionstart" will be dispatched automatically.
+    await BrowserTestUtils.synthesizeCompositionChange({
+      composition: {
+        string: "x",
+        clauses: [
+          { length: 1, attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE }
+        ]
+      },
+      caret: { start: 1, length: 0 }
+    }, browser);
+
+    info("Waiting for search suggestion table unhidden");
+    await mutationPromise;
+
     // Click the second suggestion.
     let expectedURL = Services.search.currentEngine
       .getSubmission("xbar", null, "homepage").uri.spec;
     let loadPromise = waitForDocLoadAndStopIt(expectedURL);
     await BrowserTestUtils.synthesizeMouseAtCenter("#TEMPID", {
       button: 0
     }, browser);
     await loadPromise;
--- a/browser/base/content/test/performance/browser_window_resize.js
+++ b/browser/base/content/test/performance/browser_window_resize.js
@@ -8,37 +8,19 @@
  * is a whitelist that should slowly go away as we improve the performance of
  * the front-end. Instead of adding more reflows to the whitelist, you should
  * be modifying your code to avoid the reflow.
  *
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_REFLOWS = [
-  {
-    stack: [
-      "onOverflow@resource:///modules/CustomizableUI.jsm",
-    ],
-    maxCount: 48,
-  },
-
-  {
-    stack: [
-      "_moveItemsBackToTheirOrigin@resource:///modules/CustomizableUI.jsm",
-      "_onLazyResize@resource:///modules/CustomizableUI.jsm",
-    ],
-    maxCount: 5,
-  },
-
-  {
-    stack: [
-      "_onLazyResize@resource:///modules/CustomizableUI.jsm",
-    ],
-    maxCount: 4,
-  },
+   /**
+   * Nothing here! Please don't add anything new!
+   */
 ];
 
 const gToolbar = document.getElementById("PersonalToolbar");
 
 /**
  * Sets the visibility state on the Bookmarks Toolbar, and
  * waits for it to transition to fully visible.
  *
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -4403,28 +4403,46 @@ OverflowableToolbar.prototype = {
     doc.defaultView.updateEditUIVisibility();
     let contextMenuId = this._panel.getAttribute("context");
     if (contextMenuId) {
       let contextMenu = doc.getElementById(contextMenuId);
       gELS.removeSystemEventListener(contextMenu, "command", this, true);
     }
   },
 
-  onOverflow(aEvent) {
-    // The rangeParent check is here because of bug 1111986 and ensuring that
-    // overflow events from the bookmarks toolbar items or similar things that
-    // manage their own overflow don't trigger an overflow on the entire toolbar
-    if (!this._enabled ||
-        (aEvent && aEvent.target != this._toolbar.customizationTarget) ||
-        (aEvent && aEvent.rangeParent))
+  /**
+   * Avoid re-entrancy in the overflow handling by keeping track of invocations:
+   */
+  _lastOverflowCounter: 0,
+
+  /**
+   * Handle overflow in the toolbar by moving items to the overflow menu.
+   * @param {Event} aEvent
+   *        The overflow event that triggered handling overflow. May be omitted
+   *        in some cases (e.g. when we run this method after overflow handling
+   *        is re-enabled from customize mode, to ensure correct handling of
+   *        initial overflow).
+   */
+  async onOverflow(aEvent) {
+    if (!this._enabled)
       return;
 
     let child = this._target.lastChild;
 
-    while (child && this._target.scrollLeftMin != this._target.scrollLeftMax) {
+    let thisOverflowResponse = ++this._lastOverflowCounter;
+
+    let win = this._target.ownerGlobal;
+    let [scrollLeftMin, scrollLeftMax] = await win.promiseDocumentFlushed(() => {
+      return [this._target.scrollLeftMin, this._target.scrollLeftMax];
+    });
+    if (win.closed || this._lastOverflowCounter != thisOverflowResponse) {
+      return;
+    }
+
+    while (child && scrollLeftMin != scrollLeftMax) {
       let prevChild = child.previousSibling;
 
       if (child.getAttribute("overflows") != "false") {
         this._collapsed.set(child.id, this._target.clientWidth);
         child.setAttribute("overflowedItem", true);
         child.setAttribute("cui-anchorid", this._chevron.id);
         CustomizableUIInternal.ensureButtonContextMenu(child, this._toolbar, true);
         CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target);
@@ -4433,40 +4451,70 @@ OverflowableToolbar.prototype = {
         if (!this._addedListener) {
           CustomizableUI.addListener(this);
         }
         if (!CustomizableUI.isSpecialWidget(child.id)) {
           this._toolbar.setAttribute("overflowing", "true");
         }
       }
       child = prevChild;
-    }
-
-    let win = this._target.ownerGlobal;
+      [scrollLeftMin, scrollLeftMax] = await win.promiseDocumentFlushed(() => {
+        return [this._target.scrollLeftMin, this._target.scrollLeftMax];
+      });
+      // If the window has closed or if we re-enter because we were waiting
+      // for layout, stop.
+      if (win.closed || this._lastOverflowCounter != thisOverflowResponse) {
+        return;
+      }
+    }
+
     win.UpdateUrlbarSearchSplitterState();
+    // Reset the counter because we finished handling overflow.
+    this._lastOverflowCounter = 0;
   },
 
   _onResize(aEvent) {
+    // Ignore bubbled-up resize events.
+    if (aEvent.target != aEvent.target.ownerGlobal.top) {
+      return;
+    }
     if (!this._lazyResizeHandler) {
       this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this),
                                                  LAZY_RESIZE_INTERVAL_MS, 0);
     }
     this._lazyResizeHandler.arm();
   },
 
-  _moveItemsBackToTheirOrigin(shouldMoveAllItems) {
+  /**
+   * Try to move toolbar items back to the toolbar from the overflow menu.
+   * @param {boolean} shouldMoveAllItems
+   *        Whether we should move everything (e.g. because we're being disabled)
+   * @param {number} targetWidth
+   *        Optional; the width of the toolbar in which we can put things.
+   *        Some consumers pass this to avoid reflows.
+   *        While there are items in the list, this width won't change, and so
+   *        we can avoid flushing layout by providing it and/or caching it.
+   *        Note that if `shouldMoveAllItems` is true, we never need the width
+   *        anyway.
+   */
+  _moveItemsBackToTheirOrigin(shouldMoveAllItems, targetWidth) {
     let placements = gPlacements.get(this._toolbar.id);
+    let win = this._target.ownerGlobal;
     while (this._list.firstChild) {
       let child = this._list.firstChild;
       let minSize = this._collapsed.get(child.id);
 
-      if (!shouldMoveAllItems &&
-          minSize &&
-          this._target.clientWidth <= minSize) {
-        break;
+      if (!shouldMoveAllItems && minSize) {
+        if (!targetWidth) {
+          let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+          targetWidth = Math.floor(dwu.getBoundsWithoutFlushing(this._target).width);
+        }
+        if (targetWidth <= minSize) {
+          break;
+        }
       }
 
       this._collapsed.delete(child.id);
       let beforeNodeIndex = placements.indexOf(child.id) + 1;
       // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list,
       // we're inserting it at the end. This will mean first-in, first-out (more or less)
       // leading to as little change in order as possible.
       if (beforeNodeIndex == 0) {
@@ -4488,37 +4536,43 @@ OverflowableToolbar.prototype = {
         this._target.appendChild(child);
       }
       child.removeAttribute("cui-anchorid");
       child.removeAttribute("overflowedItem");
       CustomizableUIInternal.ensureButtonContextMenu(child, this._target);
       CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target);
     }
 
-    let win = this._target.ownerGlobal;
     win.UpdateUrlbarSearchSplitterState();
 
     let collapsedWidgetIds = Array.from(this._collapsed.keys());
     if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) {
       this._toolbar.removeAttribute("overflowing");
     }
     if (this._addedListener && !this._collapsed.size) {
       CustomizableUI.removeListener(this);
       this._addedListener = false;
     }
   },
 
-  _onLazyResize() {
+  async _onLazyResize() {
     if (!this._enabled)
       return;
 
-    if (this._target.scrollLeftMin != this._target.scrollLeftMax) {
+    let win = this._target.ownerGlobal;
+    let [min, max, targetWidth] = await win.promiseDocumentFlushed(() => {
+      return [this._target.scrollLeftMin, this._target.scrollLeftMax, this._target.clientWidth];
+    });
+    if (win.closed) {
+      return;
+    }
+    if (min != max) {
       this.onOverflow();
     } else {
-      this._moveItemsBackToTheirOrigin();
+      this._moveItemsBackToTheirOrigin(false, targetWidth);
     }
   },
 
   _disable() {
     this._enabled = false;
     this._moveItemsBackToTheirOrigin(true);
     if (this._lazyResizeHandler) {
       this._lazyResizeHandler.disarm();
@@ -4603,17 +4657,17 @@ OverflowableToolbar.prototype = {
     } else if (aNode.previousSibling) {
       // but if it still is, it must have changed places. Bookkeep:
       let prevId = aNode.previousSibling.id;
       let minSize = this._collapsed.get(prevId);
       this._collapsed.set(aNode.id, minSize);
     } else {
       // If it's now the first item in the overflow list,
       // maybe we can return it:
-      this._moveItemsBackToTheirOrigin();
+      this._moveItemsBackToTheirOrigin(false);
     }
   },
 
   findOverflowedInsertionPoints(aNode) {
     let newNodeCanOverflow = aNode.getAttribute("overflows") != "false";
     let areaId = this._toolbar.id;
     let placements = gPlacements.get(areaId);
     let nodeIndex = placements.indexOf(aNode.id);
--- a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
+++ b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
@@ -44,26 +44,29 @@ add_task(async function() {
 
   let hiddenPanelPromise = promiseOverflowHidden(window);
   EventUtils.synthesizeKey("KEY_Escape");
   await hiddenPanelPromise;
   CustomizableUI.reset();
 });
 
 // Ctrl+K should open the overflow panel and focus the search bar if the search bar is overflowed.
-add_task(async function() {
+add_task(async function check_shortcut_when_in_overflow() {
   this.originalWindowWidth = window.outerWidth;
   let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
   ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
   ok(CustomizableUI.inDefaultState, "Should start in default state.");
 
   Services.prefs.setBoolPref("browser.search.widget.inNavBar", true);
 
   window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForCondition(() => navbar.getAttribute("overflowing") == "true");
+  await waitForCondition(() => {
+    return navbar.getAttribute("overflowing") == "true" &&
+      !navbar.querySelector("#search-container");
+  });
   ok(!navbar.querySelector("#search-container"), "Search container should be overflowing");
 
   let shownPanelPromise = promiseOverflowShown(window);
   sendWebSearchKeyCommand();
   await shownPanelPromise;
 
   let chevron = document.getElementById("nav-bar-overflow-button");
   await waitForCondition(() => chevron.open);
--- a/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js
+++ b/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js
@@ -63,17 +63,17 @@ add_task(async function() {
   }
 
   for (let id of widgetIds) {
     document.getElementById(id).style.minWidth = "200px";
   }
 
   let originalWindowWidth = window.outerWidth;
   window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForCondition(() => navbar.hasAttribute("overflowing"));
+  await waitForCondition(() => navbar.hasAttribute("overflowing") && !navbar.querySelector("#" + widgetIds[0]));
 
   let testWidgetId = kTestWidgetPrefix + 3;
 
   CustomizableUI.destroyWidget(testWidgetId);
 
   let btn = createDummyXULButton(testWidgetId, "test");
   CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window);
 
@@ -112,17 +112,17 @@ add_task(async function() {
   }
 
   for (let id of widgetIds) {
     document.getElementById(id).style.minWidth = "200px";
   }
 
   let originalWindowWidth = window.outerWidth;
   window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForCondition(() => navbar.hasAttribute("overflowing"));
+  await waitForCondition(() => navbar.hasAttribute("overflowing") && !navbar.querySelector("#" + widgetIds[0]));
 
   let testWidgetId = kTestWidgetPrefix + 3;
 
   CustomizableUI.destroyWidget(kTestWidgetPrefix + 2);
   CustomizableUI.destroyWidget(testWidgetId);
 
   let btn = createDummyXULButton(testWidgetId, "test");
   CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window);
@@ -162,17 +162,17 @@ add_task(async function() {
   }
 
   for (let id of widgetIds) {
     document.getElementById(id).style.minWidth = "200px";
   }
 
   let originalWindowWidth = window.outerWidth;
   window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForCondition(() => navbar.hasAttribute("overflowing"));
+  await waitForCondition(() => navbar.hasAttribute("overflowing") && !navbar.querySelector("#" + widgetIds[0]));
 
   let testWidgetId = kTestWidgetPrefix + 3;
 
   CustomizableUI.destroyWidget(kTestWidgetPrefix + 2);
   CustomizableUI.destroyWidget(testWidgetId);
   CustomizableUI.destroyWidget(kTestWidgetPrefix + 4);
 
   let btn = createDummyXULButton(testWidgetId, "test");
@@ -221,17 +221,24 @@ add_task(async function() {
   }
 
   for (let id of widgetIds) {
     document.getElementById(id).style.minWidth = "200px";
   }
 
   let originalWindowWidth = window.outerWidth;
   window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForCondition(() => navbar.hasAttribute("overflowing"));
+  // Wait for all the widgets to overflow. We can't just wait for the
+  // `overflowing` attribute because we leave time for layout flushes
+  // inbetween, so it's possible for the timeout to run before the
+  // navbar has "settled"
+  await waitForCondition(() => {
+    return navbar.hasAttribute("overflowing") &&
+      navbar.customizationTarget.lastChild.getAttribute("overflows") == "false";
+  });
 
   // Find last widget that doesn't allow overflowing
   let nonOverflowing = navbar.customizationTarget.lastChild;
   is(nonOverflowing.getAttribute("overflows"), "false", "Last child is expected to not allow overflowing");
   isnot(nonOverflowing.getAttribute("skipintoolbarset"), "true", "Last child is expected to not be skipintoolbarset");
 
   let testWidgetId = kTestWidgetPrefix + 10;
   CustomizableUI.destroyWidget(testWidgetId);
@@ -282,17 +289,17 @@ add_task(async function() {
   }
 
   let toolbarNode = createOverflowableToolbarWithPlacements(kToolbarName, widgetIds);
   assertAreaPlacements(kToolbarName, widgetIds);
   ok(!toolbarNode.hasAttribute("overflowing"), "Toolbar shouldn't overflow to start with.");
 
   let originalWindowWidth = window.outerWidth;
   window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForCondition(() => toolbarNode.hasAttribute("overflowing"));
+  await waitForCondition(() => toolbarNode.hasAttribute("overflowing") && !toolbarNode.querySelector("#" + widgetIds[1]));
   ok(toolbarNode.hasAttribute("overflowing"), "Should have an overflowing toolbar.");
 
   let btnId = kTestWidgetPrefix + missingId;
   let btn = createDummyXULButton(btnId, "test");
   CustomizableUI.ensureWidgetPlacedInWindow(btnId, window);
 
   is(btn.parentNode.id, kToolbarName + "-overflow-list", "New XUL widget should be placed inside new toolbar's overflow");
   is(btn.previousSibling.id, kTestWidgetPrefix + 1,
--- a/browser/components/customizableui/test/browser_editcontrols_update.js
+++ b/browser/components/customizableui/test/browser_editcontrols_update.js
@@ -86,17 +86,17 @@ add_task(async function test_panelui_ope
   await overridePromise;
   checkState(true, "Update when edit-controls is on panel, hidden and selection changed");
 });
 
 // Test updating when the edit-controls are moved to the toolbar.
 add_task(async function test_panelui_customize_to_toolbar() {
   await startCustomizing();
   let navbar = document.getElementById("nav-bar");
-  simulateItemDrag(document.getElementById("edit-controls"), navbar.customizationTarget);
+  simulateItemDrag(document.getElementById("edit-controls"), navbar.customizationTarget, "end");
   await endCustomizing();
 
   // updateEditUIVisibility should be called when customization ends but isn't. See bug 1359790.
   updateEditUIVisibility();
 
   // The URL bar may have been focused to begin with, which means
   // that subsequent calls to focus it won't result in command
   // updates, so we'll make sure to blur it.
@@ -120,17 +120,18 @@ add_task(async function test_panelui_cus
   registerCleanupFunction(async function() {
     kOverflowPanel.removeAttribute("animate");
     window.resizeTo(originalWidth, window.outerHeight);
     await waitForCondition(() => !navbar.hasAttribute("overflowing"));
     CustomizableUI.reset();
   });
 
   window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForCondition(() => navbar.hasAttribute("overflowing"));
+  await waitForCondition(() =>
+    navbar.hasAttribute("overflowing") && !navbar.querySelector("edit-controls"));
 
   // Mac will update the enabled state even when the buttons are overflowing,
   // so main menubar shortcuts will work properly.
   overridePromise = expectCommandUpdate(isMac ? 1 : 0);
   gURLBar.select();
   await overridePromise;
   checkState(true, "Update when edit-controls is on overflow panel, hidden and selection changed");
 
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -1165,21 +1165,25 @@ PlacesToolbar.prototype = {
         // the overflow status of the toolbar.
         if (aEvent.target == aEvent.currentTarget) {
           this.updateNodesVisibility();
         }
         break;
       case "overflow":
         if (!this._isOverflowStateEventRelevant(aEvent))
           return;
+        // Avoid triggering overflow in containers if possible
+        aEvent.stopPropagation();
         this._onOverflow();
         break;
       case "underflow":
         if (!this._isOverflowStateEventRelevant(aEvent))
           return;
+        // Avoid triggering underflow in containers if possible
+        aEvent.stopPropagation();
         this._onUnderflow();
         break;
       case "TabOpen":
       case "TabClose":
         this.updateNodesVisibility();
         break;
       case "dragstart":
         this._onDragStart(aEvent);
--- a/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js
@@ -1,17 +1,17 @@
 "use strict";
 
 const testPage = "http://example.net/browser/browser/components/resistFingerprinting/test/browser/file_dummy.html";
 
 add_task(async function() {
   let tab1, tab1Zoom, tab2, tab2Zoom, tab3, tab3Zoom;
 
   tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage);
-  FullZoom.enlarge();
+  await FullZoom.enlarge();
   tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser);
 
   tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage);
   tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser);
 
   is(tab2Zoom, tab1Zoom, "privacy.resistFingerprinting is false, site-specific zoom level should be enabled");
 
   await SpecialPowers.pushPrefEnv({
--- a/devtools/.eslintrc.js
+++ b/devtools/.eslintrc.js
@@ -11,19 +11,17 @@ module.exports = {
     "module": true,
     "reportError": true,
     "require": true,
   },
   "overrides": [{
     // XXX Bug 1230193. We're still working on enabling no-undef for these test
     // directories.
     "files": [
-      "client/scratchpad/**",
       "server/tests/mochitest/**",
-      "shared/tests/unit/**",
     ],
     "rules": {
       "no-undef": "off",
     }
   }, {
     "files": [
       "client/framework/**",
     ],
--- a/devtools/client/jsonview/converter-child.js
+++ b/devtools/client/jsonview/converter-child.js
@@ -74,16 +74,18 @@ Converter.prototype = {
   },
 
   onStartRequest: function(request, context) {
     // Set the content type to HTML in order to parse the doctype, styles
     // and scripts. The JSON will be manually inserted as text.
     request.QueryInterface(Ci.nsIChannel);
     request.contentType = "text/html";
 
+    let headers = getHttpHeaders(request);
+
     // Enforce strict CSP:
     try {
       request.QueryInterface(Ci.nsIHttpChannel);
       request.setResponseHeader("Content-Security-Policy",
         "default-src 'none' ; script-src resource:; ", false);
     } catch (ex) {
       // If this is not an HTTP channel we can't and won't do anything.
     }
@@ -100,17 +102,17 @@ Converter.prototype = {
     // origin with (other) content.
     request.loadInfo.resetPrincipalToInheritToNullPrincipal();
 
     // Start the request.
     this.listener.onStartRequest(request, context);
 
     // Initialize stuff.
     let win = NetworkHelper.getWindowForRequest(request);
-    this.data = exportData(win, request);
+    this.data = exportData(win, headers);
     insertJsonData(win, this.data.json);
     win.addEventListener("contentMessage", onContentMessage, false, true);
     keepThemeUpdated(win);
 
     // Send the initial HTML code.
     let buffer = new TextEncoder().encode(initialHTML(win.document)).buffer;
     let stream = new BufferStream(buffer, 0, buffer.byteLength);
     this.listener.onDataAvailable(request, context, stream, 0, stream.available());
@@ -159,18 +161,40 @@ function fixSave(request) {
     originalType = match[1];
   } else {
     originalType = "application/json";
   }
   request.QueryInterface(Ci.nsIWritablePropertyBag);
   request.setProperty("contentType", originalType);
 }
 
+function getHttpHeaders(request) {
+  let headers = {
+    response: [],
+    request: []
+  };
+  // The request doesn't have to be always nsIHttpChannel
+  // (e.g. in case of data: URLs)
+  if (request instanceof Ci.nsIHttpChannel) {
+    request.visitResponseHeaders({
+      visitHeader: function(name, value) {
+        headers.response.push({name: name, value: value});
+      }
+    });
+    request.visitRequestHeaders({
+      visitHeader: function(name, value) {
+        headers.request.push({name: name, value: value});
+      }
+    });
+  }
+  return headers;
+}
+
 // Exports variables that will be accessed by the non-privileged scripts.
-function exportData(win, request) {
+function exportData(win, headers) {
   let data = Cu.createObjectIn(win, {
     defineAs: "JSONView"
   });
 
   data.debugJsModules = debugJsModules;
 
   data.json = new win.Text();
 
@@ -183,34 +207,16 @@ function exportData(win, request) {
       } catch (err) {
         console.error(err);
         return undefined;
       }
     }
   };
   data.Locale = Cu.cloneInto(Locale, win, {cloneFunctions: true});
 
-  let headers = {
-    response: [],
-    request: []
-  };
-  // The request doesn't have to be always nsIHttpChannel
-  // (e.g. in case of data: URLs)
-  if (request instanceof Ci.nsIHttpChannel) {
-    request.visitResponseHeaders({
-      visitHeader: function(name, value) {
-        headers.response.push({name: name, value: value});
-      }
-    });
-    request.visitRequestHeaders({
-      visitHeader: function(name, value) {
-        headers.request.push({name: name, value: value});
-      }
-    });
-  }
   data.headers = Cu.cloneInto(headers, win);
 
   return data;
 }
 
 // Builds an HTML string that will be used to load stylesheets and scripts.
 function initialHTML(doc) {
   // Creates an element with the specified type, attributes and children.
--- a/devtools/client/jsonview/test/browser_jsonview_csp_json.js
+++ b/devtools/client/jsonview/test/browser_jsonview_csp_json.js
@@ -5,13 +5,31 @@
 
 "use strict";
 
 const TEST_JSON_URL = URL_ROOT + "csp_json.json";
 
 add_task(async function() {
   info("Test CSP JSON started");
 
-  await addJsonViewTab(TEST_JSON_URL);
+  let tab = await addJsonViewTab(TEST_JSON_URL);
 
   let count = await getElementCount(".jsonPanelBox .treeTable .treeRow");
   is(count, 1, "There must be one row");
+
+  // The JSON Viewer alters the CSP, but the displayed header should be the original one
+  await selectJsonViewContentTab("headers");
+  await ContentTask.spawn(tab.linkedBrowser, null, async function() {
+    let responseHeaders = content.document.querySelector(".netHeadersGroup");
+    let names = responseHeaders.querySelectorAll(".netInfoParamName");
+    let found = false;
+    for (let name of names) {
+      if (name.textContent.toLowerCase() == "content-security-policy") {
+        ok(!found, "The CSP header only appears once");
+        found = true;
+        let value = name.nextElementSibling.textContent;
+        let expected = "default-src 'none'; base-uri 'none';";
+        is(value, expected, "The CSP value has not been altered");
+      }
+    }
+    ok(found, "The CSP header is present");
+  });
 });
--- a/devtools/client/netmonitor/src/assets/styles/Toolbar.css
+++ b/devtools/client/netmonitor/src/assets/styles/Toolbar.css
@@ -88,8 +88,21 @@
 }
 
 .devtools-checkbox-label {
   margin-inline-start: 10px;
   margin-inline-end: 3px;
   white-space: nowrap;
   margin-top: 1px;
 }
+
+/* Search box */
+
+.devtools-searchbox {
+  height: 100%;
+}
+
+.devtools-plaininput:focus {
+  border: 1px solid var(--blue-50);
+  margin-bottom: 0;
+  margin-top: 0;
+  box-shadow: none;
+}
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -7,16 +7,21 @@
  * Original version history can be found here:
  * https://github.com/mozilla/workspace
  *
  * Copied and relicensed from the Public Domain.
  * See bug 653934 for details.
  * https://bugzilla.mozilla.org/show_bug.cgi?id=653934
  */
 
+// Via scratchpad.xul
+/* import-globals-from ../../../toolkit/content/globalOverlay.js */
+// Via editMenuCommands.inc.xul
+/* import-globals-from ../../../toolkit/content/editMenuOverlay.js */
+
 "use strict";
 
 const SCRATCHPAD_CONTEXT_CONTENT = 1;
 const SCRATCHPAD_CONTEXT_BROWSER = 2;
 const BUTTON_POSITION_SAVE = 0;
 const BUTTON_POSITION_CANCEL = 1;
 const BUTTON_POSITION_DONT_SAVE = 2;
 const BUTTON_POSITION_REVERT = 0;
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -456,21 +456,22 @@ class ShapesHighlighter extends AutoRefr
       const win =  this.win;
       const nodeWin = this.currentNode.ownerGlobal;
       // Get bounding box of iframe document relative to global document.
       const { bounds } = nodeWin.document.getBoxQuads({ relativeTo: win.document })[0];
       xOffset = bounds.left - nodeWin.scrollX + win.scrollX;
       yOffset = bounds.top - nodeWin.scrollY + win.scrollY;
     }
 
-    const { pageXOffset, pageYOffset, innerWidth, innerHeight } = this.win;
+    const { pageXOffset, pageYOffset } = this.win;
+    const { clientHeight, clientWidth } = this.win.document.documentElement;
     const left = pageXOffset + padding - xOffset;
-    const right = innerWidth + pageXOffset - padding - xOffset;
+    const right = clientWidth + pageXOffset - padding - xOffset;
     const top = pageYOffset + padding - yOffset;
-    const bottom = innerHeight + pageYOffset - padding - yOffset;
+    const bottom = clientHeight + pageYOffset - padding - yOffset;
     this.viewport = { left, right, top, bottom, padding };
   }
 
   handleEvent(event, id) {
     // No event handling if the highlighter is hidden
     if (this.areShapesHidden()) {
       return;
     }
--- a/devtools/shared/tests/unit/head_devtools.js
+++ b/devtools/shared/tests/unit/head_devtools.js
@@ -16,16 +16,19 @@ registerCleanupFunction(() => {
 
 // Register a console listener, so console messages don't just disappear
 // into the ether.
 
 // If for whatever reason the test needs to post console errors that aren't
 // failures, set this to true.
 var ALLOW_CONSOLE_ERRORS = false;
 
+// XXX This listener is broken, see bug 1456634, for now turn off no-undef here,
+// this needs turning back on!
+/* eslint-disable no-undef */
 var listener = {
   observe: function(message) {
     let string;
     try {
       message.QueryInterface(Ci.nsIScriptError);
       dump(message.sourceName + ":" + message.lineNumber + ": " +
            scriptErrorFlagsToKind(message.flags) + ": " +
            message.errorMessage + "\n");
@@ -45,10 +48,11 @@ var listener = {
       DebuggerServer.xpcInspector.exitNestedEventLoop();
     }
 
     if (!ALLOW_CONSOLE_ERRORS) {
       do_throw("head_devtools.js got console message: " + string + "\n");
     }
   }
 };
+/* eslint-enable no-undef */
 
 Services.console.registerListener(listener);
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -2674,18 +2674,18 @@ ContentParent::RecvClipboardHasType(nsTA
 
 mozilla::ipc::IPCResult
 ContentParent::RecvPlaySound(const URIParams& aURI)
 {
   nsCOMPtr<nsIURI> soundURI = DeserializeURI(aURI);
   bool isChrome = false;
   // If the check here fails, it can only mean that this message was spoofed.
   if (!soundURI || NS_FAILED(soundURI->SchemeIs("chrome", &isChrome)) || !isChrome) {
-    KillHard("PlaySound only accepts a valid chrome URI.");
-    return IPC_OK();
+    // PlaySound only accepts a valid chrome URI.
+    return IPC_FAIL_NO_REASON(this);
   }
   nsCOMPtr<nsIURL> soundURL(do_QueryInterface(soundURI));
   if (!soundURL) {
     return IPC_OK();
   }
 
   nsresult rv;
   nsCOMPtr<nsISound> sound(do_GetService(NS_SOUND_CID, &rv));
@@ -5580,17 +5580,16 @@ ContentParent::RecvFileCreationRequest(c
                                        const int64_t& aLastModified,
                                        const bool& aExistenceCheck,
                                        const bool& aIsFromNsIFile)
 {
   // We allow the creation of File via this IPC call only for the 'file' process
   // or for testing.
   if (!mRemoteType.EqualsLiteral(FILE_REMOTE_TYPE) &&
       !Preferences::GetBool("dom.file.createInChild", false)) {
-    KillHard("FileCreationRequest is not supported.");
     return IPC_FAIL_NO_REASON(this);
   }
 
   RefPtr<BlobImpl> blobImpl;
   nsresult rv =
     FileCreatorHelper::CreateBlobImplForIPC(aFullPath, aType, aName,
                                             aLastModifiedPassed,
                                             aLastModified, aExistenceCheck,
--- a/dom/media/webaudio/AnalyserNode.cpp
+++ b/dom/media/webaudio/AnalyserNode.cpp
@@ -115,22 +115,19 @@ AnalyserNode::Create(AudioContext& aAudi
     return nullptr;
   }
 
   analyserNode->SetFftSize(aOptions.mFftSize, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
-  analyserNode->SetMinDecibels(aOptions.mMinDecibels, aRv);
-  if (NS_WARN_IF(aRv.Failed())) {
-    return nullptr;
-  }
-
-  analyserNode->SetMaxDecibels(aOptions.mMaxDecibels, aRv);
+  analyserNode->SetMinAndMaxDecibels(aOptions.mMinDecibels,
+                                     aOptions.mMaxDecibels,
+                                     aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   analyserNode->SetSmoothingTimeConstant(aOptions.mSmoothingTimeConstant, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
@@ -215,16 +212,27 @@ AnalyserNode::SetMaxDecibels(double aVal
   if (aValue <= mMinDecibels) {
     aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
     return;
   }
   mMaxDecibels = aValue;
 }
 
 void
+AnalyserNode::SetMinAndMaxDecibels(double aMinValue, double aMaxValue, ErrorResult& aRv)
+{
+  if (aMinValue >= aMaxValue) {
+    aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+    return;
+  }
+  mMinDecibels = aMinValue;
+  mMaxDecibels = aMaxValue;
+}
+
+void
 AnalyserNode::SetSmoothingTimeConstant(double aValue, ErrorResult& aRv)
 {
   if (aValue < 0 || aValue > 1) {
     aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
     return;
   }
   mSmoothingTimeConstant = aValue;
 }
--- a/dom/media/webaudio/AnalyserNode.h
+++ b/dom/media/webaudio/AnalyserNode.h
@@ -67,16 +67,18 @@ public:
   virtual const char* NodeType() const override
   {
     return "AnalyserNode";
   }
 
   virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
   virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
 
+  void SetMinAndMaxDecibels(double aMinValue, double aMaxValue, ErrorResult& aRv);
+
 private:
   ~AnalyserNode() = default;
 
   friend class AnalyserNodeEngine;
   void AppendChunk(const AudioChunk& aChunk);
   bool AllocateBuffer();
   bool FFTAnalysis();
   void ApplyBlackmanWindow(float* aBuffer, uint32_t aSize);
--- a/dom/media/webaudio/AudioBuffer.cpp
+++ b/dom/media/webaudio/AudioBuffer.cpp
@@ -166,17 +166,17 @@ AudioBuffer::AudioBuffer(nsPIDOMWindowIn
 {
   // Note that a buffer with zero channels is permitted here for the sake of
   // AudioProcessingEvent, where channel counts must match parameters passed
   // to createScriptProcessor(), one of which may be zero.
   if (aSampleRate < WebAudioUtils::MinSampleRate ||
       aSampleRate > WebAudioUtils::MaxSampleRate ||
       aNumberOfChannels > WebAudioUtils::MaxChannelCount ||
       !aLength || aLength > INT32_MAX) {
-    aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return;
   }
 
   mSharedChannels.mDuration = aLength;
   mJSChannels.SetLength(aNumberOfChannels);
   mozilla::HoldJSObjects(this);
   AudioBufferMemoryTracker::RegisterAudioBuffer(this);
 }
@@ -189,17 +189,17 @@ AudioBuffer::~AudioBuffer()
 }
 
 /* static */ already_AddRefed<AudioBuffer>
 AudioBuffer::Constructor(const GlobalObject& aGlobal,
                          const AudioBufferOptions& aOptions,
                          ErrorResult& aRv)
 {
   if (!aOptions.mNumberOfChannels) {
-    aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return nullptr;
   }
 
   nsCOMPtr<nsPIDOMWindowInner> window =
     do_QueryInterface(aGlobal.GetAsSupports());
 
   return Create(window, aOptions.mNumberOfChannels, aOptions.mLength,
                 aOptions.mSampleRate, aRv);
@@ -326,17 +326,17 @@ AudioBuffer::CopyFromChannel(const Float
 {
   aDestination.ComputeLengthAndData();
 
   uint32_t length = aDestination.Length();
   CheckedInt<uint32_t> end = aStartInChannel;
   end += length;
   if (aChannelNumber >= NumberOfChannels() ||
       !end.isValid() || end.value() > Length()) {
-    aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return;
   }
 
   JS::AutoCheckCannotGC nogc;
   JSObject* channelArray = mJSChannels[aChannelNumber];
   if (channelArray) {
     if (JS_GetTypedArrayLength(channelArray) != Length()) {
       // The array's buffer was detached.
@@ -370,17 +370,17 @@ AudioBuffer::CopyToChannel(JSContext* aJ
 {
   aSource.ComputeLengthAndData();
 
   uint32_t length = aSource.Length();
   CheckedInt<uint32_t> end = aStartInChannel;
   end += length;
   if (aChannelNumber >= NumberOfChannels() ||
       !end.isValid() || end.value() > Length()) {
-    aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return;
   }
 
   if (!RestoreJSChannelData(aJSContext)) {
     aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
 
@@ -401,17 +401,17 @@ AudioBuffer::CopyToChannel(JSContext* aJ
 }
 
 void
 AudioBuffer::GetChannelData(JSContext* aJSContext, uint32_t aChannel,
                             JS::MutableHandle<JSObject*> aRetval,
                             ErrorResult& aRv)
 {
   if (aChannel >= NumberOfChannels()) {
-    aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return;
   }
 
   if (!RestoreJSChannelData(aJSContext)) {
     aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
 
--- a/dom/media/webaudio/test/test_AudioBuffer.html
+++ b/dom/media/webaudio/test/test_AudioBuffer.html
@@ -76,27 +76,27 @@ addLoadEvent(function() {
   }, DOMException.INDEX_SIZE_ERR);
 
   expectException(function() {
     buffer.copyToChannel(newData, 0, 100);
   }, DOMException.INDEX_SIZE_ERR);
 
   expectException(function() {
     context.createBuffer(2, 2048, 7999);
-  }, DOMException.INDEX_SIZE_ERR);
+  }, DOMException.NOT_SUPPORTED_ERR);
   expectException(function() {
     context.createBuffer(2, 2048, 192001);
-  }, DOMException.INDEX_SIZE_ERR);
+  }, DOMException.NOT_SUPPORTED_ERR);
   context.createBuffer(2, 2048, 8000);  // no exception
   context.createBuffer(2, 2048, 192000); // no exception
   context.createBuffer(32, 2048, 48000); // no exception
   // Null length
   expectException(function() {
     context.createBuffer(2, 0, 48000);
-  }, DOMException.INDEX_SIZE_ERR);
+  }, DOMException.NOT_SUPPORTED_ERR);
   // Null number of channels
   expectException(function() {
     context.createBuffer(0, 2048, 48000);
   }, DOMException.INDEX_SIZE_ERR);
   SimpleTest.finish();
 });
 
 </script>
--- a/dom/webidl/AnalyserNode.webidl
+++ b/dom/webidl/AnalyserNode.webidl
@@ -7,19 +7,19 @@
  * https://webaudio.github.io/web-audio-api/
  *
  * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 dictionary AnalyserOptions : AudioNodeOptions {
              unsigned long fftSize = 2048;
-             float         maxDecibels = -30;
-             float         minDecibels = -100;
-             float         smoothingTimeConstant = 0.8;
+             double        maxDecibels = -30;
+             double        minDecibels = -100;
+             double        smoothingTimeConstant = 0.8;
 };
 
 [Pref="dom.webaudio.enabled",
  Constructor(BaseAudioContext context, optional AnalyserOptions options)]
 interface AnalyserNode : AudioNode {
 
     // Real-time frequency-domain data
     void getFloatFrequencyData(Float32Array array);
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -2097,47 +2097,58 @@ void
 CompositorBridgeParent::NotifyPipelineRemoved(const wr::PipelineId& aPipelineId)
 {
   if (mAsyncImageManager) {
     mAsyncImageManager->PipelineRemoved(aPipelineId);
   }
 }
 
 void
-CompositorBridgeParent::NotifyDidCompositeToPipeline(const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch, TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd)
+CompositorBridgeParent::NotifyPipelineRendered(const wr::PipelineId& aPipelineId,
+                                               const wr::Epoch& aEpoch,
+                                               TimeStamp& aCompositeStart,
+                                               TimeStamp& aCompositeEnd)
 {
-  if (!mWrBridge || !mAsyncImageManager) {
-    return;
+  if (mAsyncImageManager) {
+    mAsyncImageManager->PipelineRendered(aPipelineId, aEpoch);
   }
-  mAsyncImageManager->PipelineRendered(aPipelineId, aEpoch);
 
-  if (mPaused) {
+  if (!mWrBridge) {
     return;
   }
 
   if (mWrBridge->PipelineId() == aPipelineId) {
-    TransactionId transactionId = mWrBridge->FlushTransactionIdsForEpoch(aEpoch, aCompositeEnd);
-    Unused << SendDidComposite(LayersId{0}, transactionId, aCompositeStart, aCompositeEnd);
+    mWrBridge->RemoveEpochDataPriorTo(aEpoch);
+
+    if (!mPaused) {
+      TransactionId transactionId = mWrBridge->FlushTransactionIdsForEpoch(aEpoch, aCompositeEnd);
+      Unused << SendDidComposite(LayersId{0}, transactionId, aCompositeStart, aCompositeEnd);
 
-    nsTArray<ImageCompositeNotificationInfo> notifications;
-    mWrBridge->ExtractImageCompositeNotifications(&notifications);
-    if (!notifications.IsEmpty()) {
-      Unused << ImageBridgeParent::NotifyImageComposites(notifications);
+      nsTArray<ImageCompositeNotificationInfo> notifications;
+      mWrBridge->ExtractImageCompositeNotifications(&notifications);
+      if (!notifications.IsEmpty()) {
+        Unused << ImageBridgeParent::NotifyImageComposites(notifications);
+      }
     }
     return;
   }
 
   MonitorAutoLock lock(*sIndirectLayerTreesLock);
   ForEachIndirectLayerTree([&] (LayerTreeState* lts, const LayersId& aLayersId) -> void {
     if (lts->mCrossProcessParent &&
         lts->mWrBridge &&
         lts->mWrBridge->PipelineId() == aPipelineId) {
-      CrossProcessCompositorBridgeParent* cpcp = lts->mCrossProcessParent;
-      TransactionId transactionId = lts->mWrBridge->FlushTransactionIdsForEpoch(aEpoch, aCompositeEnd);
-      Unused << cpcp->SendDidComposite(aLayersId, transactionId, aCompositeStart, aCompositeEnd);
+
+      lts->mWrBridge->RemoveEpochDataPriorTo(aEpoch);
+
+      if (!mPaused) {
+        CrossProcessCompositorBridgeParent* cpcp = lts->mCrossProcessParent;
+        TransactionId transactionId = lts->mWrBridge->FlushTransactionIdsForEpoch(aEpoch, aCompositeEnd);
+        Unused << cpcp->SendDidComposite(aLayersId, transactionId, aCompositeStart, aCompositeEnd);
+      }
     }
   });
 }
 
 void
 CompositorBridgeParent::NotifyDidComposite(TransactionId aTransactionId, TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd)
 {
   Unused << SendDidComposite(LayersId{0}, aTransactionId, aCompositeStart, aCompositeEnd);
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -135,20 +135,20 @@ public:
   mozilla::ipc::IPCResult RecvSyncWithCompositor() override { return IPC_OK(); }
 
   mozilla::ipc::IPCResult Recv__delete__() override { return IPC_OK(); }
 
   virtual void ObserveLayerUpdate(LayersId aLayersId, uint64_t aEpoch, bool aActive) = 0;
 
   virtual void DidComposite(LayersId aId, TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd) {}
 
-  virtual void NotifyDidCompositeToPipeline(const wr::PipelineId& aPipelineId,
-                                            const wr::Epoch& aEpoch,
-                                            TimeStamp& aCompositeStart,
-                                            TimeStamp& aCompositeEnd) { MOZ_ASSERT_UNREACHABLE("WebRender only"); }
+  virtual void NotifyPipelineRendered(const wr::PipelineId& aPipelineId,
+                                      const wr::Epoch& aEpoch,
+                                      TimeStamp& aCompositeStart,
+                                      TimeStamp& aCompositeEnd) { MOZ_ASSERT_UNREACHABLE("WebRender only"); }
   virtual void NotifyPipelineRemoved(const wr::PipelineId& aPipelineId) { MOZ_ASSERT_UNREACHABLE("WebRender only"); }
 
   // HostIPCAllocator
   base::ProcessId GetChildProcessId() override;
   void NotifyNotUsed(PTextureParent* aTexture, uint64_t aTransactionId) override;
   void SendAsyncMessage(const InfallibleTArray<AsyncParentMessageData>& aMessage) override;
 
   // ShmemAllocator
@@ -575,20 +575,20 @@ protected:
    * Return true if current state allows compositing, that is
    * finishing a layers transaction.
    */
   bool CanComposite();
 
   using CompositorBridgeParentBase::DidComposite;
   void DidComposite(TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd);
 
-  void NotifyDidCompositeToPipeline(const wr::PipelineId& aPipelineId,
-                                    const wr::Epoch& aEpoch,
-                                    TimeStamp& aCompositeStart,
-                                    TimeStamp& aCompositeEnd) override;
+  void NotifyPipelineRendered(const wr::PipelineId& aPipelineId,
+                              const wr::Epoch& aEpoch,
+                              TimeStamp& aCompositeStart,
+                              TimeStamp& aCompositeEnd) override;
   void NotifyPipelineRemoved(const wr::PipelineId& aPipelineId) override;
 
   void NotifyDidComposite(TransactionId aTransactionId, TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd);
 
   // The indirect layer tree lock must be held before calling this function.
   // Callback should take (LayerTreeState* aState, const LayersId& aLayersId)
   template <typename Lambda>
   inline void ForEachIndirectLayerTree(const Lambda& aCallback);
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -473,25 +473,37 @@ WebRenderBridgeParent::RecvUpdateResourc
 
 mozilla::ipc::IPCResult
 WebRenderBridgeParent::RecvDeleteCompositorAnimations(InfallibleTArray<uint64_t>&& aIds)
 {
   if (mDestroyed) {
     return IPC_OK();
   }
 
-  for (uint32_t i = 0; i < aIds.Length(); i++) {
-    if (mActiveAnimations.erase(aIds[i]) > 0) {
-      mAnimStorage->ClearById(aIds[i]);
-    } else {
-      NS_ERROR("Tried to delete invalid animation");
+  // Once mWrEpoch has been rendered, we can delete these compositor animations
+  mCompositorAnimationsToDelete.push(CompositorAnimationIdsForEpoch(mWrEpoch, Move(aIds)));
+  return IPC_OK();
+}
+
+void
+WebRenderBridgeParent::RemoveEpochDataPriorTo(const wr::Epoch& aRenderedEpoch)
+{
+  while (!mCompositorAnimationsToDelete.empty()) {
+    if (mCompositorAnimationsToDelete.front().mEpoch.mHandle > aRenderedEpoch.mHandle) {
+      break;
     }
+    for (uint64_t id : mCompositorAnimationsToDelete.front().mIds) {
+      if (mActiveAnimations.erase(id) > 0) {
+        mAnimStorage->ClearById(id);
+      } else {
+        NS_ERROR("Tried to delete invalid animation");
+      }
+    }
+    mCompositorAnimationsToDelete.pop();
   }
-
-  return IPC_OK();
 }
 
 CompositorBridgeParent*
 WebRenderBridgeParent::GetRootCompositorBridgeParent() const
 {
   if (!mCompositorBridge) {
     return nullptr;
   }
@@ -723,29 +735,30 @@ WebRenderBridgeParent::RecvParentCommand
   }
   ProcessWebRenderParentCommands(aCommands);
   return IPC_OK();
 }
 
 void
 WebRenderBridgeParent::ProcessWebRenderParentCommands(const InfallibleTArray<WebRenderParentCommand>& aCommands)
 {
+  wr::TransactionBuilder txn;
   for (InfallibleTArray<WebRenderParentCommand>::index_type i = 0; i < aCommands.Length(); ++i) {
     const WebRenderParentCommand& cmd = aCommands[i];
     switch (cmd.type()) {
       case WebRenderParentCommand::TOpAddPipelineIdForCompositable: {
         const OpAddPipelineIdForCompositable& op = cmd.get_OpAddPipelineIdForCompositable();
         AddPipelineIdForCompositable(op.pipelineId(),
                                      op.handle(),
                                      op.isAsync());
         break;
       }
       case WebRenderParentCommand::TOpRemovePipelineIdForCompositable: {
         const OpRemovePipelineIdForCompositable& op = cmd.get_OpRemovePipelineIdForCompositable();
-        RemovePipelineIdForCompositable(op.pipelineId());
+        RemovePipelineIdForCompositable(op.pipelineId(), txn);
         break;
       }
       case WebRenderParentCommand::TOpAddExternalImageIdForCompositable: {
         const OpAddExternalImageIdForCompositable& op = cmd.get_OpAddExternalImageIdForCompositable();
         AddExternalImageIdForCompositable(op.externalImageId(),
                                           op.handle());
         break;
       }
@@ -789,16 +802,17 @@ WebRenderBridgeParent::ProcessWebRenderP
         break;
       }
       default: {
         // other commands are handle on the child
         break;
       }
     }
   }
+  mApi->SendTransaction(txn);
 }
 
 mozilla::ipc::IPCResult
 WebRenderBridgeParent::RecvGetSnapshot(PTextureParent* aTexture)
 {
   if (mDestroyed) {
     return IPC_OK();
   }
@@ -891,33 +905,31 @@ WebRenderBridgeParent::AddPipelineIdForC
   wrHost->EnableUseAsyncImagePipeline();
   mAsyncCompositables.Put(wr::AsUint64(aPipelineId), wrHost);
   mAsyncImageManager->AddAsyncImagePipeline(aPipelineId, wrHost);
 
   return;
 }
 
 void
-WebRenderBridgeParent::RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId)
+WebRenderBridgeParent::RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId,
+                                                       wr::TransactionBuilder& aTxn)
 {
   if (mDestroyed) {
     return;
   }
 
   WebRenderImageHost* wrHost = mAsyncCompositables.Get(wr::AsUint64(aPipelineId)).get();
   if (!wrHost) {
     return;
   }
 
-  wr::TransactionBuilder txn;
-
   wrHost->ClearWrBridge();
-  mAsyncImageManager->RemoveAsyncImagePipeline(aPipelineId, txn);
-  txn.RemovePipeline(aPipelineId);
-  mApi->SendTransaction(txn);
+  mAsyncImageManager->RemoveAsyncImagePipeline(aPipelineId, aTxn);
+  aTxn.RemovePipeline(aPipelineId);
   mAsyncCompositables.Remove(wr::AsUint64(aPipelineId));
   return;
 }
 
 void
 WebRenderBridgeParent::AddExternalImageIdForCompositable(const ExternalImageId& aImageId,
                                                          const CompositableHandle& aHandle)
 {
@@ -992,16 +1004,17 @@ WebRenderBridgeParent::RecvClearCachedRe
   mApi->SendTransaction(txn);
   // Schedule generate frame to clean up Pipeline
   ScheduleGenerateFrame();
   // Remove animations.
   for (std::unordered_set<uint64_t>::iterator iter = mActiveAnimations.begin(); iter != mActiveAnimations.end(); iter++) {
     mAnimStorage->ClearById(*iter);
   }
   mActiveAnimations.clear();
+  std::queue<CompositorAnimationIdsForEpoch>().swap(mCompositorAnimationsToDelete); // clear queue
   return IPC_OK();
 }
 
 void
 WebRenderBridgeParent::UpdateWebRender(CompositorVsyncScheduler* aScheduler,
                                        wr::WebRenderAPI* aApi,
                                        AsyncImagePipelineManager* aImageMgr,
                                        CompositorAnimationStorage* aAnimStorage)
@@ -1456,16 +1469,17 @@ WebRenderBridgeParent::ClearResources()
   txn.RemovePipeline(mPipelineId);
 
   mApi->SendTransaction(txn);
 
   for (std::unordered_set<uint64_t>::iterator iter = mActiveAnimations.begin(); iter != mActiveAnimations.end(); iter++) {
     mAnimStorage->ClearById(*iter);
   }
   mActiveAnimations.clear();
+  std::queue<CompositorAnimationIdsForEpoch>().swap(mCompositorAnimationsToDelete); // clear queue
 
   if (mWidget) {
     mCompositorScheduler->Destroy();
   }
   mAnimStorage = nullptr;
   mCompositorScheduler = nullptr;
   mAsyncImageManager = nullptr;
   mApi = nullptr;
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -178,16 +178,18 @@ public:
    */
   void ScheduleGenerateFrame();
 
   void UpdateWebRender(CompositorVsyncScheduler* aScheduler,
                        wr::WebRenderAPI* aApi,
                        AsyncImagePipelineManager* aImageMgr,
                        CompositorAnimationStorage* aAnimStorage);
 
+  void RemoveEpochDataPriorTo(const wr::Epoch& aRenderedEpoch);
+
 private:
   explicit WebRenderBridgeParent(const wr::PipelineId& aPipelineId);
   virtual ~WebRenderBridgeParent();
 
   void UpdateAPZFocusState(const FocusTarget& aFocus);
   void UpdateAPZScrollData(const wr::Epoch& aEpoch, WebRenderScrollData&& aData);
 
   bool UpdateResources(const nsTArray<OpUpdateResource>& aResourceUpdates,
@@ -195,17 +197,18 @@ private:
                        const nsTArray<ipc::Shmem>& aLargeShmems,
                        wr::TransactionBuilder& aUpdates);
   bool AddExternalImage(wr::ExternalImageId aExtId, wr::ImageKey aKey,
                         wr::TransactionBuilder& aResources);
 
   void AddPipelineIdForCompositable(const wr::PipelineId& aPipelineIds,
                                     const CompositableHandle& aHandle,
                                     const bool& aAsync);
-  void RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId);
+  void RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId,
+                                       wr::TransactionBuilder& aTxn);
 
   void AddExternalImageIdForCompositable(const ExternalImageId& aImageId,
                                          const CompositableHandle& aHandle);
   void RemoveExternalImageId(const ExternalImageId& aImageId);
 
   LayersId GetLayersId() const;
   void ProcessWebRenderParentCommands(const InfallibleTArray<WebRenderParentCommand>& aCommands);
 
@@ -234,16 +237,27 @@ private:
       , mFwdTime(aFwdTime)
     {}
     wr::Epoch mEpoch;
     TransactionId mId;
     TimeStamp mTxnStartTime;
     TimeStamp mFwdTime;
   };
 
+  struct CompositorAnimationIdsForEpoch {
+    CompositorAnimationIdsForEpoch(const wr::Epoch& aEpoch, InfallibleTArray<uint64_t>&& aIds)
+      : mEpoch(aEpoch)
+      , mIds(Move(aIds))
+    {
+    }
+
+    wr::Epoch mEpoch;
+    InfallibleTArray<uint64_t> mIds;
+  };
+
   CompositorBridgeParentBase* MOZ_NON_OWNING_REF mCompositorBridge;
   wr::PipelineId mPipelineId;
   RefPtr<widget::CompositorWidget> mWidget;
   RefPtr<wr::WebRenderAPI> mApi;
   RefPtr<AsyncImagePipelineManager> mAsyncImageManager;
   RefPtr<CompositorVsyncScheduler> mCompositorScheduler;
   RefPtr<CompositorAnimationStorage> mAnimStorage;
   // mActiveAnimations is used to avoid leaking animations when WebRenderBridgeParent is
@@ -257,16 +271,17 @@ private:
   // These fields keep track of the latest layer observer epoch values in the child and the
   // parent. mChildLayerObserverEpoch is the latest epoch value received from the child.
   // mParentLayerObserverEpoch is the latest epoch value that we have told TabParent about
   // (via ObserveLayerUpdate).
   uint64_t mChildLayerObserverEpoch;
   uint64_t mParentLayerObserverEpoch;
 
   std::queue<PendingTransactionId> mPendingTransactionIds;
+  std::queue<CompositorAnimationIdsForEpoch> mCompositorAnimationsToDelete;
   wr::Epoch mWrEpoch;
   wr::IdNamespace mIdNamespace;
 
   bool mPaused;
   bool mDestroyed;
   bool mForceRendering;
   bool mReceivedDisplayList;
 };
--- a/gfx/webrender/res/brush_image.glsl
+++ b/gfx/webrender/res/brush_image.glsl
@@ -35,34 +35,16 @@ ImageBrushData fetch_image_data(int addr
     vec4[2] raw_data = fetch_from_resource_cache_2(address);
     ImageBrushData data = ImageBrushData(
         raw_data[0],
         raw_data[1]
     );
     return data;
 }
 
-struct ImageBrushExtraData {
-    RectWithSize rendered_task_rect;
-    vec2 offset;
-};
-
-ImageBrushExtraData fetch_image_extra_data(int address) {
-    vec4[2] raw_data = fetch_from_resource_cache_2(address);
-    RectWithSize rendered_task_rect = RectWithSize(
-        raw_data[0].xy,
-        raw_data[0].zw
-    );
-    ImageBrushExtraData data = ImageBrushExtraData(
-        rendered_task_rect,
-        raw_data[1].xy
-    );
-    return data;
-}
-
 #ifdef WR_FEATURE_ALPHA_PASS
 vec2 transform_point_snapped(
     vec2 local_pos,
     RectWithSize local_rect,
     mat4 transform
 ) {
     vec2 snap_offset = compute_snap_offset(local_pos, transform, local_rect);
     vec4 world_pos = transform * vec4(local_pos, 0.0, 1.0);
@@ -100,67 +82,44 @@ void brush_vs(
     vec2 min_uv = min(uv0, uv1);
     vec2 max_uv = max(uv0, uv1);
 
     vUvSampleBounds = vec4(
         min_uv + vec2(0.5),
         max_uv - vec2(0.5)
     ) / texture_size.xyxy;
 
-    vec2 f;
+    vec2 f = (vi.local_pos - local_rect.p0) / local_rect.size;
 
 #ifdef WR_FEATURE_ALPHA_PASS
     int color_mode = user_data.y >> 16;
     int raster_space = user_data.y & 0xffff;
     ImageBrushData image_data = fetch_image_data(prim_address);
 
     if (color_mode == COLOR_MODE_FROM_PASS) {
         color_mode = uMode;
     }
 
     // Derive the texture coordinates for this image, based on
     // whether the source image is a local-space or screen-space
     // image.
     switch (raster_space) {
         case RASTER_SCREEN: {
-            ImageBrushExtraData extra_data = fetch_image_extra_data(user_data.z);
-
-            vec2 snapped_device_pos;
-
-            // For drop-shadows, we need to apply a local offset
-            // in order to generate the correct screen-space UV.
-            // For other effects, we can use the 1:1 mapping of
-            // the vertex device position for the UV generation.
-            switch (color_mode) {
-                case COLOR_MODE_ALPHA: {
-                    vec2 local_pos = vi.local_pos - extra_data.offset;
-                    snapped_device_pos = transform_point_snapped(
-                        local_pos,
-                        local_rect,
-                        transform
-                    );
-                    break;
-                }
-                default:
-                    snapped_device_pos = vi.snapped_device_pos;
-                    break;
-            }
-
-            f = (snapped_device_pos - extra_data.rendered_task_rect.p0) / extra_data.rendered_task_rect.size;
-
+            // Since the screen space UVs specify an arbitrary quad, do
+            // a bilinear interpolation to get the correct UV for this
+            // local position.
+            ImageResourceExtra extra_data = fetch_image_resource_extra(user_data.x);
+            vec2 x = mix(extra_data.st_tl, extra_data.st_tr, f.x);
+            vec2 y = mix(extra_data.st_bl, extra_data.st_br, f.x);
+            f = mix(x, y, f.y);
             break;
         }
-        case RASTER_LOCAL:
-        default: {
-            f = (vi.local_pos - local_rect.p0) / local_rect.size;
+        default:
             break;
-        }
     }
-#else
-    f = (vi.local_pos - local_rect.p0) / local_rect.size;
 #endif
 
     // Offset and scale vUv here to avoid doing it in the fragment shader.
     vUv.xy = mix(uv0, uv1, f) - min_uv;
     vUv.xy /= texture_size;
     vUv.xy *= repeat.xy;
 
 #ifdef WR_FEATURE_TEXTURE_RECT
--- a/gfx/webrender/res/brush_radial_gradient.glsl
+++ b/gfx/webrender/res/brush_radial_gradient.glsl
@@ -53,18 +53,16 @@ void brush_vs(
     // Transform all coordinates by the y scale so the
     // fragment shader can work with circles
     float ratio_xy = gradient.ratio_xy_extend_mode.x;
     vPos.y *= ratio_xy;
     vCenter.y *= ratio_xy;
     vRepeatedSize = local_rect.size / tile_repeat.xy;
     vRepeatedSize.y *=  ratio_xy;
 
-    vPos;
-
     vGradientAddress = user_data.x;
 
     // Whether to repeat the gradient instead of clamping.
     vGradientRepeat = float(int(gradient.ratio_xy_extend_mode.y) != EXTEND_MODE_CLAMP);
 
 #ifdef WR_FEATURE_ALPHA_PASS
     vTileRepeat = tile_repeat.xy;
     vLocalPos = vi.local_pos;
--- a/gfx/webrender/res/cs_clip_border.glsl
+++ b/gfx/webrender/res/cs_clip_border.glsl
@@ -10,16 +10,17 @@ in vec4 aDashOrDot1;
 varying vec3 vPos;
 
 flat varying vec2 vClipCenter;
 
 flat varying vec4 vPoint_Tangent0;
 flat varying vec4 vPoint_Tangent1;
 flat varying vec3 vDotParams;
 flat varying vec2 vAlphaMask;
+flat varying vec4 vTaskRect;
 
 #ifdef WR_VERTEX_SHADER
 // Matches BorderCorner enum in border.rs
 #define CORNER_TOP_LEFT     0
 #define CORNER_TOP_RIGHT    1
 #define CORNER_BOTTOM_LEFT  2
 #define CORNER_BOTTOM_RIGHT 3
 
@@ -140,19 +141,23 @@ void main(void) {
     // Transform to world pos
     vec4 world_pos = scroll_node.transform * vec4(pos, 0.0, 1.0);
     world_pos.xyz /= world_pos.w;
 
     // Scale into device pixels.
     vec2 device_pos = world_pos.xy * uDevicePixelRatio;
 
     // Position vertex within the render task area.
-    vec2 final_pos = device_pos -
-                     area.screen_origin +
-                     area.common_data.task_rect.p0;
+    vec2 task_rect_origin = area.common_data.task_rect.p0;
+    vec2 final_pos = device_pos - area.screen_origin + task_rect_origin;
+
+    // We pass the task rectangle to the fragment shader so that we can do one last clip
+    // in order to ensure that we don't draw outside the task rectangle.
+    vTaskRect.xy = task_rect_origin;
+    vTaskRect.zw = task_rect_origin + area.common_data.task_rect.size;
 
     // Calculate the local space position for this vertex.
     vec4 node_pos = get_node_pos(world_pos.xy, scroll_node);
     vPos = node_pos.xyw;
 
     gl_Position = uTransform * vec4(final_pos, 0.0, 1.0);
 }
 #endif
@@ -185,11 +190,14 @@ void main(void) {
     float d = mix(dash_distance, dot_distance, vAlphaMask.x);
 
     // Apply AA.
     d = distance_aa(aa_range, d);
 
     // Completely mask out clip if zero'ing out the rect.
     d = d * vAlphaMask.y;
 
+    // Make sure that we don't draw outside the task rectangle.
+    d = d * point_inside_rect(gl_FragCoord.xy, vTaskRect.xy, vTaskRect.zw);
+
     oFragColor = vec4(d, 0.0, 0.0, 1.0);
 }
 #endif
--- a/gfx/webrender/res/resource_cache.glsl
+++ b/gfx/webrender/res/resource_cache.glsl
@@ -1,14 +1,16 @@
 /* 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/. */
 
 uniform HIGHP_SAMPLER_FLOAT sampler2D sResourceCache;
 
+#define VECS_PER_IMAGE_RESOURCE     2
+
 // TODO(gw): This is here temporarily while we have
 //           both GPU store and cache. When the GPU
 //           store code is removed, we can change the
 //           PrimitiveInstance instance structure to
 //           use 2x unsigned shorts as vertex attributes
 //           instead of an int, and encode the UV directly
 //           in the vertices.
 ivec2 get_resource_cache_uv(int address) {
@@ -108,9 +110,28 @@ ImageResource fetch_image_resource(int a
 }
 
 ImageResource fetch_image_resource_direct(ivec2 address) {
     vec4 data[2] = fetch_from_resource_cache_2_direct(address);
     RectWithEndpoint uv_rect = RectWithEndpoint(data[0].xy, data[0].zw);
     return ImageResource(uv_rect, data[1].x, data[1].yzw);
 }
 
+// Fetch optional extra data for a texture cache resource. This can contain
+// a polygon defining a UV rect within the texture cache resource.
+struct ImageResourceExtra {
+    vec2 st_tl;
+    vec2 st_tr;
+    vec2 st_bl;
+    vec2 st_br;
+};
+
+ImageResourceExtra fetch_image_resource_extra(int address) {
+    vec4 data[2] = fetch_from_resource_cache_2(address + VECS_PER_IMAGE_RESOURCE);
+    return ImageResourceExtra(
+        data[0].xy,
+        data[0].zw,
+        data[1].xy,
+        data[1].zw
+    );
+}
+
 #endif //WR_VERTEX_SHADER
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -13,17 +13,16 @@ use euclid::{TypedTransform3D, vec3};
 use glyph_rasterizer::GlyphFormat;
 use gpu_cache::{GpuCache, GpuCacheAddress};
 use gpu_types::{BrushFlags, BrushInstance, ClipChainRectIndex, ClipMaskBorderCornerDotDash};
 use gpu_types::{ClipMaskInstance, ClipScrollNodeIndex, CompositePrimitiveInstance};
 use gpu_types::{PrimitiveInstance, RasterizationSpace, SimplePrimitiveInstance, ZBufferId};
 use gpu_types::ZBufferIdGenerator;
 use internal_types::{FastHashMap, SavedTargetIndex, SourceTexture};
 use picture::{PictureCompositeMode, PicturePrimitive, PictureSurface};
-use picture::{IMAGE_BRUSH_BLOCKS, IMAGE_BRUSH_EXTRA_BLOCKS};
 use plane_split::{BspSplitter, Polygon, Splitter};
 use prim_store::{CachedGradient, ImageSource, PrimitiveIndex, PrimitiveKind, PrimitiveMetadata, PrimitiveStore};
 use prim_store::{BrushPrimitive, BrushKind, DeferredResolve, EdgeAaSegmentMask, PictureIndex, PrimitiveRun};
 use render_task::{RenderTaskAddress, RenderTaskId, RenderTaskKind, RenderTaskTree};
 use renderer::{BlendMode, ImageBufferKind};
 use renderer::{BLOCKS_PER_UV_RECT, ShaderColorMode};
 use resource_cache::{CacheItem, GlyphFetchResult, ImageRequest, ResourceCache};
 use scene::FilterOpHelpers;
@@ -695,17 +694,17 @@ impl AlphaBatchBuilder {
                                                     z,
                                                     segment_index: 0,
                                                     edge_flags: EdgeAaSegmentMask::empty(),
                                                     brush_flags: BrushFlags::empty(),
                                                     user_data: [
                                                         uv_rect_address.as_int(),
                                                         (ShaderColorMode::ColorBitmap as i32) << 16 |
                                                         RasterizationSpace::Screen as i32,
-                                                        picture.extra_gpu_data_handle.as_int(gpu_cache),
+                                                        0,
                                                     ],
                                                 };
                                                 batch.push(PrimitiveInstance::from(instance));
                                                 false
                                             }
                                             None => {
                                                 true
                                             }
@@ -745,47 +744,43 @@ impl AlphaBatchBuilder {
                                             let shadow_uv_rect_address = render_tasks[cache_task_id]
                                                 .get_texture_address(gpu_cache)
                                                 .as_int();
                                             let content_uv_rect_address = render_tasks[secondary_id]
                                                 .get_texture_address(gpu_cache)
                                                 .as_int();
 
                                             // Get the GPU cache address of the extra data handle.
-                                            let extra_data_address = gpu_cache.get_address(&picture.extra_gpu_data_handle);
-                                            let shadow_prim_address = extra_data_address
-                                                .offset(IMAGE_BRUSH_EXTRA_BLOCKS);
-                                            let shadow_data_address = extra_data_address
-                                                .offset(IMAGE_BRUSH_EXTRA_BLOCKS + IMAGE_BRUSH_BLOCKS);
+                                            let shadow_prim_address = gpu_cache.get_address(&picture.extra_gpu_data_handle);
 
                                             let shadow_instance = BrushInstance {
                                                 picture_address: task_address,
                                                 prim_address: shadow_prim_address,
                                                 clip_chain_rect_index,
                                                 scroll_id,
                                                 clip_task_address,
                                                 z,
                                                 segment_index: 0,
                                                 edge_flags: EdgeAaSegmentMask::empty(),
                                                 brush_flags: BrushFlags::empty(),
                                                 user_data: [
                                                     shadow_uv_rect_address,
                                                     (ShaderColorMode::Alpha as i32) << 16 |
                                                     RasterizationSpace::Screen as i32,
-                                                    shadow_data_address.as_int(),
+                                                    0,
                                                 ],
                                             };
 
                                             let content_instance = BrushInstance {
                                                 prim_address: prim_cache_address,
                                                 user_data: [
                                                     content_uv_rect_address,
                                                     (ShaderColorMode::ColorBitmap as i32) << 16 |
                                                     RasterizationSpace::Screen as i32,
-                                                    extra_data_address.as_int(),
+                                                    0,
                                                 ],
                                                 ..shadow_instance
                                             };
 
                                             self.batch_list
                                                 .get_suitable_batch(shadow_key, &task_relative_bounding_rect)
                                                 .push(PrimitiveInstance::from(shadow_instance));
 
@@ -948,17 +943,17 @@ impl AlphaBatchBuilder {
                                     z,
                                     segment_index: 0,
                                     edge_flags: EdgeAaSegmentMask::empty(),
                                     brush_flags: BrushFlags::empty(),
                                     user_data: [
                                         uv_rect_address,
                                         (ShaderColorMode::ColorBitmap as i32) << 16 |
                                         RasterizationSpace::Screen as i32,
-                                        picture.extra_gpu_data_handle.as_int(gpu_cache),
+                                        0,
                                     ],
                                 };
                                 batch.push(PrimitiveInstance::from(instance));
                                 false
                             }
                             None => {
                                 true
                             }
--- a/gfx/webrender/src/clip_scroll_node.rs
+++ b/gfx/webrender/src/clip_scroll_node.rs
@@ -96,16 +96,19 @@ pub struct ClipScrollNode {
     /// between our reference frame and this node. For reference frames, we also include
     /// whatever local transformation this reference frame provides. This can be combined
     /// with the local_viewport_rect to get its position in world space.
     pub world_viewport_transform: LayoutToWorldFastTransform,
 
     /// World transform for content transformed by this node.
     pub world_content_transform: LayoutToWorldFastTransform,
 
+    /// The current transform kind of world_content_transform.
+    pub transform_kind: TransformedRectKind,
+
     /// Pipeline that this layer belongs to
     pub pipeline_id: PipelineId,
 
     /// Parent layer. If this is None, we are the root node.
     pub parent: Option<ClipScrollNodeIndex>,
 
     /// Child layers
     pub children: Vec<ClipScrollNodeIndex>,
@@ -137,16 +140,17 @@ impl ClipScrollNode {
         parent_index: Option<ClipScrollNodeIndex>,
         rect: &LayoutRect,
         node_type: NodeType
     ) -> Self {
         ClipScrollNode {
             local_viewport_rect: *rect,
             world_viewport_transform: LayoutToWorldFastTransform::identity(),
             world_content_transform: LayoutToWorldFastTransform::identity(),
+            transform_kind: TransformedRectKind::AxisAligned,
             parent: parent_index,
             children: Vec::new(),
             pipeline_id,
             node_type,
             invertible: true,
             coordinate_system_id: CoordinateSystemId(0),
             coordinate_system_relative_transform: LayoutFastTransform::identity(),
             node_data_index: GPUClipScrollNodeIndex(0),
@@ -280,25 +284,20 @@ impl ClipScrollNode {
         let inv_transform = match self.world_content_transform.inverse() {
             Some(inverted) => inverted.to_transform(),
             None => {
                 node_data.push(ClipScrollNodeData::invalid());
                 return;
             }
         };
 
-        let transform_kind = if self.world_content_transform.preserves_2d_axis_alignment() {
-            TransformedRectKind::AxisAligned
-        } else {
-            TransformedRectKind::Complex
-        };
         let data = ClipScrollNodeData {
             transform: self.world_content_transform.into(),
             inv_transform,
-            transform_kind: transform_kind as u32 as f32,
+            transform_kind: self.transform_kind as u32 as f32,
             padding: [0.0; 3],
         };
 
         // Write the data that will be made available to the GPU for this node.
         node_data.push(data);
     }
 
     pub fn update(
@@ -316,16 +315,22 @@ impl ClipScrollNode {
         // quit here.
         if !state.invertible {
             self.mark_uninvertible();
             return;
         }
 
         self.update_transform(state, next_coordinate_system_id, scene_properties);
 
+        self.transform_kind = if self.world_content_transform.preserves_2d_axis_alignment() {
+            TransformedRectKind::AxisAligned
+        } else {
+            TransformedRectKind::Complex
+        };
+
         // If this node is a reference frame, we check if it has a non-invertible matrix.
         // For non-reference-frames we assume that they will produce only additional
         // translations which should be invertible.
         match self.node_type {
             NodeType::ReferenceFrame(info) if !info.invertible => {
                 self.mark_uninvertible();
                 return;
             }
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -1838,16 +1838,17 @@ impl<'a> DisplayListFlattener<'a> {
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
         start_point: LayoutPoint,
         end_point: LayoutPoint,
         stops: ItemRange<GradientStop>,
         stops_count: usize,
         extend_mode: ExtendMode,
         gradient_index: CachedGradientIndex,
+        stretch_size: LayoutSize,
     ) {
         // Try to ensure that if the gradient is specified in reverse, then so long as the stops
         // are also supplied in reverse that the rendered result will be equivalent. To do this,
         // a reference orientation for the gradient line must be chosen, somewhat arbitrarily, so
         // just designate the reference orientation as start < end. Aligned gradient rendering
         // manages to produce the same result regardless of orientation, so don't worry about
         // reversing in that case.
         let reverse_stops = start_point.x > end_point.x ||
@@ -1866,16 +1867,17 @@ impl<'a> DisplayListFlattener<'a> {
             BrushKind::LinearGradient {
                 stops_range: stops,
                 stops_count,
                 extend_mode,
                 reverse_stops,
                 start_point: sp,
                 end_point: ep,
                 gradient_index,
+                stretch_size,
             },
             None,
         );
 
         let prim = PrimitiveContainer::Brush(prim);
 
         self.add_primitive(clip_and_scroll, info, Vec::new(), prim);
     }
@@ -1884,76 +1886,84 @@ impl<'a> DisplayListFlattener<'a> {
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
         start_point: LayoutPoint,
         end_point: LayoutPoint,
         stops: ItemRange<GradientStop>,
         stops_count: usize,
         extend_mode: ExtendMode,
-        tile_size: LayoutSize,
+        stretch_size: LayoutSize,
         tile_spacing: LayoutSize,
     ) {
         let gradient_index = CachedGradientIndex(self.cached_gradients.len());
         self.cached_gradients.push(CachedGradient::new());
 
-        let prim_infos = info.decompose(
-            tile_size,
-            tile_spacing,
-            64 * 64,
-        );
+        if tile_spacing != LayoutSize::zero() {
+            let prim_infos = info.decompose(
+                stretch_size,
+                tile_spacing,
+                64 * 64,
+            );
 
-        if prim_infos.is_empty() {
-            self.add_gradient_impl(
-                clip_and_scroll,
-                info,
-                start_point,
-                end_point,
-                stops,
-                stops_count,
-                extend_mode,
-                gradient_index,
-            );
-        } else {
-            for prim_info in prim_infos {
-                self.add_gradient_impl(
-                    clip_and_scroll,
-                    &prim_info,
-                    start_point,
-                    end_point,
-                    stops,
-                    stops_count,
-                    extend_mode,
-                    gradient_index,
-                );
+            if !prim_infos.is_empty() {
+                for prim_info in prim_infos {
+                    self.add_gradient_impl(
+                        clip_and_scroll,
+                        &prim_info,
+                        start_point,
+                        end_point,
+                        stops,
+                        stops_count,
+                        extend_mode,
+                        gradient_index,
+                        prim_info.rect.size,
+                    );
+                }
+
+                return;
             }
         }
+
+        self.add_gradient_impl(
+            clip_and_scroll,
+            info,
+            start_point,
+            end_point,
+            stops,
+            stops_count,
+            extend_mode,
+            gradient_index,
+            stretch_size,
+        );
     }
 
     fn add_radial_gradient_impl(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
         center: LayoutPoint,
         start_radius: f32,
         end_radius: f32,
         ratio_xy: f32,
         stops: ItemRange<GradientStop>,
         extend_mode: ExtendMode,
         gradient_index: CachedGradientIndex,
+        stretch_size: LayoutSize,
     ) {
         let prim = BrushPrimitive::new(
             BrushKind::RadialGradient {
                 stops_range: stops,
                 extend_mode,
                 center,
                 start_radius,
                 end_radius,
                 ratio_xy,
                 gradient_index,
+                stretch_size,
             },
             None,
         );
 
         self.add_primitive(
             clip_and_scroll,
             info,
             Vec::new(),
@@ -1966,55 +1976,61 @@ impl<'a> DisplayListFlattener<'a> {
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
         center: LayoutPoint,
         start_radius: f32,
         end_radius: f32,
         ratio_xy: f32,
         stops: ItemRange<GradientStop>,
         extend_mode: ExtendMode,
-        tile_size: LayoutSize,
+        stretch_size: LayoutSize,
         tile_spacing: LayoutSize,
     ) {
         let gradient_index = CachedGradientIndex(self.cached_gradients.len());
         self.cached_gradients.push(CachedGradient::new());
 
-        let prim_infos = info.decompose(
-            tile_size,
-            tile_spacing,
-            64 * 64,
-        );
+        if tile_spacing != LayoutSize::zero() {
+            let prim_infos = info.decompose(
+                stretch_size,
+                tile_spacing,
+                64 * 64,
+            );
 
-        if prim_infos.is_empty() {
-            self.add_radial_gradient_impl(
-                clip_and_scroll,
-                info,
-                center,
-                start_radius,
-                end_radius,
-                ratio_xy,
-                stops,
-                extend_mode,
-                gradient_index,
-            );
-        } else {
-            for prim_info in prim_infos {
-                self.add_radial_gradient_impl(
-                    clip_and_scroll,
-                    &prim_info,
-                    center,
-                    start_radius,
-                    end_radius,
-                    ratio_xy,
-                    stops,
-                    extend_mode,
-                    gradient_index,
-                );
+            if !prim_infos.is_empty() {
+                for prim_info in prim_infos {
+                    self.add_radial_gradient_impl(
+                        clip_and_scroll,
+                        &prim_info,
+                        center,
+                        start_radius,
+                        end_radius,
+                        ratio_xy,
+                        stops,
+                        extend_mode,
+                        gradient_index,
+                        stretch_size,
+                    );
+                }
+
+                return;
             }
         }
+
+        self.add_radial_gradient_impl(
+            clip_and_scroll,
+            info,
+            center,
+            start_radius,
+            end_radius,
+            ratio_xy,
+            stops,
+            extend_mode,
+            gradient_index,
+            stretch_size,
+        );
     }
 
     pub fn add_text(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         run_offset: LayoutVector2D,
         prim_info: &LayoutPrimitiveInfo,
         font_instance_key: &FontInstanceKey,
@@ -2132,17 +2148,16 @@ impl<'a> DisplayListFlattener<'a> {
                     (texel_rect.uv1.y - texel_rect.uv0.y) as i32,
                 ),
             )
         });
 
         // See if conditions are met to run through the new
         // image brush shader, which supports segments.
         if tile_spacing == LayoutSize::zero() &&
-           stretch_size == info.rect.size &&
            tile_offset.is_none() {
             let prim = BrushPrimitive::new(
                 BrushKind::Image {
                     request,
                     current_epoch: Epoch::invalid(),
                     alpha_type,
                     stretch_size,
                     tile_spacing,
--- a/gfx/webrender/src/frame_builder.rs
+++ b/gfx/webrender/src/frame_builder.rs
@@ -5,17 +5,17 @@
 use api::{BuiltDisplayList, ColorF, DeviceIntPoint, DeviceIntRect, DevicePixelScale};
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentLayer, FontRenderMode};
 use api::{LayoutRect, LayoutSize, PipelineId, WorldPoint};
 use clip::{ClipChain, ClipStore};
 use clip_scroll_node::{ClipScrollNode};
 use clip_scroll_tree::{ClipScrollNodeIndex, ClipScrollTree};
 use display_list_flattener::{DisplayListFlattener};
 use gpu_cache::GpuCache;
-use gpu_types::{ClipChainRectIndex, ClipScrollNodeData};
+use gpu_types::{ClipChainRectIndex, ClipScrollNodeData, UvRectKind};
 use hit_test::{HitTester, HitTestingRun};
 use internal_types::{FastHashMap};
 use picture::PictureSurface;
 use prim_store::{CachedGradient, PrimitiveIndex, PrimitiveRun, PrimitiveStore};
 use profiler::{FrameProfileCounters, GpuCacheProfileCounters, TextureCacheProfileCounters};
 use render_backend::FrameId;
 use render_task::{RenderTask, RenderTaskId, RenderTaskLocation, RenderTaskTree};
 use resource_cache::{ResourceCache};
@@ -228,16 +228,17 @@ impl FrameBuilder {
         let pic = &mut self.prim_store.pictures[0];
         pic.runs = pic_context.prim_runs;
 
         let root_render_task = RenderTask::new_picture(
             RenderTaskLocation::Fixed(frame_context.screen_rect),
             PrimitiveIndex(0),
             DeviceIntPoint::zero(),
             pic_state.tasks,
+            UvRectKind::Rect,
         );
 
         let render_task_id = frame_state.render_tasks.add(root_render_task);
         pic.surface = Some(PictureSurface::RenderTask(render_task_id));
         Some(render_task_id)
     }
 
     fn update_scroll_bars(&mut self, clip_scroll_tree: &ClipScrollTree, gpu_cache: &mut GpuCache) {
--- a/gfx/webrender/src/glyph_rasterizer.rs
+++ b/gfx/webrender/src/glyph_rasterizer.rs
@@ -16,16 +16,17 @@ use api::DeviceIntSize;
 use api::{ImageData, ImageDescriptor, ImageFormat};
 use app_units::Au;
 #[cfg(not(feature = "pathfinder"))]
 use device::TextureFilter;
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedSize2D, TypedVector2D};
 use glyph_cache::{CachedGlyphInfo, GlyphCache, GlyphCacheEntry};
 use gpu_cache::GpuCache;
+use gpu_types::UvRectKind;
 use internal_types::ResourceCacheError;
 #[cfg(feature = "pathfinder")]
 use pathfinder_font_renderer;
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh as PathfinderMesh;
 #[cfg(feature = "pathfinder")]
 use pathfinder_path_utils::cubic_to_quadratic::CubicToQuadraticTransformer;
 use platform::font::FontContext;
@@ -795,16 +796,17 @@ impl GlyphRasterizer {
                                 offset: 0,
                             },
                             TextureFilter::Linear,
                             Some(ImageData::Raw(Arc::new(glyph.bytes))),
                             [glyph.left, -glyph.top, glyph.scale],
                             None,
                             gpu_cache,
                             Some(glyph_key_cache.eviction_notice()),
+                            UvRectKind::Rect,
                         );
                         GlyphCacheEntry::Cached(CachedGlyphInfo {
                             texture_cache_handle,
                             format: glyph.format,
                         })
                     }
                 };
                 glyph_key_cache.insert(key, glyph_info);
--- a/gfx/webrender/src/gpu_cache.rs
+++ b/gfx/webrender/src/gpu_cache.rs
@@ -146,23 +146,16 @@ impl GpuCacheAddress {
     }
 
     pub fn invalid() -> Self {
         GpuCacheAddress {
             u: u16::MAX,
             v: u16::MAX,
         }
     }
-
-    pub fn offset(&self, offset: usize) -> Self {
-        GpuCacheAddress {
-            u: self.u + offset as u16,
-            v: self.v
-        }
-    }
 }
 
 impl Add<usize> for GpuCacheAddress {
     type Output = GpuCacheAddress;
 
     fn add(self, other: usize) -> GpuCacheAddress {
         GpuCacheAddress {
             u: self.u + other as u16,
--- a/gfx/webrender/src/gpu_types.rs
+++ b/gfx/webrender/src/gpu_types.rs
@@ -272,35 +272,75 @@ impl ClipScrollNodeData {
         }
     }
 }
 
 #[derive(Copy, Debug, Clone, PartialEq)]
 #[repr(C)]
 pub struct ClipChainRectIndex(pub usize);
 
+// Texture cache resources can be either a simple rect, or define
+// a polygon within a rect by specifying a UV coordinate for each
+// corner. This is useful for rendering screen-space rasterized
+// off-screen surfaces.
 #[derive(Debug, Copy, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-#[repr(C)]
+pub enum UvRectKind {
+    // The 2d bounds of the texture cache entry define the
+    // valid UV space for this texture cache entry.
+    Rect,
+    // The four vertices below define a quad within
+    // the texture cache entry rect. The shader can
+    // use a bilerp() to correctly interpolate a
+    // UV coord in the vertex shader.
+    Quad {
+        top_left: DevicePoint,
+        top_right: DevicePoint,
+        bottom_left: DevicePoint,
+        bottom_right: DevicePoint,
+    },
+}
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ImageSource {
     pub p0: DevicePoint,
     pub p1: DevicePoint,
     pub texture_layer: f32,
     pub user_data: [f32; 3],
+    pub uv_rect_kind: UvRectKind,
 }
 
 impl ImageSource {
     pub fn write_gpu_blocks(&self, request: &mut GpuDataRequest) {
         request.push([
             self.p0.x,
             self.p0.y,
             self.p1.x,
             self.p1.y,
         ]);
         request.push([
             self.texture_layer,
             self.user_data[0],
             self.user_data[1],
             self.user_data[2],
         ]);
+
+        // If this is a polygon uv kind, then upload the four vertices.
+        if let UvRectKind::Quad { top_left, top_right, bottom_left, bottom_right } = self.uv_rect_kind {
+            request.push([
+                top_left.x,
+                top_left.y,
+                top_right.x,
+                top_right.y,
+            ]);
+
+            request.push([
+                bottom_left.x,
+                bottom_left.y,
+                bottom_right.x,
+                bottom_right.y,
+            ]);
+        }
     }
 }
--- a/gfx/webrender/src/picture.rs
+++ b/gfx/webrender/src/picture.rs
@@ -1,40 +1,40 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-use api::{FilterOp, MixBlendMode, PipelineId, PremultipliedColorF};
-use api::{DeviceIntRect, DeviceIntSize, LayoutRect};
-use api::{PictureIntPoint, PictureIntRect, PictureIntSize};
+use api::{DeviceRect, FilterOp, MixBlendMode, PipelineId, PremultipliedColorF};
+use api::{DeviceIntRect, DeviceIntSize, DevicePoint, LayoutPoint, LayoutRect};
+use api::{DevicePixelScale, PictureIntPoint, PictureIntRect, PictureIntSize};
 use box_shadow::{BLUR_SAMPLE_SCALE};
+use clip_scroll_node::ClipScrollNode;
 use clip_scroll_tree::ClipScrollNodeIndex;
-use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState};
+use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PrimitiveRunContext};
 use gpu_cache::{GpuCacheHandle};
+use gpu_types::UvRectKind;
 use prim_store::{PrimitiveIndex, PrimitiveRun, PrimitiveRunLocalRect};
 use prim_store::{PrimitiveMetadata, ScrollNodeAndClipChain};
 use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle};
 use render_task::{RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskId, RenderTaskLocation};
 use scene::{FilterOpHelpers, SceneProperties};
 use std::mem;
 use tiling::RenderTargetKind;
+use util::TransformedRectKind;
 
 /*
  A picture represents a dynamically rendered image. It consists of:
 
  * A number of primitives that are drawn onto the picture.
  * A composite operation describing how to composite this
    picture into its parent.
  * A configuration describing how to draw the primitives on
    this picture (e.g. in screen space or local space).
  */
 
-pub const IMAGE_BRUSH_EXTRA_BLOCKS: usize = 2;
-pub const IMAGE_BRUSH_BLOCKS: usize = 6;
-
 /// Specifies how this Picture should be composited
 /// onto the target it belongs to.
 #[derive(Debug, Copy, Clone, PartialEq)]
 pub enum PictureCompositeMode {
     /// Apply CSS mix-blend-mode effect.
     MixBlend(MixBlendMode),
     /// Apply a CSS filter.
     Filter(FilterOp),
@@ -122,20 +122,16 @@ pub struct PicturePrimitive {
 
     // The pipeline that the primitives on this picture belong to.
     pub pipeline_id: PipelineId,
 
     // If true, apply the local clip rect to primitive drawn
     // in this picture.
     pub apply_local_clip_rect: bool,
 
-    // The current screen-space rect of the rendered
-    // portion of this picture.
-    task_rect: DeviceIntRect,
-
     // If a mix-blend-mode, contains the render task for
     // the readback of the framebuffer that we use to sample
     // from in the mix-blend-mode shader.
     // For drop-shadow filter, this will store the original
     // picture task which would be rendered on screen after
     // blur pass.
     pub secondary_render_task_id: Option<RenderTaskId>,
     /// How this picture should be composited.
@@ -194,17 +190,16 @@ impl PicturePrimitive {
             composite_mode,
             is_in_3d_context,
             frame_output_pipeline_id,
             reference_frame_index,
             real_local_rect: LayoutRect::zero(),
             extra_gpu_data_handle: GpuCacheHandle::new(),
             apply_local_clip_rect,
             pipeline_id,
-            task_rect: DeviceIntRect::zero(),
             id,
         }
     }
 
     pub fn add_primitive(
         &mut self,
         prim_index: PrimitiveIndex,
         clip_and_scroll: ScrollNodeAndClipChain
@@ -276,32 +271,32 @@ impl PicturePrimitive {
     pub fn allow_subpixel_aa(&self) -> bool {
         self.can_draw_directly_to_parent_surface()
     }
 
     pub fn prepare_for_render_inner(
         &mut self,
         prim_index: PrimitiveIndex,
         prim_metadata: &mut PrimitiveMetadata,
+        prim_run_context: &PrimitiveRunContext,
         mut pic_state_for_children: PictureState,
         pic_state: &mut PictureState,
         frame_context: &FrameBuildingContext,
         frame_state: &mut FrameBuildingState,
-    ) -> Option<DeviceIntRect> {
+    ) {
         let prim_screen_rect = prim_metadata
                                 .screen_rect
                                 .as_ref()
                                 .expect("bug: trying to draw an off-screen picture!?");
         if self.can_draw_directly_to_parent_surface() {
             pic_state.tasks.extend(pic_state_for_children.tasks);
             self.surface = None;
-            return None;
+            return;
         }
 
-
         // TODO(gw): Almost all of the Picture types below use extra_gpu_cache_data
         //           to store the same type of data. The exception is the filter
         //           with a ColorMatrix, which stores the color matrix here. It's
         //           probably worth tidying this code up to be a bit more consistent.
         //           Perhaps store the color matrix after the common data, even though
         //           it's not used by that shader.
         match self.composite_mode {
             Some(PictureCompositeMode::Filter(FilterOp::Blur(blur_radius))) => {
@@ -317,28 +312,36 @@ impl PicturePrimitive {
                 // then intersect with the total screen rect, to minimize the
                 // allocation size.
                 let device_rect = prim_screen_rect
                     .clipped
                     .inflate(blur_range, blur_range)
                     .intersection(&prim_screen_rect.unclipped)
                     .unwrap();
 
+                let uv_rect_kind = calculate_uv_rect_kind(
+                    &prim_metadata.local_rect,
+                    &prim_run_context.scroll_node,
+                    &device_rect,
+                    frame_context.device_pixel_scale,
+                );
+
                 // If we are drawing a blur that has primitives or clips that contain
                 // a complex coordinate system, don't bother caching them (for now).
                 // It's likely that they are animating and caching may not help here
                 // anyway. In the future we should relax this a bit, so that we can
                 // cache tasks with complex coordinate systems if we detect the
                 // relevant transforms haven't changed from frame to frame.
                 let surface = if pic_state_for_children.has_non_root_coord_system {
                     let picture_task = RenderTask::new_picture(
                         RenderTaskLocation::Dynamic(None, Some(device_rect.size)),
                         prim_index,
                         device_rect.origin,
                         pic_state_for_children.tasks,
+                        uv_rect_kind,
                     );
 
                     let picture_task_id = frame_state.render_tasks.add(picture_task);
 
                     let blur_render_task = RenderTask::new_blur(
                         blur_std_deviation,
                         picture_task_id,
                         frame_state.render_tasks,
@@ -383,16 +386,17 @@ impl PicturePrimitive {
                         |render_tasks| {
                             let child_tasks = mem::replace(&mut pic_state_for_children.tasks, Vec::new());
 
                             let picture_task = RenderTask::new_picture(
                                 RenderTaskLocation::Dynamic(None, Some(device_rect.size)),
                                 prim_index,
                                 device_rect.origin,
                                 child_tasks,
+                                uv_rect_kind,
                             );
 
                             let picture_task_id = render_tasks.add(picture_task);
 
                             let blur_render_task = RenderTask::new_blur(
                                 blur_std_deviation,
                                 picture_task_id,
                                 render_tasks,
@@ -407,20 +411,18 @@ impl PicturePrimitive {
                             render_task_id
                         }
                     );
 
                     PictureSurface::TextureCache(cache_item)
                 };
 
                 self.surface = Some(surface);
-
-                Some(device_rect)
             }
-            Some(PictureCompositeMode::Filter(FilterOp::DropShadow(_, blur_radius, _))) => {
+            Some(PictureCompositeMode::Filter(FilterOp::DropShadow(offset, blur_radius, color))) => {
                 let blur_std_deviation = blur_radius * frame_context.device_pixel_scale.0;
                 let blur_range = (blur_std_deviation * BLUR_SAMPLE_SCALE).ceil() as i32;
 
                 // The clipped field is the part of the picture that is visible
                 // on screen. The unclipped field is the screen-space rect of
                 // the complete picture, if no screen / clip-chain was applied
                 // (this includes the extra space for blur region). To ensure
                 // that we draw a large enough part of the picture to get correct
@@ -428,21 +430,29 @@ impl PicturePrimitive {
                 // then intersect with the total screen rect, to minimize the
                 // allocation size.
                 let device_rect = prim_screen_rect
                     .clipped
                     .inflate(blur_range, blur_range)
                     .intersection(&prim_screen_rect.unclipped)
                     .unwrap();
 
+                let uv_rect_kind = calculate_uv_rect_kind(
+                    &prim_metadata.local_rect,
+                    &prim_run_context.scroll_node,
+                    &device_rect,
+                    frame_context.device_pixel_scale,
+                );
+
                 let mut picture_task = RenderTask::new_picture(
                     RenderTaskLocation::Dynamic(None, Some(device_rect.size)),
                     prim_index,
                     device_rect.origin,
                     pic_state_for_children.tasks,
+                    uv_rect_kind,
                 );
                 picture_task.mark_for_saving();
 
                 let picture_task_id = frame_state.render_tasks.add(picture_task);
 
                 let blur_render_task = RenderTask::new_blur(
                     blur_std_deviation.round(),
                     picture_task_id,
@@ -452,132 +462,24 @@ impl PicturePrimitive {
                 );
 
                 self.secondary_render_task_id = Some(picture_task_id);
 
                 let render_task_id = frame_state.render_tasks.add(blur_render_task);
                 pic_state.tasks.push(render_task_id);
                 self.surface = Some(PictureSurface::RenderTask(render_task_id));
 
-                Some(device_rect)
-            }
-            Some(PictureCompositeMode::MixBlend(..)) => {
-                let picture_task = RenderTask::new_picture(
-                    RenderTaskLocation::Dynamic(None, Some(prim_screen_rect.clipped.size)),
-                    prim_index,
-                    prim_screen_rect.clipped.origin,
-                    pic_state_for_children.tasks,
-                );
-
-                let readback_task_id = frame_state.render_tasks.add(
-                    RenderTask::new_readback(prim_screen_rect.clipped)
-                );
-
-                self.secondary_render_task_id = Some(readback_task_id);
-                pic_state.tasks.push(readback_task_id);
-
-                let render_task_id = frame_state.render_tasks.add(picture_task);
-                pic_state.tasks.push(render_task_id);
-                self.surface = Some(PictureSurface::RenderTask(render_task_id));
-
-                Some(prim_screen_rect.clipped)
-            }
-            Some(PictureCompositeMode::Filter(filter)) => {
-                let device_rect = match filter {
-                    FilterOp::ColorMatrix(m) => {
-                        if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handle) {
-                            for i in 0..5 {
-                                request.push([m[i*4], m[i*4+1], m[i*4+2], m[i*4+3]]);
-                            }
-                        }
-
-                        None
-                    }
-                    _ => Some(prim_screen_rect.clipped),
-                };
-
-                let picture_task = RenderTask::new_picture(
-                    RenderTaskLocation::Dynamic(None, Some(prim_screen_rect.clipped.size)),
-                    prim_index,
-                    prim_screen_rect.clipped.origin,
-                    pic_state_for_children.tasks,
-                );
-
-                let render_task_id = frame_state.render_tasks.add(picture_task);
-                pic_state.tasks.push(render_task_id);
-                self.surface = Some(PictureSurface::RenderTask(render_task_id));
-
-                device_rect
-            }
-            Some(PictureCompositeMode::Blit) | None => {
-                let picture_task = RenderTask::new_picture(
-                    RenderTaskLocation::Dynamic(None, Some(prim_screen_rect.clipped.size)),
-                    prim_index,
-                    prim_screen_rect.clipped.origin,
-                    pic_state_for_children.tasks,
-                );
-
-                let render_task_id = frame_state.render_tasks.add(picture_task);
-                pic_state.tasks.push(render_task_id);
-                self.surface = Some(PictureSurface::RenderTask(render_task_id));
-
-                Some(prim_screen_rect.clipped)
-            }
-        }
-    }
-
-    pub fn prepare_for_render(
-        &mut self,
-        prim_index: PrimitiveIndex,
-        prim_metadata: &mut PrimitiveMetadata,
-        pic_state_for_children: PictureState,
-        pic_state: &mut PictureState,
-        frame_context: &FrameBuildingContext,
-        frame_state: &mut FrameBuildingState,
-    ) {
-        let device_rect = self.prepare_for_render_inner(
-            prim_index,
-            prim_metadata,
-            pic_state_for_children,
-            pic_state,
-            frame_context,
-            frame_state,
-        );
-
-        // If this picture type uses the common / general GPU data
-        // format, then write it now.
-        if let Some(device_rect) = device_rect {
-            // If scrolling or property animation has resulted in the task
-            // rect being different than last time, invalidate the GPU
-            // cache entry for this picture to ensure that the correct
-            // task rect is provided to the image shader.
-            if self.task_rect != device_rect {
-                frame_state.gpu_cache.invalidate(&self.extra_gpu_data_handle);
-                self.task_rect = device_rect;
-            }
-
-            if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handle) {
-                // [GLSL ImageBrushExtraData: task_rect, offset]
-                request.push(self.task_rect.to_f32());
-                request.push([0.0; 4]);
-
-                // TODO(gw): It would make the shaders a bit simpler if the offset
-                //           was provided as part of the brush::picture instance,
-                //           rather than in the Picture data itself.
-                if let Some(PictureCompositeMode::Filter(FilterOp::DropShadow(offset, _, color))) = self.composite_mode {
+                if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handle) {
                     // TODO(gw): This is very hacky code below! It stores an extra
                     //           brush primitive below for the special case of a
                     //           drop-shadow where we need a different local
                     //           rect for the shadow. To tidy this up in future,
                     //           we could consider abstracting the code in prim_store.rs
                     //           that writes a brush primitive header.
 
-                    // NOTE: If any of the layout below changes, the IMAGE_BRUSH_EXTRA_BLOCKS and
-                    //       IMAGE_BRUSH_BLOCKS fields above *must* be updated.
-
                     // Basic brush primitive header is (see end of prepare_prim_for_render_inner in prim_store.rs)
                     //  local_rect
                     //  clip_rect
                     //  [brush specific data]
                     //  [segment_rect, (repetitions.xy, 0.0, 0.0)]
                     let shadow_rect = prim_metadata.local_rect.translate(&offset);
                     let shadow_clip_rect = prim_metadata.local_clip_rect.translate(&offset);
 
@@ -587,17 +489,180 @@ impl PicturePrimitive {
 
                     // ImageBrush colors
                     request.push(color.premultiplied());
                     request.push(PremultipliedColorF::WHITE);
 
                     // segment rect / repetitions
                     request.push(shadow_rect);
                     request.push([1.0, 1.0, 0.0, 0.0]);
+                }
+            }
+            Some(PictureCompositeMode::MixBlend(..)) => {
+                let uv_rect_kind = calculate_uv_rect_kind(
+                    &prim_metadata.local_rect,
+                    &prim_run_context.scroll_node,
+                    &prim_screen_rect.clipped,
+                    frame_context.device_pixel_scale,
+                );
 
-                    // Now write another GLSL ImageBrush struct, for the shadow to reference.
-                    request.push(self.task_rect.to_f32());
-                    request.push([offset.x, offset.y, 0.0, 0.0]);
+                let picture_task = RenderTask::new_picture(
+                    RenderTaskLocation::Dynamic(None, Some(prim_screen_rect.clipped.size)),
+                    prim_index,
+                    prim_screen_rect.clipped.origin,
+                    pic_state_for_children.tasks,
+                    uv_rect_kind,
+                );
+
+                let readback_task_id = frame_state.render_tasks.add(
+                    RenderTask::new_readback(prim_screen_rect.clipped)
+                );
+
+                self.secondary_render_task_id = Some(readback_task_id);
+                pic_state.tasks.push(readback_task_id);
+
+                let render_task_id = frame_state.render_tasks.add(picture_task);
+                pic_state.tasks.push(render_task_id);
+                self.surface = Some(PictureSurface::RenderTask(render_task_id));
+            }
+            Some(PictureCompositeMode::Filter(filter)) => {
+                if let FilterOp::ColorMatrix(m) = filter {
+                    if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handle) {
+                        for i in 0..5 {
+                            request.push([m[i*4], m[i*4+1], m[i*4+2], m[i*4+3]]);
+                        }
+                    }
                 }
+
+                let uv_rect_kind = calculate_uv_rect_kind(
+                    &prim_metadata.local_rect,
+                    &prim_run_context.scroll_node,
+                    &prim_screen_rect.clipped,
+                    frame_context.device_pixel_scale,
+                );
+
+                let picture_task = RenderTask::new_picture(
+                    RenderTaskLocation::Dynamic(None, Some(prim_screen_rect.clipped.size)),
+                    prim_index,
+                    prim_screen_rect.clipped.origin,
+                    pic_state_for_children.tasks,
+                    uv_rect_kind,
+                );
+
+                let render_task_id = frame_state.render_tasks.add(picture_task);
+                pic_state.tasks.push(render_task_id);
+                self.surface = Some(PictureSurface::RenderTask(render_task_id));
+            }
+            Some(PictureCompositeMode::Blit) | None => {
+                let uv_rect_kind = calculate_uv_rect_kind(
+                    &prim_metadata.local_rect,
+                    &prim_run_context.scroll_node,
+                    &prim_screen_rect.clipped,
+                    frame_context.device_pixel_scale,
+                );
+
+                let picture_task = RenderTask::new_picture(
+                    RenderTaskLocation::Dynamic(None, Some(prim_screen_rect.clipped.size)),
+                    prim_index,
+                    prim_screen_rect.clipped.origin,
+                    pic_state_for_children.tasks,
+                    uv_rect_kind,
+                );
+
+                let render_task_id = frame_state.render_tasks.add(picture_task);
+                pic_state.tasks.push(render_task_id);
+                self.surface = Some(PictureSurface::RenderTask(render_task_id));
             }
         }
     }
+
+    pub fn prepare_for_render(
+        &mut self,
+        prim_index: PrimitiveIndex,
+        prim_metadata: &mut PrimitiveMetadata,
+        prim_run_context: &PrimitiveRunContext,
+        pic_state_for_children: PictureState,
+        pic_state: &mut PictureState,
+        frame_context: &FrameBuildingContext,
+        frame_state: &mut FrameBuildingState,
+    ) {
+        self.prepare_for_render_inner(
+            prim_index,
+            prim_metadata,
+            prim_run_context,
+            pic_state_for_children,
+            pic_state,
+            frame_context,
+            frame_state,
+        );
+    }
 }
+
+// Calculate a single screen-space UV for a picture.
+fn calculate_screen_uv(
+    local_pos: &LayoutPoint,
+    clip_scroll_node: &ClipScrollNode,
+    rendered_rect: &DeviceRect,
+    device_pixel_scale: DevicePixelScale,
+) -> DevicePoint {
+    let world_pos = clip_scroll_node
+        .world_content_transform
+        .transform_point2d(local_pos);
+
+    let mut device_pos = world_pos * device_pixel_scale;
+
+    // Apply snapping for axis-aligned scroll nodes, as per prim_shared.glsl.
+    if clip_scroll_node.transform_kind == TransformedRectKind::AxisAligned {
+        device_pos.x = (device_pos.x + 0.5).floor();
+        device_pos.y = (device_pos.y + 0.5).floor();
+    }
+
+    DevicePoint::new(
+        (device_pos.x - rendered_rect.origin.x) / rendered_rect.size.width,
+        (device_pos.y - rendered_rect.origin.y) / rendered_rect.size.height,
+    )
+}
+
+// Calculate a UV rect within an image based on the screen space
+// vertex positions of a picture.
+fn calculate_uv_rect_kind(
+    local_rect: &LayoutRect,
+    clip_scroll_node: &ClipScrollNode,
+    rendered_rect: &DeviceIntRect,
+    device_pixel_scale: DevicePixelScale,
+) -> UvRectKind {
+    let rendered_rect = rendered_rect.to_f32();
+
+    let top_left = calculate_screen_uv(
+        &local_rect.origin,
+        clip_scroll_node,
+        &rendered_rect,
+        device_pixel_scale,
+    );
+
+    let top_right = calculate_screen_uv(
+        &local_rect.top_right(),
+        clip_scroll_node,
+        &rendered_rect,
+        device_pixel_scale,
+    );
+
+    let bottom_left = calculate_screen_uv(
+        &local_rect.bottom_left(),
+        clip_scroll_node,
+        &rendered_rect,
+        device_pixel_scale,
+    );
+
+    let bottom_right = calculate_screen_uv(
+        &local_rect.bottom_right(),
+        clip_scroll_node,
+        &rendered_rect,
+        device_pixel_scale,
+    );
+
+    UvRectKind::Quad {
+        top_left,
+        top_right,
+        bottom_left,
+        bottom_right,
+    }
+}
--- a/gfx/webrender/src/platform/windows/font.rs
+++ b/gfx/webrender/src/platform/windows/font.rs
@@ -126,17 +126,20 @@ impl FontContext {
     }
 
     pub fn add_native_font(&mut self, font_key: &FontKey, font_handle: dwrote::FontDescriptor) {
         if self.fonts.contains_key(font_key) {
             return;
         }
 
         let system_fc = dwrote::FontCollection::system();
-        let font = system_fc.get_font_from_descriptor(&font_handle).unwrap();
+        let font = match system_fc.get_font_from_descriptor(&font_handle) {
+            Some(font) => font,
+            None => { panic!("missing descriptor {:?}", font_handle) }
+        };
         let face = font.create_font_face();
         self.fonts.insert(*font_key, face);
     }
 
     pub fn delete_font(&mut self, font_key: &FontKey) {
         if let Some(_) = self.fonts.remove(font_key) {
             self.simulations.retain(|k, _| k.0 != *font_key);
         }
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -271,25 +271,27 @@ pub enum BrushKind {
     RadialGradient {
         gradient_index: CachedGradientIndex,
         stops_range: ItemRange<GradientStop>,
         extend_mode: ExtendMode,
         center: LayoutPoint,
         start_radius: f32,
         end_radius: f32,
         ratio_xy: f32,
+        stretch_size: LayoutSize,
     },
     LinearGradient {
         gradient_index: CachedGradientIndex,
         stops_range: ItemRange<GradientStop>,
         stops_count: usize,
         extend_mode: ExtendMode,
         reverse_stops: bool,
         start_point: LayoutPoint,
         end_point: LayoutPoint,
+        stretch_size: LayoutSize,
     }
 }
 
 impl BrushKind {
     fn supports_segments(&self) -> bool {
         match *self {
             BrushKind::Solid { .. } |
             BrushKind::Image { .. } |
@@ -1636,16 +1638,17 @@ impl PrimitiveStore {
                             );
                         }
                     }
                     BrushKind::Picture { pic_index, .. } => {
                         let pic = &mut self.pictures[pic_index.0];
                         pic.prepare_for_render(
                             prim_index,
                             metadata,
+                            prim_run_context,
                             pic_state_for_children,
                             pic_state,
                             frame_context,
                             frame_state,
                         );
                     }
                     BrushKind::Solid { ref color, ref mut opacity_binding, .. } => {
                         // If the opacity changed, invalidate the GPU cache so that
@@ -1679,25 +1682,42 @@ impl PrimitiveStore {
                 }
                 PrimitiveKind::TextRun => {
                     let text = &self.cpu_text_runs[metadata.cpu_prim_index.0];
                     text.write_gpu_blocks(&mut request);
                 }
                 PrimitiveKind::Brush => {
                     let brush = &self.cpu_brushes[metadata.cpu_prim_index.0];
                     brush.write_gpu_blocks(&mut request);
+
+                    let repeat = match brush.kind {
+                        BrushKind::Image { stretch_size, .. } |
+                        BrushKind::LinearGradient { stretch_size, .. } |
+                        BrushKind::RadialGradient { stretch_size, .. } => {
+                            [
+                                metadata.local_rect.size.width / stretch_size.width,
+                                metadata.local_rect.size.height / stretch_size.height,
+                                0.0,
+                                0.0,
+                            ]
+                        }
+                        _ => {
+                            [1.0, 1.0, 0.0, 0.0]
+                        }
+                    };
+
                     match brush.segment_desc {
                         Some(ref segment_desc) => {
                             for segment in &segment_desc.segments {
                                 // has to match VECS_PER_SEGMENT
-                                request.write_segment(segment.local_rect);
+                                request.write_segment(segment.local_rect, repeat);
                             }
                         }
                         None => {
-                            request.write_segment(metadata.local_rect);
+                            request.write_segment(metadata.local_rect, repeat);
                         }
                     }
                 }
             }
         }
     }
 
     fn write_brush_segment_description(
@@ -2457,18 +2477,14 @@ impl<'a> GpuDataRequest<'a> {
     // Write the GPU cache data for an individual segment.
     // TODO(gw): The second block is currently unused. In
     //           the future, it will be used to store a
     //           UV rect, allowing segments to reference
     //           part of an image.
     fn write_segment(
         &mut self,
         local_rect: LayoutRect,
+        extra_params: [f32; 4],
     ) {
         self.push(local_rect);
-        self.push([
-            1.0,
-            1.0,
-            0.0,
-            0.0
-        ]);
+        self.push(extra_params);
     }
 }
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -727,16 +727,19 @@ impl RenderBackend {
                             self.update_document(
                                 document_id,
                                 transaction_msg,
                                 &mut frame_counter,
                                 &mut profile_counters
                             );
                         }
                     },
+                    SceneBuilderResult::FlushComplete(tx) => {
+                        tx.send(()).ok();
+                    }
                     SceneBuilderResult::Stopped => {
                         panic!("We haven't sent a Stop yet, how did we get a Stopped back?");
                     }
                 }
             }
 
             keep_going = match self.api_rx.recv() {
                 Ok(msg) => {
@@ -749,16 +752,23 @@ impl RenderBackend {
             };
         }
 
         let _ = self.scene_tx.send(SceneBuilderRequest::Stop);
         // Ensure we read everything the scene builder is sending us from
         // inflight messages, otherwise the scene builder might panic.
         while let Ok(msg) = self.scene_rx.recv() {
             match msg {
+                SceneBuilderResult::FlushComplete(tx) => {
+                    // If somebody's blocked waiting for a flush, how did they
+                    // trigger the RB thread to shut down? This shouldn't happen
+                    // but handle it gracefully anyway.
+                    debug_assert!(false);
+                    tx.send(()).ok();
+                }
                 SceneBuilderResult::Stopped => break,
                 _ => continue,
             }
         }
 
         self.notifier.shut_down();
 
         if let Some(ref sampler) = self.sampler {
@@ -773,16 +783,19 @@ impl RenderBackend {
         profile_counters: &mut BackendProfileCounters,
         frame_counter: &mut u32,
     ) -> bool {
         match msg {
             ApiMsg::WakeUp => {}
             ApiMsg::WakeSceneBuilder => {
                 self.scene_tx.send(SceneBuilderRequest::WakeUp).unwrap();
             }
+            ApiMsg::FlushSceneBuilder(tx) => {
+                self.scene_tx.send(SceneBuilderRequest::Flush(tx)).unwrap();
+            }
             ApiMsg::UpdateResources(updates) => {
                 self.resource_cache
                     .update_resources(updates, &mut profile_counters.resources);
             }
             ApiMsg::GetGlyphDimensions(instance_key, glyph_keys, tx) => {
                 let mut glyph_dimensions = Vec::with_capacity(glyph_keys.len());
                 if let Some(font) = self.resource_cache.get_font_instance(instance_key) {
                     for glyph_key in &glyph_keys {
--- a/gfx/webrender/src/render_task.rs
+++ b/gfx/webrender/src/render_task.rs
@@ -9,17 +9,17 @@ use box_shadow::{BoxShadowCacheKey};
 use clip::{ClipSource, ClipStore, ClipWorkItem};
 use clip_scroll_tree::CoordinateSystemId;
 use device::TextureFilter;
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
 use glyph_rasterizer::GpuGlyphCacheKey;
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
-use gpu_types::{ImageSource, RasterizationSpace};
+use gpu_types::{ImageSource, RasterizationSpace, UvRectKind};
 use internal_types::{FastHashMap, SavedTargetIndex, SourceTexture};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
 use picture::PictureCacheKey;
 use prim_store::{PrimitiveIndex, ImageCacheKey};
 #[cfg(feature = "debugger")]
 use print_tree::{PrintTreePrinter};
 use render_backend::FrameId;
@@ -191,25 +191,27 @@ pub struct ClipRegionTask {
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PictureTask {
     pub prim_index: PrimitiveIndex,
     pub content_origin: DeviceIntPoint,
     pub uv_rect_handle: GpuCacheHandle,
+    uv_rect_kind: UvRectKind,
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct BlurTask {
     pub blur_std_deviation: f32,
     pub target_kind: RenderTargetKind,
     pub uv_rect_handle: GpuCacheHandle,
+    uv_rect_kind: UvRectKind,
 }
 
 impl BlurTask {
     #[cfg(feature = "debugger")]
     fn print_with<T: PrintTreePrinter>(&self, pt: &mut T) {
         pt.add_item(format!("std deviation: {}", self.blur_std_deviation));
         pt.add_item(format!("target: {:?}", self.target_kind));
     }
@@ -301,24 +303,26 @@ pub struct RenderTask {
 }
 
 impl RenderTask {
     pub fn new_picture(
         location: RenderTaskLocation,
         prim_index: PrimitiveIndex,
         content_origin: DeviceIntPoint,
         children: Vec<RenderTaskId>,
+        uv_rect_kind: UvRectKind,
     ) -> Self {
         RenderTask {
             children,
             location,
             kind: RenderTaskKind::Picture(PictureTask {
                 prim_index,
                 content_origin,
                 uv_rect_handle: GpuCacheHandle::new(),
+                uv_rect_kind,
             }),
             clear_mode: ClearMode::Transparent,
             saved_index: None,
         }
     }
 
     pub fn new_readback(screen_rect: DeviceIntRect) -> Self {
         RenderTask {
@@ -483,17 +487,20 @@ impl RenderTask {
         blur_std_deviation: f32,
         src_task_id: RenderTaskId,
         render_tasks: &mut RenderTaskTree,
         target_kind: RenderTargetKind,
         clear_mode: ClearMode,
     ) -> Self {
         // Adjust large std deviation value.
         let mut adjusted_blur_std_deviation = blur_std_deviation;
-        let blur_target_size = render_tasks[src_task_id].get_dynamic_size();
+        let (blur_target_size, uv_rect_kind) = {
+            let src_task = &render_tasks[src_task_id];
+            (src_task.get_dynamic_size(), src_task.uv_rect_kind())
+        };
         let mut adjusted_blur_target_size = blur_target_size;
         let mut downscaling_src_task_id = src_task_id;
         let mut scale_factor = 1.0;
         while adjusted_blur_std_deviation > MAX_BLUR_STD_DEVIATION {
             if adjusted_blur_target_size.width < MIN_DOWNSCALING_RT_SIZE ||
                adjusted_blur_target_size.height < MIN_DOWNSCALING_RT_SIZE {
                 break;
             }
@@ -510,30 +517,32 @@ impl RenderTask {
 
         let blur_task_v = RenderTask {
             children: vec![downscaling_src_task_id],
             location: RenderTaskLocation::Dynamic(None, Some(adjusted_blur_target_size)),
             kind: RenderTaskKind::VerticalBlur(BlurTask {
                 blur_std_deviation: adjusted_blur_std_deviation,
                 target_kind,
                 uv_rect_handle: GpuCacheHandle::new(),
+                uv_rect_kind,
             }),
             clear_mode,
             saved_index: None,
         };
 
         let blur_task_v_id = render_tasks.add(blur_task_v);
 
         RenderTask {
             children: vec![blur_task_v_id],
             location: RenderTaskLocation::Dynamic(None, Some(adjusted_blur_target_size)),
             kind: RenderTaskKind::HorizontalBlur(BlurTask {
                 blur_std_deviation: adjusted_blur_std_deviation,
                 target_kind,
                 uv_rect_handle: GpuCacheHandle::new(),
+                uv_rect_kind,
             }),
             clear_mode,
             saved_index: None,
         }
     }
 
     pub fn new_scaling(
         target_kind: RenderTargetKind,
@@ -570,16 +579,41 @@ impl RenderTask {
                 render_mode: render_mode,
                 embolden_amount: *embolden_amount,
             }),
             clear_mode: ClearMode::Transparent,
             saved_index: None,
         }
     }
 
+    fn uv_rect_kind(&self) -> UvRectKind {
+        match self.kind {
+            RenderTaskKind::CacheMask(..) |
+            RenderTaskKind::Glyph(_) |
+            RenderTaskKind::Readback(..) |
+            RenderTaskKind::Scaling(..) => {
+                unreachable!("bug: unexpected render task");
+            }
+
+            RenderTaskKind::Picture(ref task) => {
+                task.uv_rect_kind
+            }
+
+            RenderTaskKind::VerticalBlur(ref task) |
+            RenderTaskKind::HorizontalBlur(ref task) => {
+                task.uv_rect_kind
+            }
+
+            RenderTaskKind::ClipRegion(..) |
+            RenderTaskKind::Blit(..) => {
+                UvRectKind::Rect
+            }
+        }
+    }
+
     // Write (up to) 8 floats of data specific to the type
     // of render task that is provided to the GPU shaders
     // via a vertex texture.
     pub fn write_task_data(&self) -> RenderTaskData {
         // NOTE: The ordering and layout of these structures are
         //       required to match both the GPU structures declared
         //       in prim_shared.glsl, and also the uses in submit_batch()
         //       in renderer.rs.
@@ -773,40 +807,44 @@ impl RenderTask {
     }
 
     pub fn write_gpu_blocks(
         &mut self,
         gpu_cache: &mut GpuCache,
     ) {
         let (target_rect, target_index) = self.get_target_rect();
 
-        let cache_handle = match self.kind {
+        let (cache_handle, uv_rect_kind) = match self.kind {
             RenderTaskKind::HorizontalBlur(ref mut info) |
             RenderTaskKind::VerticalBlur(ref mut info) => {
-                &mut info.uv_rect_handle
+                (&mut info.uv_rect_handle, info.uv_rect_kind)
             }
             RenderTaskKind::Picture(ref mut info) => {
-                &mut info.uv_rect_handle
+                (&mut info.uv_rect_handle, info.uv_rect_kind)
             }
             RenderTaskKind::Readback(..) |
             RenderTaskKind::Scaling(..) |
             RenderTaskKind::Blit(..) |
             RenderTaskKind::ClipRegion(..) |
             RenderTaskKind::CacheMask(..) |
             RenderTaskKind::Glyph(..) => {
                 return;
             }
         };
 
         if let Some(mut request) = gpu_cache.request(cache_handle) {
+            let p0 = target_rect.origin.to_f32();
+            let p1 = target_rect.bottom_right().to_f32();
+
             let image_source = ImageSource {
-                p0: target_rect.origin.to_f32(),
-                p1: target_rect.bottom_right().to_f32(),
+                p0,
+                p1,
                 texture_layer: target_index.0 as f32,
                 user_data: [0.0; 3],
+                uv_rect_kind,
             };
             image_source.write_gpu_blocks(&mut request);
         }
     }
 
     #[cfg(feature = "debugger")]
     pub fn print_with<T: PrintTreePrinter>(&self, pt: &mut T, tree: &RenderTaskTree) -> bool {
         match self.kind {
@@ -1007,16 +1045,17 @@ impl RenderTaskCache {
                     &mut entry.handle,
                     descriptor,
                     TextureFilter::Linear,
                     None,
                     entry.user_data.unwrap_or([0.0; 3]),
                     None,
                     gpu_cache,
                     None,
+                    render_task.uv_rect_kind(),
                 );
 
                 // Get the allocation details in the texture cache, and store
                 // this in the render task. The renderer will draw this
                 // task into the appropriate layer and rect of the texture
                 // cache on this frame.
                 let (texture_id, texture_layer, uv_rect) =
                     texture_cache.get_cache_location(&entry.handle);
--- a/gfx/webrender/src/resource_cache.rs
+++ b/gfx/webrender/src/resource_cache.rs
@@ -19,16 +19,17 @@ use capture::PlainExternalImage;
 #[cfg(any(feature = "replay", feature = "png"))]
 use capture::CaptureConfig;
 use device::TextureFilter;
 use glyph_cache::GlyphCache;
 #[cfg(not(feature = "pathfinder"))]
 use glyph_cache::GlyphCacheEntry;
 use glyph_rasterizer::{FontInstance, GlyphFormat, GlyphRasterizer, GlyphRequest};
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
+use gpu_types::UvRectKind;
 use internal_types::{FastHashMap, FastHashSet, SourceTexture, TextureUpdateList};
 use profiler::{ResourceProfileCounters, TextureCacheProfileCounters};
 use render_backend::FrameId;
 use render_task::{RenderTaskCache, RenderTaskCacheKey, RenderTaskId};
 use render_task::{RenderTaskCacheEntry, RenderTaskCacheEntryHandle, RenderTaskTree};
 use std::collections::hash_map::Entry::{self, Occupied, Vacant};
 use std::cmp;
 use std::fmt::Debug;
@@ -1052,16 +1053,17 @@ impl ResourceCache {
                 &mut entry.texture_cache_handle,
                 descriptor,
                 filter,
                 Some(image_data),
                 [0.0; 3],
                 dirty_rect,
                 gpu_cache,
                 None,
+                UvRectKind::Rect,
             );
             image_template.dirty_rect = None;
         }
     }
 
     pub fn end_frame(&mut self) {
         debug_assert_eq!(self.state, State::QueryResources);
         self.state = State::Idle;
--- a/gfx/webrender/src/scene_builder.rs
+++ b/gfx/webrender/src/scene_builder.rs
@@ -20,29 +20,31 @@ pub enum SceneBuilderRequest {
         document_id: DocumentId,
         scene: Option<SceneRequest>,
         resource_updates: ResourceUpdates,
         frame_ops: Vec<FrameMsg>,
         render: bool,
         current_epochs: FastHashMap<PipelineId, Epoch>,
     },
     WakeUp,
+    Flush(MsgSender<()>),
     Stop
 }
 
 // Message from scene builder to render backend.
 pub enum SceneBuilderResult {
     Transaction {
         document_id: DocumentId,
         built_scene: Option<BuiltScene>,
         resource_updates: ResourceUpdates,
         frame_ops: Vec<FrameMsg>,
         render: bool,
         result_tx: Sender<SceneSwapResult>,
     },
+    FlushComplete(MsgSender<()>),
     Stopped,
 }
 
 // Message from render backend to scene builder to indicate the
 // scene swap was completed. We need a separate channel for this
 // so that they don't get mixed with SceneBuilderRequest messages.
 pub enum SceneSwapResult {
     Complete,
@@ -120,16 +122,20 @@ impl SceneBuilder {
         if let Some(ref hooks) = self.hooks {
             hooks.deregister();
         }
     }
 
     fn process_message(&mut self, msg: SceneBuilderRequest) -> bool {
         match msg {
             SceneBuilderRequest::WakeUp => {}
+            SceneBuilderRequest::Flush(tx) => {
+                self.tx.send(SceneBuilderResult::FlushComplete(tx)).unwrap();
+                let _ = self.api_tx.send(ApiMsg::WakeUp);
+            }
             SceneBuilderRequest::Transaction {
                 document_id,
                 scene,
                 resource_updates,
                 frame_ops,
                 render,
                 current_epochs,
             } => {
--- a/gfx/webrender/src/texture_cache.rs
+++ b/gfx/webrender/src/texture_cache.rs
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize};
 use api::{ExternalImageType, ImageData, ImageFormat};
 use api::ImageDescriptor;
 use device::TextureFilter;
 use freelist::{FreeList, FreeListHandle, UpsertResult, WeakFreeListHandle};
 use gpu_cache::{GpuCache, GpuCacheHandle};
-use gpu_types::ImageSource;
+use gpu_types::{ImageSource, UvRectKind};
 use internal_types::{CacheTextureId, FastHashMap, TextureUpdateList, TextureUpdateSource};
 use internal_types::{RenderTargetInfo, SourceTexture, TextureUpdate, TextureUpdateOp};
 use profiler::{ResourceProfileCounter, TextureCacheProfileCounters};
 use render_backend::FrameId;
 use resource_cache::CacheItem;
 use std::cell::Cell;
 use std::cmp;
 use std::mem;
@@ -105,38 +105,42 @@ struct CacheEntry {
     uv_rect_handle: GpuCacheHandle,
     // Image format of the item.
     format: ImageFormat,
     filter: TextureFilter,
     // The actual device texture ID this is part of.
     texture_id: CacheTextureId,
     // Optional notice when the entry is evicted from the cache.
     eviction_notice: Option<EvictionNotice>,
+    // The type of UV rect this entry specifies.
+    uv_rect_kind: UvRectKind,
 }
 
 impl CacheEntry {
     // Create a new entry for a standalone texture.
     fn new_standalone(
         texture_id: CacheTextureId,
         size: DeviceUintSize,
         format: ImageFormat,
         filter: TextureFilter,
         user_data: [f32; 3],
         last_access: FrameId,
+        uv_rect_kind: UvRectKind,
     ) -> Self {
         CacheEntry {
             size,
             user_data,
             last_access,
             kind: EntryKind::Standalone,
             texture_id,
             format,
             filter,
             uv_rect_handle: GpuCacheHandle::new(),
             eviction_notice: None,
+            uv_rect_kind,
         }
     }
 
     // Update the GPU cache for this texture cache entry.
     // This ensures that the UV rect, and texture layer index
     // are up to date in the GPU cache for vertex shaders
     // to fetch from.
     fn update_gpu_cache(&mut self, gpu_cache: &mut GpuCache) {
@@ -149,16 +153,17 @@ impl CacheEntry {
                     ..
                 } => (origin, layer_index as f32),
             };
             let image_source = ImageSource {
                 p0: origin.to_f32(),
                 p1: (origin + self.size).to_f32(),
                 texture_layer: layer_index,
                 user_data: self.user_data,
+                uv_rect_kind: self.uv_rect_kind,
             };
             image_source.write_gpu_blocks(&mut request);
         }
     }
 
     fn evict(&self) {
         if let Some(eviction_notice) = self.eviction_notice.as_ref() {
             eviction_notice.notify();
@@ -389,16 +394,17 @@ impl TextureCache {
         handle: &mut TextureCacheHandle,
         descriptor: ImageDescriptor,
         filter: TextureFilter,
         data: Option<ImageData>,
         user_data: [f32; 3],
         mut dirty_rect: Option<DeviceUintRect>,
         gpu_cache: &mut GpuCache,
         eviction_notice: Option<&EvictionNotice>,
+        uv_rect_kind: UvRectKind,
     ) {
         // Determine if we need to allocate texture cache memory
         // for this item. We need to reallocate if any of the following
         // is true:
         // - Never been in the cache
         // - Has been in the cache but was evicted.
         // - Exists in the cache but dimensions / format have changed.
         let realloc = match handle.entry {
@@ -417,17 +423,23 @@ impl TextureCache {
             }
             None => {
                 // This handle has not been allocated yet.
                 true
             }
         };
 
         if realloc {
-            self.allocate(handle, descriptor, filter, user_data);
+            self.allocate(
+                handle,
+                descriptor,
+                filter,
+                user_data,
+                uv_rect_kind,
+            );
 
             // If we reallocated, we need to upload the whole item again.
             dirty_rect = None;
         }
 
         let entry = self.entries
             .get_opt_mut(handle.entry.as_ref().unwrap())
             .expect("BUG: handle must be valid now");
@@ -634,17 +646,17 @@ impl TextureCache {
         // more items being uploaded than necessary.
         // Instead, we say we will keep evicting until both of these
         // conditions are met:
         // - We have evicted some arbitrary number of items (512 currently).
         //   AND
         // - We have freed an item that will definitely allow us to
         //   fit the currently requested allocation.
         let needed_slab_size =
-            SlabSize::new(required_alloc.width, required_alloc.height).get_size();
+            SlabSize::new(required_alloc.width, required_alloc.height);
         let mut found_matching_slab = false;
         let mut freed_complete_page = false;
         let mut evicted_items = 0;
 
         for handle in eviction_candidates {
             if evicted_items > 512 && (found_matching_slab || freed_complete_page) {
                 retained_entries.push(handle);
             } else {
@@ -693,16 +705,17 @@ impl TextureCache {
     }
 
     // Attempt to allocate a block from the shared cache.
     fn allocate_from_shared_cache(
         &mut self,
         descriptor: &ImageDescriptor,
         filter: TextureFilter,
         user_data: [f32; 3],
+        uv_rect_kind: UvRectKind,
     ) -> Option<CacheEntry> {
         // Work out which cache it goes in, based on format.
         let texture_array = match (descriptor.format, filter) {
             (ImageFormat::R8, TextureFilter::Linear) => &mut self.array_a8_linear,
             (ImageFormat::BGRA8, TextureFilter::Linear) => &mut self.array_rgba8_linear,
             (ImageFormat::BGRA8, TextureFilter::Nearest) => &mut self.array_rgba8_nearest,
             (ImageFormat::RGBAF32, _) |
             (ImageFormat::R8, TextureFilter::Nearest) |
@@ -740,16 +753,17 @@ impl TextureCache {
 
         // Do the allocation. This can fail and return None
         // if there are no free slots or regions available.
         texture_array.alloc(
             descriptor.width,
             descriptor.height,
             user_data,
             self.frame_id,
+            uv_rect_kind,
         )
     }
 
     // Returns true if the given image descriptor *may* be
     // placed in the shared texture cache.
     pub fn is_allowed_in_shared_cache(
         &self,
         filter: TextureFilter,
@@ -760,36 +774,38 @@ impl TextureCache {
         // TODO(gw): For now, anything that requests nearest filtering and isn't BGRA8
         //           just fails to allocate in a texture page, and gets a standalone
         //           texture. This is probably rare enough that it can be fixed up later.
         if filter == TextureFilter::Nearest &&
            descriptor.format != ImageFormat::BGRA8 {
             allowed_in_shared_cache = false;
         }
 
-        // Anything larger than 512 goes in a standalone texture.
+        // Anything larger than TEXTURE_REGION_DIMENSIONS goes in a standalone texture.
         // TODO(gw): If we find pages that suffer from batch breaks in this
         //           case, add support for storing these in a standalone
         //           texture array.
-        if descriptor.width > 512 || descriptor.height > 512 {
+        if descriptor.width > TEXTURE_REGION_DIMENSIONS ||
+           descriptor.height > TEXTURE_REGION_DIMENSIONS {
             allowed_in_shared_cache = false;
         }
 
         allowed_in_shared_cache
     }
 
     // Allocate storage for a given image. This attempts to allocate
     // from the shared cache, but falls back to standalone texture
     // if the image is too large, or the cache is full.
     fn allocate(
         &mut self,
         handle: &mut TextureCacheHandle,
         descriptor: ImageDescriptor,
         filter: TextureFilter,
         user_data: [f32; 3],
+        uv_rect_kind: UvRectKind,
     ) {
         assert!(descriptor.width > 0 && descriptor.height > 0);
 
         // Work out if this image qualifies to go in the shared (batching) cache.
         let allowed_in_shared_cache = self.is_allowed_in_shared_cache(
             filter,
             &descriptor,
         );
@@ -798,28 +814,30 @@ impl TextureCache {
         let size = DeviceUintSize::new(descriptor.width, descriptor.height);
         let frame_id = self.frame_id;
 
         // If it's allowed in the cache, see if there is a spot for it.
         if allowed_in_shared_cache {
             new_cache_entry = self.allocate_from_shared_cache(
                 &descriptor,
                 filter,
-                user_data
+                user_data,
+                uv_rect_kind,
             );
 
             // If we failed to allocate in the shared cache, run an
             // eviction cycle, and then try to allocate again.
             if new_cache_entry.is_none() {
                 self.expire_old_shared_entries(&descriptor);
 
                 new_cache_entry = self.allocate_from_shared_cache(
                     &descriptor,
                     filter,
-                    user_data
+                    user_data,
+                    uv_rect_kind,
                 );
             }
         }
 
         // If not allowed in the cache, or if the shared cache is full, then it
         // will just have to be in a unique texture. This hurts batching but should
         // only occur on a small number of images (or pathological test cases!).
         if new_cache_entry.is_none() {
@@ -842,16 +860,17 @@ impl TextureCache {
 
             new_cache_entry = Some(CacheEntry::new_standalone(
                 texture_id,
                 size,
                 descriptor.format,
                 filter,
                 user_data,
                 frame_id,
+                uv_rect_kind,
             ));
 
             allocated_in_shared_cache = false;
         }
 
         let new_cache_entry = new_cache_entry.expect("BUG: must have allocated by now");
 
         // We need to update the texture cache handle now, so that it
@@ -895,53 +914,58 @@ impl TextureCache {
                 self.shared_entry_handles.push(new_entry_handle);
             } else {
                 self.standalone_entry_handles.push(new_entry_handle);
             }
         }
     }
 }
 
-// A list of the block sizes that a region can be initialized with.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Copy, Clone, PartialEq)]
-enum SlabSize {
-    Size16x16,
-    Size32x32,
-    Size64x64,
-    Size128x128,
-    Size256x256,
-    Size512x512,
+struct SlabSize {
+    width: u32,
+    height: u32,
 }
 
 impl SlabSize {
     fn new(width: u32, height: u32) -> SlabSize {
-        // TODO(gw): Consider supporting non-square
-        //           allocator sizes here.
-        let max_dim = cmp::max(width, height);
+        let x_size = quantize_dimension(width);
+        let y_size = quantize_dimension(height);
+
+        assert!(x_size > 0 && x_size <= TEXTURE_REGION_DIMENSIONS);
+        assert!(y_size > 0 && y_size <= TEXTURE_REGION_DIMENSIONS);
 
-        match max_dim {
-            0 => unreachable!(),
-            1...16 => SlabSize::Size16x16,
-            17...32 => SlabSize::Size32x32,
-            33...64 => SlabSize::Size64x64,
-            65...128 => SlabSize::Size128x128,
-            129...256 => SlabSize::Size256x256,
-            257...512 => SlabSize::Size512x512,
-            _ => panic!("Invalid dimensions for cache!"),
+        let (width, height) = match (x_size, y_size) {
+            // Special cased rectangular slab pages.
+            (512, 256) => (512, 256),
+            (512, 128) => (512, 128),
+            (512,  64) => (512,  64),
+            (256, 512) => (256, 512),
+            (128, 512) => (128, 512),
+            ( 64, 512) => ( 64, 512),
+
+            // If none of those fit, use a square slab size.
+            (x_size, y_size) => {
+                let square_size = cmp::max(x_size, y_size);
+                (square_size, square_size)
+            }
+        };
+
+        SlabSize {
+            width,
+            height,
         }
     }
 
-    fn get_size(&self) -> u32 {
-        match *self {
-            SlabSize::Size16x16 => 16,
-            SlabSize::Size32x32 => 32,
-            SlabSize::Size64x64 => 64,
-            SlabSize::Size128x128 => 128,
-            SlabSize::Size256x256 => 256,
-            SlabSize::Size512x512 => 512,
+    fn invalid() -> SlabSize {
+        SlabSize {
+            width: 0,
+            height: 0,
         }
     }
 }
 
 // The x/y location within a texture region of an allocation.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct TextureLocation(u8, u8);
@@ -955,81 +979,81 @@ impl TextureLocation {
 
 // A region is a sub-rect of a texture array layer.
 // All allocations within a region are of the same size.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct TextureRegion {
     layer_index: i32,
     region_size: u32,
-    slab_size: u32,
+    slab_size: SlabSize,
     free_slots: Vec<TextureLocation>,
-    slots_per_axis: u32,
     total_slot_count: usize,
     origin: DeviceUintPoint,
 }
 
 impl TextureRegion {
     fn new(region_size: u32, layer_index: i32, origin: DeviceUintPoint) -> Self {
         TextureRegion {
             layer_index,
             region_size,
-            slab_size: 0,
+            slab_size: SlabSize::invalid(),
             free_slots: Vec::new(),
-            slots_per_axis: 0,
             total_slot_count: 0,
             origin,
         }
     }
 
     // Initialize a region to be an allocator for a specific slab size.
     fn init(&mut self, slab_size: SlabSize) {
-        debug_assert!(self.slab_size == 0);
+        debug_assert!(self.slab_size == SlabSize::invalid());
         debug_assert!(self.free_slots.is_empty());
 
-        self.slab_size = slab_size.get_size();
-        self.slots_per_axis = self.region_size / self.slab_size;
+        self.slab_size = slab_size;
+        let slots_per_x_axis = self.region_size / self.slab_size.width;
+        let slots_per_y_axis = self.region_size / self.slab_size.height;
 
         // Add each block to a freelist.
-        for y in 0 .. self.slots_per_axis {
-            for x in 0 .. self.slots_per_axis {
+        for y in 0 .. slots_per_y_axis {
+            for x in 0 .. slots_per_x_axis {
                 self.free_slots.push(TextureLocation::new(x, y));
             }
         }
 
         self.total_slot_count = self.free_slots.len();
     }
 
     // Deinit a region, allowing it to become a region with
     // a different allocator size.
     fn deinit(&mut self) {
-        self.slab_size = 0;
+        self.slab_size = SlabSize::invalid();
         self.free_slots.clear();
-        self.slots_per_axis = 0;
         self.total_slot_count = 0;
     }
 
     fn is_empty(&self) -> bool {
-        self.slab_size == 0
+        self.slab_size == SlabSize::invalid()
     }
 
     // Attempt to allocate a fixed size block from this region.
     fn alloc(&mut self) -> Option<DeviceUintPoint> {
+        debug_assert!(self.slab_size != SlabSize::invalid());
+
         self.free_slots.pop().map(|location| {
             DeviceUintPoint::new(
-                self.origin.x + self.slab_size * location.0 as u32,
-                self.origin.y + self.slab_size * location.1 as u32,
+                self.origin.x + self.slab_size.width * location.0 as u32,
+                self.origin.y + self.slab_size.height * location.1 as u32,
             )
         })
     }
 
     // Free a block in this region.
     fn free(&mut self, point: DeviceUintPoint) {
-        let x = (point.x - self.origin.x) / self.slab_size;
-        let y = (point.y - self.origin.y) / self.slab_size;
+        let x = (point.x - self.origin.x) / self.slab_size.width;
+        let y = (point.y - self.origin.y) / self.slab_size.height;
         self.free_slots.push(TextureLocation::new(x, y));
 
         // If this region is completely unused, deinit it
         // so that it can become a different slab size
         // as required.
         if self.free_slots.len() == self.total_slot_count {
             self.deinit();
         }
@@ -1084,16 +1108,17 @@ impl TextureArray {
 
     // Allocate space in this texture array.
     fn alloc(
         &mut self,
         width: u32,
         height: u32,
         user_data: [f32; 3],
         frame_id: FrameId,
+        uv_rect_kind: UvRectKind,
     ) -> Option<CacheEntry> {
         // Lazily allocate the regions if not already created.
         // This means that very rarely used image formats can be
         // added but won't allocate a cache if never used.
         if !self.is_allocated {
             debug_assert!(TEXTURE_LAYER_DIMENSIONS % TEXTURE_REGION_DIMENSIONS == 0);
             let regions_per_axis = TEXTURE_LAYER_DIMENSIONS / TEXTURE_REGION_DIMENSIONS;
             for layer_index in 0 .. self.layer_count {
@@ -1113,35 +1138,34 @@ impl TextureArray {
                 }
             }
             self.is_allocated = true;
         }
 
         // Quantize the size of the allocation to select a region to
         // allocate from.
         let slab_size = SlabSize::new(width, height);
-        let slab_size_dim = slab_size.get_size();
 
         // TODO(gw): For simplicity, the initial implementation just
         //           has a single vec<> of regions. We could easily
         //           make this more efficient by storing a list of
         //           regions for each slab size specifically...
 
         // Keep track of the location of an empty region,
         // in case we need to select a new empty region
         // after the loop.
         let mut empty_region_index = None;
         let mut entry_kind = None;
 
         // Run through the existing regions of this size, and see if
         // we can find a free block in any of them.
         for (i, region) in self.regions.iter_mut().enumerate() {
-            if region.slab_size == 0 {
+            if region.is_empty() {
                 empty_region_index = Some(i);
-            } else if region.slab_size == slab_size_dim {
+            } else if region.slab_size == slab_size {
                 if let Some(location) = region.alloc() {
                     entry_kind = Some(EntryKind::Cache {
                         layer_index: region.layer_index as u16,
                         region_index: i as u16,
                         origin: location,
                     });
                     break;
                 }
@@ -1169,16 +1193,17 @@ impl TextureArray {
                 user_data,
                 last_access: frame_id,
                 kind,
                 uv_rect_handle: GpuCacheHandle::new(),
                 format: self.format,
                 filter: self.filter,
                 texture_id: self.texture_id.unwrap(),
                 eviction_notice: None,
+                uv_rect_kind,
             }
         })
     }
 }
 
 impl TextureUpdate {
     // Constructs a TextureUpdate operation to be passed to the
     // rendering thread in order to do an upload to the right
@@ -1239,8 +1264,21 @@ impl TextureUpdate {
         };
 
         TextureUpdate {
             id: texture_id,
             op: update_op,
         }
     }
 }
+
+fn quantize_dimension(size: u32) -> u32 {
+    match size {
+        0 => unreachable!(),
+        1...16 => 16,
+        17...32 => 32,
+        33...64 => 64,
+        65...128 => 128,
+        129...256 => 256,
+        257...512 => 512,
+        _ => panic!("Invalid dimensions for cache!"),
+    }
+}
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -628,16 +628,17 @@ pub enum ApiMsg {
     /// Flush from the caches anything that isn't necessary, to free some memory.
     MemoryPressure,
     /// Change debugging options.
     DebugCommand(DebugCommand),
     /// Wakes the render backend's event loop up. Needed when an event is communicated
     /// through another channel.
     WakeUp,
     WakeSceneBuilder,
+    FlushSceneBuilder(MsgSender<()>),
     ShutDown,
 }
 
 impl fmt::Debug for ApiMsg {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         f.write_str(match *self {
             ApiMsg::UpdateResources(..) => "ApiMsg::UpdateResources",
             ApiMsg::GetGlyphDimensions(..) => "ApiMsg::GetGlyphDimensions",
@@ -648,16 +649,17 @@ impl fmt::Debug for ApiMsg {
             ApiMsg::DeleteDocument(..) => "ApiMsg::DeleteDocument",
             ApiMsg::ExternalEvent(..) => "ApiMsg::ExternalEvent",
             ApiMsg::ClearNamespace(..) => "ApiMsg::ClearNamespace",
             ApiMsg::MemoryPressure => "ApiMsg::MemoryPressure",
             ApiMsg::DebugCommand(..) => "ApiMsg::DebugCommand",
             ApiMsg::ShutDown => "ApiMsg::ShutDown",
             ApiMsg::WakeUp => "ApiMsg::WakeUp",
             ApiMsg::WakeSceneBuilder => "ApiMsg::WakeSceneBuilder",
+            ApiMsg::FlushSceneBuilder(..) => "ApiMsg::FlushSceneBuilder",
         })
     }
 }
 
 #[repr(C)]
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
 pub struct Epoch(pub u32);
 
@@ -960,16 +962,25 @@ impl RenderApi {
         self.send_frame_msg(document_id, FrameMsg::GetScrollNodeState(tx));
         rx.recv().unwrap()
     }
 
     pub fn wake_scene_builder(&self) {
         self.send_message(ApiMsg::WakeSceneBuilder);
     }
 
+    /// Block until a round-trip to the scene builder thread has completed. This
+    /// ensures that any transactions (including ones deferred to the scene
+    /// builder thread) have been processed.
+    pub fn flush_scene_builder(&self) {
+        let (tx, rx) = channel::msg_channel().unwrap();
+        self.send_message(ApiMsg::FlushSceneBuilder(tx));
+        rx.recv().unwrap(); // block until done
+    }
+
     /// Save a capture of the current frame state for debugging.
     pub fn save_capture(&self, path: PathBuf, bits: CaptureBits) {
         let msg = ApiMsg::DebugCommand(DebugCommand::SaveCapture(path, bits));
         self.send_message(msg);
     }
 
     /// Load a capture of the current frame state for debugging.
     pub fn load_capture(&self, path: PathBuf) -> Vec<CapturedDocument> {
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -242,17 +242,17 @@ RenderThread::RunEvent(wr::WindowId aWin
 
 static void
 NotifyDidRender(layers::CompositorBridgeParentBase* aBridge,
                 wr::WrPipelineInfo aInfo,
                 TimeStamp aStart,
                 TimeStamp aEnd)
 {
   for (uintptr_t i = 0; i < aInfo.epochs.length; i++) {
-    aBridge->NotifyDidCompositeToPipeline(
+    aBridge->NotifyPipelineRendered(
         aInfo.epochs.data[i].pipeline_id,
         aInfo.epochs.data[i].epoch,
         aStart,
         aEnd);
   }
   for (uintptr_t i = 0; i < aInfo.removed_pipelines.length; i++) {
     aBridge->NotifyPipelineRemoved(aInfo.removed_pipelines.data[i]);
   }
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-751236199b39bb8dac78522713133ca18c603fb3
+4b65822a2f7e1fed246a492f9fe193ede2f37d74
--- a/layout/reftests/backgrounds/gradient/reftest.list
+++ b/layout/reftests/backgrounds/gradient/reftest.list
@@ -1,3 +1,3 @@
 == scaled-color-stop-position.html scaled-color-stop-position-ref.html
 == color-stop-clamp-interpolation.html color-stop-clamp-interpolation-ref.html
-fuzzy-if(webrender&&winWidget,2-2,72-72) == linear-gradient-repeated.html linear-gradient-repeated-ref.html
+== linear-gradient-repeated.html linear-gradient-repeated-ref.html
--- a/layout/reftests/backgrounds/reftest.list
+++ b/layout/reftests/backgrounds/reftest.list
@@ -167,17 +167,17 @@ fuzzy(50,500) fuzzy-if(skiaContent,51,32
 == attachment-local-clipping-image-2.html attachment-local-clipping-image-1-ref.html  # Same ref as the previous test.
 == attachment-local-clipping-image-3.html attachment-local-clipping-image-3-ref.html
 # The next three tests are fuzzy due to bug 1128229.
 fuzzy(16,69) fuzzy-if(skiaContent,95,2206) == attachment-local-clipping-image-4.html attachment-local-clipping-image-4-ref.html
 fuzzy(16,69) fuzzy-if(skiaContent,95,2206) == attachment-local-clipping-image-5.html attachment-local-clipping-image-4-ref.html
 fuzzy(80,500) fuzzy-if(skiaContent,109,908) == attachment-local-clipping-image-6.html attachment-local-clipping-image-6-ref.html
 
 fuzzy-if(skiaContent,1,8) fuzzy-if(webrender,1,84) == background-multiple-with-border-radius.html background-multiple-with-border-radius-ref.html
-== background-repeat-large-area.html background-repeat-large-area-ref.html
+fuzzy-if(webrender&&winWidget,73-73,49600-49600) == background-repeat-large-area.html background-repeat-large-area-ref.html
 
 fuzzy(30,474) fuzzy-if(skiaContent,31,474) == background-tiling-zoom-1.html background-tiling-zoom-1-ref.html
 
 skip-if(!cocoaWidget) == background-repeat-resampling.html background-repeat-resampling-ref.html
 
 fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) fuzzy-if(webrender&&winWidget,153-153,2866-2866) == background-clip-text-1a.html background-clip-text-1-ref.html
 fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) fuzzy-if(webrender&&winWidget,153-153,2866-2866) == background-clip-text-1b.html background-clip-text-1-ref.html
 fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) fuzzy-if(webrender&&winWidget,153-153,2866-2866) == background-clip-text-1c.html background-clip-text-1-ref.html
--- a/layout/reftests/bugs/reftest.list
+++ b/layout/reftests/bugs/reftest.list
@@ -300,17 +300,17 @@ fuzzy-if(Android,3,50) fuzzy-if(skiaCont
 == 280708-1a.html 280708-1-ref.html
 == 280708-1b.html 280708-1-ref.html
 == 281241-1.html 281241-1-ref.html
 == 281241-2.xhtml 281241-1-ref.html
 == 283686-1.html about:blank
 == 283686-2.html 283686-2-ref.html
 == 283686-3.html about:blank
 == 289384-1.xhtml 289384-ref.xhtml
-random-if(d2d) fuzzy-if(Android,8,1439) HTTP == 289480.html#top 289480-ref.html # basically-verbatim acid2 test, HTTP for a 404 page -- bug 578114 for the d2d failures
+fails-if(webrender) random-if(d2d) fuzzy-if(Android,8,1439) HTTP == 289480.html#top 289480-ref.html # basically-verbatim acid2 test, HTTP for a 404 page -- bug 578114 for the d2d failures
 == 290129-1.html 290129-1-ref.html
 == 291078-1.html 291078-1-ref.html
 == 291078-2.html 291078-2-ref.html
 == 291262-1.html 291262-1-ref.html
 == 294306-1.html 294306-1a-ref.html
 != 294306-1.html 294306-1b-ref.html
 == 296361-1.html 296361-ref.html
 == 296904-1.html 296904-1-ref.html
@@ -1386,17 +1386,17 @@ pref(dom.use_xbl_scopes_for_remote_xul,t
 == 495385-5.html 495385-5-ref.html
 == 496032-1.html 496032-1-ref.html
 == 496840-1.html 496840-1-ref.html
 fuzzy-if(skiaContent,1,17000) == 498228-1.xul 498228-1-ref.xul
 == 501037.html 501037-ref.html
 == 501257-1a.html 501257-1-ref.html
 == 501257-1b.html 501257-1-ref.html
 == 501257-1.xhtml 501257-1-ref.xhtml
-== 501627-1.html 501627-1-ref.html
+fuzzy-if(webrender&&winWidget,5-5,83252-83252) == 501627-1.html 501627-1-ref.html
 == 502288-1.html 502288-1-ref.html
 fuzzy-if(gtkWidget,1,2) == 502447-1.html 502447-1-ref.html #Bug 1315834
 == 502795-1.html 502795-1-ref.html
 == 502942-1.html 502942-1-ref.html
 == 503364-1a.html 503364-1-ref.html
 == 503364-1b.html 503364-1-ref.html
 # Reftest for bug 503531 marked as failing; should be re-enabled when
 # bug 607548 gets resolved.
@@ -1975,17 +1975,17 @@ fuzzy-if(webrender,0-2,0-2601) == 124217
 random-if(!winWidget) == 1273154-1.html 1273154-1-ref.html # depends on Windows font
 random-if(!winWidget) == 1273154-2.html 1273154-2-ref.html # depends on Windows font
 == 1274368-1.html 1274368-1-ref.html
 != 1276161-1a.html 1276161-1-notref.html
 != 1276161-1b.html 1276161-1-notref.html
 != 1276161-1a.html 1276161-1b.html
 == 1275411-1.html 1275411-1-ref.html
 == 1288255.html 1288255-ref.html
-fuzzy(8,1900) == 1291528.html 1291528-ref.html
+fuzzy(8,1900) fails-if(webrender) == 1291528.html 1291528-ref.html
 # Buttons in 2 pages have different position and the rendering result can be
 # different, but they should use the same button style and the background color
 # should be same.  |fuzzy()| here allows the difference in border, but not
 # background color.
 fuzzy(255,1000) skip-if(!cocoaWidget) == 1294102-1.html 1294102-1-ref.html
 random-if(Android) fuzzy-if(skiaContent,15,50) == 1295466-1.xhtml 1295466-1-ref.xhtml #bug 982547
 fuzzy-if(Android,27,874) fuzzy-if(!Android,14,43) == 1313772.xhtml 1313772-ref.xhtml # Bug 1128229, Bug 1389319
 fuzzy(2,320000) == 1315113-1.html 1315113-1-ref.html
--- a/layout/reftests/css-gradients/reftest.list
+++ b/layout/reftests/css-gradients/reftest.list
@@ -41,18 +41,18 @@ fuzzy-if(cocoaWidget,1,28) fuzzy-if(winW
 fuzzy-if(cocoaWidget,4,22317) fuzzy-if(Android,8,771) == radial-shape-closest-corner-1a.html radial-shape-closest-corner-1-ref.html
 fuzzy(1,238) fuzzy-if(cocoaWidget,4,22608) fuzzy-if((/^Windows\x20NT\x2010\.0/.test(http.oscpu)||/^Windows\x20NT\x206\./.test(http.oscpu))&&d2d,1,336) fuzzy-if(Android,8,787) fuzzy-if(skiaContent,2,300) == radial-shape-closest-corner-1b.html radial-shape-closest-corner-1-ref.html
 fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)||/^Windows\x20NT\x206\.2/.test(http.oscpu),1,5) fuzzy-if(Android,17,3880) == radial-shape-closest-side-1a.html radial-shape-closest-side-1-ref.html
 fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)||/^Windows\x20NT\x206\.2/.test(http.oscpu),1,5) fuzzy-if(Android,17,3880) == radial-shape-closest-side-1b.html radial-shape-closest-side-1-ref.html
 fuzzy-if(Android,8,771) == radial-shape-farthest-corner-1a.html radial-shape-farthest-corner-1-ref.html
 fails-if(gtkWidget&&/x86_64-/.test(xulRuntime.XPCOMABI)) fuzzy(1,1622) fuzzy-if(cocoaWidget,2,41281) fuzzy-if(Android,8,1091) fuzzy-if(skiaContent,2,500) == radial-shape-farthest-corner-1b.html radial-shape-farthest-corner-1-ref.html
 fuzzy-if(Android,17,13320) == radial-shape-farthest-side-1a.html radial-shape-farthest-side-1-ref.html
 fuzzy-if(Android,17,13320) == radial-shape-farthest-side-1b.html radial-shape-farthest-side-1-ref.html
-== radial-size-1a.html radial-size-1-ref.html
-== radial-size-1b.html radial-size-1-ref.html
+fuzzy-if(webrender,1-2,4-9) == radial-size-1a.html radial-size-1-ref.html
+fuzzy-if(webrender,1-2,4-9) == radial-size-1b.html radial-size-1-ref.html
 fuzzy-if(Android,4,248) == radial-zero-length-1a.html radial-zero-length-1-ref.html
 fuzzy-if(Android,4,248) == radial-zero-length-1b.html radial-zero-length-1-ref.html
 fuzzy-if(Android,4,248) == radial-zero-length-1c.html radial-zero-length-1-ref.html
 fuzzy-if(Android,4,248) == radial-zero-length-1d.html radial-zero-length-1-ref.html
 fuzzy-if(Android,4,248) == radial-zero-length-1e.html radial-zero-length-1-ref.html
 fuzzy-if(Android,4,248) == radial-zero-length-1f.html radial-zero-length-1-ref.html
 == radial-premul.html radial-premul-ref.html
 == repeated-final-stop-1.html repeated-final-stop-1-ref.html
--- a/testing/mozharness/mozharness/mozilla/testing/codecoverage.py
+++ b/testing/mozharness/mozharness/mozilla/testing/codecoverage.py
@@ -284,16 +284,20 @@ class CodeCoverageMixin(SingleTestMixin)
 
             self.info("Completed compression of JSDCov artifacts!")
             self.info("Path to JSDCov compressed artifacts: " + zipFile)
 
         if not self.code_coverage_enabled:
             return
 
         if self.per_test_coverage:
+            if not self.per_test_reports:
+                self.info("No tests were found...not saving coverage data.")
+                return
+
             dest = os.path.join(dirs['abs_blob_upload_dir'], 'per-test-coverage-reports.zip')
             with zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) as z:
                 for suite, data in self.per_test_reports.items():
                     for test, grcov_file in data.items():
                         with open(grcov_file, 'r') as f:
                             report = json.load(f)
 
                         # TODO: Diff this coverage report with the baseline one.
--- a/toolkit/components/normandy/actions/PreferenceRollbackAction.jsm
+++ b/toolkit/components/normandy/actions/PreferenceRollbackAction.jsm
@@ -19,17 +19,17 @@ class PreferenceRollbackAction extends B
     return ActionSchemas["preference-rollback"];
   }
 
   async _run(recipe) {
     const {rolloutSlug} = recipe.arguments;
     const rollout = await PreferenceRollouts.get(rolloutSlug);
 
     if (!rollout) {
-      TelemetryEvents.sendEvent("unenrollFailure", "preference_rollout", rolloutSlug, {"reason": "rollout missing"});
+      TelemetryEvents.sendEvent("unenrollFailed", "preference_rollout", rolloutSlug, {"reason": "rollout missing"});
       this.log.info(`Cannot rollback ${rolloutSlug}: no rollout found with that slug`);
       return;
     }
 
     switch (rollout.state) {
       case PreferenceRollouts.STATE_ACTIVE: {
         this.log.info(`Rolling back ${rolloutSlug}`);
         rollout.state = PreferenceRollouts.STATE_ROLLED_BACK;
@@ -41,17 +41,17 @@ class PreferenceRollbackAction extends B
         TelemetryEnvironment.setExperimentInactive(rolloutSlug);
       }
       case PreferenceRollouts.STATE_ROLLED_BACK: {
         // The rollout has already been rolled back, so nothing to do here.
         break;
       }
       case PreferenceRollouts.STATE_GRADUATED: {
         // graduated rollouts can't be rolled back
-        TelemetryEvents.sendEvent("unenrollFailure", "preference_rollout", rolloutSlug, {"reason": "graduated"});
+        TelemetryEvents.sendEvent("unenrollFailed", "preference_rollout", rolloutSlug, {"reason": "graduated"});
         throw new Error(`Cannot rollback already graduated rollout ${rolloutSlug}`);
       }
       default: {
         throw new Error(`Unexpected state when rolling back ${rolloutSlug}: ${rollout.state}`);
       }
     }
   }
 
--- a/toolkit/components/normandy/actions/PreferenceRolloutAction.jsm
+++ b/toolkit/components/normandy/actions/PreferenceRolloutAction.jsm
@@ -108,17 +108,17 @@ class PreferenceRolloutAction extends Ba
       const existingPrefType = Services.prefs.getPrefType(prefSpec.preferenceName);
       const rolloutPrefType = PREFERENCE_TYPE_MAP[typeof prefSpec.value];
 
       if (existingPrefType !== Services.prefs.PREF_INVALID && existingPrefType !== rolloutPrefType) {
         TelemetryEvents.sendEvent(
           "enrollFailed",
           "preference_rollout",
           slug,
-          {reason: "invalid type", pref: prefSpec.preferenceName},
+          {reason: "invalid type", preference: prefSpec.preferenceName},
         );
         throw new Error(
           `Cannot start rollout "${slug}" on "${prefSpec.preferenceName}". ` +
           `Existing preference is of type ${existingPrefType}, but rollout ` +
           `specifies type ${rolloutPrefType}`
         );
       }
     }
--- a/toolkit/components/normandy/lib/TelemetryEvents.jsm
+++ b/toolkit/components/normandy/lib/TelemetryEvents.jsm
@@ -6,58 +6,60 @@
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var EXPORTED_SYMBOLS = ["TelemetryEvents"];
 
 const TELEMETRY_CATEGORY = "normandy";
 
 const TelemetryEvents = {
-  init() {
-    Services.telemetry.registerEvents(TELEMETRY_CATEGORY, {
-      enroll: {
-        methods: ["enroll"],
-        objects: ["preference_study", "addon_study", "preference_rollout"],
-        extra_keys: ["experimentType", "branch", "addonId", "addonVersion"],
-        record_on_release: true,
-      },
+  eventSchema: {
+    enroll: {
+      methods: ["enroll"],
+      objects: ["preference_study", "addon_study", "preference_rollout"],
+      extra_keys: ["experimentType", "branch", "addonId", "addonVersion"],
+      record_on_release: true,
+    },
 
-      enroll_failure: {
-        methods: ["enrollFailed"],
-        objects: ["addon_study", "preference_rollout"],
-        extra_keys: ["reason", "preference"],
-        record_on_release: true,
-      },
+    enroll_failed: {
+      methods: ["enrollFailed"],
+      objects: ["addon_study", "preference_rollout"],
+      extra_keys: ["reason", "preference"],
+      record_on_release: true,
+    },
+
+    update: {
+      methods: ["update"],
+      objects: ["preference_rollout"],
+      extra_keys: ["previousState"],
+      record_on_release: true,
+    },
 
-      update: {
-        methods: ["update"],
-        objects: ["preference_rollout"],
-        extra_keys: ["previousState"],
-        record_on_release: true,
-      },
-
-      unenroll: {
-        methods: ["unenroll"],
-        objects: ["preference_study", "addon_study", "preference_addon"],
-        extra_keys: ["reason", "didResetValue", "addonId", "addonVersion"],
-        record_on_release: true,
-      },
+    unenroll: {
+      methods: ["unenroll"],
+      objects: ["preference_study", "addon_study", "preference_rollout"],
+      extra_keys: ["reason", "didResetValue", "addonId", "addonVersion"],
+      record_on_release: true,
+    },
 
-      unenroll_failure: {
-        methods: ["unenrollFailed"],
-        objects: ["preference_rollout"],
-        extra_keys: ["reason"],
-        record_on_release: true,
-      },
+    unenroll_failed: {
+      methods: ["unenrollFailed"],
+      objects: ["preference_rollout"],
+      extra_keys: ["reason"],
+      record_on_release: true,
+    },
 
-      graduate: {
-        methods: ["graduate"],
-        objects: ["preference_rollout"],
-        extra_keys: [],
-        record_on_release: true,
-      },
-    });
+    graduate: {
+      methods: ["graduate"],
+      objects: ["preference_rollout"],
+      extra_keys: [],
+      record_on_release: true,
+    },
+  },
+
+  init() {
+    Services.telemetry.registerEvents(TELEMETRY_CATEGORY, this.eventSchema);
   },
 
   sendEvent(method, object, value, extra) {
     Services.telemetry.recordEvent(TELEMETRY_CATEGORY, method, object, value, extra);
   },
 };
--- a/toolkit/components/normandy/test/browser/browser_AddonStudies.js
+++ b/toolkit/components/normandy/test/browser/browser_AddonStudies.js
@@ -137,17 +137,17 @@ decorate_task(
       /already exists/,
       "start rejects when a study exists with the given recipeId already."
     );
   }
 );
 
 decorate_task(
   withStub(Addons, "applyInstall"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   withWebExtension(),
   async function testStartAddonCleanup(applyInstallStub, sendEventStub, [addonId, addonFile]) {
     const fakeError = new Error("Fake failure");
     fakeError.fileName = "fake/filename.js";
     fakeError.lineNumber = 42;
     fakeError.columnNumber = 54;
     applyInstallStub.rejects(fakeError);
 
@@ -183,17 +183,17 @@ decorate_task(
     );
 
     await Addons.uninstall(testOverwriteId);
   }
 );
 
 decorate_task(
   withWebExtension({version: "2.0"}),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   AddonStudies.withStudies(),
   async function testStart([addonId, addonFile], sendEventStub) {
     const startupPromise = AddonTestUtils.promiseWebExtensionStartup(addonId);
     const addonUrl = Services.io.newFileURI(addonFile).spec;
 
     let addon = await Addons.get(addonId);
     is(addon, null, "Before start is called, the add-on is not installed.");
 
@@ -260,17 +260,17 @@ decorate_task(
 );
 
 const testStopId = "testStop@example.com";
 decorate_task(
   AddonStudies.withStudies([
     studyFactory({active: true, addonId: testStopId, studyEndDate: null}),
   ]),
   withInstalledWebExtension({id: testStopId}),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testStop([study], [addonId, addonFile], sendEventStub) {
     await AddonStudies.stop(study.recipeId, "test-reason");
     const newStudy = await AddonStudies.get(study.recipeId);
     ok(!newStudy.active, "stop marks the study as inactive.");
     ok(newStudy.studyEndDate, "stop saves the study end date.");
 
     const addon = await Addons.get(addonId);
     is(addon, null, "stop uninstalls the study add-on.");
@@ -306,17 +306,17 @@ decorate_task(
 );
 
 decorate_task(
   AddonStudies.withStudies([
     studyFactory({active: true, addonId: "does.not.exist@example.com", studyEndDate: null}),
     studyFactory({active: true, addonId: "installed@example.com"}),
     studyFactory({active: false, addonId: "already.gone@example.com", studyEndDate: new Date(2012, 1)}),
   ]),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   withInstalledWebExtension({id: "installed@example.com"}),
   async function testInit([activeUninstalledStudy, activeInstalledStudy, inactiveStudy], sendEventStub) {
     await AddonStudies.init();
 
     const newActiveStudy = await AddonStudies.get(activeUninstalledStudy.recipeId);
     ok(!newActiveStudy.active, "init marks studies as inactive if their add-on is not installed.");
     ok(
       newActiveStudy.studyEndDate,
@@ -367,17 +367,17 @@ decorate_task(
       "The study end date is set when the add-on for the study is uninstalled."
     );
   }
 );
 
 // stop should pass "unknown" to TelemetryEvents for `reason` if none specified
 decorate_task(
   AddonStudies.withStudies([studyFactory({ active: true })]),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testStopUnknownReason([study], sendEventStub) {
     await AddonStudies.stop(study.recipeId);
     is(
       sendEventStub.getCall(0).args[3].reason,
       "unknown",
       "stop should send the correct telemetry event",
       "AddonStudies.stop() should use unknown as the default reason",
     );
--- a/toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
+++ b/toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
@@ -97,17 +97,17 @@ decorate_task(
 );
 
 // start should save experiment data, modify the preference, and register a
 // watcher.
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withStub(PreferenceExperiments, "startObserver"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testStart(experiments, mockPreferences, startObserverStub, sendEventStub) {
     mockPreferences.set("fake.preference", "oldvalue", "default");
     mockPreferences.set("fake.preference", "uservalue", "user");
 
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
       preferenceName: "fake.preference",
@@ -404,17 +404,17 @@ decorate_task(
 );
 
 // stop should mark the experiment as expired, stop its observer, and revert the
 // preference value.
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withSpy(PreferenceExperiments, "stopObserver"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testStop(experiments, mockPreferences, stopObserverSpy, sendEventStub) {
     // this assertion is mostly useful for --verify test runs, to make
     // sure that tests clean up correctly.
     is(Preferences.get("fake.preference"), null, "preference should start unset");
 
     mockPreferences.set(`${startupPrefs}.fake.preference`, "experimentvalue", "user");
     mockPreferences.set("fake.preference", "experimentvalue", "default");
     experiments.test = experimentFactory({
@@ -515,17 +515,17 @@ decorate_task(
   }
 );
 
 // stop should not modify a preference if resetValue is false
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withStub(PreferenceExperiments, "stopObserver"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testStopReset(experiments, mockPreferences, stopObserverStub, sendEventStub) {
     mockPreferences.set("fake.preference", "customvalue", "default");
     experiments.test = experimentFactory({
       name: "test",
       expired: false,
       preferenceName: "fake.preference",
       preferenceValue: "experimentvalue",
       preferenceType: "string",
@@ -696,17 +696,17 @@ decorate_task(
   },
 );
 
 // starting and stopping experiments should register in telemetry
 decorate_task(
   withMockExperiments,
   withStub(TelemetryEnvironment, "setExperimentActive"),
   withStub(TelemetryEnvironment, "setExperimentInactive"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testStartAndStopTelemetry(experiments, setActiveStub, setInactiveStub, sendEventStub) {
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
       preferenceName: "fake.preference",
       preferenceValue: "value",
       preferenceType: "string",
       preferenceBranchType: "default",
@@ -740,17 +740,17 @@ decorate_task(
   },
 );
 
 // starting experiments should use the provided experiment type
 decorate_task(
   withMockExperiments,
   withStub(TelemetryEnvironment, "setExperimentActive"),
   withStub(TelemetryEnvironment, "setExperimentInactive"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testInitTelemetryExperimentType(experiments, setActiveStub, setInactiveStub, sendEventStub) {
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
       preferenceName: "fake.preference",
       preferenceValue: "value",
       preferenceType: "string",
       preferenceBranchType: "default",
@@ -997,17 +997,17 @@ decorate_task(
   },
 );
 
 // stop should pass "unknown" to telemetry event for `reason` if none is specified
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withStub(PreferenceExperiments, "stopObserver"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testStopUnknownReason(experiments, mockPreferences, stopObserverStub, sendEventStub) {
     mockPreferences.set("fake.preference", "default value", "default");
     experiments.test = experimentFactory({ name: "test", preferenceName: "fake.preference" });
     await PreferenceExperiments.stop("test");
     is(
       sendEventStub.getCall(0).args[3].reason,
       "unknown",
       "PreferenceExperiments.stop() should use unknown as the default reason",
@@ -1015,17 +1015,17 @@ decorate_task(
   }
 );
 
 // stop should pass along the value for resetValue to Telemetry Events as didResetValue
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withStub(PreferenceExperiments, "stopObserver"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testStopResetValue(experiments, mockPreferences, stopObserverStub, sendEventStub) {
     mockPreferences.set("fake.preference1", "default value", "default");
     experiments.test1 = experimentFactory({ name: "test1", preferenceName: "fake.preference1" });
     await PreferenceExperiments.stop("test1", {resetValue: true});
     is(sendEventStub.callCount, 1);
     is(
       sendEventStub.getCall(0).args[3].didResetValue,
       "true",
@@ -1043,17 +1043,17 @@ decorate_task(
     );
   }
 );
 
 // Should send the correct event telemetry when a study ends because
 // the user changed preferences during a browser run.
 decorate_task(
   withMockPreferences,
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   withMockExperiments,
   async function testPrefChangeEventTelemetry(mockPreferences, sendEventStub, mockExperiments) {
     is(Preferences.get("fake.preference"), null, "preference should start unset");
     mockPreferences.set("fake.preference", "oldvalue", "default");
     mockExperiments.test = experimentFactory({
       name: "test",
       expired: false,
       preferenceName: "fake.preference",
--- a/toolkit/components/normandy/test/browser/browser_PreferenceRollouts.js
+++ b/toolkit/components/normandy/test/browser/browser_PreferenceRollouts.js
@@ -140,17 +140,17 @@ decorate_task(
       "rollout in database should be updated",
     );
   },
 );
 
 // recordOriginalValue should graduate a study when it is no longer relevant.
 decorate_task(
   PreferenceRollouts.withTestMock,
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function testRecordOriginalValuesUpdatesPreviousValues(sendEventStub) {
     await PreferenceRollouts.add({
       slug: "test-rollout",
       state: PreferenceRollouts.STATE_ACTIVE,
       preferences: [
         {preferenceName: "test.pref1", value: 2, previousValue: null},
         {preferenceName: "test.pref2", value: 2, previousValue: null},
       ],
--- a/toolkit/components/normandy/test/browser/browser_actions_PreferenceRollbackAction.js
+++ b/toolkit/components/normandy/test/browser/browser_actions_PreferenceRollbackAction.js
@@ -7,17 +7,17 @@ ChromeUtils.import("resource://normandy/
 ChromeUtils.import("resource://normandy/lib/Uptake.jsm", this);
 ChromeUtils.import("resource://normandy/lib/PreferenceRollouts.jsm", this);
 ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
 
 // Test that a simple recipe rollsback as expected
 decorate_task(
   PreferenceRollouts.withTestMock,
   withStub(TelemetryEnvironment, "setExperimentInactive"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function simple_rollback(setExperimentInactiveStub, sendEventStub) {
     Services.prefs.getDefaultBranch("").setIntPref("test.pref1", 2);
     Services.prefs.getDefaultBranch("").setCharPref("test.pref2", "rollout value");
     Services.prefs.getDefaultBranch("").setBoolPref("test.pref3", true);
 
     PreferenceRollouts.add({
       slug: "test-rollout",
       state: PreferenceRollouts.STATE_ACTIVE,
@@ -72,17 +72,17 @@ decorate_task(
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref2");
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref3");
   },
 );
 
 // Test that a graduated rollout can't be rolled back
 decorate_task(
   PreferenceRollouts.withTestMock,
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function cant_rollback_graduated(sendEventStub) {
     Services.prefs.getDefaultBranch("").setIntPref("test.pref", 1);
     await PreferenceRollouts.add({
       slug: "graduated-rollout",
       state: PreferenceRollouts.STATE_GRADUATED,
       preferences: [{preferenceName: "test.pref", value: 1, previousValue: 1}],
     });
 
@@ -103,56 +103,56 @@ decorate_task(
         state: PreferenceRollouts.STATE_GRADUATED,
         preferences: [{preferenceName: "test.pref", value: 1, previousValue: 1}],
       }],
       "Rollout should not change in db"
     );
 
     Assert.deepEqual(
       sendEventStub.args,
-      [["unenrollFailure", "preference_rollout", "graduated-rollout", {reason: "graduated"}]],
+      [["unenrollFailed", "preference_rollout", "graduated-rollout", {reason: "graduated"}]],
       "correct event was sent"
     );
 
     // Cleanup
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref");
   },
 );
 
 // Test that a rollback without a matching rollout
 decorate_task(
   PreferenceRollouts.withTestMock,
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   withStub(Uptake, "reportRecipe"),
   async function rollback_without_rollout(sendEventStub, reportRecipeStub) {
     let recipe = {id: 1, arguments: {rolloutSlug: "missing-rollout"}};
 
     const action = new PreferenceRollbackAction();
     await action.runRecipe(recipe);
     await action.finalize();
 
     Assert.deepEqual(
       sendEventStub.args,
-      [["unenrollFailure", "preference_rollout", "missing-rollout", {reason: "rollout missing"}]],
+      [["unenrollFailed", "preference_rollout", "missing-rollout", {reason: "rollout missing"}]],
       "an unenrollFailure event should be sent",
     );
     // This is too common a case for an error, so it should be reported as success
     Assert.deepEqual(
       reportRecipeStub.args,
       [[recipe.id, Uptake.RECIPE_SUCCESS]],
       "recipe should be reported as succesful",
     );
   },
 );
 
 // Test that rolling back an already rolled back recipe doesn't do anything
 decorate_task(
   PreferenceRollouts.withTestMock,
   withStub(TelemetryEnvironment, "setExperimentInactive"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function rollback_already_rolled_back(setExperimentInactiveStub, sendEventStub) {
     Services.prefs.getDefaultBranch("").setIntPref("test.pref", 1);
 
     const recipe = {id: 1, arguments: {rolloutSlug: "test-rollout"}};
     const rollout = {
       slug: "test-rollout",
       state: PreferenceRollouts.STATE_ROLLED_BACK,
       preferences: [{preferenceName: "test.pref", value: 2, previousValue: 1}],
--- a/toolkit/components/normandy/test/browser/browser_actions_PreferenceRolloutAction.js
+++ b/toolkit/components/normandy/test/browser/browser_actions_PreferenceRolloutAction.js
@@ -6,17 +6,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://normandy/actions/PreferenceRolloutAction.jsm", this);
 ChromeUtils.import("resource://normandy/lib/PreferenceRollouts.jsm", this);
 ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
 
 // Test that a simple recipe enrolls as expected
 decorate_task(
   PreferenceRollouts.withTestMock,
   withStub(TelemetryEnvironment, "setExperimentActive"),
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function simple_recipe_enrollment(setExperimentActiveStub, sendEventStub) {
     const recipe = {
       id: 1,
       arguments: {
         slug: "test-rollout",
         preferences: [
           {preferenceName: "test.pref1", value: 1},
           {preferenceName: "test.pref2", value: true},
@@ -70,17 +70,17 @@ decorate_task(
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref2");
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref3");
   },
 );
 
 // Test that an enrollment's values can change, be removed, and be added
 decorate_task(
   PreferenceRollouts.withTestMock,
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function update_enrollment(sendEventStub) {
     // first enrollment
     const recipe = {
       id: 1,
       arguments: {
         slug: "test-rollout",
         preferences: [
           {preferenceName: "test.pref1", value: 1},
@@ -159,17 +159,17 @@ decorate_task(
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref2");
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref3");
   },
 );
 
 // Test that a graduated rollout can be ungraduated
 decorate_task(
   PreferenceRollouts.withTestMock,
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function ungraduate_enrollment(sendEventStub) {
     Services.prefs.getDefaultBranch("").setIntPref("test.pref", 1);
     await PreferenceRollouts.add({
       slug: "test-rollout",
       state: PreferenceRollouts.STATE_GRADUATED,
       preferences: [{preferenceName: "test.pref", value: 1, previousValue: 1}],
     });
 
@@ -210,17 +210,17 @@ decorate_task(
     // Cleanup
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref");
   },
 );
 
 // Test when recipes conflict, only one is applied
 decorate_task(
   PreferenceRollouts.withTestMock,
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function conflicting_recipes(sendEventStub) {
     // create two recipes that each share a pref and have a unique pref.
     const recipe1 = {
       id: 1,
       arguments: {
         slug: "test-rollout-1",
         preferences: [
           {preferenceName: "test.pref1", value: 1},
@@ -286,17 +286,17 @@ decorate_task(
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref2");
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref3");
   },
 );
 
 // Test when the wrong value type is given, the recipe is not applied
 decorate_task(
   PreferenceRollouts.withTestMock,
-  withStub(TelemetryEvents, "sendEvent"),
+  withSendEventStub,
   async function wrong_preference_value(sendEventStub) {
     Services.prefs.getDefaultBranch("").setCharPref("test.pref", "not an int");
     const recipe = {
       id: 1,
       arguments: {
         slug: "test-rollout",
         preferences: [{preferenceName: "test.pref", value: 1}],
       },
@@ -307,17 +307,17 @@ decorate_task(
     await action.finalize();
 
     is(Services.prefs.getCharPref("test.pref"), "not an int", "the pref should not be modified");
     is(Services.prefs.getPrefType("app.normandy.startupRolloutPrefs.test.pref"), Services.prefs.PREF_INVALID, "startup pref is not set");
 
     Assert.deepEqual(await PreferenceRollouts.getAll(), [], "no rollout is stored in the db");
     Assert.deepEqual(
       sendEventStub.args,
-      [["enrollFailed", "preference_rollout", recipe.arguments.slug, {reason: "invalid type", pref: "test.pref"}]],
+      [["enrollFailed", "preference_rollout", recipe.arguments.slug, {reason: "invalid type", preference: "test.pref"}]],
       "an enrollment failed event should be sent",
     );
 
     // Cleanup
     Services.prefs.getDefaultBranch("").deleteBranch("test.pref");
   },
 );
 
--- a/toolkit/components/normandy/test/browser/head.js
+++ b/toolkit/components/normandy/test/browser/head.js
@@ -313,8 +313,44 @@ this.withSpy = function(...spyArgs) {
 };
 
 this.studyEndObserved = function(recipeId) {
   return TestUtils.topicObserved(
     "shield-study-ended",
     (subject, endedRecipeId) => Number.parseInt(endedRecipeId) === recipeId,
   );
 };
+
+this.withSendEventStub = function(testFunction) {
+  return async function wrappedTestFunction(...args) {
+
+    /* Checks that calls match the event schema. */
+    function checkEventMatchesSchema(method, object, value, extra) {
+      let match = true;
+      const spec = Array.from(Object.values(TelemetryEvents.eventSchema))
+        .filter(spec => spec.methods.includes(method))[0];
+
+      if (spec) {
+        if (!spec.objects.includes(object)) {
+          match = false;
+        }
+
+        for (const key of Object.keys(extra)) {
+          if (!spec.extra_keys.includes(key)) {
+            match = false;
+          }
+        }
+      } else {
+        match = false;
+      }
+
+      ok(match, `sendEvent(${method}, ${object}, ${value}, ${JSON.stringify(extra)}) should match spec`);
+    }
+
+    const stub = sinon.stub(TelemetryEvents, "sendEvent");
+    stub.callsFake(checkEventMatchesSchema);
+    try {
+      await testFunction(...args, stub);
+    } finally {
+      stub.restore();
+    }
+  };
+};
--- a/toolkit/content/tests/browser/browser_findbar.js
+++ b/toolkit/content/tests/browser/browser_findbar.js
@@ -222,17 +222,20 @@ function promiseFindFinished(searchText,
         if (aData === null)
           info("Result listener not called, timeout reached.");
         clearTimeout(findTimeout);
         findbar.browser.finder.removeResultListener(resultListener);
         resolve();
       };
 
       resultListener = {
-        onFindResult: foundOrTimedout
+        onFindResult: foundOrTimedout,
+        onCurrentSelection() {},
+        onMatchesCountResult() {},
+        onHighlightFinished() {},
       };
       findbar.browser.finder.addResultListener(resultListener);
       findbar._find();
     });
 
   });
 }
 
--- a/toolkit/modules/RemoteFinder.jsm
+++ b/toolkit/modules/RemoteFinder.jsm
@@ -86,18 +86,19 @@ RemoteFinder.prototype = {
 
     for (let l of this._listeners) {
       // Don't let one callback throwing stop us calling the rest
       try {
         l[callback].apply(l, params);
       } catch (e) {
         if (!l[callback]) {
           Cu.reportError(`Missing ${callback} callback on RemoteFinderListener`);
+        } else {
+          Cu.reportError(e);
         }
-        Cu.reportError(e);
       }
     }
   },
 
   get searchString() {
     return this._searchString;
   },