Merge mozilla-central to inbound a=merge on a CLOSED TREE
authorCoroiu Cristina <ccoroiu@mozilla.com>
Fri, 02 Nov 2018 07:18:47 +0200
changeset 444051 2be891b25f69091b7c8dd95c8289b87a930e55e4
parent 444025 17f76aad81aa5027af1c769d647dcc1916403c9e (current diff)
parent 444050 b8222faf7d06e64be9af80a60b87f8d2bab30f97 (diff)
child 444052 b537bec9ef64ce6cf60aa1d13110c5a84b780ba0
push id109501
push userccoroiu@mozilla.com
push dateFri, 02 Nov 2018 05:19:20 +0000
treeherdermozilla-inbound@2be891b25f69 [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 mozilla-central to inbound a=merge on a CLOSED TREE
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -175,16 +175,21 @@ export default class AddressForm extends
                                                        saveAddressDefaultChecked;
     }
 
     if (addressPage.addressFields) {
       this.form.dataset.addressFields = addressPage.addressFields;
     } else {
       this.form.dataset.addressFields = "mailing-address tel";
     }
+    if (addressPage.selectedStateKey == "selectedPayerAddress") {
+      this.form.dataset.extraRequiredFields = addressPage.addressFields;
+    } else {
+      this.form.dataset.extraRequiredFields = "";
+    }
     this.formHandler.loadRecord(record);
 
     // Add validation to some address fields
     this.updateRequiredState();
 
     // Show merchant errors for the appropriate address form.
     let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(state,
                                                                      addressPage.selectedStateKey);
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -299,17 +299,17 @@ export default class PaymentDialog exten
   _renderPayerFields(state) {
     let paymentOptions = state.request.paymentOptions;
     let payerRequested = this._isPayerRequested(paymentOptions);
     for (let element of this._payerRelatedEls) {
       element.hidden = !payerRequested;
     }
 
     if (payerRequested) {
-      let fieldNames = new Set(); // default: ["name", "tel", "email"]
+      let fieldNames = new Set();
       if (paymentOptions.requestPayerName) {
         fieldNames.add("name");
       }
       if (paymentOptions.requestPayerEmail) {
         fieldNames.add("email");
       }
       if (paymentOptions.requestPayerPhone) {
         fieldNames.add("tel");
--- a/browser/components/payments/res/unprivileged-fallbacks.js
+++ b/browser/components/payments/res/unprivileged-fallbacks.js
@@ -70,16 +70,17 @@ var PaymentDialogUtils = {
           {
             fieldId: "street-address",
             newLine: true,
           },
           {fieldId: "postal-code"},
           {fieldId: "address-level2"},
         ],
         postalCodePattern: "\\d{5}",
+        countryRequiredFields: ["street-address", "address-level2", "postal-code"],
       };
     }
 
     return {
       addressLevel3Label: "",
       addressLevel2Label: "city",
       addressLevel1Label: country == "US" ? "state" : "province",
       postalCodeLabel: country == "US" ? "zip" : "postalCode",
@@ -89,16 +90,19 @@ var PaymentDialogUtils = {
         {fieldId: "address-level2"},
         {fieldId: "address-level1"},
         {fieldId: "postal-code"},
         {fieldId: "organization"},
       ],
       // The following values come from addressReferences.js and should not be changed.
       /* eslint-disable-next-line max-len */
       postalCodePattern: country == "US" ? "(\\d{5})(?:[ \\-](\\d{4}))?" : "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d",
+      countryRequiredFields: country == "US" || country == "CA" ?
+        ["street-address", "address-level2", "address-level1", "postal-code"] :
+        ["street-address", "address-level2", "postal-code"],
     };
   },
   getDefaultPreferences() {
     let prefValues = {
       saveCreditCardDefaultChecked: false,
       saveAddressDefaultChecked: true,
     };
     return prefValues;
--- a/browser/components/payments/test/browser/browser.ini
+++ b/browser/components/payments/test/browser/browser.ini
@@ -12,16 +12,17 @@ support-files =
 skip-if = verify && debug && os == 'mac'
 [browser_card_edit.js]
 skip-if = debug && (os == 'mac' || os == 'linux') # bug 1465673
 [browser_change_shipping.js]
 [browser_dropdowns.js]
 [browser_host_name.js]
 [browser_onboarding_wizard.js]
 [browser_openPreferences.js]
+[browser_payerRequestedFields.js]
 [browser_payment_completion.js]
 [browser_profile_storage.js]
 [browser_request_serialization.js]
 [browser_request_shipping.js]
 [browser_retry.js]
 [browser_shippingaddresschange_error.js]
 [browser_show_dialog.js]
 skip-if = os == 'win' && debug # bug 1418385
--- a/browser/components/payments/test/browser/browser_address_edit.js
+++ b/browser/components/payments/test/browser/browser_address_edit.js
@@ -466,16 +466,20 @@ add_task(async function test_payer_addre
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
 
 /*
  * Test that we can correctly add an address from a private window
  */
 add_task(async function test_private_persist_addresses() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   let prefilledGuids = await setup();
 
   is((await formAutofillStorage.addresses.getAll()).length, 1,
      "Setup results in 1 stored address at start of test");
 
   await withNewTabInPrivateWindow({
     url: BLANK_PAGE_URL,
   }, async browser => {
@@ -564,17 +568,17 @@ add_task(async function test_private_per
          tempAddress.name.includes(address["family-name"]), "Address.name was computed");
     }, {address: addressToAdd, tempAddressGuid});
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "123",
     });
 
     info("clicking pay");
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
 
     // Verify response has the expected properties
     info("response: " + JSON.stringify(result.response));
     let responseAddress = result.response.shippingAddress;
--- a/browser/components/payments/test/browser/browser_card_edit.js
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -7,16 +7,20 @@ requestLongerTimeout(2);
 async function setup(addresses = [], cards = []) {
   await setupFormAutofillStorage();
   await cleanupFormAutofillStorage();
   let prefilledGuids = await addSampleAddressesAndBasicCard(addresses, cards);
   return prefilledGuids;
 }
 
 async function add_link(aOptions = {}) {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   let tabOpenFn = aOptions.isPrivate ? withNewTabInPrivateWindow : BrowserTestUtils.withNewTab;
   await tabOpenFn({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} =
       await setupPaymentDialog(browser, {
         methodData: [PTU.MethodData.basicCard],
@@ -252,17 +256,17 @@ async function add_link(aOptions = {}) {
       for (let [key, val] of Object.entries(card)) {
         is(savedCard[key], val, "Check " + key);
       }
 
       is(savedCard.billingAddressGUID, savedAddressGUID,
          "The saved card should be associated with the billing address");
     }, aOptions);
 
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
 
     // Verify response has the expected properties
     let expectedDetails = Object.assign({
       "cc-security-code": "123",
--- a/browser/components/payments/test/browser/browser_change_shipping.js
+++ b/browser/components/payments/test/browser/browser_change_shipping.js
@@ -6,16 +6,20 @@ async function setup() {
 
   info("associating the card with the billing address");
   await formAutofillStorage.creditCards.update(prefilledGuids.card1GUID, {
     billingAddressGUID: prefilledGuids.address1GUID,
   }, true);
 }
 
 add_task(async function test_change_shipping() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   await setup();
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} =
       await setupPaymentDialog(browser, {
         methodData: [PTU.MethodData.basicCard],
@@ -103,17 +107,17 @@ add_task(async function test_change_ship
       btn.click();
     });
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "123",
     });
 
     info("clicking pay");
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
     is(result.response.methodName, "basic-card", "Check methodName");
 
     let {shippingAddress} = result.response;
     let expectedAddress = PTU.Addresses.TimBL2;
@@ -244,16 +248,20 @@ add_task(async function test_default_shi
 
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
   await cleanupFormAutofillStorage();
 });
 
 add_task(async function test_no_shippingchange_without_shipping() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   await setup();
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} =
       await setupPaymentDialog(browser, {
         methodData: [PTU.MethodData.basicCard],
@@ -267,17 +275,17 @@ add_task(async function test_no_shipping
     }, PTU.ContentTasks.ensureNoPaymentRequestEvent);
     info("added shipping change handler");
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "456",
     });
 
     info("clicking pay");
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
     is(result.response.methodName, "basic-card", "Check methodName");
 
     let actualShippingAddress = result.response.shippingAddress;
     ok(actualShippingAddress === null,
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_payerRequestedFields.js
@@ -0,0 +1,108 @@
+/* eslint-disable no-shadow */
+
+"use strict";
+
+async function setup() {
+  await setupFormAutofillStorage();
+  await cleanupFormAutofillStorage();
+  // add an address and card to avoid the FTU sequence
+  let prefilledGuids = await addSampleAddressesAndBasicCard(
+    [PTU.Addresses.TimBL], [PTU.BasicCards.JohnDoe]);
+
+  info("associating the card with the billing address");
+  await formAutofillStorage.creditCards.update(prefilledGuids.card1GUID, {
+    billingAddressGUID: prefilledGuids.address1GUID,
+  }, true);
+
+  return prefilledGuids;
+}
+
+/*
+ * Test that the payerRequested* fields are marked as required
+ * on the payer address form but aren't marked as required on
+ * the shipping address form.
+ */
+add_task(async function test_add_link() {
+  await setup();
+
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    let {win, frame} =
+      await setupPaymentDialog(browser, {
+        methodData: [PTU.MethodData.basicCard],
+        details: Object.assign({}, PTU.Details.twoShippingOptions, PTU.Details.total2USD),
+        options: {...PTU.Options.requestShipping, ...PTU.Options.requestPayerNameEmailAndPhone},
+        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+      }
+    );
+
+    await navigateToAddAddressPage(frame, {
+      addLinkSelector: "address-picker.payer-related .add-link",
+      initialPageId: "payment-summary",
+      expectPersist: true,
+    });
+
+    await spawnPaymentDialogTask(frame, async () => {
+      let {
+        PaymentTestUtils,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+      let title = content.document.querySelector("address-form h2");
+      is(title.textContent, "Add Payer Contact", "Page title should be set");
+
+      let saveButton = content.document.querySelector("address-form .save-button");
+      is(saveButton.textContent, "Next", "Save button has the correct label");
+
+      info("check that payer requested fields are marked as required");
+      for (let selector of ["#given-name", "#family-name", "#email", "#tel"]) {
+        let element = content.document.querySelector(selector);
+        ok(element.required, selector + " should be required");
+      }
+
+      let backButton = content.document.querySelector("address-form .back-button");
+      ok(content.isVisible(backButton),
+         "Back button is visible on the payer address page");
+      backButton.click();
+
+      await PaymentTestUtils.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "payment-summary";
+      }, "Switched back to payment-summary from payer address form");
+    });
+
+    await navigateToAddAddressPage(frame, {
+      addLinkSelector: "address-picker .add-link",
+      initialPageId: "payment-summary",
+      expectPersist: true,
+    });
+
+    await spawnPaymentDialogTask(frame, async () => {
+      let {
+        PaymentTestUtils,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+      let title = content.document.querySelector("address-form h2");
+      is(title.textContent, "Add Shipping Address", "Page title should be set");
+
+      let saveButton = content.document.querySelector("address-form .save-button");
+      is(saveButton.textContent, "Next", "Save button has the correct label");
+
+      ok(!content.document.querySelector("#tel").required, "#tel should not be required");
+
+      let backButton = content.document.querySelector("address-form .back-button");
+      ok(content.isVisible(backButton),
+         "Back button is visible on the payer address page");
+      backButton.click();
+
+      await PaymentTestUtils.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "payment-summary";
+      }, "Switched back to payment-summary from payer address form");
+    });
+
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+  });
+  await cleanupFormAutofillStorage();
+});
+
--- a/browser/components/payments/test/browser/browser_payment_completion.js
+++ b/browser/components/payments/test/browser/browser_payment_completion.js
@@ -10,16 +10,20 @@ async function setup() {
   await cleanupFormAutofillStorage();
   let billingAddressGUID = await addAddressRecord(PTU.Addresses.TimBL);
   let card = Object.assign({}, PTU.BasicCards.JohnDoe,
                            { billingAddressGUID });
   await addCardRecord(card);
 }
 
 add_task(async function test_complete_success() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   await setup();
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} =
       await setupPaymentDialog(browser, {
         methodData: [PTU.MethodData.basicCard],
@@ -27,31 +31,35 @@ add_task(async function test_complete_su
         merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
       }
     );
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "123",
     });
 
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let {completeException} = await ContentTask.spawn(browser,
                                                       { result: "success" },
                                                       PTU.ContentTasks.addCompletionHandler);
 
     ok(!completeException, "Expect no exception to be thrown when calling complete()");
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
 
 add_task(async function test_complete_fail() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   await setup();
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} =
       await setupPaymentDialog(browser, {
         methodData: [PTU.MethodData.basicCard],
@@ -60,32 +68,36 @@ add_task(async function test_complete_fa
       }
     );
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "456",
     });
 
     info("clicking pay");
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     info("acknowledging the completion from the merchant page");
     let {completeException} = await ContentTask.spawn(browser,
                                                       { result: "fail" },
                                                       PTU.ContentTasks.addCompletionHandler);
     ok(!completeException, "Expect no exception to be thrown when calling complete()");
 
     ok(!win.closed, "dialog shouldn't be closed yet");
 
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
 
 add_task(async function test_complete_timeout() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   await setup();
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     // timeout the response asap
     Services.prefs.setIntPref(RESPONSE_TIMEOUT_PREF, 60);
 
@@ -97,17 +109,17 @@ add_task(async function test_complete_ti
       }
     );
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "789",
     });
 
     info("clicking pay");
-    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     info("acknowledging the completion from the merchant page after a delay");
     let {completeException} = await ContentTask.spawn(browser,
                                                       { result: "fail", delayMs: 1000 },
                                                       PTU.ContentTasks.addCompletionHandler);
     ok(completeException,
        "Expect an exception to be thrown when calling complete() too late");
 
--- a/browser/components/payments/test/browser/browser_retry.js
+++ b/browser/components/payments/test/browser/browser_retry.js
@@ -9,33 +9,37 @@ async function setup() {
   await cleanupFormAutofillStorage();
   let billingAddressGUID = await addAddressRecord(PTU.Addresses.TimBL);
   let card = Object.assign({}, PTU.BasicCards.JohnDoe,
                            { billingAddressGUID });
   await addCardRecord(card);
 }
 
 add_task(async function test_retry_with_genericError() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   await setup();
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} = await setupPaymentDialog(browser, {
       methodData: [PTU.MethodData.basicCard],
       details: Object.assign({}, PTU.Details.total60USD),
       merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
     });
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "123",
     });
 
     info("clicking the button to try pay the 1st time");
