Merge autoland to mozilla-central. a=merge
authorGurzau Raul <rgurzau@mozilla.com>
Tue, 06 Nov 2018 11:52:42 +0200
changeset 444539 e160f0a60e4fc9cc6e77dd57644d74d4c6cc88dd
parent 444538 53647b724d3bbc77055bd9167fa125c5d52e4eda (current diff)
parent 444537 b4e4e74aa3ccb6673df2a4b62249706e5999fcd6 (diff)
child 444586 c6a6e76dc5424395e45c3d47aa696eb2f7d09e5e
push id34996
push userrgurzau@mozilla.com
push dateTue, 06 Nov 2018 09:53:23 +0000
treeherdermozilla-central@e160f0a60e4f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.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
dom/interfaces/base/nsIContentURIGrouper.idl
layout/style/test/file_shape_outside_CORS.html
testing/web-platform/meta/content-security-policy/navigate-to/spv-only-sent-to-initiator.html.ini
testing/web-platform/meta/css/css-backgrounds/background-331.html.ini
testing/web-platform/meta/css/css-backgrounds/background-333.html.ini
testing/web-platform/meta/css/css-masking/inheritance.sub.html.ini
testing/web-platform/meta/webaudio/the-audio-api/the-audioparam-interface/event-insertion.html.ini
testing/web-platform/tests/content-security-policy/navigate-to/form-action/form-action-allows-navigate-to-allows.html
testing/web-platform/tests/content-security-policy/navigate-to/form-action/form-action-allows-navigate-to-blocks.html
testing/web-platform/tests/content-security-policy/navigate-to/form-action/form-action-blocks-navigate-to-allows.html
testing/web-platform/tests/content-security-policy/navigate-to/form-action/form-action-blocks-navigate-to-blocks.html
testing/web-platform/tests/content-security-policy/navigate-to/spv-only-sent-to-initiator.html
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1743,18 +1743,18 @@ pref("app.normandy.shieldLearnMoreUrl", 
 #ifdef MOZ_DATA_REPORTING
 pref("app.shield.optoutstudies.enabled", true);
 #else
 pref("app.shield.optoutstudies.enabled", false);
 #endif
 
 // Multi-lingual preferences
 pref("intl.multilingual.enabled", false);
-// AMO only serves language packs for release versions, so this feature only works on release.
-#ifdef RELEASE
+// AMO only serves language packs for release and beta versions.
+#ifdef RELEASE_OR_BETA
 pref("intl.multilingual.downloadEnabled", true);
 #else
 pref("intl.multilingual.downloadEnabled", false);
 #endif
 
 // Simulate conditions that will happen when the browser
 // is running with Fission enabled. This is meant to assist
 // development and testing of Fission.
--- a/browser/base/content/test/plugins/browser_private_clicktoplay.js
+++ b/browser/base/content/test/plugins/browser_private_clicktoplay.js
@@ -65,19 +65,24 @@ add_task(async function test1b() {
   // Check the button status
   let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
                                                    "Shown");
   popupNotification.reshow();
 
   await promiseShown;
   is(gPrivateWindow.PopupNotifications.panel.firstElementChild.checkbox.hidden, true, "'Remember' checkbox should be hidden in private windows");
 
+  let promises = [
+    BrowserTestUtils.browserLoaded(gTestBrowser, false, gHttpTestRoot + "plugin_test.html"),
+    BrowserTestUtils.waitForEvent(window, "activate"),
+  ];
   gPrivateWindow.close();
   BrowserTestUtils.loadURI(gTestBrowser, gHttpTestRoot + "plugin_test.html");
-  await BrowserTestUtils.browserLoaded(gTestBrowser);
+  await Promise.all(promises);
+  await SimpleTest.promiseFocus(window);
 });
 
 add_task(async function test2a() {
   // enable test plugin on this site
   let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 2a, Should have a click-to-play notification");
 
   await ContentTask.spawn(gTestBrowser, null, function() {
@@ -122,20 +127,25 @@ add_task(async function test2c() {
   popupNotification.reshow();
   await promiseShown;
   is(gPrivateWindow.PopupNotifications.panel.firstElementChild.secondaryButton.hidden, true,
      "Test 2c, Activated plugin in a private window should not have visible 'Block' button.");
   is(gPrivateWindow.PopupNotifications.panel.firstElementChild.checkbox.hidden, true,
      "Test 2c, Activated plugin in a private window should not have visible 'Remember' checkbox.");
 
   clearAllPluginPermissions();
+
+  let promises = [
+    BrowserTestUtils.browserLoaded(gTestBrowser, false, gHttpTestRoot + "plugin_test.html"),
+    BrowserTestUtils.waitForEvent(window, "activate"),
+  ];
   gPrivateWindow.close();
-
   BrowserTestUtils.loadURI(gTestBrowser, gHttpTestRoot + "plugin_test.html");
-  await BrowserTestUtils.browserLoaded(gTestBrowser);
+  await Promise.all(promises);
+  await SimpleTest.promiseFocus(window);
 });
 
 add_task(async function test3a() {
   // enable test plugin on this site
   let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 3a, Should have a click-to-play notification");
 
   await ContentTask.spawn(gTestBrowser, null, function() {
--- a/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
+++ b/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
@@ -56,17 +56,17 @@ add_task(taskWithNewTab(async function t
 
 add_task(taskWithNewTab(async function test_disabled_ac() {
   // Disable autocomplete.
   let suggestHistory = Preferences.get("browser.urlbar.suggest.history");
   Preferences.set("browser.urlbar.suggest.history", false);
   let suggestBookmarks = Preferences.get("browser.urlbar.suggest.bookmark");
   Preferences.set("browser.urlbar.suggest.bookmark", false);
   let suggestOpenPages = Preferences.get("browser.urlbar.suggest.openpage");
-  Preferences.set("browser.urlbar.suggest.openpages", false);
+  Preferences.set("browser.urlbar.suggest.openpage", false);
 
   Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
                                        "http://example.com/?q={searchTerms}");
   let engine = Services.search.getEngineByName("MozSearch");
   let originalEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
 
   function cleanup() {
--- a/browser/base/content/test/urlbar/browser_urlbarDecode.js
+++ b/browser/base/content/test/urlbar/browser_urlbarDecode.js
@@ -28,17 +28,19 @@ add_task(async function injectJSON() {
   gURLBar.handleRevert();
   gURLBar.blur();
 });
 
 add_task(function losslessDecode() {
   let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
   let url = "http://" + urlNoScheme;
   if (Services.prefs.getBoolPref("browser.urlbar.quantumbar", true)) {
-    const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, {url});
+    const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                                   UrlbarUtils.MATCH_SOURCE.TABS,
+                                   { url });
     gURLBar.setValueFromResult(result);
   } else {
     gURLBar.textValue = url;
   }
   // Since this is directly setting textValue, it is expected to be trimmed.
   Assert.equal(gURLBar.inputField.value, urlNoScheme,
                "The string displayed in the textbox should not be escaped");
   gURLBar.value = "";
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1723,21 +1723,19 @@ BrowserGlue.prototype = {
 
   _onQuitRequest: function BG__onQuitRequest(aCancelQuit, aQuitType) {
     // If user has already dismissed quit request, then do nothing
     if ((aCancelQuit instanceof Ci.nsISupportsPRBool) && aCancelQuit.data)
       return;
 
     // There are several cases where we won't show a dialog here:
     // 1. There is only 1 tab open in 1 window
-    // 2. The session will be restored at startup, indicated by
-    //    browser.startup.page == 3 or browser.sessionstore.resume_session_once == true
-    // 3. browser.warnOnQuit == false
-    // 4. The browser is currently in Private Browsing mode
-    // 5. The browser will be restarted.
+    // 2. browser.warnOnQuit or browser.warnOnClose == false
+    // 3. The browser is currently in Private Browsing mode
+    // 4. The browser will be restarted.
     //
     // Otherwise, we will show the "closing multiple tabs" dialog.
     //
     // aQuitType == "lastwindow" is overloaded. "lastwindow" is used to indicate
     // "the last window is closing but we're not quitting (a non-browser window is open)"
     // and also "we're quitting by closing the last window".
 
     if (aQuitType == "restart" || aQuitType == "os-restart")
@@ -1759,22 +1757,18 @@ BrowserGlue.prototype = {
 
     if (pagecount < 2)
       return;
 
     if (!aQuitType)
       aQuitType = "quit";
 
     // browser.warnOnQuit is a hidden global boolean to override all quit prompts
-    // browser.showQuitWarning specifically covers quitting
     // browser.tabs.warnOnClose is the global "warn when closing multiple tabs" pref
-
-    var sessionWillBeRestored = Services.prefs.getIntPref("browser.startup.page") == 3 ||
-                                Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
-    if (sessionWillBeRestored || !Services.prefs.getBoolPref("browser.warnOnQuit") ||
+    if (!Services.prefs.getBoolPref("browser.warnOnQuit") ||
         !Services.prefs.getBoolPref("browser.tabs.warnOnClose"))
       return;
 
     let win = BrowserWindowTracker.getTopWindow();
 
     // warnAboutClosingTabs checks browser.tabs.warnOnClose and returns if it's
     // ok to close the window. It doesn't actually close the window.
     if (windowcount == 1) {
--- a/browser/components/payments/res/components/rich-select.js
+++ b/browser/components/payments/res/components/rich-select.js
@@ -19,20 +19,22 @@ export default class RichSelect extends 
       "disabled",
       "hidden",
     ];
   }
 
   constructor() {
     super();
     this.popupBox = document.createElement("select");
-    this.popupBox.addEventListener("change", this);
   }
 
   connectedCallback() {
+    // the popupBox element may change in between constructor and being connected
+    // so wait until connected before listening to events on it
+    this.popupBox.addEventListener("change", this);
     this.appendChild(this.popupBox);
     this.render();
   }
 
   get selectedOption() {
     return this.getOptionByValue(this.value);
   }
 
@@ -69,17 +71,17 @@ export default class RichSelect extends 
   render() {
     let selectedRichOption = this.querySelector(":scope > .rich-select-selected-option");
     if (selectedRichOption) {
       selectedRichOption.remove();
     }
 
     if (this.value) {
       let optionType = this.getAttribute("option-type");
-      if (selectedRichOption.localName != optionType) {
+      if (!selectedRichOption || selectedRichOption.localName != optionType) {
         selectedRichOption = document.createElement(optionType);
       }
 
       let option = this.getOptionByValue(this.value);
       let attributeNames = selectedRichOption.constructor.observedAttributes;
       for (let attributeName of attributeNames) {
         let attributeValue = option.getAttribute(attributeName);
         if (attributeValue) {
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -1,15 +1,14 @@
 /* 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/. */
 
 /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
 import LabelledCheckbox from "../components/labelled-checkbox.js";
-import PaymentDialog from "./payment-dialog.js";
 import PaymentRequestPage from "../components/payment-request-page.js";
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import paymentRequest from "../paymentRequest.js";
 /* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
  * <address-form></address-form>
  *
@@ -194,17 +193,17 @@ export default class AddressForm extends
     let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(state,
                                                                      addressPage.selectedStateKey);
     for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
       let container = this.form.querySelector(errorSelector + "-container");
       let field = this.form.querySelector(errorSelector);
       // Never show errors on an 'add' screen as they would be for a different address.
       let errorText = (editing && merchantFieldErrors && merchantFieldErrors[errorName]) || "";
       field.setCustomValidity(errorText);
-      let span = PaymentDialog.maybeCreateFieldErrorElement(container);
+      let span = paymentRequest.maybeCreateFieldErrorElement(container);
       span.textContent = errorText;
     }
 
     this.updateSaveButtonState();
   }
 
   handleEvent(event) {
     switch (event.type) {
@@ -276,17 +275,17 @@ export default class AddressForm extends
 
   /**
    * @param {Event} event - "invalid" event
    * Note: Keep this in-sync with the equivalent version in basic-card-form.js
    */
   onInvalidField(event) {
     let field = event.target;
     let container = field.closest(`#${field.id}-container`);
-    let errorTextSpan = PaymentDialog.maybeCreateFieldErrorElement(container);
+    let errorTextSpan = paymentRequest.maybeCreateFieldErrorElement(container);
     errorTextSpan.textContent = field.validationMessage;
   }
 
   onInvalidForm() {
     this.saveButton.disabled = true;
   }
 
   updateRequiredState() {
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -28,17 +28,20 @@ export default class AddressPicker exten
 
   constructor() {
     super();
     this.dropdown.setAttribute("option-type", "address-option");
   }
 
   attributeChangedCallback(name, oldValue, newValue) {
     super.attributeChangedCallback(name, oldValue, newValue);
-    if (AddressPicker.pickerAttributes.includes(name) && oldValue !== newValue) {
+    // connectedCallback may add and adjust elements & values
+    // so avoid calling render before the element is connected
+    if (this.isConnected &&
+        AddressPicker.pickerAttributes.includes(name) && oldValue !== newValue) {
       this.render(this.requestStore.getState());
     }
   }
 
   get fieldNames() {
     if (this.hasAttribute("address-fields")) {
       let names = this.getAttribute("address-fields").trim().split(/\s+/);
       if (names.length) {
@@ -84,17 +87,36 @@ export default class AddressPicker exten
           uniques.add(key);
           result[guid] = address;
         }
       }
     }
     return result;
   }
 
+  get options() {
+    return this.dropdown.popupBox.options;
+  }
+
+  /**
+   * @param {object} state - See `PaymentsStore.setState`
+   * The value of the picker is retrieved from state store rather than the DOM
+   * @returns {string} guid
+   */
+  getCurrentValue(state) {
+    let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
+    let guid = state[selectedKey];
+    if (selectedLeaf) {
+      guid = guid[selectedLeaf];
+    }
+    return guid;
+  }
+
   render(state) {
+    let selectedAddressGUID = this.getCurrentValue(state) || "";
     let addresses = paymentRequest.getAddresses(state);
     let desiredOptions = [];
     let filteredAddresses = this.filterAddresses(addresses, this.fieldNames);
     for (let [guid, address] of Object.entries(filteredAddresses)) {
       let optionEl = this.dropdown.getOptionByValue(guid);
       if (!optionEl) {
         optionEl = document.createElement("option");
         optionEl.value = guid;
@@ -126,22 +148,28 @@ export default class AddressPicker exten
       // fieldNames getter is not used here because it returns a default array with
       // attributes even when "address-fields" observed attribute is null.
       let addressFields = this.getAttribute("address-fields");
       optionEl.textContent = AddressOption.formatSingleLineLabel(address, addressFields);
       desiredOptions.push(optionEl);
     }
 
     this.dropdown.popupBox.textContent = "";
+
+    if (this._allowEmptyOption) {
+      let optionEl = document.createElement("option");
+      optionEl.value = "";
+      desiredOptions.unshift(optionEl);
+    }
+
     for (let option of desiredOptions) {
       this.dropdown.popupBox.appendChild(option);
     }
 
     // Update selectedness after the options are updated
-    let selectedAddressGUID = state[this.selectedStateKey];
     this.dropdown.value = selectedAddressGUID;
 
     if (selectedAddressGUID && selectedAddressGUID !== this.dropdown.value) {
       throw new Error(`${this.selectedStateKey} option ${selectedAddressGUID} ` +
                       `does not exist in the address picker`);
     }
 
     super.render(state);
@@ -156,18 +184,18 @@ export default class AddressPicker exten
     if (superError) {
       return superError;
     }
 
     if (!this.selectedOption) {
       return "";
     }
 
-    let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(state,
-                                                                     [this.selectedStateKey]);
+    let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
+          state, this.selectedStateKey.split("|"));
     // TODO: errors in priority order.
     return Object.values(merchantFieldErrors).find(msg => {
       return typeof(msg) == "string" && msg.length;
     }) || "";
   }
 
   handleEvent(event) {
     switch (event.type) {
@@ -177,45 +205,55 @@ export default class AddressPicker exten
       }
       case "click": {
         this.onClick(event);
       }
     }
   }
 
   onChange(event) {
-    let selectedKey = this.selectedStateKey;
-    if (selectedKey) {
-      this.requestStore.setState({
-        [selectedKey]: this.dropdown.value,
-      });
+    let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
+    if (!selectedKey) {
+      return;
     }
+    // selectedStateKey can be a '|' delimited string indicating a path into the state object
+    // to update with the new value
+    let newState = {};
+
+    if (selectedLeaf) {
+      let currentState = this.requestStore.getState();
+      newState[selectedKey] = Object.assign({},
+                                            currentState[selectedKey],
+                                            { [selectedLeaf]: this.dropdown.value });
+    } else {
+      newState[selectedKey] = this.dropdown.value;
+    }
+    this.requestStore.setState(newState);
   }
 
   onClick({target}) {
     let nextState = {
       page: {
         id: "address-page",
       },
       "address-page": {
         addressFields: this.getAttribute("address-fields"),
-        selectedStateKey: [this.selectedStateKey],
+        selectedStateKey: this.selectedStateKey.split("|"),
       },
     };
 
     switch (target) {
       case this.addLink: {
         nextState["address-page"].guid = null;
         nextState["address-page"].title = this.dataset.addAddressTitle;
         break;
       }
       case this.editLink: {
-        let state = this.requestStore.getState();
-        let selectedAddressGUID = state[this.selectedStateKey];
-        nextState["address-page"].guid = selectedAddressGUID;
+        let currentState = this.requestStore.getState();
+        nextState["address-page"].guid = this.getCurrentValue(currentState);
         nextState["address-page"].title = this.dataset.editAddressTitle;
         break;
       }
       default: {
         throw new Error("Unexpected onClick");
       }
     }
 
--- a/browser/components/payments/res/containers/basic-card-form.css
+++ b/browser/components/payments/res/containers/basic-card-form.css
@@ -26,21 +26,16 @@ basic-card-form .editCreditCardForm .per
   display: flex;
   grid-area: persist-checkbox;
 }
 
 #billingAddressGUID-container {
   display: grid;
 }
 
-#billingAddressGUID {
-  /* XXX: temporary until converted to a rich-picker in bug 1482689 */
-  margin: 14px 0;
-}
-
 basic-card-form > footer > .cancel-button {
   /* When cancel is shown (during onboarding), it should always be on the left with a space after it */
   margin-right: auto;
 }
 
 basic-card-form > footer > .cancel-button[hidden] ~ .back-button {
   /* When back is shown (outside onboarding) we want "Back <space> Add/Save" */
   /* Bug 1468153 may change the button ordering to match platform conventions */
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
 import AcceptedCards from "../components/accepted-cards.js";
+import BillingAddressPicker from "./billing-address-picker.js";
 import CscInput from "../components/csc-input.js";
 import LabelledCheckbox from "../components/labelled-checkbox.js";
-import PaymentDialog from "./payment-dialog.js";
 import PaymentRequestPage from "../components/payment-request-page.js";
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import paymentRequest from "../paymentRequest.js";
 
 /* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
  * <basic-card-form></basic-card-form>
@@ -23,25 +23,16 @@ import paymentRequest from "../paymentRe
 export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRequestPage) {
   constructor() {
     super();
 
     this.genericErrorText = document.createElement("div");
     this.genericErrorText.setAttribute("aria-live", "polite");
     this.genericErrorText.classList.add("page-error");
 
-    this.addressAddLink = document.createElement("a");
-    this.addressAddLink.className = "add-link";
-    this.addressAddLink.href = "javascript:void(0)";
-    this.addressAddLink.addEventListener("click", this);
-    this.addressEditLink = document.createElement("a");
-    this.addressEditLink.className = "edit-link";
-    this.addressEditLink.href = "javascript:void(0)";
-    this.addressEditLink.addEventListener("click", this);
-
     this.cscInput = new CscInput({
       useAlwaysVisiblePlaceholder: true,
       inputId: "cc-csc",
     });
 
     this.persistCheckbox = new LabelledCheckbox();
     // The persist checkbox shouldn't be part of the record which gets saved so
     // exclude it from the form.
@@ -81,16 +72,48 @@ export default class BasicCardForm exten
       xhr.addEventListener("load", evt => {
         resolve(xhr.response);
       });
       xhr.open("GET", url);
       xhr.send();
     });
   }
 
+  _upgradeBillingAddressPicker() {
+    let addressRow = this.form.querySelector(".billingAddressRow");
+    let addressPicker = this.billingAddressPicker = new BillingAddressPicker();
+
+    // Wrap the existing <select> that the formHandler manages
+    if (addressPicker.dropdown.popupBox) {
+      addressPicker.dropdown.popupBox.remove();
+    }
+    addressPicker.dropdown.popupBox = this.form.querySelector("#billingAddressGUID");
+
+    // Hide the original label as the address picker provide its own,
+    // but we'll copy the localized textContent from it when rendering
+    addressRow.querySelector(".label-text").hidden = true;
+
+    addressPicker.dataset.addLinkLabel = this.dataset.addressAddLinkLabel;
+    addressPicker.dataset.editLinkLabel = this.dataset.addressEditLinkLabel;
+    addressPicker.dataset.fieldSeparator = this.dataset.addressFieldSeparator;
+    addressPicker.dataset.addAddressTitle = this.dataset.billingAddressTitleAdd;
+    addressPicker.dataset.editAddressTitle = this.dataset.billingAddressTitleEdit;
+    addressPicker.dataset.invalidLabel = this.dataset.invalidAddressLabel;
+    // break-after-nth-field, address-fields not needed here
+
+    // this state is only used to carry the selected guid between pages;
+    // the select#billingAddressGUID is the source of truth for the current value
+    addressPicker.setAttribute("selected-state-key", "basic-card-page|billingAddressGUID");
+
+    addressPicker.addLink.addEventListener("click", this);
+    addressPicker.editLink.addEventListener("click", this);
+
+    addressRow.appendChild(addressPicker);
+  }
+
   connectedCallback() {
     this.promiseReady.then(form => {
       this.body.appendChild(form);
 
       let record = {};
       let addresses = [];
       this.formHandler = new EditCreditCard({
         form,
@@ -102,39 +125,30 @@ export default class BasicCardForm exten
 
       // The EditCreditCard constructor adds `change` and `input` event listeners on the same
       // element, which update field validity. By adding our event listeners after this
       // constructor, validity will be updated before our handlers get the event
       form.addEventListener("change", this);
       form.addEventListener("input", this);
       form.addEventListener("invalid", this);
 
+      this._upgradeBillingAddressPicker();
+
       // The "invalid" event does not bubble and needs to be listened for on each
       // form element.
       for (let field of this.form.elements) {
         field.addEventListener("invalid", this);
       }
 
       // Replace the form-autofill cc-csc fields with our csc-input.
       let cscContainer = this.form.querySelector("#cc-csc-container");
       cscContainer.textContent = "";
       cscContainer.appendChild(this.cscInput);
 
-      let fragment = document.createDocumentFragment();
-      fragment.append(" ");
-      fragment.append(this.addressEditLink);
-      fragment.append(this.addressAddLink);
       let billingAddressRow = this.form.querySelector(".billingAddressRow");
-
-      // XXX: Bug 1482689 - Remove the label-text class from the billing field
-      // which will be removed when switching to <rich-select>.
-      billingAddressRow.querySelector(".label-text").classList.remove("label-text");
-
-      billingAddressRow.appendChild(fragment);
-
       form.insertBefore(this.persistCheckbox, billingAddressRow);
       form.insertBefore(this.acceptedCardsList, billingAddressRow);
       this.body.appendChild(this.genericErrorText);
       // Only call the connected super callback(s) once our markup is fully
       // connected, including the shared form fetched asynchronously.
       super.connectedCallback();
     });
   }
@@ -163,20 +177,23 @@ export default class BasicCardForm exten
     } else {
       this.saveButton.textContent = this.dataset.nextButtonLabel;
     }
 
     this.cscInput.placeholder = this.dataset.cscPlaceholder;
     this.cscInput.frontTooltip = this.dataset.cscFrontInfoTooltip;
     this.cscInput.backTooltip = this.dataset.cscBackInfoTooltip;
 
+    // The label text from the form isn't available until render() time.
+    let labelText = this.form.querySelector(".billingAddressRow .label-text").textContent;
+    this.billingAddressPicker.setAttribute("label", labelText);
+
     this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
     this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
-    this.addressAddLink.textContent = this.dataset.addressAddLinkLabel;
-    this.addressEditLink.textContent = this.dataset.addressEditLinkLabel;
+
     this.acceptedCardsList.label = this.dataset.acceptedCardsLabel;
 
     // The next line needs an onboarding check since we don't set previousId
     // when navigating to add/edit directly from the summary page.
     this.backButton.hidden = !page.previousId && page.onboardingWizard;
     this.cancelButton.hidden = !page.onboardingWizard;
 
     let record = {};
@@ -223,34 +240,34 @@ export default class BasicCardForm exten
                                                          saveCreditCardDefaultChecked;
       }
     }
 
     this.formHandler.loadRecord(record, addresses, basicCardPage.preserveFieldValues);
 
     this.form.querySelector(".billingAddressRow").hidden = false;
 
-    let billingAddressSelect = this.form.querySelector("#billingAddressGUID");
+    let billingAddressSelect = this.billingAddressPicker.dropdown;
     if (basicCardPage.billingAddressGUID) {
       billingAddressSelect.value = basicCardPage.billingAddressGUID;
     } else if (!editing) {
       if (paymentRequest.getAddresses(state)[selectedShippingAddress]) {
         billingAddressSelect.value = selectedShippingAddress;
       } else {
         let firstAddressGUID = Object.keys(addresses)[0];
         if (firstAddressGUID) {
           // Only set the value if we have a saved address to not mark the field
           // dirty and invalid on an add form with no saved addresses.
           billingAddressSelect.value = firstAddressGUID;
         }
       }
     }
     // Need to recalculate the populated state since
     // billingAddressSelect is updated after loadRecord.
-    this.formHandler.updatePopulatedState(billingAddressSelect);
+    this.formHandler.updatePopulatedState(billingAddressSelect.popupBox);
 
     this.updateRequiredState();
     this.updateSaveButtonState();
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "change": {
@@ -285,56 +302,47 @@ export default class BasicCardForm exten
   }
 
   onClick(evt) {
     switch (evt.target) {
       case this.cancelButton: {
         paymentRequest.cancel();
         break;
       }
-      case this.addressAddLink:
-      case this.addressEditLink: {
+      case this.billingAddressPicker.addLink:
+      case this.billingAddressPicker.editLink: {
+        // The address-picker has set state for the page to advance to, now set up the
+        // necessary state for returning to and re-rendering this page
         let {
           "basic-card-page": basicCardPage,
+          page,
         } = this.requestStore.getState();
         let nextState = {
-          page: {
-            id: "address-page",
+          page: Object.assign({}, page, {
             previousId: "basic-card-page",
-          },
-          "address-page": {
-            guid: null,
-            selectedStateKey: ["basic-card-page", "billingAddressGUID"],
-            title: this.dataset.billingAddressTitleAdd,
-          },
+          }),
           "basic-card-page": {
             preserveFieldValues: true,
             guid: basicCardPage.guid,
             persistCheckboxValue: this.persistCheckbox.checked,
             selectedStateKey: basicCardPage.selectedStateKey,
           },
         };
-        let billingAddressGUID = this.form.querySelector("#billingAddressGUID");
-        let selectedOption = billingAddressGUID.selectedOptions.length &&
-                             billingAddressGUID.selectedOptions[0];
-        if (evt.target == this.addressEditLink && selectedOption && selectedOption.value) {
-          nextState["address-page"].title = this.dataset.billingAddressTitleEdit;
-          nextState["address-page"].guid = selectedOption.value;
-        }
         this.requestStore.setState(nextState);
         break;
       }
       case this.backButton: {
+        let currentState = this.requestStore.getState();
         let {
           page,
           request,
           "address-page": addressPage,
           "basic-card-page": basicCardPage,
           selectedShippingAddress,
-        } = this.requestStore.getState();
+        } = currentState;
 
         let nextState = {
           page: {
             id: page.previousId || "payment-summary",
             onboardingWizard: page.onboardingWizard,
           },
         };
 
@@ -378,26 +386,29 @@ export default class BasicCardForm exten
 
   /**
    * @param {Event} event - "invalid" event
    * Note: Keep this in-sync with the equivalent version in address-form.js
    */
   onInvalidField(event) {
     let field = event.target;
     let container = field.closest(`#${field.id}-container`);
-    let errorTextSpan = PaymentDialog.maybeCreateFieldErrorElement(container);
+    let errorTextSpan = paymentRequest.maybeCreateFieldErrorElement(container);
     errorTextSpan.textContent = field.validationMessage;
   }
 
   onInvalidForm() {
     this.saveButton.disabled = true;
   }
 
   updateSaveButtonState() {
-    this.saveButton.disabled = !this.form.checkValidity();
+    const INVALID_CLASS_NAME = "invalid-selected-option";
+    let isValid = this.form.checkValidity() &&
+                  !this.billingAddressPicker.classList.contains(INVALID_CLASS_NAME);
+    this.saveButton.disabled = !isValid;
   }
 
   updateRequiredState() {
     for (let field of this.form.elements) {
       let container = field.closest(".container");
       let span = container.querySelector(".label-text");
       if (!span) {
         // The billing address field doesn't use a label inside the field.
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/billing-address-picker.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+import AddressPicker from "./address-picker.js";
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <billing-address-picker></billing-address-picker>
+ * Extends AddressPicker to treat the <select>'s value as the source of truth
+ */
+
+export default class BillingAddressPicker extends AddressPicker {
+  constructor() {
+    super();
+    this._allowEmptyOption = true;
+  }
+
+  /**
+   * @param {object?} state - See `PaymentsStore.setState`
+   * The value of the picker is the child dropdown element's value
+   * @returns {string} guid
+   */
+  getCurrentValue() {
+    return this.dropdown.value;
+  }
+
+  onChange(event) {
+    this.render(this.requestStore.getState());
+  }
+}
+
+customElements.define("billing-address-picker", BillingAddressPicker);
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -15,16 +15,20 @@ import "./completion-error-page.js";
 import "./order-details.js";
 import "./payment-method-picker.js";
 import "./shipping-option-picker.js";
 
 /* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
  * <payment-dialog></payment-dialog>
+ *
+ * Warning: Do not import this module from any other module as it will import
+ * everything else (see above) and ruin element independence. This can stop
+ * being exported once tests stop depending on it.
  */
 
 export default class PaymentDialog extends PaymentStateSubscriberMixin(HTMLElement) {
   constructor() {
     super();
     this._template = document.getElementById("payment-dialog-template");
     this._cachedState = {};
   }
@@ -410,21 +414,11 @@ export default class PaymentDialog exten
     if (state.changesPrevented) {
       this.setAttribute("changes-prevented", "");
     } else {
       this.removeAttribute("changes-prevented");
     }
     this.setAttribute("complete-status", request.completeStatus);
     this._disabledOverlay.hidden = !state.changesPrevented;
   }
-
-  static maybeCreateFieldErrorElement(container) {
-    let span = container.querySelector(".error-text");
-    if (!span) {
-      span = document.createElement("span");
-      span.className = "error-text";
-      container.appendChild(span);
-    }
-    return span;
-  }
 }
 
 customElements.define("payment-dialog", PaymentDialog);
--- a/browser/components/payments/res/containers/rich-picker.js
+++ b/browser/components/payments/res/containers/rich-picker.js
@@ -11,39 +11,40 @@ export default class RichPicker extends 
   }
 
   constructor() {
     super();
     this.classList.add("rich-picker");
 
     this.dropdown = new RichSelect();
     this.dropdown.addEventListener("change", this);
-    this.dropdown.popupBox.id = "select-" + Math.floor(Math.random() * 1000000);
 
     this.labelElement = document.createElement("label");
-    this.labelElement.setAttribute("for", this.dropdown.popupBox.id);
 
     this.addLink = document.createElement("a");
     this.addLink.className = "add-link";
     this.addLink.href = "javascript:void(0)";
-    this.addLink.textContent = this.dataset.addLinkLabel;
     this.addLink.addEventListener("click", this);
 
     this.editLink = document.createElement("a");
     this.editLink.className = "edit-link";
     this.editLink.href = "javascript:void(0)";
-    this.editLink.textContent = this.dataset.editLinkLabel;
     this.editLink.addEventListener("click", this);
 
     this.invalidLabel = document.createElement("label");
     this.invalidLabel.className = "invalid-label";
-    this.invalidLabel.setAttribute("for", this.dropdown.popupBox.id);
   }
 
   connectedCallback() {
+    if (!this.dropdown.popupBox.id) {
+      this.dropdown.popupBox.id = "select-" + Math.floor(Math.random() * 1000000);
+    }
+    this.labelElement.setAttribute("for", this.dropdown.popupBox.id);
+    this.invalidLabel.setAttribute("for", this.dropdown.popupBox.id);
+
     // The document order, by default, controls tab order so keep that in mind if changing this.
     this.appendChild(this.labelElement);
     this.appendChild(this.dropdown);
     this.appendChild(this.editLink);
     this.appendChild(this.addLink);
     this.appendChild(this.invalidLabel);
     super.connectedCallback();
   }
@@ -56,16 +57,18 @@ export default class RichPicker extends 
 
   render(state) {
     this.editLink.hidden = !this.dropdown.value;
 
     let errorText = this.errorForSelectedOption(state);
     this.classList.toggle("invalid-selected-option",
                           !!errorText);
     this.invalidLabel.textContent = errorText;
+    this.addLink.textContent = this.dataset.addLinkLabel;
+    this.editLink.textContent = this.dataset.editLinkLabel;
   }
 
   get selectedOption() {
     return this.dropdown.selectedOption;
   }
 
   get selectedRichOption() {
     return this.dropdown.selectedRichOption;
@@ -94,13 +97,14 @@ export default class RichPicker extends 
   }
 
   missingFieldsOfSelectedOption() {
     let selectedOption = this.selectedOption;
     if (!selectedOption) {
       return [];
     }
 
-    let fieldNames = this.selectedRichOption.requiredFields;
+    let fieldNames = this.selectedRichOption.requiredFields || [];
+
     // Return all field names that are empty or missing from the option.
     return fieldNames.filter(name => !selectedOption.getAttribute(name));
   }
 }
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -294,13 +294,23 @@ var paymentRequest = {
     let addresses = Object.assign({}, state.savedAddresses, state.tempAddresses);
     return this._sortObjectsByTimeLastUsed(addresses);
   },
 
   getBasicCards(state) {
     let cards = Object.assign({}, state.savedBasicCards, state.tempBasicCards);
     return this._sortObjectsByTimeLastUsed(cards);
   },
+
+  maybeCreateFieldErrorElement(container) {
+    let span = container.querySelector(".error-text");
+    if (!span) {
+      span = document.createElement("span");
+      span.className = "error-text";
+      container.appendChild(span);
+    }
+    return span;
+  },
 };
 
 paymentRequest.init();
 
 export default paymentRequest;
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -188,18 +188,22 @@
         <h2>&orderDetailsLabel;</h2>
         <order-details></order-details>
       </section>
 
       <basic-card-form id="basic-card-page"
                        data-add-basic-card-title="&basicCard.addPage.title;"
                        data-edit-basic-card-title="&basicCard.editPage.title;"
                        data-error-generic-save="&basicCardPage.error.genericSave;"
+
                        data-address-add-link-label="&basicCardPage.addressAddLink.label;"
                        data-address-edit-link-label="&basicCardPage.addressEditLink.label;"
+
+                       data-invalid-address-label="&invalidOption.label;"
+                       data-address-field-separator="&address.fieldSeparator;"
                        data-billing-address-title-add="&billingAddress.addPage.title;"
                        data-billing-address-title-edit="&billingAddress.editPage.title;"
                        data-back-button-label="&basicCardPage.backButton.label;"
                        data-next-button-label="&basicCardPage.nextButton.label;"
                        data-update-button-label="&basicCardPage.updateButton.label;"
                        data-cancel-button-label="&cancelPaymentButton.label;"
                        data-persist-checkbox-label="&basicCardPage.persistCheckbox.label;"
                        data-persist-checkbox-info-tooltip="&basicCardPage.persistCheckbox.infoTooltip;"
--- a/browser/components/payments/test/browser/browser_card_edit.js
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -31,16 +31,17 @@ async function add_link(aOptions = {}) {
     info("add_link, aOptions: " + JSON.stringify(aOptions, null, 2));
     await navigateToAddCardPage(frame);
     info(`add_link, from the add card page,
           verifyPersistCheckbox with expectPersist: ${aOptions.expectDefaultCardPersist}`);
     await verifyPersistCheckbox(frame, {
       checkboxSelector: "basic-card-form .persist-checkbox",
       expectPersist: aOptions.expectDefaultCardPersist,
     });
+
     await spawnPaymentDialogTask(frame, async function checkState(testArgs = {}) {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
         return Object.keys(state.savedBasicCards).length == 1 &&
                Object.keys(state.savedAddresses).length == 1;
@@ -158,22 +159,22 @@ async function add_link(aOptions = {}) {
       let addressColn = testArgs.expectAddressPersist ? state.savedAddresses : state.tempAddresses;
 
       ok(state["basic-card-page"].preserveFieldValues,
          "preserveFieldValues should be set when coming back from address-page");
 
       ok(state["basic-card-page"].billingAddressGUID,
          "billingAddressGUID should be set when coming back from address-page");
 
-      let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
+      let billingAddressPicker = Cu.waiveXrays(
+        content.document.querySelector("basic-card-form billing-address-picker"));
 
-      is(billingAddressSelect.childElementCount, 3,
-         "Three options should exist in the billingAddressSelect");
-      let selectedOption =
-        billingAddressSelect.children[billingAddressSelect.selectedIndex];
+      is(billingAddressPicker.options.length, 3,
+         "Three options should exist in the billingAddressPicker");
+      let selectedOption = billingAddressPicker.dropdown.selectedOption;
       let selectedAddressGuid = selectedOption.value;
       let lastAddress = Object.values(addressColn)[Object.keys(addressColn).length - 1];
       is(selectedAddressGuid, lastAddress.guid, "The select should have the new address selected");
     }, aOptions);
 
     cardOptions = Object.assign({}, {
       checkboxSelector: "basic-card-form .persist-checkbox",
       expectPersist: aOptions.expectCardPersist,
@@ -406,162 +407,173 @@ add_task(async function test_edit_link()
     let card = Object.assign({}, PTU.BasicCards.JohnDoe,
                              { billingAddressGUID: prefilledGuids.address1GUID });
     await addCardRecord(card);
   }
 
   const args = {
     methodData: [PTU.MethodData.basicCard],
     details: PTU.Details.total60USD,
+    prefilledGuids,
   };
-  await spawnInDialogForMerchantTask(PTU.ContentTasks.createAndShowRequest, async function check() {
-    let {
-      PaymentTestUtils: PTU,
-    } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+  await spawnInDialogForMerchantTask(
+    PTU.ContentTasks.createAndShowRequest,
+    async function check({prefilledGuids}) {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
-    let editLink = content.document.querySelector("payment-method-picker .edit-link");
-    is(editLink.textContent, "Edit", "Edit link text");
+      let editLink = content.document.querySelector("payment-method-picker .edit-link");
+      is(editLink.textContent, "Edit", "Edit link text");
 
-    editLink.click();
-
-    let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && state["basic-card-page"].guid;
-    }, "Check edit page state");
+      editLink.click();
 
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return Object.keys(state.savedBasicCards).length == 1 &&
-             Object.keys(state.savedAddresses).length == 1;
-    }, "Check card and address present at beginning of test");
+      let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page" && state["basic-card-page"].guid;
+      }, "Check edit page state");
 
-    let title = content.document.querySelector("basic-card-form h2");
-    is(title.textContent, "Edit Credit Card", "Edit title should be set");
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return Object.keys(state.savedBasicCards).length == 1 &&
+               Object.keys(state.savedAddresses).length == 1;
+      }, "Check card and address present at beginning of test");
 
-    let saveButton = content.document.querySelector("basic-card-form .save-button");
-    is(saveButton.textContent, "Update", "Save button has the correct label");
+      let title = content.document.querySelector("basic-card-form h2");
+      is(title.textContent, "Edit Credit Card", "Edit title should be set");
+
+      let saveButton = content.document.querySelector("basic-card-form .save-button");
+      is(saveButton.textContent, "Update", "Save button has the correct label");
 
-    let card = Object.assign({}, PTU.BasicCards.JohnDoe);
-    // cc-number cannot be modified
-    delete card["cc-number"];
-    card["cc-exp-year"]++;
-    card["cc-exp-month"]++;
+      let card = Object.assign({}, PTU.BasicCards.JohnDoe);
+      // cc-number cannot be modified
+      delete card["cc-number"];
+      card["cc-exp-year"]++;
+      card["cc-exp-month"]++;
 
-    info("overwriting field values");
-    for (let [key, val] of Object.entries(card)) {
-      let field = content.document.getElementById(key);
-      field.value = val;
-      ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-    }
-    ok(content.document.getElementById("cc-number").disabled, "cc-number field should be disabled");
+      info("overwriting field values");
+      for (let [key, val] of Object.entries(card)) {
+        let field = content.document.getElementById(key);
+        field.value = val;
+        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+      }
+      ok(content.document.getElementById("cc-number").disabled,
+         "cc-number field should be disabled");
 
-    let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
-    is(billingAddressSelect.childElementCount, 2,
-       "Two options should exist in the billingAddressSelect");
-    is(billingAddressSelect.selectedIndex, 1,
-       "The prefilled billing address should be selected by default");
+      let billingAddressPicker = Cu.waiveXrays(
+        content.document.querySelector("basic-card-form billing-address-picker"));
 
-    info("Test clicking 'edit' on the empty option first");
-    billingAddressSelect.selectedIndex = 0;
+      let initialSelectedAddressGuid = billingAddressPicker.dropdown.value;
+      is(billingAddressPicker.options.length, 2,
+         "Two options should exist in the billingAddressPicker");
+      is(initialSelectedAddressGuid, prefilledGuids.address1GUID,
+         "The prefilled billing address should be selected by default");
 
-    let addressEditLink = content.document.querySelector(".billingAddressRow .edit-link");
-    addressEditLink.click();
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "address-page" && !state["address-page"].guid;
-    }, "Clicking edit button when the empty option is selected will go to 'add' page (no guid)");
+      info("Test clicking 'add' on the empty option first");
+      billingAddressPicker.dropdown.popupBox.focus();
+      content.fillField(billingAddressPicker.dropdown.popupBox, "");
 
-    let addressTitle = content.document.querySelector("address-form h2");
-    is(addressTitle.textContent, "Add Billing Address",
-       "Address on add address page should be correct");
+      let addressEditLink = content.document.querySelector(".billingAddressRow .edit-link");
+      ok(addressEditLink && !content.isVisible(addressEditLink),
+         "The edit link is hidden when empty option is selected");
 
-    let addressBackButton = content.document.querySelector("address-form .back-button");
-    addressBackButton.click();
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
-             Object.keys(state.savedAddresses).length == 1;
-    }, "Check we're back at basic-card page with no state changed after adding");
+      let addressAddLink = content.document.querySelector(".billingAddressRow .add-link");
+      addressAddLink.click();
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page" && !state["address-page"].guid;
+      }, "Clicking add button when the empty option is selected will go to 'add' page (no guid)");
 
-    info("Go back to previously selected option before clicking 'edit' now");
-    billingAddressSelect.selectedIndex = 1;
+      let addressTitle = content.document.querySelector("address-form h2");
+      is(addressTitle.textContent, "Add Billing Address",
+         "Address on add address page should be correct");
 
-    let selectedOption = billingAddressSelect.selectedOptions.length &&
-                         billingAddressSelect.selectedOptions[0];
-    ok(selectedOption && selectedOption.value, "select should have a selected option value");
+      let addressBackButton = content.document.querySelector("address-form .back-button");
+      addressBackButton.click();
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
+               Object.keys(state.savedAddresses).length == 1;
+      }, "Check we're back at basic-card page with no state changed after adding");
 
-    addressEditLink.click();
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "address-page" && state["address-page"].guid;
-    }, "Check address page state (editing)");
+      info("Go back to previously selected option before clicking 'edit' now");
+      billingAddressPicker.dropdown.value = initialSelectedAddressGuid;
+
+      let selectedOption = billingAddressPicker.dropdown.selectedOption;
+      ok(selectedOption && selectedOption.value, "select should have a selected option value");
 
-    is(addressTitle.textContent, "Edit Billing Address",
-       "Address on edit address page should be correct");
+      addressEditLink.click();
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page" && state["address-page"].guid;
+      }, "Check address page state (editing)");
 
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return Object.keys(state.savedBasicCards).length == 1;
-    }, "Check card was not added again when clicking the 'edit' address button");
+      is(addressTitle.textContent, "Edit Billing Address",
+         "Address on edit address page should be correct");
+
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return Object.keys(state.savedBasicCards).length == 1;
+      }, "Check card was not added again when clicking the 'edit' address button");
 
-    addressBackButton.click();
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
-             Object.keys(state.savedAddresses).length == 1;
-    }, "Check we're back at basic-card page with no state changed after editing");
+      addressBackButton.click();
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
+               Object.keys(state.savedAddresses).length == 1;
+      }, "Check we're back at basic-card page with no state changed after editing");
 
-    for (let [key, val] of Object.entries(card)) {
-      let field = content.document.getElementById(key);
-      is(field.value, val, "Field should still have previous value entered");
-    }
+      for (let [key, val] of Object.entries(card)) {
+        let field = content.document.getElementById(key);
+        is(field.value, val, "Field should still have previous value entered");
+      }
 
-    selectedOption = billingAddressSelect.selectedOptions.length &&
-                     billingAddressSelect.selectedOptions[0];
-    ok(selectedOption && selectedOption.value, "select should have a selected option value");
+      selectedOption = billingAddressPicker.dropdown.selectedOption;
+      ok(selectedOption && selectedOption.value, "select should have a selected option value");
 
-    addressEditLink.click();
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "address-page" && state["address-page"].guid;
-    }, "Check address page state (editing)");
+      addressEditLink.click();
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page" && state["address-page"].guid;
+      }, "Check address page state (editing)");
 
-    info("modify some address fields");
-    for (let key of ["given-name", "tel", "organization", "street-address"]) {
-      let field = content.document.getElementById(key);
-      if (!field) {
-        ok(false, `${key} field not found`);
+      info("modify some address fields");
+      for (let key of ["given-name", "tel", "organization", "street-address"]) {
+        let field = content.document.getElementById(key);
+        if (!field) {
+          ok(false, `${key} field not found`);
+        }
+        field.focus();
+        EventUtils.sendKey("BACK_SPACE", content.window);
+        EventUtils.sendString("7", content.window);
+        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
       }
-      field.focus();
-      EventUtils.sendKey("BACK_SPACE", content.window);
-      EventUtils.sendString("7", content.window);
-      ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-    }
 
-    content.document.querySelector("address-form button.save-button").click();
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
-             Object.keys(state.savedAddresses).length == 1;
-    }, "Check still only one address and we're back on basic-card page");
+      content.document.querySelector("address-form button.save-button").click();
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
+               Object.keys(state.savedAddresses).length == 1;
+      }, "Check still only one address and we're back on basic-card page");
 
-    is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel.slice(0, -1) + "7",
-       "Check that address was edited and saved");
+      is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel.slice(0, -1) + "7",
+         "Check that address was edited and saved");
 
-    content.document.querySelector("basic-card-form button.save-button").click();
+      content.document.querySelector("basic-card-form button.save-button").click();
 
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      let cards = Object.entries(state.savedBasicCards);
-      return cards.length == 1 &&
-             cards[0][1]["cc-name"] == card["cc-name"];
-    }, "Check card was edited");
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        let cards = Object.entries(state.savedBasicCards);
+        return cards.length == 1 &&
+               cards[0][1]["cc-name"] == card["cc-name"];
+      }, "Check card was edited");
 
-    let cardGUIDs = Object.keys(state.savedBasicCards);
-    is(cardGUIDs.length, 1, "Check there is still one card");
-    let savedCard = state.savedBasicCards[cardGUIDs[0]];
-    is(savedCard["cc-number"], "************1111", "Card number should be masked and unmodified.");
-    for (let [key, val] of Object.entries(card)) {
-      is(savedCard[key], val, "Check updated " + key);
-    }
+      let cardGUIDs = Object.keys(state.savedBasicCards);
+      is(cardGUIDs.length, 1, "Check there is still one card");
+      let savedCard = state.savedBasicCards[cardGUIDs[0]];
+      is(savedCard["cc-number"], "************1111",
+         "Card number should be masked and unmodified.");
+      for (let [key, val] of Object.entries(card)) {
+        is(savedCard[key], val, "Check updated " + key);
+      }
 
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "payment-summary";
-    }, "Switched back to payment-summary");
-  }, args);
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "payment-summary";
+      }, "Switched back to payment-summary");
+    }, args);
 });
 
 add_task(async function test_invalid_network_card_edit() {
   // add an address and card linked to this address
   let prefilledGuids = await setup([PTU.Addresses.TimBL]);
   {
     let card = Object.assign({}, PTU.BasicCards.JohnDoe,
                              { billingAddressGUID: prefilledGuids.address1GUID });
--- a/browser/components/payments/test/mochitest/mochitest.ini
+++ b/browser/components/payments/test/mochitest/mochitest.ini
@@ -15,16 +15,17 @@ skip-if = !e10s
 [test_accepted_cards.html]
 [test_address_form.html]
 [test_address_option.html]
 skip-if = os == "linux" || os == "win" # Bug 1493216
 [test_address_picker.html]
 [test_basic_card_form.html]
 skip-if = debug || asan # Bug 1493349
 [test_basic_card_option.html]
+[test_billing_address_picker.html]
 [test_completion_error_page.html]
 [test_currency_amount.html]
 [test_labelled_checkbox.html]
 [test_order_details.html]
 [test_payer_address_picker.html]
 [test_payment_dialog.html]
 [test_payment_dialog_required_top_level_items.html]
 [test_payment_details_item.html]
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -85,28 +85,28 @@ add_task(async function test_initialStat
   is(fieldsVisiblyInvalid.length, 0, "Check no fields are visibly invalid on an empty 'add' form");
 
   form.remove();
 });
 
 add_task(async function test_backButton() {
   let form = new AddressForm();
   form.dataset.backButtonLabel = "Back";
+  await form.promiseReady;
+  display.appendChild(form);
+
   await form.requestStore.setState({
     page: {
       id: "address-page",
     },
     "address-page": {
       selectedStateKey: ["selectedShippingAddress"],
       title: "Sample page title",
     },
   });
-
-  await form.promiseReady;
-  display.appendChild(form);
   await asyncElementRendered();
 
   let stateChangePromise = promiseStateChange(form.requestStore);
   is(form.pageTitleHeading.textContent, "Sample page title", "Check title");
 
   is(form.backButton.textContent, "Back", "Check label");
   form.backButton.scrollIntoView();
   synthesizeMouseAtCenter(form.backButton, {});
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -56,34 +56,52 @@ function checkCCForm(customEl, expectedC
   for (let propName of CC_PROPERTY_NAMES) {
     let expectedVal = expectedCard[propName] || "";
     is(document.getElementById(propName).value,
        expectedVal.toString(),
        `Check ${propName}`);
   }
 }
 
+function createAddressRecord(source, props = {}) {
+  let address = Object.assign({}, source, props);
+  if (!address.name) {
+    address.name = `${address["given-name"]} ${address["family-name"]}`;
+  }
+  return address;
+}
+
 add_task(async function setup_once() {
   let templateFrame = document.getElementById("templateFrame");
   await SimpleTest.promiseFocus(templateFrame.contentWindow);
   let displayEl = document.getElementById("display");
   importDialogDependencies(templateFrame, displayEl);
 });
 
 add_task(async function test_initialState() {
   let form = new BasicCardForm();
+
+  await form.requestStore.setState({
+    savedAddresses: {
+      "TimBLGUID": createAddressRecord(PTU.Addresses.TimBL),
+    },
+  });
+
   let {page} = form.requestStore.getState();
   is(page.id, "payment-summary", "Check initial page");
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
   is(page.id, "payment-summary", "Check initial page after appending");
 
   // :-moz-ui-invalid, unlike :invalid, only applies to fields showing the error outline.
   let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
+  for (let field of fieldsVisiblyInvalid) {
+    info("invalid field: " + field.localName + "#" + field.id + "." + field.className);
+  }
   is(fieldsVisiblyInvalid.length, 0, "Check no fields are visibly invalid on an empty 'add' form");
 
   form.remove();
 });
 
 add_task(async function test_backButton() {
   let form = new BasicCardForm();
   form.dataset.backButtonLabel = "Back";
@@ -111,28 +129,28 @@ add_task(async function test_backButton(
 
   form.remove();
 });
 
 add_task(async function test_saveButton() {
   let form = new BasicCardForm();
   form.dataset.nextButtonLabel = "Next";
   form.dataset.errorGenericSave = "Generic error";
+  form.dataset.invalidAddressLabel = "Invalid";
+
   await form.promiseReady;
   display.appendChild(form);
 
-  let address1 = deepClone(PTU.Addresses.TimBL);
-  address1.guid = "TimBLGUID";
-  let address2 = deepClone(PTU.Addresses.TimBL2);
-  address2.guid = "TimBL2GUID";
-
+  let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
+  let address2 = createAddressRecord(PTU.Addresses.TimBL2, {guid: "TimBL2GUID"});
 
   await form.requestStore.setState({
     request: {
       paymentMethods,
+      paymentDetails: {},
     },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
       [address2.guid]: deepClone(address2),
     },
   });
 
   await asyncElementRendered();
@@ -269,20 +287,18 @@ add_task(async function test_add_selecte
   display.appendChild(form);
   await asyncElementRendered();
 
   info("have an existing card in storage");
   let card1 = deepClone(PTU.BasicCards.JohnDoe);
   card1.guid = "9864798564";
   card1["cc-exp-year"] = 2011;
 
-  let address1 = deepClone(PTU.Addresses.TimBL);
-  address1.guid = "TimBLGUID";
-  let address2 = deepClone(PTU.Addresses.TimBL2);
-  address2.guid = "TimBL2GUID";
+  let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
+  let address2 = createAddressRecord(PTU.Addresses.TimBL2, { guid: "TimBL2GUID" });
 
   await form.requestStore.setState({
     page: {
       id: "basic-card-page",
     },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
       [address2.guid]: deepClone(address2),
@@ -307,18 +323,17 @@ add_task(async function test_add_noSelec
   display.appendChild(form);
   await asyncElementRendered();
 
   info("have an existing card in storage but unused");
   let card1 = deepClone(PTU.BasicCards.JohnDoe);
   card1.guid = "9864798564";
   card1["cc-exp-year"] = 2011;
 
-  let address1 = deepClone(PTU.Addresses.TimBL);
-  address1.guid = "TimBLGUID";
+  let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
 
   await form.requestStore.setState({
     page: {
       id: "basic-card-page",
     },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
     },
@@ -347,28 +362,28 @@ add_task(async function test_add_noSelec
 
 add_task(async function test_edit() {
   let form = new BasicCardForm();
   form.dataset.updateButtonLabel = "Update";
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
-  let address1 = deepClone(PTU.Addresses.TimBL);
-  address1.guid = "TimBLGUID";
+  let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
 
   info("test year before current");
   let card1 = deepClone(PTU.BasicCards.JohnDoe);
   card1.guid = "9864798564";
   card1["cc-exp-year"] = 2011;
   card1.billingAddressGUID = address1.guid;
 
   await form.requestStore.setState({
     request: {
       paymentMethods,
+      paymentDetails: {},
     },
     page: {
       id: "basic-card-page",
     },
     "basic-card-page": {
       guid: card1.guid,
       selectedStateKey: "selectedPaymentCard",
     },
@@ -378,16 +393,17 @@ add_task(async function test_edit() {
     savedBasicCards: {
       [card1.guid]: deepClone(card1),
     },
   });
   await asyncElementRendered();
   is(form.saveButton.textContent, "Update", "Check label");
   is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
      "Check no fields are visibly invalid on an 'edit' form with a complete card");
+
   checkCCForm(form, card1);
   ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid card");
   ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when editing a card");
 
   let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
   ok(requiredElements.length, "There should be at least one required element");
   is(requiredElements.length, 5, "Number of required elements");
   for (let element of requiredElements) {
@@ -455,24 +471,36 @@ add_task(async function test_edit() {
   form.remove();
 });
 
 add_task(async function test_field_validity_updates() {
   let form = new BasicCardForm();
   form.dataset.updateButtonLabel = "Update";
   await form.promiseReady;
   display.appendChild(form);
+
+  let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
+  await form.requestStore.setState({
+    request: {
+      paymentMethods,
+      paymentDetails: {},
+    },
+    savedAddresses: {
+      [address1.guid]: deepClone(address1),
+    },
+  });
   await asyncElementRendered();
 
   let ccNumber = form.form.querySelector("#cc-number");
   let nameInput = form.form.querySelector("#cc-name");
   let typeInput = form.form.querySelector("#cc-type");
   let cscInput = form.form.querySelector("csc-input input");
   let monthInput = form.form.querySelector("#cc-exp-month");
   let yearInput = form.form.querySelector("#cc-exp-year");
+  let addressPicker = form.querySelector("#billingAddressGUID");
 
   info("test with valid cc-number but missing cc-name");
   fillField(ccNumber, "4111111111111111");
   ok(ccNumber.checkValidity(), "cc-number field is valid with good input");
   ok(!nameInput.checkValidity(), "cc-name field is invalid when empty");
   ok(form.saveButton.disabled, "Save button should be disabled with incomplete input");
 
   info("correct by adding cc-name and expiration values");
@@ -482,16 +510,26 @@ add_task(async function test_field_valid
   fillField(yearInput, year);
   fillField(typeInput, "visa");
   fillField(cscInput, "456");
   ok(ccNumber.checkValidity(), "cc-number field is valid with good input");
   ok(nameInput.checkValidity(), "cc-name field is valid with a value");
   ok(monthInput.checkValidity(), "cc-exp-month field is valid with a value");
   ok(yearInput.checkValidity(), "cc-exp-year field is valid with a value");
   ok(typeInput.checkValidity(), "cc-type field is valid with a value");
+
+  // should auto-select the first billing address
+  ok(addressPicker.value, "An address is selected: " + addressPicker.value);
+
+  let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
+  for (let field of fieldsVisiblyInvalid) {
+    info("invalid field: " + field.localName + "#" + field.id + "." + field.className);
+  }
+  is(fieldsVisiblyInvalid.length, 0, "No fields are visibly invalid");
+
   ok(!form.saveButton.disabled, "Save button should not be disabled with good input");
 
   info("edit to make the cc-number invalid");
   ccNumber.focus();
   sendString("aa");
   nameInput.focus();
   sendString("Surname");
 
@@ -513,16 +551,27 @@ add_task(async function test_field_valid
   form.remove();
 });
 
 add_task(async function test_numberCustomValidityReset() {
   let form = new BasicCardForm();
   form.dataset.updateButtonLabel = "Update";
   await form.promiseReady;
   display.appendChild(form);
+
+  let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
+  await form.requestStore.setState({
+    request: {
+      paymentMethods,
+      paymentDetails: {},
+    },
+    savedAddresses: {
+      [address1.guid]: deepClone(address1),
+    },
+  });
   await asyncElementRendered();
 
   fillField(form.querySelector("#cc-number"), "junk");
   sendKey("TAB");
   ok(form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is visibly invalid");
 
   info("simulate triggering an add again to reset the form");
   await form.requestStore.setState({
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/test/mochitest/test_billing_address_picker.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the address-picker component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the billing-address-picker component</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script src="payments_common.js"></script>
+  <script src="../../res/unprivileged-fallbacks.js"></script>
+  <script src="autofillEditForms.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/>
+  <link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
+  <link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+    <billing-address-picker id="picker1"
+                    data-field-separator=", "
+                    data-invalid-label="Picker1: Missing or Invalid"
+                    selected-state-key="basic-card-page|billingAddressGUID"></billing-address-picker>
+    <select id="theOptions">
+      <option></option>
+      <option value="48bnds6854t">48bnds6854t</option>
+      <option value="68gjdh354j" selected="">68gjdh354j</option>
+    </select>
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="module">
+/** Test the billing-address-picker component **/
+
+import BillingAddressPicker from "../../res/containers/billing-address-picker.js";
+
+let picker1 = document.getElementById("picker1");
+let addresses = {
+  "48bnds6854t": {
+    "address-level1": "MI",
+    "address-level2": "Some City",
+    "country": "US",
+    "guid": "48bnds6854t",
+    "name": "Mr. Foo",
+    "postal-code": "90210",
+    "street-address": "123 Sesame Street,\nApt 40",
+    "tel": "+1 519 555-5555",
+    timeLastUsed: 200,
+  },
+  "68gjdh354j": {
+    "address-level1": "CA",
+    "address-level2": "Mountain View",
+    "country": "US",
+    "guid": "68gjdh354j",
+    "name": "Mrs. Bar",
+    "postal-code": "94041",
+    "street-address": "P.O. Box 123",
+    "tel": "+1 650 555-5555",
+    timeLastUsed: 300,
+  },
+  "abcde12345": {
+    "address-level2": "Mountain View",
+    "country": "US",
+    "guid": "abcde12345",
+    "name": "Mrs. Fields",
+    timeLastUsed: 100,
+  },
+};
+
+add_task(async function test_empty() {
+  ok(picker1, "Check picker1 exists");
+  let {savedAddresses} = picker1.requestStore.getState();
+  is(Object.keys(savedAddresses).length, 0, "Check empty initial state");
+  is(picker1.editLink.hidden, true, "Check that picker edit link is hidden");
+  is(picker1.options.length, 1, "Check only the empty option is present");
+  ok(picker1.dropdown.selectedOption, "Has a selectedOption");
+  is(picker1.dropdown.value, "", "Has empty value");
+
+  // update state to trigger render without changing available addresses
+  picker1.requestStore.setState({
+    "basic-card-page": {
+      "someKey": "someValue",
+    },
+  });
+  await asyncElementRendered();
+
+  is(picker1.dropdown.popupBox.children.length, 1, "Check only the empty option is present");
+  ok(picker1.dropdown.selectedOption, "Has a selectedOption");
+  is(picker1.dropdown.value, "", "Has empty value");
+});
+
+add_task(async function test_getCurrentValue() {
+  picker1.requestStore.setState({
+    "basic-card-page": {
+      "billingAddressGUID": "68gjdh354j",
+    },
+    savedAddresses: addresses,
+  });
+  await asyncElementRendered();
+
+  picker1.dropdown.popupBox.value = "abcde12345";
+
+  is(picker1.options.length, 4, "Check we have options for each address + empty one");
+  is(picker1.getCurrentValue(picker1.requestStore.getState()), "abcde12345",
+     "Initial/current value reflects the <select>.value, " +
+     "not whatever is in the state at the selectedStateKey");
+});
+
+add_task(async function test_wrapPopupBox() {
+  let picker = new BillingAddressPicker();
+  picker.dropdown.popupBox = document.querySelector("#theOptions");
+  picker.dataset.invalidLabel = "Invalid";
+  picker.setAttribute("label", "The label");
+  picker.setAttribute("selected-state-key", "basic-card-page|billingAddressGUID");
+
+  document.querySelector("#display").appendChild(picker);
+
+  is(picker.labelElement.getAttribute("for"), "theOptions",
+     "The label points at the right element");
+  is(picker.invalidLabel.getAttribute("for"), "theOptions",
+     "The invalidLabel points at the right element");
+});
+
+</script>
+
+</body>
+</html>
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -797,17 +797,17 @@ var gMainPane = {
   hideConfirmLanguageChangeMessageBar() {
     let messageBar = document.getElementById("confirmBrowserLanguage");
     messageBar.hidden = true;
     messageBar.querySelector(".message-bar-button").removeAttribute("locales");
     gMainPane.requestingLocales = null;
   },
 
   /* Confirm the locale change and restart the browser in the new locale. */
-  confirmBrowserLanguageChange() {
+  confirmBrowserLanguageChange(event) {
     let localesString = (event.target.getAttribute("locales") || "").trim();
     if (!localesString || localesString.length == 0) {
       return;
     }
     let locales = localesString.split(",");
     Services.locale.requestedLocales = locales;
 
     // Restart with the new locale.
@@ -817,19 +817,17 @@ var gMainPane = {
       Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
     }
   },
 
   /* Show or hide the confirm change message bar based on the new locale. */
   onBrowserLanguageChange(event) {
     let locale = event.target.value;
 
-    // If there is no value, then this is the search option, leave the
-    // message bar in its current state.
-    if (!locale) {
+    if (locale == "search") {
       return;
     } else if (locale == Services.locale.requestedLocale) {
       this.hideConfirmLanguageChangeMessageBar();
       return;
     }
 
     let locales = Array.from(new Set([
       locale,
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -80,17 +80,17 @@
               preference="browser.ctrlTab.recentlyUsedOrder"
               oncommand="Services.prefs.clearUserPref('browser.ctrlTab.migrated');"/>
 
     <checkbox id="linkTargeting" data-l10n-id="open-new-link-as-tabs"
               preference="browser.link.open_newwindow"
               onsyncfrompreference="return gMainPane.readLinkTarget();"
               onsynctopreference="return gMainPane.writeLinkTarget();"/>
 
-    <checkbox id="warnCloseMultiple" data-l10n-id="warn-on-close-multiple-tabs"
+    <checkbox id="warnCloseMultiple" data-l10n-id="warn-on-quit-close-multiple-tabs"
               preference="browser.tabs.warnOnClose"/>
 
     <checkbox id="warnOpenMany" data-l10n-id="warn-on-open-many-tabs"
               preference="browser.tabs.warnOnOpen"/>
 
     <checkbox id="switchToNewTabs" data-l10n-id="switch-links-to-new-tabs"
               preference="browser.tabs.loadInBackground"/>
 
@@ -295,17 +295,17 @@
               data-l10n-id="manage-browser-languages-button"
               oncommand="gMainPane.showBrowserLanguages({search: false})"/>
     </hbox>
   </vbox>
   <hbox id="confirmBrowserLanguage" class="message-bar" align="center" hidden="true">
     <image class="message-bar-icon"/>
     <hbox class="message-bar-content" align="center" flex="1">
       <description class="message-bar-description" flex="1"/>
-      <button class="message-bar-button" oncommand="gMainPane.confirmBrowserLanguageChange()"/>
+      <button class="message-bar-button" oncommand="gMainPane.confirmBrowserLanguageChange(event)"/>
     </hbox>
   </hbox>
 
   <hbox id="languagesBox" align="center">
     <description flex="1" control="chooseLanguage" data-l10n-id="choose-language-description"/>
     <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
     <hbox>
       <button id="chooseLanguage"
--- a/browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
+++ b/browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
@@ -306,16 +306,19 @@ add_task(async function testInstallFromA
 
   let doc = gBrowser.contentDocument;
   let messageBar = doc.getElementById("confirmBrowserLanguage");
   is(messageBar.hidden, true, "The message bar is hidden at first");
 
   // Open the dialog.
   let {dialogDoc, available, requested} = await openDialog(doc, true);
 
+  // Make sure the message bar is still hidden.
+  is(messageBar.hidden, true, "The message bar is still hidden after searching");
+
   let dropdown = dialogDoc.getElementById("availableLocales");
   if (dropdown.itemCount == 1) {
     await waitForMutation(
       dropdown.firstElementChild,
       {childList: true},
       target => dropdown.itemCount > 1);
   }
 
--- a/browser/components/urlbar/UrlbarController.jsm
+++ b/browser/components/urlbar/UrlbarController.jsm
@@ -74,16 +74,17 @@ class QueryContext {
  * results and returns them to the UI for display.
  *
  * Listeners may be added to listen for the results. They must support the
  * following methods which may be called when a query is run:
  *
  * - onQueryStarted(queryContext)
  * - onQueryResults(queryContext)
  * - onQueryCancelled(queryContext)
+ * - onQueryFinished(queryContext)
  */
 class UrlbarController {
   /**
    * Initialises the class. The manager may be overridden here, this is for
    * test purposes.
    *
    * @param {object} options
    *   The initial options for UrlbarController.
@@ -114,16 +115,18 @@ class UrlbarController {
    * @param {QueryContext} queryContext The query details.
    */
   async startQuery(queryContext) {
     queryContext.autoFill = Services.prefs.getBoolPref("browser.urlbar.autoFill", true);
 
     this._notify("onQueryStarted", queryContext);
 
     await this.manager.startQuery(queryContext, this);
+
+    this._notify("onQueryFinished", queryContext);
   }
 
   /**
    * Cancels an in-progress query. Note, queries may continue running if they
    * can't be canceled.
    *
    * @param {QueryContext} queryContext The query details.
    */
--- a/browser/components/urlbar/UrlbarMatch.jsm
+++ b/browser/components/urlbar/UrlbarMatch.jsm
@@ -20,23 +20,42 @@ XPCOMUtils.defineLazyModuleGetters(this,
 });
 
 /**
  * Class used to create a match.
  */
 class UrlbarMatch {
   /**
    * Creates a match.
-   * @param {integer} matchType one of UrlbarUtils.MATCHTYPE.* values
+   * @param {integer} matchType one of UrlbarUtils.MATCH_TYPE.* values
+   * @param {integer} matchSource one of UrlbarUtils.MATCH_SOURCE.* values
    * @param {object} payload data for this match. A payload should always
    *        contain a way to extract a final url to visit. The url getter
    *        should have a case for each of the types.
    */
-  constructor(matchType, payload) {
+  constructor(matchType, matchSource, payload) {
+    // Type describes the payload and visualization that should be used for
+    // this match.
+    if (!Object.values(UrlbarUtils.MATCH_TYPE).includes(matchType)) {
+      throw new Error("Invalid match type");
+    }
     this.type = matchType;
+
+    // Source describes which data has been used to derive this match. In case
+    // multiple sources are involved, use the more privacy restricted.
+    if (!Object.values(UrlbarUtils.MATCH_SOURCE).includes(matchSource)) {
+      throw new Error("Invalid match source");
+    }
+    this.source = matchSource;
+
+    // The payload contains match data. Some of the data is common across
+    // multiple types, but most of it will vary.
+    if (!payload || (typeof payload != "object") || !payload.url) {
+      throw new Error("Invalid match payload");
+    }
     this.payload = payload;
   }
 
   /**
    * Returns a final destination for this match.
    * Different kind of matches may have different ways to express this value,
    * and this is a common getter for all of them.
    * @returns {string} a url to load when this match is confirmed byt the user.
--- a/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
+++ b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
@@ -87,16 +87,22 @@ class ProviderOpenTabs {
   /**
    * Returns the type of this provider.
    * @returns {integer} one of the types from UrlbarProvidersManager.TYPE.*
    */
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
   }
 
+  get sources() {
+    return [
+      UrlbarUtils.MATCH_SOURCE.TABS,
+    ];
+  }
+
   /**
    * Registers a tab as open.
    * @param {string} url Address of the tab
    * @param {integer} userContextId Containers user context id
    */
   registerOpenTab(url, userContextId = 0) {
     if (!this.openTabs.has(userContextId)) {
       this.openTabs.set(userContextId, []);
@@ -144,17 +150,18 @@ class ProviderOpenTabs {
     await conn.executeCached(`
       SELECT url, userContextId
       FROM moz_openpages_temp
     `, {}, (row, cancel) => {
       if (!this.queries.has(queryContext)) {
         cancel();
         return;
       }
-      addCallback(this, new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, {
+      addCallback(this, new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                                        UrlbarUtils.MATCH_SOURCE.TABS, {
         url: row.getResultByName("url"),
         userContextId: row.getResultByName("userContextId"),
       }));
     });
     // We are done.
     this.queries.delete(queryContext);
   }
 
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -151,58 +151,72 @@ class Query {
   constructor(queryContext, controller, providers) {
     this.context = queryContext;
     this.context.results = [];
     this.controller = controller;
     this.providers = providers;
     this.started = false;
     this.canceled = false;
     this.complete = false;
+    // Array of acceptable MATCH_SOURCE values for this query. Providers not
+    // returning any of these will be skipped, as well as matches not part of
+    // this subset (Note we still expect the provider to do its own internal
+    // filtering, our additional filtering will be for sanity).
+    this.acceptableSources = [];
   }
 
   /**
    * Starts querying.
    */
   async start() {
     if (this.started) {
       throw new Error("This Query has been started already");
     }
     this.started = true;
     UrlbarTokenizer.tokenize(this.context);
+    this.acceptableSources = getAcceptableMatchSources(this.context);
+    logger.debug(`Acceptable sources ${this.acceptableSources}`);
 
     let promises = [];
     for (let provider of this.providers.get(UrlbarUtils.PROVIDER_TYPE.IMMEDIATE).values()) {
       if (this.canceled) {
         break;
       }
-      promises.push(provider.startQuery(this.context, this.add));
+      if (this._providerHasAcceptableSources(provider)) {
+        promises.push(provider.startQuery(this.context, this.add));
+      }
     }
 
     // Tracks the delay timer. We will fire (in this specific case, cancel would
     // do the same, since the callback is empty) the timer when the search is
     // canceled, unblocking start().
     this._sleepTimer = new SkippableTimer(() => {}, UrlbarPrefs.get("delay"));
     await this._sleepTimer.promise;
 
     for (let providerType of [UrlbarUtils.PROVIDER_TYPE.NETWORK,
                               UrlbarUtils.PROVIDER_TYPE.PROFILE,
                               UrlbarUtils.PROVIDER_TYPE.EXTENSION]) {
       for (let provider of this.providers.get(providerType).values()) {
         if (this.canceled) {
           break;
         }
-        promises.push(provider.startQuery(this.context, this.add.bind(this)));
+        if (this._providerHasAcceptableSources(provider)) {
+          promises.push(provider.startQuery(this.context, this.add.bind(this)));
+        }
       }
     }
 
-    await Promise.all(promises.map(p => p.catch(Cu.reportError)));
+    logger.info(`Queried ${promises.length} providers`);
+    if (promises.length) {
+      await Promise.all(promises.map(p => p.catch(Cu.reportError)));
 
-    if (this._chunkTimer) {
-      // All the providers are done returning results, so we can stop chunking.
-      await this._chunkTimer.fire();
+      if (this._chunkTimer) {
+        // All the providers are done returning results, so we can stop chunking.
+        await this._chunkTimer.fire();
+      }
     }
 
     // Nothing should be failing above, since we catch all the promises, thus
     // this is not in a finally for now.
     this.complete = true;
   }
 
   /**
@@ -229,22 +243,22 @@ class Query {
 
   /**
    * Adds a match returned from a provider to the results set.
    * @param {object} provider
    * @param {object} match
    */
   add(provider, match) {
     // Stop returning results as soon as we've been canceled.
-    if (this.canceled) {
+    if (this.canceled || !this.acceptableSources.includes(match.source)) {
       return;
     }
+
     this.context.results.push(match);
 
-
     let notifyResults = () => {
       if (this._chunkTimer) {
         this._chunkTimer.cancel().catch(Cu.reportError);
         delete this._chunkTimer;
       }
       // TODO:
       //  * pass results to a muxer before sending them back to the controller.
       this.controller.receiveResults(this.context);
@@ -253,16 +267,25 @@ class Query {
     // If the provider is not of immediate type, chunk results, to improve the
     // dataflow and reduce UI flicker.
     if (provider.type == UrlbarUtils.PROVIDER_TYPE.IMMEDIATE) {
       notifyResults();
     } else if (!this._chunkTimer) {
       this._chunkTimer = new SkippableTimer(notifyResults, CHUNK_MATCHES_DELAY_MS);
     }
   }
+
+  /**
+   * Returns whether a provider's sources are acceptable for this query.
+   * @param {object} provider A provider object.
+   * @returns {boolean}whether the provider sources are acceptable.
+   */
+  _providerHasAcceptableSources(provider) {
+    return provider.sources.some(s => this.acceptableSources.includes(s));
+  }
 }
 
 /**
  * Class used to create a timer that can be manually fired, to immediately
  * invoke the callback, or canceled, as necessary.
  * Examples:
  *   let timer = new SkippableTimer();
  *   // Invokes the callback immediately without waiting for the delay.
@@ -312,8 +335,68 @@ class SkippableTimer {
    */
   cancel() {
     logger.debug(`Canceling timer for ${this._timer.delay}ms`);
     this._timer.cancel();
     delete this._timer;
     return this.fire();
   }
 }
+
+/**
+ * Gets an array of the provider sources accepted for a given QueryContext.
+ * @param {object} context The QueryContext to examine
+ * @returns {array} Array of accepted sources
+ */
+function getAcceptableMatchSources(context) {
+  let acceptedSources = [];
+  // There can be only one restrict token about sources.
+  let restrictToken = context.tokens.find(t => [ UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
+                                                 UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+                                                 UrlbarTokenizer.TYPE.RESTRICT_TAG,
+                                                 UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE,
+                                                 UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+                                               ].includes(t.type));
+  let restrictTokenType = restrictToken ? restrictToken.type : undefined;
+  for (let source of Object.values(UrlbarUtils.MATCH_SOURCE)) {
+    switch (source) {
+      case UrlbarUtils.MATCH_SOURCE.BOOKMARKS:
+        if (UrlbarPrefs.get("suggest.bookmark") &&
+            (!restrictTokenType ||
+             restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK ||
+             restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_TAG)) {
+          acceptedSources.push(source);
+        }
+        break;
+      case UrlbarUtils.MATCH_SOURCE.HISTORY:
+        if (UrlbarPrefs.get("suggest.history") &&
+            (!restrictTokenType ||
+             restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_HISTORY)) {
+          acceptedSources.push(source);
+        }
+        break;
+      case UrlbarUtils.MATCH_SOURCE.SEARCHENGINE:
+        if (UrlbarPrefs.get("suggest.searches") &&
+            (!restrictTokenType ||
+             restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_SEARCH)) {
+          acceptedSources.push(source);
+        }
+        break;
+      case UrlbarUtils.MATCH_SOURCE.TABS:
+        if (UrlbarPrefs.get("suggest.openpage") &&
+            (!restrictTokenType ||
+             restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE)) {
+          acceptedSources.push(source);
+        }
+        break;
+      case UrlbarUtils.MATCH_SOURCE.OTHER_NETWORK:
+        if (!context.isPrivate) {
+          acceptedSources.push(source);
+        }
+        break;
+      case UrlbarUtils.MATCH_SOURCE.OTHER_LOCAL:
+      default:
+        acceptedSources.push(source);
+        break;
+    }
+  }
+  return acceptedSources;
+}
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -62,16 +62,29 @@ var UrlbarUtils = {
 
   // Defines UrlbarMatch types.
   MATCH_TYPE: {
     // Indicates an open tab.
     // The payload is: { url, userContextId }
     TAB_SWITCH: 1,
   },
 
+  // This defines the source of matches returned by a provider. Each provider
+  // can return matches from more than one source. This is used by the
+  // ProvidersManager to decide which providers must be queried and which
+  // matches can be returned.
+  MATCH_SOURCE: {
+    BOOKMARKS: 1,
+    HISTORY: 2,
+    SEARCHENGINE: 3,
+    TABS: 4,
+    OTHER_LOCAL: 5,
+    OTHER_NETWORK: 6,
+  },
+
   /**
    * Adds a url to history as long as it isn't in a private browsing window,
    * and it is valid.
    *
    * @param {string} url The url to add to history.
    * @param {nsIDomWindow} window The window from where the url is being added.
    */
   addToUrlbarHistory(url, window) {
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -78,16 +78,24 @@ class UrlbarView {
     this.panel.hidePopup();
   }
 
   // UrlbarController listener methods.
   onQueryStarted(queryContext) {
     this._rows.textContent = "";
   }
 
+  onQueryCancelled(queryContext) {
+    // Nothing.
+  }
+
+  onQueryFinished(queryContext) {
+    // Nothing.
+  }
+
   onQueryResults(queryContext) {
     // XXX For now, clear the results for each set received. We should really
     // be updating the existing list.
     this._rows.textContent = "";
     this._queryContext = queryContext;
     for (let resultIndex in queryContext.results) {
       this._addRow(resultIndex);
     }
--- a/browser/components/urlbar/tests/browser/browser_UrlbarController_resultOpening.js
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarController_resultOpening.js
@@ -52,17 +52,19 @@ add_task(function test_handleEnteredText
 
 add_task(function test_resultSelected_switchtab() {
   sandbox.stub(window, "switchToTabHavingURI").returns(true);
   sandbox.stub(window.gBrowser.selectedTab, "isEmpty").returns(false);
   sandbox.stub(window.gBrowser, "removeTab");
 
   const event = new MouseEvent("click", {button: 0});
   const url = "https://example.com/1";
-  const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, {url});
+  const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                                 UrlbarUtils.MATCH_SOURCE.TABS,
+                                 { url });
 
   Assert.equal(gURLBar.value, "", "urlbar input is empty before selecting a result");
   if (Services.prefs.getBoolPref("browser.urlbar.quantumbar", true)) {
     gURLBar.resultSelected(event, result);
     Assert.equal(gURLBar.value, url, "urlbar value updated for selected result");
   } else {
     controller.resultSelected(event, result);
   }
--- a/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js
@@ -44,18 +44,22 @@ async function expectNoTooltip() {
   synthesizeMouseOver(element);
 
   is(element.getAttribute("title"), null, "title attribute shouldn't be set");
 
   synthesizeMouseOut(element);
 }
 
 add_task(async function() {
+  // Ensure the URL bar isn't focused.
+  gBrowser.selectedBrowser.focus();
+
   gURLBar.value = "short string";
   await expectNoTooltip();
 
   let longURL = "http://longurl.com/" + "foobar/".repeat(30);
   let overflowPromise = BrowserTestUtils.waitForEvent(gURLBar.inputField, "overflow");
+  await window.promiseDocumentFlushed(() => {});
   gURLBar.value = longURL;
   await overflowPromise;
   is(gURLBar.inputField.value, longURL.replace(/^http:\/\//, ""), "Urlbar value has http:// stripped");
   await expectTooltip(longURL);
 });
--- a/browser/components/urlbar/tests/unit/head.js
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -49,37 +49,41 @@ function createContext(searchString = "f
   });
 }
 
 /**
  * Waits for the given notification from the supplied controller.
  *
  * @param {UrlbarController} controller The controller to wait for a response from.
  * @param {string} notification The name of the notification to wait for.
+ * @param {boolean} expected Wether the notification is expected.
  * @returns {Promise} A promise that is resolved with the arguments supplied to
  *   the notification.
  */
-function promiseControllerNotification(controller, notification) {
-  return new Promise(resolve => {
+function promiseControllerNotification(controller, notification, expected = true) {
+  return new Promise((resolve, reject) => {
     let proxifiedObserver = new Proxy({}, {
       get: (target, name) => {
         if (name == notification) {
           return (...args) => {
             controller.removeQueryListener(proxifiedObserver);
-            resolve(args);
+            if (expected) {
+              resolve(args);
+            } else {
+              reject();
+            }
           };
         }
         return () => false;
       },
     });
     controller.addQueryListener(proxifiedObserver);
   });
 }
 
-
 /**
  * Helper function to clear the existing providers and register a basic provider
  * that returns only the results given.
  *
  * @param {array} results The results for the provider to return.
  * @param {function} [cancelCallback] Optional, called when the query provider
  *                                    receives a cancel instruction.
  */
@@ -96,16 +100,19 @@ function registerBasicTestProvider(resul
   }
   UrlbarProvidersManager.registerProvider({
     get name() {
       return "TestProvider";
     },
     get type() {
       return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     },
+    get sources() {
+      return results.map(r => r.source);
+    },
     async startQuery(context, add) {
       Assert.ok(context, "context is passed-in");
       Assert.equal(typeof add, "function", "add is a callback");
       this._context = context;
       for (const result of results) {
         add(this, result);
       }
     },
--- a/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
@@ -5,17 +5,19 @@
  * These tests test the UrlbarController in association with the model.
  */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 
 const TEST_URL = "http://example.com";
-const match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, { url: TEST_URL });
+const match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                              UrlbarUtils.MATCH_SOURCE.TABS,
+                              { url: TEST_URL });
 let controller;
 
 /**
  * Asserts that the query context has the expected values.
  *
  * @param {QueryContext} context
  * @param {object} expectedValues The expected values for the QueryContext.
  */
--- a/browser/components/urlbar/tests/unit/test_providersManager.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -1,15 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 add_task(async function test_providers() {
-  let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, { url: "http://mozilla.org/foo/" });
+  let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                              UrlbarUtils.MATCH_SOURCE.TABS,
+                              { url: "http://mozilla.org/foo/" });
   registerBasicTestProvider([match]);
 
   let context = createContext();
   let controller = new UrlbarController({
     browserWindow: {
       location: {
         href: AppConstants.BROWSER_CHROME_URL,
       },
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_providers() {
+  let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                              UrlbarUtils.MATCH_SOURCE.TABS,
+                              { url: "http://mozilla.org/foo/" });
+  registerBasicTestProvider([match]);
+
+  let context = createContext();
+  let controller = new UrlbarController({
+    browserWindow: {
+      location: {
+        href: AppConstants.BROWSER_CHROME_URL,
+      },
+    },
+  });
+
+  info("Disable the only available source, should get no matches");
+  Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+  let promise = Promise.race([
+    promiseControllerNotification(controller, "onQueryResults", false),
+    promiseControllerNotification(controller, "onQueryFinished"),
+  ]);
+  await controller.startQuery(context);
+  await promise;
+  Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+
+  let matches = [
+    match,
+    new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                    UrlbarUtils.MATCH_SOURCE.HISTORY,
+                    { url: "http://mozilla.org/foo/" }),
+  ];
+  registerBasicTestProvider(matches);
+
+  info("Disable one of the sources, should get a single match");
+  Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+  promise = Promise.all([
+    promiseControllerNotification(controller, "onQueryResults"),
+    promiseControllerNotification(controller, "onQueryFinished"),
+  ]);
+  await controller.startQuery(context, controller);
+  await promise;
+  Assert.deepEqual(context.results, [match]);
+  Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+
+  info("Use a restriction character, should get a single match");
+  context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`);
+  promise = Promise.all([
+    promiseControllerNotification(controller, "onQueryResults"),
+    promiseControllerNotification(controller, "onQueryFinished"),
+  ]);
+  await controller.startQuery(context, controller);
+  await promise;
+  Assert.deepEqual(context.results, [match]);
+});
--- a/browser/components/urlbar/tests/unit/xpcshell.ini
+++ b/browser/components/urlbar/tests/unit/xpcshell.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
 head = head.js
 firefox-appdir = browser
 
 [test_providerOpenTabs.js]
 [test_providersManager.js]
+[test_providersManager_filtering.js]
 [test_QueryContext.js]
 [test_tokenizer.js]
 [test_UrlbarController_unit.js]
 [test_UrlbarController_integration.js]
 [test_UrlbarUtils_addToUrlbarHistory.js]
 [test_UrlbarUtils_getShortcutOrURIAndPostData.js]
--- a/browser/docs/AddressBar.rst
+++ b/browser/docs/AddressBar.rst
@@ -63,16 +63,17 @@ It is augmented as it progresses through
                 // may actually return more results than expected, so that the
                 // View and the Controller can do additional filtering.
     isPrivate; // {boolean} Whether the search started in a private context.
     userContextId; // {integer} The user context ID (containers feature).
 
     // Properties added by the Model.
     tokens; // {array} tokens extracted from the searchString, each token is an
             // object in the form {type, value}.
+    results; // {array} list of UrlbarMatch objects.
   }
 
 
 The Model
 =========
 
 The *Model* is the component responsible for retrieving search results based on
 the user's input, and sorting them accordingly to their importance.
@@ -138,16 +139,18 @@ implementation details may vary deeply a
   *PlacesUtils.promiseLargeCacheDBConnection* utility.
 
 .. highlight:: JavaScript
 .. code::
 
   UrlbarProvider {
     name; // {string} A simple name to track the provider.
     type; // {integer} One of UrlbarUtils.PROVIDER_TYPE.
+    sources; // {array} List of UrlbarUtils.MATCH_SOURCE, representing the
+             // data sources used by this provider.
     // The returned promise should be resolved when the provider is done
     // searching AND returning matches.
     // Each new UrlbarMatch should be passed to the AddCallback function.
     async startQuery(QueryContext, AddCallback);
     // Any cleaning/resetting task should happen here.
     cancelQuery(QueryContext);
   }
 
@@ -267,16 +270,20 @@ Represents the base *View* implementatio
   UrlbarView {
     // Manage View visibility.
     open();
     close();
     // Invoked when the query starts.
     onQueryStarted(queryContext);
     // Invoked when new matches are available.
     onQueryResults(queryContext);
+    // Invoked when the query has been canceled.
+    onQueryCancelled(queryContext);
+    // Invoked when the query is done.
+    onQueryFinished(queryContext);
   }
 
 UrlbarMatch
 ===========
 
 An `UrlbarMatch <https://dxr.mozilla.org/mozilla-central/source/browser/components/urlbar/UrlbarMatch.jsm>`_
 instance represents a single match (search result) with a match type, that
 identifies specific kind of results.
@@ -289,16 +296,18 @@ properties, supported by all of the matc
 
 .. highlight:: JavaScript
 .. code::
 
   UrlbarMatch {
     constructor(matchType, payload);
 
     // Common properties:
+    type: {integer} One of UrlbarUtils.MATCH_TYPE.
+    source: {integer} One of UrlbarUtils.MATCH_SOURCE.
     url: {string} The url pointed by this match.
     title: {string} A title that may be used as a label for this match.
   }
 
 
 Shared Modules
 ==============
 
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -170,18 +170,18 @@ tabs-group-header = Tabs
 ctrl-tab-recently-used-order =
     .label = Ctrl+Tab cycles through tabs in recently used order
     .accesskey = T
 
 open-new-link-as-tabs =
     .label = Open links in tabs instead of new windows
     .accesskey = w
 
-warn-on-close-multiple-tabs =
-    .label = Warn you when closing multiple tabs
+warn-on-quit-close-multiple-tabs =
+    .label = Warn you when quitting and closing multiple tabs
     .accesskey = m
 
 warn-on-open-many-tabs =
     .label = Warn you when opening multiple tabs might slow down { -brand-short-name }
     .accesskey = d
 
 switch-links-to-new-tabs =
     .label = When you open a link in a new tab, switch to it immediately
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -33,17 +33,16 @@ const COLOR_TAKING_FUNCTIONS = ["linear-
                                 "-moz-repeating-radial-gradient", "drop-shadow"];
 // Functions that accept a shape argument.
 const BASIC_SHAPE_FUNCTIONS = ["polygon", "circle", "ellipse", "inset"];
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const FLEXBOX_HIGHLIGHTER_ENABLED_PREF = "devtools.inspector.flexboxHighlighter.enabled";
 const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
-const CSS_SHAPE_OUTSIDE_ENABLED_PREF = "layout.css.shape-outside.enabled";
 
 /**
  * This module is used to process text for output by developer tools. This means
  * linking JS files with the debugger, CSS files with the style editor, JS
  * functions with the debugger, placing color swatches next to colors and
  * adding doorhanger previews where possible (images, angles, lengths,
  * border radius, cubic-bezier etc.).
  *
@@ -93,19 +92,17 @@ OutputParser.prototype = {
    *         A document fragment containing color swatches etc.
    */
   parseCssProperty: function(name, value, options = {}) {
     options = this._mergeOptions(options);
 
     options.expectCubicBezier = this.supportsType(name, CSS_TYPES.TIMING_FUNCTION);
     options.expectDisplay = name === "display";
     options.expectFilter = name === "filter";
-    options.expectShape = name === "clip-path" ||
-                          (name === "shape-outside"
-                           && Services.prefs.getBoolPref(CSS_SHAPE_OUTSIDE_ENABLED_PREF));
+    options.expectShape = name === "clip-path" || name === "shape-outside";
     options.expectFont = name === "font-family";
     options.supportsColor = this.supportsType(name, CSS_TYPES.COLOR) ||
                             this.supportsType(name, CSS_TYPES.GRADIENT);
 
     // The filter property is special in that we want to show the
     // swatch even if the value is invalid, because this way the user
     // can easily use the editor to fix it.
     if (options.expectFilter || this._cssPropertySupportsValue(name, value)) {
--- a/devtools/shared/css/generated/properties-db.js
+++ b/devtools/shared/css/generated/properties-db.js
@@ -9358,20 +9358,16 @@ exports.PREFERENCES = [
     "scroll-snap-type-x",
     "layout.css.scroll-snap.enabled"
   ],
   [
     "scroll-snap-type-y",
     "layout.css.scroll-snap.enabled"
   ],
   [
-    "shape-image-threshold",
-    "layout.css.shape-outside.enabled"
-  ],
-  [
     "background-blend-mode",
     "layout.css.background-blend-mode.enabled"
   ],
   [
     "font-variation-settings",
     "layout.css.font-variations.enabled"
   ],
   [
@@ -9390,40 +9386,32 @@ exports.PREFERENCES = [
     "scroll-snap-coordinate",
     "layout.css.scroll-snap.enabled"
   ],
   [
     "scrollbar-color",
     "layout.css.scrollbar-color.enabled"
   ],
   [
-    "shape-outside",
-    "layout.css.shape-outside.enabled"
-  ],
-  [
     "translate",
     "layout.css.individual-transform.enabled"
   ],
   [
     "scroll-snap-points-x",
     "layout.css.scroll-snap.enabled"
   ],
   [
     "scroll-snap-points-y",
     "layout.css.scroll-snap.enabled"
   ],
   [
     "scroll-snap-destination",
     "layout.css.scroll-snap.enabled"
   ],
   [
-    "shape-margin",
-    "layout.css.shape-outside.enabled"
-  ],
-  [
     "-webkit-text-stroke-width",
     "layout.css.prefixes.webkit"
   ],
   [
     "-webkit-text-fill-color",
     "layout.css.prefixes.webkit"
   ],
   [
--- a/dom/base/nsJSUtils.cpp
+++ b/dom/base/nsJSUtils.cpp
@@ -120,17 +120,17 @@ EvaluationExceptionToNSResult(JSContext*
   return NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW_UNCATCHABLE;
 }
 
 nsJSUtils::ExecutionContext::ExecutionContext(JSContext* aCx,
                                               JS::Handle<JSObject*> aGlobal)
   :
 #ifdef MOZ_GECKO_PROFILER
     mAutoProfilerLabel("nsJSUtils::ExecutionContext", /* dynamicStr */ nullptr,
-                       __LINE__, js::ProfilingStackFrame::Category::JS),
+                       js::ProfilingStackFrame::Category::JS),
 #endif
     mCx(aCx)
   , mRealm(aCx, aGlobal)
   , mRetValue(aCx)
   , mScopeChain(aCx)
   , mRv(NS_OK)
   , mSkip(false)
   , mCoerceToString(false)
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -1581,22 +1581,16 @@ class CGAbstractMethod(CGThing):
         elif self.inline:
             decorators.append('inline')
         if self.static:
             decorators.append('static')
         decorators.append(self.returnType)
         maybeNewline = " " if self.inline else "\n"
         return ' '.join(decorators) + maybeNewline
 
-    def _auto_profiler_label(self):
-        profiler_label_and_jscontext = self.profiler_label_and_jscontext()
-        if profiler_label_and_jscontext:
-            return 'AUTO_PROFILER_LABEL_FAST("%s", DOM, %s);' % profiler_label_and_jscontext
-        return None
-
     def declare(self):
         if self.inline:
             return self._define(True)
         return "%s%s%s(%s);\n" % (self._template(), self._decorators(), self.name, self._argstring(True))
 
     def indent_body(self, body):
         """
         Indent the code returned by self.definition_body(). Most classes
@@ -1611,33 +1605,33 @@ class CGAbstractMethod(CGThing):
                 self.definition_epilogue())
 
     def define(self):
         return "" if self.inline else self._define()
 
     def definition_prologue(self, fromDeclare):
         prologue = "%s%s%s(%s)\n{\n" % (self._template(), self._decorators(),
                                         self.name, self._argstring(fromDeclare))
-        profiler_label = self._auto_profiler_label()
+        profiler_label = self.auto_profiler_label()
         if profiler_label:
-            prologue += "  %s\n\n" % profiler_label
+            prologue += indent(profiler_label) + "\n"
 
         return prologue
 
     def definition_epilogue(self):
         return "}\n"
 
     def definition_body(self):
         assert False  # Override me!
 
     """
     Override this method to return a pair of (descriptive string, name of a
     JSContext* variable) in order to generate a profiler label for this method.
     """
-    def profiler_label_and_jscontext(self):
+    def auto_profiler_label(self):
         return None # Override me!
 
 class CGAbstractStaticMethod(CGAbstractMethod):
     """
     Abstract base class for codegen of implementation-only (no
     declaration) static methods.
     """
     def __init__(self, descriptor, name, returnType, args, canRunScript=False):
@@ -1870,23 +1864,27 @@ class CGClassConstructor(CGAbstractStati
 
         name = self._ctor.identifier.name
         nativeName = MakeNativeName(self.descriptor.binaryNameFor(name))
         callGenerator = CGMethodCall(nativeName, True, self.descriptor,
                                      self._ctor, isConstructor=True,
                                      constructorName=ctorName)
         return preamble + "\n" + callGenerator.define()
 
-    def profiler_label_and_jscontext(self):
+    def auto_profiler_label(self):
         name = self._ctor.identifier.name
         if name != "constructor":
             ctorName = name
         else:
             ctorName = self.descriptor.interface.identifier.name
-        return ("%s constructor" % ctorName, "cx")
+        return fill(
+            """
+            AUTO_PROFILER_LABEL_DYNAMIC_FAST("${ctorName}", "constructor", DOM, cx, 0);
+            """,
+            ctorName=ctorName)
 
 # Encapsulate the constructor in a helper method to share genConstructorBody with CGJSImplMethod.
 class CGConstructNavigatorObject(CGAbstractMethod):
     """
     Construct a new JS-implemented WebIDL DOM object, for use on navigator.
     """
     def __init__(self, descriptor):
         args = [Argument('JSContext*', 'cx'),
@@ -8666,27 +8664,19 @@ class CGAbstractStaticBindingMethod(CGAb
         # later use it to wrap return values.
         unwrap = dedent("""
             JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
             JS::Rooted<JSObject*> obj(cx, &args.callee());
 
             """)
         return unwrap + self.generate_code().define()
 
-    def profiler_label_and_jscontext(self):
-        # Our args are JSNativeArguments() which contain a "JSContext* cx"
-        # argument. We let our subclasses choose the label.
-        return (self.profiler_label(), "cx")
-
     def generate_code(self):
         assert False  # Override me
 
-    def profiler_label(self):
-        assert False # Override me
-
 
 def MakeNativeName(name):
     return name[0].upper() + IDLToCIdentifier(name[1:])
 
 
 class CGSpecializedMethod(CGAbstractStaticMethod):
     """
     A class for generating the C++ code for a specialized method that the JIT
@@ -8703,20 +8693,27 @@ class CGSpecializedMethod(CGAbstractStat
                                         canRunScript=True)
 
     def definition_body(self):
         nativeName = CGSpecializedMethod.makeNativeName(self.descriptor,
                                                         self.method)
         return CGMethodCall(nativeName, self.method.isStatic(), self.descriptor,
                             self.method).define()
 
-    def profiler_label_and_jscontext(self):
+    def auto_profiler_label(self):
         interface_name = self.descriptor.interface.identifier.name
         method_name = self.method.identifier.name
-        return ("%s.%s" % (interface_name, method_name), "cx")
+        return fill(
+            """
+            AUTO_PROFILER_LABEL_DYNAMIC_FAST(
+              "${interface_name}", "${method_name}", DOM, cx,
+              uint32_t(js::ProfilingStackFrame::Flags::STRING_TEMPLATE_METHOD));
+            """,
+            interface_name=interface_name,
+            method_name=method_name)
 
     @staticmethod
     def makeNativeName(descriptor, method):
         name = method.identifier.name
         return MakeNativeName(descriptor.binaryNameFor(name))
 
 
 class CGMethodPromiseWrapper(CGAbstractStaticMethod):
@@ -8963,20 +8960,27 @@ class CGStaticMethod(CGAbstractStaticBin
         name = CppKeywords.checkMethodName(IDLToCIdentifier(method.identifier.name))
         CGAbstractStaticBindingMethod.__init__(self, descriptor, name)
 
     def generate_code(self):
         nativeName = CGSpecializedMethod.makeNativeName(self.descriptor,
                                                         self.method)
         return CGMethodCall(nativeName, True, self.descriptor, self.method)
 
-    def profiler_label(self):
+    def auto_profiler_label(self):
         interface_name = self.descriptor.interface.identifier.name
         method_name = self.method.identifier.name
-        return "%s.%s" % (interface_name, method_name)
+        return fill(
+            """
+            AUTO_PROFILER_LABEL_DYNAMIC_FAST(
+              "${interface_name}", "${method_name}", DOM, cx,
+              uint32_t(js::ProfilingStackFrame::Flags::STRING_TEMPLATE_METHOD));
+            """,
+            interface_name=interface_name,
+            method_name=method_name)
 
 
 class CGSpecializedGetter(CGAbstractStaticMethod):
     """
     A class for generating the code for a specialized attribute getter
     that the JIT can call with lower overhead.
     """
     def __init__(self, descriptor, attr):
@@ -9070,20 +9074,27 @@ class CGSpecializedGetter(CGAbstractStat
         if self.attr.navigatorObjectGetter:
             cgGetterCall = CGNavigatorGetterCall
         else:
             cgGetterCall = CGGetterCall
         return (prefix +
                 cgGetterCall(self.attr.type, nativeName,
                              self.descriptor, self.attr).define())
 
-    def profiler_label_and_jscontext(self):
+    def auto_profiler_label(self):
         interface_name = self.descriptor.interface.identifier.name
         attr_name = self.attr.identifier.name
-        return ("get %s.%s" % (interface_name, attr_name), "cx")
+        return fill(
+            """
+            AUTO_PROFILER_LABEL_DYNAMIC_FAST(
+              "${interface_name}", "${attr_name}", DOM, cx,
+              uint32_t(js::ProfilingStackFrame::Flags::STRING_TEMPLATE_GETTER));
+            """,
+            interface_name=interface_name,
+            attr_name=attr_name)
 
     @staticmethod
     def makeNativeName(descriptor, attr):
         name = attr.identifier.name
         nativeName = MakeNativeName(descriptor.binaryNameFor(name))
         _, resultOutParam, _, _, _ = getRetvalDeclarationForType(attr.type,
                                                                  descriptor)
         extendedAttrs = descriptor.getExtendedAttributes(attr, getter=True)
@@ -9133,20 +9144,27 @@ class CGStaticGetter(CGAbstractStaticBin
         CGAbstractStaticBindingMethod.__init__(self, descriptor, name)
 
     def generate_code(self):
         nativeName = CGSpecializedGetter.makeNativeName(self.descriptor,
                                                         self.attr)
         return CGGetterCall(self.attr.type, nativeName, self.descriptor,
                             self.attr)
 
-    def profiler_label(self):
+    def auto_profiler_label(self):
         interface_name = self.descriptor.interface.identifier.name
         attr_name = self.attr.identifier.name
-        return "get %s.%s" % (interface_name, attr_name)
+        return fill(
+            """
+            AUTO_PROFILER_LABEL_DYNAMIC_FAST(
+              "${interface_name}", "${attr_name}", DOM, cx,
+              uint32_t(js::ProfilingStackFrame::Flags::STRING_TEMPLATE_GETTER));
+            """,
+            interface_name=interface_name,
+            attr_name=attr_name)
 
 
 class CGSpecializedSetter(CGAbstractStaticMethod):
     """
     A class for generating the code for a specialized attribute setter
     that the JIT can call with lower overhead.
     """
     def __init__(self, descriptor, attr):
@@ -9160,20 +9178,27 @@ class CGSpecializedSetter(CGAbstractStat
                                         canRunScript=True)
 
     def definition_body(self):
         nativeName = CGSpecializedSetter.makeNativeName(self.descriptor,
                                                         self.attr)
         return CGSetterCall(self.attr.type, nativeName, self.descriptor,
                             self.attr).define()
 
-    def profiler_label_and_jscontext(self):
+    def auto_profiler_label(self):
         interface_name = self.descriptor.interface.identifier.name
         attr_name = self.attr.identifier.name
-        return ("set %s.%s" % (interface_name, attr_name), "cx")
+        return fill(
+            """
+            AUTO_PROFILER_LABEL_DYNAMIC_FAST(
+              "${interface_name}", "${attr_name}", DOM, cx,
+              uint32_t(js::ProfilingStackFrame::Flags::STRING_TEMPLATE_SETTER));
+            """,
+            interface_name=interface_name,
+            attr_name=attr_name)
 
     @staticmethod
     def makeNativeName(descriptor, attr):
         name = attr.identifier.name
         return "Set" + MakeNativeName(descriptor.binaryNameFor(name))
 
 
 class CGStaticSetter(CGAbstractStaticBindingMethod):
@@ -9194,20 +9219,27 @@ class CGStaticSetter(CGAbstractStaticBin
               return ThrowErrorMessage(cx, MSG_MISSING_ARGUMENTS, "${name} setter");
             }
             """,
             name=self.attr.identifier.name))
         call = CGSetterCall(self.attr.type, nativeName, self.descriptor,
                             self.attr)
         return CGList([checkForArg, call])
 
-    def profiler_label(self):
+    def auto_profiler_label(self):
         interface_name = self.descriptor.interface.identifier.name
         attr_name = self.attr.identifier.name
-        return "set %s.%s" % (interface_name, attr_name)
+        return fill(
+            """
+            AUTO_PROFILER_LABEL_DYNAMIC_FAST(
+              "${interface_name}", "${attr_name}", DOM, cx,
+              uint32_t(js::ProfilingStackFrame::Flags::STRING_TEMPLATE_SETTER));
+            """,
+            interface_name=interface_name,
+            attr_name=attr_name)
 
 
 class CGSpecializedForwardingSetter(CGSpecializedSetter):
     """
     A class for generating the code for a specialized attribute setter with
     PutForwards that the JIT can call with lower overhead.
     """
     def __init__(self, descriptor, attr):
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -775,16 +775,25 @@ NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Ca
 
 class CanvasShutdownObserver final : public nsIObserver
 {
 public:
   explicit CanvasShutdownObserver(CanvasRenderingContext2D* aCanvas)
   : mCanvas(aCanvas)
   {}
 
+  void OnShutdown() {
+    if (!mCanvas) {
+      return;
+    }
+
+    mCanvas = nullptr;
+    nsContentUtils::UnregisterShutdownObserver(this);
+  }
+
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
 private:
   ~CanvasShutdownObserver() {}
 
   CanvasRenderingContext2D* mCanvas;
 };
 
@@ -792,17 +801,17 @@ NS_IMPL_ISUPPORTS(CanvasShutdownObserver
 
 NS_IMETHODIMP
 CanvasShutdownObserver::Observe(nsISupports* aSubject,
                                 const char* aTopic,
                                 const char16_t* aData)
 {
   if (mCanvas && strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
     mCanvas->OnShutdown();
-    nsContentUtils::UnregisterShutdownObserver(this);
+    OnShutdown();
   }
 
   return NS_OK;
 }
 
 class CanvasDrawObserver
 {
 public:
@@ -1185,17 +1194,17 @@ CanvasRenderingContext2D::OnShutdown()
     provider->OnShutdown();
   }
 }
 
 void
 CanvasRenderingContext2D::RemoveShutdownObserver()
 {
   if (mShutdownObserver) {
-    nsContentUtils::UnregisterShutdownObserver(mShutdownObserver);
+    mShutdownObserver->OnShutdown();
     mShutdownObserver = nullptr;
   }
 }
 
 void
 CanvasRenderingContext2D::SetStyleFromString(const nsAString& aStr,
                                              Style aWhichStyle)
 {
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -1476,17 +1476,17 @@ FetchDriver::SetRequestHeaders(nsIHttpCh
                                  false /* merge */);
     MOZ_ASSERT(NS_SUCCEEDED(rv));
   }
 
   if (mRequest->ForceOriginHeader()) {
     nsAutoString origin;
     if (NS_SUCCEEDED(nsContentUtils::GetUTFOrigin(mPrincipal, origin))) {
       DebugOnly<nsresult> rv =
-        aChannel->SetRequestHeader(NS_LITERAL_CSTRING("origin"),
+        aChannel->SetRequestHeader(nsDependentCString(net::nsHttp::Origin),
                                    NS_ConvertUTF16toUTF8(origin),
                                    false /* merge */);
       MOZ_ASSERT(NS_SUCCEEDED(rv));
     }
   }
 }
 
 void
--- a/dom/interfaces/base/moz.build
+++ b/dom/interfaces/base/moz.build
@@ -9,17 +9,16 @@ with Files("**"):
 
 XPIDL_SOURCES += [
     'domstubs.idl',
     'nsIBrowser.idl',
     'nsIBrowserDOMWindow.idl',
     'nsIContentPermissionPrompt.idl',
     'nsIContentPrefService2.idl',
     'nsIContentProcess.idl',
-    'nsIContentURIGrouper.idl',
     'nsIDOMChromeWindow.idl',
     'nsIDOMGlobalPropertyInitializer.idl',
     'nsIDOMWindow.idl',
     'nsIDOMWindowUtils.idl',
     'nsIFocusManager.idl',
     'nsIQueryContentEventResult.idl',
     'nsIServiceWorkerManager.idl',
     'nsIStructuredCloneContainer.idl',
deleted file mode 100644
--- a/dom/interfaces/base/nsIContentURIGrouper.idl
+++ /dev/null
@@ -1,23 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#include "nsIURI.idl"
-
-[scriptable, uuid(4bb38cb4-c3cb-4d17-9799-1b3132b39723)]
-interface nsIContentURIGrouper : nsISupports
-{
-    /**
-     * Determine the group to which the URI belongs.
-     *
-     * @param    aURI       the URI to group
-     *
-     * @returns  the group to which the URI belongs
-     */
-    AString group(in nsIURI aURI);
-};
-
-%{C++
-// The contractID for the generic implementation built in to xpcom.
-#define NS_HOSTNAME_GROUPER_SERVICE_CONTRACTID "@mozilla.org/content-pref/hostname-grouper;1"
-%}
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -148,16 +148,29 @@ static void InitVideoQueuePrefs()
       "media.video-queue.default-size", MAX_VIDEO_QUEUE_SIZE);
     sVideoQueueHWAccelSize = Preferences::GetUint(
       "media.video-queue.hw-accel-size", HW_VIDEO_QUEUE_SIZE);
     sVideoQueueSendToCompositorSize = Preferences::GetUint(
       "media.video-queue.send-to-compositor-size", VIDEO_QUEUE_SEND_TO_COMPOSITOR_SIZE);
   }
 }
 
+template <typename Type, typename Function>
+static void
+DiscardFramesFromTail(MediaQueue<Type>& aQueue, const Function&& aTest)
+{
+  while(aQueue.GetSize()) {
+    if (aTest(aQueue.PeekBack()->mTime.ToMicroseconds())) {
+      RefPtr<Type> releaseMe = aQueue.PopBack();
+      continue;
+    }
+    break;
+  }
+}
+
 // Delay, in milliseconds, that tabs needs to be in background before video
 // decoding is suspended.
 static TimeDuration
 SuspendBackgroundVideoDelay()
 {
   return TimeDuration::FromMilliseconds(
     StaticPrefs::MediaSuspendBkgndVideoDelayMs());
 }
@@ -220,16 +233,18 @@ public:
   virtual void HandleVideoSuspendTimeout() = 0;
 
   virtual void HandleResumeVideoDecoding(const TimeUnit& aTarget);
 
   virtual void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) { }
 
   virtual nsCString GetDebugInfo() { return nsCString(); }
 
+  virtual void HandleLoopingChanged() {}
+
 private:
   template <class S, typename R, typename... As>
   auto ReturnTypeHelper(R(S::*)(As...)) -> R;
 
   void Crash(const char* aReason, const char* aSite)
   {
     char buf[1024];
     SprintfLiteral(buf, "%s state=%s callsite=%s", aReason,
@@ -300,16 +315,18 @@ protected:
     master->mStateObj.reset(s);
     return CallEnterMemberFunction(s, copiedArgs,
                                    std::index_sequence_for<Ts...>{});
   }
 
   RefPtr<MediaDecoder::SeekPromise>
   SetSeekingState(SeekJob&& aSeekJob, EventVisibility aVisibility);
 
+  void SetDecodingState();
+
   // Take a raw pointer in order not to change the life cycle of MDSM.
   // It is guaranteed to be valid by MDSM.
   Master* mMaster;
 };
 
 /**
  * Purpose: decode metadata like duration and dimensions of the media resource.
  *
@@ -392,17 +409,17 @@ public:
   {
     if (mMaster->IsPlaying()) {
       mMaster->StopPlayback();
     }
 
     // Calculate the position to seek to when exiting dormant.
     auto t = mMaster->mMediaSink->IsStarted()
       ? mMaster->GetClock() : mMaster->GetMediaTime();
-    Reader()->AdjustByLooping(t);
+    mMaster->AdjustByLooping(t);
     mPendingSeek.mTarget.emplace(t, SeekTarget::Accurate);
     // SeekJob asserts |mTarget.IsValid() == !mPromise.IsEmpty()| so we
     // need to create the promise even it is not used at all.
     // The promise may be used when coming out of DormantState into
     // SeekingState.
     RefPtr<MediaDecoder::SeekPromise> x =
       mPendingSeek.mPromise.Ensure(__func__);
 
@@ -466,17 +483,17 @@ private:
 };
 
 /**
  * Purpose: decode the 1st audio and video frames to fire the 'loadeddata' event.
  *
  * Transition to:
  *   SHUTDOWN if any decode error.
  *   SEEKING if any seek request.
- *   DECODING when the 'loadeddata' event is fired.
+ *   DECODING/LOOPING_DECODING when the 'loadeddata' event is fired.
  */
 class MediaDecoderStateMachine::DecodingFirstFrameState
   : public MediaDecoderStateMachine::StateObject
 {
 public:
   explicit DecodingFirstFrameState(Master* aPtr) : StateObject(aPtr) { }
 
   void Enter();
@@ -580,16 +597,17 @@ private:
  * Purpose: decode audio/video data for playback.
  *
  * Transition to:
  *   DORMANT if playback is paused for a while.
  *   SEEKING if any seek request.
  *   SHUTDOWN if any decode error.
  *   BUFFERING if playback can't continue due to lack of decoded data.
  *   COMPLETED when having decoded all audio/video data.
+ *   LOOPING_DECODING when media start seamless looping
  */
 class MediaDecoderStateMachine::DecodingState
   : public MediaDecoderStateMachine::StateObject
 {
 public:
   explicit DecodingState(Master* aPtr)
     : StateObject(aPtr)
     , mDormantTimer(OwnerThread())
@@ -700,19 +718,26 @@ public:
     }
   }
 
   nsCString GetDebugInfo() override
   {
     return nsPrintfCString("mIsPrerolling=%d", mIsPrerolling);
   }
 
+  void HandleLoopingChanged() override
+  {
+    SetDecodingState();
+  }
+
+protected:
+  virtual void EnsureAudioDecodeTaskQueued();
+
 private:
   void DispatchDecodeTasksIfNeeded();
-  void EnsureAudioDecodeTaskQueued();
   void EnsureVideoDecodeTaskQueued();
   void MaybeStartBuffering();
 
   // At the start of decoding we want to "preroll" the decode until we've
   // got a few frames decoded before we consider whether decode is falling
   // behind. Otherwise our "we're falling behind" logic will trigger
   // unnecessarily if we start playing as soon as the first sample is
   // decoded. These two fields store how many video frames and audio
@@ -802,24 +827,182 @@ private:
   // Fired when playback is paused for a while to enter dormant.
   DelayedScheduler mDormantTimer;
 
   MediaEventListener mOnAudioPopped;
   MediaEventListener mOnVideoPopped;
 };
 
 /**
+ * Purpose: decode audio/video data for playback when media is in seamless
+ * looping, we will adjust media time to make samples time monotonically
+ * increasing.
+ *
+ * Transition to:
+ *   DORMANT if playback is paused for a while.
+ *   SEEKING if any seek request.
+ *   SHUTDOWN if any decode error.
+ *   BUFFERING if playback can't continue due to lack of decoded data.
+ *   COMPLETED when having decoded all audio/video data.
+ *   DECODING when media stop seamless looping
+ */
+class MediaDecoderStateMachine::LoopingDecodingState
+  : public MediaDecoderStateMachine::DecodingState
+{
+public:
+  explicit LoopingDecodingState(Master* aPtr)
+    : DecodingState(aPtr)
+  {
+    MOZ_ASSERT(mMaster->mLooping);
+  }
+
+  void Exit() override
+  {
+    mAudioDataRequest.DisconnectIfExists();
+    mAudioSeekRequest.DisconnectIfExists();
+    if (ShouldDiscardLoopedAudioData()) {
+      DiscardLoopedAudioData();
+    }
+    DecodingState::Exit();
+  }
+
+  State GetState() const override
+  {
+    return DECODER_STATE_LOOPING_DECODING;
+  }
+
+  void HandleAudioDecoded(AudioData* aAudio) override
+  {
+    MediaResult rv = LoopingAudioTimeAdjustment(aAudio);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      mMaster->DecodeError(rv);
+      return;
+    }
+    mMaster->mDecodedAudioEndTime = std::max(
+      aAudio->GetEndTime(), mMaster->mDecodedAudioEndTime);
+    SLOG("sample after time-adjustment [%" PRId64 ",%" PRId64 "]",
+         aAudio->mTime.ToMicroseconds(),
+         aAudio->GetEndTime().ToMicroseconds());
+    DecodingState::HandleAudioDecoded(aAudio);
+  }
+
+  void HandleEndOfAudio() override
+  {
+    // The data time in the audio queue is assumed to be increased linearly,
+    // so we need to add the last ending time as the offset to correct the
+    // audio data time in the next round when seamless looping is enabled.
+    mAudioLoopingOffset = mMaster->mDecodedAudioEndTime;
+
+    if (mMaster->mAudioDecodedDuration.isNothing()) {
+      mMaster->mAudioDecodedDuration.emplace(mMaster->mDecodedAudioEndTime);
+    }
+
+    SLOG("received EOS when seamless looping, starts seeking");
+    Reader()->ResetDecode(TrackInfo::kAudioTrack);
+    Reader()->Seek(SeekTarget(media::TimeUnit::Zero(), SeekTarget::Accurate))
+      ->Then(OwnerThread(), __func__,
+              [this] () -> void {
+                mAudioSeekRequest.Complete();
+                SLOG("seeking completed, start to request first sample, "
+                     "queueing audio task - queued=%zu, decoder-queued=%zu",
+                     AudioQueue().GetSize(), Reader()->SizeOfAudioQueueInFrames());
+
+                Reader()->RequestAudioData()
+                  ->Then(OwnerThread(), __func__, [this] (RefPtr<AudioData> aAudio) {
+                          mAudioDataRequest.Complete();
+                          SLOG("got audio decoded sample [%" PRId64 ",%" PRId64 "]",
+                               aAudio->mTime.ToMicroseconds(),
+                               aAudio->GetEndTime().ToMicroseconds());
+                          HandleAudioDecoded(aAudio);
+                        }, [this] (const MediaResult& aError) {
+                          mAudioDataRequest.Complete();
+                          HandleError(aError);
+                        })
+                  ->Track(mAudioDataRequest);
+              },
+              [this] (const SeekRejectValue& aReject) -> void {
+                mAudioSeekRequest.Complete();
+                HandleError(aReject.mError);
+              })
+      ->Track(mAudioSeekRequest);
+  }
+
+private:
+  void HandleError(const MediaResult& aError)
+  {
+    SLOG("audio looping failed, aError=%s", aError.ErrorName().get());
+    MOZ_ASSERT(aError != NS_ERROR_DOM_MEDIA_END_OF_STREAM);
+    mMaster->DecodeError(aError);
+  }
+
+  void EnsureAudioDecodeTaskQueued() override
+  {
+    if (mAudioSeekRequest.Exists() ||
+        mAudioDataRequest.Exists()) {
+      return;
+    }
+    DecodingState::EnsureAudioDecodeTaskQueued();
+  }
+
+  MediaResult LoopingAudioTimeAdjustment(AudioData* aAudio)
+  {
+    if (mAudioLoopingOffset != media::TimeUnit::Zero()) {
+      aAudio->mTime += mAudioLoopingOffset;
+    }
+    return aAudio->mTime.IsValid() ?
+              MediaResult(NS_OK) :
+              MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+                          "Audio sample overflow during looping time adjustment");
+  }
+
+  bool ShouldDiscardLoopedAudioData() const
+  {
+    /**
+     * If media cancels looping, we should check whether there are audio data
+     * whose time is later than EOS. If so, we should discard them because we
+     * won't have a chance to play them.
+     *
+     *    playback                     last decoded
+     *    position          EOS        data time
+     *   ----|---------------|------------|---------> (Increasing timeline)
+     *    mCurrent        mLooping      mMaster's
+     * PlaybackPosition    Offset      mDecodedAudioEndTime
+     *
+     */
+    return (mAudioLoopingOffset != media::TimeUnit::Zero() &&
+            mMaster->mCurrentPosition.Ref() < mAudioLoopingOffset &&
+            mAudioLoopingOffset < mMaster->mDecodedAudioEndTime);
+  }
+
+  void DiscardLoopedAudioData()
+  {
+    if (mAudioLoopingOffset == media::TimeUnit::Zero()) {
+        return;
+    }
+
+    SLOG("Discard frames after the time=%" PRId64, mAudioLoopingOffset.ToMicroseconds());
+    DiscardFramesFromTail(AudioQueue(), [&] (int64_t aSampleTime) {
+        return aSampleTime > mAudioLoopingOffset.ToMicroseconds();
+    });
+  }
+
+  media::TimeUnit mAudioLoopingOffset = media::TimeUnit::Zero();
+  MozPromiseRequestHolder<MediaFormatReader::SeekPromise> mAudioSeekRequest;
+  MozPromiseRequestHolder<AudioDataPromise> mAudioDataRequest;
+};
+
+/**
  * Purpose: seek to a particular new playback position.
  *
  * Transition to:
  *   SEEKING if any new seek request.
  *   SHUTDOWN if seek failed.
  *   COMPLETED if the new playback position is the end of the media resource.
  *   NextFrameSeekingState if completing a NextFrameSeekingFromDormantState.
- *   DECODING otherwise.
+ *   DECODING/LOOPING_DECODING otherwise.
  */
 class MediaDecoderStateMachine::SeekingState
   : public MediaDecoderStateMachine::StateObject
 {
 public:
   explicit SeekingState(Master* aPtr)
     : StateObject(aPtr)
     , mVisibility(static_cast<EventVisibility>(0))
@@ -892,17 +1075,17 @@ public:
   }
 
 protected:
   SeekJob mSeekJob;
   EventVisibility mVisibility;
 
   virtual void DoSeek() = 0;
   // Transition to the next state (defined by the subclass) when seek is completed.
-  virtual void GoToNextState() { SetState<DecodingState>(); }
+  virtual void GoToNextState() { SetDecodingState(); }
   void SeekCompleted();
   virtual TimeUnit CalculateNewCurrentTime() const = 0;
 };
 
 class MediaDecoderStateMachine::AccurateSeekingState
   : public MediaDecoderStateMachine::SeekingState
 {
 public:
@@ -1780,17 +1963,17 @@ MediaDecoderStateMachine::DormantState::
 
 /**
  * Purpose: stop playback until enough data is decoded to continue playback.
  *
  * Transition to:
  *   SEEKING if any seek request.
  *   SHUTDOWN if any decode error.
  *   COMPLETED when having decoded all audio/video data.
- *   DECODING when having decoded enough data to continue playback.
+ *   DECODING/LOOPING_DECODING when having decoded enough data to continue playback.
  */
 class MediaDecoderStateMachine::BufferingState
   : public MediaDecoderStateMachine::StateObject
 {
 public:
   explicit BufferingState(Master* aPtr) : StateObject(aPtr) { }
 
   void Enter()
@@ -1942,17 +2125,17 @@ public:
     // StopPlayback in order to reset the IsPlaying() state so audio
     // is restarted correctly.
     mMaster->StopPlayback();
 
     if (!mSentPlaybackEndedEvent) {
       auto clockTime =
         std::max(mMaster->AudioEndTime(), mMaster->VideoEndTime());
       // Correct the time over the end once looping was turned on.
-      Reader()->AdjustByLooping(clockTime);
+      mMaster->AdjustByLooping(clockTime);
       if (mMaster->mDuration.Ref()->IsInfinite()) {
         // We have a finite duration when playback reaches the end.
         mMaster->mDuration = Some(clockTime);
         DDLOGEX(mMaster,
                 DDLogCategory::Property,
                 "duration_us",
                 mMaster->mDuration.Ref()->ToMicroseconds());
       }
@@ -2172,16 +2355,26 @@ StateObject::SetSeekingState(SeekJob&& a
     return SetState<NextFrameSeekingState>(std::move(aSeekJob), aVisibility);
   }
 
   MOZ_ASSERT_UNREACHABLE("Unknown SeekTarget::Type.");
   return nullptr;
 }
 
 void
+MediaDecoderStateMachine::StateObject::SetDecodingState()
+{
+  if (mMaster->mLooping && mMaster->mSeamlessLoopingAllowed) {
+    SetState<LoopingDecodingState>();
+    return;
+  }
+  SetState<DecodingState>();
+}
+
+void
 MediaDecoderStateMachine::
 DecodeMetadataState::OnMetadataRead(MetadataHolder&& aMetadata)
 {
   mMetadataRequest.Complete();
 
   mMaster->mInfo.emplace(*aMetadata.mInfo);
   mMaster->mMediaSeekable = Info().mMediaSeekable;
   mMaster->mMediaSeekableOnlyInBufferedRanges =
@@ -2241,17 +2434,17 @@ DormantState::HandlePlayStateChanged(Med
 }
 
 void
 MediaDecoderStateMachine::
 DecodingFirstFrameState::Enter()
 {
   // Transition to DECODING if we've decoded first frames.
   if (mMaster->mSentFirstFrameLoadedEvent) {
-    SetState<DecodingState>();
+    SetDecodingState();
     return;
   }
 
   MOZ_ASSERT(!mMaster->mVideoDecodeSuspended);
 
   // Dispatch tasks to decode first frames.
   if (mMaster->HasAudio()) {
     mMaster->RequestAudioData();
@@ -2271,17 +2464,17 @@ DecodingFirstFrameState::MaybeFinishDeco
       (mMaster->IsVideoDecoding() && VideoQueue().GetSize() == 0)) {
     return;
   }
 
   mMaster->FinishDecodeFirstFrame();
   if (mPendingSeek.Exists()) {
     SetSeekingState(std::move(mPendingSeek), EventVisibility::Observable);
   } else {
-    SetState<DecodingState>();
+    SetDecodingState();
   }
 }
 
 void
 MediaDecoderStateMachine::
 DecodingState::Enter()
 {
   MOZ_ASSERT(mMaster->mSentFirstFrameLoadedEvent);
@@ -2301,23 +2494,23 @@ DecodingState::Enter()
     HandleVideoSuspendTimeout();
   }
 
   if (!mMaster->IsVideoDecoding() && !mMaster->IsAudioDecoding()) {
     SetState<CompletedState>();
     return;
   }
 
-  mOnAudioPopped = AudioQueue().PopEvent().Connect(
+  mOnAudioPopped = AudioQueue().PopFrontEvent().Connect(
     OwnerThread(), [this] () {
     if (mMaster->IsAudioDecoding() && !mMaster->HaveEnoughDecodedAudio()) {
       EnsureAudioDecodeTaskQueued();
     }
   });
-  mOnVideoPopped = VideoQueue().PopEvent().Connect(
+  mOnVideoPopped = VideoQueue().PopFrontEvent().Connect(
     OwnerThread(), [this] () {
     if (mMaster->IsVideoDecoding() && !mMaster->HaveEnoughDecodedVideo()) {
       EnsureVideoDecodeTaskQueued();
     }
   });
 
   mMaster->mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
 
@@ -2355,35 +2548,16 @@ DecodingState::Step()
   TimeUnit before = mMaster->GetMediaTime();
   mMaster->UpdatePlaybackPositionPeriodically();
 
   // Fire the `seeking` and `seeked` events to meet the HTML spec
   // when the media is looped back from the end to the beginning.
   if (before > mMaster->GetMediaTime()) {
     MOZ_ASSERT(mMaster->mLooping);
     mMaster->mOnPlaybackEvent.Notify(MediaPlaybackEvent::Loop);
-  // After looping is cancelled, the time won't be corrected, and therefore we
-  // can check it to see if the end of the media track is reached. Make sure
-  // the media is started before comparing the time, or it's meaningless.
-  // Without checking IsStarted(), the media will be terminated immediately
-  // after seeking forward. When the state is just transited from seeking state,
-  // GetClock() is smaller than GetMediaTime(), since GetMediaTime() is updated
-  // upon seek is completed while GetClock() will be updated after the media is
-  // started again.
-  } else if (mMaster->mMediaSink->IsStarted() && !mMaster->mLooping) {
-    TimeUnit adjusted = mMaster->GetClock();
-    Reader()->AdjustByLooping(adjusted);
-    if (adjusted < before) {
-      mMaster->StopPlayback();
-      mMaster->mAudioDataRequest.DisconnectIfExists();
-      AudioQueue().Finish();
-      mMaster->mAudioCompleted = true;
-      SetState<CompletedState>();
-      return;
-    }
   }
 
   MOZ_ASSERT(!mMaster->IsPlaying() || mMaster->IsStateMachineScheduled(),
              "Must have timer scheduled");
 
   MaybeStartBuffering();
 }
 
@@ -2587,17 +2761,17 @@ BufferingState::Step()
          mMaster->OutOfDecodedAudio(),
          mMaster->AudioRequestStatus(),
          mMaster->OutOfDecodedVideo(),
          mMaster->VideoRequestStatus());
     return;
   }
 
   SLOG("Buffered for %.3lfs", (now - mBufferingStart).ToSeconds());
-  SetState<DecodingState>();
+  SetDecodingState();
 }
 
 void
 MediaDecoderStateMachine::
 BufferingState::HandleEndOfAudio()
 {
   AudioQueue().Finish();
   if (!mMaster->IsVideoDecoding()) {
@@ -2898,19 +3072,19 @@ nsresult MediaDecoderStateMachine::Init(
   // Dispatch initialization that needs to happen on that task queue.
   nsCOMPtr<nsIRunnable> r = NewRunnableMethod<RefPtr<MediaDecoder>>(
     "MediaDecoderStateMachine::InitializationTask",
     this,
     &MediaDecoderStateMachine::InitializationTask,
     aDecoder);
   mTaskQueue->DispatchStateChange(r.forget());
 
-  mAudioQueueListener = AudioQueue().PopEvent().Connect(
+  mAudioQueueListener = AudioQueue().PopFrontEvent().Connect(
     mTaskQueue, this, &MediaDecoderStateMachine::OnAudioPopped);
-  mVideoQueueListener = VideoQueue().PopEvent().Connect(
+  mVideoQueueListener = VideoQueue().PopFrontEvent().Connect(
     mTaskQueue, this, &MediaDecoderStateMachine::OnVideoPopped);
 
   mMetadataManager.Connect(mReader->TimedMetadataEvent(), OwnerThread());
 
   mOnMediaNotSeekable = mReader->OnMediaNotSeekable().Connect(
     OwnerThread(), this, &MediaDecoderStateMachine::SetMediaNotSeekable);
 
   mMediaSink = CreateMediaSink(mAudioCaptured);
@@ -3004,16 +3178,17 @@ MediaDecoderStateMachine::ToStateStr(Sta
     case DECODER_STATE_DECODING_METADATA:   return "DECODING_METADATA";
     case DECODER_STATE_DORMANT:             return "DORMANT";
     case DECODER_STATE_DECODING_FIRSTFRAME: return "DECODING_FIRSTFRAME";
     case DECODER_STATE_DECODING:            return "DECODING";
     case DECODER_STATE_SEEKING:             return "SEEKING";
     case DECODER_STATE_BUFFERING:           return "BUFFERING";
     case DECODER_STATE_COMPLETED:           return "COMPLETED";
     case DECODER_STATE_SHUTDOWN:            return "SHUTDOWN";
+    case DECODER_STATE_LOOPING_DECODING:    return "LOOPING_DECODING";
     default: MOZ_ASSERT_UNREACHABLE("Invalid state.");
   }
   return "UNKNOWN";
 }
 
 const char*
 MediaDecoderStateMachine::ToStateStr()
 {
@@ -3548,17 +3723,17 @@ MediaDecoderStateMachine::UpdatePlayback
   // This ensures that if we're running off the system clock, we don't
   // advance the clock to after the media end time.
   if (VideoEndTime() > TimeUnit::Zero() || AudioEndTime() > TimeUnit::Zero()) {
 
     auto clockTime = GetClock();
 
     // Once looping was turned on, the time is probably larger than the duration
     // of the media track, so the time over the end should be corrected.
-    mReader->AdjustByLooping(clockTime);
+    AdjustByLooping(clockTime);
     bool loopback = clockTime < GetMediaTime() && mLooping;
 
     // Skip frames up to the frame at the playback position, and figure out
     // the time remaining until it's time to display the next frame and drop
     // the current frame.
     NS_ASSERTION(clockTime >= TimeUnit::Zero(), "Should have positive clock time.");
 
     // These will be non -1 if we've displayed a video frame, or played an audio
@@ -3660,17 +3835,17 @@ void MediaDecoderStateMachine::Preserves
   mMediaSink->SetPreservesPitch(mPreservesPitch);
 }
 
 void
 MediaDecoderStateMachine::LoopingChanged()
 {
   MOZ_ASSERT(OnTaskQueue());
   if (mSeamlessLoopingAllowed) {
-    mReader->SetSeamlessLoopingEnabled(mLooping);
+    mStateObj->HandleLoopingChanged();
   }
 }
 
 RefPtr<GenericPromise>
 MediaDecoderStateMachine::InvokeSetSink(RefPtr<AudioDeviceInfo> aSink)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aSink);
@@ -4055,16 +4230,25 @@ MediaDecoderStateMachine::CancelSuspendT
       mVideoDecodeSuspendTimer.IsScheduled() ? 'T' : 'F');
   MOZ_ASSERT(OnTaskQueue());
   if (mVideoDecodeSuspendTimer.IsScheduled()) {
     mOnPlaybackEvent.Notify(MediaPlaybackEvent::CancelVideoSuspendTimer);
   }
   mVideoDecodeSuspendTimer.Reset();
 }
 
+void
+MediaDecoderStateMachine::AdjustByLooping(media::TimeUnit& aTime) const
+{
+  MOZ_ASSERT(OnTaskQueue());
+  if (mAudioDecodedDuration.isSome() && mAudioDecodedDuration.ref().IsPositive()) {
+    aTime = aTime % mAudioDecodedDuration.ref().ToMicroseconds();
+  }
+}
+
 } // namespace mozilla
 
 // avoid redefined macro in unified build
 #undef LOG
 #undef LOGV
 #undef LOGW
 #undef LOGE
 #undef SLOGW
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -185,16 +185,17 @@ public:
 
   // Enumeration for the valid decoding states
   enum State
   {
     DECODER_STATE_DECODING_METADATA,
     DECODER_STATE_DORMANT,
     DECODER_STATE_DECODING_FIRSTFRAME,
     DECODER_STATE_DECODING,
+    DECODER_STATE_LOOPING_DECODING,
     DECODER_STATE_SEEKING,
     DECODER_STATE_BUFFERING,
     DECODER_STATE_COMPLETED,
     DECODER_STATE_SHUTDOWN
   };
 
   // Returns the state machine task queue.
   TaskQueue* OwnerThread() const { return mTaskQueue; }
@@ -299,16 +300,17 @@ public:
   RefPtr<GenericPromise> InvokeSetSink(RefPtr<AudioDeviceInfo> aSink);
 
 private:
   class StateObject;
   class DecodeMetadataState;
   class DormantState;
   class DecodingFirstFrameState;
   class DecodingState;
+  class LoopingDecodingState;
   class SeekingState;
   class AccurateSeekingState;
   class NextFrameSeekingState;
   class NextFrameSeekingFromDormantState;
   class VideoOnlySeekingState;
   class BufferingState;
   class CompletedState;
   class ShutdownState;
@@ -707,16 +709,21 @@ private:
   MediaEventProducer<DecoderDoctorEvent> mOnDecoderDoctorEvent;
 
   MediaEventProducer<NextFrameStatus> mOnNextFrameStatus;
 
   const bool mIsMSE;
 
   bool mSeamlessLoopingAllowed;
 
+  // If media was in looping and had reached to the end before, then we need
+  // to adjust sample time from clock time to media time.
+  void AdjustByLooping(media::TimeUnit& aTime) const;
+  Maybe<media::TimeUnit> mAudioDecodedDuration;
+
   // Current playback position in the stream in bytes.
   int64_t mPlaybackOffset = 0;
 
 private:
   // The buffered range. Mirrored from the decoder thread.
   Mirror<media::TimeIntervals> mBuffered;
 
   // The current play state, mirrored from the main thread.
--- a/dom/media/MediaQueue.h
+++ b/dom/media/MediaQueue.h
@@ -51,26 +51,37 @@ public:
     nsDeque::Push(aItem);
     mPushEvent.Notify(RefPtr<T>(aItem));
   }
 
   inline already_AddRefed<T> PopFront() {
     RecursiveMutexAutoLock lock(mRecursiveMutex);
     RefPtr<T> rv = dont_AddRef(static_cast<T*>(nsDeque::PopFront()));
     if (rv) {
-      mPopEvent.Notify(rv);
+      mPopFrontEvent.Notify(rv);
     }
     return rv.forget();
   }
 
+  inline already_AddRefed<T> PopBack() {
+    RecursiveMutexAutoLock lock(mRecursiveMutex);
+    RefPtr<T> rv = dont_AddRef(static_cast<T*>(nsDeque::Pop()));
+    return rv.forget();
+  }
+
   inline RefPtr<T> PeekFront() const {
     RecursiveMutexAutoLock lock(mRecursiveMutex);
     return static_cast<T*>(nsDeque::PeekFront());
   }
 
+  inline RefPtr<T> PeekBack() const {
+    RecursiveMutexAutoLock lock(mRecursiveMutex);
+    return static_cast<T*>(nsDeque::Peek());
+  }
+
   void Reset() {
     RecursiveMutexAutoLock lock(mRecursiveMutex);
     while (GetSize() > 0) {
       RefPtr<T> x = dont_AddRef(static_cast<T*>(nsDeque::PopFront()));
     }
     mEndOfStream = false;
   }
 
@@ -149,31 +160,31 @@ public:
     uint32_t frames = 0;
     for (size_t i = 0; i < GetSize(); ++i) {
       T* v = static_cast<T*>(ObjectAt(i));
       frames += v->mFrames;
     }
     return frames;
   }
 
-  MediaEventSource<RefPtr<T>>& PopEvent() {
-    return mPopEvent;
+  MediaEventSource<RefPtr<T>>& PopFrontEvent() {
+    return mPopFrontEvent;
   }
 
   MediaEventSource<RefPtr<T>>& PushEvent() {
     return mPushEvent;
   }
 
   MediaEventSource<void>& FinishEvent() {
     return mFinishEvent;
   }
 
 private:
   mutable RecursiveMutex mRecursiveMutex;
-  MediaEventProducer<RefPtr<T>> mPopEvent;
+  MediaEventProducer<RefPtr<T>> mPopFrontEvent;
   MediaEventProducer<RefPtr<T>> mPushEvent;
   MediaEventProducer<void> mFinishEvent;
   // True when we've decoded the last frame of data in the
   // bitstream for which we're queueing frame data.
   bool mEndOfStream;
 };
 
 } // namespace mozilla
--- a/dom/media/ReaderProxy.cpp
+++ b/dom/media/ReaderProxy.cpp
@@ -14,18 +14,16 @@ namespace mozilla {
 ReaderProxy::ReaderProxy(AbstractThread* aOwnerThread,
                          MediaFormatReader* aReader)
   : mOwnerThread(aOwnerThread)
   , mReader(aReader)
   , mWatchManager(this, aReader->OwnerThread())
   , mDuration(aReader->OwnerThread(),
               media::NullableTimeUnit(),
               "ReaderProxy::mDuration (Mirror)")
-  , mSeamlessLoopingBlocked(false)
-  , mSeamlessLoopingEnabled(false)
 {
   // Must support either heuristic buffering or WaitForData().
   MOZ_ASSERT(mReader->UseBufferingHeuristics() ||
              mReader->IsWaitForDataSupported());
 }
 
 ReaderProxy::~ReaderProxy()
 {}
@@ -53,79 +51,37 @@ ReaderProxy::ReadMetadata()
            &ReaderProxy::OnMetadataNotRead);
 }
 
 RefPtr<ReaderProxy::AudioDataPromise>
 ReaderProxy::OnAudioDataRequestCompleted(RefPtr<AudioData> aAudio)
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
 
-  // Subtract the start time and add the looping-offset time.
-  int64_t offset =
-    StartTime().ToMicroseconds() - mLoopingOffset.ToMicroseconds();
-  aAudio->AdjustForStartTime(offset);
+  aAudio->AdjustForStartTime(StartTime().ToMicroseconds());
   if (aAudio->mTime.IsValid()) {
-    mLastAudioEndTime = aAudio->mTime;
     return AudioDataPromise::CreateAndResolve(aAudio.forget(), __func__);
   }
   return AudioDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
                                            __func__);
 }
 
 RefPtr<ReaderProxy::AudioDataPromise>
 ReaderProxy::OnAudioDataRequestFailed(const MediaResult& aError)
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
-
-  if (mSeamlessLoopingBlocked || !mSeamlessLoopingEnabled ||
-      aError.Code() != NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
-    return AudioDataPromise::CreateAndReject(aError, __func__);
-  }
-
-  // The data time in the audio queue is assumed to be increased linearly,
-  // so we need to add the last ending time as the offset to correct the
-  // audio data time in the next round when seamless looping is enabled.
-  mLoopingOffset = mLastAudioEndTime;
-
-  // Save the duration of the audio track if it hasn't been set.
-  if (!mAudioDuration.IsValid()) {
-    mAudioDuration = mLastAudioEndTime;
-  }
-
-  // For seamless looping, the demuxer is sought to the beginning and then
-  // keep requesting decoded data in advance, upon receiving EOS.
-  // The MDSM will not be aware of the EOS and keep receiving decoded data
-  // as usual while looping is on.
-  RefPtr<ReaderProxy> self = this;
-  RefPtr<MediaFormatReader> reader = mReader;
-  ResetDecode(TrackInfo::kAudioTrack);
-  return SeekInternal(SeekTarget(media::TimeUnit::Zero(), SeekTarget::Accurate))
-    ->Then(mReader->OwnerThread(),
-           __func__,
-           [reader]() { return reader->RequestAudioData(); },
-           [](const SeekRejectValue& aReject) {
-             return AudioDataPromise::CreateAndReject(aReject.mError, __func__);
-           })
-    ->Then(mOwnerThread,
-           __func__,
-           [self](RefPtr<AudioData> aAudio) {
-             return self->OnAudioDataRequestCompleted(aAudio.forget());
-           },
-           [](const MediaResult& aError) {
-             return AudioDataPromise::CreateAndReject(aError, __func__);
-           });
+  return AudioDataPromise::CreateAndReject(aError, __func__);
 }
 
 RefPtr<ReaderProxy::AudioDataPromise>
 ReaderProxy::RequestAudioData()
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
   MOZ_ASSERT(!mShutdown);
 
-  mSeamlessLoopingBlocked = false;
   return InvokeAsync(mReader->OwnerThread(),
                      mReader.get(),
                      __func__,
                      &MediaFormatReader::RequestAudioData)
     ->Then(mOwnerThread,
            __func__,
            this,
            &ReaderProxy::OnAudioDataRequestCompleted,
@@ -133,17 +89,16 @@ ReaderProxy::RequestAudioData()
 }
 
 RefPtr<ReaderProxy::VideoDataPromise>
 ReaderProxy::RequestVideoData(const media::TimeUnit& aTimeThreshold)
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
   MOZ_ASSERT(!mShutdown);
 
-  mSeamlessLoopingBlocked = false;
   const auto threshold = aTimeThreshold > media::TimeUnit::Zero()
                          ? aTimeThreshold + StartTime()
                          : aTimeThreshold;
 
   int64_t startTime = StartTime().ToMicroseconds();
   return InvokeAsync(mReader->OwnerThread(),
                      mReader.get(),
                      __func__,
@@ -163,21 +118,16 @@ ReaderProxy::RequestVideoData(const medi
              return VideoDataPromise::CreateAndReject(aError, __func__);
            });
 }
 
 RefPtr<ReaderProxy::SeekPromise>
 ReaderProxy::Seek(const SeekTarget& aTarget)
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
-  mSeamlessLoopingBlocked = true;
-  // Reset the members for seamless looping if the seek is triggered outside.
-  mLoopingOffset = media::TimeUnit::Zero();
-  mLastAudioEndTime = media::TimeUnit::Zero();
-  mAudioDuration = media::TimeUnit::Invalid();
   return SeekInternal(aTarget);
 }
 
 RefPtr<ReaderProxy::SeekPromise>
 ReaderProxy::SeekInternal(const SeekTarget& aTarget)
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
   SeekTarget adjustedTarget = aTarget;
@@ -295,27 +245,9 @@ ReaderProxy::SetCanonicalDuration(
       mDuration.Connect(canonical);
       mWatchManager.Watch(mDuration, &ReaderProxy::UpdateDuration);
     });
   nsresult rv = mReader->OwnerThread()->Dispatch(r.forget());
   MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
   Unused << rv;
 }
 
-void
-ReaderProxy::SetSeamlessLoopingEnabled(bool aEnabled)
-{
-  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
-  mSeamlessLoopingEnabled = aEnabled;
-}
-
-void
-ReaderProxy::AdjustByLooping(media::TimeUnit& aTime)
-{
-  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
-  MOZ_ASSERT(!mShutdown);
-  MOZ_ASSERT(!mSeamlessLoopingEnabled || !mSeamlessLoopingBlocked);
-  if (mAudioDuration.IsValid() && mAudioDuration.IsPositive()) {
-    aTime = aTime % mAudioDuration.ToMicroseconds();
-  }
-}
-
 } // namespace mozilla
--- a/dom/media/ReaderProxy.h
+++ b/dom/media/ReaderProxy.h
@@ -79,20 +79,16 @@ public:
 
   void SetCDMProxy(CDMProxy* aProxy) { mReader->SetCDMProxy(aProxy); }
 
   void SetVideoBlankDecode(bool aIsBlankDecode);
 
   void SetCanonicalDuration(
     AbstractCanonical<media::NullableTimeUnit>* aCanonical);
 
-  void SetSeamlessLoopingEnabled(bool aEnabled);
-
-  void AdjustByLooping(media::TimeUnit& aTime);
-
 private:
   ~ReaderProxy();
   RefPtr<MetadataPromise> OnMetadataRead(MetadataHolder&& aMetadata);
   RefPtr<MetadataPromise> OnMetadataNotRead(const MediaResult& aError);
   void UpdateDuration();
   RefPtr<SeekPromise> SeekInternal(const SeekTarget& aTarget);
 
   RefPtr<ReaderProxy::AudioDataPromise> OnAudioDataRequestCompleted(
@@ -106,25 +102,13 @@ private:
   bool mShutdown = false;
   Maybe<media::TimeUnit> mStartTime;
 
   // State-watching manager.
   WatchManager<ReaderProxy> mWatchManager;
 
   // Duration, mirrored from the state machine task queue.
   Mirror<media::NullableTimeUnit> mDuration;
-
-  // The total duration of audio looping in previous rounds.
-  media::TimeUnit mLoopingOffset = media::TimeUnit::Zero();
-  // To keep tracking the latest time of decoded audio data.
-  media::TimeUnit mLastAudioEndTime = media::TimeUnit::Zero();
-  // The duration of the audio track.
-  media::TimeUnit mAudioDuration = media::TimeUnit::Invalid();
-
-  // To prevent seamless looping while seeking.
-  bool mSeamlessLoopingBlocked;
-  // Indicates whether we should loop the media.
-  bool mSeamlessLoopingEnabled;
 };
 
 } // namespace mozilla
 
 #endif // ReaderProxy_h_
--- a/dom/media/mediasink/AudioSink.cpp
+++ b/dom/media/mediasink/AudioSink.cpp
@@ -75,17 +75,17 @@ nsresult
 AudioSink::Init(const PlaybackParams& aParams, RefPtr<GenericPromise>& aEndPromise)
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
 
   mAudioQueueListener = mAudioQueue.PushEvent().Connect(
     mOwnerThread, this, &AudioSink::OnAudioPushed);
   mAudioQueueFinishListener = mAudioQueue.FinishEvent().Connect(
     mOwnerThread, this, &AudioSink::NotifyAudioNeeded);
-  mProcessedQueueListener = mProcessedQueue.PopEvent().Connect(
+  mProcessedQueueListener = mProcessedQueue.PopFrontEvent().Connect(
     mOwnerThread, this, &AudioSink::OnAudioPopped);
 
   // To ensure at least one audio packet will be popped from AudioQueue and
   // ready to be played.
   NotifyAudioNeeded();
   aEndPromise = mEndPromise.Ensure(__func__);
   nsresult rv = InitializeAudioStream(aParams);
   if (NS_FAILED(rv)) {
--- a/dom/media/platforms/omx/PureOmxPlatformLayer.cpp
+++ b/dom/media/platforms/omx/PureOmxPlatformLayer.cpp
@@ -111,19 +111,16 @@ PureOmxPlatformLayer::PureOmxPlatformLay
   , mImageContainer(aImageContainer)
 {
   LOG("");
 }
 
 PureOmxPlatformLayer::~PureOmxPlatformLayer()
 {
   LOG("");
-  if (mComponent) {
-    OMX_FreeHandle(mComponent);
-  }
 }
 
 OMX_ERRORTYPE
 PureOmxPlatformLayer::InitOmxToStateLoaded(const TrackInfo* aInfo)
 {
   LOG("");
 
   if (!aInfo) {
@@ -272,16 +269,22 @@ PureOmxPlatformLayer::SetParameter(OMX_I
                           aParamIndex,
                           aComponentParameterStructure);
 }
 
 nsresult
 PureOmxPlatformLayer::Shutdown()
 {
   LOG("");
+  if (mComponent) {
+    OMX_FreeHandle(mComponent);
+    mComponent = nullptr;
+  }
+  mPromiseLayer = nullptr;
+  mDataDecoder = nullptr;
   return NS_OK;
 }
 
 /* static */ OMX_ERRORTYPE
 PureOmxPlatformLayer::EventHandler(OMX_HANDLETYPE hComponent,
                                    OMX_PTR pAppData,
                                    OMX_EVENTTYPE eEventType,
                                    OMX_U32 nData1,
--- a/dom/media/webaudio/AudioEventTimeline.h
+++ b/dom/media/webaudio/AudioEventTimeline.h
@@ -209,17 +209,17 @@ public:
   void InsertEvent(const AudioTimelineEvent& aEvent)
   {
     for (unsigned i = 0; i < mEvents.Length(); ++i) {
       if (aEvent.Time<TimeType>() == mEvents[i].Time<TimeType>()) {
         // If two events happen at the same time, have them in chronological
         // order of insertion.
         do {
           ++i;
-        } while (i < mEvents.Length() && aEvent.mType != mEvents[i].mType &&
+        } while (i < mEvents.Length() &&
                  aEvent.Time<TimeType>() == mEvents[i].Time<TimeType>());
         mEvents.InsertElementAt(i, aEvent);
         return;
       }
       // Otherwise, place the event right after the latest existing event
       if (aEvent.Time<TimeType>() < mEvents[i].Time<TimeType>()) {
         mEvents.InsertElementAt(i, aEvent);
         return;
--- a/dom/script/ScriptSettings.cpp
+++ b/dom/script/ScriptSettings.cpp
@@ -640,17 +640,17 @@ AutoEntryScript::AutoEntryScript(nsIGlob
                                  const char* aReason,
                                  bool aIsMainThread)
   : AutoJSAPI(aGlobalObject, aIsMainThread, eEntryScript)
   , mWebIDLCallerPrincipal(nullptr)
   // This relies on us having a cx() because the AutoJSAPI constructor already
   // ran.
   , mCallerOverride(cx())
 #ifdef MOZ_GECKO_PROFILER
-  , mAutoProfilerLabel("AutoEntryScript", aReason, __LINE__,
+  , mAutoProfilerLabel("AutoEntryScript", aReason,
                        js::ProfilingStackFrame::Category::JS)
 #endif
 {
   MOZ_ASSERT(aGlobalObject);
 
   if (aIsMainThread) {
     if (gRunToCompletionListeners > 0) {
       mDocShellEntryMonitor.emplace(cx(), aReason);
--- a/gfx/layers/apz/src/AsyncPanZoomAnimation.h
+++ b/gfx/layers/apz/src/AsyncPanZoomAnimation.h
@@ -28,24 +28,30 @@ class AsyncPanZoomAnimation {
 
 public:
   explicit AsyncPanZoomAnimation() = default;
 
   virtual bool DoSample(FrameMetrics& aFrameMetrics,
                         const TimeDuration& aDelta) = 0;
 
   /**
-   * Attempt to apply a translation to the animation in response to content
-   * providing a relative scroll offset update.
+   * Attempt to handle a main-thread scroll offset update without cancelling
+   * the animation. This may or may not make sense depending on the type of
+   * the animation and whether the scroll update is relative or absolute.
    *
-   * @param aShiftDelta the amount to translate the animation in app units
-   * @returns Whether the animation was able to translate. If false, the
-   *    animation must be canceled.
+   * If the scroll update is relative, |aRelativeDelta| will contain the
+   * delta of the relative update. If the scroll update is absolute,
+   * |aRelativeDelta| will be Nothing() (the animation can check the APZC's
+   * FrameMetrics for the new absolute scroll offset if it wants to handle
+   * and absolute update).
+   *
+   * Returns whether the animation could handle the scroll update. If the
+   * return value is false, the animation will be cancelled.
    */
-  virtual bool ApplyContentShift(const CSSPoint& aShiftDelta)
+  virtual bool HandleScrollOffsetUpdate(const Maybe<CSSPoint>& aRelativeDelta)
   {
     return false;
   }
 
   bool Sample(FrameMetrics& aFrameMetrics,
               const TimeDuration& aDelta) {
     // In some situations, particularly when handoff is involved, it's possible
     // for |aDelta| to be negative on the first call to sample. Ignore such a
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -4404,22 +4404,21 @@ void AsyncPanZoomController::NotifyLayer
         Metrics().ApplyScrollUpdateFrom(aLayerMetrics);
       }
       Metrics().RecalculateViewportOffset();
 
       mCompositedLayoutViewport = Metrics().GetViewport();
       mCompositedScrollOffset = Metrics().GetScrollOffset();
       mExpectedGeckoMetrics = aLayerMetrics;
 
-      // If we have applied a relative scroll update and a scroll animation is
-      // happening, attempt to apply a content shift and preserve the
-      // animation.
+      // If an animation is underway, tell it about the scroll offset update.
+      // Some animations can handle some scroll offset updates and continue
+      // running. Those that can't will return false, and we cancel them.
       if (!mAnimation ||
-          relativeDelta.isNothing() ||
-          !mAnimation->ApplyContentShift(relativeDelta.value())) {
+          !mAnimation->HandleScrollOffsetUpdate(relativeDelta)) {
         // Cancel the animation (which might also trigger a repaint request)
         // after we update the scroll offset above. Otherwise we can be left
         // in a state where things are out of sync.
         CancelAnimation();
       }
 
       // Since the scroll offset has changed, we need to recompute the
       // displayport margins and send them to layout. Otherwise there might be
--- a/gfx/layers/apz/src/AutoscrollAnimation.h
+++ b/gfx/layers/apz/src/AutoscrollAnimation.h
@@ -17,20 +17,20 @@ class AsyncPanZoomController;
 class AutoscrollAnimation : public AsyncPanZoomAnimation
 {
 public:
   AutoscrollAnimation(AsyncPanZoomController& aApzc,
                       const ScreenPoint& aAnchorLocation);
 
   bool DoSample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) override;
 
-  bool ApplyContentShift(const CSSPoint& aShiftDelta) override
+  bool HandleScrollOffsetUpdate(const Maybe<CSSPoint>& aRelativeDelta) override
   {
     // Autoscroll works using screen space coordinates, so there's no work we
-    // need to do to handle a content shift
+    // need to do to handle either a relative or an absolute scroll update.
     return true;
   }
 
   void Cancel(CancelAnimationFlags aFlags) override;
 private:
   AsyncPanZoomController& mApzc;
   ScreenPoint mAnchorLocation;
 };
--- a/gfx/layers/apz/src/GenericScrollAnimation.cpp
+++ b/gfx/layers/apz/src/GenericScrollAnimation.cpp
@@ -106,16 +106,19 @@ GenericScrollAnimation::DoSample(FrameMe
     return false;
   }
 
   mApzc.ScrollBy(adjustedOffset / zoom);
   return !finished;
 }
 
 bool
-GenericScrollAnimation::ApplyContentShift(const CSSPoint& aShiftDelta)
+GenericScrollAnimation::HandleScrollOffsetUpdate(const Maybe<CSSPoint>& aRelativeDelta)
 {
-  mAnimationPhysics->ApplyContentShift(aShiftDelta);
-  return true;
+  if (aRelativeDelta) {
+    mAnimationPhysics->ApplyContentShift(*aRelativeDelta);
+    return true;
+  }
+  return false;
 }
 
 } // namespace layers
 } // namespace mozilla
--- a/gfx/layers/apz/src/GenericScrollAnimation.h
+++ b/gfx/layers/apz/src/GenericScrollAnimation.h
@@ -23,17 +23,17 @@ class GenericScrollAnimation
 {
 public:
   GenericScrollAnimation(AsyncPanZoomController& aApzc,
                          const nsPoint& aInitialPosition,
                          const ScrollAnimationBezierPhysicsSettings& aSettings);
 
   bool DoSample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) override;
 
-  bool ApplyContentShift(const CSSPoint& aShiftDelta) override;
+  bool HandleScrollOffsetUpdate(const Maybe<CSSPoint>& aRelativeDelta) override;
 
   void UpdateDelta(TimeStamp aTime,
                    const nsPoint& aDelta,
                    const nsSize& aCurrentVelocity);
   void UpdateDestination(TimeStamp aTime,
                          const nsPoint& aDestination,
                          const nsSize& aCurrentVelocity);
 
--- a/gfx/vr/ipc/VRParent.cpp
+++ b/gfx/vr/ipc/VRParent.cpp
@@ -109,16 +109,22 @@ VRParent::ActorDestroy(ActorDestroyReaso
 }
 
 bool
 VRParent::Init(base::ProcessId aParentPid,
                const char* aParentBuildID,
                MessageLoop* aIOLoop,
                IPC::Channel* aChannel)
 {
+  // Initialize the thread manager before starting IPC. Otherwise, messages
+  // may be posted to the main thread and we won't be able to process them.
+  if (NS_WARN_IF(NS_FAILED(nsThreadManager::get().Init()))) {
+    return false;
+  }
+
   // Now it's safe to start IPC.
   if (NS_WARN_IF(!Open(aChannel, aParentPid, aIOLoop))) {
     return false;
   }
 
   // This must be checked before any IPDL message, which may hit sentinel
   // errors due to parent and content processes having different
   // versions.
@@ -131,13 +137,17 @@ VRParent::Init(base::ProcessId aParentPi
 
   // Ensure gfxPrefs are initialized.
   gfxPrefs::GetSingleton();
   gfxConfig::Init();
   gfxVars::Initialize();
 #if defined(XP_WIN)
   DeviceManagerDx::Init();
 #endif
+  if (NS_FAILED(NS_InitMinimalXPCOM())) {
+    return false;
+  }
+
   return true;
 }
 
 } // namespace gfx
 } // namespace mozilla
\ No newline at end of file
--- a/gfx/vr/service/OpenVRSession.cpp
+++ b/gfx/vr/service/OpenVRSession.cpp
@@ -152,18 +152,20 @@ OpenVRSession::Initialize(mozilla::gfx::
   // Configure coordinate system
   mVRCompositor->SetTrackingSpace(::vr::TrackingUniverseSeated);
 
   if (!InitState(aSystemState)) {
     Shutdown();
     return false;
   }
 
-  StartHapticThread();
-  StartHapticTimer();
+  NS_DispatchToMainThread(NS_NewRunnableFunction(
+    "OpenVRSession::StartHapticThread", [this]() {
+      StartHapticThread();
+  }));
   
   // Succeeded
   return true;
 }
 
 #if defined(XP_WIN)
 bool
 OpenVRSession::CreateD3DObjects()
@@ -922,27 +924,33 @@ OpenVRSession::VibrateHaptic(uint32_t aC
    *         and replace the TriggerHapticPulse calls which have been
    *         deprecated.
    */
 }
 
 void
 OpenVRSession::StartHapticThread()
 {
+  MOZ_ASSERT(NS_IsMainThread());
   if (!mHapticThread) {
     mHapticThread = new VRThread(NS_LITERAL_CSTRING("VR_OpenVR_Haptics"));
   }
   mHapticThread->Start();
+  StartHapticTimer();
 }
 
 void
 OpenVRSession::StopHapticThread()
 {
   if (mHapticThread) {
-    mHapticThread->Shutdown();
+    NS_DispatchToMainThread(NS_NewRunnableFunction(
+      "mHapticThread::Shutdown",
+      [thread = mHapticThread]() {
+        thread->Shutdown();
+    }));
     mHapticThread = nullptr;
   }
 }
 
 void
 OpenVRSession::StartHapticTimer()
 {
   if (!mHapticTimer && mHapticThread) {
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -537,16 +537,64 @@ impl AlphaBatchBuilder {
                                          .expect("bug");
         let z_id = z_generator.next();
 
         let clip_task_address = prim_instance
             .clip_task_id
             .map_or(OPAQUE_TASK_ADDRESS, |id| render_tasks.get_task_address(id));
 
         match prim_instance.kind {
+            PrimitiveInstanceKind::Clear => {
+                let prim_data = &ctx
+                    .resources
+                    .prim_data_store[prim_instance.prim_data_handle];
+
+                let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+                // TODO(gw): We can abstract some of the common code below into
+                //           helper methods, as we port more primitives to make
+                //           use of interning.
+
+                let prim_header = PrimitiveHeader {
+                    local_rect: prim_data.prim_rect,
+                    local_clip_rect: prim_instance.combined_local_clip_rect,
+                    task_address,
+                    specific_prim_address: prim_cache_address,
+                    clip_task_address,
+                    transform_id,
+                };
+
+                let prim_header_index = prim_headers.push(
+                    &prim_header,
+                    z_id,
+                    [get_shader_opacity(1.0), 0, 0],
+                );
+
+                let batch_key = BatchKey {
+                    blend_mode: BlendMode::PremultipliedDestOut,
+                    kind: BatchKind::Brush(BrushBatchKind::Solid),
+                    textures: BatchTextures::no_texture(),
+                };
+
+                let instance = PrimitiveInstanceData::from(BrushInstance {
+                    segment_index: 0,
+                    edge_flags: EdgeAaSegmentMask::all(),
+                    clip_task_address,
+                    brush_flags: BrushFlags::PERSPECTIVE_INTERPOLATION,
+                    prim_header_index,
+                    user_data: 0,
+                });
+
+                self.batch_list.push_single_instance(
+                    batch_key,
+                    bounding_rect,
+                    z_id,
+                    PrimitiveInstanceData::from(instance),
+                );
+            }
             PrimitiveInstanceKind::TextRun { ref run, .. } => {
                 let subpx_dir = run.used_font.get_subpx_dir();
 
                 // The GPU cache data is stored in the template and reused across
                 // frames and display lists.
                 let prim_data = &ctx
                     .resources
                     .prim_data_store[prim_instance.prim_data_handle];
@@ -760,17 +808,18 @@ impl AlphaBatchBuilder {
                     // Convert all children of the 3D hierarchy root into batches.
                     Picture3DContext::In { root_data: Some(ref list), .. } => {
                         for child in list {
                             let prim_instance = &picture.prim_list.prim_instances[child.anchor];
                             let pic_index = match prim_instance.kind {
                                 PrimitiveInstanceKind::Picture { pic_index } => pic_index,
                                 PrimitiveInstanceKind::LineDecoration { .. } |
                                 PrimitiveInstanceKind::TextRun { .. } |
-                                PrimitiveInstanceKind::LegacyPrimitive { .. } => {
+                                PrimitiveInstanceKind::LegacyPrimitive { .. } |
+                                PrimitiveInstanceKind::Clear => {
                                     unreachable!();
                                 }
                             };
                             let pic = &ctx.prim_store.pictures[pic_index.0];
 
                             let clip_task_address = prim_instance
                                 .clip_task_id
                                 .map_or(OPAQUE_TASK_ADDRESS, |id| render_tasks.get_task_address(id));
@@ -1719,24 +1768,16 @@ impl BrushPrimitive {
             BrushKind::Solid { ref opacity_binding, .. } => {
                 Some(BrushBatchParameters::shared(
                     BrushBatchKind::Solid,
                     BatchTextures::no_texture(),
                     [get_shader_opacity(opacity_binding.current), 0, 0],
                     0,
                 ))
             }
-            BrushKind::Clear => {
-                Some(BrushBatchParameters::shared(
-                    BrushBatchKind::Solid,
-                    BatchTextures::no_texture(),
-                    [get_shader_opacity(1.0), 0, 0],
-                    0,
-                ))
-            }
             BrushKind::RadialGradient { ref stops_handle, .. } => {
                 Some(BrushBatchParameters::shared(
                     BrushBatchKind::RadialGradient,
                     BatchTextures::no_texture(),
                     [
                         stops_handle.as_int(gpu_cache),
                         0,
                         0,
@@ -1819,19 +1860,16 @@ impl BrushPrimitive {
 impl PrimitiveInstance {
     fn get_blend_mode(
         &self,
         details: &PrimitiveDetails,
     ) -> BlendMode {
         match *details {
             PrimitiveDetails::Brush(ref brush) => {
                 match brush.kind {
-                    BrushKind::Clear => {
-                        BlendMode::PremultipliedDestOut
-                    }
                     BrushKind::Image { alpha_type, .. } => {
                         match alpha_type {
                             AlphaType::PremultipliedAlpha => BlendMode::PremultipliedAlpha,
                             AlphaType::Alpha => BlendMode::Alpha,
                         }
                     }
                     BrushKind::Solid { .. } |
                     BrushKind::YuvImage { .. } |
--- a/gfx/webrender/src/box_shadow.rs
+++ b/gfx/webrender/src/box_shadow.rs
@@ -1,23 +1,29 @@
 /* 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::{BorderRadius, BoxShadowClipMode, ClipMode, ColorF, DeviceIntSize, LayoutPrimitiveInfo};
-use api::{LayoutRect, LayoutSize, LayoutVector2D, MAX_BLUR_RADIUS};
+use api::{LayoutRect, LayoutSize, LayoutToDeviceScale, LayoutVector2D, MAX_BLUR_RADIUS};
 use clip::ClipItemKey;
 use display_list_flattener::DisplayListFlattener;
 use gpu_cache::GpuCacheHandle;
 use gpu_types::BoxShadowStretchMode;
 use prim_store::{BrushKind, BrushPrimitive, PrimitiveContainer};
 use prim_store::ScrollNodeAndClipChain;
 use render_task::RenderTaskCacheEntryHandle;
 use util::RectHelpers;
 
+/// In the majority of cases box-shadow radius in device pixels should be quite
+/// small (< 256), so the choice of max is trade off between panic! in
+/// pathological case of being presented with huge radius and using too much
+/// texture memory for better rendering of such cases.
+const MAX_BOX_SHADOW_RESOLUTION: u32 = 2048;
+
 #[derive(Debug, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct BoxShadowClipSource {
     // Parameters that define the shadow and are constant.
     pub shadow_radius: BorderRadius,
     pub blur_radius: f32,
     pub clip_mode: BoxShadowClipMode,
@@ -74,71 +80,63 @@ impl<'a> DisplayListFlattener<'a> {
         clip_mode: BoxShadowClipMode,
     ) {
         if color.a == 0.0 {
             return;
         }
 
         // Inset shadows get smaller as spread radius increases.
         let (spread_amount, prim_clip_mode) = match clip_mode {
-            BoxShadowClipMode::Outset => {
-                (spread_radius, ClipMode::ClipOut)
-            }
-            BoxShadowClipMode::Inset => {
-                (-spread_radius, ClipMode::Clip)
-            }
+            BoxShadowClipMode::Outset => (spread_radius, ClipMode::ClipOut),
+            BoxShadowClipMode::Inset => (-spread_radius, ClipMode::Clip),
         };
 
         // Ensure the blur radius is somewhat sensible.
         blur_radius = f32::min(blur_radius, MAX_BLUR_RADIUS);
 
         // Adjust the border radius of the box shadow per CSS-spec.
-        let shadow_radius = adjust_border_radius_for_box_shadow(
-            border_radius,
-            spread_amount,
-        );
+        let shadow_radius = adjust_border_radius_for_box_shadow(border_radius, spread_amount);
 
         // Apply parameters that affect where the shadow rect
         // exists in the local space of the primitive.
-        let shadow_rect = prim_info.rect
+        let shadow_rect = prim_info
+            .rect
             .translate(box_offset)
             .inflate(spread_amount, spread_amount);
 
         // If blur radius is zero, we can use a fast path with
         // no blur applied.
         if blur_radius == 0.0 {
             // Trivial reject of box-shadows that are not visible.
-            if box_offset.x == 0.0 &&
-               box_offset.y == 0.0 &&
-               spread_amount == 0.0 {
+            if box_offset.x == 0.0 && box_offset.y == 0.0 && spread_amount == 0.0 {
                 return;
             }
 
             let mut clips = Vec::with_capacity(2);
             let (final_prim_rect, clip_radius) = match clip_mode {
                 BoxShadowClipMode::Outset => {
                     if !shadow_rect.is_well_formed_and_nonempty() {
                         return;
                     }
 
                     // TODO(gw): Add a fast path for ClipOut + zero border radius!
                     clips.push(ClipItemKey::rounded_rect(
                         prim_info.rect,
                         border_radius,
-                        ClipMode::ClipOut
+                        ClipMode::ClipOut,
                     ));
 
                     (shadow_rect, shadow_radius)
                 }
                 BoxShadowClipMode::Inset => {
                     if shadow_rect.is_well_formed_and_nonempty() {
                         clips.push(ClipItemKey::rounded_rect(
                             shadow_rect,
                             shadow_radius,
-                            ClipMode::ClipOut
+                            ClipMode::ClipOut,
                         ));
                     }
 
                     (prim_info.rect, border_radius)
                 }
             };
 
             clips.push(ClipItemKey::rounded_rect(
@@ -146,22 +144,17 @@ impl<'a> DisplayListFlattener<'a> {
                 clip_radius,
                 ClipMode::Clip,
             ));
 
             self.add_primitive(
                 clip_and_scroll,
                 &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
                 clips,
-                PrimitiveContainer::Brush(
-                    BrushPrimitive::new(
-                        BrushKind::new_solid(*color),
-                        None,
-                    )
-                ),
+                PrimitiveContainer::Brush(BrushPrimitive::new(BrushKind::new_solid(*color), None)),
             );
         } else {
             // Normal path for box-shadows with a valid blur radius.
             let blur_offset = BLUR_SAMPLE_SCALE * blur_radius;
             let mut extra_clips = vec![];
 
             // Add a normal clip mask to clip out the contents
             // of the surrounding primitive.
@@ -172,20 +165,17 @@ impl<'a> DisplayListFlattener<'a> {
             ));
 
             // Get the local rect of where the shadow will be drawn,
             // expanded to include room for the blurred region.
             let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);
 
             // Draw the box-shadow as a solid rect, using a box-shadow
             // clip mask item.
-            let prim = BrushPrimitive::new(
-                BrushKind::new_solid(*color),
-                None,
-            );
+            let prim = BrushPrimitive::new(BrushKind::new_solid(*color), None);
 
             // Create the box-shadow clip item.
             let shadow_clip_source = ClipItemKey::box_shadow(
                 shadow_rect,
                 shadow_radius,
                 dest_rect,
                 blur_radius,
                 clip_mode,
@@ -203,18 +193,20 @@ impl<'a> DisplayListFlattener<'a> {
 
                     // Outset shadows are expanded by the shadow
                     // region from the original primitive.
                     LayoutPrimitiveInfo::with_clip_rect(dest_rect, prim_info.clip_rect)
                 }
                 BoxShadowClipMode::Inset => {
                     // If the inner shadow rect contains the prim
                     // rect, no pixels will be shadowed.
-                    if border_radius.is_zero() &&
-                       shadow_rect.inflate(-blur_radius, -blur_radius).contains_rect(&prim_info.rect) {
+                    if border_radius.is_zero() && shadow_rect
+                        .inflate(-blur_radius, -blur_radius)
+                        .contains_rect(&prim_info.rect)
+                    {
                         return;
                     }
 
                     // Inset shadows are still visible, even if the
                     // inset shadow rect becomes invalid (they will
                     // just look like a solid rectangle).
                     if shadow_rect.is_well_formed_and_nonempty() {
                         extra_clips.push(shadow_clip_source);
@@ -230,58 +222,36 @@ impl<'a> DisplayListFlattener<'a> {
                 &prim_info,
                 extra_clips,
                 PrimitiveContainer::Brush(prim),
             );
         }
     }
 }
 
-fn adjust_border_radius_for_box_shadow(
-    radius: BorderRadius,
-    spread_amount: f32,
-) -> BorderRadius {
+fn adjust_border_radius_for_box_shadow(radius: BorderRadius, spread_amount: f32) -> BorderRadius {
     BorderRadius {
-        top_left: adjust_corner_for_box_shadow(
-            radius.top_left,
-            spread_amount,
-        ),
-        top_right: adjust_corner_for_box_shadow(
-            radius.top_right,
-            spread_amount,
-        ),
-        bottom_right: adjust_corner_for_box_shadow(
-            radius.bottom_right,
-            spread_amount,
-        ),
-        bottom_left: adjust_corner_for_box_shadow(
-            radius.bottom_left,
-            spread_amount,
-        ),
+        top_left: adjust_corner_for_box_shadow(radius.top_left, spread_amount),
+        top_right: adjust_corner_for_box_shadow(radius.top_right, spread_amount),
+        bottom_right: adjust_corner_for_box_shadow(radius.bottom_right, spread_amount),
+        bottom_left: adjust_corner_for_box_shadow(radius.bottom_left, spread_amount),
     }
 }
 
-fn adjust_corner_for_box_shadow(
-    corner: LayoutSize,
-    spread_amount: f32,
-) -> LayoutSize {
+fn adjust_corner_for_box_shadow(corner: LayoutSize, spread_amount: f32) -> LayoutSize {
     LayoutSize::new(
-        adjust_radius_for_box_shadow(
-            corner.width,
-            spread_amount
-        ),
-        adjust_radius_for_box_shadow(
-            corner.height,
-            spread_amount
-        ),
+        adjust_radius_for_box_shadow(corner.width, spread_amount),
+        adjust_radius_for_box_shadow(corner.height, spread_amount),
     )
 }
 
-fn adjust_radius_for_box_shadow(
-    border_radius: f32,
-    spread_amount: f32,
-) -> f32 {
+fn adjust_radius_for_box_shadow(border_radius: f32, spread_amount: f32) -> f32 {
     if border_radius > 0.0 {
         (border_radius + spread_amount).max(0.0)
     } else {
         0.0
     }
 }
+
+pub fn get_max_scale_for_box_shadow(rect_size: &LayoutSize) -> LayoutToDeviceScale {
+    let r = rect_size.width.max(rect_size.height);
+    LayoutToDeviceScale::new(MAX_BOX_SHADOW_RESOLUTION as f32 / r)
+}
--- a/gfx/webrender/src/clip.rs
+++ b/gfx/webrender/src/clip.rs
@@ -5,16 +5,17 @@
 use api::{BorderRadius, ClipMode, ComplexClipRegion, DeviceIntRect, DevicePixelScale, ImageMask};
 use api::{ImageRendering, LayoutRect, LayoutSize, LayoutPoint, LayoutVector2D};
 use api::{BoxShadowClipMode, LayoutToWorldScale, PicturePixel, WorldPixel};
 use api::{PictureRect, LayoutPixel, WorldPoint, WorldSize, WorldRect, LayoutToWorldTransform};
 use api::{VoidPtrToSizeFn, LayoutRectAu, ImageKey, AuHelpers};
 use app_units::Au;
 use border::{ensure_no_corner_overlap, BorderRadiusAu};
 use box_shadow::{BLUR_SAMPLE_SCALE, BoxShadowClipSource, BoxShadowCacheKey};
+use box_shadow::get_max_scale_for_box_shadow;
 use clip_scroll_tree::{ClipScrollTree, CoordinateSystemId, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use ellipse::Ellipse;
 use gpu_cache::{GpuCache, GpuCacheHandle, ToGpuBlocks};
 use gpu_types::{BoxShadowStretchMode};
 use image::{self, Repetition};
 use intern;
 use internal_types::FastHashSet;
 use prim_store::{ClipData, ImageMaskData, SpaceMapper, VisibleMaskImageTile};
@@ -350,23 +351,31 @@ impl ClipNode {
                         0.0,
                     ]);
                     request.push(info.prim_shadow_rect);
                 }
 
                 // Quote from https://drafts.csswg.org/css-backgrounds-3/#shadow-blur
                 // "the image that would be generated by applying to the shadow a
                 // Gaussian blur with a standard deviation equal to half the blur radius."
-                let blur_radius_dp = (info.blur_radius * 0.5 * device_pixel_scale.0).round();
+                let blur_radius_dp = info.blur_radius * 0.5;
+
+                // Create scaling from requested size to cache size.  If the
+                // requested size exceeds the maximum size of the cache, the
+                // box shadow will be scaled down and stretched when rendered
+                // into the document.
+                let world_scale = LayoutToWorldScale::new(1.0);
+                let mut content_scale = world_scale * device_pixel_scale;
+                let max_scale = get_max_scale_for_box_shadow(&info.shadow_rect_alloc_size);
+                content_scale.0 = content_scale.0.min(max_scale.0);
 
                 // Create the cache key for this box-shadow render task.
-                let content_scale = LayoutToWorldScale::new(1.0) * device_pixel_scale;
                 let cache_size = to_cache_size(info.shadow_rect_alloc_size * content_scale);
                 let bs_cache_key = BoxShadowCacheKey {
-                    blur_radius_dp: blur_radius_dp as i32,
+                    blur_radius_dp: (blur_radius_dp * content_scale.0).round() as i32,
                     clip_mode: info.clip_mode,
                     rect_size: (info.shadow_rect_alloc_size * content_scale).round().to_i32(),
                     br_top_left: (info.shadow_radius.top_left * content_scale).round().to_i32(),
                     br_top_right: (info.shadow_radius.top_right * content_scale).round().to_i32(),
                     br_bottom_right: (info.shadow_radius.bottom_right * content_scale).round().to_i32(),
                     br_bottom_left: (info.shadow_radius.bottom_left * content_scale).round().to_i32(),
                 };
 
--- a/gfx/webrender/src/device/gl.rs
+++ b/gfx/webrender/src/device/gl.rs
@@ -21,34 +21,60 @@ use std::io::Read;
 use std::marker::PhantomData;
 use std::mem;
 use std::ops::Add;
 use std::path::PathBuf;
 use std::ptr;
 use std::rc::Rc;
 use std::slice;
 use std::sync::Arc;
+use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
 use std::thread;
 
+/// Sequence number for frames, as tracked by the device layer.
 #[derive(Debug, Copy, Clone, PartialEq, Ord, Eq, PartialOrd)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct FrameId(usize);
+pub struct GpuFrameId(usize);
+
+/// Tracks the total number of GPU bytes allocated across all WebRender instances.
+///
+/// Assuming all WebRender instances run on the same thread, this doesn't need
+/// to be atomic per se, but we make it atomic to satisfy the thread safety
+/// invariants in the type system. We could also put the value in TLS, but that
+/// would be more expensive to access.
+static GPU_BYTES_ALLOCATED: AtomicUsize = ATOMIC_USIZE_INIT;
+
+/// Returns the number of GPU bytes currently allocated.
+pub fn total_gpu_bytes_allocated() -> usize {
+    GPU_BYTES_ALLOCATED.load(Ordering::Relaxed)
+}
 
-impl FrameId {
+/// Records an allocation in VRAM.
+fn record_gpu_alloc(num_bytes: usize) {
+    GPU_BYTES_ALLOCATED.fetch_add(num_bytes, Ordering::Relaxed);
+}
+
+/// Records an deallocation in VRAM.
+fn record_gpu_free(num_bytes: usize) {
+    let old = GPU_BYTES_ALLOCATED.fetch_sub(num_bytes, Ordering::Relaxed);
+    assert!(old >= num_bytes, "Freeing {} bytes but only {} allocated", num_bytes, old);
+}
+
+impl GpuFrameId {
     pub fn new(value: usize) -> Self {
-        FrameId(value)
+        GpuFrameId(value)
     }
 }
 
-impl Add<usize> for FrameId {
-    type Output = FrameId;
+impl Add<usize> for GpuFrameId {
+    type Output = GpuFrameId;
 
-    fn add(self, other: usize) -> FrameId {
-        FrameId(self.0 + other)
+    fn add(self, other: usize) -> GpuFrameId {
+        GpuFrameId(self.0 + other)
     }
 }
 
 const SHADER_VERSION_GL: &str = "#version 150\n";
 const SHADER_VERSION_GLES: &str = "#version 300 es\n";
 
 const SHADER_KIND_VERTEX: &str = "#define WR_VERTEX_SHADER\n";
 const SHADER_KIND_FRAGMENT: &str = "#define WR_FRAGMENT_SHADER\n";
@@ -112,16 +138,24 @@ pub enum UploadMethod {
     PixelBuffer(VertexUsageHint),
 }
 
 /// Plain old data that can be used to initialize a texture.
 pub unsafe trait Texel: Copy {}
 unsafe impl Texel for u8 {}
 unsafe impl Texel for f32 {}
 
+/// Returns the size in bytes of a depth target with the given dimensions.
+fn depth_target_size_in_bytes(dimensions: &DeviceUintSize) -> usize {
+    // DEPTH24 textures generally reserve 3 bytes for depth and 1 byte
+    // for stencil, so we measure them as 32 bits.
+    let pixels = dimensions.width * dimensions.height;
+    (pixels as usize) * 4
+}
+
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub enum ReadPixelsFormat {
     Standard(ImageFormat),
     Rgba8,
 }
 
 pub fn get_gl_target(target: TextureTarget) -> gl::GLuint {
     match target {
@@ -460,17 +494,17 @@ pub struct Texture {
     /// the depth buffer has never been requested.
     ///
     /// Note that we always fill fbos, and then lazily create fbos_with_depth
     /// when needed. We could make both lazy (i.e. render targets would have one
     /// or the other, but not both, unless they were actually used in both
     /// configurations). But that would complicate a lot of logic in this module,
     /// and FBOs are cheap enough to create.
     fbos_with_depth: Vec<FBOId>,
-    last_frame_used: FrameId,
+    last_frame_used: GpuFrameId,
 }
 
 impl Texture {
     pub fn get_dimensions(&self) -> DeviceUintSize {
         DeviceUintSize::new(self.width, self.height)
     }
 
     pub fn get_layer_count(&self) -> i32 {
@@ -485,23 +519,23 @@ impl Texture {
     pub fn get_filter(&self) -> TextureFilter {
         self.filter
     }
 
     pub fn supports_depth(&self) -> bool {
         !self.fbos_with_depth.is_empty()
     }
 
-    pub fn used_in_frame(&self, frame_id: FrameId) -> bool {
+    pub fn used_in_frame(&self, frame_id: GpuFrameId) -> bool {
         self.last_frame_used == frame_id
     }
 
     /// Returns true if this texture was used within `threshold` frames of
     /// the current frame.
-    pub fn used_recently(&self, current_frame_id: FrameId, threshold: usize) -> bool {
+    pub fn used_recently(&self, current_frame_id: GpuFrameId, threshold: usize) -> bool {
         self.last_frame_used + threshold >= current_frame_id
     }
 
     /// Returns the number of bytes (generally in GPU memory) that this texture
     /// consumes.
     pub fn size_in_bytes(&self) -> usize {
         assert!(self.layer_count > 0 || self.width + self.height == 0);
         let bpp = self.format.bytes_per_pixel() as usize;
@@ -731,16 +765,25 @@ struct SharedDepthTarget {
 
 #[cfg(debug_assertions)]
 impl Drop for SharedDepthTarget {
     fn drop(&mut self) {
         debug_assert!(thread::panicking() || self.refcount == 0);
     }
 }
 
+/// Describes for which texture formats to use the glTexStorage*
+/// family of functions.
+#[derive(PartialEq, Debug)]
+enum TexStorageUsage {
+    Never,
+    NonBGRA8,
+    Always,
+}
+
 pub struct Device {
     gl: Rc<gl::Gl>,
     // device state
     bound_textures: [gl::GLuint; 16],
     bound_program: gl::GLuint,
     bound_vao: gl::GLuint,
     bound_read_fbo: FBOId,
     bound_draw_fbo: FBOId,
@@ -775,23 +818,24 @@ pub struct Device {
     resource_override_path: Option<PathBuf>,
 
     max_texture_size: u32,
     renderer_name: String,
     cached_programs: Option<Rc<ProgramCache>>,
 
     // Frame counter. This is used to map between CPU
     // frames and GPU frames.
-    frame_id: FrameId,
+    frame_id: GpuFrameId,
 
-    /// Whether glTexStorage* is supported. We prefer this over glTexImage*
-    /// because it guarantees that mipmaps won't be generated (which they
-    /// otherwise are on some drivers, particularly ANGLE), If it's not
-    /// supported, we fall back to glTexImage*.
-    supports_texture_storage: bool,
+    /// When to use glTexStorage*. We prefer this over glTexImage* because it
+    /// guarantees that mipmaps won't be generated (which they otherwise are on
+    /// some drivers, particularly ANGLE). However, it is not always supported
+    /// at all, or for BGRA8 format. If it's not supported for the required
+    /// format, we fall back to glTexImage*.
+    texture_storage_usage: TexStorageUsage,
 
     // GL extensions
     extensions: Vec<String>,
 }
 
 /// Contains the parameters necessary to bind a draw target.
 #[derive(Clone, Copy)]
 pub enum DrawTarget<'a> {
@@ -883,32 +927,75 @@ impl Device {
         // On Mac, Apple docs [1] claim that BGRA is a more efficient internal
         // format, so we may want to consider doing that at some point, since it
         // would give us both a more efficient internal format and avoid the
         // swizzling in the common case.
         //
         // We also need our internal format types to be sized, since glTexStorage*
         // will reject non-sized internal format types.
         //
+        // Unfortunately, with GL_EXT_texture_format_BGRA8888, BGRA8 is not a
+        // valid internal format (for glTexImage* or glTexStorage*) unless
+        // GL_EXT_texture_storage is also available [2][3], which is usually
+        // not the case on GLES 3 as the latter's functionality has been
+        // included by default but the former has not been updated.
+        // The extension is available on ANGLE, but on Android this usually
+        // means we must fall back to using unsized BGRA and glTexImage*.
+        //
         // [1] https://developer.apple.com/library/archive/documentation/
         //     GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/
         //     opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW22
+        // [2] https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_format_BGRA8888.txt
+        // [3] https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_storage.txt
         let supports_bgra = supports_extension(&extensions, "GL_EXT_texture_format_BGRA8888");
-        let (bgra_format_internal, bgra_format_external) = if supports_bgra {
-            assert_eq!(gl.get_type(), gl::GlType::Gles, "gleam only detects bgra on gles");
-            (gl::BGRA8_EXT, gl::BGRA_EXT)
-        } else {
-            (gl::RGBA8, gl::BGRA)
-        };
-
         let supports_texture_storage = match gl.get_type() {
             gl::GlType::Gl => supports_extension(&extensions, "GL_ARB_texture_storage"),
             gl::GlType::Gles => true,
         };
 
+
+        let (bgra_format_internal, bgra_format_external, texture_storage_usage) = if supports_bgra {
+            assert_eq!(gl.get_type(), gl::GlType::Gles, "gleam only detects bgra on gles");
+            // To support BGRA8 with glTexStorage* we specifically need
+            // GL_EXT_texture_storage and GL_EXT_texture_format_BGRA8888.
+            if supports_extension(&extensions, "GL_EXT_texture_format_BGRA8888") && supports_extension(&extensions, "GL_EXT_texture_storage") {
+                // We can use glTexStorage with BGRA8 as the internal format.
+                (gl::BGRA8_EXT, gl::BGRA_EXT, TexStorageUsage::Always)
+            } else {
+                // For BGRA8 textures we must must use the unsized BGRA internal
+                // format and glTexImage. If texture storage is supported we can
+                // use it for other formats.
+                (
+                    gl::BGRA_EXT,
+                    gl::BGRA_EXT,
+                    if supports_texture_storage {
+                        TexStorageUsage::NonBGRA8
+                    } else {
+                        TexStorageUsage::Never
+                    },
+                )
+            }
+        } else {
+            // BGRA is not supported as an internal format, therefore we will
+            // use RGBA and swizzle during upload. Note that this is not
+            // supported on GLES.
+            // Since the internal format will actually be RGBA, if texture
+            // storage is supported we can use it for such textures.
+            assert_ne!(gl.get_type(), gl::GlType::Gles, "gles must have compatible internal and external formats");
+            (
+                gl::RGBA8,
+                gl::BGRA,
+                if supports_texture_storage {
+                    TexStorageUsage::Always
+                } else {
+                    TexStorageUsage::Never
+                },
+            )
+        };
+
         Device {
             gl,
             resource_override_path,
             // This is initialized to 1 by default, but it is reset
             // at the beginning of each frame in `Renderer::bind_frame_data`.
             device_pixel_ratio: 1.0,
             upload_method,
             inside_frame: false,
@@ -932,19 +1019,19 @@ impl Device {
             default_read_fbo: FBOId(0),
             default_draw_fbo: FBOId(0),
 
             depth_available: true,
 
             max_texture_size,
             renderer_name,
             cached_programs,
-            frame_id: FrameId(0),
+            frame_id: GpuFrameId(0),
             extensions,
-            supports_texture_storage,
+            texture_storage_usage,
         }
     }
 
     pub fn gl(&self) -> &gl::Gl {
         &*self.gl
     }
 
     pub fn rc_gl(&self) -> &Rc<gl::Gl> {
@@ -1017,17 +1104,17 @@ impl Device {
         } else {
             if !log.is_empty() {
                 warn!("Warnings detected on shader: {}\n{}", name, log);
             }
             Ok(id)
         }
     }
 
-    pub fn begin_frame(&mut self) -> FrameId {
+    pub fn begin_frame(&mut self) -> GpuFrameId {
         debug_assert!(!self.inside_frame);
         self.inside_frame = true;
 
         // Retrieve the currently set FBO.
         let mut default_read_fbo = [0];
         unsafe {
             self.gl.get_integer_v(gl::READ_FRAMEBUFFER_BINDING, &mut default_read_fbo);
         }
@@ -1370,17 +1457,22 @@ impl Device {
             ((max_dimension) as f64).log2() as gl::GLint + 1
         } else {
             1
         };
 
         // Use glTexStorage where available, since it avoids allocating
         // unnecessary mipmap storage and generally improves performance with
         // stronger invariants.
-        match (self.supports_texture_storage, is_array) {
+        let use_texture_storage = match self.texture_storage_usage {
+            TexStorageUsage::Always => true,
+            TexStorageUsage::NonBGRA8 => texture.format != ImageFormat::BGRA8,
+            TexStorageUsage::Never => false,
+        };
+        match (use_texture_storage, is_array) {
             (true, true) =>
                 self.gl.tex_storage_3d(
                     gl::TEXTURE_2D_ARRAY,
                     mipmap_levels,
                     desc.internal,
                     texture.width as gl::GLint,
                     texture.height as gl::GLint,
                     texture.layer_count,
@@ -1423,16 +1515,18 @@ impl Device {
         // Set up FBOs, if required.
         if let Some(rt_info) = render_target {
             self.init_fbos(&mut texture, false);
             if rt_info.has_depth {
                 self.init_fbos(&mut texture, true);
             }
         }
 
+        record_gpu_alloc(texture.size_in_bytes());
+
         texture
     }
 
     fn set_texture_parameters(&mut self, target: gl::GLuint, filter: TextureFilter) {
         let mag_filter = match filter {
             TextureFilter::Nearest => gl::NEAREST,
             TextureFilter::Linear | TextureFilter::Trilinear => gl::LINEAR,
         };
@@ -1586,30 +1680,34 @@ impl Device {
                 dimensions.width as _,
                 dimensions.height as _,
             );
             SharedDepthTarget {
                 rbo_id: RBOId(depth_rb),
                 refcount: 0,
             }
         });
+        if target.refcount == 0 {
+            record_gpu_alloc(depth_target_size_in_bytes(&dimensions));
+        }
         target.refcount += 1;
         target.rbo_id
     }
 
     fn release_depth_target(&mut self, dimensions: DeviceUintSize) {
         let mut entry = match self.depth_targets.entry(dimensions) {
             Entry::Occupied(x) => x,
             Entry::Vacant(..) => panic!("Releasing unknown depth target"),
         };
         debug_assert!(entry.get().refcount != 0);
         entry.get_mut().refcount -= 1;
         if entry.get().refcount == 0 {
-            let t = entry.remove();
-            self.gl.delete_renderbuffers(&[t.rbo_id.0]);
+            let (dimensions, target) = entry.remove_entry();
+            self.gl.delete_renderbuffers(&[target.rbo_id.0]);
+            record_gpu_free(depth_target_size_in_bytes(&dimensions));
         }
     }
 
     pub fn blit_render_target(&mut self, src_rect: DeviceIntRect, dest_rect: DeviceIntRect) {
         debug_assert!(self.inside_frame);
 
         self.gl.blit_framebuffer(
             src_rect.origin.x,
@@ -1622,16 +1720,17 @@ impl Device {
             dest_rect.origin.y + dest_rect.size.height,
             gl::COLOR_BUFFER_BIT,
             gl::LINEAR,
         );
     }
 
     pub fn delete_texture(&mut self, mut texture: Texture) {
         debug_assert!(self.inside_frame);
+        record_gpu_free(texture.size_in_bytes());
         let had_depth = texture.supports_depth();
         self.deinit_fbos(&mut texture.fbos);
         self.deinit_fbos(&mut texture.fbos_with_depth);
         if had_depth {
             self.release_depth_target(texture.get_dimensions());
         }
 
         self.gl.delete_textures(&[texture.id]);
@@ -2460,20 +2559,17 @@ impl Device {
             },
         }
     }
 
     /// Generates a memory report for the resources managed by the device layer.
     pub fn report_memory(&self) -> MemoryReport {
         let mut report = MemoryReport::default();
         for dim in self.depth_targets.keys() {
-            // DEPTH24 textures generally reserve 3 bytes for depth and 1 byte
-            // for stencil, so we measure them as 32 bytes.
-            let pixels: u32 = dim.width * dim.height;
-            report.depth_target_textures += (pixels as usize) * 4;
+            report.depth_target_textures += depth_target_size_in_bytes(dim);
         }
         report
     }
 }
 
 struct FormatDesc {
     internal: gl::GLenum,
     external: gl::GLuint,
--- a/gfx/webrender/src/device/query_gl.rs
+++ b/gfx/webrender/src/device/query_gl.rs
@@ -2,17 +2,17 @@
  * 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 gleam::gl;
 #[cfg(feature = "debug_renderer")]
 use std::mem;
 use std::rc::Rc;
 
-use device::FrameId;
+use device::GpuFrameId;
 
 
 pub trait NamedTag {
     fn get_label(&self) -> &str;
 }
 
 #[derive(Debug, Clone)]
 pub struct GpuTimer<T> {
@@ -64,28 +64,28 @@ impl<T> QuerySet<T> {
         data
     }
 }
 
 pub struct GpuFrameProfile<T> {
     gl: Rc<gl::Gl>,
     timers: QuerySet<GpuTimer<T>>,
     samplers: QuerySet<GpuSampler<T>>,
-    frame_id: FrameId,
+    frame_id: GpuFrameId,
     inside_frame: bool,
     ext_debug_marker: bool
 }
 
 impl<T> GpuFrameProfile<T> {
     fn new(gl: Rc<gl::Gl>, ext_debug_marker: bool) -> Self {
         GpuFrameProfile {
             gl,
             timers: QuerySet::new(),
             samplers: QuerySet::new(),
-            frame_id: FrameId::new(0),
+            frame_id: GpuFrameId::new(0),
             inside_frame: false,
             ext_debug_marker
         }
     }
 
     fn enable_timers(&mut self, count: i32) {
         self.timers.set = self.gl.gen_queries(count);
     }
@@ -103,17 +103,17 @@ impl<T> GpuFrameProfile<T> {
 
     fn disable_samplers(&mut self) {
         if !self.samplers.set.is_empty() {
             self.gl.delete_queries(&self.samplers.set);
         }
         self.samplers.set = Vec::new();
     }
 
-    fn begin_frame(&mut self, frame_id: FrameId) {
+    fn begin_frame(&mut self, frame_id: GpuFrameId) {
         self.frame_id = frame_id;
         self.timers.reset();
         self.samplers.reset();
         self.inside_frame = true;
     }
 
     fn end_frame(&mut self) {
         self.finish_timer();
@@ -157,17 +157,17 @@ impl<T: NamedTag> GpuFrameProfile<T> {
         if let Some(query) = self.samplers.add(GpuSampler { tag, count: 0 }) {
             self.gl.begin_query(gl::SAMPLES_PASSED, query);
         }
 
         GpuSampleQuery
     }
 
     #[cfg(feature = "debug_renderer")]
-    fn build_samples(&mut self) -> (FrameId, Vec<GpuTimer<T>>, Vec<GpuSampler<T>>) {
+    fn build_samples(&mut self) -> (GpuFrameId, Vec<GpuTimer<T>>, Vec<GpuSampler<T>>) {
         debug_assert!(!self.inside_frame);
         let gl = &self.gl;
 
         (
             self.frame_id,
             self.timers.take(|timer, query| {
                 timer.time_ns = gl.get_query_object_ui64v(query, gl::QUERY_RESULT)
             }),
@@ -236,21 +236,21 @@ impl<T> GpuProfiler<T> {
         for frame in &mut self.frames {
             frame.disable_samplers();
         }
     }
 }
 
 impl<T: NamedTag> GpuProfiler<T> {
     #[cfg(feature = "debug_renderer")]
-    pub fn build_samples(&mut self) -> (FrameId, Vec<GpuTimer<T>>, Vec<GpuSampler<T>>) {
+    pub fn build_samples(&mut self) -> (GpuFrameId, Vec<GpuTimer<T>>, Vec<GpuSampler<T>>) {
         self.frames[self.next_frame].build_samples()
     }
 
-    pub fn begin_frame(&mut self, frame_id: FrameId) {
+    pub fn begin_frame(&mut self, frame_id: GpuFrameId) {
         self.frames[self.next_frame].begin_frame(frame_id);
     }
 
     pub fn end_frame(&mut self) {
         self.frames[self.next_frame].end_frame();
         self.next_frame = (self.next_frame + 1) % self.frames.len();
     }
 
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -1690,26 +1690,21 @@ impl<'a> DisplayListFlattener<'a> {
         );
     }
 
     pub fn add_clear_rectangle(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
     ) {
-        let prim = BrushPrimitive::new(
-            BrushKind::Clear,
-            None,
-        );
-
         self.add_primitive(
             clip_and_scroll,
             info,
             Vec::new(),
-            PrimitiveContainer::Brush(prim),
+            PrimitiveContainer::Clear,
         );
     }
 
     pub fn add_line(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
         wavy_line_thickness: f32,
--- a/gfx/webrender/src/frame_builder.rs
+++ b/gfx/webrender/src/frame_builder.rs
@@ -332,17 +332,17 @@ impl FrameBuilder {
         );
 
         let mut profile_counters = FrameProfileCounters::new();
         profile_counters
             .total_primitives
             .set(self.prim_store.prim_count());
 
         resource_cache.begin_frame(frame_id);
-        gpu_cache.begin_frame();
+        gpu_cache.begin_frame(frame_id);
 
         let mut transform_palette = TransformPalette::new();
         clip_scroll_tree.update_tree(
             pan,
             scene_properties,
             Some(&mut transform_palette),
         );
 
--- a/gfx/webrender/src/freelist.rs
+++ b/gfx/webrender/src/freelist.rs
@@ -1,32 +1,70 @@
 /* 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 std::marker::PhantomData;
+//! A generic backing store for caches.
+//!
+//! `FreeList` is a simple vector-backed data structure where each entry in the
+//! vector contains an Option<T>. It maintains an index-based (rather than
+//! pointer-based) free list to efficiently locate the next unused entry. If all
+//! entries are occupied, insertion appends a new element to the vector.
+//!
+//! It also supports both strong and weak handle semantics. There is exactly one
+//! (non-Clonable) strong handle per occupied entry, which must be passed by
+//! value into `free()` to release an entry. Strong handles can produce an
+//! unlimited number of (Clonable) weak handles, which are used to perform
+//! lookups which may fail of the entry has been freed. A per-entry epoch ensures
+//! that weak handle lookups properly fail even if the entry has been freed and
+//! reused.
+//!
+//! TODO(gw): Add an occupied list head, for fast iteration of the occupied list
+//! to implement retain() style functionality.
 
-// TODO(gw): Add an occupied list head, for fast
-//           iteration of the occupied list to implement
-//           retain() style functionality.
+use std::fmt;
+use std::marker::PhantomData;
 
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct Epoch(u32);
 
-#[derive(Debug)]
+impl Epoch {
+    /// Mints a new epoch.
+    ///
+    /// We start at 1 so that 0 is always an invalid epoch.
+    fn new() -> Self {
+        Epoch(1)
+    }
+
+    /// Returns an always-invalid epoch.
+    fn invalid() -> Self {
+        Epoch(0)
+    }
+}
+
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct FreeListHandle<M> {
     index: u32,
     epoch: Epoch,
     _marker: PhantomData<M>,
 }
 
+/// More-compact textual representation for debug logging.
+impl<M> fmt::Debug for FreeListHandle<M> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("StrongHandle")
+            .field("index", &self.index)
+            .field("epoch", &self.epoch.0)
+            .finish()
+    }
+}
+
 impl<M> FreeListHandle<M> {
     pub fn weak(&self) -> WeakFreeListHandle<M> {
         WeakFreeListHandle {
             index: self.index,
             epoch: self.epoch,
             _marker: PhantomData,
         }
     }
@@ -37,25 +75,51 @@ impl<M> Clone for WeakFreeListHandle<M> 
         WeakFreeListHandle {
             index: self.index,
             epoch: self.epoch,
             _marker: PhantomData,
         }
     }
 }
 
-#[derive(Debug)]
+impl<M> PartialEq for WeakFreeListHandle<M> {
+    fn eq(&self, other: &Self) -> bool {
+        self.index == other.index && self.epoch == other.epoch
+    }
+}
+
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct WeakFreeListHandle<M> {
     index: u32,
     epoch: Epoch,
     _marker: PhantomData<M>,
 }
 
+/// More-compact textual representation for debug logging.
+impl<M> fmt::Debug for WeakFreeListHandle<M> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("WeakHandle")
+            .field("index", &self.index)
+            .field("epoch", &self.epoch.0)
+            .finish()
+    }
+}
+
+impl<M> WeakFreeListHandle<M> {
+    /// Returns an always-invalid handle.
+    pub fn invalid() -> Self {
+        Self {
+            index: 0,
+            epoch: Epoch::invalid(),
+            _marker: PhantomData,
+        }
+    }
+}
+
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct Slot<T> {
     next: Option<u32>,
     epoch: Epoch,
     value: Option<T>,
 }
@@ -71,20 +135,31 @@ pub struct FreeList<T, M> {
 }
 
 pub enum UpsertResult<T, M> {
     Updated(T),
     Inserted(FreeListHandle<M>),
 }
 
 impl<T, M> FreeList<T, M> {
+    /// Mints a new `FreeList` with no entries.
+    ///
+    /// Triggers a 1-entry allocation.
     pub fn new() -> Self {
+        // We guarantee that we never have zero entries by starting with one
+        // free entry. This allows WeakFreeListHandle::invalid() to work
+        // without adding any additional branches.
+        let first_slot = Slot {
+            next: None,
+            epoch: Epoch::new(),
+            value: None,
+        };
         FreeList {
-            slots: Vec::new(),
-            free_list_head: None,
+            slots: vec![first_slot],
+            free_list_head: Some(0),
             active_count: 0,
             _marker: PhantomData,
         }
     }
 
     pub fn clear(&mut self) {
         self.slots.clear();
         self.free_list_head = None;
@@ -149,17 +224,17 @@ impl<T, M> FreeList<T, M> {
                 FreeListHandle {
                     index: free_index,
                     epoch: slot.epoch,
                     _marker: PhantomData,
                 }
             }
             None => {
                 let index = self.slots.len() as u32;
-                let epoch = Epoch(0);
+                let epoch = Epoch::new();
 
                 self.slots.push(Slot {
                     next: None,
                     epoch,
                     value: Some(item),
                 });
 
                 FreeListHandle {
--- a/gfx/webrender/src/glyph_rasterizer/mod.rs
+++ b/gfx/webrender/src/glyph_rasterizer/mod.rs
@@ -729,17 +729,17 @@ mod test_glyph_rasterizer {
             })
             .build();
         let workers = Arc::new(worker.unwrap());
         let mut glyph_rasterizer = GlyphRasterizer::new(workers).unwrap();
         let mut glyph_cache = GlyphCache::new();
         let mut gpu_cache = GpuCache::new();
         let mut texture_cache = TextureCache::new(2048);
         let mut render_task_cache = RenderTaskCache::new();
-        let mut render_task_tree = RenderTaskTree::new(FrameId(0));
+        let mut render_task_tree = RenderTaskTree::new(FrameId::invalid());
         let mut special_render_passes = SpecialRenderPasses::new(&DeviceIntSize::new(1366, 768));
 
         let mut font_file =
             File::open("../wrench/reftests/text/VeraBd.ttf").expect("Couldn't open font file");
         let mut font_data = vec![];
         font_file
             .read_to_end(&mut font_data)
             .expect("failed to read font file");
--- a/gfx/webrender/src/glyph_rasterizer/no_pathfinder.rs
+++ b/gfx/webrender/src/glyph_rasterizer/no_pathfinder.rs
@@ -164,17 +164,17 @@ impl GlyphRasterizer {
                 let glyph_info = match result {
                     GlyphRasterResult::LoadFailed => GlyphCacheEntry::Blank,
                     GlyphRasterResult::Bitmap(ref glyph) if glyph.width == 0 ||
                                                             glyph.height == 0 => {
                         GlyphCacheEntry::Blank
                     }
                     GlyphRasterResult::Bitmap(glyph) => {
                         assert_eq!((glyph.left.fract(), glyph.top.fract()), (0.0, 0.0));
-                        let mut texture_cache_handle = TextureCacheHandle::new();
+                        let mut texture_cache_handle = TextureCacheHandle::invalid();
                         texture_cache.request(&texture_cache_handle, gpu_cache);
                         texture_cache.update(
                             &mut texture_cache_handle,
                             ImageDescriptor {
                                 size: size2(glyph.width, glyph.height),
                                 stride: None,
                                 format: ImageFormat::BGRA8,
                                 is_opaque: false,
--- a/gfx/webrender/src/gpu_cache.rs
+++ b/gfx/webrender/src/gpu_cache.rs
@@ -21,19 +21,19 @@
 //!
 //! After ```end_frame``` has occurred, callers can
 //! use the ```get_address``` API to get the allocated
 //! address in the GPU cache of a given resource slot
 //! for this frame.
 
 use api::{PremultipliedColorF, TexelRect};
 use api::{VoidPtrToSizeFn};
-use device::FrameId;
 use euclid::TypedRect;
 use profiler::GpuCacheProfileCounters;
+use render_backend::FrameId;
 use renderer::MAX_VERTEX_TEXTURE_WIDTH;
 use std::{mem, u16, u32};
 use std::ops::Add;
 use std::os::raw::c_void;
 
 
 pub const GPU_CACHE_INITIAL_HEIGHT: u32 = 512;
 const FRAMES_BEFORE_EVICTION: usize = 10;
@@ -544,27 +544,27 @@ pub struct GpuCache {
     /// True if the Renderer expects to receive the metadata
     /// about GPU blocks with on each update.
     in_debug: bool,
 }
 
 impl GpuCache {
     pub fn new() -> Self {
         GpuCache {
-            frame_id: FrameId::new(0),
+            frame_id: FrameId::invalid(),
             texture: Texture::new(),
             saved_block_count: 0,
             in_debug: false,
         }
     }
 
     /// Begin a new frame.
-    pub fn begin_frame(&mut self) {
+    pub fn begin_frame(&mut self, frame_id: FrameId) {
         debug_assert!(self.texture.pending_blocks.is_empty());
-        self.frame_id = self.frame_id + 1;
+        self.frame_id = frame_id;
         self.texture.evict_old_blocks(self.frame_id);
         self.saved_block_count = 0;
     }
 
     // Invalidate a (possibly) existing block in the cache.
     // This means the next call to request() for this location
     // will rebuild the data and upload it to the GPU.
     pub fn invalidate(&mut self, handle: &GpuCacheHandle) {
--- a/gfx/webrender/src/lib.rs
+++ b/gfx/webrender/src/lib.rs
@@ -35,16 +35,26 @@ These are obtained by finalizing a `Disp
 But it doesn't only contain trivial geometry, it can also store another StackingContext, as
 they're nestable.
 
 [stacking_contexts]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
 [newframe]: ../webrender_api/struct.RenderApi.html#method.set_display_list
 [notifier]: renderer/struct.Renderer.html#method.set_render_notifier
 */
 
+// Cribbed from the |matches| crate, for simplicity.
+macro_rules! matches {
+    ($expression:expr, $($pattern:tt)+) => {
+        match $expression {
+            $($pattern)+ => true,
+            _ => false
+        }
+    }
+}
+
 #[macro_use]
 extern crate bitflags;
 #[macro_use]
 extern crate cfg_if;
 #[macro_use]
 extern crate lazy_static;
 #[macro_use]
 extern crate log;
@@ -177,17 +187,17 @@ extern crate base64;
 #[cfg(all(feature = "capture", feature = "png"))]
 extern crate png;
 
 pub extern crate webrender_api;
 
 #[doc(hidden)]
 pub use device::{build_shader_strings, ReadPixelsFormat, UploadMethod, VertexUsageHint};
 pub use device::{ProgramBinary, ProgramCache, ProgramCacheObserver, ProgramSources};
-pub use device::{Device};
+pub use device::{Device, total_gpu_bytes_allocated};
 pub use frame_builder::ChasePrimitive;
 pub use renderer::{AsyncPropertySampler, CpuProfile, DebugFlags, OutputImageHandler, RendererKind};
 pub use renderer::{ExternalImage, ExternalImageHandler, ExternalImageSource, GpuProfile};
 pub use renderer::{GraphicsApi, GraphicsApiInfo, PipelineInfo, Renderer, RendererOptions};
 pub use renderer::{RendererStats, SceneBuilderHooks, ThreadListener, ShaderPrecacheFlags};
 pub use renderer::MAX_VERTEX_TEXTURE_WIDTH;
 pub use shade::{Shaders, WrShaders};
 pub use webrender_api as api;
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -325,16 +325,18 @@ pub enum PrimitiveKeyKind {
     LineDecoration {
         // If the cache_key is Some(..) it is a line decoration
         // that relies on a render task (e.g. wavy). If the
         // cache key is None, it uses a fast path to draw the
         // line decoration as a solid rect.
         cache_key: Option<LineDecorationCacheKey>,
         color: ColorU,
     },
+    /// Clear an existing rect, used for special effects on some platforms.
+    Clear,
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Clone, Eq, PartialEq, Hash)]
 pub struct PrimitiveKey {
     pub is_backface_visible: bool,
     pub prim_rect: LayoutRectAu,
@@ -370,16 +372,19 @@ impl PrimitiveKey {
                 PrimitiveInstanceKind::TextRun {
                     run: TextRunPrimitive {
                         used_font: font.clone(),
                         glyph_keys: Vec::new(),
                         shadow,
                     }
                 }
             }
+            PrimitiveKeyKind::Clear => {
+                PrimitiveInstanceKind::Clear
+            }
             PrimitiveKeyKind::Unused => {
                 // Should never be hit as this method should not be
                 // called for old style primitives.
                 unreachable!();
             }
         }
     }
 }
@@ -393,16 +398,17 @@ pub enum PrimitiveTemplateKind {
         cache_key: Option<LineDecorationCacheKey>,
         color: ColorF,
     },
     TextRun {
         font: FontInstance,
         offset: LayoutVector2DAu,
         glyphs: Vec<GlyphInstance>,
     },
+    Clear,
     Unused,
 }
 
 /// Construct the primitive template data from a primitive key. This
 /// is invoked when a primitive key is created and the interner
 /// doesn't currently contain a primitive with this key.
 impl From<PrimitiveKeyKind> for PrimitiveTemplateKind {
     fn from(item: PrimitiveKeyKind) -> Self {
@@ -410,16 +416,19 @@ impl From<PrimitiveKeyKind> for Primitiv
             PrimitiveKeyKind::Unused => PrimitiveTemplateKind::Unused,
             PrimitiveKeyKind::TextRun { glyphs, font, offset, .. } => {
                 PrimitiveTemplateKind::TextRun {
                     font,
                     offset,
                     glyphs,
                 }
             }
+            PrimitiveKeyKind::Clear => {
+                PrimitiveTemplateKind::Clear
+            }
             PrimitiveKeyKind::LineDecoration { cache_key, color } => {
                 PrimitiveTemplateKind::LineDecoration {
                     cache_key,
                     color: color.into(),
                 }
             }
         }
     }
@@ -456,16 +465,27 @@ impl PrimitiveTemplate {
     /// times per frame, by each primitive reference that refers to this interned
     /// template. The initial request call to the GPU cache ensures that work is only
     /// done if the cache entry is invalid (due to first use or eviction).
     pub fn update(
         &mut self,
         gpu_cache: &mut GpuCache,
     ) {
         match self.kind {
+            PrimitiveTemplateKind::Clear => {
+                if let Some(mut request) = gpu_cache.request(&mut self.gpu_cache_handle) {
+                    // Opaque black with operator dest out
+                    request.push(PremultipliedColorF::BLACK);
+
+                    request.write_segment(
+                        self.prim_rect,
+                        [0.0; 4],
+                    );
+                }
+            }
             PrimitiveTemplateKind::LineDecoration { ref cache_key, ref color } => {
                 if let Some(mut request) = gpu_cache.request(&mut self.gpu_cache_handle) {
                     // Work out the stretch parameters (for image repeat) based on the
                     // line decoration parameters.
 
                     match cache_key {
                         Some(cache_key) => {
                             request.push(color.premultiplied());
@@ -623,17 +643,16 @@ pub enum BorderSource {
     },
 }
 
 pub enum BrushKind {
     Solid {
         color: ColorF,
         opacity_binding: OpacityBinding,
     },
-    Clear,
     Image {
         request: ImageRequest,
         alpha_type: AlphaType,
         stretch_size: LayoutSize,
         tile_spacing: LayoutSize,
         color: ColorF,
         source: ImageSource,
         sub_rect: Option<DeviceIntRect>,
@@ -687,18 +706,16 @@ impl BrushKind {
                     .is_none()
             }
 
             BrushKind::Solid { .. } |
             BrushKind::YuvImage { .. } |
             BrushKind::RadialGradient { .. } |
             BrushKind::Border { .. } |
             BrushKind::LinearGradient { .. } => true,
-
-            BrushKind::Clear => false,
         }
     }
 
     // Construct a brush that is a solid color rectangle.
     pub fn new_solid(color: ColorF) -> BrushKind {
         BrushKind::Solid {
             color,
             opacity_binding: OpacityBinding::new(),
@@ -921,20 +938,16 @@ impl BrushPrimitive {
                     0.0,
                     0.0,
                 ]);
             }
             // Solid rects also support opacity collapsing.
             BrushKind::Solid { ref color, .. } => {
                 request.push(color.premultiplied());
             }
-            BrushKind::Clear => {
-                // Opaque black with operator dest out
-                request.push(PremultipliedColorF::BLACK);
-            }
             BrushKind::LinearGradient { stretch_size, start_point, end_point, extend_mode, .. } => {
                 request.push([
                     start_point.x,
                     start_point.y,
                     end_point.x,
                     end_point.y,
                 ]);
                 request.push([
@@ -1492,16 +1505,17 @@ impl ClipData {
 
 pub enum PrimitiveContainer {
     TextRun {
         font: FontInstance,
         offset: LayoutVector2D,
         glyphs: Vec<GlyphInstance>,
         shadow: bool,
     },
+    Clear,
     Brush(BrushPrimitive),
     LineDecoration {
         color: ColorF,
         style: LineStyle,
         orientation: LineOrientation,
         wavy_line_thickness: f32,
     },
 }
@@ -1519,26 +1533,28 @@ impl PrimitiveContainer {
             PrimitiveContainer::TextRun { ref font, .. } => {
                 font.color.a > 0
             }
             PrimitiveContainer::Brush(ref brush) => {
                 match brush.kind {
                     BrushKind::Solid { ref color, .. } => {
                         color.a > 0.0
                     }
-                    BrushKind::Clear |
                     BrushKind::Image { .. } |
                     BrushKind::YuvImage { .. } |
                     BrushKind::RadialGradient { .. } |
                     BrushKind::Border { .. } |
                     BrushKind::LinearGradient { .. } => {
                         true
                     }
                 }
             }
+            PrimitiveContainer::Clear => {
+                true
+            }
             PrimitiveContainer::LineDecoration { ref color, .. } => {
                 color.a > 0.0
             }
         }
     }
 
     /// Convert a source primitive container into a key, and optionally
     /// an old style PrimitiveDetails structure.
@@ -1552,16 +1568,19 @@ impl PrimitiveContainer {
                     font,
                     offset: offset.to_au(),
                     glyphs,
                     shadow,
                 };
 
                 (key, None)
             }
+            PrimitiveContainer::Clear => {
+                (PrimitiveKeyKind::Clear, None)
+            }
             PrimitiveContainer::LineDecoration { color, style, orientation, wavy_line_thickness } => {
                 // For line decorations, we can construct the render task cache key
                 // here during scene building, since it doesn't depend on device
                 // pixel ratio or transform.
 
                 let size = get_line_decoration_sizes(
                     &info.rect.size,
                     orientation,
@@ -1687,24 +1706,26 @@ impl PrimitiveContainer {
                     BrushKind::Image { request, stretch_size, .. } => {
                         PrimitiveContainer::Brush(BrushPrimitive::new(
                             BrushKind::new_image(request.clone(),
                                                  stretch_size.clone(),
                                                  shadow.color),
                             None,
                         ))
                     }
-                    BrushKind::Clear |
                     BrushKind::YuvImage { .. } |
                     BrushKind::RadialGradient { .. } |
                     BrushKind::LinearGradient { .. } => {
                         panic!("bug: other brush kinds not expected here yet");
                     }
                 }
             }
+            PrimitiveContainer::Clear => {
+                panic!("bug: clear rects are not supported in shadow contexts");
+            }
         }
     }
 }
 
 pub enum PrimitiveDetails {
     Brush(BrushPrimitive),
 }
 
@@ -1742,16 +1763,18 @@ pub enum PrimitiveInstanceKind {
         //           prepare_prims and read during the batching pass.
         //           Once we unify the prepare_prims and batching to
         //           occur at the same time, we can remove most of
         //           the things we store here in the instance, and
         //           use them directly. This will remove cache_handle,
         //           but also the opacity, clip_task_id etc below.
         cache_handle: Option<RenderTaskCacheEntryHandle>,
     },
+    /// Clear out a rect, used for special effects.
+    Clear,
 }
 
 #[derive(Clone, Debug)]
 pub struct PrimitiveInstance {
     /// Identifies the kind of primitive this
     /// instance is, and references to where
     /// the relevant information for the primitive
     /// can be found.
@@ -1809,17 +1832,17 @@ impl PrimitiveInstance {
         spatial_node_index: SpatialNodeIndex,
     ) -> Self {
         PrimitiveInstance {
             kind,
             prim_data_handle,
             combined_local_clip_rect: LayoutRect::zero(),
             clipped_world_rect: None,
             #[cfg(debug_assertions)]
-            prepared_frame_id: FrameId(0),
+            prepared_frame_id: FrameId::invalid(),
             #[cfg(debug_assertions)]
             id: PrimitiveDebugId(NEXT_PRIM_ID.fetch_add(1, Ordering::Relaxed)),
             clip_task_id: None,
             gpu_location: GpuCacheHandle::new(),
             opacity: PrimitiveOpacity::translucent(),
             clip_chain_id,
             spatial_node_index,
             cluster_range: ClusterRange { start: 0, end: 0 },
@@ -1928,16 +1951,17 @@ impl PrimitiveStore {
 
         let prim_instance = &pic.prim_list.prim_instances[0];
 
         // For now, we only support opacity collapse on solid rects and images.
         // This covers the most common types of opacity filters that can be
         // handled by this optimization. In the future, we can easily extend
         // this to other primitives, such as text runs and gradients.
         match prim_instance.kind {
+            PrimitiveInstanceKind::Clear |
             PrimitiveInstanceKind::TextRun { .. } |
             PrimitiveInstanceKind::LineDecoration { .. } => {
                 // TODO: Once rectangles and/or images are ported
                 //       to use interned primitives, we will need
                 //       to handle opacity collapse here.
             }
             PrimitiveInstanceKind::Picture { pic_index } => {
                 let pic = &self.pictures[pic_index.0];
@@ -1957,18 +1981,17 @@ impl PrimitiveStore {
                             // If we find a single rect or image, we can use that
                             // as the primitive to collapse the opacity into.
                             BrushKind::Solid { .. } | BrushKind::Image { .. } => {
                                 return Some(prim_index)
                             }
                             BrushKind::Border { .. } |
                             BrushKind::YuvImage { .. } |
                             BrushKind::LinearGradient { .. } |
-                            BrushKind::RadialGradient { .. } |
-                            BrushKind::Clear => {}
+                            BrushKind::RadialGradient { .. } => {}
                         }
                     }
                 }
             }
         }
 
         None
     }
@@ -2000,17 +2023,16 @@ impl PrimitiveStore {
                     PrimitiveDetails::Brush(ref mut brush) => {
                         // By this point, we know we should only have found a primitive
                         // that supports opacity collapse.
                         match brush.kind {
                             BrushKind::Solid { ref mut opacity_binding, .. } |
                             BrushKind::Image { ref mut opacity_binding, .. } => {
                                 opacity_binding.push(binding);
                             }
-                            BrushKind::Clear { .. } |
                             BrushKind::YuvImage { .. } |
                             BrushKind::Border { .. } |
                             BrushKind::LinearGradient { .. } |
                             BrushKind::RadialGradient { .. } => {
                                 unreachable!("bug: invalid prim type for opacity collapse");
                             }
                         }
                     }
@@ -2069,17 +2091,18 @@ impl PrimitiveStore {
                             }
 
                             return false;
                         }
                     }
                 }
                 PrimitiveInstanceKind::TextRun { .. } |
                 PrimitiveInstanceKind::LineDecoration { .. } |
-                PrimitiveInstanceKind::LegacyPrimitive { .. } => {
+                PrimitiveInstanceKind::LegacyPrimitive { .. } |
+                PrimitiveInstanceKind::Clear => {
                     None
                 }
             }
         };
 
         let (is_passthrough, clip_node_collector) = match pic_info {
             Some((pic_context_for_children, mut pic_state_for_children, mut prim_list)) => {
                 // Mark whether this picture has a complex coordinate system.
@@ -2122,16 +2145,17 @@ impl PrimitiveStore {
         };
 
         let (prim_local_rect, prim_local_clip_rect) = match prim_instance.kind {
             PrimitiveInstanceKind::Picture { pic_index } => {
                 let pic = &self.pictures[pic_index.0];
                 (pic.local_rect, LayoutRect::max_rect())
             }
             PrimitiveInstanceKind::TextRun { .. } |
+            PrimitiveInstanceKind::Clear |
             PrimitiveInstanceKind::LineDecoration { .. } => {
                 let prim_data = &frame_state
                     .resources
                     .prim_data_store[prim_instance.prim_data_handle];
                 (prim_data.prim_rect, prim_data.clip_rect)
             }
             PrimitiveInstanceKind::LegacyPrimitive { prim_index } => {
                 let prim = &self.primitives[prim_index.0];
@@ -2293,16 +2317,17 @@ impl PrimitiveStore {
                     ]);
                     request.write_segment(
                         pic.local_rect,
                         [0.0; 4],
                     );
                 }
             }
             PrimitiveInstanceKind::TextRun { .. } |
+            PrimitiveInstanceKind::Clear |
             PrimitiveInstanceKind::LineDecoration { .. } => {
                 prim_instance.prepare_interned_prim_for_render(
                     prim_context,
                     pic_context,
                     pic_state,
                     frame_context,
                     frame_state,
                 );
@@ -2723,16 +2748,17 @@ impl PrimitiveInstance {
         frame_context: &FrameBuildingContext,
         frame_state: &mut FrameBuildingState,
         clip_node_collector: &Option<ClipNodeCollector>,
         primitives: &mut [Primitive],
     ) -> bool {
         let brush = match self.kind {
             PrimitiveInstanceKind::Picture { .. } |
             PrimitiveInstanceKind::TextRun { .. } |
+            PrimitiveInstanceKind::Clear |
             PrimitiveInstanceKind::LineDecoration { .. } => {
                 return false;
             }
             PrimitiveInstanceKind::LegacyPrimitive { prim_index } => {
                 let prim = &mut primitives[prim_index.0];
                 match prim.details {
                     PrimitiveDetails::Brush(ref mut brush) => brush,
                 }
@@ -2906,16 +2932,25 @@ impl PrimitiveInstance {
                     frame_state.resource_cache,
                     frame_state.gpu_cache,
                     frame_state.render_tasks,
                     frame_state.special_render_passes,
                 );
 
                 PrimitiveOpacity::translucent()
             }
+            (
+                PrimitiveInstanceKind::Clear,
+                PrimitiveTemplateKind::Clear
+            ) => {
+                // Nothing specific to prepare for clear rects, since the
+                // GPU cache is updated by the template earlier.
+
+                PrimitiveOpacity::translucent()
+            }
             _ => {
                 unreachable!();
             }
         };
     }
 
     fn prepare_prim_for_render_inner(
         &mut self,
@@ -3343,17 +3378,16 @@ impl PrimitiveInstance {
                         } else {
                             PrimitiveOpacity::translucent()
                         }
                     }
                     BrushKind::Solid { ref color, ref mut opacity_binding, .. } => {
                         opacity_binding.update(frame_context.scene_properties);
                         PrimitiveOpacity::from_alpha(opacity_binding.current * color.a)
                     }
-                    BrushKind::Clear => PrimitiveOpacity::translucent(),
                 }
             }
         };
 
         if is_tiled {
             // we already requested each tile's gpu data.
             return;
         }
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -78,17 +78,61 @@ impl DocumentView {
             self.pinch_zoom_factor
         )
     }
 }
 
 #[derive(Copy, Clone, Hash, PartialEq, PartialOrd, Debug, Eq, Ord)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct FrameId(pub u32);
+pub struct FrameId(usize);
+
+impl FrameId {
+    /// Returns an invalid sentinel FrameId, which will always compare less than
+    /// any valid FrameId.
+    pub fn invalid() -> Self {
+        FrameId(0)
+    }
+
+    /// Returns a FrameId corresponding to the first frame.
+    ///
+    /// Note that we use 0 as the internal id here because the current code
+    /// increments the frame id at the beginning of the frame, rather than
+    /// at the end, and we want the first frame to be 1. It would probably
+    /// be sensible to move the advance() call to after frame-building, and
+    /// then make this method return FrameId(1).
+    pub fn first() -> Self {
+        FrameId(0)
+    }
+
+    /// Returns the backing usize for this FrameId.
+    pub fn as_usize(&self) -> usize {
+        self.0
+    }
+
+    /// Advances this FrameId to the next frame.
+    fn advance(&mut self) {
+        self.0 += 1;
+    }
+}
+
+impl ::std::ops::Add<usize> for FrameId {
+    type Output = Self;
+    fn add(self, other: usize) -> FrameId {
+        FrameId(self.0 + other)
+    }
+}
+
+impl ::std::ops::Sub<usize> for FrameId {
+    type Output = Self;
+    fn sub(self, other: usize) -> FrameId {
+        assert!(self.0 >= other, "Underflow subtracting FrameIds");
+        FrameId(self.0 - other)
+    }
+}
 
 // A collection of resources that are shared by clips, primitives
 // between display lists.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct FrameResources {
     /// The store of currently active / available clip nodes. This is kept
     /// in sync with the clip interner in the scene builder for each document.
@@ -167,17 +211,17 @@ impl Document {
                 inner_rect: DeviceUintRect::new(DeviceUintPoint::zero(), window_size),
                 layer,
                 pan: DeviceIntPoint::zero(),
                 page_zoom_factor: 1.0,
                 pinch_zoom_factor: 1.0,
                 device_pixel_ratio: default_device_pixel_ratio,
             },
             clip_scroll_tree: ClipScrollTree::new(),
-            frame_id: FrameId(0),
+            frame_id: FrameId::first(),
             frame_builder: None,
             output_pipelines: FastHashSet::default(),
             hit_tester: None,
             dynamic_properties: SceneProperties::new(),
             frame_is_valid: false,
             hit_tester_is_valid: false,
             rendered_frame_is_valid: false,
             has_built_scene: false,
@@ -296,16 +340,19 @@ impl Document {
         &mut self,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         resource_profile: &mut ResourceProfileCounters,
     ) -> RenderedDocument {
         let accumulated_scale_factor = self.view.accumulated_scale_factor();
         let pan = self.view.pan.to_f32() / accumulated_scale_factor;
 
+        assert!(self.frame_id != FrameId::invalid(),
+                "First frame increment must happen before build_frame()");
+
         let frame = {
             let frame_builder = self.frame_builder.as_mut().unwrap();
             let frame = frame_builder.build(
                 resource_cache,
                 gpu_cache,
                 self.frame_id,
                 &mut self.clip_scroll_tree,
                 &self.scene.pipelines,
@@ -397,17 +444,17 @@ impl Document {
 
         self.frame_builder = Some(built_scene.frame_builder);
 
         let old_scrolling_states = self.clip_scroll_tree.drain();
         self.clip_scroll_tree = built_scene.clip_scroll_tree;
         self.clip_scroll_tree.finalize_and_apply_pending_scroll_offsets(old_scrolling_states);
 
         // Advance to the next frame.
-        self.frame_id.0 += 1;
+        self.frame_id.advance();
     }
 }
 
 struct DocumentOps {
     scroll: bool,
 }
 
 impl DocumentOps {
@@ -1477,17 +1524,17 @@ impl RenderBackend {
             let frame_resources = CaptureConfig::deserialize::<FrameResources, _>(root, &frame_resources_name)
                 .expect(&format!("Unable to open {}.ron", frame_resources_name));
 
             let mut doc = Document {
                 scene: scene.clone(),
                 removed_pipelines: Vec::new(),
                 view: view.clone(),
                 clip_scroll_tree: ClipScrollTree::new(),
-                frame_id: FrameId(0),
+                frame_id: FrameId::first(),
                 frame_builder: Some(FrameBuilder::empty()),
                 output_pipelines: FastHashSet::default(),
                 dynamic_properties: SceneProperties::new(),
                 hit_tester: None,
                 frame_is_valid: false,
                 hit_tester_is_valid: false,
                 rendered_frame_is_valid: false,
                 has_built_scene: false,
--- a/gfx/webrender/src/render_task.rs
+++ b/gfx/webrender/src/render_task.rs
@@ -1196,27 +1196,35 @@ impl RenderTaskCache {
                     size.height as u32,
                     image_format,
                     entry.is_opaque,
                     false,
                 );
 
                 // Allocate space in the texture cache, but don't supply
                 // and CPU-side data to be uploaded.
+                //
+                // Note that we currently use Eager eviction for cached render
+                // tasks, which means that any cached item not used in the last
+                // frame is discarded. There's room to be a lot smarter here,
+                // especially by considering the relative costs of re-rendering
+                // each type of item (box shadow blurs are an order of magnitude
+                // more expensive than borders, for example). Telemetry could
+                // inform our decisions here as well.
                 texture_cache.update(
                     &mut entry.handle,
                     descriptor,
                     TextureFilter::Linear,
                     None,
                     entry.user_data.unwrap_or([0.0; 3]),
                     None,
                     gpu_cache,
                     None,
                     render_task.uv_rect_kind(),
-                    Eviction::Auto,
+                    Eviction::Eager,
                 );
 
                 // 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);
@@ -1243,17 +1251,17 @@ impl RenderTaskCache {
          where F: FnMut(&mut RenderTaskTree) -> Result<RenderTaskId, ()> {
         // Get the texture cache handle for this cache key,
         // or create one.
         let cache_entries = &mut self.cache_entries;
         let entry_handle = self.map
                                .entry(key)
                                .or_insert_with(|| {
                                     let entry = RenderTaskCacheEntry {
-                                        handle: TextureCacheHandle::new(),
+                                        handle: TextureCacheHandle::invalid(),
                                         pending_render_task_id: None,
                                         user_data,
                                         is_opaque,
                                     };
                                     cache_entries.insert(entry)
                                 });
         let cache_entry = cache_entries.get_mut(entry_handle);
 
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -30,17 +30,17 @@ use api::{MemoryReport, VoidPtrToSizeFn}
 use api::{RenderApiSender, RenderNotifier, TexelRect, TextureTarget};
 use api::{channel};
 use api::DebugCommand;
 use api::channel::PayloadReceiverHelperMethods;
 use batch::{BatchKind, BatchTextures, BrushBatchKind};
 #[cfg(any(feature = "capture", feature = "replay"))]
 use capture::{CaptureConfig, ExternalCaptureImage, PlainExternalImage};
 use debug_colors;
-use device::{DepthFunction, Device, FrameId, Program, UploadMethod, Texture, PBO};
+use device::{DepthFunction, Device, GpuFrameId, Program, UploadMethod, Texture, PBO};
 use device::{DrawTarget, ExternalTexture, FBOId, ReadTarget, TextureSlot};
 use device::{ShaderError, TextureFilter,
              VertexUsageHint, VAO, VBO, CustomVAO};
 use device::{ProgramCache, ReadPixelsFormat};
 #[cfg(feature = "debug_renderer")]
 use euclid::rect;
 use euclid::Transform3D;
 use frame_builder::{ChasePrimitive, FrameBuilderConfig};
@@ -57,17 +57,17 @@ use internal_types::{CacheTextureId, Deb
 use internal_types::{LayerIndex, TextureUpdateList, TextureUpdateOp, TextureUpdateSource};
 use internal_types::{RenderTargetInfo, SavedTargetIndex};
 use prim_store::DeferredResolve;
 use profiler::{BackendProfileCounters, FrameProfileCounters,
                GpuProfileTag, RendererProfileCounters, RendererProfileTimers};
 use device::query::GpuProfiler;
 use rayon::{ThreadPool, ThreadPoolBuilder};
 use record::ApiRecordingReceiver;
-use render_backend::RenderBackend;
+use render_backend::{FrameId, RenderBackend};
 use scene_builder::{SceneBuilder, LowPrioritySceneBuilder};
 use shade::{Shaders, WrShaders};
 use smallvec::SmallVec;
 use render_task::{RenderTask, RenderTaskKind, RenderTaskTree};
 use resource_cache::ResourceCache;
 use util::drain_filter;
 
 use std;
@@ -700,45 +700,45 @@ impl From<TextureTarget> for ImageBuffer
 #[derive(Debug, Copy, Clone)]
 pub enum RendererKind {
     Native,
     OSMesa,
 }
 
 #[derive(Debug)]
 pub struct GpuProfile {
-    pub frame_id: FrameId,
+    pub frame_id: GpuFrameId,
     pub paint_time_ns: u64,
 }
 
 impl GpuProfile {
     #[cfg(feature = "debug_renderer")]
-    fn new<T>(frame_id: FrameId, timers: &[GpuTimer<T>]) -> GpuProfile {
+    fn new<T>(frame_id: GpuFrameId, timers: &[GpuTimer<T>]) -> GpuProfile {
         let mut paint_time_ns = 0;
         for timer in timers {
             paint_time_ns += timer.time_ns;
         }
         GpuProfile {
             frame_id,
             paint_time_ns,
         }
     }
 }
 
 #[derive(Debug)]
 pub struct CpuProfile {
-    pub frame_id: FrameId,
+    pub frame_id: GpuFrameId,
     pub backend_time_ns: u64,
     pub composite_time_ns: u64,
     pub draw_calls: usize,
 }
 
 impl CpuProfile {
     fn new(
-        frame_id: FrameId,
+        frame_id: GpuFrameId,
         backend_time_ns: u64,
         composite_time_ns: u64,
         draw_calls: usize,
     ) -> CpuProfile {
         CpuProfile {
             frame_id,
             backend_time_ns,
             composite_time_ns,
@@ -848,17 +848,17 @@ impl TextureResolver {
     }
 
     fn begin_frame(&mut self) {
         assert!(self.prev_pass_color.is_none());
         assert!(self.prev_pass_alpha.is_none());
         assert!(self.saved_targets.is_empty());
     }
 
-    fn end_frame(&mut self, device: &mut Device, frame_id: FrameId) {
+    fn end_frame(&mut self, device: &mut Device, frame_id: GpuFrameId) {
         // return the cached targets to the pool
         self.end_pass(device, None, None);
         // return the saved targets as well
         while let Some(target) = self.saved_targets.pop() {
             self.return_to_pool(device, target);
         }
 
         // GC the render target pool.
@@ -1436,17 +1436,17 @@ impl VertexDataTexture {
         device.delete_pbo(self.pbo);
         if let Some(t) = self.texture.take() {
             device.delete_texture(t);
         }
     }
 }
 
 struct FrameOutput {
-    last_access: FrameId,
+    last_access: GpuFrameId,
     fbo_id: FBOId,
 }
 
 #[derive(PartialEq)]
 struct TargetSelector {
     size: DeviceUintSize,
     num_layers: usize,
     format: ImageFormat,
@@ -2017,17 +2017,17 @@ impl Renderer {
             output_image_handler: None,
             size_of_op: options.size_of_op,
             output_targets: FastHashMap::default(),
             cpu_profiles: VecDeque::new(),
             gpu_profiles: VecDeque::new(),
             gpu_cache_texture,
             #[cfg(feature = "debug_renderer")]
             gpu_cache_debug_chunks: Vec::new(),
-            gpu_cache_frame_id: FrameId::new(0),
+            gpu_cache_frame_id: FrameId::invalid(),
             gpu_cache_overflow: false,
             texture_cache_upload_pbo,
             texture_resolver,
             renderer_errors: Vec::new(),
             #[cfg(feature = "capture")]
             read_fbo,
             #[cfg(feature = "replay")]
             owned_external_images: FastHashMap::default(),
@@ -2731,17 +2731,17 @@ impl Renderer {
     fn update_gpu_cache(&mut self) {
         let _gm = self.gpu_profile.start_marker("gpu cache update");
 
         // For an artificial stress test of GPU cache resizing,
         // always pass an extra update list with at least one block in it.
         let gpu_cache_height = self.gpu_cache_texture.get_height();
         if gpu_cache_height != 0 && GPU_CACHE_RESIZE_TEST {
             self.pending_gpu_cache_updates.push(GpuCacheUpdateList {
-                frame_id: FrameId::new(0),
+                frame_id: FrameId::invalid(),
                 height: gpu_cache_height,
                 blocks: vec![[1f32; 4].into()],
                 updates: Vec::new(),
                 debug_chunks: Vec::new(),
             });
         }
 
         let (updated_blocks, max_requested_height) = self
@@ -3104,17 +3104,17 @@ impl Renderer {
         &mut self,
         draw_target: DrawTarget,
         target: &ColorRenderTarget,
         framebuffer_target_rect: DeviceUintRect,
         depth_is_ready: bool,
         clear_color: Option<[f32; 4]>,
         render_tasks: &RenderTaskTree,
         projection: &Transform3D<f32>,
-        frame_id: FrameId,
+        frame_id: GpuFrameId,
         stats: &mut RendererStats,
     ) {
         self.profile_counters.color_targets.inc();
         let _gm = self.gpu_profile.start_marker("color target");
 
         // sanity check for the depth buffer
         if let DrawTarget::Texture { texture, .. } = draw_target {
             assert!(texture.supports_depth() >= target.needs_depth());
@@ -3740,17 +3740,17 @@ impl Renderer {
             return None;
         }
 
         let handler = self.external_image_handler
             .as_mut()
             .expect("Found external image, but no handler set!");
 
         let mut list = GpuCacheUpdateList {
-            frame_id: FrameId::new(0),
+            frame_id: FrameId::invalid(),
             height: self.gpu_cache_texture.get_height(),
             blocks: Vec::new(),
             updates: Vec::new(),
             debug_chunks: Vec::new(),
         };
 
         for deferred_resolve in deferred_resolves {
             self.gpu_profile.place_marker("deferred resolve");
@@ -3940,17 +3940,17 @@ impl Renderer {
         debug_assert!(self.texture_resolver.prev_pass_color.is_none());
     }
 
     fn draw_tile_frame(
         &mut self,
         frame: &mut Frame,
         framebuffer_size: Option<DeviceUintSize>,
         framebuffer_depth_is_ready: bool,
-        frame_id: FrameId,
+        frame_id: GpuFrameId,
         stats: &mut RendererStats,
     ) {
         let _gm = self.gpu_profile.start_marker("tile frame draw");
 
         if frame.passes.is_empty() {
             frame.has_been_rendered = true;
             return;
         }
--- a/gfx/webrender/src/resource_cache.rs
+++ b/gfx/webrender/src/resource_cache.rs
@@ -439,17 +439,17 @@ impl ResourceCache {
         ResourceCache {
             cached_glyphs: GlyphCache::new(),
             cached_images: ResourceClassCache::new(),
             cached_render_tasks: RenderTaskCache::new(),
             resources: Resources::default(),
             cached_glyph_dimensions: FastHashMap::default(),
             texture_cache,
             state: State::Idle,
-            current_frame_id: FrameId(0),
+            current_frame_id: FrameId::invalid(),
             pending_image_requests: FastHashSet::default(),
             glyph_rasterizer,
             blob_image_handler,
             rasterized_blob_images: FastHashMap::default(),
             blob_image_templates: FastHashMap::default(),
             missing_blob_images: Vec::new(),
             blob_image_rasterizer: None,
         }
@@ -919,17 +919,17 @@ impl ResourceCache {
                 // a second entry. In such cases we need to move the old entry
                 // out first, replacing it with a dummy entry, and then creating
                 // the tiled/multi-entry variant.
                 let entry = e.into_mut();
                 if !request.is_untiled_auto() {
                     let untiled_entry = match entry {
                         &mut ImageResult::UntiledAuto(ref mut entry) => {
                             Some(mem::replace(entry, CachedImageInfo {
-                                texture_cache_handle: TextureCacheHandle::new(),
+                                texture_cache_handle: TextureCacheHandle::invalid(),
                                 dirty_rect: None,
                                 manual_eviction: false,
                             }))
                         }
                         _ => None
                     };
 
                     if let Some(untiled_entry) = untiled_entry {
@@ -942,34 +942,34 @@ impl ResourceCache {
                         *entry = ImageResult::Multi(entries);
                     }
                 }
                 entry
             }
             Vacant(entry) => {
                 entry.insert(if request.is_untiled_auto() {
                     ImageResult::UntiledAuto(CachedImageInfo {
-                        texture_cache_handle: TextureCacheHandle::new(),
+                        texture_cache_handle: TextureCacheHandle::invalid(),
                         dirty_rect: Some(template.descriptor.full_rect()),
                         manual_eviction: false,
                     })
                 } else {
                     ImageResult::Multi(ResourceClassCache::new())
                 })
             }
         };
 
         // If this image exists in the texture cache, *and* the dirty rect
         // in the cache is empty, then it is valid to use as-is.
         let entry = match *storage {
             ImageResult::UntiledAuto(ref mut entry) => entry,
             ImageResult::Multi(ref mut entries) => {
                 entries.entry(request.into())
                     .or_insert(CachedImageInfo {
-                        texture_cache_handle: TextureCacheHandle::new(),
+                        texture_cache_handle: TextureCacheHandle::invalid(),
                         dirty_rect: Some(template.descriptor.full_rect()),
                         manual_eviction: false,
                     })
             },
             ImageResult::Err(_) => panic!("Errors should already have been handled"),
         };
 
         let needs_upload = self.texture_cache.request(&entry.texture_cache_handle, gpu_cache);
@@ -2027,17 +2027,17 @@ impl ResourceCache {
                 self.current_frame_id = cached.current_frame_id;
                 self.cached_glyphs = cached.glyphs;
                 self.cached_glyph_dimensions = cached.glyph_dimensions;
                 self.cached_images = cached.images;
                 self.cached_render_tasks = cached.render_tasks;
                 self.texture_cache = cached.textures;
             }
             None => {
-                self.current_frame_id = FrameId(0);
+                self.current_frame_id = FrameId::invalid();
                 self.cached_glyphs.clear();
                 self.cached_glyph_dimensions.clear();
                 self.cached_images.clear();
                 self.cached_render_tasks.clear();
                 let max_texture_size = self.texture_cache.max_texture_size();
                 self.texture_cache = TextureCache::new(max_texture_size);
             }
         }
--- a/gfx/webrender/src/texture_cache.rs
+++ b/gfx/webrender/src/texture_cache.rs
@@ -1,16 +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/. */
 
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize};
 use api::{ExternalImageType, ImageData, ImageFormat};
 use api::ImageDescriptor;
-use device::TextureFilter;
+use device::{TextureFilter, total_gpu_bytes_allocated};
 use freelist::{FreeList, FreeListHandle, UpsertResult, WeakFreeListHandle};
 use gpu_cache::{GpuCache, GpuCacheHandle};
 use gpu_types::{ImageSource, UvRectKind};
 use internal_types::{CacheTextureId, LayerIndex, TextureUpdateList, TextureUpdateSource};
 use internal_types::{RenderTargetInfo, TextureSource, TextureUpdate, TextureUpdateOp};
 use profiler::{ResourceProfileCounter, TextureCacheProfileCounters};
 use render_backend::FrameId;
 use resource_cache::CacheItem;
@@ -34,16 +34,23 @@ enum EntryKind {
         origin: DeviceUintPoint,
         // The layer index of the texture array.
         layer_index: u16,
         // The region that this entry belongs to in the layer.
         region_index: u16,
     },
 }
 
+impl EntryKind {
+    /// Returns true if this corresponds to a standalone cache entry.
+    fn is_standalone(&self) -> bool {
+        matches!(*self, EntryKind::Standalone)
+    }
+}
+
 #[derive(Debug)]
 pub enum CacheEntryMarker {}
 
 // Stores information related to a single entry in the texture
 // cache. This is stored for each item whether it's in the shared
 // cache or a standalone texture.
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
@@ -125,43 +132,40 @@ impl CacheEntry {
 
     fn evict(&self) {
         if let Some(eviction_notice) = self.eviction_notice.as_ref() {
             eviction_notice.notify();
         }
     }
 }
 
-type WeakCacheEntryHandle = WeakFreeListHandle<CacheEntryMarker>;
 
-// A texture cache handle is a weak reference to a cache entry.
-// If the handle has not been inserted into the cache yet, the
-// value will be None. Even when the value is Some(), the location
-// may not actually be valid if it has been evicted by the cache.
-// In this case, the cache handle needs to re-upload this item
-// to the texture cache (see request() below).
-#[derive(Debug)]
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct TextureCacheHandle {
-    entry: Option<WeakCacheEntryHandle>,
-}
+/// A texture cache handle is a weak reference to a cache entry.
+///
+/// If the handle has not been inserted into the cache yet, or if the entry was
+/// previously inserted and then evicted, lookup of the handle will fail, and
+/// the cache handle needs to re-upload this item to the texture cache (see
+/// request() below).
+pub type TextureCacheHandle = WeakFreeListHandle<CacheEntryMarker>;
 
-impl TextureCacheHandle {
-    pub fn new() -> Self {
-        TextureCacheHandle { entry: None }
-    }
-}
-
+/// Describes the eviction policy for a given entry in the texture cache.
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum Eviction {
+    /// The entry will be evicted under the normal rules (which differ between
+    /// standalone and shared entries).
     Auto,
+    /// The entry will not be evicted until the policy is explicitly set to a
+    /// different value.
     Manual,
+    /// The entry will be evicted if it was not used in the last frame.
+    ///
+    /// FIXME(bholley): Currently this only applies to the standalone case.
+    Eager,
 }
 
 // An eviction notice is a shared condition useful for detecting
 // when a TextureCacheHandle gets evicted from the TextureCache.
 // It is optionally installed to the TextureCache when an update()
 // is scheduled. A single notice may be shared among any number of
 // TextureCacheHandle updates. The notice may then be subsequently
 // checked to see if any of the updates using it have been evicted.
@@ -266,17 +270,17 @@ impl TextureCache {
             array_rgba8_nearest: TextureArray::new(
                 ImageFormat::BGRA8,
                 TextureFilter::Nearest,
                 1024,
                 1,
             ),
             next_id: CacheTextureId(1),
             pending_updates: TextureUpdateList::new(),
-            frame_id: FrameId(0),
+            frame_id: FrameId::invalid(),
             entries: FreeList::new(),
             standalone_entry_handles: Vec::new(),
             shared_entry_handles: Vec::new(),
         }
     }
 
     pub fn clear(&mut self) {
         let standalone_entry_handles = mem::replace(
@@ -353,41 +357,33 @@ impl TextureCache {
     // be used on a frame *must* have request() called on their
     // handle, to update the last used timestamp and ensure
     // that resources are not flushed from the cache too early.
     //
     // Returns true if the image needs to be uploaded to the
     // texture cache (either never uploaded, or has been
     // evicted on a previous frame).
     pub fn request(&mut self, handle: &TextureCacheHandle, gpu_cache: &mut GpuCache) -> bool {
-        match handle.entry {
-            Some(ref handle) => {
-                match self.entries.get_opt_mut(handle) {
-                    // If an image is requested that is already in the cache,
-                    // refresh the GPU cache data associated with this item.
-                    Some(entry) => {
-                        entry.last_access = self.frame_id;
-                        entry.update_gpu_cache(gpu_cache);
-                        false
-                    }
-                    None => true,
-                }
+        match self.entries.get_opt_mut(handle) {
+            // If an image is requested that is already in the cache,
+            // refresh the GPU cache data associated with this item.
+            Some(entry) => {
+                entry.last_access = self.frame_id;
+                entry.update_gpu_cache(gpu_cache);
+                false
             }
             None => true,
         }
     }
 
     // Returns true if the image needs to be uploaded to the
     // texture cache (either never uploaded, or has been
     // evicted on a previous frame).
     pub fn needs_upload(&self, handle: &TextureCacheHandle) -> bool {
-        match handle.entry {
-            Some(ref handle) => self.entries.get_opt(handle).is_none(),
-            None => true,
-        }
+        self.entries.get_opt(handle).is_none()
     }
 
     pub fn max_texture_size(&self) -> u32 {
         self.max_texture_size
     }
 
     pub fn pending_updates(&mut self) -> TextureUpdateList {
         mem::replace(&mut self.pending_updates, TextureUpdateList::new())
@@ -408,30 +404,22 @@ impl TextureCache {
         eviction: Eviction,
     ) {
         // 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 {
-            Some(ref handle) => {
-                match self.entries.get_opt(handle) {
-                    Some(entry) => {
-                        entry.size != descriptor.size || entry.format != descriptor.format
-                    }
-                    None => {
-                        // Was previously allocated but has been evicted.
-                        true
-                    }
-                }
+        let realloc = match self.entries.get_opt(handle) {
+            Some(entry) => {
+                entry.size != descriptor.size || entry.format != descriptor.format
             }
             None => {
-                // This handle has not been allocated yet.
+                // Not allocated, or was previously allocated but has been evicted.
                 true
             }
         };
 
         if realloc {
             self.allocate(
                 handle,
                 descriptor,
@@ -439,18 +427,17 @@ impl TextureCache {
                 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())
+        let entry = self.entries.get_opt_mut(handle)
             .expect("BUG: handle must be valid now");
 
         // Install the new eviction notice for this update, if applicable.
         entry.eviction_notice = eviction_notice.cloned();
 
         // Invalidate the contents of the resource rect in the GPU cache.
         // This ensures that the update_gpu_cache below will add
         // the new information to the GPU cache.
@@ -509,67 +496,55 @@ impl TextureCache {
         };
 
         &mut texture_array.regions[region_index as usize]
     }
 
     // Check if a given texture handle has a valid allocation
     // in the texture cache.
     pub fn is_allocated(&self, handle: &TextureCacheHandle) -> bool {
-        handle.entry.as_ref().map_or(false, |handle| {
-            self.entries.get_opt(handle).is_some()
-        })
+        self.entries.get_opt(handle).is_some()
     }
 
     // Retrieve the details of an item in the cache. This is used
     // during batch creation to provide the resource rect address
     // to the shaders and texture ID to the batching logic.
     // This function will assert in debug modes if the caller
     // tries to get a handle that was not requested this frame.
     pub fn get(&self, handle: &TextureCacheHandle) -> CacheItem {
-        match handle.entry {
-            Some(ref handle) => {
-                let entry = self.entries
-                    .get_opt(handle)
-                    .expect("BUG: was dropped from cache or not updated!");
-                debug_assert_eq!(entry.last_access, self.frame_id);
-                let (layer_index, origin) = match entry.kind {
-                    EntryKind::Standalone { .. } => {
-                        (0, DeviceUintPoint::zero())
-                    }
-                    EntryKind::Cache {
-                        layer_index,
-                        origin,
-                        ..
-                    } => (layer_index, origin),
-                };
-                CacheItem {
-                    uv_rect_handle: entry.uv_rect_handle,
-                    texture_id: TextureSource::TextureCache(entry.texture_id),
-                    uv_rect: DeviceUintRect::new(origin, entry.size),
-                    texture_layer: layer_index as i32,
-                }
+        let entry = self.entries
+            .get_opt(handle)
+            .expect("BUG: was dropped from cache or not updated!");
+        debug_assert_eq!(entry.last_access, self.frame_id);
+        let (layer_index, origin) = match entry.kind {
+            EntryKind::Standalone { .. } => {
+                (0, DeviceUintPoint::zero())
             }
-            None => panic!("BUG: handle not requested earlier in frame"),
+            EntryKind::Cache {
+                layer_index,
+                origin,
+                ..
+            } => (layer_index, origin),
+        };
+        CacheItem {
+            uv_rect_handle: entry.uv_rect_handle,
+            texture_id: TextureSource::TextureCache(entry.texture_id),
+            uv_rect: DeviceUintRect::new(origin, entry.size),
+            texture_layer: layer_index as i32,
         }
     }
 
     /// A more detailed version of get(). This allows access to the actual
     /// device rect of the cache allocation.
     ///
     /// Returns a tuple identifying the texture, the layer, and the region.
     pub fn get_cache_location(
         &self,
         handle: &TextureCacheHandle,
     ) -> (CacheTextureId, LayerIndex, DeviceUintRect) {
-        let handle = handle
-            .entry
-            .as_ref()
-            .expect("BUG: handle not requested earlier in frame");
-
         let entry = self.entries
             .get_opt(handle)
             .expect("BUG: was dropped from cache or not updated!");
         debug_assert_eq!(entry.last_access, self.frame_id);
         let (layer_index, origin) = match entry.kind {
             EntryKind::Standalone { .. } => {
                 (0, DeviceUintPoint::zero())
             }
@@ -580,66 +555,81 @@ impl TextureCache {
             } => (layer_index, origin),
         };
         (entry.texture_id,
          layer_index as usize,
          DeviceUintRect::new(origin, entry.size))
     }
 
     pub fn mark_unused(&mut self, handle: &TextureCacheHandle) {
-        if let Some(ref handle) = handle.entry {
-            if let Some(entry) = self.entries.get_opt_mut(handle) {
-                // Set a very low last accessed frame to make it very likely that this entry
-                // will get cleaned up next time we try to expire entries.
-                entry.last_access = FrameId(0);
-                entry.eviction = Eviction::Auto;
-            }
+        if let Some(entry) = self.entries.get_opt_mut(handle) {
+            // Set last accessed frame to invalid to ensure it gets cleaned up
+            // next time we expire entries.
+            entry.last_access = FrameId::invalid();
+            entry.eviction = Eviction::Auto;
         }
     }
 
-    // Expire old standalone textures.
+    /// Expires old standalone textures. Called at the end of every frame.
+    ///
+    /// Most of the time, standalone cache entries correspond to images whose
+    /// width or height is greater than the region size in the shared cache, i.e.
+    /// 512 pixels. Cached render tasks also frequently get standalone entries,
+    /// but those use the Eviction::Eager policy (for now). So the tradeoff here
+    /// is largely around reducing texture upload jank while keeping memory usage
+    /// at an acceptable level.
+    ///
+    /// Our eviction scheme is based on the age of the entry, i.e. the difference
+    /// between the current frame index and that of the last frame in
+    /// which the entry was used. It does not directly consider the size of the
+    /// entry, but does consider overall memory usage by WebRender, by making
+    /// eviction increasingly aggressive as overall memory usage increases.
     fn expire_old_standalone_entries(&mut self) {
-        let mut eviction_candidates = Vec::new();
-        let mut retained_entries = Vec::new();
+        // These parameters are based on some discussion and local tuning, but
+        // no hard measurement. There may be room for improvement.
+        //
+        // See discussion at https://mozilla.logbot.info/gfx/20181030#c15541654
+        const MAX_FRAME_AGE_WITHOUT_PRESSURE: f64 = 75.0;
+        const MAX_MEMORY_PRESSURE_BYTES: f64 = (500 * 1024 * 1024) as f64;
+
+        // Compute the memory pressure factor in the range of [0, 1.0].
+        let pressure_factor =
+            (total_gpu_bytes_allocated() as f64 / MAX_MEMORY_PRESSURE_BYTES as f64).min(1.0);
 
-        // Build a list of eviction candidates (which are
-        // anything not used this frame).
-        for handle in self.standalone_entry_handles.drain(..) {
-            let entry = self.entries.get(&handle);
-            if entry.eviction == Eviction::Manual || entry.last_access == self.frame_id {
-                retained_entries.push(handle);
-            } else {
-                eviction_candidates.push(handle);
+        // Use the pressure factor to compute the maximum number of frames that
+        // a standalone texture can go unused before being evicted.
+        let max_frame_age_raw =
+            ((1.0 - pressure_factor) * MAX_FRAME_AGE_WITHOUT_PRESSURE) as usize;
+
+        // We clamp max_frame_age to frame_id - 1 so that entries with FrameId(0)
+        // always get evicted, even early in the lifetime of the Renderer.
+        let max_frame_age = max_frame_age_raw.min(self.frame_id.as_usize() - 1);
+
+        // Compute the oldest FrameId for which we will not evict.
+        let frame_id_threshold = self.frame_id - max_frame_age;
+
+        // Iterate over the entries in reverse order, evicting the ones older than
+        // the frame age threshold. Reverse order avoids iterator invalidation when
+        // removing entries.
+        for i in (0..self.standalone_entry_handles.len()).rev() {
+            let evict = {
+                let entry = self.entries.get(&self.standalone_entry_handles[i]);
+                match entry.eviction {
+                    Eviction::Manual => false,
+                    Eviction::Auto => entry.last_access < frame_id_threshold,
+                    Eviction::Eager => entry.last_access < self.frame_id,
+                }
+            };
+            if evict {
+                let handle = self.standalone_entry_handles.swap_remove(i);
+                let entry = self.entries.free(handle);
+                entry.evict();
+                self.free(entry);
             }
         }
-
-        // Sort by access time so we remove the oldest ones first.
-        eviction_candidates.sort_by_key(|handle| {
-            let entry = self.entries.get(handle);
-            entry.last_access
-        });
-
-        // We only allow an arbitrary number of unused
-        // standalone textures to remain in GPU memory.
-        // TODO(gw): We should make this a better heuristic,
-        //           for example based on total memory size.
-        if eviction_candidates.len() > 32 {
-            let entries_to_keep = eviction_candidates.split_off(32);
-            retained_entries.extend(entries_to_keep);
-        }
-
-        // Free the selected items
-        for handle in eviction_candidates {
-            let entry = self.entries.free(handle);
-            entry.evict();
-            self.free(entry);
-        }
-
-        // Keep a record of the remaining handles for next frame.
-        self.standalone_entry_handles = retained_entries;
     }
 
     // Expire old shared items. Pass in the allocation size
     // that is being requested, so we know when we've evicted
     // enough items to guarantee we can fit this allocation in
     // the cache.
     fn expire_old_shared_entries(&mut self, required_alloc: &ImageDescriptor) {
         let mut eviction_candidates = Vec::new();
@@ -695,18 +685,17 @@ impl TextureCache {
         // Keep a record of the remaining handles for next eviction cycle.
         self.shared_entry_handles = retained_entries;
     }
 
     // Free a cache entry from the standalone list or shared cache.
     fn free(&mut self, entry: CacheEntry) -> Option<&TextureRegion> {
         match entry.kind {
             EntryKind::Standalone { .. } => {
-                // This is a standalone texture allocation. Just push it back onto the free
-                // list.
+                // This is a standalone texture allocation. Free it directly.
                 self.pending_updates.push(TextureUpdate {
                     id: entry.texture_id,
                     op: TextureUpdateOp::Free,
                 });
                 None
             }
             EntryKind::Cache {
                 origin,
@@ -756,23 +745,18 @@ impl TextureCache {
 
             let update_op = TextureUpdate {
                 id: texture_id,
                 op: TextureUpdateOp::Create {
                     width: texture_array.dimensions,
                     height: texture_array.dimensions,
                     format: descriptor.format,
                     filter: texture_array.filter,
-                    // TODO(gw): Creating a render target here is only used
-                    //           for the texture cache debugger display. In
-                    //           the future, we should change the debug
-                    //           display to use a shader that blits the
-                    //           texture, and then we can remove this
-                    //           memory allocation (same for the other
-                    //           standalone texture below).
+                    // This needs to be a render target because some render
+                    // tasks get rendered into the texture cache.
                     render_target: Some(RenderTargetInfo { has_depth: false }),
                     layer_count: texture_array.layer_count as i32,
                 },
             };
             self.pending_updates.push(update_op);
 
             texture_array.texture_id = Some(texture_id);
         }
@@ -829,17 +813,17 @@ impl TextureCache {
     ) {
         assert!(descriptor.size.width > 0 && descriptor.size.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,
         );
-        let mut allocated_in_shared_cache = true;
+        let mut allocated_standalone = false;
         let mut new_cache_entry = None;
         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,
@@ -888,62 +872,52 @@ impl TextureCache {
                 descriptor.size,
                 descriptor.format,
                 filter,
                 user_data,
                 frame_id,
                 uv_rect_kind,
             ));
 
-            allocated_in_shared_cache = false;
+            allocated_standalone = true;
         }
-
         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
-        // points to the correct location.
-        let new_entry_handle = match handle.entry {
-            Some(ref existing_entry) => {
-                // If the handle already exists, there's two possibilities:
-                // 1) It points to a valid entry in the freelist.
-                // 2) It points to a stale entry in the freelist (i.e. has been evicted).
-                //
-                // For (1) we want to replace the cache entry with our
-                // newly updated location. We also need to ensure that
-                // the storage (region or standalone) associated with the
-                // previous entry here gets freed.
-                //
-                // For (2) we need to add the data to a new location
-                // in the freelist.
-                //
-                // This is managed with a database style upsert operation.
-                match self.entries.upsert(existing_entry, new_cache_entry) {
-                    UpsertResult::Updated(old_entry) => {
-                        self.free(old_entry);
-                        None
-                    }
-                    UpsertResult::Inserted(new_handle) => Some(new_handle),
+        // If the handle points to a valid cache entry, we want to replace the
+        // cache entry with our newly updated location. We also need to ensure
+        // that the storage (region or standalone) associated with the previous
+        // entry here gets freed.
+        //
+        // If the handle is invalid, we need to insert the data, and append the
+        // result to the corresponding vector.
+        //
+        // This is managed with a database style upsert operation.
+        match self.entries.upsert(handle, new_cache_entry) {
+            UpsertResult::Updated(old_entry) => {
+                if allocated_standalone != old_entry.kind.is_standalone() {
+                    // Handle the rare case than an update moves an entry from
+                    // shared to standalone or vice versa. This involves a linear
+                    // search, but should be rare enough not to matter.
+                    let (from, to) = if allocated_standalone {
+                        (&mut self.shared_entry_handles, &mut self.standalone_entry_handles)
+                    } else {
+                        (&mut self.standalone_entry_handles, &mut self.shared_entry_handles)
+                    };
+                    let idx = from.iter().position(|h| h.weak() == *handle).unwrap();
+                    to.push(from.remove(idx));
                 }
-            }
-            None => {
-                // This handle has never been allocated, so just
-                // insert a new cache entry.
-                Some(self.entries.insert(new_cache_entry))
+                self.free(old_entry);
             }
-        };
-
-        // If the cache entry is new, update it in the cache handle.
-        if let Some(new_entry_handle) = new_entry_handle {
-            handle.entry = Some(new_entry_handle.weak());
-            // Store the strong handle in the list that we scan for
-            // cache evictions.
-            if allocated_in_shared_cache {
-                self.shared_entry_handles.push(new_entry_handle);
-            } else {
-                self.standalone_entry_handles.push(new_entry_handle);
+            UpsertResult::Inserted(new_handle) => {
+                *handle = new_handle.weak();
+                if allocated_standalone {
+                    self.standalone_entry_handles.push(new_handle);
+                } else {
+                    self.shared_entry_handles.push(new_handle);
+                }
             }
         }
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Copy, Clone, PartialEq)]
--- a/gfx/webrender/src/tiling.rs
+++ b/gfx/webrender/src/tiling.rs
@@ -3,29 +3,29 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ColorF, BorderStyle, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixelScale};
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentLayer, FilterOp, ImageFormat};
 use api::{MixBlendMode, PipelineId, DeviceRect, LayoutSize};
 use batch::{AlphaBatchBuilder, AlphaBatchContainer, ClipBatcher, resolve_image};
 use clip::ClipStore;
 use clip_scroll_tree::{ClipScrollTree};
-use device::{FrameId, Texture};
+use device::{Texture};
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use gpu_cache::{GpuCache};
 use gpu_types::{BorderInstance, BlurDirection, BlurInstance, PrimitiveHeaders, ScalingInstance};
 use gpu_types::{TransformData, TransformPalette, ZBufferIdGenerator};
 use internal_types::{CacheTextureId, FastHashMap, SavedTargetIndex, TextureSource};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
 use picture::SurfaceInfo;
 use prim_store::{PrimitiveStore, DeferredResolve};
 use profiler::FrameProfileCounters;
-use render_backend::FrameResources;
+use render_backend::{FrameId, FrameResources};
 use render_task::{BlitSource, RenderTaskAddress, RenderTaskId, RenderTaskKind};
 use render_task::{BlurTask, ClearMode, GlyphTask, RenderTaskLocation, RenderTaskTree, ScalingTask};
 use resource_cache::ResourceCache;
 use std::{cmp, usize, f32, i32, mem};
 use texture_allocator::GuillotineAllocator;
 #[cfg(feature = "pathfinder")]
 use webrender_api::{DevicePixel, FontRenderMode};
 
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -787,16 +787,20 @@ pub struct MemoryReport {
     //
     // GPU memory.
     //
     pub gpu_cache_textures: usize,
     pub vertex_data_textures: usize,
     pub render_target_textures: usize,
     pub texture_cache_textures: usize,
     pub depth_target_textures: usize,
+    //
+    // GPU memory total (tracked separately, should equal the sum of the above).
+    //
+    pub total_gpu_bytes_allocated: usize,
 }
 
 impl ::std::ops::AddAssign for MemoryReport {
     fn add_assign(&mut self, other: MemoryReport) {
         self.primitive_stores += other.primitive_stores;
         self.clip_stores += other.clip_stores;
         self.gpu_cache_metadata += other.gpu_cache_metadata;
         self.gpu_cache_cpu_mirror += other.gpu_cache_cpu_mirror;
@@ -805,16 +809,20 @@ impl ::std::ops::AddAssign for MemoryRep
         self.fonts += other.fonts;
         self.images += other.images;
         self.rasterized_blobs += other.rasterized_blobs;
         self.gpu_cache_textures += other.gpu_cache_textures;
         self.vertex_data_textures += other.vertex_data_textures;
         self.render_target_textures += other.render_target_textures;
         self.texture_cache_textures += other.texture_cache_textures;
         self.depth_target_textures += other.depth_target_textures;
+
+        // The total_gpu_memory value accounts for all WebRender instances, and
+        // thus can't be aggregated. It should really be reported out of band,
+        // but putting it in this struct facilitates sending it across Gecko IPC.
     }
 }
 
 /// A C function that takes a pointer to a heap allocation and returns its size.
 ///
 /// This is borrowed from the malloc_size_of crate, upon which we want to avoid
 /// a dependency from WebRender.
 pub type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize;
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-b04e28d59a339fe0b155ff4e5edfe2f2edac71f9
+236adf7b7737789359265cb26701625e2333729c
--- a/gfx/webrender_bindings/webrender_ffi_generated.h
+++ b/gfx/webrender_bindings/webrender_ffi_generated.h
@@ -535,32 +535,34 @@ struct MemoryReport {
   uintptr_t fonts;
   uintptr_t images;
   uintptr_t rasterized_blobs;
   uintptr_t gpu_cache_textures;
   uintptr_t vertex_data_textures;
   uintptr_t render_target_textures;
   uintptr_t texture_cache_textures;
   uintptr_t depth_target_textures;
+  uintptr_t total_gpu_bytes_allocated;
 
   bool operator==(const MemoryReport& aOther) const {
     return primitive_stores == aOther.primitive_stores &&
            clip_stores == aOther.clip_stores &&
            gpu_cache_metadata == aOther.gpu_cache_metadata &&
            gpu_cache_cpu_mirror == aOther.gpu_cache_cpu_mirror &&
            render_tasks == aOther.render_tasks &&
            hit_testers == aOther.hit_testers &&
            fonts == aOther.fonts &&
            images == aOther.images &&
            rasterized_blobs == aOther.rasterized_blobs &&
            gpu_cache_textures == aOther.gpu_cache_textures &&
            vertex_data_textures == aOther.vertex_data_textures &&
            render_target_textures == aOther.render_target_textures &&
            texture_cache_textures == aOther.texture_cache_textures &&
-           depth_target_textures == aOther.depth_target_textures;
+           depth_target_textures == aOther.depth_target_textures &&
+           total_gpu_bytes_allocated == aOther.total_gpu_bytes_allocated;
   }
 };
 
 template<typename T, typename U>
 struct TypedSize2D {
   T width;
   T height;
 
--- a/image/DecodePool.cpp
+++ b/image/DecodePool.cpp
@@ -189,16 +189,17 @@ private:
         return PopWorkFromQueue(mLowPriorityQueue);
       }
 
       if (mShuttingDown) {
         return CreateShutdownWork();
       }
 
       // Nothing to do; block until some work is available.
+      AUTO_PROFILER_LABEL("DecodePoolImpl::PopWorkLocked::Wait", IDLE);
       if (!aShutdownIdle) {
         // This thread was created before we hit the idle thread maximum. It
         // will never shutdown until the process itself is torn down.
         ++mIdleThreads;
         MOZ_ASSERT(mIdleThreads <= mThreads.Capacity());
         mMonitor.Wait();
       } else {
         // This thread should shutdown if it is idle. If we have waited longer
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -2313,16 +2313,21 @@ imgLoader::LoadImage(nsIURI* aURI,
   VerifyCacheSizes();
 
   NS_ASSERTION(aURI, "imgLoader::LoadImage -- NULL URI pointer");
 
   if (!aURI) {
     return NS_ERROR_NULL_POINTER;
   }
 
+#ifdef MOZ_GECKO_PROFILER
+  AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING(
+    "imgLoader::LoadImage", NETWORK, aURI->GetSpecOrDefault());
+#endif
+
   LOG_SCOPE_WITH_PARAM(gImgLog, "imgLoader::LoadImage", "aURI", aURI);
 
   *_retval = nullptr;
 
   RefPtr<imgRequest> request;
 
   nsresult rv;
   nsLoadFlags requestFlags = nsIRequest::LOAD_NORMAL;
--- a/js/public/ProfilingStack.h
+++ b/js/public/ProfilingStack.h
@@ -140,62 +140,80 @@ class ProfilingStackFrame
     // be null.
     mozilla::Atomic<const char*, mozilla::ReleaseAcquire,
                     mozilla::recordreplay::Behavior::DontPreserve> dynamicString_;
 
     // Stack pointer for non-JS stack frames, the script pointer otherwise.
     mozilla::Atomic<void*, mozilla::ReleaseAcquire,
                     mozilla::recordreplay::Behavior::DontPreserve> spOrScript;
 
-    // Line number for non-JS stack frames, the bytecode offset otherwise.
+    // The bytecode offset for JS stack frames.
+    // Must not be used on non-JS frames; it'll contain either the default 0,
+    // or a leftover value from a previous JS stack frame that was using this
+    // ProfilingStackFrame object.
     mozilla::Atomic<int32_t, mozilla::ReleaseAcquire,
-                    mozilla::recordreplay::Behavior::DontPreserve> lineOrPcOffset;
+                    mozilla::recordreplay::Behavior::DontPreserve> pcOffsetIfJS_;
 
-    // Bits 0...1 hold the Kind. Bits 2...31 hold the category.
+    // Bits 0...6 hold the Flags. Bits 7...31 hold the category.
     mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire,
-                    mozilla::recordreplay::Behavior::DontPreserve> kindAndCategory_;
+                    mozilla::recordreplay::Behavior::DontPreserve> flagsAndCategory_;
 
     static int32_t pcToOffset(JSScript* aScript, jsbytecode* aPc);
 
   public:
     ProfilingStackFrame() = default;
     ProfilingStackFrame& operator=(const ProfilingStackFrame& other)
     {
         label_ = other.label();
         dynamicString_ = other.dynamicString();
         void* spScript = other.spOrScript;
         spOrScript = spScript;
-        int32_t offset = other.lineOrPcOffset;
-        lineOrPcOffset = offset;
-        uint32_t kindAndCategory = other.kindAndCategory_;
-        kindAndCategory_ = kindAndCategory;
+        int32_t offsetIfJS = other.pcOffsetIfJS_;
+        pcOffsetIfJS_ = offsetIfJS;
+        uint32_t flagsAndCategory = other.flagsAndCategory_;
+        flagsAndCategory_ = flagsAndCategory;
         return *this;
     }
 
-    enum class Kind : uint32_t {
+    // 7 bits for the flags.
+    // That leaves 32 - 7 = 25 bits for the category.
+    enum class Flags : uint32_t {
+        // The first three flags describe the kind of the frame and are
+        // mutually exclusive. (We still give them individual bits for
+        // simplicity.)
+
         // A regular label frame. These usually come from AutoProfilerLabel.
-        LABEL = 0,
+        IS_LABEL_FRAME = 1 << 0,
 
         // A special frame indicating the start of a run of JS profiling stack
-        // frames. SP_MARKER frames are ignored, except for the sp field.
-        // These frames are needed to get correct ordering between JS and LABEL
-        // frames because JS frames don't carry sp information.
+        // frames. IS_SP_MARKER_FRAME frames are ignored, except for the sp
+        // field. These frames are needed to get correct ordering between JS
+        // and LABEL frames because JS frames don't carry sp information.
         // SP is short for "stack pointer".
-        SP_MARKER = 1,
+        IS_SP_MARKER_FRAME = 1 << 1,
+
+        // A JS frame.
+        IS_JS_FRAME = 1 << 2,
 
-        // A normal JS frame.
-        JS_NORMAL = 2,
+        // An interpreter JS frame that has OSR-ed into baseline. IS_JS_FRAME
+        // frames can have this flag set and unset during their lifetime.
+        // JS_OSR frames are ignored.
+        JS_OSR = 1 << 3,
 
-        // An interpreter JS frame that has OSR-ed into baseline. JS_NORMAL
-        // frames can be converted to JS_OSR and back. JS_OSR frames are
-        // ignored.
-        JS_OSR = 3,
+        // The next three are mutually exclusive.
+        // By default, for profiling stack frames that have both a label and a
+        // dynamic string, the two strings are combined into one string of the
+        // form "<label> <dynamicString>" during JSON serialization. The
+        // following flags can be used to change this preset.
+        STRING_TEMPLATE_METHOD = 1 << 4, // "<label>.<dynamicString>"
+        STRING_TEMPLATE_GETTER = 1 << 5, // "get <label>.<dynamicString>"
+        STRING_TEMPLATE_SETTER = 1 << 6, // "set <label>.<dynamicString>"
 
-        KIND_BITCOUNT = 2,
-        KIND_MASK = (1 << KIND_BITCOUNT) - 1
+        FLAGS_BITCOUNT = 7,
+        FLAGS_MASK = (1 << FLAGS_BITCOUNT) - 1
     };
 
     // Keep these in sync with devtools/client/performance/modules/categories.js
     enum class Category : uint32_t {
         IDLE,
         OTHER,
         LAYOUT,
         JS,
@@ -203,96 +221,107 @@ class ProfilingStackFrame
         NETWORK,
         GRAPHICS,
         DOM,
 
         FIRST    = OTHER,
         LAST     = DOM,
     };
 
-    static_assert(uint32_t(Category::LAST) <= (UINT32_MAX >> uint32_t(Kind::KIND_BITCOUNT)),
-                  "Too many categories to fit into u32 with two bits reserved for the kind");
+    static_assert(uint32_t(Category::LAST) <= (UINT32_MAX >> uint32_t(Flags::FLAGS_BITCOUNT)),
+                  "Too many categories to fit into u32 with together with the reserved bits for the flags");
 
     bool isLabelFrame() const
     {
-        return kind() == Kind::LABEL;
+        return uint32_t(flagsAndCategory_) & uint32_t(Flags::IS_LABEL_FRAME);
     }
 
     bool isSpMarkerFrame() const
     {
-        return kind() == Kind::SP_MARKER;
+        return uint32_t(flagsAndCategory_) & uint32_t(Flags::IS_SP_MARKER_FRAME);
     }
 
     bool isJsFrame() const
     {
-        Kind k = kind();
-        return k == Kind::JS_NORMAL || k == Kind::JS_OSR;
+        return uint32_t(flagsAndCategory_) & uint32_t(Flags::IS_JS_FRAME);
+    }
+
+    bool isOSRFrame() const {
+        return uint32_t(flagsAndCategory_) & uint32_t(Flags::JS_OSR);
+    }
+
+    void setIsOSRFrame(bool isOSR) {
+        if (isOSR) {
+            flagsAndCategory_ =
+                uint32_t(flagsAndCategory_) | uint32_t(Flags::JS_OSR);
+        } else {
+            flagsAndCategory_ =
+                uint32_t(flagsAndCategory_) & ~uint32_t(Flags::JS_OSR);
+        }
     }
 
     void setLabel(const char* aLabel) { label_ = aLabel; }
     const char* label() const { return label_; }
 
     const char* dynamicString() const { return dynamicString_; }
 
     void initLabelFrame(const char* aLabel, const char* aDynamicString, void* sp,
-                        uint32_t aLine, Category aCategory)
+                        Category aCategory, uint32_t aFlags)
     {
         label_ = aLabel;
         dynamicString_ = aDynamicString;
         spOrScript = sp;
-        lineOrPcOffset = static_cast<int32_t>(aLine);
-        kindAndCategory_ = uint32_t(Kind::LABEL) | (uint32_t(aCategory) << uint32_t(Kind::KIND_BITCOUNT));
+        // pcOffsetIfJS_ is not set and must not be used on label frames.
+        flagsAndCategory_ =
+            uint32_t(Flags::IS_LABEL_FRAME) |
+            (uint32_t(aCategory) << uint32_t(Flags::FLAGS_BITCOUNT)) |
+            aFlags;
         MOZ_ASSERT(isLabelFrame());
     }
 
     void initSpMarkerFrame(void* sp)
     {
         label_ = "";
         dynamicString_ = nullptr;
         spOrScript = sp;
-        lineOrPcOffset = 0;
-        kindAndCategory_ = uint32_t(Kind::SP_MARKER) | (uint32_t(Category::OTHER) << uint32_t(Kind::KIND_BITCOUNT));
+        // pcOffsetIfJS_ is not set and must not be used on sp marker frames.
+        flagsAndCategory_ =
+            uint32_t(Flags::IS_SP_MARKER_FRAME) |
+            (uint32_t(Category::OTHER) << uint32_t(Flags::FLAGS_BITCOUNT));
         MOZ_ASSERT(isSpMarkerFrame());
     }
 
     void initJsFrame(const char* aLabel, const char* aDynamicString, JSScript* aScript,
                      jsbytecode* aPc)
     {
         label_ = aLabel;
         dynamicString_ = aDynamicString;
         spOrScript = aScript;
-        lineOrPcOffset = pcToOffset(aScript, aPc);
-        kindAndCategory_ = uint32_t(Kind::JS_NORMAL) | (uint32_t(Category::JS) << uint32_t(Kind::KIND_BITCOUNT));
+        pcOffsetIfJS_ = pcToOffset(aScript, aPc);
+        flagsAndCategory_ =
+            uint32_t(Flags::IS_JS_FRAME) |
+            (uint32_t(Category::JS) << uint32_t(Flags::FLAGS_BITCOUNT));
         MOZ_ASSERT(isJsFrame());
     }
 
-    void setKind(Kind aKind) {
-        kindAndCategory_ = uint32_t(aKind) | (uint32_t(category()) << uint32_t(Kind::KIND_BITCOUNT));
-    }
-
-    Kind kind() const {
-        return Kind(kindAndCategory_ & uint32_t(Kind::KIND_MASK));
+    uint32_t flags() const {
+        return uint32_t(flagsAndCategory_) & uint32_t(Flags::FLAGS_MASK);
     }
 
     Category category() const {
-        return Category(kindAndCategory_ >> uint32_t(Kind::KIND_BITCOUNT));
+        return Category(flagsAndCategory_ >> uint32_t(Flags::FLAGS_BITCOUNT));
     }
 
     void* stackAddress() const {
         MOZ_ASSERT(!isJsFrame());
         return spOrScript;
     }
 
     JS_PUBLIC_API(JSScript*) script() const;
 
-    uint32_t line() const {
-        MOZ_ASSERT(!isJsFrame());
-        return static_cast<uint32_t>(lineOrPcOffset);
-    }
-
     // Note that the pointer returned might be invalid.
     JSScript* rawScript() const {
         MOZ_ASSERT(isJsFrame());
         void* script = spOrScript;
         return static_cast<JSScript*>(script);
     }
 
     // We can't know the layout of JSScript, so look in vm/GeckoProfiler.cpp.
@@ -358,75 +387,88 @@ class ProfilingStack final
   public:
     ProfilingStack()
       : stackPointer(0)
     {}
 
     ~ProfilingStack();
 
     void pushLabelFrame(const char* label, const char* dynamicString, void* sp,
-                        uint32_t line, js::ProfilingStackFrame::Category category) {
-        uint32_t oldStackPointer = stackPointer;
+                        js::ProfilingStackFrame::Category category,
+                        uint32_t flags = 0) {
+        // This thread is the only one that ever changes the value of
+        // stackPointer.
+        // Store the value of the atomic in a non-atomic local variable so that
+        // the compiler won't generate two separate loads from the atomic for
+        // the size check and the frames[] array indexing operation.
+        uint32_t stackPointerVal = stackPointer;
 
-        if (MOZ_LIKELY(capacity > oldStackPointer) || MOZ_LIKELY(ensureCapacitySlow())) {
-            frames[oldStackPointer].initLabelFrame(label, dynamicString, sp, line, category);
+        if (MOZ_UNLIKELY(stackPointerVal >= capacity)) {
+            ensureCapacitySlow();
         }
+        frames[stackPointerVal].initLabelFrame(label, dynamicString, sp,
+                                               category, flags);
 
         // This must happen at the end! The compiler will not reorder this
         // update because stackPointer is Atomic<..., ReleaseAcquire>, so any
         // the writes above will not be reordered below the stackPointer store.
         // Do the read and the write as two separate statements, in order to
         // make it clear that we don't need an atomic increment, which would be
         // more expensive on x86 than the separate operations done here.
-        // This thread is the only one that ever changes the value of
-        // stackPointer.
-        stackPointer = oldStackPointer + 1;
+        // However, don't use stackPointerVal here; instead, allow the compiler
+        // to turn this store into a non-atomic increment instruction which
+        // takes up less code size.
+        stackPointer = stackPointer + 1;
     }
 
     void pushSpMarkerFrame(void* sp) {
         uint32_t oldStackPointer = stackPointer;
 
-        if (MOZ_LIKELY(capacity > oldStackPointer) || MOZ_LIKELY(ensureCapacitySlow())) {
-            frames[oldStackPointer].initSpMarkerFrame(sp);
+        if (MOZ_UNLIKELY(oldStackPointer >= capacity)) {
+            ensureCapacitySlow();
         }
+        frames[oldStackPointer].initSpMarkerFrame(sp);
 
         // This must happen at the end, see the comment in pushLabelFrame.
         stackPointer = oldStackPointer + 1;
     }
 
     void pushJsFrame(const char* label, const char* dynamicString, JSScript* script,
                      jsbytecode* pc) {
+        // This thread is the only one that ever changes the value of
+        // stackPointer. Only load the atomic once.
         uint32_t oldStackPointer = stackPointer;
 
-        if (MOZ_LIKELY(capacity > oldStackPointer) || MOZ_LIKELY(ensureCapacitySlow())) {
-            frames[oldStackPointer].initJsFrame(label, dynamicString, script, pc);
+        if (MOZ_UNLIKELY(oldStackPointer >= capacity)) {
+            ensureCapacitySlow();
         }
+        frames[oldStackPointer].initJsFrame(label, dynamicString, script, pc);
 
         // This must happen at the end, see the comment in pushLabelFrame.
-        stackPointer = oldStackPointer + 1;
+        stackPointer = stackPointer + 1;
     }
 
     void pop() {
         MOZ_ASSERT(stackPointer > 0);
         // Do the read and the write as two separate statements, in order to
         // make it clear that we don't need an atomic decrement, which would be
         // more expensive on x86 than the separate operations done here.
         // This thread is the only one that ever changes the value of
         // stackPointer.
         uint32_t oldStackPointer = stackPointer;
         stackPointer = oldStackPointer - 1;
     }
 
-    uint32_t stackSize() const { return std::min(uint32_t(stackPointer), stackCapacity()); }
+    uint32_t stackSize() const { return stackPointer; }
     uint32_t stackCapacity() const { return capacity; }
 
   private:
     // Out of line path for expanding the buffer, since otherwise this would get inlined in every
     // DOM WebIDL call.
-    MOZ_COLD MOZ_MUST_USE bool ensureCapacitySlow();
+    MOZ_COLD void ensureCapacitySlow();
 
     // No copying.
     ProfilingStack(const ProfilingStack&) = delete;
     void operator=(const ProfilingStack&) = delete;
 
     // No moving either.
     ProfilingStack(ProfilingStack&&) = delete;
     void operator=(ProfilingStack&&) = delete;
@@ -466,31 +508,36 @@ class GeckoProfilerBaselineOSRMarker;
 class GeckoProfilerThread
 {
     friend class AutoGeckoProfilerEntry;
     friend class GeckoProfilerEntryMarker;
     friend class GeckoProfilerBaselineOSRMarker;
 
     ProfilingStack*         profilingStack_;
 
+    // Same as profilingStack_ if the profiler is currently active, otherwise null.
+    ProfilingStack*         profilingStackIfEnabled_;
+
   public:
     GeckoProfilerThread();
 
     uint32_t stackPointer() { MOZ_ASSERT(infraInstalled()); return profilingStack_->stackPointer; }
     ProfilingStackFrame* stack() { return profilingStack_->frames; }
     ProfilingStack* getProfilingStack() { return profilingStack_; }
+    ProfilingStack* getProfilingStackIfEnabled() { return profilingStackIfEnabled_; }
 
     /*
      * True if the profiler infrastructure is setup.  Should be true in builds
      * that include profiler support except during early startup or late
      * shutdown.  Unrelated to the presence of the Gecko Profiler addon.
      */
     bool infraInstalled() { return profilingStack_ != nullptr; }
 
-    void setProfilingStack(ProfilingStack* profilingStack);
+    void setProfilingStack(ProfilingStack* profilingStack, bool enabled);
+    void enable(bool enable) { profilingStackIfEnabled_ = enable ? profilingStack_ : nullptr; }
     void trace(JSTracer* trc);
 
     /*
      * Functions which are the actual instrumentation to track run information
      *
      *   - enter: a function has started to execute
      *   - updatePC: updates the pc information about where a function
      *               is currently executing
--- a/js/public/RootingAPI.h
+++ b/js/public/RootingAPI.h
@@ -861,18 +861,18 @@ class RootingContext
     template <typename T> friend class JS::Rooted;
 
     // Stack GC roots for AutoFooRooter classes.
     JS::AutoGCRooter* autoGCRooters_;
     friend class JS::AutoGCRooter;
 
     // Gecko profiling metadata.
     // This isn't really rooting related. It's only here because we want
-    // GetContextProfilingStack to be inlineable into non-JS code, and we
-    // didn't want to add another superclass of JSContext just for this.
+    // GetContextProfilingStackIfEnabled to be inlineable into non-JS code, and
+    // we didn't want to add another superclass of JSContext just for this.
     js::GeckoProfilerThread geckoProfiler_;
 
   public:
     RootingContext();
 
     void traceStackRoots(JSTracer* trc);
     void checkNoGCRooters();
 
@@ -1088,19 +1088,19 @@ GetContextCompartment(const JSContext* c
 
 inline JS::Zone*
 GetContextZone(const JSContext* cx)
 {
     return JS::RootingContext::get(cx)->zone_;
 }
 
 inline ProfilingStack*
-GetContextProfilingStack(JSContext* cx)
+GetContextProfilingStackIfEnabled(JSContext* cx)
 {
-    return JS::RootingContext::get(cx)->geckoProfiler().getProfilingStack();
+    return JS::RootingContext::get(cx)->geckoProfiler().getProfilingStackIfEnabled();
 }
 
 /**
  * Augment the generic Rooted<T> interface when T = JSObject* with
  * class-querying and downcasting operations.
  *
  * Given a Rooted<JSObject*> obj, one can view
  *   Handle<StringObject*> h = obj.as<StringObject*>();
--- a/js/src/jit/JitSpewer.cpp
+++ b/js/src/jit/JitSpewer.cpp
@@ -39,17 +39,16 @@
 
 using namespace js;
 using namespace js::jit;
 
 class IonSpewer
 {
   private:
     Mutex outputLock_;
-    Fprinter c1Output_;
     Fprinter jsonOutput_;
     bool firstFunction_;
     bool asyncLogging_;
     bool inited_;
 
     void release();
 
   public:
@@ -152,19 +151,16 @@ jit::EnableIonDebugAsyncLogging()
 {
     ionspewer.init();
     ionspewer.setAsyncLogging(true);
 }
 
 void
 IonSpewer::release()
 {
-    if (c1Output_.isInitialized()) {
-        c1Output_.finish();
-    }
     if (jsonOutput_.isInitialized()) {
         jsonOutput_.finish();
     }
     inited_ = false;
 }
 
 bool
 IonSpewer::init()
@@ -172,42 +168,32 @@ IonSpewer::init()
     if (inited_) {
         return true;
     }
 
     // Filter expression for spewing
     gSpewFilter = getenv("IONFILTER");
 
     const size_t bufferLength = 256;
-    char c1Buffer[bufferLength];
     char jsonBuffer[bufferLength];
-    const char *c1Filename = JIT_SPEW_DIR "/ion.cfg";
     const char *jsonFilename = JIT_SPEW_DIR "/ion.json";
 
     const char* usePid = getenv("ION_SPEW_BY_PID");
     if (usePid && *usePid != 0) {
         uint32_t pid = getpid();
         size_t len;
         len = snprintf(jsonBuffer, bufferLength, JIT_SPEW_DIR "/ion%" PRIu32 ".json", pid);
         if (bufferLength <= len) {
             fprintf(stderr, "Warning: IonSpewer::init: Cannot serialize file name.");
             return false;
         }
         jsonFilename = jsonBuffer;
-
-        len = snprintf(c1Buffer, bufferLength, JIT_SPEW_DIR "/ion%" PRIu32 ".cfg", pid);
-        if (bufferLength <= len) {
-            fprintf(stderr, "Warning: IonSpewer::init: Cannot serialize file name.");
-            return false;
-        }
-        c1Filename = c1Buffer;
     }
 
-    if (!c1Output_.init(c1Filename) ||
-        !jsonOutput_.init(jsonFilename))
+    if (!jsonOutput_.init(jsonFilename))
     {
         release();
         return false;
     }
 
     jsonOutput_.printf("{\n  \"functions\": [\n");
     firstFunction_ = true;
 
@@ -227,29 +213,29 @@ IonSpewer::beginFunction()
     }
 }
 
 void
 IonSpewer::spewPass(GraphSpewer* gs)
 {
     if (!getAsyncLogging()) {
         LockGuard<Mutex> guard(outputLock_);
-        gs->dump(c1Output_, jsonOutput_);
+        gs->dump(jsonOutput_);
     }
 }
 
 void
 IonSpewer::endFunction(GraphSpewer* gs)
 {
     LockGuard<Mutex> guard(outputLock_);
     if (getAsyncLogging() && !firstFunction_) {
         jsonOutput_.put(","); // separate functions
     }
 
-    gs->dump(c1Output_, jsonOutput_);
+    gs->dump(jsonOutput_);
     firstFunction_ = false;
 }
 
 IonSpewer::~IonSpewer()
 {
     if (!inited_) {
         return;
     }
@@ -350,17 +336,17 @@ GraphSpewer::endFunction()
 
     jsonSpewer_.endFunction();
 
     ionspewer.endFunction(this);
     graph_ = nullptr;
 }
 
 void
-GraphSpewer::dump(Fprinter& c1Out, Fprinter& jsonOut)
+GraphSpewer::dump(Fprinter& jsonOut)
 {
     if (!jsonPrinter_.hadOutOfMemory()) {
         jsonPrinter_.exportInto(jsonOut);
     } else {
         jsonOut.put("{}");
     }
     jsonOut.flush();
     jsonPrinter_.clear();
@@ -438,17 +424,17 @@ jit::CheckLogging()
             "  bailouts      Bailouts\n"
             "  caches        Inline caches\n"
             "  osi           Invalidation\n"
             "  safepoints    Safepoints\n"
             "  pools         Literal Pools (ARM only for now)\n"
             "  cacheflush    Instruction Cache flushes (ARM only for now)\n"
             "  range         Range Analysis\n"
             "  unroll        Loop unrolling\n"
-            "  logs          C1 and JSON visualization logging\n"
+            "  logs          JSON visualization logging\n"
             "  logs-sync     Same as logs, but flushes between each pass (sync. compiled functions only).\n"
             "  profiling     Profiling-related information\n"
             "  trackopts     Optimization tracking information gathered by the Gecko profiler. "
                             "(Note: call enableGeckoProfiling() in your script to enable it).\n"
             "  trackopts-ext Encoding information about optimization tracking\n"
             "  dump-mir-expr Dump the MIR expressions\n"
             "  cfg           Control flow graph generation\n"
             "  all           Everything\n"
--- a/js/src/jit/JitSpewer.h
+++ b/js/src/jit/JitSpewer.h
@@ -141,17 +141,17 @@ class GraphSpewer
         return graph_;
     }
     void init(MIRGraph* graph, JSScript* function);
     void beginFunction(JSScript* function);
     void spewPass(const char* pass);
     void spewPass(const char* pass, BacktrackingAllocator* ra);
     void endFunction();
 
-    void dump(Fprinter& c1, Fprinter& json);
+    void dump(Fprinter& json);
 };
 
 void SpewBeginFunction(MIRGenerator* mir, JSScript* function);
 class AutoSpewEndFunction
 {
   private:
     MIRGenerator* mir_;
 
--- a/js/src/vm/GeckoProfiler-inl.h
+++ b/js/src/vm/GeckoProfiler-inl.h
@@ -93,17 +93,16 @@ AutoGeckoProfilerEntry::AutoGeckoProfile
         return;
     }
 #ifdef DEBUG
     spBefore_ = profiler_->stackPointer();
 #endif
     profiler_->profilingStack_->pushLabelFrame(label,
                                             /* dynamicString = */ nullptr,
                                             /* sp = */ this,
-                                            /* line = */ 0,
                                             category);
 }
 
 MOZ_ALWAYS_INLINE
 AutoGeckoProfilerEntry::~AutoGeckoProfilerEntry()
 {
     if (MOZ_LIKELY(!profiler_)) {
         return;
--- a/js/src/vm/GeckoProfiler.cpp
+++ b/js/src/vm/GeckoProfiler.cpp
@@ -25,33 +25,35 @@
 #include "gc/Marking-inl.h"
 
 using namespace js;
 
 using mozilla::DebugOnly;
 
 GeckoProfilerThread::GeckoProfilerThread()
   : profilingStack_(nullptr)
+  , profilingStackIfEnabled_(nullptr)
 {
 }
 
 GeckoProfilerRuntime::GeckoProfilerRuntime(JSRuntime* rt)
   : rt(rt),
     strings(mutexid::GeckoProfilerStrings),
     slowAssertions(false),
     enabled_(false),
     eventMarker_(nullptr)
 {
     MOZ_ASSERT(rt != nullptr);
 }
 
 void
-GeckoProfilerThread::setProfilingStack(ProfilingStack* profilingStack)
+GeckoProfilerThread::setProfilingStack(ProfilingStack* profilingStack, bool enabled)
 {
     profilingStack_ = profilingStack;
+    profilingStackIfEnabled_ = enabled ? profilingStack : nullptr;
 }
 
 void
 GeckoProfilerRuntime::setEventMarker(void (*fn)(const char*))
 {
     eventMarker_ = fn;
 }
 
@@ -249,17 +251,17 @@ GeckoProfilerThread::exit(JSScript* scri
                             (void*) profilingStack_->frames,
                             uint32_t(profilingStack_->stackPointer),
                             profilingStack_->stackCapacity());
             for (int32_t i = sp; i >= 0; i--) {
                 ProfilingStackFrame& frame = profilingStack_->frames[i];
                 if (frame.isJsFrame()) {
                     fprintf(stderr, "  [%d] JS %s\n", i, frame.dynamicString());
                 } else {
-                    fprintf(stderr, "  [%d] C line %d %s\n", i, frame.line(), frame.dynamicString());
+                    fprintf(stderr, "  [%d] Label %s\n", i, frame.dynamicString());
                 }
             }
         }
 
         ProfilingStackFrame& frame = profilingStack_->frames[sp];
         MOZ_ASSERT(frame.isJsFrame());
         MOZ_ASSERT(frame.script() == script);
         MOZ_ASSERT(strcmp((const char*) frame.dynamicString(), dynamicString) == 0);
@@ -396,35 +398,35 @@ GeckoProfilerBaselineOSRMarker::GeckoPro
     }
 
     spBefore_ = sp;
     if (sp == 0) {
         return;
     }
 
     ProfilingStackFrame& frame = profiler->profilingStack_->frames[sp - 1];
-    MOZ_ASSERT(frame.kind() == ProfilingStackFrame::Kind::JS_NORMAL);
-    frame.setKind(ProfilingStackFrame::Kind::JS_OSR);
+    MOZ_ASSERT(!frame.isOSRFrame());
+    frame.setIsOSRFrame(true);
 }
 
 GeckoProfilerBaselineOSRMarker::~GeckoProfilerBaselineOSRMarker()
 {
     if (profiler == nullptr) {
         return;
     }
 
     uint32_t sp = profiler->stackPointer();
     MOZ_ASSERT(spBefore_ == sp);
     if (sp == 0) {
         return;
     }
 
     ProfilingStackFrame& frame = profiler->stack()[sp - 1];
-    MOZ_ASSERT(frame.kind() == ProfilingStackFrame::Kind::JS_OSR);
-    frame.setKind(ProfilingStackFrame::Kind::JS_NORMAL);
+    MOZ_ASSERT(frame.isOSRFrame());
+    frame.setIsOSRFrame(false);
 }
 
 JS_PUBLIC_API(JSScript*)
 ProfilingStackFrame::script() const
 {
     MOZ_ASSERT(isJsFrame());
     auto script = reinterpret_cast<JSScript*>(spOrScript.operator void*());
     if (!script) {
@@ -442,47 +444,49 @@ ProfilingStackFrame::script() const
     MOZ_ASSERT(!IsForwarded(script));
     return script;
 }
 
 JS_FRIEND_API(jsbytecode*)
 ProfilingStackFrame::pc() const
 {
     MOZ_ASSERT(isJsFrame());
-    if (lineOrPcOffset == NullPCOffset) {
+    if (pcOffsetIfJS_ == NullPCOffset) {
         return nullptr;
     }
 
     JSScript* script = this->script();
-    return script ? script->offsetToPC(lineOrPcOffset) : nullptr;
+    return script ? script->offsetToPC(pcOffsetIfJS_) : nullptr;
 }
 
 /* static */ int32_t
 ProfilingStackFrame::pcToOffset(JSScript* aScript, jsbytecode* aPc) {
     return aPc ? aScript->pcToOffset(aPc) : NullPCOffset;
 }
 
 void
 ProfilingStackFrame::setPC(jsbytecode* pc)
 {
     MOZ_ASSERT(isJsFrame());
     JSScript* script = this->script();
     MOZ_ASSERT(script); // This should not be called while profiling is suppressed.
-    lineOrPcOffset = pcToOffset(script, pc);
+    pcOffsetIfJS_ = pcToOffset(script, pc);
 }
 
 JS_FRIEND_API(void)
 js::SetContextProfilingStack(JSContext* cx, ProfilingStack* profilingStack)
 {
-    cx->geckoProfiler().setProfilingStack(profilingStack);
+    cx->geckoProfiler().setProfilingStack(profilingStack,
+        cx->runtime()->geckoProfiler().enabled());
 }
 
 JS_FRIEND_API(void)
 js::EnableContextProfilingStack(JSContext* cx, bool enabled)
 {
+    cx->geckoProfiler().enable(enabled);
     cx->runtime()->geckoProfiler().enable(enabled);
 }
 
 JS_FRIEND_API(void)
 js::RegisterContextProfilingEventMarker(JSContext* cx, void (*fn)(const char*))
 {
     MOZ_ASSERT(cx->runtime()->geckoProfiler().enabled());
     cx->runtime()->geckoProfiler().setEventMarker(fn);
--- a/js/src/vm/HelperThreads.cpp
+++ b/js/src/vm/HelperThreads.cpp
@@ -51,17 +51,17 @@ GlobalHelperThreadState* gHelperThreadSt
 
 // These macros are identical in function to the same-named ones in
 // GeckoProfiler.h, but they are defined separately because SpiderMonkey can't
 // use GeckoProfiler.h.
 #define PROFILER_RAII_PASTE(id, line) id ## line
 #define PROFILER_RAII_EXPAND(id, line) PROFILER_RAII_PASTE(id, line)
 #define PROFILER_RAII PROFILER_RAII_EXPAND(raiiObject, __LINE__)
 #define AUTO_PROFILER_LABEL(label, category) \
-  HelperThread::AutoProfilerLabel PROFILER_RAII(this, label, __LINE__, \
+  HelperThread::AutoProfilerLabel PROFILER_RAII(this, label, \
                                                 js::ProfilingStackFrame::Category::category)
 
 bool
 js::CreateHelperThreadsState()
 {
     MOZ_ASSERT(!gHelperThreadState);
     gHelperThreadState = js_new<GlobalHelperThreadState>();
     return gHelperThreadState != nullptr;
@@ -2578,22 +2578,21 @@ const HelperThread::TaskSpec HelperThrea
         THREAD_TYPE_WASM_TIER2,
         &GlobalHelperThreadState::canStartWasmTier2Generator,
         &HelperThread::handleWasmTier2GeneratorWorkload
     }
 };
 
 HelperThread::AutoProfilerLabel::AutoProfilerLabel(HelperThread* helperThread,
                                                    const char* label,
-                                                   uint32_t line,
                                                    ProfilingStackFrame::Category category)
   : profilingStack(helperThread->profilingStack)
 {
     if (profilingStack) {
-        profilingStack->pushLabelFrame(label, nullptr, this, line, category);
+        profilingStack->pushLabelFrame(label, nullptr, this, category);
     }
 }
 
 HelperThread::AutoProfilerLabel::~AutoProfilerLabel()
 {
     if (profilingStack) {
         profilingStack->pop();
     }
--- a/js/src/vm/HelperThreads.h
+++ b/js/src/vm/HelperThreads.h
@@ -416,17 +416,16 @@ struct HelperThread
 
     void ensureRegisteredWithProfiler();
     void unregisterWithProfilerIfNeeded();
 
   private:
     struct AutoProfilerLabel
     {
         AutoProfilerLabel(HelperThread* helperThread, const char* label,
-                          uint32_t line,
                           ProfilingStackFrame::Category category);
         ~AutoProfilerLabel();
 
     private:
         ProfilingStack* profilingStack;
     };
 
     /*
--- a/js/src/vm/ProfilingStack.cpp
+++ b/js/src/vm/ProfilingStack.cpp
@@ -19,36 +19,30 @@ ProfilingStack::~ProfilingStack()
     // The label macros keep a reference to the ProfilingStack to avoid a TLS
     // access. If these are somehow not all cleared we will get a
     // use-after-free so better to crash now.
     MOZ_RELEASE_ASSERT(stackPointer == 0);
 
     delete[] frames;
 }
 
-bool
+void
 ProfilingStack::ensureCapacitySlow()
 {
     MOZ_ASSERT(stackPointer >= capacity);
     const uint32_t kInitialCapacity = 128;
 
     uint32_t sp = stackPointer;
     auto newCapacity = std::max(sp + 1,  capacity ? capacity * 2 : kInitialCapacity);
 
-    auto* newFrames =
-        new (mozilla::fallible) js::ProfilingStackFrame[newCapacity];
-    if (MOZ_UNLIKELY(!newFrames)) {
-        return false;
-    }
+    auto* newFrames = new js::ProfilingStackFrame[newCapacity];
 
     // It's important that `frames` / `capacity` / `stackPointer` remain consistent here at
     // all times.
     for (auto i : mozilla::IntegerRange(capacity)) {
         newFrames[i] = frames[i];
     }
 
     js::ProfilingStackFrame* oldFrames = frames;
     frames = newFrames;
     capacity = newCapacity;
     delete[] oldFrames;
-
-    return true;
 }
--- a/js/xpconnect/tests/components/js/xpctest.manifest
+++ b/js/xpconnect/tests/components/js/xpctest.manifest
@@ -19,8 +19,11 @@ contract @mozilla.org/js/xpc/test/js/Int
 component {90ec5c9e-f6da-406b-9a38-14d00f59db76} xpctest_interfaces.js
 contract @mozilla.org/js/xpc/test/js/TestInterfaceAll;1 {90ec5c9e-f6da-406b-9a38-14d00f59db76}
 
 component {38dd78aa-467f-4fad-8dcf-4383a743e235} xpctest_returncode_child.js
 contract @mozilla.org/js/xpc/test/js/ReturnCodeChild;1 {38dd78aa-467f-4fad-8dcf-4383a743e235}
 
 component {e86573c4-a384-441a-8c92-7b99e8575b28} xpctest_utils.js
 contract @mozilla.org/js/xpc/test/js/TestUtils;1 {e86573c4-a384-441a-8c92-7b99e8575b28}
+
+component {43929c74-dc70-11e8-b6f9-8fce71a2796a} xpctest_cenums.js
+contract @mozilla.org/js/xpc/test/js/CEnums;1 {43929c74-dc70-11e8-b6f9-8fce71a2796a}
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/components/js/xpctest_cenums.js
@@ -0,0 +1,27 @@
+/* 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/. */
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function TestCEnums() {
+}
+
+TestCEnums.prototype = {
+  /* Boilerplate */
+  QueryInterface: ChromeUtils.generateQI([Ci["nsIXPCTestCEnums"]]),
+  contractID: "@mozilla.org/js/xpc/test/js/CEnums;1",
+  classID: Components.ID("{43929c74-dc70-11e8-b6f9-8fce71a2796a}"),
+
+  testCEnumInput: function(input) {
+    if (input != Ci.nsIXPCTestCEnums.shouldBe12Explicit)
+    {
+      throw new Error("Enum values do not match expected value");
+    }
+  },
+
+  testCEnumOutput: function() {
+    return Ci.nsIXPCTestCEnums.shouldBe8Explicit;
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TestCEnums]);
--- a/js/xpconnect/tests/components/native/moz.build
+++ b/js/xpconnect/tests/components/native/moz.build
@@ -5,14 +5,15 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 EXPORTS += [
     'xpctest_private.h',
 ]
 
 UNIFIED_SOURCES += [
     'xpctest_attributes.cpp',
+    'xpctest_cenums.cpp',
     'xpctest_module.cpp',
     'xpctest_params.cpp',
     'xpctest_returncode.cpp',
 ]
 
 FINAL_LIBRARY = 'xul'
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/components/native/xpctest_cenums.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+/* local header for xpconnect tests components */
+
+#include "xpctest_private.h"
+
+NS_IMPL_ISUPPORTS(xpcTestCEnums, nsIXPCTestCEnums)
+
+// If this compiles, we pass. Otherwise, this means that XPIDL bitflag
+// generation is broken.
+xpcTestCEnums::xpcTestCEnums() {
+    static_assert(0 == static_cast<uint32_t>(shouldBe0Implicit),
+                  "XPIDL bitflag generation did not create correct shouldBe0Implicit flag");
+    static_assert(1 == static_cast<uint32_t>(shouldBe1Implicit),
+                  "XPIDL bitflag generation did not create correct shouldBe1Implicit flag");
+    static_assert(2 == static_cast<uint32_t>(shouldBe2Implicit),
+                  "XPIDL bitflag generation did not create correct shouldBe2Implicit flag");
+    static_assert(3 == static_cast<uint32_t>(shouldBe3Implicit),
+                  "XPIDL bitflag generation did not create correct shouldBe3Implicit flag");
+    static_assert(5 == static_cast<uint32_t>(shouldBe5Implicit),
+                  "XPIDL bitflag generation did not create correct shouldBe5Implicit flag");
+    static_assert(6 == static_cast<uint32_t>(shouldBe6Implicit),
+                  "XPIDL bitflag generation did not create correct shouldBe6Implicit flag");
+    static_assert(2 == static_cast<uint32_t>(shouldBe2AgainImplicit),
+                  "XPIDL bitflag generation did not create correct shouldBe2AgainImplicit flag");
+    static_assert(3 == static_cast<uint32_t>(shouldBe3AgainImplicit),
+                  "XPIDL bitflag generation did not create correct shouldBe3AgainImplicit flag");
+    static_assert(1 == static_cast<uint32_t>(shouldBe1Explicit),
+                  "XPIDL bitflag generation did not create correct shouldBe1Explicit flag");
+    static_assert(2 == static_cast<uint32_t>(shouldBe2Explicit),
+                  "XPIDL bitflag generation did not create correct shouldBe2Explicit flag");
+    static_assert(4 == static_cast<uint32_t>(shouldBe4Explicit),
+                  "XPIDL bitflag generation did not create correct shouldBe4Explicit flag");
+    static_assert(8 == static_cast<uint32_t>(shouldBe8Explicit),
+                  "XPIDL bitflag generation did not create correct shouldBe8Explicit flag");
+    static_assert(12 == static_cast<uint32_t>(shouldBe12Explicit),
+                  "XPIDL bitflag generation did not create correct shouldBe12Explicit flag");
+}
+
+nsresult
+xpcTestCEnums::TestCEnumInput(testFlagsExplicit a)
+{
+    if (a != shouldBe12Explicit) {
+        return NS_ERROR_FAILURE;
+    }
+    return NS_OK;
+}
+
+nsresult
+xpcTestCEnums::TestCEnumOutput(testFlagsExplicit* a)
+{
+    *a = shouldBe8Explicit;
+    return NS_OK;
+}
--- a/js/xpconnect/tests/components/native/xpctest_module.cpp
+++ b/js/xpconnect/tests/components/native/xpctest_module.cpp
@@ -19,34 +19,42 @@
 #define NS_XPCTESTPARAMS_CID                                                  \
 { 0x1f11076a, 0x0fa2, 0x4f07,                                                 \
     { 0xb4, 0x7a, 0xa1, 0x54, 0x31, 0xf2, 0xce, 0xf7 } }
 
 #define NS_XPCTESTRETURNCODEPARENT_CID                                        \
 { 0x3818f744, 0x5445, 0x4e9c,                                                 \
     { 0x9b, 0xb8, 0x64, 0x62, 0xfe, 0x81, 0xb6, 0x19 } }
 
+#define NS_XPCTESTCENUMS_CID                                                  \
+{ 0x89ba673a, 0xa987, 0xb89c,                                                 \
+    { 0x92, 0x02, 0xb9, 0xc6, 0x23, 0x38, 0x64, 0x55 } }
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(xpcTestCEnums)
 NS_GENERIC_FACTORY_CONSTRUCTOR(xpcTestObjectReadOnly)
 NS_GENERIC_FACTORY_CONSTRUCTOR(xpcTestObjectReadWrite)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsXPCTestParams)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsXPCTestReturnCodeParent)
 NS_DEFINE_NAMED_CID(NS_XPCTESTOBJECTREADONLY_CID);
 NS_DEFINE_NAMED_CID(NS_XPCTESTOBJECTREADWRITE_CID);
 NS_DEFINE_NAMED_CID(NS_XPCTESTPARAMS_CID);
 NS_DEFINE_NAMED_CID(NS_XPCTESTRETURNCODEPARENT_CID);
+NS_DEFINE_NAMED_CID(NS_XPCTESTCENUMS_CID);
 
 static const mozilla::Module::CIDEntry kXPCTestCIDs[] = {
     { &kNS_XPCTESTOBJECTREADONLY_CID, false, nullptr, xpcTestObjectReadOnlyConstructor },
     { &kNS_XPCTESTOBJECTREADWRITE_CID, false, nullptr, xpcTestObjectReadWriteConstructor },
     { &kNS_XPCTESTPARAMS_CID, false, nullptr, nsXPCTestParamsConstructor },
     { &kNS_XPCTESTRETURNCODEPARENT_CID, false, nullptr, nsXPCTestReturnCodeParentConstructor },
+    { &kNS_XPCTESTCENUMS_CID, false, nullptr, xpcTestCEnumsConstructor },
     { nullptr }
 };
 
 static const mozilla::Module::ContractIDEntry kXPCTestContracts[] = {
+    { "@mozilla.org/js/xpc/test/native/CEnums;1", &kNS_XPCTESTCENUMS_CID },
     { "@mozilla.org/js/xpc/test/native/ObjectReadOnly;1", &kNS_XPCTESTOBJECTREADONLY_CID },
     { "@mozilla.org/js/xpc/test/native/ObjectReadWrite;1", &kNS_XPCTESTOBJECTREADWRITE_CID },
     { "@mozilla.org/js/xpc/test/native/Params;1", &kNS_XPCTESTPARAMS_CID },
     { "@mozilla.org/js/xpc/test/native/ReturnCodeParent;1", &kNS_XPCTESTRETURNCODEPARENT_CID },
     { nullptr }
 };
 
 const mozilla::Module kXPCTestModule = {
--- a/js/xpconnect/tests/components/native/xpctest_private.h
+++ b/js/xpconnect/tests/components/native/xpctest_private.h
@@ -10,16 +10,17 @@
 #define xpctest_private_h___
 
 #include "nsISupports.h"
 #include "nsMemory.h"
 #include "nsString.h"
 #include "xpctest_attributes.h"
 #include "xpctest_params.h"
 #include "xpctest_returncode.h"
+#include "xpctest_cenums.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/ModuleUtils.h"
 
 extern const mozilla::Module kXPCTestModule;
 
 class xpcTestObjectReadOnly final : public nsIXPCTestObjectReadOnly {
  public:
   NS_DECL_ISUPPORTS
@@ -75,9 +76,18 @@ public:
     NS_DECL_NSIXPCTESTRETURNCODEPARENT
 
     nsXPCTestReturnCodeParent();
 
 private:
     ~nsXPCTestReturnCodeParent();
 };
 
+class xpcTestCEnums final : public nsIXPCTestCEnums {
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIXPCTESTCENUMS
+
+  xpcTestCEnums();
+private:
+  ~xpcTestCEnums() = default;
+};
 #endif /* xpctest_private_h___ */
--- a/js/xpconnect/tests/idl/moz.build
+++ b/js/xpconnect/tests/idl/moz.build
@@ -2,15 +2,16 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPIDL_SOURCES += [
     'xpctest_attributes.idl',
     'xpctest_bug809674.idl',
+    'xpctest_cenums.idl',
     'xpctest_interfaces.idl',
     'xpctest_params.idl',
     'xpctest_returncode.idl',
     'xpctest_utils.idl',
 ]
 
 XPIDL_MODULE = 'xpctest'
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/idl/xpctest_cenums.idl
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+/*
+ *	This defines the interface for a test object.
+ *
+ */
+
+[scriptable, uuid(6a2f918e-cda2-11e8-bc9a-a34c716d1f2a)]
+interface nsIXPCTestCEnums : nsISupports {
+  const long testConst = 1;
+
+  cenum testFlagsExplicit: 8 {
+    shouldBe1Explicit = 1,
+    shouldBe2Explicit = 2,
+    shouldBe4Explicit = 4,
+    shouldBe8Explicit = 8,
+    shouldBe12Explicit = shouldBe4Explicit | shouldBe8Explicit,
+  };
+
+  cenum testFlagsImplicit: 8 {
+    shouldBe0Implicit,
+    shouldBe1Implicit,
+    shouldBe2Implicit,
+    shouldBe3Implicit,
+    shouldBe5Implicit = 5,
+    shouldBe6Implicit,
+    shouldBe2AgainImplicit = 2,
+    shouldBe3AgainImplicit,
+  };
+
+  void testCEnumInput(in nsIXPCTestCEnums_testFlagsExplicit abc);
+
+  nsIXPCTestCEnums_testFlagsExplicit testCEnumOutput();
+};
--- a/js/xpconnect/tests/moz.build
+++ b/js/xpconnect/tests/moz.build
@@ -20,13 +20,14 @@ if CONFIG['COMPILE_ENVIRONMENT']:
 XPCSHELL_TESTS_MANIFESTS += [
     'unit/xpcshell.ini',
 ]
 
 TEST_HARNESS_FILES.xpcshell.js.xpconnect.tests.components.js += [
     'components/js/xpctest.manifest',
     'components/js/xpctest_attributes.js',
     'components/js/xpctest_bug809674.js',
+    'components/js/xpctest_cenums.js',
     'components/js/xpctest_interfaces.js',
     'components/js/xpctest_params.js',
     'components/js/xpctest_returncode_child.js',
     'components/js/xpctest_utils.js',
 ]
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/unit/test_cenums.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+function run_test() {
+
+  // Load the component manifests.
+  registerXPCTestComponents();
+  registerAppManifest(do_get_file('../components/js/xpctest.manifest'));
+
+  // Test for each component.
+  test_interface_consts();
+  test_component("@mozilla.org/js/xpc/test/native/CEnums;1");
+  test_component("@mozilla.org/js/xpc/test/js/CEnums;1");
+}
+
+function test_interface_consts() {
+  Assert.equal(Ci.nsIXPCTestCEnums.testConst, 1);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe1Explicit, 1);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe2Explicit, 2);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe4Explicit, 4);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe8Explicit, 8);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe12Explicit, 12);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe1Implicit, 1);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe2Implicit, 2);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe3Implicit, 3);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe5Implicit, 5);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe6Implicit, 6);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe2AgainImplicit, 2);
+  Assert.equal(Ci.nsIXPCTestCEnums.shouldBe3AgainImplicit, 3);
+}
+
+function test_component(contractid) {
+
+  // Instantiate the object.
+  var o = Cc[contractid].createInstance(Ci["nsIXPCTestCEnums"]);
+  o.testCEnumInput(Ci.nsIXPCTestCEnums.shouldBe12Explicit);
+  o.testCEnumInput(Ci.nsIXPCTestCEnums.shouldBe8Explicit | Ci.nsIXPCTestCEnums.shouldBe4Explicit);
+  var a = o.testCEnumOutput();
+  Assert.equal(a, Ci.nsIXPCTestCEnums.shouldBe8Explicit);
+}
+
--- a/js/xpconnect/tests/unit/xpcshell.ini
+++ b/js/xpconnect/tests/unit/xpcshell.ini
@@ -59,16 +59,17 @@ support-files =
 [test_bug1131707.js]
 [test_bug1150106.js]
 [test_bug1150771.js]
 [test_bug1151385.js]
 [test_bug1170311.js]
 [test_bug1244222.js]
 [test_bug_442086.js]
 [test_callFunctionWithAsyncStack.js]
+[test_cenums.js]
 [test_classesByID_instanceof.js]
 [test_compileScript.js]
 [test_deepFreezeClone.js]
 [test_defineModuleGetter.js]
 [test_file.js]
 [test_blob.js]
 [test_blob2.js]
 [test_file2.js]
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -1148,16 +1148,18 @@ PresShell::Destroy()
   // Do not add code before this line please!
   if (mHaveShutDown) {
     return;
   }
 
   NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(),
     "destroy called on presshell while scripts not blocked");
 
+  AUTO_PROFILER_LABEL("PresShell::Destroy", LAYOUT);
+
   // dump out cumulative text perf metrics
   gfxTextPerfMetrics* tp;
   if (mPresContext && (tp = mPresContext->GetTextPerfMetrics())) {
     tp->Accumulate();
     if (tp->cumulative.numChars > 0) {
       LogTextPerfStats(tp, this, tp->cumulative, 0.0, eLog_totals, nullptr);
     }
   }
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -8324,19 +8324,16 @@ nsCSSFrameConstructor::CreateContinuingF
   nsIFrame*                  newFrame = nullptr;
   nsIFrame*                  nextContinuation = aFrame->GetNextContinuation();
   nsIFrame*                  nextInFlow = aFrame->GetNextInFlow();
 
   // Use the frame type to determine what type of frame to create
   LayoutFrameType frameType = aFrame->Type();
   nsIContent* content = aFrame->GetContent();
 
-  NS_ASSERTION(aFrame->GetSplittableType() != NS_FRAME_NOT_SPLITTABLE,
-               "why CreateContinuingFrame for a non-splittable frame?");
-
   if (LayoutFrameType::Text == frameType) {
     newFrame = NS_NewContinuingTextFrame(shell, computedStyle);
     newFrame->Init(content, aParentFrame, aFrame);
   } else if (LayoutFrameType::Inline == frameType) {
     newFrame = NS_NewInlineFrame(shell, computedStyle);
     newFrame->Init(content, aParentFrame, aFrame);
   } else if (LayoutFrameType::Block == frameType) {
     MOZ_ASSERT(!aFrame->IsTableCaption(),
--- a/layout/build/nsLayoutModule.cpp
+++ b/layout/build/nsLayoutModule.cpp
@@ -217,16 +217,21 @@ nsLayoutModuleInitialize()
   }
 
   static_assert(sizeof(uintptr_t) == sizeof(void*),
                 "Eeek! You'll need to adjust the size of uintptr_t to the "
                 "size of a pointer on your platform.");
 
   gInitialized = true;
 
+  if (XRE_GetProcessType() == GeckoProcessType_VR) {
+    // VR process doesn't need the layout module.
+    return;
+  }
+
   if (XRE_GetProcessType() == GeckoProcessType_GPU) {
     // We mark the layout module as being available in the GPU process so that
     // XPCOM's component manager initializes the power manager service, which
     // is needed for nsAppShell. However, we don't actually need anything in
     // the layout module itself.
     return;
   }
 
@@ -681,17 +686,18 @@ Initialize()
   // nsLayoutModuleInitialize should be called first.
   MOZ_RELEASE_ASSERT(gInitialized);
   return NS_OK;
 }
 
 static void
 LayoutModuleDtor()
 {
-  if (XRE_GetProcessType() == GeckoProcessType_GPU) {
+  if (XRE_GetProcessType() == GeckoProcessType_GPU ||
+      XRE_GetProcessType() == GeckoProcessType_VR) {
     return;
   }
 
   Shutdown();
   nsContentUtils::XPCOMShutdown();
 
   // Layout depends heavily on gfx and imagelib, so we want to make sure that
   // these modules are shut down after all the layout cleanup runs.
--- a/layout/generic/nsAtomicContainerFrame.h
+++ b/layout/generic/nsAtomicContainerFrame.h
@@ -30,20 +30,16 @@ public:
   }
   FrameSearchResult
   PeekOffsetCharacter(bool aForward, int32_t* aOffset,
                       PeekOffsetCharacterOptions aOptions =
                         PeekOffsetCharacterOptions()) override
   {
     return nsFrame::PeekOffsetCharacter(aForward, aOffset, aOptions);
   }
-  nsSplittableType GetSplittableType() const override
-  {
-    return nsFrame::GetSplittableType();
-  }
 
 protected:
   nsAtomicContainerFrame(ComputedStyle* aStyle, ClassID aID)
     : nsContainerFrame(aStyle, aID)
   {}
 };
 
 #endif // nsAtomicContainerFrame_h___
--- a/layout/generic/nsBlockFrame.cpp
+++ b/layout/generic/nsBlockFrame.cpp
@@ -399,22 +399,16 @@ nsBlockFrame::GetLineIterator()
   }
   return it;
 }
 
 NS_QUERYFRAME_HEAD(nsBlockFrame)
   NS_QUERYFRAME_ENTRY(nsBlockFrame)
 NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
 
-nsSplittableType
-nsBlockFrame::GetSplittableType() const
-{
-  return NS_FRAME_SPLITTABLE_NON_RECTANGULAR;
-}
-
 #ifdef DEBUG_FRAME_DUMP
 void
 nsBlockFrame::List(FILE* out, const char* aPrefix, uint32_t aFlags) const
 {
   nsCString str;
   ListGeneric(str, aPrefix, aFlags);
 
   fprintf_stderr(out, "%s<\n", str.get());
--- a/layout/generic/nsBlockFrame.h
+++ b/layout/generic/nsBlockFrame.h
@@ -132,17 +132,16 @@ public:
     }
     return false;
   }
   bool GetNaturalBaselineBOffset(mozilla::WritingMode aWM,
                                  BaselineSharingGroup aBaselineGroup,
                                  nscoord*             aBaseline) const override;
   nscoord GetCaretBaseline() const override;
   void DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData) override;
-  nsSplittableType GetSplittableType() const override;
   bool IsFloatContainingBlock() const override;
   void BuildDisplayList(nsDisplayListBuilder* aBuilder,
                         const nsDisplayListSet& aLists) override;
   bool IsFrameOfType(uint32_t aFlags) const override
   {
     return nsContainerFrame::IsFrameOfType(aFlags &
              ~(nsIFrame::eCanContainOverflowContainers |
                nsIFrame::eBlockFrame));
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -6625,22 +6625,16 @@ nsFrame::AttributeChanged(int32_t       
                           nsAtom*        aAttribute,
                           int32_t         aModType)
 {
   return NS_OK;
 }
 
 // Flow member functions
 
-nsSplittableType
-nsFrame::GetSplittableType() const
-{
-  return NS_FRAME_NOT_SPLITTABLE;
-}
-
 nsIFrame* nsFrame::GetPrevContinuation() const
 {
   return nullptr;
 }
 
 void
 nsFrame::SetPrevContinuation(nsIFrame* aPrevContinuation)
 {
--- a/layout/generic/nsFrame.h
+++ b/layout/generic/nsFrame.h
@@ -196,17 +196,16 @@ public:
                                                  nsIFrame *aBlockFrame,
                                                  int32_t aLineStart,
                                                  int8_t aOutSideLimit);
 
   nsresult CharacterDataChanged(const CharacterDataChangeInfo& aInfo) override;
   nsresult AttributeChanged(int32_t  aNameSpaceID,
                             nsAtom* aAttribute,
                             int32_t aModType) override;
-  nsSplittableType GetSplittableType() const override;
   nsIFrame* GetPrevContinuation() const override;
   void SetPrevContinuation(nsIFrame*) override;
   nsIFrame* GetNextContinuation() const override;
   void SetNextContinuation(nsIFrame*) override;
   nsIFrame* GetPrevInFlowVirtual() const override;
   void SetPrevInFlow(nsIFrame*) override;
   nsIFrame* GetNextInFlowVirtual() const override;
   void SetNextInFlow(nsIFrame*) override;
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -194,22 +194,16 @@ void
 nsHTMLScrollFrame::RemoveFrame(ChildListID aListID,
                                nsIFrame* aOldFrame)
 {
   NS_ASSERTION(aListID == kPrincipalList, "Only main list supported");
   mFrames.DestroyFrame(aOldFrame);
   mHelper.ReloadChildFrames();
 }
 
-nsSplittableType
-nsHTMLScrollFrame::GetSplittableType() const
-{
-  return NS_FRAME_NOT_SPLITTABLE;
-}
-
 /**
  HTML scrolling implementation
 
  All other things being equal, we prefer layouts with fewer scrollbars showing.
 */
 
 namespace mozilla {
 
@@ -1611,22 +1605,16 @@ nsXULScrollFrame::InsertFrames(ChildList
 void
 nsXULScrollFrame::RemoveFrame(ChildListID     aListID,
                               nsIFrame*       aOldFrame)
 {
   nsBoxFrame::RemoveFrame(aListID, aOldFrame);
   mHelper.ReloadChildFrames();
 }
 
-nsSplittableType
-nsXULScrollFrame::GetSplittableType() const
-{
-  return NS_FRAME_NOT_SPLITTABLE;
-}
-
 nsresult
 nsXULScrollFrame::GetXULPadding(nsMargin& aMargin)
 {
   aMargin.SizeTo(0,0,0,0);
   return NS_OK;
 }
 
 nscoord
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -813,17 +813,16 @@ public:
     return this;
   }
 
   virtual nsContainerFrame* GetContentInsertionFrame() override {
     return mHelper.GetScrolledFrame()->GetContentInsertionFrame();
   }
 
   virtual bool DoesClipChildren() override { return true; }
-  virtual nsSplittableType GetSplittableType() const override;
 
   nsPoint GetPositionOfChildIgnoringScrolling(const nsIFrame* aChild) override
   { nsPoint pt = aChild->GetPosition();
     if (aChild == mHelper.GetScrolledFrame()) pt += GetScrollPosition();
     return pt;
   }
 
   // nsIAnonymousContentCreator
@@ -1241,17 +1240,16 @@ public:
     return this;
   }
 
   virtual nsContainerFrame* GetContentInsertionFrame() override {
     return mHelper.GetScrolledFrame()->GetContentInsertionFrame();
   }
 
   virtual bool DoesClipChildren() override { return true; }
-  virtual nsSplittableType GetSplittableType() const override;
 
   nsPoint GetPositionOfChildIgnoringScrolling(const nsIFrame* aChild) override
   { nsPoint pt = aChild->GetPosition();
     if (aChild == mHelper.GetScrolledFrame())
       pt += mHelper.GetLogicalScrollPosition();
     return pt;
   }
 
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -115,41 +115,16 @@ class LayerManager;
 } // namespace layers
 
 namespace dom {
 class Selection;
 } // namespace dom
 
 } // namespace mozilla
 
-/**
- * Indication of how the frame can be split. This is used when doing runaround
- * of floats, and when pulling up child frames from a next-in-flow.
- *
- * The choices are splittable, not splittable at all, and splittable in
- * a non-rectangular fashion. This last type only applies to block-level
- * elements, and indicates whether splitting can be used when doing runaround.
- * If you can split across page boundaries, but you expect each continuing
- * frame to be the same width then return frSplittable and not
- * frSplittableNonRectangular.
- *
- * @see #GetSplittableType()
- */
-typedef uint32_t nsSplittableType;
-
-#define NS_FRAME_NOT_SPLITTABLE             0   // Note: not a bit!
-#define NS_FRAME_SPLITTABLE                 0x1
-#define NS_FRAME_SPLITTABLE_NON_RECTANGULAR 0x3
-
-#define NS_FRAME_IS_SPLITTABLE(type)\
-  (0 != ((type) & NS_FRAME_SPLITTABLE))
-
-#define NS_FRAME_IS_NOT_SPLITTABLE(type)\
-  (0 == ((type) & NS_FRAME_SPLITTABLE))
-
 //----------------------------------------------------------------------
 
 #define NS_SUBTREE_DIRTY(_frame)  \
   (((_frame)->GetStateBits() &      \
     (NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN)) != 0)
 
 // 1 million CSS pixels less than our max app unit measure.
 // For reflowing with an "infinite" available inline space per [css-sizing].
@@ -186,26 +161,16 @@ enum nsSelectionAmount {
   // Don't rearrange without checking the usage in nsSelection.cpp!
 
   eSelectBeginLine = 5,
   eSelectEndLine   = 6,
   eSelectNoAmount  = 7, // just bounce back current offset.
   eSelectParagraph = 8  // select a "paragraph"
 };
 
-enum nsSpread {
-  eSpreadNone   = 0,
-  eSpreadAcross = 1,
-  eSpreadDown   = 2
-};
-
-// Carried out margin flags
-#define NS_CARRIED_TOP_MARGIN_IS_AUTO    0x1
-#define NS_CARRIED_BOTTOM_MARGIN_IS_AUTO 0x2
-
 //----------------------------------------------------------------------
 // Reflow status returned by the Reflow() methods.
 class nsReflowStatus final {
   using StyleClear = mozilla::StyleClear;
 
 public:
   nsReflowStatus()
     : mBreakType(StyleClear::None)
@@ -2071,21 +2036,16 @@ public:
    * When the content states of a content object change, this method is invoked
    * on the primary frame of that content object.
    *
    * @param aStates the changed states
    */
   virtual void ContentStatesChanged(mozilla::EventStates aStates);
 
   /**
-   * Return how your frame can be split.
-   */
-  virtual nsSplittableType GetSplittableType() const = 0;
-
-  /**
    * Continuation member functions
    */
   virtual nsIFrame* GetPrevContinuation() const = 0;
   virtual void SetPrevContinuation(nsIFrame*) = 0;
   virtual nsIFrame* GetNextContinuation() const = 0;
   virtual void SetNextContinuation(nsIFrame*) = 0;
   virtual nsIFrame* FirstContinuation() const {
     return const_cast<nsIFrame*>(this);
--- a/layout/generic/nsImageFrame.h
+++ b/layout/generic/nsImageFrame.h
@@ -119,21 +119,16 @@ public:
   }
 
 #ifdef DEBUG_FRAME_DUMP
   virtual nsresult GetFrameName(nsAString& aResult) const override;
   void List(FILE* out = stderr, const char* aPrefix = "",
             uint32_t aFlags = 0) const override;
 #endif
 
-  nsSplittableType GetSplittableType() const override
-  {
-    return NS_FRAME_SPLITTABLE;
-  }
-
   virtual LogicalSides GetLogicalSkipSides(const ReflowInput* aReflowInput = nullptr) const override;
 
   nsresult GetIntrinsicImageSize(nsSize& aSize);
 
   static void ReleaseGlobals() {
     if (gIconLoad) {
       gIconLoad->Shutdown();
       gIconLoad = nullptr;
--- a/layout/generic/nsSplittableFrame.cpp
+++ b/layout/generic/nsSplittableFrame.cpp
@@ -36,22 +36,16 @@ nsSplittableFrame::DestroyFrom(nsIFrame*
   if (mPrevContinuation || mNextContinuation) {
     RemoveFromFlow(this);
   }
 
   // Let the base class destroy the frame
   nsFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
 }
 
-nsSplittableType
-nsSplittableFrame::GetSplittableType() const
-{
-  return NS_FRAME_SPLITTABLE;
-}
-
 nsIFrame* nsSplittableFrame::GetPrevContinuation() const
 {
   return mPrevContinuation;
 }
 
 void
 nsSplittableFrame::SetPrevContinuation(nsIFrame* aFrame)
 {
--- a/layout/generic/nsSplittableFrame.h
+++ b/layout/generic/nsSplittableFrame.h
@@ -20,18 +20,16 @@ class nsSplittableFrame : public nsFrame
 {
 public:
   NS_DECL_ABSTRACT_FRAME(nsSplittableFrame)
 
   void Init(nsIContent*       aContent,
             nsContainerFrame* aParent,
             nsIFrame*         aPrevInFlow) override;
 
-  nsSplittableType GetSplittableType() const override;
-
   void DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData) override;
 
   /*
    * Frame continuations can be either fluid or not:
    * Fluid continuations ("in-flows") are the result of line breaking,
    * column breaking, or page breaking.
    * Other (non-fluid) continuations can be the result of BiDi frame splitting.
    * A "flow" is a chain of fluid continuations.
--- a/layout/generic/nsTextFrame.h
+++ b/layout/generic/nsTextFrame.h
@@ -130,21 +130,16 @@ public:
     }
     if (aNextInFlow) {
       aNextInFlow->AddStateBits(NS_FRAME_IS_FLUID_CONTINUATION);
     }
   }
   nsTextFrame* LastInFlow() const final;
   nsTextFrame* LastContinuation() const final;
 
-  nsSplittableType GetSplittableType() const final
-  {
-    return NS_FRAME_SPLITTABLE;
-  }
-
   bool IsFrameOfType(uint32_t aFlags) const final
   {
     // Set the frame state bit for text frames to mark them as replaced.
     // XXX kipp: temporary
     return nsFrame::IsFrameOfType(
       aFlags & ~(nsIFrame::eReplaced | nsIFrame::eLineParticipant));
   }
 
--- a/layout/reftests/w3c-css/submitted/shapes1/reftest.list
+++ b/layout/reftests/w3c-css/submitted/shapes1/reftest.list
@@ -1,10 +1,8 @@
-default-preferences pref(layout.css.shape-outside.enabled,true)
-
 # <shape-box> only
 == shape-outside-margin-box-001.html shape-outside-margin-box-001-ref.html
 == shape-outside-margin-box-002.html shape-outside-margin-box-002-ref.html
 == shape-outside-border-box-001.html shape-outside-border-box-001-ref.html
 == shape-outside-border-box-002.html shape-outside-border-box-002-ref.html
 == shape-outside-padding-box-001.html shape-outside-padding-box-001-ref.html
 == shape-outside-padding-box-002.html shape-outside-padding-box-002-ref.html
 == shape-outside-content-box-001.html shape-outside-content-box-001-ref.html
--- a/layout/style/ServoCSSPropList.mako.py
+++ b/layout/style/ServoCSSPropList.mako.py
@@ -64,16 +64,17 @@ def method(prop):
 #
 # TODO(emilio): This will go away once the rest of the longhands have been
 # moved or perhaps using a blacklist for the ones with non-layout-dependence
 # but other non-trivial dependence like scrollbar colors.
 SERIALIZED_PREDEFINED_TYPES = [
     "Appearance",
     "BackgroundRepeat",
     "BackgroundSize",
+    "BorderImageRepeat",
     "Clear",
     "ClipRectOrAuto",
     "Color",
     "Content",
     "CounterIncrement",
     "CounterReset",
     "FillRule",
     "Float",
--- a/layout/style/nsCSSKeywordList.h
+++ b/layout/style/nsCSSKeywordList.h
@@ -138,17 +138,16 @@ CSS_KEY(justify, justify)
 CSS_KEY(last baseline, last_baseline) // only used for DevTools auto-completion
 CSS_KEY(layout, layout)
 CSS_KEY(left, left)
 CSS_KEY(legacy, legacy)
 CSS_KEY(line-through, line_through)
 CSS_KEY(list-item, list_item)
 CSS_KEY(mandatory, mandatory)
 CSS_KEY(manipulation, manipulation)
-CSS_KEY(match-parent, match_parent)
 CSS_KEY(matrix, matrix)
 CSS_KEY(matrix3d, matrix3d)
 CSS_KEY(max-content, max_content)
 CSS_KEY(middle, middle)
 CSS_KEY(min-content, min_content)
 CSS_KEY(move, move)
 CSS_KEY(n-resize, n_resize)
 CSS_KEY(ne-resize, ne_resize)
@@ -169,25 +168,23 @@ CSS_KEY(paint, paint)
 CSS_KEY(padding-box, padding_box)
 CSS_KEY(pan-x, pan_x)
 CSS_KEY(pan-y, pan_y)
 CSS_KEY(perspective, perspective)
 CSS_KEY(pointer, pointer)
 CSS_KEY(polygon, polygon)
 CSS_KEY(progress, progress)
 CSS_KEY(proximity, proximity)
-CSS_KEY(repeat, repeat)
 CSS_KEY(ridge, ridge)
 CSS_KEY(right, right)
 CSS_KEY(rotate, rotate)
 CSS_KEY(rotate3d, rotate3d)
 CSS_KEY(rotatex, rotatex)
 CSS_KEY(rotatey, rotatey)
 CSS_KEY(rotatez, rotatez)
-CSS_KEY(round, round)
 CSS_KEY(row, row)
 CSS_KEY(row-resize, row_resize)
 CSS_KEY(ruby, ruby)
 CSS_KEY(ruby-base, ruby_base)
 CSS_KEY(ruby-base-container, ruby_base_container)
 CSS_KEY(ruby-text, ruby_text)
 CSS_KEY(ruby-text-container, ruby_text_container)
 CSS_KEY(s-resize, s_resize)
@@ -250,10 +247,9 @@ CSS_KEY(w-resize, w_resize)
 CSS_KEY(wait, wait)
 CSS_KEY(wavy, wavy)
 CSS_KEY(zoom-in, zoom_in)
 CSS_KEY(zoom-out, zoom_out)
 
 // Appearance keywords for widget styles
 //CSS_KEY(middle, middle)
 //CSS_KEY(start, start)
-CSS_KEY(space, space)
 
--- a/layout/style/nsCSSProps.cpp
+++ b/layout/style/nsCSSProps.cpp
@@ -183,24 +183,16 @@ nsCSSProps::GetStringValue(nsCSSCounterD
   } else {
     static nsDependentCString sNullStr("");
     return sNullStr;
   }
 }
 
 /***************************************************************************/
 
-const KTableEntry nsCSSProps::kBorderImageRepeatKTable[] = {
-  { eCSSKeyword_stretch, StyleBorderImageRepeat::Stretch },
-  { eCSSKeyword_repeat, StyleBorderImageRepeat::Repeat },
-  { eCSSKeyword_round, StyleBorderImageRepeat::Round },
-  { eCSSKeyword_space, StyleBorderImageRepeat::Space },
-  { eCSSKeyword_UNKNOWN, -1 }
-};
-
 const KTableEntry nsCSSProps::kBorderStyleKTable[] = {
   { eCSSKeyword_none,   NS_STYLE_BORDER_STYLE_NONE },
   { eCSSKeyword_hidden, NS_STYLE_BORDER_STYLE_HIDDEN },
   { eCSSKeyword_dotted, NS_STYLE_BORDER_STYLE_DOTTED },
   { eCSSKeyword_dashed, NS_STYLE_BORDER_STYLE_DASHED },
   { eCSSKeyword_solid,  NS_STYLE_BORDER_STYLE_SOLID },
   { eCSSKeyword_double, NS_STYLE_BORDER_STYLE_DOUBLE },
   { eCSSKeyword_groove, NS_STYLE_BORDER_STYLE_GROOVE },
--- a/layout/style/nsCSSProps.h
+++ b/layout/style/nsCSSProps.h
@@ -304,17 +304,16 @@ public:
                             es_ = (nsCSSPropertyID)((enabledstate_) |       \
                                                   CSSEnabledState(0));    \
        *it_ != eCSSProperty_UNKNOWN; ++it_)                               \
     if (nsCSSProps::IsEnabled(*it_, (mozilla::CSSEnabledState) es_))
 
   // Keyword/Enum value tables
   // Not const because we modify its entries when the pref
   // "layout.css.background-clip.text" changes:
-  static const KTableEntry kBorderImageRepeatKTable[];
   static const KTableEntry kBorderStyleKTable[];
   static const KTableEntry kShapeRadiusKTable[];
   static const KTableEntry kFilterFunctionKTable[];
   static const KTableEntry kBoxShadowTypeKTable[];
   static const KTableEntry kCursorKTable[];
   // Not const because we modify its entries when various
   // "layout.css.*.enabled" prefs changes:
   static KTableEntry kDisplayKTable[];
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -3011,39 +3011,16 @@ nsComputedDOMStyle::DoGetBorderImageOuts
 {
   const nsStyleBorder* border = StyleBorder();
   RefPtr<nsDOMCSSValueList> valueList = GetROCSSValueList(false);
   AppendFourSideCoordValues(valueList, border->mBorderImageOutset);
   return valueList.forget();
 }
 
 already_AddRefed<CSSValue>
-nsComputedDOMStyle::DoGetBorderImageRepeat()
-{
-  RefPtr<nsDOMCSSValueList> valueList = GetROCSSValueList(false);
-
-  const nsStyleBorder* border = StyleBorder();
-
-  // horizontal repeat
-  RefPtr<nsROCSSPrimitiveValue> valX = new nsROCSSPrimitiveValue;
-  valX->SetIdent(
-    nsCSSProps::ValueToKeywordEnum(border->mBorderImageRepeatH,
-                                   nsCSSProps::kBorderImageRepeatKTable));
-  valueList->AppendCSSValue(valX.forget());
-
-  // vertical repeat
-  RefPtr<nsROCSSPrimitiveValue> valY = new nsROCSSPrimitiveValue;
-  valY->SetIdent(
-    nsCSSProps::ValueToKeywordEnum(border->mBorderImageRepeatV,
-                                   nsCSSProps::kBorderImageRepeatKTable));
-  valueList->AppendCSSValue(valY.forget());
-  return valueList.forget();
-}
-
-already_AddRefed<CSSValue>
 nsComputedDOMStyle::DoGetFlexBasis()
 {
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
 
   // XXXdholbert We could make this more automagic and resolve percentages
   // if we wanted, by passing in a PercentageBaseGetter instead of nullptr
   // below.  Logic would go like this:
   //   if (i'm a flex item) {
--- a/layout/style/nsComputedDOMStyle.h
+++ b/layout/style/nsComputedDOMStyle.h
@@ -306,17 +306,16 @@ private:
   already_AddRefed<CSSValue> DoGetBorderBottomRightRadius();
   already_AddRefed<CSSValue> DoGetBorderTopLeftRadius();
   already_AddRefed<CSSValue> DoGetBorderTopRightRadius();
 
   /* Border Image */
   already_AddRefed<CSSValue> DoGetBorderImageSlice();
   already_AddRefed<CSSValue> DoGetBorderImageWidth();
   already_AddRefed<CSSValue> DoGetBorderImageOutset();
-  already_AddRefed<CSSValue> DoGetBorderImageRepeat();
 
   /* Box Shadow */
   already_AddRefed<CSSValue> DoGetBoxShadow();
 
   /* Window Shadow */
 
   /* Margin Properties */
   already_AddRefed<CSSValue> DoGetMarginTopWidth();
deleted file mode 100644
--- a/layout/style/test/file_shape_outside_CORS.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<meta charset="utf-8">
-<style>
-.container {
-  clear: both;
-  width: 500px;
-}
-.shaper {
-  width: 50px;
-  height: 50px;
-  float: left;
-  background-color: green;
-}
-.shapeAllow {
-  shape-outside: url("support/1x1-transparent.png");
-}
-.shapeRefuse {
-  shape-outside: url("http://example.com/layout/style/test/support/1x1-transparent.png");
-}
-.sibling {
-  display: inline-block;
-}
-</style>
-
-<script>
-const DOMAIN = "http://mochi.test:8888";
-
-function sendResults() {
-  let divAllow = document.getElementById("allow");
-  let divAllowSib = divAllow.nextElementSibling;
-  window.parent.postMessage({
-    "result": (divAllowSib.getBoundingClientRect().left == divAllow.getBoundingClientRect().left),
-    "message": "Test 1: Sibling should be at same left offset as div (shape-outside should be allowed), and onload should only fire after layout is complete.",
-    },
-    DOMAIN);
-
-  let divRefuse = document.getElementById("refuse");
-  let divRefuseSib = divRefuse.nextElementSibling;
-  window.parent.postMessage({
-    "result": (divRefuseSib.getBoundingClientRect().left != divRefuse.getBoundingClientRect().left),
-    "message": "Test 2: Sibling should be at different left offset from div (shape-outside should be refused).",
-    },
-    DOMAIN);
-
-  window.parent.postMessage({"done": true}, DOMAIN);
-}
-</script>
-</head>
-<body onload="sendResults()">
-  <div class="container">
-    <div id="allow" class="shaper shapeAllow"></div><div class="sibling">allow (image is blank, so text is flush left)</div>
-  </div>
-  <div class="container">
-    <div id="refuse" class="shaper shapeRefuse"></div><div class="sibling">refuse (image unread, so text is moved to box edge)</div>
-  </div>
-</body>
-</html>
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -302,17 +302,16 @@ skip-if = toolkit == 'android' # bug 132
 [test_rule_serialization.html]
 [test_rules_out_of_sheets.html]
 [test_selectors.html]
 skip-if = toolkit == 'android' #bug 775227
 [test_selectors_on_anonymous_content.html]
 [test_setPropertyWithNull.html]
 [test_shape_outside_CORS.html]
 support-files =
-  file_shape_outside_CORS.html
   support/1x1-transparent.png
 [test_shorthand_property_getters.html]
 [test_specified_value_serialization.html]
 support-files = file_specified_value_serialization_individual_transforms.html
 [test_style_attr_listener.html]
 [test_style_attribute_quirks.html]
 [test_style_attribute_standards.html]
 [test_style_struct_copy_constructors.html]
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -5193,16 +5193,56 @@ var gCSSProperties = {
   "marker-start": {
     domProp: "markerStart",
     inherited: true,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "none" ],
     other_values: [ "url(#mysym)" ],
     invalid_values: []
   },
+  "shape-image-threshold": {
+    domProp: "shapeImageThreshold",
+    inherited: false,
+    type: CSS_TYPE_LONGHAND,
+    applies_to_first_letter: true,
+    initial_values: [ "0", "0.0000", "-3", ],
+    other_values: [ "0.4", "1", "17", "397.376", "3e1", "3e+1", "3e-1", "3e0", "3e+0", "3e-0" ],
+    invalid_values: [ "0px", "1px", "20%", "default", "auto" ]
+  },
+  "shape-margin": {
+    domProp: "shapeMargin",
+    inherited: false,
+    type: CSS_TYPE_LONGHAND,
+    applies_to_first_letter: true,
+    initial_values: [ "0", ],
+    other_values: [ "2px", "2%", "1em", "calc(1px + 1em)", "calc(1%)" ],
+    invalid_values: [ "-1px", "auto", "none", "1px 1px", "-1%" ],
+  },
+  "shape-outside": {
+    domProp: "shapeOutside",
+    inherited: false,
+    type: CSS_TYPE_LONGHAND,
+    applies_to_first_letter: true,
+    initial_values: [ "none" ],
+    other_values: [
+      "url(#my-shape-outside)",
+    ].concat(
+      basicShapeOtherValues,
+      validGradientAndElementValues
+    ),
+    invalid_values: [].concat(
+      basicShapeSVGBoxValues,
+      basicShapeInvalidValues,
+      invalidGradientAndElementValues
+    ),
+    unbalanced_values: [].concat(
+      basicShapeUnbalancedValues,
+      unbalancedGradientAndElementValues
+    )
+  },
   "shape-rendering": {
     domProp: "shapeRendering",
     inherited: true,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "auto" ],
     other_values: [ "optimizeSpeed", "crispEdges", "geometricPrecision" ],
     invalid_values: []
   },
@@ -6445,61 +6485,16 @@ if (IsCSSPropertyPrefEnabled("svg.transf
     inherited: false,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "border-box" ],
     other_values: [ "fill-box", "view-box" ],
     invalid_values: ["content-box", "padding-box", "stroke-box", "margin-box"]
   };
 }
 
-if (IsCSSPropertyPrefEnabled("layout.css.shape-outside.enabled")) {
-  gCSSProperties["shape-image-threshold"] = {
-    domProp: "shapeImageThreshold",
-    inherited: false,
-    type: CSS_TYPE_LONGHAND,
-    applies_to_first_letter: true,
-    initial_values: [ "0", "0.0000", "-3", ],
-    other_values: [ "0.4", "1", "17", "397.376", "3e1", "3e+1", "3e-1", "3e0", "3e+0", "3e-0" ],
-    invalid_values: [ "0px", "1px", "20%", "default", "auto" ]
-  };
-
-  gCSSProperties["shape-margin"] = {
-    domProp: "shapeMargin",
-    inherited: false,
-    type: CSS_TYPE_LONGHAND,
-    applies_to_first_letter: true,
-    initial_values: [ "0", ],
-    other_values: [ "2px", "2%", "1em", "calc(1px + 1em)", "calc(1%)" ],
-    invalid_values: [ "-1px", "auto", "none", "1px 1px", "-1%" ],
-  };
-
-  gCSSProperties["shape-outside"] = {
-    domProp: "shapeOutside",
-    inherited: false,
-    type: CSS_TYPE_LONGHAND,
-    applies_to_first_letter: true,
-    initial_values: [ "none" ],
-    other_values: [
-      "url(#my-shape-outside)",
-    ].concat(
-      basicShapeOtherValues,
-      validGradientAndElementValues
-    ),
-    invalid_values: [].concat(
-      basicShapeSVGBoxValues,
-      basicShapeInvalidValues,
-      invalidGradientAndElementValues
-    ),
-    unbalanced_values: [].concat(
-      basicShapeUnbalancedValues,
-      unbalancedGradientAndElementValues
-    )
-  };
-}
-
 var isGridTemplateSubgridValueEnabled =
   IsCSSPropertyPrefEnabled("layout.css.grid-template-subgrid-value.enabled");
 
 gCSSProperties["display"].other_values.push("grid", "inline-grid");
 gCSSProperties["grid-auto-flow"] = {
   domProp: "gridAutoFlow",
   inherited: false,
   type: CSS_TYPE_LONGHAND,
--- a/layout/style/test/test_computed_style.html
+++ b/layout/style/test/test_computed_style.html
@@ -179,17 +179,17 @@ var noframe_container = document.getElem
     [ "calc(0%) 0", "0% 0px", "0% calc horizontal"],
     [ "0 calc(0%)", "0px 0%", "0% calc vertical"],
     [ "calc(3px + 2% - 2%) 0", "calc(0% + 3px) 0px",
                       "computed 0% calc horizontal"],
     [ "0 calc(3px + 2% - 2%)", "0px calc(0% + 3px)",
                       "computed 0% calc vertical"],
     [ "calc(3px - 5px) calc(6px - 9px)",
       "calc(-2px) calc(-3px)", "negative pixel width" ],
-    [ "", "auto auto", "initial value" ],
+    [ "", "auto", "initial value" ],
   ];
 
   var p = document.createElement("p");
   var cs = getComputedStyle(p, "");
   frame_container.appendChild(p);
 
   for (var i = 0; i < backgroundSizes.length; ++i) {
     var test = backgroundSizes[i];
--- a/layout/style/test/test_shape_outside_CORS.html
+++ b/layout/style/test/test_shape_outside_CORS.html
@@ -3,46 +3,57 @@
 <head>
 <meta charset="utf-8">
 <title>CSS Test: shape-outside with a CORS violation</title>
 <link rel="author" title="Brad Werth" href="mailto:bwerth@mozilla.com"/>
 <link rel="help" href="https://drafts.csswg.org/css-shapes/#shape-outside-property"/>
 <script src="/tests/SimpleTest/SimpleTest.js"></script>
 <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
 
+<style>
+.container {
+  clear: both;
+  width: 500px;
+}
+.shaper {
+  width: 50px;
+  height: 50px;
+  float: left;
+  background-color: green;
+}
+.shapeAllow {
+  shape-outside: url("support/1x1-transparent.png");
+}
+.shapeRefuse {
+  shape-outside: url("http://example.com/layout/style/test/support/1x1-transparent.png");
+}
+.sibling {
+  display: inline-block;
+}
+</style>
+
 <script>
 SimpleTest.waitForExplicitFinish();
 
-// We'll eventually receive messages from our iframe, so prep to receive them here.
-function receiveMessage(event)
-{
-  if (event.data.done) {
-    // Remove ourself as an event listener, just to be thorough.
-    window.removeEventListener("message", receiveMessage);
-    // Undo our meddling in preferences, then finish the test.
-    SpecialPowers.popPrefEnv(SimpleTest.finish);
-    return;
-  }
+function runTests() {
+  let divAllow = document.getElementById("allow");
+  let divAllowSib = divAllow.nextElementSibling;
+  ok(divAllowSib.getBoundingClientRect().left == divAllow.getBoundingClientRect().left,
+     "Test 1: Sibling should be at same left offset as div (shape-outside should be allowed), and onload should only fire after layout is complete.");
 
-  let reportResult = event.data.todo ? todo : ok;
-  reportResult(event.data.result, event.data.message);
-}
-
-function runTests()
-{
-  window.addEventListener("message", receiveMessage);
+  let divRefuse = document.getElementById("refuse");
+  let divRefuseSib = divRefuse.nextElementSibling;
+  ok(divRefuseSib.getBoundingClientRect().left != divRefuse.getBoundingClientRect().left,
+     "Test 2: Sibling should be at different left offset from div (shape-outside should be refused).");
 
-  // Set a pref that we'll need, then set the source of the iframe.
-  // Once the iframe source is set, the contents will start sending
-  // messages to us.
-  SpecialPowers.pushPrefEnv({"set": [
-    ["layout.css.shape-outside.enabled", true],
-  ]}, () => {
-    let content = document.getElementById("content");
-    content.src = "file_shape_outside_CORS.html";
-  });
+  SimpleTest.finish();
 }
 </script>
 </head>
 <body onload="runTests()">
-<iframe id="content"></iframe>
+  <div class="container">
+    <div id="allow" class="shaper shapeAllow"></div><div class="sibling">allow (image is blank, so text is flush left)</div>
+  </div>
+  <div class="container">
+    <div id="refuse" class="shaper shapeRefuse"></div><div class="sibling">refuse (image unread, so text is moved to box edge)</div>
+  </div>
 </body>
 </html>
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -235,16 +235,24 @@ var supported_properties = {
                      test_length_clamped, test_percent_clamped ],
     "perspective": [ test_length_transition ],
     "perspective-origin": [ test_length_pair_transition,
                             test_length_percent_pair_transition,
                             test_length_percent_pair_unclamped ],
     "right": [ test_length_transition, test_percent_transition,
                test_length_percent_calc_transition,
                test_length_unclamped, test_percent_unclamped ],
+    "shape-image-threshold": [ test_float_zeroToOne_transition,
+                               // shape-image-threshold (like opacity) is
+                               // clamped in computed style
+                               // (not parsing/interpolation)
+                               test_float_zeroToOne_clamped ],
+    "shape-margin": [ test_length_transition, test_percent_transition,
+                      test_length_clamped, test_percent_clamped ],
+    "shape-outside": [ test_basic_shape_or_url_transition ],
     "stop-color": [ test_color_transition,
                     test_currentcolor_transition ],
     "stop-opacity" : [ test_float_zeroToOne_transition,
                        // opacity is clamped in computed style
                        // (not parsing/interpolation)
                        test_float_zeroToOne_clamped ],
     "stroke": [ test_color_transition,
                 test_currentcolor_transition ],
@@ -290,31 +298,16 @@ var supported_properties = {
     "word-spacing": [ test_length_transition, test_length_unclamped ],
     "z-index": [ test_integer_transition, test_pos_integer_or_auto_transition ],
     "-webkit-text-fill-color": [ test_color_transition,
                                  test_currentcolor_transition ],
     "-webkit-text-stroke-color": [ test_color_transition,
                                    test_currentcolor_transition ]
 };
 
-if (SpecialPowers.getBoolPref("layout.css.shape-outside.enabled")) {
-  supported_properties["shape-image-threshold"] =
-    [ test_float_zeroToOne_transition,
-      // shape-image-threshold (like opacity) is clamped in computed style
-      // (not parsing/interpolation)
-      test_float_zeroToOne_clamped ];
-
-  supported_properties["shape-margin"] =
-    [ test_length_transition, test_percent_transition,
-      test_length_clamped, test_percent_clamped ];
-
-  supported_properties["shape-outside"] =
-    [ test_basic_shape_or_url_transition ];
-}
-
 if (IsCSSPropertyPrefEnabled("layout.css.motion-path.enabled")) {
   supported_properties["offset-path"] = [ test_path_function ];
 }
 
 if (IsCSSPropertyPrefEnabled("layout.css.clip-path-path.enabled")) {
   supported_properties["clip-path"].push(test_path_function);
 }
 
--- a/layout/svg/nsSVGOuterSVGFrame.cpp
+++ b/layout/svg/nsSVGOuterSVGFrame.cpp
@@ -793,22 +793,16 @@ nsSVGOuterSVGFrame::BuildDisplayList(nsD
                          contentList, contentList, contentList);
     BuildDisplayListForNonBlockChildren(aBuilder, set);
   } else if (IsVisibleForPainting(aBuilder) || !aBuilder->IsForPainting()) {
     aLists.Content()->AppendToTop(
       MakeDisplayItem<nsDisplayOuterSVG>(aBuilder, this));
   }
 }
 
-nsSplittableType
-nsSVGOuterSVGFrame::GetSplittableType() const
-{
-  return NS_FRAME_NOT_SPLITTABLE;
-}
-
 //----------------------------------------------------------------------
 // nsISVGSVGFrame methods:
 
 void
 nsSVGOuterSVGFrame::NotifyViewportOrTransformChanged(uint32_t aFlags)
 {
   MOZ_ASSERT(aFlags &&
              !(aFlags & ~(COORD_CONTEXT_CHANGED | TRANSFORM_CHANGED |
--- a/layout/svg/nsSVGOuterSVGFrame.h
+++ b/layout/svg/nsSVGOuterSVGFrame.h
@@ -75,18 +75,16 @@ public:
                     nsIFrame*         aPrevInFlow) override;
 
   bool IsFrameOfType(uint32_t aFlags) const override
   {
     return nsSVGDisplayContainerFrame::IsFrameOfType(
       aFlags & ~eSupportsContainLayoutAndPaint);
   }
 
-  virtual nsSplittableType GetSplittableType() const override;
-
 #ifdef DEBUG_FRAME_DUMP
   virtual nsresult GetFrameName(nsAString& aResult) const override
   {
     return MakeFrameName(NS_LITERAL_STRING("SVGOuterSVG"), aResult);
   }
 #endif
 
   virtual nsresult  AttributeChanged(int32_t         aNameSpaceID,
--- a/layout/tables/nsTableColFrame.cpp
+++ b/layout/tables/nsTableColFrame.cpp
@@ -197,22 +197,16 @@ nsTableColFrame::GetNextCol() const
 #ifdef DEBUG_FRAME_DUMP
 nsresult
 nsTableColFrame::GetFrameName(nsAString& aResult) const
 {
   return MakeFrameName(NS_LITERAL_STRING("TableCol"), aResult);
 }
 #endif
 
-nsSplittableType
-nsTableColFrame::GetSplittableType() const
-{
-  return NS_FRAME_NOT_SPLITTABLE;
-}
-
 void
 nsTableColFrame::InvalidateFrame(uint32_t aDisplayItemKey, bool aRebuildDisplayItems)
 {
   nsIFrame::InvalidateFrame(aDisplayItemKey, aRebuildDisplayItems);
   if (GetTableFrame()->IsBorderCollapse()) {
     GetParent()->InvalidateFrameWithRect(GetVisualOverflowRect() + GetPosition(), aDisplayItemKey, false);
   }
 }
--- a/layout/tables/nsTableColFrame.h
+++ b/layout/tables/nsTableColFrame.h
@@ -55,18 +55,16 @@ public:
 
   virtual void BuildDisplayList(nsDisplayListBuilder*   aBuilder,
                                 const nsDisplayListSet& aLists) override;
 
 #ifdef DEBUG_FRAME_DUMP
   virtual nsresult GetFrameName(nsAString& aResult) const override;
 #endif
 
-  virtual nsSplittableType GetSplittableType() const override;
-
   nsTableColGroupFrame* GetTableColGroupFrame() const
   {
     nsIFrame* parent = GetParent();
     MOZ_ASSERT(parent && parent->IsTableColGroupFrame());
     return static_cast<nsTableColGroupFrame*>(parent);
   }
 
   nsTableFrame* GetTableFrame() const
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
@@ -92,17 +92,17 @@ public final class NotificationHelper im
          * Default notification channel.
          */
         DEFAULT,
         /**
          * Mozilla Location Services notification channel.
          */
         MLS,
         /**
-         * Mozilla Location Services notification channel.
+         * File downloads.
          */
         DOWNLOAD,
         /**
          *  Media notification channel
          */
         MEDIA,
         /**
          * Built-in updater - use only when <code>AppConstants.MOZ_UPDATER</code> is true.
@@ -237,17 +237,17 @@ public final class NotificationHelper im
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
                             mContext.getString(R.string.download_notification_channel),
                             NotificationManager.IMPORTANCE_LOW);
                 }
                 break;
 
                 case MEDIA: {
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
-                            mContext.getString(R.string.media_notification_channel),
+                            mContext.getString(R.string.media_notification_channel2),
                             NotificationManager.IMPORTANCE_LOW);
                 }
                 break;
 
                 case UPDATER: {
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
                             mContext.getString(R.string.updater_notification_channel),
                             NotificationManager.IMPORTANCE_LOW);
@@ -258,32 +258,32 @@ public final class NotificationHelper im
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
                             mContext.getString(R.string.synced_tabs_notification_channel),
                             NotificationManager.IMPORTANCE_HIGH);
                 }
                 break;
 
                 case LP_DEFAULT: {
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
-                            mContext.getString(R.string.leanplum_default_notifications_channel),
+                            mContext.getString(R.string.leanplum_default_notifications_channel2),
                             NotificationManager.IMPORTANCE_LOW);
                 }
                 break;
 
                 case SITE_NOTIFICATIONS: {
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
                             mContext.getString(R.string.site_notifications_channel),
                             NotificationManager.IMPORTANCE_DEFAULT);
                 }
                 break;
 
                 case DEFAULT:
                 default: {
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
-                            mContext.getString(R.string.default_notification_channel),
+                            mContext.getString(R.string.default_notification_channel2),
                             NotificationManager.IMPORTANCE_LOW);
                 }
                 break;
             }
 
             manager.createNotificationChannel(channel);
         }
     }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -892,19 +892,19 @@ See also https://bug1409261.bmoattachmen
 <!-- Used by accessibility services to identify the play/pause buttons shown in the
 Picture-in-picture mini window -->
 <!ENTITY pip_play_button_title "Play">
 <!ENTITY pip_play_button_description "Resume playing">
 <!ENTITY pip_pause_button_title "Pause">
 <!ENTITY pip_pause_button_description "Pause playing">
 
 <!-- Notification channels names -->
-<!ENTITY default_notification_channel "&brandShortName;">
+<!ENTITY default_notification_channel2 "Browser">
 <!ENTITY mls_notification_channel "&vendorShortName; Location Service">
 <!ENTITY download_notification_channel "Downloads">
-<!ENTITY media_notification_channel "Media playback">
+<!ENTITY media_notification_channel2 "Sound and video">
 <!-- These push notifications come without a specific channel and/or name from Leanplum -->
-<!ENTITY leanplum_default_notifications_channel "&brandShortName; Push notifications">
+<!ENTITY leanplum_default_notifications_channel2 "&vendorShortName; tips and tricks">
 <!ENTITY updater_notification_channel "App updates">
 <!ENTITY synced_tabs_notification_channel "Synced tabs">
 <!-- LOCALIZATION NOTE (site_notifications_channel): This is for system notifications displayed by
 web sites through the HTML Notifications API. -->
 <!ENTITY site_notifications_channel "Site notifications">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -642,17 +642,17 @@
   <string name="pwa_onboarding_sumo">&pwa_onboarding_sumo;</string>
   <string name="pwa_continue_to_website">&pwa_continue_to_website;</string>
 
   <string name="pip_play_button_title">&pip_play_button_title;</string>
   <string name="pip_play_button_description">&pip_play_button_description;</string>
   <string name="pip_pause_button_title">&pip_pause_button_title;</string>
   <string name="pip_pause_button_description">&pip_pause_button_description;</string>
 
-  <string name="default_notification_channel">&default_notification_channel;</string>
+  <string name="default_notification_channel2">&default_notification_channel2;</string>
   <string name="mls_notification_channel">&mls_notification_channel;</string>
-  <string name="media_notification_channel">&media_notification_channel;</string>
+  <string name="media_notification_channel2">&media_notification_channel2;</string>
   <string name="download_notification_channel">&download_notification_channel;</string>
-  <string name="leanplum_default_notifications_channel">&leanplum_default_notifications_channel;</string>
+  <string name="leanplum_default_notifications_channel2">&leanplum_default_notifications_channel2;</string>
   <string name="updater_notification_channel">&updater_notification_channel;</string>
   <string name="synced_tabs_notification_channel">&synced_tabs_notification_channel;</string>
   <string name="site_notifications_channel">&site_notifications_channel;</string>
 </resources>
--- a/modules/libmar/src/mar_read.c
+++ b/modules/libmar/src/mar_read.c
@@ -12,16 +12,22 @@
 #include "mar.h"
 
 #ifdef XP_WIN
 #include <winsock2.h>
 #else
 #include <netinet/in.h>
 #endif
 
+/* This block must be at most 104 bytes.
+   MAR channel name < 64 bytes, and product version < 32 bytes + 3 NULL
+   terminator bytes. We only check for 96 though because we remove 8
+   bytes above from the additionalBlockSize: We subtract
+   sizeof(additionalBlockSize) and sizeof(additionalBlockID) */
+#define MAXADDITIONALBLOCKSIZE 96
 
 /* this is the same hash algorithm used by nsZipArchive.cpp */
 static uint32_t mar_hash_name(const char *name) {
   uint32_t val = 0;
   unsigned char* c;
 
   for (c = (unsigned char *) name; *c; ++c)
     val = val*37 + *c;
@@ -392,62 +398,61 @@ read_product_info_block(char *path,
  *
  * @param infoBlock Out parameter for where to store the result to
  * @return 0 on success, -1 on failure
 */
 int
 mar_read_product_info_block(MarFile *mar,
                             struct ProductInformationBlock *infoBlock)
 {
-  uint32_t i, offsetAdditionalBlocks, numAdditionalBlocks,
+  uint32_t offsetAdditionalBlocks, numAdditionalBlocks,
     additionalBlockSize, additionalBlockID;
   int hasAdditionalBlocks;
 
   /* The buffer size is 97 bytes because the MAR channel name < 64 bytes, and
      product version < 32 bytes + 3 NULL terminator bytes. */
-  char buf[97] = { '\0' };
+  char buf[MAXADDITIONALBLOCKSIZE + 1] = { '\0' };
   if (get_mar_file_info_fp(mar->fp, NULL, NULL,
                            &hasAdditionalBlocks,
                            &offsetAdditionalBlocks,
                            &numAdditionalBlocks) != 0) {
     return -1;
   }
-  for (i = 0; i < numAdditionalBlocks; ++i) {
+
+  /* We only have the one additional block type and only one is expected to be
+     in a MAR file so check if any exist and process the first found */
+  if (numAdditionalBlocks > 0) {
     /* Read the additional block size */
     if (fread(&additionalBlockSize,
               sizeof(additionalBlockSize),
               1, mar->fp) != 1) {
       return -1;
     }
     additionalBlockSize = ntohl(additionalBlockSize) -
                           sizeof(additionalBlockSize) -
                           sizeof(additionalBlockID);
 
+    /* Additional Block sizes should only be 96 bytes long */
+    if (additionalBlockSize > MAXADDITIONALBLOCKSIZE) {
+      return -1;
+    }
+
     /* Read the additional block ID */
     if (fread(&additionalBlockID,
               sizeof(additionalBlockID),
               1, mar->fp) != 1) {
       return -1;
     }
     additionalBlockID = ntohl(additionalBlockID);
 
     if (PRODUCT_INFO_BLOCK_ID == additionalBlockID) {
       const char *location;
       int len;
 
-      /* This block must be at most 104 bytes.
-         MAR channel name < 64 bytes, and product version < 32 bytes + 3 NULL
-         terminator bytes. We only check for 96 though because we remove 8
-         bytes above from the additionalBlockSize: We subtract
-         sizeof(additionalBlockSize) and sizeof(additionalBlockID) */
-      if (additionalBlockSize > 96) {
-        return -1;
-      }
-
-    if (fread(buf, additionalBlockSize, 1, mar->fp) != 1) {
+      if (fread(buf, additionalBlockSize, 1, mar->fp) != 1) {
         return -1;
       }
 
       /* Extract the MAR channel name from the buffer.  For now we
          point to the stack allocated buffer but we strdup this
          if we are within bounds of each field's max length. */
       location = buf;
       len = strlen(location);
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -3085,19 +3085,16 @@ pref("layout.css.scroll-behavior.spring-
 // reduced speed without overshooting.
 // When equal to 1.0, the system is critically-damped; it will reach the target
 // at the greatest speed without overshooting.
 pref("layout.css.scroll-behavior.damping-ratio", "1.0");
 
 // Is support for scroll-snap enabled?
 pref("layout.css.scroll-snap.enabled", true);
 
-// Is support for CSS shape-outside enabled?
-pref("layout.css.shape-outside.enabled", true);
-
 // Is support for document.fonts enabled?
 pref("layout.css.font-loading-api.enabled", true);
 
 // Are inter-character ruby annotations enabled?
 pref("layout.css.ruby.intercharacter.enabled", false);
 
 // Is support for overscroll-behavior enabled?
 pref("layout.css.overscroll-behavior.enabled", true);
--- a/mozglue/build/WindowsDllBlocklist.cpp
+++ b/mozglue/build/WindowsDllBlocklist.cpp
@@ -664,18 +664,17 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
 continue_loading:
 #ifdef DEBUG_very_verbose
     printf_stderr("LdrLoadDll: continuing load... ('%S')\n", moduleFileName->Buffer);
 #endif
 
   // A few DLLs such as xul.dll and nss3.dll get loaded before mozglue's
   // AutoProfilerLabel is initialized, and this is a no-op in those cases. But
   // the vast majority of DLLs do get labelled here.
-  AutoProfilerLabel label("WindowsDllBlocklist::patched_LdrLoadDll", dllName,
-                          __LINE__);
+  AutoProfilerLabel label("WindowsDllBlocklist::patched_LdrLoadDll", dllName);
 
 #ifdef _M_AMD64
   // Prevent the stack walker from suspending this thread when LdrLoadDll
   // holds the RtlLookupFunctionEntry lock.
   AutoSuppressStackWalking suppress;
 #endif
   NTSTATUS ret;
   HANDLE myHandle;
--- a/mozglue/misc/AutoProfilerLabel.cpp
+++ b/mozglue/misc/AutoProfilerLabel.cpp
@@ -15,23 +15,22 @@ void
 RegisterProfilerLabelEnterExit(ProfilerLabelEnter aEnter,
                                ProfilerLabelExit aExit)
 {
   sEnter = aEnter;
   sExit = aExit;
 }
 
 AutoProfilerLabel::AutoProfilerLabel(const char* aLabel,
-                                     const char* aDynamicString,
-                                     uint32_t aLine
+                                     const char* aDynamicString
                                      MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)
 {
   MOZ_GUARD_OBJECT_NOTIFIER_INIT;
 
-  mProfilingStack = sEnter ? sEnter(aLabel, aDynamicString, this, aLine) : nullptr;
+  mProfilingStack = sEnter ? sEnter(aLabel, aDynamicString, this) : nullptr;
 }
 
 AutoProfilerLabel::~AutoProfilerLabel()
 {
   if (sExit && mProfilingStack) {
     sExit(mProfilingStack);
   }
 }
--- a/mozglue/misc/AutoProfilerLabel.h
+++ b/mozglue/misc/AutoProfilerLabel.h
@@ -24,33 +24,31 @@
 // the callbacks provided by the profiler use. (Specifying the category in
 // this file would require #including ProfilingStack.h in mozglue, which we
 // don't want to do.)
 
 class ProfilingStack;
 
 namespace mozilla {
 
-typedef ProfilingStack* (*ProfilerLabelEnter)(const char*, const char*, void*,
-                                           uint32_t);
+typedef ProfilingStack* (*ProfilerLabelEnter)(const char*, const char*, void*);
 typedef void (*ProfilerLabelExit)(ProfilingStack*);
 
 // Register callbacks that do the entry/exit work involving sProfilingStack.
 MFBT_API void RegisterProfilerLabelEnterExit(ProfilerLabelEnter aEnter,
                                              ProfilerLabelExit aExit);
 
 // This #ifdef prevents this AutoProfilerLabel from being defined in libxul,
 // which would conflict with the one in the profiler.
 #ifdef IMPL_MFBT
 
 class MOZ_RAII AutoProfilerLabel
 {
 public:
-  AutoProfilerLabel(const char* aLabel, const char* aDynamicString,
-                    uint32_t aLine
+  AutoProfilerLabel(const char* aLabel, const char* aDynamicString
                     MOZ_GUARD_OBJECT_NOTIFIER_PARAM);
   ~AutoProfilerLabel();
 
 private:
   MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
   ProfilingStack* mProfilingStack;
 };
 
--- a/netwerk/base/nsProtocolProxyService.cpp
+++ b/netwerk/base/nsProtocolProxyService.cpp
@@ -2485,31 +2485,31 @@ nsProtocolProxyService::PruneProxyInfo(c
                 iter = iter->mNext;
             }
         }
         if (!head) {
             return;
         }
     }
 
-    // Now, scan to see if all remaining proxies are disabled.  If so, then
+    // Scan to see if all remaining non-direct proxies are disabled. If so, then
     // we'll just bail and return them all.  Otherwise, we'll go and prune the
     // disabled ones.
 
-    bool allDisabled = true;
+    bool allNonDirectProxiesDisabled = true;
 
     nsProxyInfo *iter;
     for (iter = head; iter; iter = iter->mNext) {
-        if (!IsProxyDisabled(iter)) {
-            allDisabled = false;
+        if (!IsProxyDisabled(iter) && iter->mType != kProxyType_DIRECT) {
+            allNonDirectProxiesDisabled = false;
             break;
         }
     }
 
-    if (allDisabled) {
+    if (allNonDirectProxiesDisabled) {
         LOG(("All proxies are disabled, so trying all again"));
     } else {
         // remove any disabled proxies.
         nsProxyInfo *last = nullptr;
         for (iter = head; iter; ) {
             if (IsProxyDisabled(iter)) {
                 // reject!
                 nsProxyInfo *reject = iter;
--- a/netwerk/protocol/http/nsCORSListenerProxy.cpp
+++ b/netwerk/protocol/http/nsCORSListenerProxy.cpp
@@ -1008,17 +1008,17 @@ nsCORSListenerProxy::UpdateChannel(nsICh
   // Add the Origin header
   nsAutoCString origin;
   rv = nsContentUtils::GetASCIIOrigin(mOriginHeaderPrincipal, origin);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel);
   NS_ENSURE_TRUE(http, NS_ERROR_FAILURE);
 
-  rv = http->SetRequestHeader(NS_LITERAL_CSTRING("Origin"), origin, false);
+  rv = http->SetRequestHeader(nsDependentCString(net::nsHttp::Origin), origin, false);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Make cookie-less if needed. We don't need to do anything here if the
   // channel was opened with AsyncOpen2, since then AsyncOpen2 will take
   // care of the cookie policy for us.
   if (!mWithCredentials &&
       (!loadInfo || !loadInfo->GetEnforceSecurity())) {
     nsLoadFlags flags;
--- a/servo/components/style/properties/longhands/box.mako.rs
+++ b/servo/components/style/properties/longhands/box.mako.rs
@@ -599,40 +599,37 @@
     products="gecko",
     animation_value_type="none",
     spec="https://drafts.csswg.org/css-will-change/#will-change",
 )}
 
 ${helpers.predefined_type(
     "shape-image-threshold", "Opacity", "0.0",
     products="gecko",
-    gecko_pref="layout.css.shape-outside.enabled",
     animation_value_type="ComputedValue",
     flags="APPLIES_TO_FIRST_LETTER",
     spec="https://drafts.csswg.org/css-shapes/#shape-image-threshold-property",
 )}
 
 ${helpers.predefined_type(
     "shape-margin",
     "NonNegativeLengthOrPercentage",
     "computed::NonNegativeLengthOrPercentage::zero()",
     products="gecko",
-    gecko_pref="layout.css.shape-outside.enabled",
     animation_value_type="NonNegativeLengthOrPercentage",
     flags="APPLIES_TO_FIRST_LETTER",
     spec="https://drafts.csswg.org/css-shapes/#shape-margin-property",
 )}
 
 ${helpers.predefined_type(
     "shape-outside",
     "basic_shape::FloatAreaShape",
     "generics::basic_shape::ShapeSource::None",
     products="gecko",
     boxed=True,
-    gecko_pref="layout.css.shape-outside.enabled",
     animation_value_type="ComputedValue",
     flags="APPLIES_TO_FIRST_LETTER",
     spec="https://drafts.csswg.org/css-shapes/#shape-outside-property",
 )}
 
 ${helpers.predefined_type(
     "touch-action",
     "TouchAction",
--- a/servo/components/style/values/computed/length.rs
+++ b/servo/components/style/values/computed/length.rs
@@ -6,17 +6,17 @@
 
 use app_units::Au;
 use ordered_float::NotNan;
 use std::fmt::{self, Write};
 use std::ops::{Add, Neg};
 use style_traits::{CssWriter, ToCss};
 use style_traits::values::specified::AllowedNumericType;
 use super::{Context, Number, Percentage, ToComputedValue};
-use values::{specified, Auto, CSSFloat, Either, Normal};
+use values::{specified, Auto, CSSFloat, Either, Normal, IsAuto};
 use values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero};
 use values::distance::{ComputeSquaredDistance, SquaredDistance};
 use values::generics::NonNegative;
 use values::generics::length::{MaxLength as GenericMaxLength, MozLength as GenericMozLength};
 use values::specified::length::{AbsoluteLength, FontBaseSize, FontRelativeLength};
 use values::specified::length::ViewportPercentageLength;
 
 pub use super::image::Image;
@@ -523,16 +523,23 @@ impl LengthOrPercentageOrAuto {
             &(*other).into(),
         )
     }
 }
 
 /// A wrapper of LengthOrPercentageOrAuto, whose value must be >= 0.
 pub type NonNegativeLengthOrPercentageOrAuto = NonNegative<LengthOrPercentageOrAuto>;
 
+impl IsAuto for NonNegativeLengthOrPercentageOrAuto {
+    #[inline]
+    fn is_auto(&self) -> bool {
+        *self == Self::auto()
+    }
+}
+
 impl NonNegativeLengthOrPercentageOrAuto {
     /// `auto`
     #[inline]
     pub fn auto() -> Self {
         NonNegative(LengthOrPercentageOrAuto::Auto)
     }
 }
 
--- a/servo/components/style/values/generics/background.rs
+++ b/servo/components/style/values/generics/background.rs
@@ -1,36 +1,65 @@
 /* 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/. */
 
 //! Generic types for CSS values related to backgrounds.
 
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+use values::IsAuto;
+
 /// A generic value for the `background-size` property.
 #[derive(
     Animate,
     Clone,
     ComputeSquaredDistance,
     Copy,
     Debug,
     MallocSizeOf,
     PartialEq,
     SpecifiedValueInfo,
     ToAnimatedValue,
     ToAnimatedZero,
     ToComputedValue,
-    ToCss,
 )]
 pub enum BackgroundSize<LengthOrPercentageOrAuto> {
     /// `<width> <height>`
     Explicit {
         /// Explicit width.
         width: LengthOrPercentageOrAuto,
         /// Explicit height.
         height: LengthOrPercentageOrAuto,
     },
     /// `cover`
     #[animation(error)]
     Cover,
     /// `contain`
     #[animation(error)]
     Contain,
 }
+
+impl<LengthOrPercentageOrAuto> ToCss for BackgroundSize<LengthOrPercentageOrAuto>
+where
+    LengthOrPercentageOrAuto: ToCss + IsAuto,
+{
+    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+    where
+        W: Write,
+    {
+        match self {
+            BackgroundSize::Explicit { width, height } => {
+                width.to_css(dest)?;
+                // NOTE(emilio): We should probably simplify all these in case
+                // `width == `height`, but all other browsers agree on only
+                // special-casing `auto`.
+                if !width.is_auto() || !height.is_auto() {
+                    dest.write_str(" ")?;
+                    height.to_css(dest)?;
+                }
+                Ok(())
+            }
+            BackgroundSize::Cover => dest.write_str("cover"),
+            BackgroundSize::Contain => dest.write_str("contain"),
+        }
+    }
+}
--- a/servo/components/style/values/mod.rs
+++ b/servo/components/style/values/mod.rs
@@ -236,16 +236,23 @@ impl KeyframesName {
             KeyframesName::Ident(ref ident) => &ident.0,
             KeyframesName::QuotedString(ref atom) => atom,
         }
     }
 }
 
 impl Eq for KeyframesName {}
 
+/// A trait that returns whether a given type is the `auto` value or not. So far
+/// only needed for background-size serialization, which special-cases `auto`.