-    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     let retryUpdatePromise = spawnPaymentDialogTask(frame, async function checkDialog() {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       let state = await PTU.DialogContentUtils.waitForState(content, ({request}) => {
         return request.completeStatus === "processing";
@@ -66,18 +70,17 @@ add_task(async function test_retry_with_
                                            delayMs: 1000,
                                            validationErrors: {
                                              error: "My generic error",
                                            },
                                          },
                                          PTU.ContentTasks.addRetryHandler);
 
     await retryUpdatePromise;
-
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     // We can only check the retry response after the closing as it only resolves upon complete.
     let {retryException} = await retryPromise;
     ok(!retryException, "Expect no exception to be thrown when calling retry()");
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
--- a/browser/components/payments/test/browser/browser_show_dialog.js
+++ b/browser/components/payments/test/browser/browser_show_dialog.js
@@ -36,16 +36,20 @@ add_task(async function test_show_manual
     );
 
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
 
 add_task(async function test_show_completePayment() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
   let {address1GUID, card1GUID} = await addSampleAddressesAndBasicCard();
 
   let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
                                           (subject, data) => data == "update");
   info("associating the card with the billing address");
   await formAutofillStorage.creditCards.update(card1GUID, {
     billingAddressGUID: address1GUID,
   }, true);
@@ -67,17 +71,17 @@ add_task(async function test_show_comple
     info("select the shipping address");
     await selectPaymentDialogShippingAddressByCountry(frame, "US");
 
     info("entering CSC");
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "999",
     });
     info("clicking pay");
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
 
     let {shippingAddress} = result.response;
     checkPaymentAddressMatchesStorageAddress(shippingAddress, PTU.Addresses.TimBL, "Shipping");
 
@@ -92,16 +96,21 @@ add_task(async function test_show_comple
 
     is(result.response.shippingOption, "2", "Check shipping option");
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
 
 add_task(async function test_show_completePayment2() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} =
       await setupPaymentDialog(browser, {
         methodData,
         details,
@@ -126,17 +135,17 @@ add_task(async function test_show_comple
     await selectPaymentDialogShippingAddressByCountry(frame, "US");
 
     info("entering CSC");
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "123",
     });
 
     info("clicking pay");
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+    await loginAndCompletePayment(frame);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
 
     is(result.response.shippingOption, "1", "Check shipping option");
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -323,16 +323,22 @@ async function spawnInDialogForMerchantT
     });
 
     await spawnPaymentDialogTask(frame, dialogTaskFn, taskArgs);
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 }
 
+async function loginAndCompletePayment(frame) {
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+  await osKeyStoreLoginShown;
+}
+
 async function setupFormAutofillStorage() {
   await formAutofillStorage.initialize();
 }
 
 function cleanupFormAutofillStorage() {
   formAutofillStorage.addresses.removeAll();
   formAutofillStorage.creditCards.removeAll();
 }
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -55,17 +55,17 @@ function checkAddressForm(customEl, expe
        expectedVal.toString(),
        `Check ${propName}`);
   }
 }
 
 function sendStringAndCheckValidity(element, string, isValid) {
   fillField(element, string);
   ok(element.checkValidity() == isValid,
-     `${element.id} should be ${isValid ? "valid" : "invalid"} (${string})`);
+     `${element.id} should be ${isValid ? "valid" : "invalid"} ("${string}")`);
 }
 
 add_task(async function test_initialState() {
   let form = new AddressForm();
 
   await form.requestStore.setState({
     "address-page": {
       selectedStateKey: ["selectedShippingAddress"],
@@ -273,18 +273,19 @@ add_task(async function test_edit() {
   });
   ok(form.saveButton.disabled, "Save button should be disabled for an empty form");
 
   form.remove();
 });
 
 add_task(async function test_restricted_address_fields() {
   let form = new AddressForm();
+  form.dataset.errorGenericSave = "Generic error";
+  form.dataset.fieldRequiredSymbol = "*";
   form.dataset.nextButtonLabel = "Next";
-  form.dataset.errorGenericSave = "Generic error";
   await form.promiseReady;
   display.appendChild(form);
   await form.requestStore.setState({
     "address-page": {
       addressFields: "name email tel",
       selectedStateKey: ["selectedPayerAddress"],
     },
   });
@@ -307,25 +308,33 @@ add_task(async function test_restricted_
   ok(isHidden(form.form.querySelector("#address-level1")),
      "address-level1 should be hidden");
   ok(isHidden(form.form.querySelector("#postal-code")),
      "postal-code should be hidden");
   ok(isHidden(form.form.querySelector("#country")),
      "country should be hidden");
   ok(!isHidden(form.form.querySelector("#email")),
      "email should be visible");
-  ok(!isHidden(form.form.querySelector("#tel")),
+  let telField = form.form.querySelector("#tel");
+  ok(!isHidden(telField),
      "tel should be visible");
+  let telContainer = telField.closest(`#${telField.id}-container`);
+  ok(telContainer.hasAttribute("required"), "tel container should have required attribute");
+  let telSpan = telContainer.querySelector("span");
+  is(telSpan.getAttribute("fieldRequiredSymbol"), "*",
+     "tel span should have asterisk as fieldRequiredSymbol");
+  is(getComputedStyle(telSpan, "::after").content, "attr(fieldRequiredSymbol)",
+     "Asterisk should be on tel");
 
   fillField(form.form.querySelector("#given-name"), "John");
   fillField(form.form.querySelector("#family-name"), "Smith");
   ok(form.saveButton.disabled, "Save button should be disabled due to empty fields");
   fillField(form.form.querySelector("#email"), "john@example.com");
-  todo(form.saveButton.disabled,
-       "Save button should be disabled due to empty fields - Bug 1483412");
+  ok(form.saveButton.disabled,
+     "Save button should be disabled due to empty fields");
   fillField(form.form.querySelector("#tel"), "+15555555555");
   ok(!form.saveButton.disabled, "Save button should be enabled with all required fields filled");
 
   form.remove();
   await form.requestStore.setState({
     "address-page": {
       selectedStateKey: ["selectedShippingAddress"],
     },
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -446,16 +446,29 @@ this.FormAutofillUtils = {
     if (!this._collators[country]) {
       let dataset = this.getCountryAddressData(country);
       let languages = dataset.languages || [dataset.lang];
       this._collators[country] = languages.map(lang => new Intl.Collator(lang, {sensitivity: "base", ignorePunctuation: true}));
     }
     return this._collators[country];
   },
 
+  // Based on the list of fields abbreviations in
+  // https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata
+  FIELDS_LOOKUP: {
+    N: "name",
+    O: "organization",
+    A: "street-address",
+    S: "address-level1",
+    C: "address-level2",
+    D: "address-level3",
+    Z: "postal-code",
+    n: "newLine",
+  },
+
   /**
    * Parse a country address format string and outputs an array of fields.
    * Spaces, commas, and other literals are ignored in this implementation.
    * For example, format string "%A%n%C, %S" should return:
    * [
    *   {fieldId: "street-address", newLine: true},
    *   {fieldId: "address-level2"},
    *   {fieldId: "address-level1"},
@@ -463,32 +476,20 @@ this.FormAutofillUtils = {
    *
    * @param   {string} fmt Country address format string
    * @returns {array<object>} List of fields
    */
   parseAddressFormat(fmt) {
     if (!fmt) {
       throw new Error("fmt string is missing.");
     }
-    // Based on the list of fields abbreviations in
-    // https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata
-    const fieldsLookup = {
-      N: "name",
-      O: "organization",
-      A: "street-address",
-      S: "address-level1",
-      C: "address-level2",
-      D: "address-level3",
-      Z: "postal-code",
-      n: "newLine",
-    };
 
     return fmt.match(/%[^%]/g).reduce((parsed, part) => {
       // Take the first letter of each segment and try to identify it
-      let fieldId = fieldsLookup[part[1]];
+      let fieldId = this.FIELDS_LOOKUP[part[1]];
       // Early return if cannot identify part.
       if (!fieldId) {
         return parsed;
       }
       // If a new line is detected, add an attribute to the previous field.
       if (fieldId == "newLine") {
         let size = parsed.length;
         if (size) {
@@ -496,16 +497,33 @@ this.FormAutofillUtils = {
         }
         return parsed;
       }
       return parsed.concat({fieldId});
     }, []);
   },
 
   /**
+   * Parse a require string and outputs an array of fields.
+   * Spaces, commas, and other literals are ignored in this implementation.
+   * For example, a require string "ACS" should return:
+   * ["street-address", "address-level2", "address-level1"]
+   *
+   * @param   {string} requireString Country address require string
+   * @returns {array<string>} List of fields
+   */
+  parseRequireString(requireString) {
+    if (!requireString) {
+      throw new Error("requireString string is missing.");
+    }
+
+    return requireString.split("").map(fieldId => this.FIELDS_LOOKUP[fieldId]);
+  },
+
+  /**
    * Use alternative country name list to identify a country code from a
    * specified country name.
    * @param   {string} countryName A country name to be identified
    * @param   {string} [countrySpecified] A country code indicating that we only
    *                                      search its alternative names if specified.
    * @returns {string} The matching country code.
    */
   identifyCountryCode(countryName, countrySpecified) {
@@ -803,26 +821,25 @@ this.FormAutofillUtils = {
    *           {string} postalCodeLabel
    *           {object} fieldsOrder
    *           {string} postalCodePattern
    *         }
    */
   getFormFormat(country) {
     const dataset = this.getCountryAddressData(country);
     return {
-      // Phillipines doesn't specify a sublocality_name_type but
-      // has one referenced in their fmt value.
+      // When particular values are missing for a country, the
+      // data/ZZ value should be used instead.
       addressLevel3Label: dataset.sublocality_name_type || "suburb",
-      // Many locales don't specify a locality_name_type but
-      // have one referenced in their fmt value.
       addressLevel2Label: dataset.locality_name_type || "city",
       addressLevel1Label: dataset.state_name_type || "province",
       postalCodeLabel: dataset.zip_name_type || "postalCode",
-      fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C, %S %Z"),
+      fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"),
       postalCodePattern: dataset.zip,
+      countryRequiredFields: this.parseRequireString(dataset.require || "AC"),
     };
   },
 
   /**
    * Localize "data-localization" or "data-localization-region" attributes.
    * @param {Element} element
    * @param {string} attributeName
    */
--- a/browser/extensions/formautofill/OSKeyStore.jsm
+++ b/browser/extensions/formautofill/OSKeyStore.jsm
@@ -13,16 +13,18 @@ var EXPORTED_SYMBOLS = [
 ];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "nativeOSKeyStore",
                                    "@mozilla.org/security/oskeystore;1", Ci.nsIOSKeyStore);
+XPCOMUtils.defineLazyServiceGetter(this, "osReauthenticator",
+                                   "@mozilla.org/security/osreauthenticator;1", Ci.nsIOSReauthenticator);
 
 // Skip reauth during tests, only works in non-official builds.
 const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
 
 var OSKeyStore = {
   /**
    * On macOS this becomes part of the name label visible on Keychain Acesss as
    * "org.mozilla.nss.keystore.firefox" (where "firefox" is the MOZ_APP_NAME).
@@ -52,40 +54,30 @@ var OSKeyStore = {
    * @returns {boolean} True if there is another login dialog existing and false
    *                    otherwise.
    */
   get isUIBusy() {
     return !!this._pendingUnlockPromise;
   },
 
   /**
-   * If the test pref exist and applicable,
-   * this method will dispatch a observer message and return
-   * to simulate successful reauth, or throw to simulate
-   * failed reauth.
+   * If the test pref exists, this method will dispatch a observer message and
+   * resolves to simulate successful reauth, or rejects to simulate failed reauth.
    *
-   * @returns {boolean} True when reauth should NOT be skipped,
-   *                    false when reauth has been skipped.
-   * @throws            If it needs to simulate reauth login failure.
+   * @returns {Promise<undefined>} Resolves when sucessful login, rejects when
+   *                               login fails.
    */
-  _maybeSkipReauthForTest() {
-    // Don't take test reauth pref in the following configurations.
-    if (nativeOSKeyStore.isNSSKeyStore ||
-        AppConstants.MOZILLA_OFFICIAL ||
-        !this._testReauth) {
-      return true;
-    }
-
+  async _reauthInTests() {
     // Skip this reauth because there is no way to mock the
     // native dialog in the testing environment, for now.
     log.debug("_ensureReauth: _testReauth: ", this._testReauth);
     switch (this._testReauth) {
       case "pass":
         Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "pass");
-        return false;
+        break;
       case "cancel":
         Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "cancel");
         throw new Components.Exception("Simulating user cancelling login dialog", Cr.NS_ERROR_FAILURE);
       default:
         throw new Components.Exception("Unknown test pref value", Cr.NS_ERROR_FAILURE);
     }
   },
 
@@ -94,38 +86,55 @@ var OSKeyStore = {
    * login prompt or do nothing if it's logged in already. If an existing login
    * prompt is already prompted, the result from it will be used instead.
    *
    * Note: This method must set _pendingUnlockPromise before returning the
    * promise (i.e. the first |await|), otherwise we'll risk re-entry.
    * This is why there aren't an |await| in the method. The method is marked as
    * |async| to communicate that it's async.
    *
-   * @param   {boolean} reauth Prompt the login dialog no matter it's logged in
-   *                           or not if it's set to true.
-   * @returns {Promise<boolean>} True if it's logged in or no password is set
-   *                             and false if it's still not logged in (prompt
-   *                             canceled or other error).
+   * @param   {boolean|string} reauth If it's set to true or a string, prompt
+   *                                  the reauth login dialog.
+   *                                  The string will be shown on the native OS
+   *                                  login dialog.
+   * @returns {Promise<boolean>}      True if it's logged in or no password is set
+   *                                  and false if it's still not logged in (prompt
+   *                                  canceled or other error).
    */
   async ensureLoggedIn(reauth = false) {
     if (this._pendingUnlockPromise) {
       log.debug("ensureLoggedIn: Has a pending unlock operation");
       return this._pendingUnlockPromise;
     }
     log.debug("ensureLoggedIn: Creating new pending unlock promise. reauth: ", reauth);
 
-    // TODO: Implementing re-auth by passing this value to the native implementation
-    // in some way. Set this to false for now to ignore the reauth request (bug 1429265).
-    reauth = false;
+    let unlockPromise;
 
-    let unlockPromise = Promise.resolve().then(async () => {
-      if (reauth) {
-        reauth = this._maybeSkipReauthForTest();
-      }
+    // Decides who should handle reauth
+    if (typeof reauth == "boolean" && !reauth) {
+      unlockPromise = Promise.resolve();
+    } else if (!AppConstants.MOZILLA_OFFICIAL && this._testReauth) {
+      unlockPromise = this._reauthInTests();
+    } else if (AppConstants.platform == "win" ||
+               AppConstants.platform == "macosx") {
+      let reauthLabel = typeof reauth == "string" ? reauth : "";
+      // On Windows, this promise rejects when the user cancels login dialog, see bug 1502121.
+      // On macOS this resolves to false, so we would need to check it.
+      unlockPromise = osReauthenticator.asyncReauthenticateUser(reauthLabel)
+        .then(reauthResult => {
+          if (typeof reauthResult == "boolean" && !reauthResult) {
+            throw new Components.Exception("User canceled OS reauth entry", Cr.NS_ERROR_FAILURE);
+          }
+        });
+    } else {
+      log.debug("ensureLoggedIn: Skipping reauth on unsupported platforms");
+      unlockPromise = Promise.resolve();
+    }
 
+    unlockPromise = unlockPromise.then(async () => {
       if (!await nativeOSKeyStore.asyncSecretAvailable(this.STORE_LABEL)) {
         log.debug("ensureLoggedIn: Secret unavailable, attempt to generate new secret.");
         let recoveryPhrase = await nativeOSKeyStore.asyncGenerateSecret(this.STORE_LABEL);
         // TODO We should somehow have a dialog to ask the user to write this down,
         // and another dialog somewhere for the user to restore the secret with it.
         // (Intentionally not printing it out in the console)
         log.debug("ensureLoggedIn: Secret generated. Recovery phrase length: " + recoveryPhrase.length);
       }
@@ -165,20 +174,22 @@ var OSKeyStore = {
   /**
    * Decrypts cipherText.
    *
    * Note: In the event of an rejection, check the result property of the Exception
    *       object. Handles NS_ERROR_ABORT as user has cancelled the action (e.g.,
    *       don't show that dialog), apart from other errors (e.g., gracefully
    *       recover from that and still shows the dialog.)
    *
-   * @param   {string} cipherText Encrypted string including the algorithm details.
-   * @param   {boolean} reauth True if we want to force the prompt to show up
-   *                    even if the user is already logged in.
-   * @returns {Promise<string>} resolves to the decrypted string, or rejects otherwise.
+   * @param   {string}         cipherText Encrypted string including the algorithm details.
+   * @param   {boolean|string} reauth     If it's set to true or a string, prompt
+   *                                      the reauth login dialog.
+   *                                      The string may be shown on the native OS
+   *                                      login dialog.
+   * @returns {Promise<string>}           resolves to the decrypted string, or rejects otherwise.
    */
   async decrypt(cipherText, reauth = false) {
     if (!await this.ensureLoggedIn(reauth)) {
       throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
     }
     let bytes = await nativeOSKeyStore.asyncDecryptBytes(this.STORE_LABEL, cipherText);
     return String.fromCharCode.apply(String, bytes);
   },
@@ -225,24 +236,16 @@ var OSKeyStore = {
   },
 
   /**
    * Remove the store. For tests.
    */
   async cleanup() {
     return nativeOSKeyStore.asyncDeleteSecret(this.STORE_LABEL);
   },
-
-  /**
-   * Check if the implementation is using the NSS key store.
-   * If so, tests will be able to handle the reauth dialog.
-   */
-  get isNSSKeyStore() {
-    return nativeOSKeyStore.isNSSKeyStore;
-  },
 };
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
   return new ConsoleAPI({
     maxLogLevelPref: "extensions.formautofill.loglevel",
     prefix: "OSKeyStore",
   });
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -208,51 +208,69 @@ class EditAddress extends EditAutofillFo
   formatForm(country) {
     const {
       addressLevel3Label,
       addressLevel2Label,
       addressLevel1Label,
       postalCodeLabel,
       fieldsOrder: mailingFieldsOrder,
       postalCodePattern,
+      countryRequiredFields,
     } = this.getFormFormat(country);
     this._elements.addressLevel3Label.dataset.localization = addressLevel3Label;
     this._elements.addressLevel2Label.dataset.localization = addressLevel2Label;
     this._elements.addressLevel1Label.dataset.localization = addressLevel1Label;
     this._elements.postalCodeLabel.dataset.localization = postalCodeLabel;
     let addressFields = this._elements.form.dataset.addressFields;
+    let extraRequiredFields = this._elements.form.dataset.extraRequiredFields;
     let fieldClasses = EditAddress.computeVisibleFields(mailingFieldsOrder, addressFields);
-    this.arrangeFields(fieldClasses);
+    let requiredFields = new Set(countryRequiredFields);
+    if (extraRequiredFields) {
+      for (let extraRequiredField of extraRequiredFields.trim().split(/\s+/)) {
+        requiredFields.add(extraRequiredField);
+      }
+    }
+    this.arrangeFields(fieldClasses, requiredFields);
     this.updatePostalCodeValidation(postalCodePattern);
   }
 
   /**
    * Update address field visibility and order based on libaddressinput data.
    *
    * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties
+   * @param {Set} requiredFields Set of `fieldId` strings that mark which fields are required
    */
-  arrangeFields(fieldsOrder) {
+  arrangeFields(fieldsOrder, requiredFields) {
     let fields = [
       "name",
       "organization",
       "street-address",
       "address-level3",
       "address-level2",
       "address-level1",
       "postal-code",
       "country",
       "tel",
       "email",
     ];
     let inputs = [];
     for (let i = 0; i < fieldsOrder.length; i++) {
       let {fieldId, newLine} = fieldsOrder[i];
+
       let container = this._elements.form.querySelector(`#${fieldId}-container`);
       let containerInputs = [...container.querySelectorAll("input, textarea, select")];
-      containerInputs.forEach(function(input) { input.disabled = false; });
+      containerInputs.forEach(function(input) {
+        input.disabled = false;
+        // libaddressinput doesn't list 'country' or 'name' as required.
+        // The additional-name field should never get marked as required.
+        input.required = (fieldId == "country" ||
+                          fieldId == "name" ||
+                          requiredFields.has(fieldId)) &&
+                         input.id != "additional-name";
+      });
       inputs.push(...containerInputs);
       container.style.display = "flex";
       container.style.order = i;
       container.style.pageBreakAfter = newLine ? "always" : "auto";
       // Remove the field from the list of fields
       fields.splice(fields.indexOf(fieldId), 1);
     }
     for (let i = 0; i < inputs.length; i++) {
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -36,33 +36,33 @@
         <span data-localization="familyName" class="label-text"/>
       </label>
     </div>
     <label id="organization-container" class="container">
       <input id="organization" type="text"/>
       <span data-localization="organization2" class="label-text"/>
     </label>
     <label id="street-address-container" class="container">
-      <textarea id="street-address" rows="3" required="required"/>
+      <textarea id="street-address" rows="3"/>
       <span data-localization="streetAddress" class="label-text"/>
     </label>
     <label id="address-level3-container" class="container">
-      <input id="address-level3" type="text" required="required"/>
+      <input id="address-level3" type="text"/>
       <span class="label-text"/>
     </label>
     <label id="address-level2-container" class="container">
-      <input id="address-level2" type="text" required="required"/>
+      <input id="address-level2" type="text"/>
       <span class="label-text"/>
     </label>
     <label id="address-level1-container" class="container">
-      <input id="address-level1" type="text" required="required"/>
+      <input id="address-level1" type="text"/>
       <span class="label-text"/>
     </label>
     <label id="postal-code-container" class="container">
-      <input id="postal-code" type="text" required="required"/>
+      <input id="postal-code" type="text"/>
       <span class="label-text"/>
     </label>
     <label id="country-container" class="container">
       <select id="country" required="required">
         <option/>
       </select>
       <span data-localization="country" class="label-text"/>
     </label>
--- a/browser/extensions/formautofill/test/browser/browser.ini
+++ b/browser/extensions/formautofill/test/browser/browser.ini
@@ -11,17 +11,16 @@ support-files =
 skip-if = verify
 [browser_autocomplete_marked_back_forward.js]
 [browser_autocomplete_marked_detached_tab.js]
 skip-if = (verify && (os == 'win' || os == 'mac'))
 [browser_check_installed.js]
 [browser_creditCard_doorhanger.js]
 skip-if = (os == "linux") || (os == "mac" && debug) || (os == "win") # bug 1425884
 [browser_creditCard_fill_cancel_login.js]
-skip-if = true # Re-auth is not implemented, cannot cancel OS key store login (bug 1429265)
 [browser_dropdown_layout.js]
 [browser_editAddressDialog.js]
 [browser_editCreditCardDialog.js]
 skip-if = (verify && (os == 'linux'))
 [browser_first_time_use_doorhanger.js]
 skip-if = verify
 [browser_insecure_form.js]
 skip-if = (os == 'linux' && !debug) || (os == 'win') # bug 1456284
--- a/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
@@ -148,29 +148,25 @@ add_task(async function test_hasEditLogi
   await saveCreditCard(TEST_CREDIT_CARD_1);
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await waitForFocusAndFormReady(win);
 
   let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
   let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
   let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd);
-  // let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
+  let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
 
   EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
 
-  // Login dialog should show when trying to edit a credit card record.
-  // TODO: test disabled because re-auth is not implemented yet (bug 1429265).
-  /*
   let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(); // cancel
   EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win);
   await osKeyStoreLoginShown;
   await new Promise(resolve => waitForFocus(resolve, win));
   await new Promise(resolve => executeSoon(resolve));
-  */
 
   // Login is not required for removing credit cards.
   EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
   is(selRecords.length, 0, "Credit card is removed");
 
   // gSubDialog.open should be called when trying to add a credit card,
   // no OS login dialog is required.
--- a/browser/extensions/formautofill/test/fixtures/OSKeyStoreTestUtils.jsm
+++ b/browser/extensions/formautofill/test/fixtures/OSKeyStoreTestUtils.jsm
@@ -3,91 +3,47 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
   "OSKeyStoreTestUtils",
 ];
 
 ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
-// TODO: Consider AppConstants.MOZILLA_OFFICIAL to decide if we could test re-auth (bug 1429265).
-/*
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
-*/
-ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
 ChromeUtils.import("resource://testing-common/TestUtils.jsm");
 
 var OSKeyStoreTestUtils = {
-  /*
   TEST_ONLY_REAUTH: "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin",
-  */
 
   setup() {
-    // TODO: run tests with master password enabled to ensure NSS-implemented
-    // key store prompts on re-auth (bug 1429265)
-    /*
-    LoginTestUtils.masterPassword.enable();
-    */
-
     this.ORIGINAL_STORE_LABEL = OSKeyStore.STORE_LABEL;
     OSKeyStore.STORE_LABEL = "test-" + Math.random().toString(36).substr(2);
   },
 
   async cleanup() {
-    // TODO: run tests with master password enabled to ensure NSS-implemented
-    // key store prompts on re-auth (bug 1429265)
-    /*
-    LoginTestUtils.masterPassword.disable();
-    */
-
     await OSKeyStore.cleanup();
     OSKeyStore.STORE_LABEL = this.ORIGINAL_STORE_LABEL;
   },
 
+  /**
+   * Checks whether or not the test can be run by bypassing
+   * the OS login dialog. We do not want the user to be able to
+   * do so with in official builds.
+   * @returns {boolean} True if the test can be preformed.
+   */
   canTestOSKeyStoreLogin() {
-    // TODO: return true based on whether or not we could test the prompt on
-    // the platform (bug 1429265).
-    /*
-    return OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
-    */
-    return true;
+    return !AppConstants.MOZILLA_OFFICIAL;
   },
 
-  // Wait for the master password dialog to popup and enter the password to log in
-  // if "login" is "true" or dismiss it directly if otherwise.
+  // Wait for the observer message that simulates login success of failure.
   async waitForOSKeyStoreLogin(login = false) {
-    // TODO: Always resolves for now, because we are skipping re-auth on all
-    // platforms (bug 1429265).
-    /*
-    if (OSKeyStore.isNSSKeyStore) {
-      await this.waitForMasterPasswordDialog(login);
-      return;
-    }
-
     const str = login ? "pass" : "cancel";
 
     Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, str);
 
     await TestUtils.topicObserved("oskeystore-testonly-reauth",
       (subject, data) => data == str);
 
     Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, "");
-    */
-  },
-
-  async waitForMasterPasswordDialog(login = false) {
-    let [subject] = await TestUtils.topicObserved("common-dialog-loaded");
-
-    let dialog = subject.Dialog;
-    if (dialog.args.title !== "Password Required") {
-      throw new Error("Incorrect master password dialog title");
-    }
-
-    if (login) {
-      dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
-      dialog.ui.button0.click();
-    } else {
-      dialog.ui.button1.click();
-    }
-    await TestUtils.waitForTick();
   },
 };
--- a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
@@ -1,18 +1,16 @@
 // assert is available to chrome scripts loaded via SpecialPowers.loadChromeScript.
 /* global assert */
 /* eslint-env mozilla/frame-script */
 
 "use strict";
 
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
 ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm");
 
 let {formAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {});
 
 const {ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME} = FormAutofillUtils;
 
 var ParentUtils = {
   async _getRecords(collectionName) {
@@ -221,17 +219,17 @@ addMessageListener("FormAutofillTest:Che
 });
 
 addMessageListener("FormAutofillTest:CleanUpCreditCards", (msg) => {
   ParentUtils.cleanUpCreditCards();
 });
 
 addMessageListener("FormAutofillTest:CanTestOSKeyStoreLogin", (msg) => {
   sendAsyncMessage("FormAutofillTest:CanTestOSKeyStoreLoginResult",
-    {canTest: OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL});
+    {canTest: OSKeyStoreTestUtils.canTestOSKeyStoreLogin()});
 });
 
 addMessageListener("FormAutofillTest:OSKeyStoreLogin", async (msg) => {
   await OSKeyStoreTestUtils.waitForOSKeyStoreLogin(msg.login);
   sendAsyncMessage("FormAutofillTest:OSKeyStoreLoggedIn");
 });
 
 addMessageListener("setup", async () => {
--- a/browser/extensions/formautofill/test/unit/test_osKeyStore.js
+++ b/browser/extensions/formautofill/test/unit/test_osKeyStore.js
@@ -1,143 +1,70 @@
 /**
  * Tests of OSKeyStore.jsm
  */
 
 "use strict";
 
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-ChromeUtils.import("resource://testing-common/MockRegistrar.jsm");
-
 let OSKeyStore;
 add_task(async function setup() {
   ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
 });
 
 // Ensure that the appropriate initialization has happened.
 do_get_profile();
 
-// For NSS key store, mocking out the dialog and control it from here.
-let gMockPrompter = {
-  passwordToTry: "hunter2",
-  resolve: null,
-  login: undefined,
-
-  // This intentionally does not use arrow function syntax to avoid an issue
-  // where in the context of the arrow function, |this != gMockPrompter| due to
-  // how objects get wrapped when going across xpcom boundaries.
-  promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
-    equal(text,
-          "Please enter your master password.",
-          "password prompt text should be as expected");
-    equal(checkMsg, null, "checkMsg should be null");
-    if (this.login) {
-      password.value = this.passwordToTry;
-    }
-    this.resolve();
-    this.resolve = null;
-
-    return this.login;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
-};
-
-// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
-// to call promptPassword. We return the mock one, above.
-let gWindowWatcher = {
-  getNewPrompter: () => gMockPrompter,
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
-};
-
-let nssToken;
-
-const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
-
-async function waitForReauth(login = false) {
-  if (OSKeyStore.isNSSKeyStore) {
-    gMockPrompter.login = login;
-    await new Promise(resolve => { gMockPrompter.resolve = resolve; });
-
-    return;
-  }
-
-  let value = login ? "pass" : "cancel";
-  Services.prefs.setStringPref(TEST_ONLY_REAUTH, value);
-  await TestUtils.topicObserved("oskeystore-testonly-reauth",
-    (subject, data) => data == value);
-}
-
 const testText = "test string";
 let cipherText;
 
 add_task(async function test_encrypt_decrypt() {
   Assert.equal(await OSKeyStore.ensureLoggedIn(), true, "Started logged in.");
 
   cipherText = await OSKeyStore.encrypt(testText);
   Assert.notEqual(testText, cipherText);
 
   let plainText = await OSKeyStore.decrypt(cipherText);
   Assert.equal(testText, plainText);
 });
 
-// TODO: skipped because re-auth is not implemented (bug 1429265).
 add_task(async function test_reauth() {
-  let canTest = OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
+  let canTest = OSKeyStoreTestUtils.canTestOSKeyStoreLogin();
   if (!canTest) {
-    todo_check_false(canTest,
+    todo_check_true(canTest,
       "test_reauth: Cannot test OS key store login on official builds.");
     return;
   }
 
-  if (OSKeyStore.isNSSKeyStore) {
-    let windowWatcherCID;
-    windowWatcherCID =
-      MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
-                             gWindowWatcher);
-    registerCleanupFunction(() => {
-      MockRegistrar.unregister(windowWatcherCID);
-    });
-
-    // If we use the NSS key store implementation test that everything works
-    // when a master password is set.
-    // Set an initial password.
-    let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]
-                    .getService(Ci.nsIPK11TokenDB);
-    nssToken = tokenDB.getInternalKeyToken();
-    nssToken.initPassword("hunter2");
-  }
-
-  let reauthObserved = waitForReauth(false);
+  let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false);
   await new Promise(resolve => TestUtils.executeSoon(resolve));
   try {
     await OSKeyStore.decrypt(cipherText, true);
     throw new Error("Not receiving canceled OS unlock error");
   } catch (ex) {
     Assert.equal(ex.message, "User canceled OS unlock entry");
     Assert.equal(ex.result, Cr.NS_ERROR_ABORT);
   }
   await reauthObserved;
 
-  reauthObserved = waitForReauth(false);
+  reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false);
   await new Promise(resolve => TestUtils.executeSoon(resolve));
   Assert.equal(await OSKeyStore.ensureLoggedIn(true), false, "Reauth cancelled.");
   await reauthObserved;
 
-  reauthObserved = waitForReauth(true);
+  reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
   await new Promise(resolve => TestUtils.executeSoon(resolve));
   let plainText2 = await OSKeyStore.decrypt(cipherText, true);
   await reauthObserved;
   Assert.equal(testText, plainText2);
 
-  reauthObserved = waitForReauth(true);
+  reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
   await new Promise(resolve => TestUtils.executeSoon(resolve));
   Assert.equal(await OSKeyStore.ensureLoggedIn(true), true, "Reauth logged in.");
   await reauthObserved;
-}).skip();
+});
 
 add_task(async function test_decryption_failure() {
   try {
     await OSKeyStore.decrypt("Malformed cipher text");
     throw new Error("Not receiving decryption error");
   } catch (ex) {
     Assert.notEqual(ex.result, Cr.NS_ERROR_ABORT);
   }
--- a/dom/animation/ComputedTimingFunction.cpp
+++ b/dom/animation/ComputedTimingFunction.cpp
@@ -1,17 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ComputedTimingFunction.h"
+#include "mozilla/ServoBindings.h"
 #include "nsAlgorithm.h" // For clamped()
-#include "nsStyleUtil.h"
 
 namespace mozilla {
 
 void
 ComputedTimingFunction::Init(const nsTimingFunction &aFunction)
 {
   const StyleComputedTimingFunction& timing = aFunction.mTiming;
   switch (timing.tag) {
@@ -173,34 +173,42 @@ ComputedTimingFunction::Compare(const Co
   }
 
   return 0;
 }
 
 void
 ComputedTimingFunction::AppendToString(nsAString& aResult) const
 {
+  nsTimingFunction timing;
   switch (mType) {
     case Type::CubicBezier:
-      nsStyleUtil::AppendCubicBezierTimingFunction(mTimingFunction.X1(),
-                                                   mTimingFunction.Y1(),
-                                                   mTimingFunction.X2(),
-                                                   mTimingFunction.Y2(),
-                                                   aResult);
+      timing.mTiming = StyleComputedTimingFunction::CubicBezier(
+        mTimingFunction.X1(),
+        mTimingFunction.Y1(),
+        mTimingFunction.X2(),
+        mTimingFunction.Y2());
       break;
     case Type::Step:
-      nsStyleUtil::AppendStepsTimingFunction(mSteps.mSteps,
-                                             mSteps.mPos,
-                                             aResult);
+      timing.mTiming = StyleComputedTimingFunction::Steps(
+        mSteps.mSteps,
+        mSteps.mPos);
+      break;
+    case Type::Linear:
+    case Type::Ease:
+    case Type::EaseIn:
+    case Type::EaseOut:
+    case Type::EaseInOut:
+      timing.mTiming = StyleComputedTimingFunction::Keyword(
+        static_cast<StyleTimingKeyword>(mType));
       break;
     default:
-      nsStyleUtil::AppendCubicBezierKeywordTimingFunction(
-        StyleTimingKeyword(uint8_t(mType)), aResult);
-      break;
+      MOZ_ASSERT_UNREACHABLE("Unsupported timing type");
   }
+  Servo_SerializeEasing(&timing, &aResult);
 }
 
 /* static */ int32_t
 ComputedTimingFunction::Compare(const Maybe<ComputedTimingFunction>& aLhs,
                                 const Maybe<ComputedTimingFunction>& aRhs)
 {
   // We can't use |operator<| for const Maybe<>& here because
   // 'ease' is prior to 'linear' which is represented by Nothing().
--- a/dom/animation/test/mozilla/test_cubic_bezier_limits.html
+++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
@@ -14,17 +14,17 @@
 }
 </style>
 <div id="log"></div>
 <script>
 'use strict';
 
 // We clamp +infinity or -inifinity value in floating point to
 // maximum floating point value or -maxinum floating point value.
-const max_float = 3.40282e+38;
+const max_float = '3.40282e38';
 
 test(function(t) {
   var div = addDiv(t);
   var anim = div.animate({ }, 100 * MS_PER_SEC);
 
   anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 0)' });
   assert_equals(anim.effect.getComputedTiming().easing,
     'cubic-bezier(0, ' + max_float + ', 0, 0)',
@@ -32,22 +32,22 @@ test(function(t) {
 
   anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, 1e+39)' });
   assert_equals(anim.effect.getComputedTiming().easing,
     'cubic-bezier(0, 0, 0, ' + max_float + ')',
     'y2 control point for effect easing is out of upper boundary');
 
   anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, 0)' });
   assert_equals(anim.effect.getComputedTiming().easing,
-    'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+    'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
     'y1 control point for effect easing is out of lower boundary');
 
   anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, -1e+39)' });
   assert_equals(anim.effect.getComputedTiming().easing,
-    'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+    'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')',
     'y2 control point for effect easing is out of lower boundary');
 
 }, 'Clamp y1 and y2 control point out of boundaries for effect easing' );
 
 test(function(t) {
   var div = addDiv(t);
   var anim = div.animate({ }, 100 * MS_PER_SEC);
 
@@ -58,22 +58,22 @@ test(function(t) {
 
   anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]);
   assert_equals(anim.effect.getKeyframes()[0].easing,
     'cubic-bezier(0, 0, 0, ' + max_float + ')',
     'y2 control point for keyframe easing is out of upper boundary');
 
   anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]);
   assert_equals(anim.effect.getKeyframes()[0].easing,
-    'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+    'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
     'y1 control point for keyframe easing is out of lower boundary');
 
   anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]);
   assert_equals(anim.effect.getKeyframes()[0].easing,
-    'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+    'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')',
     'y2 control point for keyframe easing is out of lower boundary');
 
 }, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' );
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)';
@@ -84,22 +84,22 @@ test(function(t) {
 
   div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)';
   assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
     'cubic-bezier(0, 0, 0, ' + max_float + ')',
     'y2 control point for CSS animation is out of upper boundary');
 
   div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)';
   assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
-    'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+    'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
     'y1 control point for CSS animation is out of lower boundary');
 
   div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)';
   assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
-    'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+    'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')',
     'y2 control point for CSS animation is out of lower boundary');
 
 }, 'Clamp y1 and y2 control point out of boundaries for CSS animation' );
 
 test(function(t) {
   var div = addDiv(t, {'class': 'transition-div'});
 
   div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)';
@@ -119,26 +119,26 @@ test(function(t) {
     'y2 control point for CSS transition on upper boundary');
   div.style.transition = '';
   div.style.marginLeft = '';
 
   div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)';
   flushComputedStyle(div);
   div.style.marginLeft = '0px';
   assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
-    'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+    'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
     'y1 control point for CSS transition on lower boundary');
   div.style.transition = '';
   div.style.marginLeft = '';
 
   div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)';
   flushComputedStyle(div);
   div.style.marginLeft = '0px';
   assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
-    'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+    'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')',
     'y2 control point for CSS transition on lower boundary');
 
 }, 'Clamp y1 and y2 control point out of boundaries for CSS transition' );
 
 test(function(t) {
   var div = addDiv(t);
   var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' });
 
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width; initial-scale=1.0">
+  <title>Dragging the mouse on a scrollbar for a scrollframe inside nested transforms</title>
+  <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+  <script type="application/javascript" src="apz_test_utils.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <script type="text/javascript">
+
+function* test(testDriver) {
+  var scrollableDiv = document.getElementById('scrollable');
+  scrollableDiv.addEventListener('scroll', () => setTimeout(testDriver, 0), {once: true});
+
+  // Scroll down a small amount (10px). The bug in this case is that the
+  // scrollthumb "jumps" by an additional 40 pixels (height of the "gap" div)
+  // and the scrollframe scrolls by a corresponding amount. So after doing this
+  // drag we check the scroll position to make sure it hasn't scrolled by
+  // too much.
+  // Given the scrollable height of 2000px and scrollframe height of 400px,
+  // the scrollthumb should be approximately 80px tall, and dragging it 10px
+  // should scroll approximately 50 pixels. If the bug manifests, it will get
+  // dragged 50px and scroll approximately 250px.
+  var dragFinisher = yield* dragVerticalScrollbar(scrollableDiv, testDriver, 10, 10);
+  if (!dragFinisher) {
+    ok(true, "No scrollbar, can't do this test");
+    return;
+  }
+
+  // the events above might be stuck in APZ input queue for a bit until the
+  // layer is activated, so we wait here until the scroll event listener is
+  // triggered.
+  yield;
+
+  yield* dragFinisher();
+
+  // Flush everything just to be safe
+  yield flushApzRepaints(testDriver);
+
+  // In this case we just want to make sure the scroll position moved from 0
+  // which indicates the thumb dragging worked properly.
+  ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+  </script>
+</head>
+<body>
+    <div id="gap" style="min-height: 40px"></div>
+    <div style="height: 400px; transform: translateZ(0)">
+        <div style="height: 100%; overflow-x: auto; overflow-y: hidden; transform: translateZ(0)">
+            <div id="scrollable" style="display: inline-block; height: 100%; overflow-y: auto; transform: translateZ(0)">
+                <div style="min-height: 2000px">Yay text</div>
+            </div>
+            <div style="display: inline-block; width: 2000px; height: 100%;"></div>
+        </div>
+    </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1490393.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width; initial-scale=1.0">
+  <title>Dragging the mouse on a scrollbar for a scrollframe inside nested transforms</title>
+  <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+  <script type="application/javascript" src="apz_test_utils.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <script type="text/javascript">
+
+function* test(testDriver) {
+  var scrollableDiv = document.getElementById('scrollable');
+  scrollableDiv.addEventListener('scroll', () => setTimeout(testDriver, 0), {once: true});
+
+  // Scroll down a small amount (10px). The bug in this case is that the
+  // scrollthumb "jumps" by an additional 40 pixels (height of the "gap" div)
+  // and the scrollframe scrolls by a corresponding amount. So after doing this
+  // drag we check the scroll position to make sure it hasn't scrolled by
+  // too much.
+  // Given the scrollable height of 2000px and scrollframe height of 400px,
+  // the scrollthumb should be approximately 80px tall, and dragging it 10px
+  // should scroll approximately 50 pixels. If the bug manifests, it will get
+  // dragged 50px and scroll approximately 250px.
+  var dragFinisher = yield* dragVerticalScrollbar(scrollableDiv, testDriver, 10, 10);
+  if (!dragFinisher) {
+    ok(true, "No scrollbar, can't do this test");
+    return;
+  }
+
+  // the events above might be stuck in APZ input queue for a bit until the
+  // layer is activated, so we wait here until the scroll event listener is
+  // triggered.
+  yield;
+
+  yield* dragFinisher();
+
+  // Flush everything just to be safe
+  yield flushApzRepaints(testDriver);
+
+  // In this case we just want to make sure the scroll position moved from 0
+  // which indicates the thumb dragging worked properly.
+  ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+  </script>
+</head>
+<body>
+    <div id="gap" style="min-height: 40px"></div>
+    <div style="height: 400px; transform: translateZ(0)">
+        <div style="height: 100%; opacity: 0.9; will-change: opacity">
+            <div id="scrollable" style="height: 100%; overflow-y: auto; transform: translateZ(0)">
+                <div style="min-height: 2000px">Yay text</div>
+            </div>
+        </div>
+    </div>
+</body>
+</html>
--- a/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
+++ b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
@@ -23,17 +23,20 @@ var subtests = [
   // Test for scrollbar-dragging on a scrollframe that's inactive
   {'file': 'helper_bug1326290.html'},
   // Test for scrollbar-dragging on a scrollframe inside an SVGEffects
   {'file': 'helper_bug1331693.html'},
   // Test for scrollbar-dragging on a transformed scrollframe inside a fixed-pos item
   {'file': 'helper_bug1462961.html'},
   // Scrollbar dragging where we exercise the snapback behaviour by moving the
   // mouse away from the scrollbar during drag
-  {'file': 'helper_scrollbar_snap_bug1501062.html'}
+  {'file': 'helper_scrollbar_snap_bug1501062.html'},
+  // Tests for scrollbar-dragging on scrollframes inside nested transforms
+  {'file': 'helper_bug1490393.html'},
+  {'file': 'helper_bug1490393-2.html'}
 ];
 
 if (isApzEnabled()) {
   SimpleTest.waitForExplicitFinish();
   window.onload = function() {
     runSubtestsSeriallyInFreshWindows(subtests)
     .then(SimpleTest.finish, SimpleTest.finish);
   };
--- a/gfx/layers/wr/StackingContextHelper.cpp
+++ b/gfx/layers/wr/StackingContextHelper.cpp
@@ -18,16 +18,17 @@ StackingContextHelper::StackingContextHe
   , mAffectsClipPositioning(false)
   , mIsPreserve3D(false)
   , mRasterizeLocally(false)
 {
   // mOrigin remains at 0,0
 }
 
 StackingContextHelper::StackingContextHelper(const StackingContextHelper& aParentSC,
+                                             const ActiveScrolledRoot* aAsr,
                                              wr::DisplayListBuilder& aBuilder,
                                              const nsTArray<wr::WrFilterOp>& aFilters,
                                              const LayoutDeviceRect& aBounds,
                                              const gfx::Matrix4x4* aBoundTransform,
                                              const wr::WrAnimationProperty* aAnimation,
                                              const float* aOpacityPtr,
                                              const gfx::Matrix4x4* aTransformPtr,
                                              const gfx::Matrix4x4* aPerspectivePtr,
@@ -75,25 +76,62 @@ StackingContextHelper::StackingContextHe
           aPerspectivePtr,
           wr::ToMixBlendMode(aMixBlendMode),
           aFilters,
           aBackfaceVisible,
           rasterSpace);
 
   mAffectsClipPositioning = mReferenceFrameId.isSome() ||
       (aBounds.TopLeft() != LayoutDevicePoint());
+
+  // If the parent stacking context has a deferred transform item, inherit it
+  // into this stacking context, as long as the ASR hasn't changed. Refer to
+  // the comments on StackingContextHelper::mDeferredTransformItem for an
+  // explanation of what goes in these fields.
+  if (aParentSC.mDeferredTransformItem &&
+      aAsr == (*aParentSC.mDeferredTransformItem)->GetActiveScrolledRoot()) {
+    if (mDeferredTransformItem) {
+      // If we are deferring another transform, put the combined transform from
+      // all the ancestor deferred items into mDeferredAncestorTransform
+      mDeferredAncestorTransform = aParentSC.GetDeferredTransformMatrix();
+    } else {
+      // We are not deferring another transform, so we can just inherit the
+      // parent stacking context's deferred data without any modification.
+      mDeferredTransformItem = aParentSC.mDeferredTransformItem;
+      mDeferredAncestorTransform = aParentSC.mDeferredAncestorTransform;
+    }
+  }
 }
 
 StackingContextHelper::~StackingContextHelper()
 {
   if (mBuilder) {
     mBuilder->PopStackingContext(mReferenceFrameId.isSome());
   }
 }
 
 const Maybe<nsDisplayTransform*>&
 StackingContextHelper::GetDeferredTransformItem() const
 {
   return mDeferredTransformItem;
 }
 
+Maybe<gfx::Matrix4x4>
+StackingContextHelper::GetDeferredTransformMatrix() const
+{
+  if (mDeferredTransformItem) {
+    // See the comments on StackingContextHelper::mDeferredTransformItem for
+    // an explanation of what's stored in mDeferredTransformItem and
+    // mDeferredAncestorTransform. Here we need to return the combined transform
+    // transform from all the deferred ancestors, including
+    // mDeferredTransformItem.
+    gfx::Matrix4x4 result = (*mDeferredTransformItem)->GetTransform().GetMatrix();
+    if (mDeferredAncestorTransform) {
+      result = *mDeferredAncestorTransform * result;
+    }
+    return Some(result);
+  } else {
+    return Nothing();
+  }
+}
+
 } // namespace layers
 } // namespace mozilla
--- a/gfx/layers/wr/StackingContextHelper.h
+++ b/gfx/layers/wr/StackingContextHelper.h
@@ -11,26 +11,30 @@
 #include "mozilla/gfx/MatrixFwd.h"
 #include "mozilla/webrender/WebRenderAPI.h"
 #include "mozilla/webrender/WebRenderTypes.h"
 #include "Units.h"
 
 class nsDisplayTransform;
 
 namespace mozilla {
+
+struct ActiveScrolledRoot;
+
 namespace layers {
 
 /**
  * This is a helper class that pushes/pops a stacking context, and manages
  * some of the coordinate space transformations needed.
  */
 class MOZ_RAII StackingContextHelper
 {
 public:
   StackingContextHelper(const StackingContextHelper& aParentSC,
+                        const ActiveScrolledRoot* aAsr,
                         wr::DisplayListBuilder& aBuilder,
                         const nsTArray<wr::WrFilterOp>& aFilters = nsTArray<wr::WrFilterOp>(),
                         const LayoutDeviceRect& aBounds = LayoutDeviceRect(),
                         const gfx::Matrix4x4* aBoundTransform = nullptr,
                         const wr::WrAnimationProperty* aAnimation = nullptr,
                         const float* aOpacityPtr = nullptr,
                         const gfx::Matrix4x4* aTransformPtr = nullptr,
                         const gfx::Matrix4x4* aPerspectivePtr = nullptr,
@@ -58,16 +62,17 @@ public:
   }
 
   const gfx::Matrix& GetSnappingSurfaceTransform() const
   {
     return mSnappingSurfaceTransform;
   }
 
   const Maybe<nsDisplayTransform*>& GetDeferredTransformItem() const;
+  Maybe<gfx::Matrix4x4> GetDeferredTransformMatrix() const;
 
   bool AffectsClipPositioning() const { return mAffectsClipPositioning; }
   Maybe<wr::WrClipId> ReferenceFrameId() const { return mReferenceFrameId; }
 
 private:
   wr::DisplayListBuilder* mBuilder;
   gfx::Size mScale;
   gfx::Matrix mInheritedTransform;
@@ -75,17 +80,57 @@ private:
   // The "snapping surface" defines the space that we want to snap in.
   // You can think of it as the nearest physical surface.
   // Animated transforms create a new snapping surface, so that changes to their transform don't affect the snapping of their contents.
   // Non-animated transforms do *not* create a new snapping surface,
   // so that for example the existence of a non-animated identity transform does not affect snapping.
   gfx::Matrix mSnappingSurfaceTransform;
   bool mAffectsClipPositioning;
   Maybe<wr::WrClipId> mReferenceFrameId;
+
+  // The deferred transform item is used when building the WebRenderScrollData
+  // structure. The backstory is that APZ needs to know about transforms that
+  // apply to the different APZC instances. Prior to bug 1423370, we would do
+  // this by creating a new WebRenderLayerScrollData for each nsDisplayTransform
+  // item we encountered. However, this was unnecessarily expensive because it
+  // turned out a lot of nsDisplayTransform items didn't have new ASRs defined
+  // as descendants, so we'd create the WebRenderLayerScrollData and send it
+  // over to APZ even though the transform information was not needed in that
+  // case.
+  //
+  // In bug 1423370 and friends, this was optimized by "deferring" a
+  // nsDisplayTransform item when we encountered it during display list
+  // traversal. If we found a descendant of that transform item that had a
+  // new ASR or otherwise was "relevant to APZ", we would then pluck the
+  // transform matrix off the deferred item and put it on the
+  // WebRenderLayerScrollData instance created for that APZ-relevant descendant.
+  //
+  // One complication with this is if there are multiple nsDisplayTransform
+  // items in the ancestor chain for the APZ-relevant item. As we traverse the
+  // display list, we will defer the outermost nsDisplayTransform item, and when
+  // we encounter the next one we will need to merge it with the already-
+  // deferred one somehow. What we do in this case is have mDeferredTransformItem
+  // always point to the "innermost" deferred transform item (i.e. the closest
+  // ancestor nsDisplayTransform item of the item that created this
+  // StackingContextHelper). And then we use mDeferredAncestorTransform to store
+  // the product of all the other transforms that were deferred. As a result,
+  // there is an invariant here that if mDeferredTransformItem is Nothing(),
+  // mDeferredAncestorTransform will also be Nothing(). Note that we
+  // can only do this if the nsDisplayTransform items share the same ASR. If
+  // we are processing an nsDisplayTransform item with a different ASR than the
+  // previously-deferred item, we assume that the previously-deferred transform
+  // will get sent to APZ as part of a separate WebRenderLayerScrollData item,
+  // and so we don't need to bother with any merging. (The merging probably
+  // wouldn't even make sense because the coordinate spaces might be different
+  // in the face of async scrolling). This behaviour of forcing a
+  // WebRenderLayerScrollData item to be generated when the ASR changes is
+  // implemented in WebRenderCommandBuilder::CreateWebRenderCommandsFromDisplayList.
   Maybe<nsDisplayTransform*> mDeferredTransformItem;
+  Maybe<gfx::Matrix4x4> mDeferredAncestorTransform;
+
   bool mIsPreserve3D;
   bool mRasterizeLocally;
 };
 
 } // namespace layers
 } // namespace mozilla
 
 #endif /* GFX_STACKINGCONTEXTHELPER_H */
--- a/gfx/layers/wr/WebRenderCommandBuilder.cpp
+++ b/gfx/layers/wr/WebRenderCommandBuilder.cpp
@@ -1308,17 +1308,17 @@ WebRenderCommandBuilder::BuildWebRenderC
   mLastCanvasDatas.Clear();
   mLastAsr = nullptr;
   mBuilderDumpIndex = 0;
   mContainsSVGGroup = false;
   MOZ_ASSERT(mDumpIndent == 0);
   mClipManager.BeginBuild(mManager, aBuilder);
 
   {
-    StackingContextHelper pageRootSc(sc, aBuilder, aFilters);
+    StackingContextHelper pageRootSc(sc, nullptr, aBuilder, aFilters);
     if (ShouldDumpDisplayList(aDisplayListBuilder)) {
       mBuilderDumpIndex = aBuilder.Dump(mDumpIndent + 1, Some(mBuilderDumpIndex), Nothing());
     }
     CreateWebRenderCommandsFromDisplayList(aDisplayList, nullptr, aDisplayListBuilder,
                                            pageRootSc, aBuilder, aResourceUpdates);
   }
 
   // Make a "root" layer data that has everything else as descendants
@@ -1418,16 +1418,30 @@ WebRenderCommandBuilder::CreateWebRender
       // display item than previously, so we can't squash the display items
       // into the same "layer".
       const ActiveScrolledRoot* asr = item->GetActiveScrolledRoot();
       if (asr != mLastAsr) {
         mLastAsr = asr;
         forceNewLayerData = true;
       }
 
+      // Refer to the comment on StackingContextHelper::mDeferredTransformItem
+      // for an overview of what this is about. This bit of code applies to the
+      // case where we are deferring a transform item, and we then need to defer
+      // another transform with a different ASR. In such a case we cannot just
+      // merge the deferred transforms, but need to force a new
+      // WebRenderLayerScrollData item to flush the old deferred transform, so
+      // that we can then start deferring the new one.
+      if (!forceNewLayerData &&
+          item->GetType() == DisplayItemType::TYPE_TRANSFORM &&
+          aSc.GetDeferredTransformItem() &&
+          (*aSc.GetDeferredTransformItem())->GetActiveScrolledRoot() != asr) {
+        forceNewLayerData = true;
+      }
+
       // If we're going to create a new layer data for this item, stash the
       // ASR so that if we recurse into a sublist they will know where to stop
       // walking up their ASR chain when building scroll metadata.
       if (forceNewLayerData) {
         mAsrStack.push_back(asr);
       }
     }
 
@@ -1470,23 +1484,22 @@ WebRenderCommandBuilder::CreateWebRender
         // Pop the thing we pushed before the recursion, so the topmost item on
         // the stack is enclosing display item's ASR (or the stack is empty)
         mAsrStack.pop_back();
         const ActiveScrolledRoot* stopAtAsr =
             mAsrStack.empty() ? nullptr : mAsrStack.back();
 
         int32_t descendants = mLayerScrollData.size() - layerCountBeforeRecursing;
 
-        // A deferred transform item is a nsDisplayTransform for which we did
-        // not create a dedicated WebRenderLayerScrollData item at the point
-        // that we encountered the item. Instead, we "deferred" the transform
-        // from that item to combine it into the WebRenderLayerScrollData produced
-        // by child display items. However, in the case where we have a child
-        // display item with a different ASR than the nsDisplayTransform item,
-        // we cannot do this, because it will not conform to APZ's expectations
+        // See the comments on StackingContextHelper::mDeferredTransformItem
+        // for an overview of what deferred transforms are.
+        // In the case where we deferred a transform, but have a child display
+        // item with a different ASR than the deferred transform item, we cannot
+        // put the transform on the WebRenderLayerScrollData item for the child.
+        // We cannot do this because it will not conform to APZ's expectations
         // with respect to how the APZ tree ends up structured. In particular,
         // the GetTransformToThis() for the child APZ (which is created for the
         // child item's ASR) will not include the transform when we actually do
         // want it to.
         // When we run into this scenario, we solve it by creating two
         // WebRenderLayerScrollData items; one that just holds the transform,
         // that we deferred, and a child WebRenderLayerScrollData item that
         // holds the scroll metadata for the child's ASR.
@@ -1501,30 +1514,30 @@ WebRenderCommandBuilder::CreateWebRender
           mLayerScrollData.back().Initialize(mManager->GetScrollData(), item,
               descendants, (*deferred)->GetActiveScrolledRoot(), Nothing());
 
           // The above WebRenderLayerScrollData will also be a descendant of
           // the transform-holding WebRenderLayerScrollData we create below.
           descendants++;
 
           // This creates the WebRenderLayerScrollData for the deferred transform
-          // item. This holds the transform matrix. and the remaining ASRs
+          // item. This holds the transform matrix and the remaining ASRs
           // needed to complete the ASR chain (i.e. the ones from the stopAtAsr
           // down to the deferred transform item's ASR, which must be "between"
-          // stopAtAsr and |item|'s ASR in the ASR tree.
+          // stopAtAsr and |item|'s ASR in the ASR tree).
           mLayerScrollData.emplace_back();
           mLayerScrollData.back().Initialize(mManager->GetScrollData(), *deferred,
-              descendants, stopAtAsr, Some((*deferred)->GetTransform().GetMatrix()));
+              descendants, stopAtAsr, aSc.GetDeferredTransformMatrix());
         } else {
           // This is the "simple" case where we don't need to create two
           // WebRenderLayerScrollData items; we can just create one that also
           // holds the deferred transform matrix, if any.
           mLayerScrollData.emplace_back();
           mLayerScrollData.back().Initialize(mManager->GetScrollData(), item,
-              descendants, stopAtAsr, deferred ? Some((*deferred)->GetTransform().GetMatrix()) : Nothing());
+              descendants, stopAtAsr, aSc.GetDeferredTransformMatrix());
         }
       }
     }
   }
 
   mDumpIndent--;
   mClipManager.EndList(aSc);
 }
--- a/gfx/layers/wr/WebRenderScrollData.cpp
+++ b/gfx/layers/wr/WebRenderScrollData.cpp
@@ -74,20 +74,20 @@ WebRenderLayerScrollData::Initialize(Web
         mScrollIds.AppendElement(aOwner.AddMetadata(metadata.ref()));
       } else {
         MOZ_ASSERT_UNREACHABLE("Expected scroll metadata to be available!");
       }
     }
     asr = asr->mParent;
   }
 
-  // aAncestorTransform, if present, is the transform from an ancestor
-  // nsDisplayTransform that was stored on the stacking context in order to
-  // propagate it downwards in the tree. (i.e. |aItem| is a strict descendant of
-  // the nsDisplayTranform which produced aAncestorTransform). We store this
+  // See the comments on StackingContextHelper::mDeferredTransformItem for an
+  // overview of what deferred transforms are.
+  // aAncestorTransform, if present, is the transform from a deferred transform
+  // item that is an ancestor of |aItem|. We store this transform value
   // separately from mTransform because in the case where we have multiple
   // scroll metadata on this layer item, the mAncestorTransform is associated
   // with the "topmost" scroll metadata, and the mTransform is associated with
   // the "bottommost" scroll metadata. The code in
   // WebRenderScrollDataWrapper::GetTransform() is responsible for combining
   // these transforms and exposing them appropriately. Also, we don't save the
   // ancestor transform for thumb layers, because those are a special case in
   // APZ; we need to keep the ancestor transform for the scrollable content that
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -695,17 +695,16 @@ private:
   DECL_GFX_PREF(Live, "layout.display-list.rebuild-frame-limit", LayoutRebuildFrameLimit, uint32_t, 500);
   DECL_GFX_PREF(Live, "layout.display-list.dump",              LayoutDumpDisplayList, bool, false);
   DECL_GFX_PREF(Live, "layout.display-list.dump-content",      LayoutDumpDisplayListContent, bool, false);
   DECL_GFX_PREF(Live, "layout.display-list.dump-parent",       LayoutDumpDisplayListParent, bool, false);
   DECL_GFX_PREF(Live, "layout.display-list.show-rebuild-area", LayoutDisplayListShowArea, bool, false);
   DECL_GFX_PREF(Live, "layout.display-list.flatten-transform", LayoutFlattenTransform, bool, true);
 
   DECL_GFX_PREF(Once, "layout.frame_rate",                     LayoutFrameRate, int32_t, -1);
-  DECL_GFX_PREF(Once, "layout.less-event-region-items",        LessEventRegionItems, bool, true);
   DECL_GFX_PREF(Live, "layout.min-active-layer-size",          LayoutMinActiveLayerSize, int, 64);
   DECL_GFX_PREF(Once, "layout.paint_rects_separately",         LayoutPaintRectsSeparately, bool, true);
 
   // This and code dependent on it should be removed once containerless scrolling looks stable.
   DECL_OVERRIDE_PREF(Live, "layout.scroll.root-frame-containers",   LayoutUseContainersForRootFrames, !OverrideBase_WebRender());
   // This pref is to be set by test code only.
   DECL_GFX_PREF(Live, "layout.scrollbars.always-layerize-track", AlwaysLayerizeScrollbarTrackTestOnly, bool, false);
   DECL_GFX_PREF(Live, "layout.smaller-painted-layers",         LayoutSmallerPaintedLayers, bool, false);
--- a/js/public/TraceLoggerAPI.h
+++ b/js/public/TraceLoggerAPI.h
@@ -13,31 +13,31 @@
 namespace JS {
 #ifdef JS_TRACE_LOGGING
 
 // Begin trace logging events.  This will activate some of the
 // textId's for various events and set the global option
 // JSJITCOMPILER_ENABLE_TRACELOGGER to true.
 // This does nothing except return if the trace logger is already active.
 extern JS_PUBLIC_API(void)
-StartTraceLogger(JSContext *cx);
+StartTraceLogger(JSContext *cx, mozilla::TimeStamp profilerStart);
 
 // Stop trace logging events.  All textId's will be set to false, and the
 // global JSJITCOMPILER_ENABLE_TRACELOGGER will be set to false.
 // This does nothing except return if the trace logger is not active.
 extern JS_PUBLIC_API(void)
 StopTraceLogger(JSContext *cx);
 
 // Clear and free any event data that was recorded by the trace logger.
 extern JS_PUBLIC_API(void)
 ResetTraceLogger(void);
 
 #else
 // Define empty inline functions for when trace logging compilation is not
 // enabled.  TraceLogging.cpp will not be built in that case so we need to
 // provide something for any routines that reference these.
-inline void StartTraceLogger(JSContext *cx) {}
+inline void StartTraceLogger(JSContext *cx, mozilla::TimeStamp profilerStart) {}
 inline void StopTraceLogger(JSContext *cx) {}
 inline void ResetTraceLogger(void) {}
 #endif
 };
 
 #endif /* js_TraceLoggerAPI_h */
--- a/js/src/vm/TraceLogging.cpp
+++ b/js/src/vm/TraceLogging.cpp
@@ -25,69 +25,16 @@
 #include "vm/TraceLoggingGraph.h"
 
 #include "jit/JitFrames-inl.h"
 
 using namespace js;
 
 TraceLoggerThreadState* traceLoggerState = nullptr;
 
-#if defined(MOZ_HAVE_RDTSC)
-
-uint64_t inline rdtsc() {
-    return ReadTimestampCounter();
-}
-
-#elif defined(__powerpc__)
-static __inline__ uint64_t
-rdtsc(void)
-{
-    uint64_t result=0;
-    uint32_t upper, lower,tmp;
-    __asm__ volatile(
-            "0:                  \n"
-            "\tmftbu   %0           \n"
-            "\tmftb    %1           \n"
-            "\tmftbu   %2           \n"
-            "\tcmpw    %2,%0        \n"
-            "\tbne     0b         \n"
-            : "=r"(upper),"=r"(lower),"=r"(tmp)
-            );
-    result = upper;
-    result = result<<32;
-    result = result|lower;
-
-    return result;
-
-}
-#elif defined(__arm__) || defined(__aarch64__)
-
-#include <sys/time.h>
-
-static __inline__ uint64_t
-rdtsc(void)
-{
-    struct timeval tv;
-    gettimeofday(&tv, NULL);
-    uint64_t ret = tv.tv_sec;
-    ret *= 1000000;
-    ret += tv.tv_usec;
-    return ret;
-}
-
-#else
-
-uint64_t inline
-rdtsc(void)
-{
-    return 0;
-}
-
-#endif // defined(MOZ_HAVE_RDTSC)
-
 static bool
 EnsureTraceLoggerState()
 {
     if (MOZ_LIKELY(traceLoggerState)) {
         return true;
     }
 
     traceLoggerState = js_new<TraceLoggerThreadState>();
@@ -170,17 +117,18 @@ TraceLoggerThread::initGraph()
     // is destructed.
     graph.reset(js_new<TraceLoggerGraph>());
     if (!graph.get()) {
         return;
     }
 
     MOZ_ASSERT(traceLoggerState);
     bool graphFile = traceLoggerState->isGraphFileEnabled();
-    uint64_t start = rdtsc() - traceLoggerState->startupTime;
+    double delta = traceLoggerState->getTimeStampOffset(mozilla::TimeStamp::Now());
+    uint64_t start = static_cast<uint64_t>(delta);
     if (!graph->init(start, graphFile)) {
         graph = nullptr;
         return;
     }
 
     if (graphFile) {
         // Report the textIds to the graph.
         for (uint32_t i = 0; i < TraceLogger_TreeItemEnd; i++) {
@@ -298,17 +246,17 @@ TraceLoggerThreadState::enableFrontendLo
     enabledTextIds[TraceLogger_BytecodeFoldConstants] = true;
     enabledTextIds[TraceLogger_BytecodeNameFunctions] = true;
 }
 
 TraceLoggerThread::~TraceLoggerThread()
 {
     if (graph.get()) {
         if (!failed) {
-            graph->log(events);
+            graph->log(events, traceLoggerState->startTime);
         }
         graph = nullptr;
     }
 }
 
 bool
 TraceLoggerThread::enable()
 {
@@ -763,21 +711,21 @@ TraceLoggerThread::log(uint32_t id)
 #endif
 
     MOZ_ASSERT(traceLoggerState);
 
     // We request for 3 items to add, since if we don't have enough room
     // we record the time it took to make more space. To log this information
     // we need 2 extra free entries.
     if (!events.hasSpaceForAdd(3)) {
-        uint64_t start = rdtsc() - traceLoggerState->startupTime;
+        mozilla::TimeStamp start = mozilla::TimeStamp::Now();
 
         if (!events.ensureSpaceBeforeAdd(3)) {
             if (graph.get()) {
-                graph->log(events);
+                graph->log(events, traceLoggerState->startTime);
             }
 
             // The data structures are full, and the graph file is not enabled
             // so we cannot flush to disk.  Trace logging should stop here.
             if (!traceLoggerState->isGraphFileEnabled()) {
                 enabled_ = 0;
                 return;
             }
@@ -793,23 +741,23 @@ TraceLoggerThread::log(uint32_t id)
         // Tracelogger.
         if (graph.get()) {
             MOZ_ASSERT(events.hasSpaceForAdd(2));
             EventEntry& entryStart = events.pushUninitialized();
             entryStart.time = start;
             entryStart.textId = TraceLogger_Internal;
 
             EventEntry& entryStop = events.pushUninitialized();
-            entryStop.time = rdtsc() - traceLoggerState->startupTime;
+            entryStop.time = mozilla::TimeStamp::Now();
             entryStop.textId = TraceLogger_Stop;
         }
 
     }
 
-    uint64_t time = rdtsc() - traceLoggerState->startupTime;
+    mozilla::TimeStamp time = mozilla::TimeStamp::Now();
 
     EventEntry& entry = events.pushUninitialized();
     entry.time = time;
     entry.textId = id;
 }
 
 void TraceLoggerThreadState::clear()
 {
@@ -1013,17 +961,17 @@ TraceLoggerThreadState::init()
     } else {
             mainThreadEnabled = true;
             helperThreadEnabled = true;
             graphEnabled = false;
             graphFileEnabled = false;
             spewErrors = false;
     }
 
-    startupTime = rdtsc();
+    startTime = mozilla::TimeStamp::Now();
 
 #ifdef DEBUG
     initialized = true;
 #endif
 
     return true;
 }
 
@@ -1243,28 +1191,28 @@ TraceLoggerEvent::TraceLoggerEvent(const
 
 JS_PUBLIC_API(void)
 JS::ResetTraceLogger(void)
 {
     js::ResetTraceLogger();
 }
 
 JS_PUBLIC_API(void)
-JS::StartTraceLogger(JSContext *cx)
+JS::StartTraceLogger(JSContext *cx, mozilla::TimeStamp profilerStart)
 {
     if (jit::JitOptions.enableTraceLogger || !traceLoggerState)  {
         return;
     }
 
     LockGuard<Mutex> guard(traceLoggerState->lock);
     traceLoggerState->enableTextIdsForProfiler();
     jit::JitOptions.enableTraceLogger = true;
 
     // Reset the start time to profile start so it aligns with sampling.
-    traceLoggerState->startupTime = rdtsc();
+    traceLoggerState->startTime = profilerStart;
 
     if (cx->traceLogger) {
         cx->traceLogger->enable();
     }
 }
 
 JS_PUBLIC_API(void)
 JS::StopTraceLogger(JSContext *cx)
--- a/js/src/vm/TraceLogging.h
+++ b/js/src/vm/TraceLogging.h
@@ -413,17 +413,22 @@ class TraceLoggerThreadState
     TextIdToPayloadMap textIdPayloads;
     StringHashToDictionaryMap payloadDictionary;
     DictionaryVector dictionaryData;
 
     uint32_t nextTextId;
     uint32_t nextDictionaryId;
 
   public:
-    uint64_t startupTime;
+    mozilla::TimeStamp startTime;
+
+    double getTimeStampOffset(mozilla::TimeStamp time) {
+        mozilla::TimeDuration delta = time - startTime;
+        return delta.ToMicroseconds();
+    }
 
     // Mutex to guard the data structures used to hold the payload data:
     // textIdPayloads, payloadDictionary & dictionaryData.
     Mutex lock;
 
     TraceLoggerThreadState()
       :
 #ifdef DEBUG
@@ -431,17 +436,16 @@ class TraceLoggerThreadState
 #endif
         mainThreadEnabled(false),
         helperThreadEnabled(false),
         graphEnabled(false),
         graphFileEnabled(false),
         spewErrors(false),
         nextTextId(TraceLogger_Last),
         nextDictionaryId(0),
-        startupTime(0),
         lock(js::mutexid::TraceLoggerThreadState)
     { }
 
     bool init();
     ~TraceLoggerThreadState();
 
     void enableDefaultLogging();
     void enableIonLogging();
--- a/js/src/vm/TraceLoggingGraph.cpp
+++ b/js/src/vm/TraceLoggingGraph.cpp
@@ -676,25 +676,28 @@ TraceLoggerGraph::disable(uint64_t times
     while (stack.size() > 1) {
         stopEvent(timestamp);
     }
 
     enabled = false;
 }
 
 void
-TraceLoggerGraph::log(ContinuousSpace<EventEntry>& events)
+TraceLoggerGraph::log(ContinuousSpace<EventEntry>& events, mozilla::TimeStamp startTime)
 {
     for (uint32_t i = 0; i < events.size(); i++) {
+        mozilla::TimeDuration delta = events[i].time-startTime;
+        uint64_t timeOffset = static_cast<uint64_t>(delta.ToMicroseconds());
+
         if (events[i].textId == TraceLogger_Stop) {
-            stopEvent(events[i].time);
+            stopEvent(timeOffset);
         } else if (TLTextIdIsTreeEvent(events[i].textId)) {
-            startEvent(events[i].textId, events[i].time);
+            startEvent(events[i].textId, timeOffset);
         } else {
-            logTimestamp(events[i].textId, events[i].time);
+            logTimestamp(events[i].textId, timeOffset);
         }
     }
 }
 
 void
 TraceLoggerGraph::addTextId(uint32_t id, const char* text)
 {
     mozilla::Maybe<uint32_t> line   = mozilla::Nothing();
--- a/js/src/vm/TraceLoggingGraph.h
+++ b/js/src/vm/TraceLoggingGraph.h
@@ -227,17 +227,18 @@ class TraceLoggerGraph
 
     // Link a textId with a particular text.
     void addTextId(uint32_t id, const char* text);
     void addTextId(uint32_t id, const char* text,
                    mozilla::Maybe<uint32_t>& line,
                    mozilla::Maybe<uint32_t>& column);
 
     // Create a tree out of all the given events.
-    void log(ContinuousSpace<EventEntry>& events);
+    void log(ContinuousSpace<EventEntry>& events,
+             mozilla::TimeStamp startTime);
 
     static size_t treeSizeFlushLimit() {
         // Allow tree size to grow to 100MB.
         return 100 * 1024 * 1024 / sizeof(TreeEntry);
     }
 
     uint32_t nextTextId() { return nextTextId_; }
 
--- a/js/src/vm/TraceLoggingTypes.h
+++ b/js/src/vm/TraceLoggingTypes.h
@@ -309,16 +309,16 @@ class ContinuousSpace {
     size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const {
         return mallocSizeOf(data_);
     }
 };
 
 // The layout of the event log in memory and in the log file.
 // Readable by JS using TypedArrays.
 struct EventEntry {
-    uint64_t time;
+    mozilla::TimeStamp time;
     uint32_t textId;
-    EventEntry(uint64_t time, uint32_t textId)
-      : time(time), textId(textId)
-    { }
+    EventEntry()
+      : textId(0)
+    {}
 };
 
 #endif /* TraceLoggingTypes_h */
--- a/layout/base/nsLayoutDebugger.cpp
+++ b/layout/base/nsLayoutDebugger.cpp
@@ -181,16 +181,27 @@ PrintDisplayItemTo(nsDisplayListBuilder*
         aStream << ",";
       }
       nsDependentAtomString buffer(willChange[i]);
       aStream << NS_LossyConvertUTF16toASCII(buffer).get();
     }
     aStream << ")";
   }
 
+  if (aItem->HasHitTestInfo()) {
+    auto* hitTestInfoItem = static_cast<nsDisplayHitTestInfoItem*>(aItem);
+
+    aStream << nsPrintfCString(" hitTestInfo(0x%x)",
+      hitTestInfoItem->HitTestFlags().serialize());
+
+    nsRect area = hitTestInfoItem->HitTestArea();
+    aStream << nsPrintfCString(" hitTestArea(%d,%d,%d,%d)",
+      area.x, area.y, area.width, area.height);
+  }
+
   // Display item specific debug info
   aItem->WriteDebugInfo(aStream);
 
 #ifdef MOZ_DUMP_PAINTING
   if (aDumpHtml && aItem->Painted()) {
     aStream << "</a>";
   }
 #endif
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -3504,16 +3504,72 @@ GetOrCreateRetainedDisplayListBuilder(ns
       new RetainedDisplayListBuilder(aFrame, nsDisplayListBuilderMode::PAINTING,
                                      aBuildCaret);
     aFrame->SetProperty(RetainedDisplayListBuilder::Cached(), retainedBuilder);
   }
 
   return retainedBuilder;
 }
 
+// #define PRINT_HITTESTINFO_STATS
+#ifdef PRINT_HITTESTINFO_STATS
+void
+PrintHitTestInfoStatsInternal(nsDisplayList& aList,
+                              int& aTotal,
+                              int& aHitTest,
+                              int& aVisible,
+                              int& aSpecial)
+{
+  for (nsDisplayItem* i = aList.GetBottom(); i; i = i->GetAbove()) {
+    aTotal++;
+
+    if (i->GetChildren()) {
+      PrintHitTestInfoStatsInternal(
+        *i->GetChildren(), aTotal, aHitTest, aVisible, aSpecial);
+    }
+
+    if (i->GetType() == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) {
+      aHitTest++;
+
+      const auto& hitTestInfo =
+        static_cast<nsDisplayHitTestInfoItem*>(i)->HitTestFlags();
+
+      if (hitTestInfo.size() > 1) {
+        aSpecial++;
+        continue;
+      }
+
+      if (hitTestInfo == CompositorHitTestVisibleToHit) {
+        aVisible++;
+        continue;
+      }
+
+      aSpecial++;
+    }
+  }
+}
+
+void
+PrintHitTestInfoStats(nsDisplayList& aList)
+{
+  int total = 0;
+  int hitTest = 0;
+  int visible = 0;
+  int special = 0;
+
+  PrintHitTestInfoStatsInternal(
+    aList, total, hitTest, visible, special);
+
+  double ratio = (double)hitTest / (double)total;
+
+  printf("List %p: total items: %d, hit test items: %d, ratio: %f, visible: %d, special: %d\n",
+    &aList, total, hitTest, ratio, visible, special);
+}
+#endif
+
 nsresult
 nsLayoutUtils::PaintFrame(gfxContext* aRenderingContext, nsIFrame* aFrame,
                           const nsRegion& aDirtyRegion, nscolor aBackstop,
                           nsDisplayListBuilderMode aBuilderMode,
                           PaintFrameFlags aFlags)
 {
   AUTO_PROFILER_LABEL("nsLayoutUtils::PaintFrame", GRAPHICS);
   typedef RetainedDisplayListBuilder::PartialUpdateResult PartialUpdateResult;
@@ -3902,16 +3958,22 @@ nsLayoutUtils::PaintFrame(gfxContext* aR
   if (aFlags & PaintFrameFlags::PAINT_COMPRESSED) {
     flags |= nsDisplayList::PAINT_COMPRESSED;
   }
   if (updateState == PartialUpdateResult::NoChange &&
       !aRenderingContext) {
     flags |= nsDisplayList::PAINT_IDENTICAL_DISPLAY_LIST;
   }
 
+#ifdef PRINT_HITTESTINFO_STATS
+  if (XRE_IsContentProcess()) {
+    PrintHitTestInfoStats(list);
+  }
+#endif
+
   TimeStamp paintStart = TimeStamp::Now();
   RefPtr<LayerManager> layerManager
     = list.PaintRoot(&builder, aRenderingContext, flags);
   Telemetry::AccumulateTimeDelta(Telemetry::PAINT_RASTERIZE_TIME,
                                  paintStart);
 
   builder.Check();
 
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -14,16 +14,17 @@
 #include "gfx2DGlue.h"
 #include "gfxUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/ComputedStyle.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/dom/ElementInlines.h"
 #include "mozilla/dom/Selection.h"
 #include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/gfxVars.h"
 #include "mozilla/gfx/PathHelpers.h"
 #include "mozilla/Sprintf.h"
 
 #include "nsCOMPtr.h"
 #include "nsFlexContainerFrame.h"
 #include "nsFrameList.h"
 #include "nsPlaceholderFrame.h"
 #include "nsPluginFrame.h"
@@ -2674,26 +2675,37 @@ ItemParticipatesIn3DContext(nsIFrame* aA
   }
   if (aAncestor == transformFrame) {
     return true;
   }
   return FrameParticipatesIn3DContext(aAncestor, transformFrame);
 }
 
 static void
-WrapSeparatorTransform(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
-                       nsDisplayList* aSource, nsDisplayList* aTarget,
-                       int aIndex) {
-  if (!aSource->IsEmpty()) {
-    nsDisplayTransform *sepIdItem =
-      MakeDisplayItem<nsDisplayTransform>(aBuilder, aFrame, aSource,
-                                        aBuilder->GetVisibleRect(), Matrix4x4(), aIndex);
-    sepIdItem->SetNoExtendContext();
-    aTarget->AppendToTop(sepIdItem);
-  }
+WrapSeparatorTransform(nsDisplayListBuilder* aBuilder,
+                       nsIFrame* aFrame,
+                       nsDisplayList* aNonParticipants,
+                       nsDisplayList* aParticipants,
+                       int aIndex,
+                       nsDisplayItem** aSeparator)
+{
+  if (aNonParticipants->IsEmpty()) {
+    return;
+  }
+
+  nsDisplayTransform* item =
+    MakeDisplayItem<nsDisplayTransform>(aBuilder, aFrame, aNonParticipants,
+      aBuilder->GetVisibleRect(), Matrix4x4(), aIndex);
+  item->SetNoExtendContext();
+
+  if (*aSeparator == nullptr) {
+    *aSeparator = item;
+  }
+
+  aParticipants->AppendToTop(item);
 }
 
 // Try to compute a clip rect to bound the contents of the mask item
 // that will be built for |aMaskedFrame|. If we're not able to compute
 // one, return an empty Maybe.
 // The returned clip rect, if there is one, is relative to |aMaskedFrame|.
 static Maybe<nsRect>
 ComputeClipForMaskItem(nsDisplayListBuilder* aBuilder, nsIFrame* aMaskedFrame)
@@ -2806,16 +2818,70 @@ struct AutoCheckBuilder {
   ~AutoCheckBuilder()
   {
     mBuilder->Check();
   }
 
   nsDisplayListBuilder* mBuilder;
 };
 
+/**
+ * Helper class to track container creation. Stores the first tracked container.
+ * Used to find the innermost container for hit test information, and to notify
+ * callers whether a container item was created or not.
+ */
+struct ContainerTracker
+{
+  void TrackContainer(nsDisplayItem* aContainer)
+  {
+    MOZ_ASSERT(aContainer);
+
+    if (!mContainer) {
+      mContainer = aContainer;
+    }
+
+    mCreatedContainer = true;
+  }
+
+  void ResetCreatedContainer()
+  {
+    mCreatedContainer = false;
+  }
+
+  nsDisplayItem* mContainer = nullptr;
+  bool mCreatedContainer = false;
+};
+
+/**
+ * Adds hit test information |aHitTestInfo| on the container item |aContainer|,
+ * or if the container item is null, creates a separate hit test item that is
+ * added to the bottom of the display list |aList|.
+ */
+static void
+AddHitTestInfo(nsDisplayListBuilder* aBuilder,
+               nsDisplayList* aList,
+               nsDisplayItem* aContainer,
+               nsIFrame* aFrame,
+               mozilla::UniquePtr<HitTestInfo>&& aHitTestInfo)
+{
+  nsDisplayHitTestInfoItem* hitTestItem;
+
+  if (aContainer) {
+    MOZ_ASSERT(aContainer->IsHitTestItem());
+    hitTestItem = static_cast<nsDisplayHitTestInfoItem*>(aContainer);
+    hitTestItem->SetHitTestInfo(std::move(aHitTestInfo));
+  } else {
+    // No container item was created for this frame. Create a separate
+    // nsDisplayCompositorHitTestInfo item instead.
+    hitTestItem = MakeDisplayItem<nsDisplayCompositorHitTestInfo>(
+      aBuilder, aFrame, std::move(aHitTestInfo));
+    aList->AppendToBottom(hitTestItem);
+  }
+}
+
 void
 nsIFrame::BuildDisplayListForStackingContext(nsDisplayListBuilder* aBuilder,
                                              nsDisplayList*        aList,
                                              bool*                 aCreatedContainerItem) {
   AutoCheckBuilder check(aBuilder);
   if (GetStateBits() & NS_FRAME_TOO_DEEP_IN_FRAME_TREE)
     return;
 
@@ -3036,16 +3102,19 @@ nsIFrame::BuildDisplayListForStackingCon
     clipState.Clear();
   }
 
   Maybe<nsRect> clipForMask;
   if (usingMask) {
     clipForMask = ComputeClipForMaskItem(aBuilder, this);
   }
 
+
+  mozilla::UniquePtr<HitTestInfo> hitTestInfo;
+
   nsDisplayListCollection set(aBuilder);
   {
     DisplayListClipState::AutoSaveRestore nestedClipState(aBuilder);
     nsDisplayListBuilder::AutoInTransformSetter
       inTransformSetter(aBuilder, inTransform);
     nsDisplayListBuilder::AutoEnterFilter
       filterASRSetter(aBuilder, usingFilter);
 
@@ -3070,18 +3139,33 @@ nsIFrame::BuildDisplayListForStackingCon
     if (extend3DContext) {
       // Mark these first so MarkAbsoluteFramesForDisplayList knows if we are
       // going to be forced to descend into frames.
       aBuilder->MarkPreserve3DFramesForDisplayList(this);
     }
 
     aBuilder->AdjustWindowDraggingRegion(this);
 
-    aBuilder->BuildCompositorHitTestInfoIfNeeded(this, set.BorderBackground(),
-                                                 true);
+    if (gfxVars::UseWebRender()) {
+      aBuilder->BuildCompositorHitTestInfoIfNeeded(
+        this, set.BorderBackground(), true);
+    } else {
+      CompositorHitTestInfo info = aBuilder->BuildCompositorHitTestInfo()
+                                 ? GetCompositorHitTestInfo(aBuilder)
+                                 : CompositorHitTestInvisibleToHit;
+
+      if (info != CompositorHitTestInvisibleToHit) {
+        // Frame has hit test flags set, initialize the hit test info structure.
+        hitTestInfo = mozilla::MakeUnique<HitTestInfo>(aBuilder, this, info);
+
+        // Let child frames know the current hit test area and hit test flags.
+        aBuilder->SetCompositorHitTestInfo(
+          hitTestInfo->mArea, hitTestInfo->mFlags);
+      }
+    }
 
     MarkAbsoluteFramesForDisplayList(aBuilder);
     aBuilder->Check();
     BuildDisplayList(aBuilder, set);
     aBuilder->Check();
 
     // Blend modes are a real pain for retained display lists. We build a blend
     // container item if the built list contains any blend mode items within
@@ -3174,35 +3258,31 @@ nsIFrame::BuildDisplayListForStackingCon
 #endif
   resultList.AppendToTop(set.Outlines());
   // 8, 9: non-negative z-index children
   resultList.AppendToTop(set.PositionedDescendants());
 
   // Get the ASR to use for the container items that we create here.
   const ActiveScrolledRoot* containerItemASR = contASRTracker.GetContainerASR();
 
-  if (aCreatedContainerItem) {
-    *aCreatedContainerItem = false;
-  }
+  ContainerTracker ct;
 
   /* If adding both a nsDisplayBlendContainer and a nsDisplayBlendMode to the
    * same list, the nsDisplayBlendContainer should be added first. This only
    * happens when the element creating this stacking context has mix-blend-mode
    * and also contains a child which has mix-blend-mode.
    * The nsDisplayBlendContainer must be added to the list first, so it does not
    * isolate the containing element blending as well.
    */
   if (aBuilder->ContainsBlendMode()) {
     DisplayListClipState::AutoSaveRestore blendContainerClipState(aBuilder);
     resultList.AppendToTop(
       nsDisplayBlendContainer::CreateForMixBlendMode(aBuilder, this, &resultList,
                                                      containerItemASR));
-    if (aCreatedContainerItem) {
-      *aCreatedContainerItem = true;
-    }
+    ct.TrackContainer(resultList.GetTop());
   }
 
   /* If there are any SVG effects, wrap the list up in an SVG effects item
    * (which also handles CSS group opacity). Note that we create an SVG effects
    * item even if resultList is empty, since a filter can produce graphical
    * output even if the element being filtered wouldn't otherwise do so.
    */
   if (usingSVGEffects) {
@@ -3215,16 +3295,17 @@ nsIFrame::BuildDisplayListForStackingCon
     // Revert to the post-filter dirty rect.
     aBuilder->SetVisibleRect(visibleRectOutsideSVGEffects);
 
     // Skip all filter effects while generating glyph mask.
     if (usingFilter && !aBuilder->IsForGenerateGlyphMask()) {
       /* List now emptied, so add the new list to the top. */
       resultList.AppendToTop(MakeDisplayItem<nsDisplayFilters>(
         aBuilder, this, &resultList));
+      ct.TrackContainer(resultList.GetTop());
     }
 
     if (usingMask) {
       DisplayListClipState::AutoSaveRestore maskClipState(aBuilder);
       // The mask should move with aBuilder->CurrentActiveScrolledRoot(), so
       // that's the ASR we prefer to use for the mask item. However, we can
       // only do this if the mask if clipped with respect to that ASR, because
       // an item always needs to have finite bounds with respect to its ASR.
@@ -3234,25 +3315,27 @@ nsIFrame::BuildDisplayListForStackingCon
       // the base requirement of the ASR system (that items have finite bounds
       // wrt. their ASR).
       const ActiveScrolledRoot* maskASR = clipForMask.isSome()
                                         ? aBuilder->CurrentActiveScrolledRoot()
                                         : containerItemASR;
       /* List now emptied, so add the new list to the top. */
       resultList.AppendToTop(MakeDisplayItem<nsDisplayMasksAndClipPaths>(
         aBuilder, this, &resultList, maskASR));
-    }
+      ct.TrackContainer(resultList.GetTop());
+    }
+
+    // TODO(miko): We could probably create a wraplist here and avoid creating
+    // it later in |BuildDisplayListForChild()|.
+    ct.ResetCreatedContainer();
 
     // Also add the hoisted scroll info items. We need those for APZ scrolling
     // because nsDisplayMasksAndClipPaths items can't build active layers.
     aBuilder->ExitSVGEffectsContents();
     resultList.AppendToTop(&hoistedScrollInfoItemsStorage);
-    if (aCreatedContainerItem) {
-      *aCreatedContainerItem = false;
-    }
   }
 
   /* If the list is non-empty and there is CSS group opacity without SVG
    * effects, wrap it up in an opacity item.
    */
   if (useOpacity) {
     // Don't clip nsDisplayOpacity items. We clip their descendants instead.
     // The clip we would set on an element with opacity would clip
@@ -3261,19 +3344,17 @@ nsIFrame::BuildDisplayListForStackingCon
     const bool needsActiveOpacityLayer =
       nsDisplayOpacity::NeedsActiveLayer(aBuilder, this);
 
     resultList.AppendToTop(
       MakeDisplayItem<nsDisplayOpacity>(aBuilder, this, &resultList,
                                         containerItemASR,
                                         opacityItemForEventsAndPluginsOnly,
                                         needsActiveOpacityLayer));
-    if (aCreatedContainerItem) {
-      *aCreatedContainerItem = true;
-    }
+    ct.TrackContainer(resultList.GetTop());
   }
 
   /* If we're going to apply a transformation and don't have preserve-3d set, wrap
    * everything in an nsDisplayTransform. If there's nothing in the list, don't add
    * anything.
    *
    * For the preserve-3d case we want to individually wrap every child in the list with
    * a separate nsDisplayTransform instead. When the child is already an nsDisplayTransform,
@@ -3284,33 +3365,44 @@ nsIFrame::BuildDisplayListForStackingCon
    */
   if (isTransformed && extend3DContext) {
     // Install dummy nsDisplayTransform as a leaf containing
     // descendants not participating this 3D rendering context.
     nsDisplayList nonparticipants;
     nsDisplayList participants;
     int index = 1;
 
+    nsDisplayItem* separator = nullptr;
+
     while (nsDisplayItem* item = resultList.RemoveBottom()) {
-      if (ItemParticipatesIn3DContext(this, item) && !item->GetClip().HasClip()) {
+      if (ItemParticipatesIn3DContext(this, item) &&
+          !item->GetClip().HasClip()) {
         // The frame of this item participates the same 3D context.
-        WrapSeparatorTransform(aBuilder, this, &nonparticipants, &participants, index++);
+        WrapSeparatorTransform(
+          aBuilder, this, &nonparticipants, &participants, index++, &separator);
+
         participants.AppendToTop(item);
       } else {
         // The frame of the item doesn't participate the current
         // context, or has no transform.
         //
         // For items participating but not transformed, they are add
         // to nonparticipants to get a separator layer for handling
         // clips, if there is, on an intermediate surface.
         // \see ContainerLayer::DefaultComputeEffectiveTransforms().
         nonparticipants.AppendToTop(item);
       }
     }
-    WrapSeparatorTransform(aBuilder, this, &nonparticipants, &participants, index++);
+    WrapSeparatorTransform(
+      aBuilder, this, &nonparticipants, &participants, index++, &separator);
+
+    if (separator) {
+      ct.TrackContainer(separator);
+    }
+
     resultList.AppendToTop(&participants);
   }
 
   if (isTransformed) {
     if (clipCapturedBy == ContainerItemType::eTransform) {
       // Restore clip state now so nsDisplayTransform is clipped properly.
       clipState.Restore();
     }
@@ -3328,41 +3420,36 @@ nsIFrame::BuildDisplayListForStackingCon
     buildingDisplayList.SetReferenceFrameAndCurrentOffset(outerReferenceFrame,
       GetOffsetToCrossDoc(outerReferenceFrame));
 
     nsDisplayTransform *transformItem =
       MakeDisplayItem<nsDisplayTransform>(aBuilder, this,
                                         &resultList, visibleRect, 0,
                                         allowAsyncAnimation);
     resultList.AppendToTop(transformItem);
+    ct.TrackContainer(transformItem);
 
     if (hasPerspective) {
       if (clipCapturedBy == ContainerItemType::ePerspective) {
         clipState.Restore();
       }
-      resultList.AppendToTop(
-        MakeDisplayItem<nsDisplayPerspective>(
+      resultList.AppendToTop(MakeDisplayItem<nsDisplayPerspective>(
           aBuilder, this, &resultList));
-    }
-
-    if (aCreatedContainerItem) {
-      *aCreatedContainerItem = true;
+      ct.TrackContainer(resultList.GetTop());
     }
   }
 
   if (clipCapturedBy == ContainerItemType::eOwnLayerForTransformWithRoundedClip) {
     clipState.Restore();
     resultList.AppendToTop(
       MakeDisplayItem<nsDisplayOwnLayer>(aBuilder, this, &resultList,
                                        aBuilder->CurrentActiveScrolledRoot(),
                                        nsDisplayOwnLayerFlags::eNone,
                                        ScrollbarData{}, /* aForceActive = */ false));
-    if (aCreatedContainerItem) {
-      *aCreatedContainerItem = true;
-    }
+    ct.TrackContainer(resultList.GetTop());
   }
 
   /* If we have sticky positioning, wrap it in a sticky position item.
    */
   if (useFixedPosition) {
     if (clipCapturedBy == ContainerItemType::eFixedPosition) {
       clipState.Restore();
     }
@@ -3374,19 +3461,17 @@ nsIFrame::BuildDisplayListForStackingCon
     // case where the fixed-pos item is not a "real" fixed-pos item (e.g. it's
     // nested inside a scrolling transform), so we stash that on the display
     // item as well.
     const ActiveScrolledRoot* fixedASR =
       ActiveScrolledRoot::PickAncestor(containerItemASR, aBuilder->CurrentActiveScrolledRoot());
     resultList.AppendToTop(
         MakeDisplayItem<nsDisplayFixedPosition>(aBuilder, this, &resultList,
           fixedASR, containerItemASR));
-    if (aCreatedContainerItem) {
-      *aCreatedContainerItem = true;
-    }
+    ct.TrackContainer(resultList.GetTop());
   } else if (useStickyPosition) {
     // For position:sticky, the clip needs to be applied both to the sticky
     // container item and to the contents. The container item needs the clip
     // because a scrolled clip needs to move independently from the sticky
     // contents, and the contents need the clip so that they have finite
     // clipped bounds with respect to the container item's ASR. The latter is
     // a little tricky in the case where the sticky item has both fixed and
     // non-fixed descendants, because that means that the sticky container
@@ -3396,38 +3481,49 @@ nsIFrame::BuildDisplayListForStackingCon
     // that on the display item as the "container ASR" (i.e. the normal ASR of
     // the container item, excluding the special behaviour induced by fixed
     // descendants).
     const ActiveScrolledRoot* stickyASR =
       ActiveScrolledRoot::PickAncestor(containerItemASR, aBuilder->CurrentActiveScrolledRoot());
     resultList.AppendToTop(
         MakeDisplayItem<nsDisplayStickyPosition>(aBuilder, this, &resultList,
           stickyASR, aBuilder->CurrentActiveScrolledRoot()));
-    if (aCreatedContainerItem) {
-      *aCreatedContainerItem = true;
-    }
+    ct.TrackContainer(resultList.GetTop());
   }
 
   /* If there's blending, wrap up the list in a blend-mode item. Note
    * that opacity can be applied before blending as the blend color is
    * not affected by foreground opacity (only background alpha).
    */
 
   if (useBlendMode) {
     DisplayListClipState::AutoSaveRestore blendModeClipState(aBuilder);
     resultList.AppendToTop(
         MakeDisplayItem<nsDisplayBlendMode>(aBuilder, this, &resultList,
                                           effects->mMixBlendMode,
                                           containerItemASR));
-    if (aCreatedContainerItem) {
-      *aCreatedContainerItem = true;
-    }
-  }
-
-  CreateOwnLayerIfNeeded(aBuilder, &resultList, aCreatedContainerItem);
+    ct.TrackContainer(resultList.GetTop());
+  }
+
+  bool createdOwnLayer = false;
+  CreateOwnLayerIfNeeded(aBuilder, &resultList, &createdOwnLayer);
+  if (createdOwnLayer) {
+    ct.TrackContainer(resultList.GetTop());
+  }
+
+  if (aCreatedContainerItem) {
+    *aCreatedContainerItem = ct.mCreatedContainer;
+  }
+
+  if (hitTestInfo) {
+    // WebRender support is not yet implemented.
+    MOZ_ASSERT(!gfxVars::UseWebRender());
+    AddHitTestInfo(
+      aBuilder, &resultList, ct.mContainer, this, std::move(hitTestInfo));
+  }
 
   aList->AppendToTop(&resultList);
 }
 
 static nsDisplayItem*
 WrapInWrapList(nsDisplayListBuilder* aBuilder,
                nsIFrame* aFrame, nsDisplayList* aList,
                const ActiveScrolledRoot* aContainerASR,
@@ -11261,16 +11357,41 @@ nsIFrame::AddSizeOfExcludingThisForTree(
   while (!iter.IsDone()) {
     for (const nsIFrame* f : iter.CurrentList()) {
       f->AddSizeOfExcludingThisForTree(aSizes);
     }
     iter.Next();
   }
 }
 
+nsRect
+nsIFrame::GetCompositorHitTestArea(nsDisplayListBuilder* aBuilder)
+{
+  nsRect area;
+
+  nsIScrollableFrame* scrollFrame =
+    nsLayoutUtils::GetScrollableFrameFor(this);
+  if (scrollFrame) {
+    // If the frame is content of a scrollframe, then we need to pick up the
+    // area corresponding to the overflow rect as well. Otherwise the parts of
+    // the overflow that are not occupied by descendants get skipped and the
+    // APZ code sends touch events to the content underneath instead.
+    // See https://bugzilla.mozilla.org/show_bug.cgi?id=1127773#c15.
+    area = GetScrollableOverflowRect();
+  } else {
+    area = nsRect(nsPoint(0, 0), GetSize());
+  }
+
+  if (!area.IsEmpty()) {
+    return area + aBuilder->ToReferenceFrame(this);
+  }
+
+  return area;
+}
+
 CompositorHitTestInfo
 nsIFrame::GetCompositorHitTestInfo(nsDisplayListBuilder* aBuilder)
 {
   CompositorHitTestInfo result = CompositorHitTestInvisibleToHit;
 
   if (aBuilder->IsInsidePointerEventsNoneDoc()) {
     // Somewhere up the parent document chain is a subdocument with pointer-
     // events:none set on it.
@@ -11315,20 +11436,18 @@ nsIFrame::GetCompositorHitTestInfo(nsDis
     docShell = PresShell()->GetDocument()->GetDocShell();
   }
   if (dom::TouchEvent::PrefEnabled(docShell)) {
     // Inherit the touch-action flags from the parent, if there is one. We do this
     // because of how the touch-action on a frame combines the touch-action from
     // ancestor DOM elements. Refer to the documentation in TouchActionHelper.cpp
     // for details; this code is meant to be equivalent to that code, but woven
     // into the top-down recursive display list building process.
-    CompositorHitTestInfo inheritedTouchAction = CompositorHitTestInvisibleToHit;
-    if (nsDisplayCompositorHitTestInfo* parentInfo = aBuilder->GetCompositorHitTestInfo()) {
-      inheritedTouchAction = parentInfo->HitTestInfo() & CompositorHitTestTouchActionMask;
-    }
+    CompositorHitTestInfo inheritedTouchAction =
+      aBuilder->GetHitTestInfo() & CompositorHitTestTouchActionMask;
 
     nsIFrame* touchActionFrame = this;
     if (nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetScrollableFrameFor(this)) {
       touchActionFrame = do_QueryFrame(scrollFrame);
       // On scrollframes, stop inheriting the pan-x and pan-y flags; instead,
       // reset them back to zero to allow panning on the scrollframe unless we
       // encounter an element that disables it that's inside the scrollframe.
       // This is equivalent to the |considerPanning| variable in
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -3681,29 +3681,32 @@ ScrollFrameHelper::BuildDisplayList(nsDi
     if (mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow()) {
       asrSetter.EnterScrollFrame(sf);
     }
 
     if (mIsScrollableLayerInRootContainer && isRcdRsf) {
       aBuilder->SetActiveScrolledRootForRootScrollframe(aBuilder->CurrentActiveScrolledRoot());
     }
 
-    if (mWillBuildScrollableLayer) {
+    if (mWillBuildScrollableLayer && aBuilder->BuildCompositorHitTestInfo()) {
       // Create a hit test info item for the scrolled content that's not
       // clipped to the displayport. This ensures that within the bounds
       // of the scroll frame, the scrolled content is always hit, even
       // if we are checkerboarding.
-      if (aBuilder->BuildCompositorHitTestInfo()) {
-        CompositorHitTestInfo info = mScrolledFrame->GetCompositorHitTestInfo(aBuilder);
-        if (info != CompositorHitTestInvisibleToHit) {
-          nsDisplayCompositorHitTestInfo* hitInfo =
-              MakeDisplayItem<nsDisplayCompositorHitTestInfo>(aBuilder, mScrolledFrame, info, 1);
-          aBuilder->SetCompositorHitTestInfo(hitInfo);
-          scrolledContent.BorderBackground()->AppendToTop(hitInfo);
-        }
+      CompositorHitTestInfo info =
+        mScrolledFrame->GetCompositorHitTestInfo(aBuilder);
+
+      if (info != CompositorHitTestInvisibleToHit) {
+        auto* hitInfo = MakeDisplayItem<nsDisplayCompositorHitTestInfo>(
+          aBuilder, mScrolledFrame, info, 1);
+
+        aBuilder->SetCompositorHitTestInfo(
+          hitInfo->HitTestArea(), hitInfo->HitTestFlags());
+
+        scrolledContent.BorderBackground()->AppendToTop(hitInfo);
       }
     }
 
     {
       // Clip our contents to the unsnapped scrolled rect. This makes sure that
       // we don't have display items over the subpixel seam at the edge of the
       // scrolled area.
       DisplayListClipState::AutoSaveRestore scrolledRectClipState(aBuilder);
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -4129,16 +4129,21 @@ public:
 
   bool HasOverrideDirtyRegion() { return mHasOverrideDirtyRegion; }
   void SetHasOverrideDirtyRegion(bool aHasDirtyRegion) { mHasOverrideDirtyRegion = aHasDirtyRegion; }
 
   bool MayHaveWillChangeBudget() { return mMayHaveWillChangeBudget; }
   void SetMayHaveWillChangeBudget(bool aHasBudget) { mMayHaveWillChangeBudget = aHasBudget; }
 
   /**
+   * Returns the hit test area of the frame.
+   */
+  nsRect GetCompositorHitTestArea(nsDisplayListBuilder* aBuilder);
+
+  /**
    * Returns the set of flags indicating the properties of the frame that the
    * compositor might care about for hit-testing purposes. Note that this function
    * must be called during Gecko display list construction time (i.e while the
    * frame tree is being traversed) because that is when the display list builder
    * has the necessary state set up correctly.
    */
   mozilla::gfx::CompositorHitTestInfo GetCompositorHitTestInfo(nsDisplayListBuilder* aBuilder);
 
--- a/layout/painting/FrameLayerBuilder.cpp
+++ b/layout/painting/FrameLayerBuilder.cpp
@@ -201,87 +201,16 @@ AddMarkerIfNeeded(nsDisplayItem* aItem, 
       MOZ_ASSERT_UNREACHABLE("Invalid display item type!");
       break;
   }
 
   aMarkers.emplace_back(aItem, marker);
   return true;
 }
 
-class FLBDisplayItemIterator : protected FlattenedDisplayItemIterator
-{
-public:
-  FLBDisplayItemIterator(nsDisplayListBuilder* aBuilder,
-                         nsDisplayList* aList,
-                         ContainerState* aState)
-    : FlattenedDisplayItemIterator(aBuilder, aList, false)
-    , mState(aState)
-    , mStoreMarker(false)
-  {
-    MOZ_ASSERT(mState);
-    ResolveFlattening();
-  }
-
-  DisplayItemEntry GetNextEntry()
-  {
-    if (!mMarkers.empty()) {
-      DisplayItemEntry entry = mMarkers.front();
-      mMarkers.pop_front();
-      return entry;
-    }
-
-    nsDisplayItem* next = GetNext();
-    return DisplayItemEntry{ next, DisplayItemEntryType::ITEM };
-  }
-
-  nsDisplayItem* GetNext();
-
-  bool HasNext() const
-  {
-    return FlattenedDisplayItemIterator::HasNext() || !mMarkers.empty();
-  }
-
-  nsDisplayItem* PeekNext() { return mNext; }
-
-private:
-  bool ShouldFlattenNextItem() override;
-
-  void StartNested(nsDisplayItem* aItem) override
-  {
-    if (!mStoreMarker) {
-      return;
-    }
-
-    if (AddMarkerIfNeeded<MarkerType::StartMarker>(aItem, mMarkers)) {
-      mActiveMarkers.AppendElement(aItem);
-    }
-
-    mStoreMarker = false;
-  }
-
-  void EndNested(nsDisplayItem* aItem) override
-  {
-    if (mActiveMarkers.IsEmpty() || mActiveMarkers.LastElement() != aItem) {
-      // Do not emit an end marker if this item did not emit a start marker.
-      return;
-    }
-
-    if (AddMarkerIfNeeded<MarkerType::EndMarker>(aItem, mMarkers)) {
-      mActiveMarkers.RemoveLastElement();
-    }
-  }
-
-  bool NextItemWantsInactiveLayer();
-
-  std::deque<DisplayItemEntry> mMarkers;
-  AutoTArray<nsDisplayItem*, 4> mActiveMarkers;
-  ContainerState* mState;
-  bool mStoreMarker;
-};
-
 DisplayItemData::DisplayItemData(LayerManagerData* aParent,
                                  uint32_t aKey,
                                  Layer* aLayer,
                                  nsIFrame* aFrame)
 
   : mRefCnt(0)
   , mParent(aParent)
   , mLayer(aLayer)
@@ -678,18 +607,18 @@ public:
   /**
    * A region including the horizontal pan, vertical pan, and no action regions.
    */
   nsRegion CombinedTouchActionRegion();
 
   /**
    * Add the given hit test info to the hit regions for this PaintedLayer.
    */
-  void AccumulateHitTestInfo(ContainerState* aState,
-                             nsDisplayCompositorHitTestInfo* aItem,
+  void AccumulateHitTestItem(ContainerState* aState,
+                             nsDisplayItem* aItem,
                              TransformClipNode* aTransform);
 
   /**
    * If this represents only a nsDisplayImage, and the image type supports being
    * optimized to an ImageLayer, returns true.
    */
   bool CanOptimizeToImageLayer(nsDisplayListBuilder* aBuilder);
 
@@ -1293,16 +1222,17 @@ public:
     , mContainerLayer(aContainerLayer)
     , mContainerBounds(aContainerBounds)
     , mContainerASR(aContainerASR)
     , mContainerScrollMetadataASR(aContainerScrollMetadataASR)
     , mContainerCompositorASR(aContainerCompositorASR)
     , mParameters(aParameters)
     , mPaintedLayerDataTree(*this, aBackgroundColor)
     , mLastDisplayPortAGR(nullptr)
+    , mContainerItem(aContainerItem)
   {
     nsPresContext* presContext = aContainerFrame->PresContext();
     mAppUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
     mContainerReferenceFrame = const_cast<nsIFrame*>(
       aContainerItem ? aContainerItem->ReferenceFrameForChildren()
                      : mBuilder->FindReferenceFrameFor(mContainerFrame));
     bool isAtRoot = !aContainerItem ||
                     (aContainerItem->Frame() == mBuilder->RootReferenceFrame());
@@ -1721,16 +1651,18 @@ protected:
 
   nsDataHashtable<nsGenericHashKey<MaskLayerKey>, RefPtr<ImageLayer>>
     mRecycledMaskImageLayers;
   // Keep display port of AGR to avoid wasting time on doing the same
   // thing repeatly.
   AnimatedGeometryRoot* mLastDisplayPortAGR;
   nsRect mLastDisplayPortRect;
 
+  nsDisplayItem* mContainerItem;
+
   // Cache ScrollMetadata so it doesn't need recomputed if the ASR and clip are
   // unchanged. If mASR == nullptr then mMetadata is not valid.
   struct CachedScrollMetadata
   {
     const ActiveScrolledRoot* mASR;
     const DisplayItemClip* mClip;
     Maybe<ScrollMetadata> mMetadata;
 
@@ -1738,105 +1670,185 @@ protected:
       : mASR(nullptr)
       , mClip(nullptr)
     {
     }
   };
   CachedScrollMetadata mCachedScrollMetadata;
 };
 
-nsDisplayItem*
-FLBDisplayItemIterator::GetNext()
-{
-  // This function is only supposed to be called if there are no markers set.
-  // Breaking this invariant can potentially break effect flattening and/or
-  // display item merging.
-  MOZ_ASSERT(mMarkers.empty());
-
-  nsDisplayItem* next = mNext;
-
-  // Advance mNext to the following item
-  if (next) {
-    nsDisplayItem* peek = next->GetAbove();
-
-    // Peek ahead to the next item and see if it can be merged with the
-    // current item.
-    if (peek && next->CanMerge(peek)) {
-      // Create a list of consecutive items that can be merged together.
-      AutoTArray<nsDisplayItem*, 2> mergedItems{ next, peek };
-      while ((peek = peek->GetAbove())) {
-        if (!next->CanMerge(peek)) {
-          break;
-        }
-
-        mergedItems.AppendElement(peek);
-      }
-
-      // We have items that can be merged together.
-      // Merge them into a temporary item and process that item immediately.
-      MOZ_ASSERT(mergedItems.Length() > 1);
-      next = mState->mBuilder->MergeItems(mergedItems);
-    }
-
-    // |mNext| is either the first item that could not be merged with |next|,
-    // or a nullptr.
-    mNext = peek;
+class FLBDisplayItemIterator : protected FlattenedDisplayItemIterator
+{
+public:
+  FLBDisplayItemIterator(nsDisplayListBuilder* aBuilder,
+                         nsDisplayList* aList,
+                         ContainerState* aState)
+    : FlattenedDisplayItemIterator(aBuilder, aList, false)
+    , mState(aState)
+    , mAddingEffectMarker(false)
+  {
+    MOZ_ASSERT(mState);
+
+    if (aState->mContainerItem) {
+      // Add container item hit test information for processing, if needed.
+      AddHitTestMarker(aState->mContainerItem);
+    }
 
     ResolveFlattening();
   }
 
-  return next;
-}
-
-bool
-FLBDisplayItemIterator::NextItemWantsInactiveLayer()
-{
-  LayerState layerState = mNext->GetLayerState(
-    mState->mBuilder, mState->mManager, mState->mParameters);
-
-  return layerState == LayerState::LAYER_INACTIVE;
-}
-
-bool
-FLBDisplayItemIterator::ShouldFlattenNextItem()
-{
-  if (!mNext) {
-    return false;
-  }
-
-  if (!mNext->ShouldFlattenAway(mBuilder)) {
-    return false;
-  }
-
-  const DisplayItemType type = mNext->GetType();
-  if (type != DisplayItemType::TYPE_OPACITY &&
-      type != DisplayItemType::TYPE_TRANSFORM) {
+  void AddHitTestMarker(nsDisplayItem* aItem)
+  {
+    if (aItem->HasHitTestInfo()) {
+      mMarkers.emplace_back(aItem, DisplayItemEntryType::HIT_TEST_INFO);
+    }
+  }
+
+  DisplayItemEntry GetNextEntry()
+  {
+    if (!mMarkers.empty()) {
+      DisplayItemEntry entry = mMarkers.front();
+      mMarkers.pop_front();
+      return entry;
+    }
+
+    nsDisplayItem* next = GetNext();
+    return DisplayItemEntry{ next, DisplayItemEntryType::ITEM };
+  }
+
+  nsDisplayItem* GetNext()
+  {
+    // This function is only supposed to be called if there are no markers set.
+    // Breaking this invariant can potentially break effect flattening and/or
+    // display item merging.
+    MOZ_ASSERT(mMarkers.empty());
+
+    nsDisplayItem* next = mNext;
+
+    // Advance mNext to the following item
+    if (next) {
+      nsDisplayItem* peek = next->GetAbove();
+
+      // Peek ahead to the next item and see if it can be merged with the
+      // current item.
+      if (peek && next->CanMerge(peek)) {
+        // Create a list of consecutive items that can be merged together.
+        AutoTArray<nsDisplayItem*, 2> mergedItems{ next, peek };
+        while ((peek = peek->GetAbove())) {
+          if (!next->CanMerge(peek)) {
+            break;
+          }
+
+          mergedItems.AppendElement(peek);
+        }
+
+        // We have items that can be merged together.
+        // Merge them into a temporary item and process that item immediately.
+        MOZ_ASSERT(mergedItems.Length() > 1);
+        next = mState->mBuilder->MergeItems(mergedItems);
+      }
+
+      // |mNext| is either the first item that could not be merged with |next|,
+      // or a nullptr.
+      mNext = peek;
+
+      ResolveFlattening();
+    }
+
+    return next;
+  }
+
+  bool HasNext() const
+  {
+    return FlattenedDisplayItemIterator::HasNext() || !mMarkers.empty();
+  }
+
+  nsDisplayItem* PeekNext() { return mNext; }
+
+private:
+  bool ShouldFlattenNextItem() override
+  {
+    if (!mNext) {
+      return false;
+    }
+
+    if (!mNext->ShouldFlattenAway(mBuilder)) {
+      return false;
+    }
+
+    const DisplayItemType type = mNext->GetType();
+    if (type != DisplayItemType::TYPE_OPACITY &&
+        type != DisplayItemType::TYPE_TRANSFORM) {
+      return true;
+    }
+
+    if (type == DisplayItemType::TYPE_OPACITY) {
+      nsDisplayOpacity* opacity = static_cast<nsDisplayOpacity*>(mNext);
+
+      if (opacity->OpacityAppliedToChildren()) {
+        // This is the previous opacity flattening path, where the opacity has
+        // been applied to children.
+        return true;
+      }
+    }
+
+    if (mState->IsInInactiveLayer() || !NextItemWantsInactiveLayer()) {
+      // Do not flatten nested inactive display items, or display items that
+      // want an active layer.
+      return false;
+    }
+
+    // Flatten inactive nsDisplayOpacity and nsDisplayTransform.
+    mAddingEffectMarker = true;
     return true;
   }
 
-  if (type == DisplayItemType::TYPE_OPACITY) {
-    nsDisplayOpacity* opacity = static_cast<nsDisplayOpacity*>(mNext);
-
-    if (opacity->OpacityAppliedToChildren()) {
-      // This is the previous opacity flattening path, where the opacity has
-      // been applied to children.
-      return true;
-    }
-  }
-
-  if (mState->IsInInactiveLayer() || !NextItemWantsInactiveLayer()) {
-    // Do not flatten nested inactive display items, or display items that want
-    // an active layer.
-    return false;
-  }
-
-  // Flatten inactive nsDisplayOpacity and nsDisplayTransform.
-  mStoreMarker = true;
-  return true;
-}
+  void EnterChildList(nsDisplayItem* aItem) override
+  {
+    if (!mAddingEffectMarker) {
+      // A container item will be flattened but no effect marker is needed.
+      AddHitTestMarker(aItem);
+      return;
+    }
+
+    if (AddMarkerIfNeeded<MarkerType::StartMarker>(aItem, mMarkers)) {
+      mActiveMarkers.AppendElement(aItem);
+    }
+
+    // Place the hit test marker between the effect markers.
+    AddHitTestMarker(aItem);
+
+    mAddingEffectMarker = false;
+  }
+
+  void ExitChildList(nsDisplayItem* aItem) override
+  {
+    if (mActiveMarkers.IsEmpty() || mActiveMarkers.LastElement() != aItem) {
+      // Do not emit an end marker if this item did not emit a start marker.
+      return;
+    }
+
+    if (AddMarkerIfNeeded<MarkerType::EndMarker>(aItem, mMarkers)) {
+      mActiveMarkers.RemoveLastElement();
+    }
+  }
+
+  bool NextItemWantsInactiveLayer()
+  {
+    LayerState layerState = mNext->GetLayerState(
+      mState->mBuilder, mState->mManager, mState->mParameters);
+
+    return layerState == LayerState::LAYER_INACTIVE;
+  }
+
+  std::deque<DisplayItemEntry> mMarkers;
+  AutoTArray<nsDisplayItem*, 4> mActiveMarkers;
+  ContainerState* mState;
+  bool mAddingEffectMarker;
+};
 
 class PaintedDisplayItemLayerUserData : public LayerUserData
 {
 public:
   PaintedDisplayItemLayerUserData()
     : mForcedBackgroundColor(NS_RGBA(0, 0, 0, 0))
     , mXScale(1.f)
     , mYScale(1.f)
@@ -4014,16 +4026,19 @@ PaintedLayerData::Accumulate(ContainerSt
                              const nsRect& aContentRect,
                              const DisplayItemClip& aClip,
                              LayerState aLayerState,
                              nsDisplayList* aList,
                              DisplayItemEntryType aType,
                              nsTArray<size_t>& aOpacityIndices,
                              const RefPtr<TransformClipNode>& aTransform)
 {
+  MOZ_ASSERT(aType != DisplayItemEntryType::HIT_TEST_INFO,
+             "Should have handled hit test items earlier!");
+
   FLB_LOG_PAINTED_LAYER_DECISION(
     this,
     "Accumulating dp=%s(%p), f=%p against pld=%p\n",
     aItem->Name(),
     aItem,
     aItem->Frame(),
     this);
 
@@ -4232,61 +4247,79 @@ PaintedLayerData::CombinedTouchActionReg
 {
   nsRegion result;
   result.Or(mHorizontalPanRegion, mVerticalPanRegion);
   result.OrWith(mNoActionRegion);
   return result;
 }
 
 void
-PaintedLayerData::AccumulateHitTestInfo(ContainerState* aState,
-                                        nsDisplayCompositorHitTestInfo* aItem,
+PaintedLayerData::AccumulateHitTestItem(ContainerState* aState,
+                                        nsDisplayItem* aItem,
                                         TransformClipNode* aTransform)
 {
+  MOZ_ASSERT(aItem->HasHitTestInfo());
+  auto* item = static_cast<nsDisplayHitTestInfoItem*>(aItem);
+
+  const HitTestInfo& info = item->GetHitTestInfo();
+
+  nsRect area = info.mArea;
+  const CompositorHitTestInfo& flags = info.mFlags;
+
   FLB_LOG_PAINTED_LAYER_DECISION(
-    this, "Accumulating hit test info %p against pld=%p\n", aItem, this);
-
-  const mozilla::DisplayItemClip& clip = aItem->GetClip();
-  nsRect area = clip.ApplyNonRoundedIntersection(aItem->Area());
+    this, "Accumulating hit test info %p against pld=%p, "
+          "area: [%d, %d, %d, %d], flags: 0x%x]\n",
+    item, this, area.x, area.y, area.width, area.height, flags.serialize());
+
+  const DisplayItemClip& clip = info.mClip
+                              ? *info.mClip
+                              : DisplayItemClip::NoClip();
+
+  area = clip.ApplyNonRoundedIntersection(area);
+
   if (aTransform) {
     area = aTransform->TransformRect(area, aState->mAppUnitsPerDevPixel);
   }
-  const mozilla::gfx::CompositorHitTestInfo hitTestInfo = aItem->HitTestInfo();
+
+  if (area.IsEmpty()) {
+    FLB_LOG_PAINTED_LAYER_DECISION(
+      this, "Discarded empty hit test info %p for pld=%p\n", item, this);
+    return;
+  }
 
   bool hasRoundedCorners = clip.GetRoundedRectCount() > 0;
 
   // use the NS_FRAME_SIMPLE_EVENT_REGIONS to avoid calling the slightly
   // expensive HasNonZeroCorner function if we know from a previous run that
   // the frame has zero corners.
-  nsIFrame* frame = aItem->Frame();
-
+  nsIFrame* frame = item->Frame();
   bool simpleRegions = frame->HasAnyStateBits(NS_FRAME_SIMPLE_EVENT_REGIONS);
   if (!simpleRegions) {
     if (nsLayoutUtils::HasNonZeroCorner(frame->StyleBorder()->mBorderRadius)) {
       hasRoundedCorners = true;
     } else {
       frame->AddStateBits(NS_FRAME_SIMPLE_EVENT_REGIONS);
     }
   }
 
   if (hasRoundedCorners || (frame->GetStateBits() & NS_FRAME_SVG_LAYOUT)) {
     mMaybeHitRegion.OrWith(area);
   } else {
     mHitRegion.OrWith(area);
   }
 
-  if (aItem->HitTestInfo().contains(CompositorHitTestFlags::eDispatchToContent)) {
+  if (flags.contains(CompositorHitTestFlags::eDispatchToContent)) {
     mDispatchToContentHitRegion.OrWith(area);
 
-  if (aItem->HitTestInfo().contains(CompositorHitTestFlags::eRequiresTargetConfirmation)) {
+  if (flags.contains(CompositorHitTestFlags::eRequiresTargetConfirmation)) {
       mDTCRequiresTargetConfirmation = true;
     }
   }
 
-  const auto touchFlags = hitTestInfo & CompositorHitTestTouchActionMask;
+  const auto touchFlags = flags & CompositorHitTestTouchActionMask;
   if (!touchFlags.isEmpty()) {
     // If there are multiple touch-action areas, there are multiple elements
     // with touch-action properties. We don't know what the relationship is
     // between those elements in terms of DOM ancestry, and so we don't know how
     // to combine the regions properly. Instead, we just add all the areas to
     // the dispatch-to-content region, so that the APZ knows to check with the
     // main thread. See bug 1286957.
     if (mCollapsedTouchActions) {
@@ -4307,21 +4340,21 @@ PaintedLayerData::AccumulateHitTestInfo(
       // With CompositorHitTestInfo we can now represent that case correctly,
       // but only if we use CompositorHitTestInfo all the way to the compositor
       // (i.e. in the WebRender-enabled case). In the non-WebRender case where
       // we still use the event regions, we must collapse these two cases back
       // together. Or add another region to the event regions to fix this
       // properly.
       if (touchFlags !=
           CompositorHitTestFlags::eTouchActionDoubleTapZoomDisabled) {
-        if (!hitTestInfo.contains(CompositorHitTestFlags::eTouchActionPanXDisabled)) {
+        if (!flags.contains(CompositorHitTestFlags::eTouchActionPanXDisabled)) {
           // pan-x is allowed
           mHorizontalPanRegion.OrWith(area);
         }
-        if (!hitTestInfo.contains(CompositorHitTestFlags::eTouchActionPanYDisabled)) {
+        if (!flags.contains(CompositorHitTestFlags::eTouchActionPanYDisabled)) {
           // pan-y is allowed
           mVerticalPanRegion.OrWith(area);
         }
       } else {
         // the touch-action: manipulation case described above. To preserve the
         // existing behaviour, don't touch either mHorizontalPanRegion or
         // mVerticalPanRegion
       }
@@ -4739,16 +4772,17 @@ ContainerState::ProcessDisplayItems(nsDi
   AnimatedGeometryRoot* lastAnimatedGeometryRoot = nullptr;
   nsPoint lastTopLeft;
 
   // Tracks the PaintedLayerData that the item will be accumulated in, if it is
   // non-null.
   PaintedLayerData* selectedLayer = nullptr;
   AutoTArray<size_t, 2> opacityIndices;
 
+  // AGR and ASR for the container item that was flattened.
   AnimatedGeometryRoot* containerAGR = nullptr;
   const ActiveScrolledRoot* containerASR = nullptr;
   RefPtr<TransformClipNode> transformNode = nullptr;
 
   const auto InTransform = [&]() { return transformNode; };
 
   const auto InOpacity = [&]() {
     return selectedLayer && opacityIndices.Length() > 0;
@@ -4757,132 +4791,155 @@ ContainerState::ProcessDisplayItems(nsDi
   FLBDisplayItemIterator iter(mBuilder, aList, this);
   while (iter.HasNext()) {
     DisplayItemEntry e = iter.GetNextEntry();
     DisplayItemEntryType marker = e.mType;
     nsDisplayItem* item = e.mItem;
     MOZ_ASSERT(item);
     DisplayItemType itemType = item->GetType();
 
+    if (itemType == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) {
+      // Override the marker for nsDisplayCompositorHitTestInfo items.
+      marker = DisplayItemEntryType::HIT_TEST_INFO;
+    }
+
     const bool inEffect = InTransform() || InOpacity();
 
-    if (itemType == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) {
-      nsDisplayCompositorHitTestInfo* hitTestInfo =
-        static_cast<nsDisplayCompositorHitTestInfo*>(item);
-
-      if (hitTestInfo->Area().IsEmpty()) {
-        continue;
-      }
-
-      if (inEffect) {
-        // If this item is inside a flattened effect, everything below is
-        // unnecessary processing.
-        MOZ_ASSERT(selectedLayer);
-        selectedLayer->AccumulateHitTestInfo(this, hitTestInfo, transformNode);
-        continue;
-      }
-    }
-
-    MOZ_ASSERT(item->GetType() != DisplayItemType::TYPE_WRAP_LIST);
+    if (marker == DisplayItemEntryType::HIT_TEST_INFO && inEffect) {
+      // Fast-path for hit test items inside flattened inactive layers.
+      MOZ_ASSERT(selectedLayer);
+      selectedLayer->AccumulateHitTestItem(this, item, transformNode);
+      continue;
+    }
 
     NS_ASSERTION(mAppUnitsPerDevPixel == AppUnitsPerDevPixel(item),
                  "items in a container layer should all have the same app "
                  "units per dev pixel");
 
     if (mBuilder->NeedToForceTransparentSurfaceForItem(item)) {
       aList->SetNeedsTransparentSurface();
     }
 
-    if (mParameters.mForEventsAndPluginsOnly && !item->GetChildren() &&
-        (itemType != DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO &&
+    if (mParameters.mForEventsAndPluginsOnly &&
+        (marker != DisplayItemEntryType::HIT_TEST_INFO &&
          itemType != DisplayItemType::TYPE_PLUGIN)) {
+      // Only process hit test info items or plugin items.
       continue;
     }
 
     LayerState layerState = LAYER_NONE;
-
     if (marker == DisplayItemEntryType::ITEM) {
       layerState = item->GetLayerState(mBuilder, mManager, mParameters);
 
       if (layerState == LAYER_INACTIVE && nsDisplayItem::ForceActiveLayers()) {
         layerState = LAYER_ACTIVE;
       }
     }
 
-    bool forceInactive = false;
-    AnimatedGeometryRoot* animatedGeometryRoot;
+    auto FuseItemClipChainIfNeeded = [&](const ActiveScrolledRoot* aASR) {
+      if (marker == DisplayItemEntryType::ITEM || IsEffectStartMarker(marker)) {
+        // No need to fuse clip chain for effect end markers, since it was
+        // already done for effect start markers.
+        item->FuseClipChainUpTo(mBuilder, aASR);
+      }
+    };
+
+    if (inEffect && marker == DisplayItemEntryType::ITEM) {
+      // Fast-path for items inside flattened inactive layers. This works
+      // because the layer state of the item cannot be active, otherwise the
+      // parent item would not have been flattened.
+      MOZ_ASSERT(selectedLayer);
+
+      FuseItemClipChainIfNeeded(containerASR);
+      selectedLayer->Accumulate(this,
+                                item,
+                                nsIntRect(),
+                                nsRect(),
+                                item->GetClip(),
+                                layerState,
+                                aList,
+                                marker,
+                                opacityIndices,
+                                transformNode);
+      continue;
+    }
+
+    // Items outside of flattened effects and non-item markers inside flattened
+    // effects are processed here.
+    MOZ_ASSERT(!inEffect || (marker != DisplayItemEntryType::ITEM));
+
+    AnimatedGeometryRoot* itemAGR = nullptr;
     const ActiveScrolledRoot* itemASR = nullptr;
     const DisplayItemClipChain* layerClipChain = nullptr;
+    const DisplayItemClipChain* itemClipChain = nullptr;
+    const DisplayItemClip* itemClipPtr = nullptr;
+
+    bool snap = false;
+    nsRect itemContent;
+
+    if (marker == DisplayItemEntryType::HIT_TEST_INFO) {
+      const auto& hitTestInfo =
+        static_cast<nsDisplayHitTestInfoItem*>(item)->GetHitTestInfo();
+
+      // Override the layer selection hints for items that have hit test
+      // information. This is needed because container items may have different
+      // clipping, AGR, or ASR than the child items in them.
+      itemAGR = hitTestInfo.mAGR;
+      itemASR = hitTestInfo.mASR;
+      itemClipChain = hitTestInfo.mClipChain;
+      itemClipPtr = hitTestInfo.mClip;
+      itemContent = hitTestInfo.mArea;
+    } else {
+      itemAGR = item->GetAnimatedGeometryRoot();
+      itemASR = item->GetActiveScrolledRoot();
+      itemClipChain = item->GetClipChain();
+      itemClipPtr = &item->GetClip();
+      itemContent = item->GetBounds(mBuilder, &snap);
+    }
 
     if (mManager->IsWidgetLayerManager() && !inEffect) {
-      animatedGeometryRoot = item->GetAnimatedGeometryRoot();
-      itemASR = item->GetActiveScrolledRoot();
-      const DisplayItemClipChain* itemClipChain = item->GetClipChain();
       if (itemClipChain && itemClipChain->mASR == itemASR &&
           itemType != DisplayItemType::TYPE_STICKY_POSITION) {
         layerClipChain = itemClipChain->mParent;
       } else {
         layerClipChain = itemClipChain;
       }
-    } else if (inEffect) {
-      animatedGeometryRoot = containerAGR;
-      itemASR = containerASR;
-
-      if (marker != DisplayItemEntryType::POP_TRANSFORM) {
-        item->FuseClipChainUpTo(mBuilder, containerASR);
+    } else {
+      // Inside a flattened effect or inactive layer, use container AGR and ASR.
+      itemAGR = inEffect ? containerAGR : mContainerAnimatedGeometryRoot;
+      itemASR = inEffect ? containerASR : mContainerASR;
+
+      FuseItemClipChainIfNeeded(itemASR);
+      if (marker != DisplayItemEntryType::HIT_TEST_INFO) {
+        // Because of the clip chain fusing, |itemClipPtr| needs to be updated.
+        itemClipPtr = &item->GetClip();
       }
-    } else {
-      animatedGeometryRoot = mContainerAnimatedGeometryRoot;
-      itemASR = mContainerASR;
-      item->FuseClipChainUpTo(mBuilder, mContainerASR);
-    }
-
-    const DisplayItemClip& itemClip = item->GetClip();
-
-    if (inEffect && marker == DisplayItemEntryType::ITEM) {
-      MOZ_ASSERT(selectedLayer);
-      selectedLayer->Accumulate(this,
-                                item,
-                                nsIntRect(),
-                                nsRect(),
-                                itemClip,
-                                layerState,
-                                aList,
-                                marker,
-                                opacityIndices,
-                                transformNode);
-      continue;
-    }
-
-    if (animatedGeometryRoot == lastAnimatedGeometryRoot) {
+    }
+
+    const DisplayItemClip& itemClip = itemClipPtr
+                                    ? *itemClipPtr
+                                    : DisplayItemClip::NoClip();
+
+    if (itemAGR == lastAnimatedGeometryRoot) {
       topLeft = lastTopLeft;
     } else {
       lastTopLeft = topLeft =
-        (*animatedGeometryRoot)->GetOffsetToCrossDoc(mContainerReferenceFrame);
-      lastAnimatedGeometryRoot = animatedGeometryRoot;
+        (*itemAGR)->GetOffsetToCrossDoc(mContainerReferenceFrame);
+      lastAnimatedGeometryRoot = itemAGR;
     }
 
     const ActiveScrolledRoot* scrollMetadataASR =
       layerClipChain
         ? ActiveScrolledRoot::PickDescendant(itemASR, layerClipChain->mASR)
         : itemASR;
 
     const bool prerenderedTransform =
       itemType == DisplayItemType::TYPE_TRANSFORM &&
       static_cast<nsDisplayTransform*>(item)->MayBeAnimated(mBuilder);
 
-    bool snap;
-    nsRect itemContent = item->GetBounds(mBuilder, &snap);
-
-    if (itemType == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) {
-      nsDisplayCompositorHitTestInfo* hitInfo =
-        static_cast<nsDisplayCompositorHitTestInfo*>(item);
-      itemContent = hitInfo->Area();
-    }
-
     nsIntRect itemDrawRect = ScaleToOutsidePixels(itemContent, snap);
     ParentLayerIntRect clipRect;
     if (itemClip.HasClip()) {
       const nsRect& itemClipRect = itemClip.GetClipRect();
       itemContent.IntersectRect(itemContent, itemClipRect);
       clipRect = ViewAs<ParentLayerPixel>(ScaleToNearestPixels(itemClipRect));
 
       if (!prerenderedTransform && !IsScrollThumbLayer(item)) {
@@ -4906,17 +4963,17 @@ ContainerState::ProcessDisplayItems(nsDi
 
       itemDrawRect =
         transformNode->TransformRect(itemDrawRect);
     }
 
 #ifdef DEBUG
     nsRect bounds = itemContent;
 
-    if (itemType == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO || inEffect) {
+    if (marker == DisplayItemEntryType::HIT_TEST_INFO || inEffect) {
       bounds.SetEmpty();
     }
 
     if (!bounds.IsEmpty() && itemASR != mContainerASR) {
       if (Maybe<nsRect> clip =
             item->GetClipWithRespectToASR(mBuilder, mContainerASR)) {
         bounds = clip.ref();
       }
@@ -4937,19 +4994,17 @@ ContainerState::ProcessDisplayItems(nsDi
         itemBuildingRect =
           transformNode->TransformRect(itemBuildingRect, mAppUnitsPerDevPixel);
       }
 
       itemVisibleRect = itemVisibleRect.Intersect(
         ScaleToOutsidePixels(itemBuildingRect, false));
     }
 
-    if (maxLayers != -1 && layerCount >= maxLayers) {
-      forceInactive = true;
-    }
+    const bool forceInactive = maxLayers != -1 && layerCount >= maxLayers;
 
     // Assign the item to a layer
     bool treatInactiveItemAsActive =
       (layerState == LAYER_INACTIVE &&
        mLayerBuilder->GetContainingPaintedLayerData());
     if (layerState == LAYER_ACTIVE_FORCE || treatInactiveItemAsActive ||
         (!forceInactive &&
          (layerState == LAYER_ACTIVE_EMPTY || layerState == LAYER_ACTIVE))) {
@@ -5023,59 +5078,58 @@ ContainerState::ProcessDisplayItems(nsDi
         AnimatedGeometryRoot* clipAGR =
           mBuilder->AnimatedGeometryRootForASR(clipASR);
         nsIntRect scrolledClipRect =
           ScaleToNearestPixels(layerClipChain->mClip.GetClipRect()) +
           mParameters.mOffset;
         mPaintedLayerDataTree.AddingOwnLayer(
           clipAGR, &scrolledClipRect, uniformColorPtr);
       } else if (item->ShouldFixToViewport(mBuilder) && itemClip.HasClip() &&
-                 item->AnimatedGeometryRootForScrollMetadata() !=
-                   animatedGeometryRoot &&
+                 item->AnimatedGeometryRootForScrollMetadata() != itemAGR &&
                  !nsLayoutUtils::UsesAsyncScrolling(item->Frame())) {
         // This is basically the same as the case above, but for the non-APZ
         // case. At the moment, when APZ is off, there is only the root ASR
         // (because scroll frames without display ports don't create ASRs) and
         // the whole clip chain is always just one fused clip.
         // Bug 1336516 aims to change that and to remove this workaround.
         AnimatedGeometryRoot* clipAGR =
           item->AnimatedGeometryRootForScrollMetadata();
         nsIntRect scrolledClipRect =
           ScaleToNearestPixels(itemClip.GetClipRect()) + mParameters.mOffset;
         mPaintedLayerDataTree.AddingOwnLayer(
           clipAGR, &scrolledClipRect, uniformColorPtr);
       } else if (IsScrollThumbLayer(item) && mManager->IsWidgetLayerManager()) {
         // For scrollbar thumbs, the clip we care about is the clip added by the
         // slider frame.
         mPaintedLayerDataTree.AddingOwnLayer(
-          animatedGeometryRoot->mParentAGR, clipPtr, uniformColorPtr);
+          itemAGR->mParentAGR, clipPtr, uniformColorPtr);
       } else if (prerenderedTransform && mManager->IsWidgetLayerManager()) {
-        if (animatedGeometryRoot->mParentAGR) {
+        if (itemAGR->mParentAGR) {
           mPaintedLayerDataTree.AddingOwnLayer(
-            animatedGeometryRoot->mParentAGR, clipPtr, uniformColorPtr);
+            itemAGR->mParentAGR, clipPtr, uniformColorPtr);
         } else {
           mPaintedLayerDataTree.AddingOwnLayer(
-            animatedGeometryRoot, nullptr, uniformColorPtr);
+            itemAGR, nullptr, uniformColorPtr);
         }
       } else {
         // Using itemVisibleRect here isn't perfect. itemVisibleRect can be
         // larger or smaller than the potential bounds of item's contents in
-        // animatedGeometryRoot: It's too large if there's a clipped display
+        // itemAGR: It's too large if there's a clipped display
         // port somewhere among item's contents (see bug 1147673), and it can
         // be too small if the contents can move, because it only looks at the
         // contents' current bounds and doesn't anticipate any animations.
         // Time will tell whether this is good enough, or whether we need to do
         // something more sophisticated here.
         mPaintedLayerDataTree.AddingOwnLayer(
-          animatedGeometryRoot, &itemVisibleRect, uniformColorPtr);
+          itemAGR, &itemVisibleRect, uniformColorPtr);
       }
 
       ContainerLayerParameters params = mParameters;
       params.mBackgroundColor = uniformColor;
-      params.mLayerCreationHint = GetLayerCreationHint(animatedGeometryRoot);
+      params.mLayerCreationHint = GetLayerCreationHint(itemAGR);
       params.mScrollMetadataASR = ActiveScrolledRoot::PickDescendant(
         mContainerScrollMetadataASR, scrollMetadataASR);
       params.mCompositorASR =
         params.mScrollMetadataASR != mContainerScrollMetadataASR
           ? params.mScrollMetadataASR
           : mContainerCompositorASR;
       if (itemType == DisplayItemType::TYPE_FIXED_POSITION) {
         params.mCompositorASR = itemASR;
@@ -5231,17 +5285,17 @@ ContainerState::ProcessDisplayItems(nsDi
       if (oldContainer && oldContainer != mContainerLayer) {
         oldContainer->RemoveChild(ownLayer);
       }
       NS_ASSERTION(FindIndexOfLayerIn(mNewChildLayers, ownLayer) < 0,
                    "Layer already in list???");
 
       NewLayerEntry* newLayerEntry = mNewChildLayers.AppendElement();
       newLayerEntry->mLayer = ownLayer;
-      newLayerEntry->mAnimatedGeometryRoot = animatedGeometryRoot;
+      newLayerEntry->mAnimatedGeometryRoot = itemAGR;
       newLayerEntry->mASR = itemASR;
       newLayerEntry->mScrollMetadataASR = scrollMetadataASR;
       newLayerEntry->mClipChain = layerClipChain;
       newLayerEntry->mLayerState = layerState;
       if (itemType == DisplayItemType::TYPE_FIXED_POSITION) {
         newLayerEntry->mIsFixedToRootScrollFrame =
           item->Frame()->StyleDisplay()->mPosition == NS_STYLE_POSITION_FIXED &&
           nsLayoutUtils::IsReallyFixedPos(item->Frame());
@@ -5279,17 +5333,17 @@ ContainerState::ProcessDisplayItems(nsDi
           newLayerEntry->mVisibleRegion =
             item->GetBuildingRectForChildren().ScaleToOutsidePixels(
               contentXScale, contentYScale, mAppUnitsPerDevPixel);
         } else {
           newLayerEntry->mVisibleRegion = itemVisibleRegion;
         }
         newLayerEntry->mOpaqueRegion = ComputeOpaqueRect(
           item,
-          animatedGeometryRoot,
+          itemAGR,
           itemASR,
           itemClip,
           aList,
           &newLayerEntry->mHideAllLayersBelow,
           &newLayerEntry->mOpaqueForAnimatedGeometryRootParent);
       } else {
         bool useChildrenVisible = itemType == DisplayItemType::TYPE_TRANSFORM &&
                                   (item->Frame()->IsPreserve3DLeaf() ||
@@ -5328,61 +5382,67 @@ ContainerState::ProcessDisplayItems(nsDi
        */
       if (ownLayer->Manager() == mLayerBuilder->GetRetainingLayerManager()) {
         oldData = mLayerBuilder->GetOldLayerForFrame(item->Frame(),
                                                      item->GetPerFrameKey());
         mLayerBuilder->StoreDataForFrame(item, ownLayer, layerState, oldData);
       }
     } else {
       const bool backfaceHidden = item->In3DContextAndBackfaceIsHidden();
-      const nsIFrame* referenceFrame = item->ReferenceFrame();
+
+      // When container item hit test info is processed, we need to use the same
+      // reference frame as the container children.
+      const nsIFrame* referenceFrame = item == mContainerItem
+                                     ? mContainerReferenceFrame
+                                     : item->ReferenceFrame(); 
+
+      MOZ_ASSERT(item != mContainerItem ||
+                 marker == DisplayItemEntryType::HIT_TEST_INFO);
 
       PaintedLayerData* paintedLayerData = selectedLayer;
 
       if (!paintedLayerData) {
         paintedLayerData = mPaintedLayerDataTree.FindPaintedLayerFor(
-          animatedGeometryRoot,
+          itemAGR,
           itemASR,
           layerClipChain,
           itemVisibleRect,
           backfaceHidden,
           [&](PaintedLayerData* aData) {
             NewPaintedLayerData(aData,
-                                animatedGeometryRoot,
+                                itemAGR,
                                 itemASR,
                                 layerClipChain,
                                 scrollMetadataASR,
                                 topLeft,
                                 referenceFrame,
                                 backfaceHidden);
           });
       }
       MOZ_ASSERT(paintedLayerData);
 
-      if (itemType == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) {
-        nsDisplayCompositorHitTestInfo* hitTestInfo =
-          static_cast<nsDisplayCompositorHitTestInfo*>(item);
+      if (marker == DisplayItemEntryType::HIT_TEST_INFO) {
         MOZ_ASSERT(!transformNode);
-        paintedLayerData->AccumulateHitTestInfo(this, hitTestInfo, nullptr);
+        paintedLayerData->AccumulateHitTestItem(this, item, nullptr);
       } else {
         paintedLayerData->Accumulate(this,
                                      item,
                                      itemVisibleRect,
                                      itemContent,
                                      itemClip,
                                      layerState,
                                      aList,
                                      marker,
                                      opacityIndices,
                                      transformNode);
 
         if (!paintedLayerData->mLayer) {
           // Try to recycle the old layer of this display item.
           RefPtr<PaintedLayer> layer = AttemptToRecyclePaintedLayer(
-            animatedGeometryRoot, item, topLeft, referenceFrame);
+            itemAGR, item, topLeft, referenceFrame);
           if (layer) {
             paintedLayerData->mLayer = layer;
 
             PaintedDisplayItemLayerUserData* userData =
               GetPaintedDisplayItemLayerUserData(layer);
             paintedLayerData->mAssignedDisplayItems.reserve(
               userData->mLastItemCount);
 
--- a/layout/painting/FrameLayerBuilder.h
+++ b/layout/painting/FrameLayerBuilder.h
@@ -48,17 +48,18 @@ class PaintedDisplayItemLayerUserData;
 
 enum class DisplayItemEntryType
 {
   ITEM,
   PUSH_OPACITY,
   PUSH_OPACITY_WITH_BG,
   POP_OPACITY,
   PUSH_TRANSFORM,
-  POP_TRANSFORM
+  POP_TRANSFORM,
+  HIT_TEST_INFO
 };
 
 /**
  * Retained data storage:
  *
  * Each layer manager (widget, and inactive) stores a LayerManagerData object
  * that keeps a hash-set of DisplayItemData items that were drawn into it.
  * Each frame also keeps a list of DisplayItemData pointers that were
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -1008,17 +1008,16 @@ nsDisplayListBuilder::OutOfFlowDisplayDa
 }
 
 nsDisplayListBuilder::nsDisplayListBuilder(nsIFrame* aReferenceFrame,
                                            nsDisplayListBuilderMode aMode,
                                            bool aBuildCaret,
                                            bool aRetainingDisplayList)
   : mReferenceFrame(aReferenceFrame)
   , mIgnoreScrollFrame(nullptr)
-  , mCompositorHitTestInfo(nullptr)
   , mCurrentTableItem(nullptr)
   , mCurrentActiveScrolledRoot(nullptr)
   , mCurrentContainerASR(nullptr)
   , mCurrentFrame(aReferenceFrame)
   , mCurrentReferenceFrame(aReferenceFrame)
   , mRootAGR(AnimatedGeometryRoot::CreateAGRForFrame(aReferenceFrame,
                                                      nullptr,
                                                      true,
@@ -1064,23 +1063,23 @@ nsDisplayListBuilder::nsDisplayListBuild
   , mAsyncPanZoomEnabled(nsLayoutUtils::AsyncPanZoomEnabled(aReferenceFrame))
   , mBuildingInvisibleItems(false)
   , mHitTestIsForVisibility(false)
   , mIsBuilding(false)
   , mInInvalidSubtree(false)
   , mDisablePartialUpdates(false)
   , mPartialBuildFailed(false)
   , mIsInActiveDocShell(false)
+  , mHitTestArea()
+  , mHitTestInfo(CompositorHitTestInvisibleToHit)
 {
   MOZ_COUNT_CTOR(nsDisplayListBuilder);
 
   mBuildCompositorHitTestInfo = mAsyncPanZoomEnabled && IsForPainting();
 
-  mLessEventRegionItems = gfxPrefs::LessEventRegionItems();
-
   nsPresContext* pc = aReferenceFrame->PresContext();
   nsIPresShell* shell = pc->PresShell();
   if (pc->IsRenderingOnlySelection()) {
     nsCOMPtr<nsISelectionController> selcon(do_QueryInterface(shell));
     if (selcon) {
       mBoundingSelection =
         selcon->GetSelection(nsISelectionController::SELECTION_NORMAL);
     }
@@ -1111,18 +1110,16 @@ nsDisplayListBuilder::EndFrame()
   NS_ASSERTION(!mInInvalidSubtree,
                "Someone forgot to cleanup mInInvalidSubtree!");
   mFrameToAnimatedGeometryRootMap.Clear();
   mAGRBudgetSet.Clear();
   mActiveScrolledRoots.Clear();
   FreeClipChains();
   FreeTemporaryItems();
   nsCSSRendering::EndFrameTreesLocked();
-
-  MOZ_ASSERT(!mCompositorHitTestInfo);
 }
 
 void
 nsDisplayListBuilder::MarkFrameForDisplay(nsIFrame* aFrame,
                                           nsIFrame* aStopAtFrame)
 {
   mFramesMarkedForDisplay.AppendElement(aFrame);
   for (nsIFrame* f = aFrame; f;
@@ -2307,92 +2304,47 @@ void
 nsDisplayListBuilder::AppendNewScrollInfoItemForHoisting(
   nsDisplayScrollInfoLayer* aScrollInfoItem)
 {
   MOZ_ASSERT(ShouldBuildScrollInfoItemsForHoisting());
   MOZ_ASSERT(mScrollInfoItemsForHoisting);
   mScrollInfoItemsForHoisting->AppendToTop(aScrollInfoItem);
 }
 
-static nsRect
-GetFrameArea(const nsDisplayListBuilder* aBuilder, const nsIFrame* aFrame)
-{
-  nsRect area;
-
-  nsIScrollableFrame* scrollFrame =
-    nsLayoutUtils::GetScrollableFrameFor(aFrame);
-  if (scrollFrame) {
-    // If the frame is content of a scrollframe, then we need to pick up the
-    // area corresponding to the overflow rect as well. Otherwise the parts of
-    // the overflow that are not occupied by descendants get skipped and the
-    // APZ code sends touch events to the content underneath instead.
-    // See https://bugzilla.mozilla.org/show_bug.cgi?id=1127773#c15.
-    area = aFrame->GetScrollableOverflowRect();
-  } else {
-    area = nsRect(nsPoint(0, 0), aFrame->GetSize());
-  }
-
-  if (!area.IsEmpty()) {
-    return area + aBuilder->ToReferenceFrame(aFrame);
-  }
-
-  return area;
-}
-
 void
 nsDisplayListBuilder::BuildCompositorHitTestInfoIfNeeded(nsIFrame* aFrame,
                                                          nsDisplayList* aList,
                                                          const bool aBuildNew)
 {
   MOZ_ASSERT(aFrame);
   MOZ_ASSERT(aList);
 
   if (!BuildCompositorHitTestInfo()) {
     return;
   }
 
-  CompositorHitTestInfo info = aFrame->GetCompositorHitTestInfo(this);
-  if (!ShouldBuildCompositorHitTestInfo(aFrame, info, aBuildNew)) {
-    // Either the parent hit test info can be reused, or this frame has no hit
-    // test flags set.
+  const CompositorHitTestInfo info = aFrame->GetCompositorHitTestInfo(this);
+  if (info == CompositorHitTestInvisibleToHit) {
     return;
   }
 
-  nsDisplayCompositorHitTestInfo* item =
-    MakeDisplayItem<nsDisplayCompositorHitTestInfo>(this, aFrame, info);
-
-  SetCompositorHitTestInfo(item);
+  const nsRect area = aFrame->GetCompositorHitTestArea(this);
+  if (!aBuildNew &&
+      GetHitTestInfo() == info &&
+      GetHitTestArea().Contains(area)) {
+    return;
+  }
+
+  auto* item = MakeDisplayItem<nsDisplayCompositorHitTestInfo>(
+    this, aFrame, info, 0, Some(area));
+
+  SetCompositorHitTestInfo(area, info);
   aList->AppendToTop(item);
 }
 
-bool
-nsDisplayListBuilder::ShouldBuildCompositorHitTestInfo(
-  const nsIFrame* aFrame,
-  const CompositorHitTestInfo& aInfo,
-  const bool aBuildNew) const
-{
-  MOZ_ASSERT(mBuildCompositorHitTestInfo);
-
-  if (aInfo == CompositorHitTestInvisibleToHit) {
-    return false;
-  }
-
-  if (!mCompositorHitTestInfo || !mLessEventRegionItems || aBuildNew) {
-    return true;
-  }
-
-  if (mCompositorHitTestInfo->HitTestInfo() != aInfo) {
-    // Hit test flags are different.
-    return true;
-  }
-
-  // Create a new item if the parent does not contain the child completely.
-  return !mCompositorHitTestInfo->Area().Contains(GetFrameArea(this, aFrame));
-}
-
 void
 nsDisplayListSet::MoveTo(const nsDisplayListSet& aDestination) const
 {
   aDestination.BorderBackground()->AppendToTop(BorderBackground());
   aDestination.BlockBorderBackgrounds()->AppendToTop(BlockBorderBackgrounds());
   aDestination.Floats()->AppendToTop(Floats());
   aDestination.Content()->AppendToTop(Content());
   aDestination.PositionedDescendants()->AppendToTop(PositionedDescendants());
@@ -5330,55 +5282,73 @@ nsDisplayEventReceiver::CreateWebRenderC
   // list for WebRender consumption, so this function should never get called.
   MOZ_ASSERT(false);
   return true;
 }
 
 nsDisplayCompositorHitTestInfo::nsDisplayCompositorHitTestInfo(
   nsDisplayListBuilder* aBuilder,
   nsIFrame* aFrame,
-  mozilla::gfx::CompositorHitTestInfo aHitTestInfo,
+  const mozilla::gfx::CompositorHitTestInfo& aHitTestFlags,
   uint32_t aIndex,
   const mozilla::Maybe<nsRect>& aArea)
-  : nsDisplayEventReceiver(aBuilder, aFrame)
-  , mHitTestInfo(aHitTestInfo)
+  : nsDisplayHitTestInfoItem(aBuilder, aFrame)
   , mIndex(aIndex)
   , mAppUnitsPerDevPixel(mFrame->PresContext()->AppUnitsPerDevPixel())
 {
   MOZ_COUNT_CTOR(nsDisplayCompositorHitTestInfo);
   // We should never even create this display item if we're not building
   // compositor hit-test info or if the computed hit info indicated the
   // frame is invisible to hit-testing
   MOZ_ASSERT(aBuilder->BuildCompositorHitTestInfo());
-  MOZ_ASSERT(mHitTestInfo != CompositorHitTestInvisibleToHit);
-
+  MOZ_ASSERT(aHitTestFlags != CompositorHitTestInvisibleToHit);
+
+  const nsRect& area = aArea.isSome()
+                     ? *aArea
+                     : aFrame->GetCompositorHitTestArea(aBuilder);
+
+  SetHitTestInfo(area, aHitTestFlags);
+  InitializeScrollTarget(aBuilder);
+}
+
+nsDisplayCompositorHitTestInfo::nsDisplayCompositorHitTestInfo(
+  nsDisplayListBuilder* aBuilder,
+  nsIFrame* aFrame,
+  mozilla::UniquePtr<HitTestInfo>&& aHitTestInfo)
+  : nsDisplayHitTestInfoItem(aBuilder, aFrame)
+  , mIndex(0)
+  , mAppUnitsPerDevPixel(mFrame->PresContext()->AppUnitsPerDevPixel())
+{
+  MOZ_COUNT_CTOR(nsDisplayCompositorHitTestInfo);
+  SetHitTestInfo(std::move(aHitTestInfo));
+  InitializeScrollTarget(aBuilder);
+}
+
+void
+nsDisplayCompositorHitTestInfo::InitializeScrollTarget(
+  nsDisplayListBuilder* aBuilder)
+{
   if (aBuilder->GetCurrentScrollbarDirection().isSome()) {
     // In the case of scrollbar frames, we use the scrollbar's target
     // scrollframe instead of the scrollframe with which the scrollbar actually
     // moves.
-    MOZ_ASSERT(mHitTestInfo.contains(CompositorHitTestFlags::eScrollbar));
-    mScrollTarget = Some(aBuilder->GetCurrentScrollbarTarget());
-  }
-
-  if (aArea.isSome()) {
-    mArea = *aArea;
-  } else {
-    mArea = GetFrameArea(aBuilder, aFrame);
+    MOZ_ASSERT(HitTestFlags().contains(CompositorHitTestFlags::eScrollbar));
+    mScrollTarget = mozilla::Some(aBuilder->GetCurrentScrollbarTarget());
   }
 }
 
 bool
 nsDisplayCompositorHitTestInfo::CreateWebRenderCommands(
   mozilla::wr::DisplayListBuilder& aBuilder,
   mozilla::wr::IpcResourceUpdateQueue& aResources,
   const StackingContextHelper& aSc,
   mozilla::layers::WebRenderLayerManager* aManager,
   nsDisplayListBuilder* aDisplayListBuilder)
 {
-  if (mArea.IsEmpty()) {
+  if (HitTestArea().IsEmpty()) {
     return true;
   }
 
   // XXX: eventually this scrollId computation and the SetHitTestInfo
   // call will get moved out into the WR display item iteration code so that
   // we don't need to do it as often, and so that we can do it for other
   // display item types as well (reducing the need for as many instances of
   // this display item).
@@ -5392,37 +5362,30 @@ nsDisplayCompositorHitTestInfo::CreateWe
       }
       if (asr) {
         return asr->GetViewId();
       }
       return FrameMetrics::NULL_SCROLL_ID;
     });
 
   // Insert a transparent rectangle with the hit-test info
-  aBuilder.SetHitTestInfo(scrollId, mHitTestInfo);
+  aBuilder.SetHitTestInfo(scrollId, HitTestFlags());
 
   const LayoutDeviceRect devRect =
-    LayoutDeviceRect::FromAppUnits(mArea, mAppUnitsPerDevPixel);
+    LayoutDeviceRect::FromAppUnits(HitTestArea(), mAppUnitsPerDevPixel);
 
   const wr::LayoutRect rect = wr::ToRoundedLayoutRect(devRect);
 
   aBuilder.PushRect(
     rect, rect, !BackfaceIsHidden(), wr::ToColorF(gfx::Color()));
   aBuilder.ClearHitTestInfo();
 
   return true;
 }
 
-void
-nsDisplayCompositorHitTestInfo::WriteDebugInfo(std::stringstream& aStream)
-{
-  aStream << nsPrintfCString(" (hitTestInfo 0x%x)", mHitTestInfo.serialize()).get();
-  AppendToString(aStream, mArea, " hitTestArea");
-}
-
 uint32_t
 nsDisplayCompositorHitTestInfo::GetPerFrameKey() const
 {
   return (mIndex << TYPE_BITS) | nsDisplayItem::GetPerFrameKey();
 }
 
 int32_t
 nsDisplayCompositorHitTestInfo::ZIndex() const
@@ -6038,17 +6001,17 @@ nsDisplayWrapList::nsDisplayWrapList(nsD
 nsDisplayWrapList::nsDisplayWrapList(
   nsDisplayListBuilder* aBuilder,
   nsIFrame* aFrame,
   nsDisplayList* aList,
   const ActiveScrolledRoot* aActiveScrolledRoot,
   bool aClearClipChain,
   uint32_t aIndex,
   bool aAnonymous)
-  : nsDisplayItem(aBuilder, aFrame, aActiveScrolledRoot, aAnonymous)
+  : nsDisplayHitTestInfoItem(aBuilder, aFrame, aActiveScrolledRoot, aAnonymous)
   , mFrameActiveScrolledRoot(aBuilder->CurrentActiveScrolledRoot())
   , mOverrideZIndex(0)
   , mIndex(aIndex)
   , mHasZIndexOverride(false)
   , mClearingClipChain(aClearClipChain)
 {
   MOZ_COUNT_CTOR(nsDisplayWrapList);
 
@@ -6085,20 +6048,20 @@ nsDisplayWrapList::nsDisplayWrapList(
 
   SetBuildingRect(visible);
 }
 
 nsDisplayWrapList::nsDisplayWrapList(nsDisplayListBuilder* aBuilder,
                                      nsIFrame* aFrame,
                                      nsDisplayItem* aItem,
                                      bool aAnonymous)
-  : nsDisplayItem(aBuilder,
-                  aFrame,
-                  aBuilder->CurrentActiveScrolledRoot(),
-                  aAnonymous)
+  : nsDisplayHitTestInfoItem(aBuilder,
+                             aFrame,
+                             aBuilder->CurrentActiveScrolledRoot(),
+                             aAnonymous)
   , mOverrideZIndex(0)
   , mIndex(0)
   , mHasZIndexOverride(false)
 {
   MOZ_COUNT_CTOR(nsDisplayWrapList);
 
   mBaseBuildingRect = GetBuildingRect();
 
@@ -6745,16 +6708,17 @@ nsDisplayOpacity::CreateWebRenderCommand
     aManager->AddActiveCompositorAnimationId(animationsId);
   } else if (animationsId) {
     aManager->AddCompositorAnimationsIdForDiscard(animationsId);
     animationsId = 0;
   }
 
   nsTArray<mozilla::wr::WrFilterOp> filters;
   StackingContextHelper sc(aSc,
+                           GetActiveScrolledRoot(),
                            aBuilder,
                            filters,
                            LayoutDeviceRect(),
                            nullptr,
                            animationsId ? &prop : nullptr,
                            opacityForSC);
 
   aManager->CommandBuilder().CreateWebRenderCommandsFromDisplayList(
@@ -6798,16 +6762,17 @@ nsDisplayBlendMode::CreateWebRenderComma
   mozilla::wr::DisplayListBuilder& aBuilder,
   mozilla::wr::IpcResourceUpdateQueue& aResources,
   const StackingContextHelper& aSc,
   mozilla::layers::WebRenderLayerManager* aManager,
   nsDisplayListBuilder* aDisplayListBuilder)
 {
   nsTArray<mozilla::wr::WrFilterOp> filters;
   StackingContextHelper sc(aSc,
+                           GetActiveScrolledRoot(),
                            aBuilder,
                            filters,
                            LayoutDeviceRect(),
                            nullptr,
                            nullptr,
                            nullptr,
                            nullptr,
                            nullptr,
@@ -6949,17 +6914,17 @@ nsDisplayBlendContainer::GetLayerState(
 bool
 nsDisplayBlendContainer::CreateWebRenderCommands(
   mozilla::wr::DisplayListBuilder& aBuilder,
   mozilla::wr::IpcResourceUpdateQueue& aResources,
   const StackingContextHelper& aSc,
   mozilla::layers::WebRenderLayerManager* aManager,
   nsDisplayListBuilder* aDisplayListBuilder)
 {
-  StackingContextHelper sc(aSc, aBuilder);
+  StackingContextHelper sc(aSc, GetActiveScrolledRoot(), aBuilder);
 
   return nsDisplayWrapList::CreateWebRenderCommands(
     aBuilder, aResources, sc, aManager, aDisplayListBuilder);
 }
 
 /* static */ nsDisplayTableBlendContainer*
 nsDisplayTableBlendContainer::CreateForBackgroundBlendMode(
   nsDisplayListBuilder* aBuilder,
@@ -7089,16 +7054,17 @@ nsDisplayOwnLayer::CreateWebRenderComman
   animationInfo.EnsureAnimationsId();
   mWrAnimationId = animationInfo.GetCompositorAnimationsId();
 
   wr::WrAnimationProperty prop;
   prop.id = mWrAnimationId;
   prop.effect_type = wr::WrAnimationType::Transform;
 
   StackingContextHelper sc(aSc,
+                           GetActiveScrolledRoot(),
                            aBuilder,
                            nsTArray<wr::WrFilterOp>(),
                            LayoutDeviceRect(),
                            nullptr,
                            &prop);
 
   nsDisplayWrapList::CreateWebRenderCommands(
     aBuilder, aResources, sc, aManager, aDisplayListBuilder);
@@ -7857,17 +7823,17 @@ nsDisplayStickyPosition::CreateWebRender
                                  hBounds,
                                  applied);
 
     aBuilder.PushClip(id);
     aManager->CommandBuilder().PushOverrideForASR(mContainerASR, Some(id));
   }
 
   {
-    StackingContextHelper sc(aSc, aBuilder);
+    StackingContextHelper sc(aSc, GetActiveScrolledRoot(), aBuilder);
     nsDisplayWrapList::CreateWebRenderCommands(
       aBuilder, aResources, sc, aManager, aDisplayListBuilder);
   }
 
   if (stickyScrollContainer) {
     aManager->CommandBuilder().PopOverrideForASR(mContainerASR);
     aBuilder.PopClip();
   }
@@ -8066,17 +8032,17 @@ nsDisplayZoom::ComputeVisibility(nsDispl
 
 nsDisplayTransform::nsDisplayTransform(
   nsDisplayListBuilder* aBuilder,
   nsIFrame* aFrame,
   nsDisplayList* aList,
   const nsRect& aChildrenBuildingRect,
   ComputeTransformFunction aTransformGetter,
   uint32_t aIndex)
-  : nsDisplayItem(aBuilder, aFrame)
+  : nsDisplayHitTestInfoItem(aBuilder, aFrame)
   , mStoredList(aBuilder, aFrame, aList)
   , mTransformGetter(aTransformGetter)
   , mAnimatedGeometryRootForChildren(mAnimatedGeometryRoot)
   , mAnimatedGeometryRootForScrollMetadata(mAnimatedGeometryRoot)
   , mChildrenBuildingRect(aChildrenBuildingRect)
   , mIndex(aIndex)
   , mNoExtendContext(false)
   , mIsTransformSeparator(false)
@@ -8135,17 +8101,17 @@ nsDisplayTransform::Init(nsDisplayListBu
 }
 
 nsDisplayTransform::nsDisplayTransform(nsDisplayListBuilder* aBuilder,
                                        nsIFrame* aFrame,
                                        nsDisplayList* aList,
                                        const nsRect& aChildrenBuildingRect,
                                        uint32_t aIndex,
                                        bool aAllowAsyncAnimation)
-  : nsDisplayItem(aBuilder, aFrame)
+  : nsDisplayHitTestInfoItem(aBuilder, aFrame)
   , mStoredList(aBuilder, aFrame, aList)
   , mTransformGetter(nullptr)
   , mAnimatedGeometryRootForChildren(mAnimatedGeometryRoot)
   , mAnimatedGeometryRootForScrollMetadata(mAnimatedGeometryRoot)
   , mChildrenBuildingRect(aChildrenBuildingRect)
   , mIndex(aIndex)
   , mNoExtendContext(false)
   , mIsTransformSeparator(false)
@@ -8160,17 +8126,17 @@ nsDisplayTransform::nsDisplayTransform(n
 }
 
 nsDisplayTransform::nsDisplayTransform(nsDisplayListBuilder* aBuilder,
                                        nsIFrame* aFrame,
                                        nsDisplayList* aList,
                                        const nsRect& aChildrenBuildingRect,
                                        const Matrix4x4& aTransform,
                                        uint32_t aIndex)
-  : nsDisplayItem(aBuilder, aFrame)
+  : nsDisplayHitTestInfoItem(aBuilder, aFrame)
   , mStoredList(aBuilder, aFrame, aList)
   , mTransform(Some(aTransform))
   , mTransformGetter(nullptr)
   , mAnimatedGeometryRootForChildren(mAnimatedGeometryRoot)
   , mAnimatedGeometryRootForScrollMetadata(mAnimatedGeometryRoot)
   , mChildrenBuildingRect(aChildrenBuildingRect)
   , mIndex(aIndex)
   , mNoExtendContext(false)
@@ -8844,16 +8810,17 @@ nsDisplayTransform::CreateWebRenderComma
   }
 
   // If it looks like we're animated, we should rasterize in local space
   // (disabling subpixel-aa and global pixel snapping)
   bool animated =
     ActiveLayerTracker::IsStyleMaybeAnimated(Frame(), eCSSProperty_transform);
 
   StackingContextHelper sc(aSc,
+                           GetActiveScrolledRoot(),
                            aBuilder,
                            filters,
                            LayoutDeviceRect(position, LayoutDeviceSize()),
                            &newTransformMatrix,
                            animationsId ? &prop : nullptr,
                            nullptr,
                            transformForSC,
                            nullptr,
@@ -9375,17 +9342,17 @@ nsDisplayTransform::WriteDebugInfo(std::
   if (mFrame->Combines3DTransformWithAncestors()) {
     aStream << " combines-3d-with-ancestors";
   }
 }
 
 nsDisplayPerspective::nsDisplayPerspective(nsDisplayListBuilder* aBuilder,
                                            nsIFrame* aFrame,
                                            nsDisplayList* aList)
-  : nsDisplayItem(aBuilder, aFrame)
+  : nsDisplayHitTestInfoItem(aBuilder, aFrame)
   , mList(aBuilder, aFrame, aList, true)
 {
   MOZ_ASSERT(mList.GetChildren()->Count() == 1);
   MOZ_ASSERT(mList.GetChildren()->GetTop()->GetType() ==
              DisplayItemType::TYPE_TRANSFORM);
   mAnimatedGeometryRoot = aBuilder->FindAnimatedGeometryRootFor(
     mFrame->GetContainingBlock(nsIFrame::SKIP_SCROLLED_FRAME));
 }
@@ -9493,16 +9460,17 @@ nsDisplayPerspective::CreateWebRenderCom
   Point3D roundedOrigin(NS_round(newOrigin.x), NS_round(newOrigin.y), 0);
 
   gfx::Matrix4x4 transformForSC = gfx::Matrix4x4::Translation(roundedOrigin);
   
   nsIFrame* perspectiveFrame = mFrame->GetContainingBlock(nsIFrame::SKIP_SCROLLED_FRAME);
 
   nsTArray<mozilla::wr::WrFilterOp> filters;
   StackingContextHelper sc(aSc,
+                           GetActiveScrolledRoot(),
                            aBuilder,
                            filters,
                            LayoutDeviceRect(),
                            nullptr,
                            nullptr,
                            nullptr,
                            &transformForSC,
                            &perspectiveMatrix,
@@ -10179,16 +10147,17 @@ nsDisplayMasksAndClipPaths::CreateWebRen
 
     wr::WrClipId clipId = clip->first();
 
     Maybe<float> opacity = clip->second() == HandleOpacity::Yes
       ? Some(mFrame->StyleEffects()->mOpacity)
       : Nothing();
 
     layer.emplace(aSc,
+                  GetActiveScrolledRoot(),
                   aBuilder,
                   /*aFilters: */ nsTArray<wr::WrFilterOp>(),
                   /*aBounds: */ bounds,
                   /*aBoundTransform: */ nullptr,
                   /*aAnimation: */ nullptr,
                   /*aOpacity: */ opacity.ptrOr(nullptr),
                   /*aTransform: */ nullptr,
                   /*aPerspective: */ nullptr,
@@ -10493,16 +10462,17 @@ nsDisplayFilters::CreateWebRenderCommand
   // currently on the clip stack for this item.
   //
   // FIXME(emilio, bug 1486557): clipping to "bounds" isn't really necessary.
   wr::WrClipId clipId =
     aBuilder.DefineClip(Nothing(), wr::ToRoundedLayoutRect(bounds));
 
   float opacity = mFrame->StyleEffects()->mOpacity;
   StackingContextHelper sc(aSc,
+                           GetActiveScrolledRoot(),
                            aBuilder,
                            wrFilters,
                            LayoutDeviceRect(),
                            nullptr,
                            nullptr,
                            opacity != 1.0f && mHandleOpacity ? &opacity
                                                              : nullptr,
                            nullptr,
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -469,16 +469,17 @@ public:
   typedef mozilla::DisplayItemClipChainHasher DisplayItemClipChainHasher;
   typedef mozilla::DisplayItemClipChainEqualer DisplayItemClipChainEqualer;
   typedef mozilla::DisplayListClipState DisplayListClipState;
   typedef mozilla::ActiveScrolledRoot ActiveScrolledRoot;
   typedef nsIWidget::ThemeGeometry ThemeGeometry;
   typedef mozilla::layers::Layer Layer;
   typedef mozilla::layers::FrameMetrics FrameMetrics;
   typedef mozilla::layers::FrameMetrics::ViewID ViewID;
+  typedef mozilla::gfx::CompositorHitTestInfo CompositorHitTestInfo;
   typedef mozilla::gfx::Matrix4x4 Matrix4x4;
   typedef mozilla::Maybe<mozilla::layers::ScrollDirection> MaybeScrollDirection;
 
   /**
    * @param aReferenceFrame the frame at the root of the subtree; its origin
    * is the origin of the reference coordinate system for this display list
    * @param aMode encodes what the builder is being used for.
    * @param aBuildCaret whether or not we should include the caret in any
@@ -790,29 +791,30 @@ public:
    */
   bool AllowMergingAndFlattening() { return mAllowMergingAndFlattening; }
   void SetAllowMergingAndFlattening(bool aAllow)
   {
     mAllowMergingAndFlattening = aAllow;
   }
 
   /**
-   * Sets the current compositor hit test info to |aHitTestInfo|.
+   * Sets the current compositor hit test area and info to |aHitTestArea| and
+   * |aHitTestInfo|.
    * This is used during display list building to determine if the parent frame
    * hit test info contains the same information that child frame needs.
    */
-  void SetCompositorHitTestInfo(nsDisplayCompositorHitTestInfo* aHitTestInfo)
-  {
-    mCompositorHitTestInfo = aHitTestInfo;
-  }
-
-  nsDisplayCompositorHitTestInfo* GetCompositorHitTestInfo() const
-  {
-    return mCompositorHitTestInfo;
-  }
+  void SetCompositorHitTestInfo(const nsRect& aHitTestArea,
+                                const CompositorHitTestInfo& aHitTestInfo)
+  {
+    mHitTestArea = aHitTestArea;
+    mHitTestInfo = aHitTestInfo;
+  }
+
+  const nsRect& GetHitTestArea() const { return mHitTestArea; }
+  const CompositorHitTestInfo& GetHitTestInfo() const { return mHitTestInfo; }
 
   /**
    * Builds a new nsDisplayCompositorHitTestInfo for the frame |aFrame| if
    * needed, and adds it to the top of |aList|. If |aBuildNew| is true, the
    * previous hit test info will not be reused.
    */
   void BuildCompositorHitTestInfoIfNeeded(nsIFrame* aFrame,
                                           nsDisplayList* aList,
@@ -1166,17 +1168,18 @@ public:
     AutoBuildingDisplayList(nsDisplayListBuilder* aBuilder,
                             nsIFrame* aForChild,
                             const nsRect& aVisibleRect,
                             const nsRect& aDirtyRect,
                             bool aIsRoot)
       : mBuilder(aBuilder)
       , mPrevFrame(aBuilder->mCurrentFrame)
       , mPrevReferenceFrame(aBuilder->mCurrentReferenceFrame)
-      , mPrevCompositorHitTestInfo(aBuilder->mCompositorHitTestInfo)
+      , mPrevHitTestArea(aBuilder->mHitTestArea)
+      , mPrevHitTestInfo(aBuilder->mHitTestInfo)
       , mPrevOffset(aBuilder->mCurrentOffsetToReferenceFrame)
       , mPrevVisibleRect(aBuilder->mVisibleRect)
       , mPrevDirtyRect(aBuilder->mDirtyRect)
       , mPrevAGR(aBuilder->mCurrentAGR)
       , mPrevIsAtRootOfPseudoStackingContext(
           aBuilder->mIsAtRootOfPseudoStackingContext)
       , mPrevAncestorHasApzAwareEventHandler(
           aBuilder->mAncestorHasApzAwareEventHandler)
@@ -1232,17 +1235,18 @@ public:
     {
       mBuilder->mBuildingInvisibleItems = mPrevBuildingInvisibleItems;
     }
 
     ~AutoBuildingDisplayList()
     {
       mBuilder->mCurrentFrame = mPrevFrame;
       mBuilder->mCurrentReferenceFrame = mPrevReferenceFrame;
-      mBuilder->mCompositorHitTestInfo = mPrevCompositorHitTestInfo;
+      mBuilder->mHitTestArea = mPrevHitTestArea;
+      mBuilder->mHitTestInfo = mPrevHitTestInfo;
       mBuilder->mCurrentOffsetToReferenceFrame = mPrevOffset;
       mBuilder->mVisibleRect = mPrevVisibleRect;
       mBuilder->mDirtyRect = mPrevDirtyRect;
       mBuilder->mCurrentAGR = mPrevAGR;
       mBuilder->mIsAtRootOfPseudoStackingContext =
         mPrevIsAtRootOfPseudoStackingContext;
       mBuilder->mAncestorHasApzAwareEventHandler =
         mPrevAncestorHasApzAwareEventHandler;
@@ -1250,17 +1254,18 @@ public:
       mBuilder->mInInvalidSubtree = mPrevInInvalidSubtree;
     }
 
   private:
     nsDisplayListBuilder* mBuilder;
     AGRState mCurrentAGRState;
     const nsIFrame* mPrevFrame;
     const nsIFrame* mPrevReferenceFrame;
-    nsDisplayCompositorHitTestInfo* mPrevCompositorHitTestInfo;
+    nsRect mPrevHitTestArea;
+    CompositorHitTestInfo mPrevHitTestInfo;
     nsPoint mPrevOffset;
     nsRect mPrevVisibleRect;
     nsRect mPrevDirtyRect;
     RefPtr<AnimatedGeometryRoot> mPrevAGR;
     bool mPrevIsAtRootOfPseudoStackingContext;
     bool mPrevAncestorHasApzAwareEventHandler;
     bool mPrevBuildingInvisibleItems;
     bool mPrevInInvalidSubtree;
@@ -1942,35 +1947,26 @@ private:
                                   nsIFrame** aParent = nullptr);
 
   /**
    * Returns the nearest ancestor frame to aFrame that is considered to have
    * (or will have) animated geometry. This can return aFrame.
    */
   nsIFrame* FindAnimatedGeometryRootFrameFor(nsIFrame* aFrame, bool& aIsAsync);
 
-  /**
-   * Returns true if nsDisplayCompositorHitTestInfo item should be build for
-   * |aFrame|. Otherwise returns false. If |aBuildNew| is true, reusing the
-   * previous hit test info will not be considered.
-   */
-  bool ShouldBuildCompositorHitTestInfo(
-    const nsIFrame* aFrame,
-    const mozilla::gfx::CompositorHitTestInfo& aInfo,
-    const bool aBuildNew) const;
-
   friend class nsDisplayCanvasBackgroundImage;
   friend class nsDisplayBackgroundImage;
   friend class nsDisplayFixedPosition;
   friend class nsDisplayPerspective;
   AnimatedGeometryRoot* FindAnimatedGeometryRootFor(nsDisplayItem* aItem);
 
   friend class nsDisplayItem;
   friend class nsDisplayOwnLayer;
   friend struct RetainedDisplayListBuilder;
+  friend struct HitTestInfo;
   AnimatedGeometryRoot* FindAnimatedGeometryRootFor(nsIFrame* aFrame);
 
   AnimatedGeometryRoot* WrapAGRForFrame(
     nsIFrame* aAnimatedGeometryRoot,
     bool aIsAsync,
     AnimatedGeometryRoot* aParent = nullptr);
 
   nsDataHashtable<nsPtrHashKey<nsIFrame>, RefPtr<AnimatedGeometryRoot>>
@@ -2033,18 +2029,16 @@ private:
     }
 
     nsPresContext* mPresContext;
     uint32_t mUsage;
   };
 
   nsIFrame* const mReferenceFrame;
   nsIFrame* mIgnoreScrollFrame;
-  nsDisplayCompositorHitTestInfo* mCompositorHitTestInfo;
-
   nsPresArena mPool;
 
   RefPtr<mozilla::dom::Selection> mBoundingSelection;
   AutoTArray<PresShellState, 8> mPresShellStates;
   AutoTArray<nsIFrame*, 400> mFramesMarkedForDisplay;
   AutoTArray<nsIFrame*, 40> mFramesMarkedForDisplayIfVisible;
   AutoTArray<nsIFrame*, 20> mFramesWithOOFData;
   nsClassHashtable<nsPtrHashKey<nsDisplayItem>, nsTArray<ThemeGeometry>>
@@ -2153,20 +2147,22 @@ private:
   bool mIsBuildingForPopup;
   bool mForceLayerForScrollParent;
   bool mAsyncPanZoomEnabled;
   bool mBuildingInvisibleItems;
   bool mHitTestIsForVisibility;
   bool mIsBuilding;
   bool mInInvalidSubtree;
   bool mBuildCompositorHitTestInfo;
-  bool mLessEventRegionItems;
   bool mDisablePartialUpdates;
   bool mPartialBuildFailed;
   bool mIsInActiveDocShell;
+
+  nsRect mHitTestArea;
+  CompositorHitTestInfo mHitTestInfo;
 };
 
 class nsDisplayItem;
 class nsDisplayList;
 class RetainedDisplayList;
 /**
  * nsDisplayItems are put in singly-linked lists rooted in an nsDisplayList.
  * nsDisplayItemLink holds the link. The lists are linked from lowest to
@@ -2256,16 +2252,17 @@ public:
   typedef mozilla::layers::LayerManager LayerManager;
   typedef mozilla::layers::StackingContextHelper StackingContextHelper;
   typedef mozilla::layers::WebRenderCommand WebRenderCommand;
   typedef mozilla::layers::WebRenderParentCommand WebRenderParentCommand;
   typedef mozilla::LayerState LayerState;
   typedef mozilla::image::imgDrawingParams imgDrawingParams;
   typedef mozilla::image::ImgDrawResult ImgDrawResult;
   typedef class mozilla::gfx::DrawTarget DrawTarget;
+  typedef mozilla::gfx::CompositorHitTestInfo CompositorHitTestInfo;
 
   // This is never instantiated directly (it has pure virtual methods), so no
   // need to count constructors and destructors.
   nsDisplayItem(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame);
   nsDisplayItem(nsDisplayListBuilder* aBuilder,
                 nsIFrame* aFrame,
                 const ActiveScrolledRoot* aActiveScrolledRoot,
                 bool aAnonymous = false);
@@ -3137,16 +3134,22 @@ public:
       return false;
     }
     *aOutIndex = mOldListIndex;
     return true;
   }
 
   const nsRect& GetPaintRect() const { return mPaintRect; }
 
+  virtual bool HasHitTestInfo() const { return false; }
+
+#ifdef DEBUG
+  virtual bool IsHitTestItem() const { return false; }
+#endif
+
 protected:
   typedef bool (*PrefFunc)(void);
   bool ShouldUseAdvancedLayer(LayerManager* aManager, PrefFunc aFunc) const;
   bool CanUseAdvancedLayer(LayerManager* aManager) const;
 
   nsIFrame* mFrame;
   RefPtr<const DisplayItemClipChain> mClipChain;
   const DisplayItemClip* mClip;
@@ -3807,44 +3810,147 @@ protected:
   {
     // Handle the case where we reach the end of a nested list, or the current
     // item should start a new nested list. Repeat this until we find an actual
     // item, or the very end of the outer list.
     while (AtEndOfNestedList() || ShouldFlattenNextItem()) {
       if (AtEndOfNestedList()) {
         // Pop the last item off the stack.
         mNext = mStack.LastElement();
-        EndNested(mNext);
+        ExitChildList(mNext);
         mStack.RemoveElementAt(mStack.Length() - 1);
         // We stored the item that was flattened, so advance to the next.
         mNext = mNext->GetAbove();
       } else {
         // This item wants to be flattened. Store the current item on the stack,
         // and use the first item in the child list instead.
         mStack.AppendElement(mNext);
-        StartNested(mNext);
+        EnterChildList(mNext);
 
         nsDisplayList* childItems =
           mNext->GetType() != DisplayItemType::TYPE_TRANSFORM
             ? mNext->GetSameCoordinateSystemChildren()
             : mNext->GetChildren();
 
         mNext = childItems->GetBottom();
       }
     }
   }
 
-  virtual void EndNested(nsDisplayItem* aItem) {}
-  virtual void StartNested(nsDisplayItem* aItem) {}
+  virtual void ExitChildList(nsDisplayItem* aItem) {}
+  virtual void EnterChildList(nsDisplayItem* aItem) {}
 
   nsDisplayListBuilder* mBuilder;
   nsDisplayItem* mNext;
   AutoTArray<nsDisplayItem*, 10> mStack;
 };
 
+struct HitTestInfo
+{
+  HitTestInfo(nsDisplayListBuilder* aBuilder,
+              nsIFrame* aFrame,
+              const mozilla::gfx::CompositorHitTestInfo& aHitTestFlags)
+    : mArea(aFrame->GetCompositorHitTestArea(aBuilder))
+    , mFlags(aHitTestFlags)
+    , mAGR(aBuilder->FindAnimatedGeometryRootFor(aFrame))
+    , mASR(aBuilder->CurrentActiveScrolledRoot())
+    , mClipChain(aBuilder->ClipState().GetCurrentCombinedClipChain(aBuilder))
+    , mClip(mozilla::DisplayItemClipChain::ClipForASR(mClipChain, mASR))
+  {
+  }
+
+  HitTestInfo(const nsRect& aArea,
+              const mozilla::gfx::CompositorHitTestInfo& aHitTestFlags)
+    : mArea(aArea)
+    , mFlags(aHitTestFlags)
+    , mAGR(nullptr)
+    , mASR(nullptr)
+    , mClipChain(nullptr)
+    , mClip(nullptr)
+  {
+  }
+
+  nsRect mArea;
+  mozilla::gfx::CompositorHitTestInfo mFlags;
+
+  AnimatedGeometryRoot* mAGR;
+  const mozilla::ActiveScrolledRoot* mASR;
+  RefPtr<const mozilla::DisplayItemClipChain> mClipChain;
+  const mozilla::DisplayItemClip* mClip;
+};
+
+class nsDisplayHitTestInfoItem : public nsDisplayItem
+{
+public:
+  nsDisplayHitTestInfoItem(nsDisplayListBuilder* aBuilder,
+                           nsIFrame* aFrame)
+    : nsDisplayItem(aBuilder, aFrame)
+  {
+  }
+
+  nsDisplayHitTestInfoItem(nsDisplayListBuilder* aBuilder,
+                           nsIFrame* aFrame,
+                           const ActiveScrolledRoot* aActiveScrolledRoot,
+                           bool aAnonymous = false)
+    : nsDisplayItem(aBuilder, aFrame, aActiveScrolledRoot, aAnonymous)
+  {
+  }
+
+  nsDisplayHitTestInfoItem(nsDisplayListBuilder* aBuilder,
+                           const nsDisplayHitTestInfoItem& aOther)
+    : nsDisplayItem(aBuilder, aOther)
+  {
+  }
+
+  const HitTestInfo& GetHitTestInfo() const
+  {
+    return *mHitTestInfo;
+  }
+
+  void SetHitTestInfo(mozilla::UniquePtr<HitTestInfo>&& aHitTestInfo)
+  {
+    MOZ_ASSERT(aHitTestInfo);
+    MOZ_ASSERT(aHitTestInfo->mFlags !=
+      mozilla::gfx::CompositorHitTestInvisibleToHit);
+
+    mHitTestInfo = std::move(aHitTestInfo);
+  }
+
+  void SetHitTestInfo(const nsRect& aArea,
+                      const mozilla::gfx::CompositorHitTestInfo& aHitTestFlags)
+  {
+    MOZ_ASSERT(aHitTestFlags != mozilla::gfx::CompositorHitTestInvisibleToHit);
+
+    mHitTestInfo = mozilla::MakeUnique<HitTestInfo>(aArea, aHitTestFlags);
+    mHitTestInfo->mAGR = mAnimatedGeometryRoot;
+    mHitTestInfo->mASR = mActiveScrolledRoot;
+    mHitTestInfo->mClipChain = mClipChain;
+    mHitTestInfo->mClip = mClip;
+  }
+
+  const nsRect& HitTestArea() const
+  {
+    return mHitTestInfo->mArea;
+  }
+
+  const mozilla::gfx::CompositorHitTestInfo& HitTestFlags() const
+  {
+    return mHitTestInfo->mFlags;
+  }
+
+  bool HasHitTestInfo() const override { return mHitTestInfo.get(); }
+
+#ifdef DEBUG
+  bool IsHitTestItem() const override { return true; }
+#endif
+
+protected:
+  mozilla::UniquePtr<HitTestInfo> mHitTestInfo;
+};
+
 class nsDisplayImageContainer : public nsDisplayItem
 {
 public:
   typedef mozilla::LayerIntPoint LayerIntPoint;
   typedef mozilla::LayoutDeviceRect LayoutDeviceRect;
   typedef mozilla::layers::ImageContainer ImageContainer;
   typedef mozilla::layers::ImageLayer ImageLayer;
 
@@ -5257,57 +5363,53 @@ public:
 };
 
 /**
  * Similar to nsDisplayEventReceiver in that it is used for hit-testing. However
  * this gets built when we're doing widget painting and we need to send the
  * compositor some hit-test info for a frame. This is effectively a dummy item
  * whose sole purpose is to carry the hit-test info to the compositor.
  */
-class nsDisplayCompositorHitTestInfo : public nsDisplayEventReceiver
+class nsDisplayCompositorHitTestInfo : public nsDisplayHitTestInfoItem
 {
 public:
   nsDisplayCompositorHitTestInfo(
     nsDisplayListBuilder* aBuilder,
     nsIFrame* aFrame,
-    mozilla::gfx::CompositorHitTestInfo aHitTestInfo,
+    const mozilla::gfx::CompositorHitTestInfo& aHitTestFlags,
     uint32_t aIndex = 0,
     const mozilla::Maybe<nsRect>& aArea = mozilla::Nothing());
 
+  nsDisplayCompositorHitTestInfo(
+    nsDisplayListBuilder* aBuilder,
+    nsIFrame* aFrame,
+    mozilla::UniquePtr<HitTestInfo>&& aHitTestInfo);
+
 #ifdef NS_BUILD_REFCNT_LOGGING
   ~nsDisplayCompositorHitTestInfo() override
   {
     MOZ_COUNT_DTOR(nsDisplayCompositorHitTestInfo);
   }
 #endif
 
   NS_DISPLAY_DECL_NAME("CompositorHitTestInfo", TYPE_COMPOSITOR_HITTEST_INFO)
 
-  mozilla::gfx::CompositorHitTestInfo HitTestInfo() const
-  {
-    return mHitTestInfo;
-  }
+  void InitializeScrollTarget(nsDisplayListBuilder* aBuilder);
 
   bool CreateWebRenderCommands(
     mozilla::wr::DisplayListBuilder& aBuilder,
     mozilla::wr::IpcResourceUpdateQueue& aResources,
     const StackingContextHelper& aSc,
     mozilla::layers::WebRenderLayerManager* aManager,
     nsDisplayListBuilder* aDisplayListBuilder) override;
-  void WriteDebugInfo(std::stringstream& aStream) override;
   uint32_t GetPerFrameKey() const override;
   int32_t ZIndex() const override;
   void SetOverrideZIndex(int32_t aZIndex);
 
   /**
-   * Returns the hit test area of this item.
-   */
-  const nsRect& Area() const { return mArea; }
-
-  /**
    * ApplyOpacity() is overriden for opacity flattening.
    */
   void ApplyOpacity(nsDisplayListBuilder* aBuilder,
                     float aOpacity,
                     const DisplayItemClipChain* aClip) override
   {
   }
 
@@ -5318,19 +5420,17 @@ public:
 
   nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) const override
   {
     *aSnap = false;
     return nsRect();
   }
 
 private:
-  mozilla::gfx::CompositorHitTestInfo mHitTestInfo;
   mozilla::Maybe<mozilla::layers::FrameMetrics::ViewID> mScrollTarget;
-  nsRect mArea;
   uint32_t mIndex;
   mozilla::Maybe<int32_t> mOverrideZIndex;
   int32_t mAppUnitsPerDevPixel;
 };
 
 /**
  * A class that lets you wrap a display list as a display item.
  *
@@ -5340,17 +5440,17 @@ private:
  * underlying frame should be the root of a stacking context, because sorting
  * a list containing this item will not get at the children.
  *
  * In some cases (e.g., clipping) we want to wrap a list but we don't have a
  * particular underlying frame that is a stacking context root. In that case
  * we allow the frame to be nullptr. Callers to GetUnderlyingFrame must
  * detect and handle this case.
  */
-class nsDisplayWrapList : public nsDisplayItem
+class nsDisplayWrapList : public nsDisplayHitTestInfoItem
 {
 public:
   /**
    * Takes all the items from aList and puts them in our list.
    */
   nsDisplayWrapList(nsDisplayListBuilder* aBuilder,
                     nsIFrame* aFrame,
                     nsDisplayList* aList,
@@ -5362,17 +5462,17 @@ public:
                     bool aClearClipChain = false,
                     uint32_t aIndex = 0,
                     bool aAnonymous = false);
   nsDisplayWrapList(nsDisplayListBuilder* aBuilder,
                     nsIFrame* aFrame,
                     nsDisplayItem* aItem,
                     bool aAnonymous = false);
   nsDisplayWrapList(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame)
-    : nsDisplayItem(aBuilder, aFrame)
+    : nsDisplayHitTestInfoItem(aBuilder, aFrame)
     , mFrameActiveScrolledRoot(aBuilder->CurrentActiveScrolledRoot())
     , mOverrideZIndex(0)
     , mIndex(0)
     , mHasZIndexOverride(false)
   {
     MOZ_COUNT_CTOR(nsDisplayWrapList);
     mBaseBuildingRect = GetBuildingRect();
     mListPtr = &mList;
@@ -5382,17 +5482,17 @@ public:
 
   /**
    * A custom copy-constructor that does not copy mList, as this would mutate
    * the other item.
    */
   nsDisplayWrapList(const nsDisplayWrapList& aOther) = delete;
   nsDisplayWrapList(nsDisplayListBuilder* aBuilder,
                     const nsDisplayWrapList& aOther)
-    : nsDisplayItem(aBuilder, aOther)
+    : nsDisplayHitTestInfoItem(aBuilder, aOther)
     , mListPtr(&mList)
     , mFrameActiveScrolledRoot(aOther.mFrameActiveScrolledRoot)
     , mMergedFrames(aOther.mMergedFrames)
     , mBounds(aOther.mBounds)
     , mBaseBuildingRect(aOther.mBaseBuildingRect)
     , mOverrideZIndex(aOther.mOverrideZIndex)
     , mIndex(aOther.mIndex)
     , mHasZIndexOverride(aOther.mHasZIndexOverride)
@@ -6923,17 +7023,17 @@ private:
  * because CSS-transforms allow percentage values for the x and y components
  * of <translation-value>s, where percentages are percentages of the element's
  * border box.
  *
  * INVARIANT: The wrapped frame is transformed or we supplied a transform getter
  * function.
  * INVARIANT: The wrapped frame is non-null.
  */
-class nsDisplayTransform : public nsDisplayItem
+class nsDisplayTransform : public nsDisplayHitTestInfoItem
 {
   typedef mozilla::gfx::Matrix4x4 Matrix4x4;
   typedef mozilla::gfx::Matrix4x4Flagged Matrix4x4Flagged;
   typedef mozilla::gfx::Point3D Point3D;
 
   /*
    * Avoid doing UpdateBounds() during construction.
    */
@@ -7432,17 +7532,17 @@ private:
   bool mShouldFlatten;
 };
 
 /* A display item that applies a perspective transformation to a single
  * nsDisplayTransform child item. We keep this as a separate item since the
  * perspective-origin is relative to an ancestor of the transformed frame, and
  * APZ can scroll the child separately.
  */
-class nsDisplayPerspective : public nsDisplayItem
+class nsDisplayPerspective : public nsDisplayHitTestInfoItem
 {
   typedef mozilla::gfx::Point3D Point3D;
 
 public:
   nsDisplayPerspective(nsDisplayListBuilder* aBuilder,
                        nsIFrame* aFrame,
                        nsDisplayList* aList);
   ~nsDisplayPerspective() override = default;
--- a/layout/style/ServoBindings.h
+++ b/layout/style/ServoBindings.h
@@ -598,16 +598,20 @@ RawServoDeclarationBlockStrong Servo_Par
   nsCompatibility quirks_mode,
   mozilla::css::Loader* loader);
 
 bool Servo_ParseEasing(
   const nsAString* easing,
   RawGeckoURLExtraData* data,
   nsTimingFunctionBorrowedMut output);
 
+void Servo_SerializeEasing(
+  nsTimingFunctionBorrowed easing,
+  nsAString* output);
+
 void Servo_GetComputedKeyframeValues(
   RawGeckoKeyframeListBorrowed keyframes,
   RawGeckoElementBorrowed element,
   ComputedStyleBorrowed style,
   RawServoStyleSetBorrowed set,
   RawGeckoComputedKeyframeValuesListBorrowedMut result);
 
 RawServoAnimationValueStrong Servo_ComputedValues_ExtractAnimationValue(
--- a/layout/style/ServoCSSPropList.mako.py
+++ b/layout/style/ServoCSSPropList.mako.py
@@ -98,16 +98,17 @@ SERIALIZED_PREDEFINED_TYPES = [
     "LengthOrPercentage",
     "NonNegativeLength",
     "NonNegativeLengthOrPercentage",
     "ListStyleType",
     "OffsetPath",
     "Opacity",
     "Resize",
     "TextAlign",
+    "TimingFunction",
     "TransformStyle",
     "background::BackgroundSize",
     "basic_shape::ClippingShape",
     "basic_shape::FloatAreaShape",
     "position::HorizontalPosition",
     "position::VerticalPosition",
     "url::ImageUrlOrNone",
 ]
--- a/layout/style/nsCSSKeywordList.h
+++ b/layout/style/nsCSSKeywordList.h
@@ -100,20 +100,16 @@ CSS_KEY(dashed, dashed)
 CSS_KEY(dense, dense)
 CSS_KEY(default, default)
 CSS_KEY(dot, dot)
 CSS_KEY(dotted, dotted)
 CSS_KEY(double, double)
 CSS_KEY(double-circle, double_circle)
 CSS_KEY(drop-shadow, drop_shadow)
 CSS_KEY(e-resize, e_resize)
-CSS_KEY(ease, ease)
-CSS_KEY(ease-in, ease_in)
-CSS_KEY(ease-in-out, ease_in_out)
-CSS_KEY(ease-out, ease_out)
 CSS_KEY(ellipse, ellipse)
 CSS_KEY(ellipsis, ellipsis)
 CSS_KEY(end, end)
 CSS_KEY(ew-resize, ew_resize)
 CSS_KEY(farthest-side, farthest_side)
 CSS_KEY(fill, fill)
 CSS_KEY(filled, filled)
 CSS_KEY(flex, flex)
@@ -139,17 +135,16 @@ CSS_KEY(interpolatematrix, interpolatema
 CSS_KEY(accumulatematrix, accumulatematrix)
 CSS_KEY(invert, invert)
 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(linear, linear)
 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)
--- a/layout/style/nsCSSProps.cpp
+++ b/layout/style/nsCSSProps.cpp
@@ -613,25 +613,16 @@ const KTableEntry nsCSSProps::kTouchActi
   { eCSSKeyword_none,         NS_STYLE_TOUCH_ACTION_NONE },
   { eCSSKeyword_auto,         NS_STYLE_TOUCH_ACTION_AUTO },
   { eCSSKeyword_pan_x,        NS_STYLE_TOUCH_ACTION_PAN_X },
   { eCSSKeyword_pan_y,        NS_STYLE_TOUCH_ACTION_PAN_Y },
   { eCSSKeyword_manipulation, NS_STYLE_TOUCH_ACTION_MANIPULATION },
   { eCSSKeyword_UNKNOWN,      -1 }
 };
 
-const KTableEntry nsCSSProps::kTransitionTimingFunctionKTable[] = {
-  { eCSSKeyword_linear,       StyleTimingKeyword::Linear },
-  { eCSSKeyword_ease,         StyleTimingKeyword::Ease },
-  { eCSSKeyword_ease_in,      StyleTimingKeyword::EaseIn },
-  { eCSSKeyword_ease_out,     StyleTimingKeyword::EaseOut },
-  { eCSSKeyword_ease_in_out,  StyleTimingKeyword::EaseInOut },
-  { eCSSKeyword_UNKNOWN, -1 }
-};
-
 const KTableEntry nsCSSProps::kVerticalAlignKTable[] = {
   { eCSSKeyword_baseline, NS_STYLE_VERTICAL_ALIGN_BASELINE },
   { eCSSKeyword_sub, NS_STYLE_VERTICAL_ALIGN_SUB },
   { eCSSKeyword_super, NS_STYLE_VERTICAL_ALIGN_SUPER },
   { eCSSKeyword_top, NS_STYLE_VERTICAL_ALIGN_TOP },
   { eCSSKeyword_text_top, NS_STYLE_VERTICAL_ALIGN_TEXT_TOP },
   { eCSSKeyword_middle, NS_STYLE_VERTICAL_ALIGN_MIDDLE },
   { eCSSKeyword__moz_middle_with_baseline, NS_STYLE_VERTICAL_ALIGN_MIDDLE_WITH_BASELINE },
--- a/layout/style/nsCSSProps.h
+++ b/layout/style/nsCSSProps.h
@@ -346,15 +346,14 @@ public:
   static const KTableEntry kOverscrollBehaviorKTable[];
   static const KTableEntry kScrollSnapTypeKTable[];
   static const KTableEntry kTextAlignKTable[];
   static const KTableEntry kTextDecorationLineKTable[];
   static const KTableEntry kTextDecorationStyleKTable[];
   static const KTableEntry kTextEmphasisStyleShapeKTable[];
   static const KTableEntry kTextOverflowKTable[];
   static const KTableEntry kTouchActionKTable[];
-  static const KTableEntry kTransitionTimingFunctionKTable[];
   static const KTableEntry kVerticalAlignKTable[];
   static const KTableEntry kWidthKTable[]; // also min-width, max-width
   static const KTableEntry kFlexBasisKTable[];
 };
 
 #endif /* nsCSSProps_h___ */
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -4571,66 +4571,16 @@ nsComputedDOMStyle::DoGetTransitionPrope
       property->SetString(nsCSSProps::GetStringValue(cssprop));
 
     valueList->AppendCSSValue(property.forget());
   } while (++i < display->mTransitionPropertyCount);
 
   return valueList.forget();
 }
 
-void
-nsComputedDOMStyle::AppendTimingFunction(nsDOMCSSValueList *aValueList,
-                                         const nsTimingFunction& aTimingFunction)
-{
-  RefPtr<nsROCSSPrimitiveValue> timingFunction = new nsROCSSPrimitiveValue;
-
-  nsAutoString tmp;
-  switch (aTimingFunction.mTiming.tag) {
-    case StyleComputedTimingFunction::Tag::CubicBezier:
-      nsStyleUtil::AppendCubicBezierTimingFunction(
-        aTimingFunction.mTiming.cubic_bezier.x1,
-        aTimingFunction.mTiming.cubic_bezier.y1,
-        aTimingFunction.mTiming.cubic_bezier.x2,
-        aTimingFunction.mTiming.cubic_bezier.y2,
-        tmp);
-      break;
-    case StyleComputedTimingFunction::Tag::Steps:
-      nsStyleUtil::AppendStepsTimingFunction(
-        aTimingFunction.mTiming.steps._0,
-        aTimingFunction.mTiming.steps._1,
-        tmp);
-      break;
-    case StyleComputedTimingFunction::Tag::Keyword:
-      nsStyleUtil::AppendCubicBezierKeywordTimingFunction(
-        aTimingFunction.mTiming.keyword._0,
-        tmp);
-      break;
-  }
-  timingFunction->SetString(tmp);
-  aValueList->AppendCSSValue(timingFunction.forget());
-}
-
-already_AddRefed<CSSValue>
-nsComputedDOMStyle::DoGetTransitionTimingFunction()
-{
-  const nsStyleDisplay* display = StyleDisplay();
-
-  RefPtr<nsDOMCSSValueList> valueList = GetROCSSValueList(true);
-
-  MOZ_ASSERT(display->mTransitionTimingFunctionCount > 0,
-             "first item must be explicit");
-  uint32_t i = 0;
-  do {
-    AppendTimingFunction(valueList,
-                         display->mTransitions[i].GetTimingFunction());
-  } while (++i < display->mTransitionTimingFunctionCount);
-
-  return valueList.forget();
-}
-
 already_AddRefed<CSSValue>
 nsComputedDOMStyle::DoGetAnimationName()
 {
   const nsStyleDisplay* display = StyleDisplay();
 
   RefPtr<nsDOMCSSValueList> valueList = GetROCSSValueList(true);
 
   MOZ_ASSERT(display->mAnimationNameCount > 0,
@@ -4692,34 +4642,16 @@ nsComputedDOMStyle::DoGetAnimationDurati
     duration->SetTime((float)animation->GetDuration() / (float)PR_MSEC_PER_SEC);
     valueList->AppendCSSValue(duration.forget());
   } while (++i < display->mAnimationDurationCount);
 
   return valueList.forget();
 }
 
 already_AddRefed<CSSValue>
-nsComputedDOMStyle::DoGetAnimationTimingFunction()
-{
-  const nsStyleDisplay* display = StyleDisplay();
-
-  RefPtr<nsDOMCSSValueList> valueList = GetROCSSValueList(true);
-
-  MOZ_ASSERT(display->mAnimationTimingFunctionCount > 0,
-             "first item must be explicit");
-  uint32_t i = 0;
-  do {
-    AppendTimingFunction(valueList,
-                         display->mAnimations[i].GetTimingFunction());
-  } while (++i < display->mAnimationTimingFunctionCount);
-
-  return valueList.forget();
-}
-
-already_AddRefed<CSSValue>
 nsComputedDOMStyle::DoGetAnimationIterationCount()
 {
   const nsStyleDisplay* display = StyleDisplay();
 
   RefPtr<nsDOMCSSValueList> valueList = GetROCSSValueList(true);
 
   MOZ_ASSERT(display->mAnimationIterationCountCount > 0,
              "first item must be explicit");
--- a/layout/style/nsComputedDOMStyle.h
+++ b/layout/style/nsComputedDOMStyle.h
@@ -43,17 +43,16 @@ class nsDOMCSSValueList;
 struct nsMargin;
 class nsROCSSPrimitiveValue;
 class nsStyleCoord;
 class nsStyleCorners;
 struct nsStyleFilter;
 class nsStyleGradient;
 struct nsStyleImage;
 class nsStyleSides;
-struct nsTimingFunction;
 
 class nsComputedDOMStyle final : public nsDOMCSSDeclaration
                                , public nsStubMutationObserver
 {
 private:
   // Convenience typedefs:
   typedef nsCSSKTableEntry KTableEntry;
   typedef mozilla::dom::CSSValue CSSValue;
@@ -226,18 +225,16 @@ private:
                                                bool aIsBoxShadow);
 
   void GetCSSGradientString(const nsStyleGradient* aGradient,
                             nsAString& aString);
   void GetImageRectString(nsIURI* aURI,
                           const nsStyleSides& aCropRect,
                           nsString& aString);
   already_AddRefed<CSSValue> GetScrollSnapPoints(const nsStyleCoord& aCoord);
-  void AppendTimingFunction(nsDOMCSSValueList *aValueList,
-                            const nsTimingFunction& aTimingFunction);
 
   bool ShouldHonorMinSizeAutoInAxis(mozilla::PhysicalAxis aAxis);
 
   /* Properties queryable as CSSValues.
    * To avoid a name conflict with nsIDOM*CSS2Properties, these are all
    * DoGetXXX instead of GetXXX.
    */
 
@@ -397,23 +394,21 @@ private:
   already_AddRefed<CSSValue> DoGetColumnCount();
   already_AddRefed<CSSValue> DoGetColumnWidth();
   already_AddRefed<CSSValue> DoGetColumnRuleWidth();
 
   /* CSS Transitions */
   already_AddRefed<CSSValue> DoGetTransitionProperty();
   already_AddRefed<CSSValue> DoGetTransitionDuration();
   already_AddRefed<CSSValue> DoGetTransitionDelay();
-  already_AddRefed<CSSValue> DoGetTransitionTimingFunction();
 
   /* CSS Animations */
   already_AddRefed<CSSValue> DoGetAnimationName();
   already_AddRefed<CSSValue> DoGetAnimationDuration();
   already_AddRefed<CSSValue> DoGetAnimationDelay();
-  already_AddRefed<CSSValue> DoGetAnimationTimingFunction();
   already_AddRefed<CSSValue> DoGetAnimationIterationCount();
 
   /* CSS Flexbox properties */
   already_AddRefed<CSSValue> DoGetFlexBasis();
   already_AddRefed<CSSValue> DoGetFlexGrow();
   already_AddRefed<CSSValue> DoGetFlexShrink();
 
   /* CSS Flexbox/Grid properties */
--- a/layout/style/nsStyleUtil.cpp
+++ b/layout/style/nsStyleUtil.cpp
@@ -269,87 +269,16 @@ nsStyleUtil::AppendPaintOrderValue(uint8
 
       default:
         MOZ_ASSERT_UNREACHABLE("unexpected paint-order component value");
     }
     aValue >>= NS_STYLE_PAINT_ORDER_BITWIDTH;
   }
 }
 
-/* static */ void
-nsStyleUtil::AppendStepsTimingFunction(uint32_t aStepNumber,
-                                       mozilla::StyleStepPosition aStepPos,
-                                       nsAString& aResult)
-{
-  aResult.AppendLiteral("steps(");
-  aResult.AppendInt(aStepNumber);
-  switch (aStepPos) {
-    case StyleStepPosition::JumpStart:
-      aResult.AppendLiteral(", jump-start)");
-      break;
-    case StyleStepPosition::JumpNone:
-      aResult.AppendLiteral(", jump-none)");
-      break;
-    case StyleStepPosition::JumpBoth:
-      aResult.AppendLiteral(", jump-both)");
-      break;
-    case StyleStepPosition::Start:
-      aResult.AppendLiteral(", start)");
-      break;
-    case StyleStepPosition::JumpEnd:
-    case StyleStepPosition::End:
-      aResult.AppendLiteral(")");
-      break;
-    default:
-      MOZ_ASSERT_UNREACHABLE("Unsupported timing function");
-  }
-}
-
-/* static */ void
-nsStyleUtil::AppendCubicBezierTimingFunction(float aX1, float aY1,
-                                             float aX2, float aY2,
-                                             nsAString& aResult)
-{
-  // set the value from the cubic-bezier control points
-  // (We could try to regenerate the keywords if we want.)
-  aResult.AppendLiteral("cubic-bezier(");
-  aResult.AppendFloat(aX1);
-  aResult.AppendLiteral(", ");
-  aResult.AppendFloat(aY1);
-  aResult.AppendLiteral(", ");
-  aResult.AppendFloat(aX2);
-  aResult.AppendLiteral(", ");
-  aResult.AppendFloat(aY2);
-  aResult.Append(')');
-}
-
-/* static */ void
-nsStyleUtil::AppendCubicBezierKeywordTimingFunction(
-    StyleTimingKeyword aType,
-    nsAString& aResult)
-{
-  switch (aType) {
-    case StyleTimingKeyword::Linear:
-    case StyleTimingKeyword::Ease:
-    case StyleTimingKeyword::EaseIn:
-    case StyleTimingKeyword::EaseOut:
-    case StyleTimingKeyword::EaseInOut: {
-      nsCSSKeyword keyword = nsCSSProps::ValueToKeywordEnum(
-          static_cast<int32_t>(aType),
-          nsCSSProps::kTransitionTimingFunctionKTable);
-      AppendASCIItoUTF16(nsCSSKeywords::GetStringValue(keyword),
-                         aResult);
-      break;
-    }
-    default:
-      MOZ_ASSERT_UNREACHABLE("unexpected aType");
-      break;
-  }
-}
-
 /* static */ float
 nsStyleUtil::ColorComponentToFloat(uint8_t aAlpha)
 {
   // Alpha values are expressed as decimals, so we should convert
   // back, using as few decimal places as possible for
   // round-tripping.
   // First try two decimal places:
   float rounded = NS_roundf(float(aAlpha) * 100.0f / 255.0f) / 100.0f;
--- a/layout/style/nsStyleUtil.h
+++ b/layout/style/nsStyleUtil.h
@@ -71,26 +71,16 @@ public:
 
   static void AppendPaintOrderValue(uint8_t aValue, nsAString& aResult);
 
   static void AppendCSSNumber(float aNumber, nsAString& aResult)
   {
     aResult.AppendFloat(aNumber);
   }
 
-  static void AppendStepsTimingFunction(uint32_t aStepNumber,
-                                        mozilla::StyleStepPosition aStepPos,
-                                        nsAString& aResult);
-  static void AppendCubicBezierTimingFunction(float aX1, float aY1,
-                                              float aX2, float aY2,
-                                              nsAString& aResult);
-  static void AppendCubicBezierKeywordTimingFunction(
-      mozilla::StyleTimingKeyword aType,
-      nsAString& aResult);
-
   /*
    * Convert an author-provided floating point number to an integer (0
    * ... 255) appropriate for use in the alpha component of a color.
    */
   static uint8_t FloatToColorComponent(float aAlpha)
   {
     NS_ASSERTION(0.0 <= aAlpha && aAlpha <= 1.0, "out of range");
     return NSToIntRound(aAlpha * 255);
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/www/fullscreen.html
@@ -0,0 +1,6 @@
+<html>
+    <head><title>Fullscreen</title></head>
+    <body>
+        <div id="fullscreen">Fullscreen Div</div>
+    </body>
+</html>
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -36,16 +36,17 @@ open class BaseSessionTest(noErrorCollec
         const val LOREM_IPSUM_HTML_PATH = "/assets/www/loremIpsum.html"
         const val NEW_SESSION_CHILD_HTML_PATH = "/assets/www/newSession_child.html"
         const val NEW_SESSION_HTML_PATH = "/assets/www/newSession.html"
         const val POPUP_HTML_PATH = "/assets/www/popup.html"
         const val SAVE_STATE_PATH = "/assets/www/saveState.html"
         const val TITLE_CHANGE_HTML_PATH = "/assets/www/titleChange.html"
         const val TRACKERS_PATH = "/assets/www/trackers.html"
         const val UNKNOWN_HOST_URI = "http://www.test.invalid/"
+        const val FULLSCREEN_PATH = "/assets/www/fullscreen.html"
     }
 
     @get:Rule val sessionRule = GeckoSessionTestRule()
 
     @get:Rule val errors = ErrorCollector()
 
     val mainSession get() = sessionRule.session
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -440,9 +440,43 @@ class ContentDelegateTest : BaseSessionT
                            notification,
                            equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
                 assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
             }
         })
         assertThat("Should not have focused field",
                    countAutoFillNodes({ it.isFocused }), equalTo(0))
     }
+
+    private fun goFullscreen() {
+        sessionRule.setPrefsUntilTestEnd(mapOf("full-screen-api.allow-trusted-requests-only" to false))
+        mainSession.loadTestPath(FULLSCREEN_PATH)
+        mainSession.waitForPageStop()
+        mainSession.evaluateJS("$('#fullscreen').requestFullscreen()")
+        sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
+            override  fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
+                assertThat("Div went fullscreen", fullScreen, equalTo(true))
+            }
+        })
+    }
+
+    private fun waitForFullscreenExit() {
+        sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
+            override  fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
+                assertThat("Div went fullscreen", fullScreen, equalTo(false))
+            }
+        })
+    }
+
+    @WithDevToolsAPI
+    @Test fun fullscreen() {
+        goFullscreen()
+        mainSession.evaluateJS("document.exitFullscreen()")
+        waitForFullscreenExit()
+    }
+
+    @WithDevToolsAPI
+    @Test fun sessionExitFullscreen() {
+        goFullscreen()
+        mainSession.exitFullScreen()
+        waitForFullscreenExit()
+    }
 }
--- a/servo/ports/geckolib/glue.rs
+++ b/servo/ports/geckolib/glue.rs
@@ -3463,16 +3463,24 @@ pub extern "C" fn Servo_ParseEasing(
             (*output).mTiming = parsed_easing.to_computed_value_without_context();
             true
         },
         Err(_) => false
     }
 }
 
 #[no_mangle]
+pub unsafe extern "C" fn Servo_SerializeEasing(
+    easing: nsTimingFunctionBorrowed,
+    output: *mut nsAString,
+) {
+    easing.mTiming.to_css(&mut CssWriter::new(&mut *output)).unwrap();
+}
+
+#[no_mangle]
 pub extern "C" fn Servo_GetProperties_Overriding_Animation(
     element: RawGeckoElementBorrowed,
     list: RawGeckoCSSPropertyIDListBorrowed,
     set: nsCSSPropertyIDSetBorrowedMut,
 ) {
     let element = GeckoElement(element);
     let element_data = match element.borrow_data() {
         Some(data) => data,
--- a/taskcluster/ci/repo-update/kind.yml
+++ b/taskcluster/ci/repo-update/kind.yml
@@ -63,17 +63,17 @@ jobs:
       worker-type: aws-provisioner-v1/gecko-{level}-b-linux
       worker:
          implementation: docker-worker
          os: linux
          docker-image: {in-tree: periodic-updates}
          max-run-time: 10800  # Takes 2-3 hours
          env:
             PRODUCT: firefox
-            REVIEWERS: "mtabara, jlund"
+            REVIEWERS: "ryanvm"
          command:
             - /runme.sh
          taskcluster-proxy: true
          artifacts:
             - name: 'public/build/nsSTSPreloadList.diff'
               path: '/home/worker/artifacts/nsSTSPreloadList.diff'
               type: file
             - name: 'public/build/StaticHPKPins.h.diff'
--- a/testing/mozharness/mozharness/mozilla/testing/raptor.py
+++ b/testing/mozharness/mozharness/mozilla/testing/raptor.py
@@ -105,16 +105,28 @@ class Raptor(TestingMixin, MercurialScri
             "type": "int",
             "help": "The interval between samples taken by the profiler (milliseconds)"
         }],
         [["--gecko-profile-entries"], {
             "dest": "gecko_profile_entries",
             "type": "int",
             "help": "How many samples to take with the profiler"
         }],
+        [["--page-cycles"], {
+            "dest": "page_cycles",
+            "type": "int",
+            "help": "How many times to repeat loading the test page (for page load tests); "
+                    "for benchmark tests this is how many times the benchmark test will be run"
+        }],
+        [["--page-timeout"], {
+            "dest": "page_timeout",
+            "type": "int",
+            "help": "How long to wait (ms) for one page_cycle to complete, before timing out"
+        }],
+
     ] + testing_config_options + copy.deepcopy(code_coverage_config_options)
 
     def __init__(self, **kwargs):
         kwargs.setdefault('config_options', self.config_options)
         kwargs.setdefault('all_actions', ['clobber',
                                           'download-and-extract',
                                           'populate-webroot',
                                           'install-chrome',
--- a/testing/raptor/raptor/cmdline.py
+++ b/testing/raptor/raptor/cmdline.py
@@ -32,16 +32,21 @@ def create_parser(mach_interface=False):
             "can analyze the local profiles. To disable auto-launching of perf-html.io "
             "set the DISABLE_PROFILE_LAUNCH=1 env var.")
     add_arg('--gecko-profile-interval', dest='gecko_profile_interval', type=float,
             help="How frequently to take samples (milliseconds)")
     add_arg('--gecko-profile-entries', dest="gecko_profile_entries", type=int,
             help="How many samples to take with the profiler")
     add_arg('--symbolsPath', dest='symbols_path',
             help="Path to the symbols for the build we are testing")
+    add_arg('--page-cycles', dest="page_cycles", type=int,
+            help="How many times to repeat loading the test page (for page load tests); "
+                 "for benchmark tests this is how many times the benchmark test will be run")
+    add_arg('--page-timeout', dest="page_timeout", type=int,
+            help="How long to wait (ms) for one page_cycle to complete, before timing out")
     if not mach_interface:
         add_arg('--branchName', dest="branch_name", default='',
                 help="Name of the branch we are testing on")
         add_arg('--run-local', dest="run_local", default=False, action="store_true",
                 help="Flag that indicates if raptor is running locally or in production")
         add_arg('--obj-path', dest="obj_path", default=None,
                 help="Browser build obj_path (received when running in production)")
 
--- a/testing/raptor/raptor/manifest.py
+++ b/testing/raptor/raptor/manifest.py
@@ -61,16 +61,17 @@ def validate_test_ini(test_details):
 
     return valid_settings
 
 
 def write_test_settings_json(test_details, oskey):
     # write test settings json file with test details that the control
     # server will provide for the web ext
     test_url = transform_platform(test_details['test_url'], oskey)
+
     test_settings = {
         "raptor-options": {
             "type": test_details['type'],
             "test_url": test_url,
             "page_cycles": int(test_details['page_cycles'])
         }
     }
 
@@ -166,16 +167,28 @@ def get_raptor_test_list(args, oskey):
     if len(tests_to_run) == 0:
         _ini = args.test + ".ini"
         for next_test in available_tests:
             head, tail = os.path.split(next_test['manifest'])
             if tail == _ini:
                 # subtest comes from matching test ini file name, so add it
                 tests_to_run.append(next_test)
 
+    # if --page-cycles command line arg was provided, override the page_cycles value
+    # that was in the manifest/test INI with the command line arg value instead
+    if args.page_cycles is not None:
+        LOG.info("setting page-cycles to %d as specified on the command line" % args.page_cycles)
+        next_test['page_cycles'] = args.page_cycles
+
+    # if --page-timeout command line arg was provided, override the page_timeout value
+    # that was in the manifest/test INI with the command line arg value instead
+    if args.page_timeout is not None:
+        LOG.info("setting page-timeout to %d as specified on the command line" % args.page_timeout)
+        next_test['page_timeout'] = args.page_timeout
+
     # if geckoProfile is enabled, turn it on in test settings and limit pagecycles to 2
     if args.gecko_profile is True:
         for next_test in tests_to_run:
             next_test['gecko_profile'] = True
             if next_test['page_cycles'] > 2:
                 LOG.info("gecko profiling enabled, limiting pagecycles "
                          "to 2 for test %s" % next_test['name'])
                 next_test['page_cycles'] = 2
--- a/testing/raptor/raptor/raptor.py
+++ b/testing/raptor/raptor/raptor.py
@@ -441,16 +441,17 @@ def main(args=sys.argv[1:]):
 
     raptor.start_control_server()
 
     for next_test in raptor_test_list:
         if 'page_timeout' not in next_test.keys():
             next_test['page_timeout'] = 120000
         if 'page_cycles' not in next_test.keys():
             next_test['page_cycles'] = 1
+
         raptor.run_test(next_test, timeout=int(next_test['page_timeout']))
 
     success = raptor.process_results()
     raptor.clean_up()
 
     if not success:
         # didn't get test results; test timed out or crashed, etc. we want job to fail
         LOG.critical("TEST-UNEXPECTED-FAIL: no raptor test results were found")
--- a/testing/raptor/test/test_cmdline.py
+++ b/testing/raptor/test/test_cmdline.py
@@ -5,17 +5,21 @@ import pytest
 
 import mozunit
 
 from argparse import ArgumentParser, Namespace
 from raptor.cmdline import verify_options
 
 
 def test_verify_options(filedir):
-    args = Namespace(app='firefox', binary='invalid/path', gecko_profile='False')
+    args = Namespace(app='firefox',
+                     binary='invalid/path',
+                     gecko_profile='False',
+                     page_cycles=1,
+                     page_timeout=60000)
     parser = ArgumentParser()
 
     with pytest.raises(SystemExit):
         verify_options(parser, args)
 
     args.binary = os.path.join(filedir, 'fake_binary.exe')
     verify_options(parser, args)  # assert no exception
 
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -12672,18 +12672,18 @@
     "expires_in_version": "61",
     "kind": "boolean",
     "operating_systems": ["linux"],
     "description": "Whether the system has seccomp-bpf thread-sync capability"
   },
   "SANDBOX_HAS_USER_NAMESPACES": {
     "record_in_processes": ["main"],
     "alert_emails": ["gcp@mozilla.com", "jld@mozilla.com"],
-    "bug_numbers": [1098428, 1370578, 1461546],
-    "expires_in_version": "65",
+    "bug_numbers": [1098428, 1370578, 1461546, 1464220],
+    "expires_in_version": "70",
     "releaseChannelCollection": "opt-out",
     "kind": "boolean",
     "operating_systems": ["linux"],
     "description": "Whether our process succedeed in creating a user namespace"
   },
   "SANDBOX_HAS_USER_NAMESPACES_PRIVILEGED": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["gcp@mozilla.com"],
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2074,19 +2074,18 @@ var AddonManagerInternal = {
    *
    * The optional window parameter will be used to determine
    * the screen resolution and select a more appropriate icon.
    * Calling this method with 48px on retina screens will try to
    * match an icon of size 96px.
    *
    * @param  aAddon
    *         An addon object, meaning:
-   *         An object with either an icons property that is a key-value
-   *         list of icon size and icon URL, or an object having an iconURL
-   *         and icon64URL property.
+   *         An object with either an icons property that is a key-value list
+   *         of icon size and icon URL, or an object having an iconURL property.
    * @param  aSize
    *         Ideal icon size in pixels
    * @param  aWindow
    *         Optional window object for determining the correct scale.
    * @return {String} The absolute URL of the icon or null if the addon doesn't have icons
    */
   getPreferredIconURL(aAddon, aSize, aWindow = undefined) {
     if (aWindow && aWindow.devicePixelRatio) {
@@ -2097,19 +2096,16 @@ var AddonManagerInternal = {
 
     // certain addon-types only have iconURLs
     if (!icons) {
       icons = {};
       if (aAddon.iconURL) {
         icons[32] = aAddon.iconURL;
         icons[48] = aAddon.iconURL;
       }
-      if (aAddon.icon64URL) {
-        icons[64] = aAddon.icon64URL;
-      }
     }
 
     // quick return if the exact size was found
     if (icons[aSize]) {
       return icons[aSize];
     }
 
     let bestSize = null;
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -953,17 +953,17 @@ var AddonTestUtils = {
     rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
            '     xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
 
     rdf += '<Description about="urn:mozilla:install-manifest">\n';
 
     data = Object.assign({}, defaults, data);
 
     let props = ["id", "version", "type", "internalName", "updateURL",
-                 "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
+                 "optionsURL", "optionsType", "aboutURL", "iconURL",
                  "skinnable", "bootstrap", "strictCompatibility",
                  "hasEmbeddedWebExtension"];
     rdf += this._writeProps(data, props);
 
     rdf += this._writeLocaleStrings(data);
 
     for (let platform of data.targetPlatforms || [])
       rdf += escaped`<em:targetPlatform>${platform}</em:targetPlatform>\n`;
--- a/toolkit/mozapps/extensions/internal/RDFManifestConverter.jsm
+++ b/toolkit/mozapps/extensions/internal/RDFManifestConverter.jsm
@@ -86,17 +86,17 @@ class InstallRDF extends Manifest {
     });
   }
 
   decode() {
     let root = this.ds.getResource(RDFURI_INSTALL_MANIFEST_ROOT);
     let result = {};
 
     let props = ["id", "version", "type", "updateURL", "optionsURL",
-                 "optionsType", "aboutURL", "iconURL", "icon64URL",
+                 "optionsType", "aboutURL", "iconURL",
                  "bootstrap", "unpack", "strictCompatibility",
                  "hasEmbeddedWebExtension"];
     this._readProps(root, result, props);
 
     let decodeTargetApplication = source => {
       let app = {};
       this._readProps(source, app, ["id", "minVersion", "maxVersion"]);
       return app;
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -110,17 +110,17 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
                           "defaultLocale", "visible", "active", "userDisabled",
                           "appDisabled", "pendingUninstall", "installDate",
                           "updateDate", "applyBackgroundUpdates", "path",
                           "skinnable", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall",
                           "strictCompatibility", "locales", "targetApplications",
                           "targetPlatforms", "signedState",
                           "seen", "dependencies", "hasEmbeddedWebExtension",
-                          "userPermissions", "icons", "iconURL", "icon64URL",
+                          "userPermissions", "icons", "iconURL",
                           "blocklistState", "blocklistURL", "startupData",
                           "previewImage", "hidden", "installTelemetryInfo"];
 
 const LEGACY_TYPES = new Set([
   "extension",
 ]);
 
 // Some add-on types that we track internally are presented as other types
@@ -802,20 +802,16 @@ AddonWrapper = class {
   async getBlocklistURL() {
     return addonFor(this).blocklistURL;
   }
 
   get iconURL() {
     return AddonManager.getPreferredIconURL(this, 48);
   }
 
-  get icon64URL() {
-    return AddonManager.getPreferredIconURL(this, 64);
-  }
-
   get icons() {
     let addon = addonFor(this);
     let icons = {};
 
     if (addon._repositoryAddon) {
       for (let size in addon._repositoryAddon.icons) {
         icons[size] = addon._repositoryAddon.icons[size];
       }
@@ -828,20 +824,16 @@ AddonWrapper = class {
     }
 
     let canUseIconURLs = this.isActive;
     if (canUseIconURLs && addon.iconURL) {
       icons[32] = addon.iconURL;
       icons[48] = addon.iconURL;
     }
 
-    if (canUseIconURLs && addon.icon64URL) {
-      icons[64] = addon.icon64URL;
-    }
-
     Object.freeze(icons);
     return icons;
   }
 
   get screenshots() {
     let addon = addonFor(this);
     let repositoryAddon = addon._repositoryAddon;
     if (repositoryAddon && ("screenshots" in repositoryAddon)) {
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -159,18 +159,17 @@ const KEY_TEMPDIR                     = 
 
 const KEY_APP_PROFILE                 = "app-profile";
 
 const DIR_STAGE                       = "staged";
 const DIR_TRASH                       = "trash";
 
 // Properties that exist in the install manifest
 const PROP_METADATA      = ["id", "version", "type", "internalName", "updateURL",
-                            "optionsURL", "optionsType", "aboutURL",
-                            "iconURL", "icon64URL"];
+                            "optionsURL", "optionsType", "aboutURL", "iconURL"];
 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
 const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
 
 // Map new string type identifiers to old style nsIUpdateItem types.
 // Retired values:
 // 32 = multipackage xpi file
 // 8 = locale
 // 256 = apiextension
@@ -481,17 +480,16 @@ async function loadManifestFromWebManife
     else
       addon.optionsType = AddonManager.OPTIONS_TYPE_INLINE_BROWSER;
 
     addon.optionsBrowserStyle = manifest.options_ui.browser_style;
   }
 
   // WebExtensions don't use iconURLs
   addon.iconURL = null;
-  addon.icon64URL = null;
   addon.icons = manifest.icons || {};
   addon.userPermissions = extension.manifestPermissions;
 
   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
 
   function getLocale(aLocale) {
     // Use the raw manifest, here, since we need values with their
     // localization placeholders still in place.
--- a/toolkit/mozapps/extensions/test/browser/browser_details.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_details.js
@@ -50,17 +50,16 @@ async function test() {
   gProvider.createAddons([{
     id: "addon1@tests.mozilla.org",
     name: "Test add-on 1",
     version: "2.1",
     description: "Short description",
     fullDescription: "Longer description",
     type: "extension",
     iconURL: "chrome://foo/skin/icon.png",
-    icon64URL: "chrome://foo/skin/icon64.png",
     contributionURL: "http://foo.com",
     contributionAmount: "$0.99",
     sourceURI: Services.io.newURI("http://example.com/foo"),
     averageRating: 4,
     reviewCount: 5,
     reviewURL: "http://example.com/reviews",
     homepageURL: "http://example.com/addon1",
     applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE,
@@ -509,17 +508,16 @@ add_test(function() {
     gProvider.createAddons([{
       id: "addon1@tests.mozilla.org",
       name: "Test add-on replacement",
       version: "2.5",
       description: "Short description replacement",
       fullDescription: "Longer description replacement",
       type: "extension",
       iconURL: "chrome://foo/skin/icon.png",
-      icon64URL: "chrome://foo/skin/icon264.png",
       sourceURI: Services.io.newURI("http://example.com/foo"),
       averageRating: 2,
       optionsURL: "chrome://foo/content/options.xul",
       applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE,
       operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE,
     }]);
 
     is(get("detail-name").textContent, "Test add-on replacement", "Name should be correct");
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
@@ -129,17 +129,16 @@ add_task(async function test_1() {
     iconURL: null,
     sourceURI: uri,
   });
 
   let {addon} = install;
   checkAddon("addon1@tests.mozilla.org", addon, {
     install,
     iconURL: `jar:${uri.spec}!/icon.png`,
-    icon64URL: `jar:${uri.spec}!/icon64.png`,
     sourceURI: uri,
   });
   notEqual(addon.syncGUID, null);
   equal(addon.getResourceURI("install.rdf").spec, `jar:${uri.spec}!/install.rdf`);
 
   let activeInstalls = await AddonManager.getAllInstalls();
   equal(activeInstalls.length, 1);
   equal(activeInstalls[0], install);
@@ -200,17 +199,16 @@ add_task(async function test_1() {
 
   checkAddon("addon1@tests.mozilla.org", a1, {
     syncGUID: installSyncGUID,
     type: "extension",
     version: "1.0",
     name: "Test 1",
     foreignInstall: false,
     iconURL: uri2 + "icon.png",
-    icon64URL: uri2 + "icon64.png",
     sourceURI: Services.io.newFileURI(XPIS.test_install1),
   });
 
   notEqual(a1.syncGUID, null);
   ok(a1.syncGUID.length >= 9);
 
   ok(isExtensionInBootstrappedList(profileDir, a1.id));
   ok(XPIS.test_install1.exists());
--- a/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js
@@ -9,17 +9,16 @@
 const ADDONS = [
   {
     "install.rdf": {
       id: "addon1@tests.mozilla.org",
       version: "1.0",
       bootstrap: true,
       aboutURL: "chrome://test/content/about.xul",
       iconURL: "chrome://test/skin/icon.png",
-      icon64URL: "chrome://test/skin/icon64.png",
       targetApplications: [{
         id: "xpcshell@tests.mozilla.org",
         minVersion: "1",
         maxVersion: "1",
       }],
       name: "Test Addon 1",
       description: "Test Description",
       creator: "Test Creator",
@@ -40,18 +39,17 @@ const ADDONS = [
 
     expected: {
       id: "addon1@tests.mozilla.org",
       type: "extension",
       version: "1.0",
       optionsType: null,
       aboutURL: "chrome://test/content/about.xul",
       iconURL: "chrome://test/skin/icon.png",
-      icon64URL: "chrome://test/skin/icon64.png",
-      icons: {32: "chrome://test/skin/icon.png", 48: "chrome://test/skin/icon.png", 64: "chrome://test/skin/icon64.png"},
+      icons: {32: "chrome://test/skin/icon.png", 48: "chrome://test/skin/icon.png"},
       name: "Test Addon 1",
       description: "Test Description",
       creator: "Test Creator",
       homepageURL: "http://www.example.com",
       developers: ["Test Developer 1", "Test Developer 2"],
       translators: ["Test Translator 1", "Test Translator 2"],
       contributors: ["Test Contributor 1", "Test Contributor 2"],
       isActive: true,
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -90,17 +90,16 @@ add_task(async function test_1() {
     isCompatible: true,
     appDisabled: false,
     isActive: true,
     isSystem: false,
     type: "extension",
     isWebExtension: true,
     signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
     iconURL: `${uri}icon48.png`,
-    icon64URL: `${uri}icon64.png`,
   });
 
   // Should persist through a restart
   await promiseShutdownManager();
 
   equal(GlobalManager.extensionMap.size, 0);
 
   await promiseStartupManager();
@@ -117,17 +116,16 @@ add_task(async function test_1() {
     name: "Web Extension Name",
     isCompatible: true,
     appDisabled: false,
     isActive: true,
     isSystem: false,
     type: "extension",
     signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
     iconURL: `${uri}icon48.png`,
-    icon64URL: `${uri}icon64.png`,
   });
 
   await addon.disable();
 
   equal(GlobalManager.extensionMap.size, 0);
 
   await addon.enable();
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
@@ -23,19 +23,18 @@ async function testSimpleIconsetParsing(
   function check_icons(addon_copy) {
     deepEqual(addon_copy.icons, {
         16: uri + "icon16.png",
         32: uri + "icon32.png",
         48: uri + "icon48.png",
         64: uri + "icon64.png",
     });
 
-    // iconURL should map to icons[48] and icons[64]
+    // iconURL should map to icons[48]
     equal(addon.iconURL, uri + "icon48.png");
-    equal(addon.icon64URL, uri + "icon64.png");
 
     // AddonManager gets the correct icon sizes from addon.icons
     equal(AddonManager.getPreferredIconURL(addon, 1), uri + "icon16.png");
     equal(AddonManager.getPreferredIconURL(addon, 16), uri + "icon16.png");
     equal(AddonManager.getPreferredIconURL(addon, 30), uri + "icon32.png");
     equal(AddonManager.getPreferredIconURL(addon, 48), uri + "icon48.png");
     equal(AddonManager.getPreferredIconURL(addon, 64), uri + "icon64.png");
     equal(AddonManager.getPreferredIconURL(addon, 128), uri + "icon64.png");
@@ -86,17 +85,16 @@ async function testNoIconsParsing(manife
   await promiseRestartManager();
 
   let addon = await promiseAddonByID(ID);
   Assert.notEqual(addon, null);
 
   deepEqual(addon.icons, {});
 
   equal(addon.iconURL, null);
-  equal(addon.icon64URL, null);
 
   equal(AddonManager.getPreferredIconURL(addon, 128), null);
 
   await addon.uninstall();
 }
 
 // Test simple icon set parsing
 add_task(async function() {