Merge mozilla-central to mozilla-inbound
authorDorel Luca <dluca@mozilla.com>
Fri, 27 Jul 2018 13:48:50 +0300
changeset 823522 53ea40188efab3406b5ec7709699bb673176bee6
parent 823521 953910565893b2da0891a9d033d3fa0a6bad8b81 (current diff)
parent 823465 87bcafe428a4ad6017e59b915581ae00aa863407 (diff)
child 823523 3430a73e5f1c35e93997d95b25252403dc7709d0
push id117712
push userrwood@mozilla.com
push dateFri, 27 Jul 2018 15:10:54 +0000
milestone63.0a1
Merge mozilla-central to mozilla-inbound
python/mozboot/mozboot/android.py
toolkit/modules/RemotePageManager.jsm
toolkit/modules/tests/browser/browser_RemotePageManager.js
toolkit/modules/tests/browser/testremotepagemanager.html
toolkit/modules/tests/browser/testremotepagemanager2.html
--- a/accessible/base/RoleMap.h
+++ b/accessible/base/RoleMap.h
@@ -1419,10 +1419,10 @@ ROLE(EDITCOMBOBOX,
      ROLE_SYSTEM_COMBOBOX,
      eNameFromValueRule)
 
 ROLE(BLOCKQUOTE,
      "blockquote",
      ATK_ROLE_BLOCK_QUOTE,
      NSAccessibilityGroupRole,
      ROLE_SYSTEM_GROUPING,
-     IA2_ROLE_SECTION,
+     IA2_ROLE_BLOCK_QUOTE,
      eNoNameRule)
--- a/accessible/generic/Accessible.cpp
+++ b/accessible/generic/Accessible.cpp
@@ -1783,17 +1783,17 @@ Accessible::RelationByType(RelationType 
           }
         }
       } else {
         // In XUL, use first <button default="true" .../> in the document
         nsIDocument* doc = mContent->OwnerDoc();
         nsCOMPtr<nsIDOMXULButtonElement> buttonEl;
         if (doc->IsXULDocument()) {
           dom::XULDocument* xulDoc = doc->AsXULDocument();
-          nsCOMPtr<nsINodeList> possibleDefaultButtons =
+          nsCOMPtr<nsIHTMLCollection> possibleDefaultButtons =
             xulDoc->GetElementsByAttribute(NS_LITERAL_STRING("default"),
                                            NS_LITERAL_STRING("true"));
           if (possibleDefaultButtons) {
             uint32_t length = possibleDefaultButtons->Length();
             // Check for button in list of default="true" elements
             for (uint32_t count = 0; count < length && !buttonEl; count ++) {
               buttonEl = do_QueryInterface(possibleDefaultButtons->Item(count));
             }
--- a/browser/base/content/test/performance/browser_startup.js
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -34,19 +34,16 @@ const startupPhases = {
       "WebContentConverter.js", // bug 1369443
       "nsSessionStartup.js", // bug 1369456
       "PushComponents.js", // bug 1369436
     ]),
     modules: new Set([
       "resource://gre/modules/AppConstants.jsm",
       "resource://gre/modules/XPCOMUtils.jsm",
       "resource://gre/modules/Services.jsm",
-
-      // Bugs to fix: Probably loaded too early, needs investigation.
-      "resource://gre/modules/RemotePageManager.jsm", // bug 1369466
     ])
   }},
 
   // For the following phases of startup we have only a black list for now
 
   // We are at this phase after creating the first browser window (ie. after final-ui-startup).
   "before opening first browser window": {blacklist: {
     modules: new Set([
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -56,17 +56,16 @@ const whitelist = {
     "resource:///modules/ContentLinkHandler.jsm",
     "resource:///modules/ContentMetaHandler.jsm",
     "resource:///modules/PageStyleHandler.jsm",
     "resource:///modules/LightweightThemeChildListener.jsm",
     "resource://gre/modules/BrowserUtils.jsm",
     "resource://gre/modules/E10SUtils.jsm",
     "resource://gre/modules/PrivateBrowsingUtils.jsm",
     "resource://gre/modules/ReaderMode.jsm",
-    "resource://gre/modules/RemotePageManager.jsm",
 
     // Pocket
     "chrome://pocket/content/AboutPocket.jsm",
 
     // Telemetry
     "resource://gre/modules/TelemetryController.jsm", // bug 1470339
     "resource://gre/modules/TelemetrySession.jsm", // bug 1470339
     "resource://gre/modules/TelemetryUtils.jsm", // bug 1470339
--- a/browser/components/about/AboutPrivateBrowsingHandler.jsm
+++ b/browser/components/about/AboutPrivateBrowsingHandler.jsm
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["AboutPrivateBrowsingHandler"];
 
-ChromeUtils.import("resource://gre/modules/RemotePageManager.jsm");
+ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm");
 
 var AboutPrivateBrowsingHandler = {
   _topics: [
     "DontShowIntroPanelAgain",
     "OpenPrivateWindow",
   ],
 
   init() {
--- a/browser/components/newtab/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/components/newtab/lib/ActivityStreamMessageChannel.jsm
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 ChromeUtils.import("resource:///modules/AboutNewTab.jsm");
-ChromeUtils.import("resource://gre/modules/RemotePageManager.jsm");
+ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm");
 
 const {actionCreators: ac, actionTypes: at, actionUtils: au} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
 
 const ABOUT_NEW_TAB_URL = "about:newtab";
 const ABOUT_HOME_URL = "about:home";
 
 const DEFAULT_OPTIONS = {
   dispatch(action) {
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -555,79 +555,62 @@ var paymentDialogWrapper = {
     paymentSrv.changeShippingOption(this.request.requestId, optionID);
   },
 
   onCloseDialogMessage() {
     // The PR is complete(), just close the dialog
     window.close();
   },
 
-  async onUpdateAutofillRecord(collectionName, record, guid, {
-    errorStateChange,
-    preserveOldProperties,
-    selectedStateKey,
-    successStateChange,
-  }) {
-    if (collectionName == "creditCards" && !guid && !record.isTemporary) {
-      // We need to be logged in so we can encrypt the credit card number and
-      // that's only supported when we're adding a new record.
-      // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
-      // APIs are refactored to be async functions (bug 1399367).
-      if (!await MasterPassword.ensureLoggedIn()) {
-        Cu.reportError("User canceled master password entry");
-        return;
+  async onUpdateAutofillRecord(collectionName, record, guid, messageID) {
+    let responseMessage = {
+      guid,
+      messageID,
+      stateChange: {},
+    };
+    try {
+      if (collectionName == "creditCards" && !guid && !record.isTemporary) {
+        // We need to be logged in so we can encrypt the credit card number and
+        // that's only supported when we're adding a new record.
+        // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
+        // APIs are refactored to be async functions (bug 1399367).
+        if (!await MasterPassword.ensureLoggedIn()) {
+          throw new Error("User canceled master password entry");
+        }
       }
-    }
-    let isTemporary = record.isTemporary;
-    let collection = isTemporary ? this.temporaryStore[collectionName] :
-                                   formAutofillStorage[collectionName];
 
-    try {
+      let isTemporary = record.isTemporary;
+      let collection = isTemporary ? this.temporaryStore[collectionName] :
+                                     formAutofillStorage[collectionName];
+
       if (guid) {
+        let preserveOldProperties = true;
         await collection.update(guid, record, preserveOldProperties);
       } else {
-        guid = await collection.add(record);
+        responseMessage.guid = await collection.add(record);
       }
 
       if (isTemporary && collectionName == "addresses") {
         // there will be no formautofill-storage-changed event to update state
         // so add updated collection here
-        Object.assign(successStateChange, {
+        Object.assign(responseMessage.stateChange, {
           tempAddresses: this.temporaryStore.addresses.getAll(),
         });
       }
       if (isTemporary && collectionName == "creditCards") {
         // there will be no formautofill-storage-changed event to update state
         // so add updated collection here
-        Object.assign(successStateChange, {
+        Object.assign(responseMessage.stateChange, {
           tempBasicCards: this.temporaryStore.creditCards.getAll(),
         });
       }
-
-      // Select the new record
-      if (selectedStateKey) {
-        if (selectedStateKey.length == 1) {
-          Object.assign(successStateChange, {
-            [selectedStateKey[0]]: guid,
-          });
-        } else if (selectedStateKey.length == 2) {
-          // Need to keep properties like preserveFieldValues from getting removed.
-          let subObj = Object.assign({}, successStateChange[selectedStateKey[0]]);
-          subObj[selectedStateKey[1]] = guid;
-          Object.assign(successStateChange, {
-            [selectedStateKey[0]]: subObj,
-          });
-        } else {
-          throw new Error(`selectedStateKey not supported: '${selectedStateKey}'`);
-        }
-      }
-
-      this.sendMessageToContent("updateState", successStateChange);
     } catch (ex) {
-      this.sendMessageToContent("updateState", errorStateChange);
+      responseMessage.error = true;
+    } finally {
+      this.sendMessageToContent("updateAutofillRecord:Response", responseMessage);
     }
   },
 
   /**
    * @implement {nsIDOMEventListener}
    * @param {Event} event
    */
   handleEvent(event) {
@@ -690,22 +673,17 @@ var paymentDialogWrapper = {
         this.onPaymentCancel();
         break;
       }
       case "pay": {
         this.onPay(data);
         break;
       }
       case "updateAutofillRecord": {
-        this.onUpdateAutofillRecord(data.collectionName, data.record, data.guid, {
-          errorStateChange: data.errorStateChange,
-          preserveOldProperties: data.preserveOldProperties,
-          selectedStateKey: data.selectedStateKey,
-          successStateChange: data.successStateChange,
-        });
+        this.onUpdateAutofillRecord(data.collectionName, data.record, data.guid, data.messageID);
         break;
       }
     }
   },
 };
 
 if ("document" in this) {
   // Running in a browser, not a unit test
--- a/browser/components/payments/res/components/basic-card-option.css
+++ b/browser/components/payments/res/components/basic-card-option.css
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 basic-card-option {
-  grid-row-gap: 5px;
-  grid-column-gap: 10px;
-  grid-template-areas: "type cc-number cc-name cc-exp";
+  grid-column-gap: 1ch;
+  grid-template-areas: "type cc-number cc-exp cc-name";
+  justify-content: start;
 }
 
 basic-card-option > .cc-number {
   grid-area: cc-number;
 }
 
 basic-card-option > .cc-name {
   grid-area: cc-name;
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -238,64 +238,86 @@ export default class AddressForm extends
         break;
       }
       default: {
         throw new Error("Unexpected click target");
       }
     }
   }
 
-  saveRecord() {
+  async saveRecord() {
     let record = this.formHandler.buildFormObject();
     let currentState = this.requestStore.getState();
     let {
       page,
       tempAddresses,
       savedBasicCards,
       "address-page": addressPage,
     } = currentState;
     let editing = !!addressPage.guid;
 
     if (editing ? (addressPage.guid in tempAddresses) : !this.persistCheckbox.checked) {
       record.isTemporary = true;
     }
 
-    let state = {
-      errorStateChange: {
-        page: {
-          id: "address-page",
-          onboardingWizard: page.onboardingWizard,
-          error: this.dataset.errorGenericSave,
-        },
-        "address-page": addressPage,
-      },
-      preserveOldProperties: true,
-      selectedStateKey: page.selectedStateKey,
-    };
-
+    let successStateChange;
     const previousId = page.previousId;
     if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
-      state.successStateChange = {
+      successStateChange = {
+        "basic-card-page": {
+          // Preserve field values as the user may have already edited the card
+          // page and went back to the address page to make a correction.
+          preserveFieldValues: true,
+        },
         page: {
           id: "basic-card-page",
           previousId: "address-page",
           onboardingWizard: page.onboardingWizard,
         },
       };
     } else {
-      state.successStateChange = {
+      successStateChange = {
         page: {
           id: previousId || "payment-summary",
           onboardingWizard: page.onboardingWizard,
         },
       };
     }
 
     if (previousId) {
-      state.successStateChange[previousId] = Object.assign({}, currentState[previousId]);
-      state.successStateChange[previousId].preserveFieldValues = true;
+      successStateChange[previousId] = Object.assign({}, currentState[previousId]);
+      successStateChange[previousId].preserveFieldValues = true;
     }
 
-    paymentRequest.updateAutofillRecord("addresses", record, addressPage.guid, state);
+    try {
+      let {guid} = await paymentRequest.updateAutofillRecord("addresses", record, addressPage.guid);
+      let selectedStateKey = addressPage.selectedStateKey;
+
+      if (selectedStateKey.length == 1) {
+        Object.assign(successStateChange, {
+          [selectedStateKey[0]]: guid,
+        });
+      } else if (selectedStateKey.length == 2) {
+        // Need to keep properties like preserveFieldValues from getting removed.
+        let subObj = Object.assign({}, successStateChange[selectedStateKey[0]]);
+        subObj[selectedStateKey[1]] = guid;
+        Object.assign(successStateChange, {
+          [selectedStateKey[0]]: subObj,
+        });
+      } else {
+        throw new Error(`selectedStateKey not supported: '${selectedStateKey}'`);
+      }
+
+      this.requestStore.setState(successStateChange);
+    } catch (ex) {
+      log.warn("saveRecord: error:", ex);
+      this.requestStore.setState({
+        page: {
+          id: "address-page",
+          onboardingWizard: page.onboardingWizard,
+          error: this.dataset.errorGenericSave,
+        },
+      });
+    }
   }
 }
 
 customElements.define("address-form", AddressForm);
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -139,20 +139,20 @@ export default class AddressPicker exten
       });
     }
   }
 
   onClick({target}) {
     let nextState = {
       page: {
         id: "address-page",
-        selectedStateKey: [this.selectedStateKey],
       },
       "address-page": {
         addressFields: this.getAttribute("address-fields"),
+        selectedStateKey: [this.selectedStateKey],
       },
     };
 
     switch (target) {
       case this.addLink: {
         nextState["address-page"].guid = null;
         nextState["address-page"].title = this.dataset.addAddressTitle;
         break;
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -17,16 +17,18 @@ import paymentRequest from "../paymentRe
  * as it will be much easier to share the logic once we switch to Fluent.
  */
 
 export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRequestPage) {
   constructor() {
     super();
 
     this.genericErrorText = document.createElement("div");
+    this.genericErrorText.setAttribute("aria-live", "polite");
+    this.genericErrorText.classList.add("page-error");
 
     this.addressAddLink = document.createElement("a");
     this.addressAddLink.className = "add-link";
     this.addressAddLink.href = "javascript:void(0)";
     this.addressAddLink.addEventListener("click", this);
     this.addressEditLink = document.createElement("a");
     this.addressEditLink.className = "edit-link";
     this.addressEditLink.href = "javascript:void(0)";
@@ -49,16 +51,18 @@ export default class BasicCardForm exten
     this.saveButton.addEventListener("click", this);
 
     this.footer.append(this.cancelButton, this.backButton, this.saveButton);
 
     // The markup is shared with form autofill preferences.
     let url = "formautofill/editCreditCard.xhtml";
     this.promiseReady = this._fetchMarkup(url).then(doc => {
       this.form = doc.getElementById("form");
+      this.form.addEventListener("input", this);
+      this.form.addEventListener("invalid", this);
       return this.form;
     });
   }
 
   _fetchMarkup(url) {
     return new Promise((resolve, reject) => {
       let xhr = new XMLHttpRequest();
       xhr.responseType = "document";
@@ -162,24 +166,34 @@ export default class BasicCardForm exten
       billingAddressSelect.value = basicCardPage.billingAddressGUID;
     } else if (!editing) {
       if (paymentRequest.getAddresses(state)[selectedShippingAddress]) {
         billingAddressSelect.value = selectedShippingAddress;
       } else {
         billingAddressSelect.value = Object.keys(addresses)[0];
       }
     }
+
+    this.updateSaveButtonState();
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
       }
+      case "input": {
+        this.onInput(event);
+        break;
+      }
+      case "invalid": {
+        this.onInvalid(event);
+        break;
+      }
     }
   }
 
   onClick(evt) {
     switch (evt.target) {
       case this.cancelButton: {
         paymentRequest.cancel();
         break;
@@ -188,20 +202,20 @@ export default class BasicCardForm exten
       case this.addressEditLink: {
         let {
           "basic-card-page": basicCardPage,
         } = this.requestStore.getState();
         let nextState = {
           page: {
             id: "address-page",
             previousId: "basic-card-page",
-            selectedStateKey: ["basic-card-page", "billingAddressGUID"],
           },
           "address-page": {
             guid: null,
+            selectedStateKey: ["basic-card-page", "billingAddressGUID"],
             title: this.dataset.billingAddressTitleAdd,
           },
           "basic-card-page": {
             preserveFieldValues: true,
             guid: basicCardPage.guid,
           },
         };
         let billingAddressGUID = this.form.querySelector("#billingAddressGUID");
@@ -246,30 +260,43 @@ export default class BasicCardForm exten
             "basic-card-page": basicCardPageState,
           });
         }
 
         this.requestStore.setState(nextState);
         break;
       }
       case this.saveButton: {
-        this.saveRecord();
+        if (this.form.checkValidity()) {
+          this.saveRecord();
+        }
         break;
       }
       default: {
         throw new Error("Unexpected click target");
       }
     }
   }
 
-  saveRecord() {
+  onInput(event) {
+    this.updateSaveButtonState();
+  }
+
+  onInvalid(event) {
+    this.saveButton.disabled = true;
+  }
+
+  updateSaveButtonState() {
+    this.saveButton.disabled = !this.form.checkValidity();
+  }
+
+  async saveRecord() {
     let record = this.formHandler.buildFormObject();
     let currentState = this.requestStore.getState();
     let {
-      page,
       tempBasicCards,
       "basic-card-page": basicCardPage,
     } = currentState;
     let editing = !!basicCardPage.guid;
 
     if (editing ? (basicCardPage.guid in tempBasicCards) : !this.persistCheckbox.checked) {
       record.isTemporary = true;
     }
@@ -279,34 +306,30 @@ export default class BasicCardForm exten
     }
 
     // Only save the card number if we're saving a new record, otherwise we'd
     // overwrite the unmasked card number with the masked one.
     if (!editing) {
       record["cc-number"] = record["cc-number"] || "";
     }
 
-    let state = {
-      errorStateChange: {
+    try {
+      let {guid} = await paymentRequest.updateAutofillRecord("creditCards", record,
+                                                             basicCardPage.guid);
+      this.requestStore.setState({
+        page: {
+          id: "payment-summary",
+        },
+        selectedPaymentCard: guid,
+      });
+    } catch (ex) {
+      log.warn("saveRecord: error:", ex);
+      this.requestStore.setState({
         page: {
           id: "basic-card-page",
           error: this.dataset.errorGenericSave,
         },
-      },
-      preserveOldProperties: true,
-      selectedStateKey: ["selectedPaymentCard"],
-      successStateChange: {
-        page: {
-          id: "payment-summary",
-        },
-      },
-    };
-
-    const previousId = page.previousId;
-    if (previousId) {
-      state.successStateChange[previousId] = Object.assign({}, currentState[previousId]);
+      });
     }
-
-    paymentRequest.updateAutofillRecord("creditCards", record, basicCardPage.guid, state);
   }
 }
 
 customElements.define("basic-card-form", BasicCardForm);
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -48,17 +48,17 @@ export default class PaymentDialog exten
 
     this._shippingAddressPicker = contents.querySelector("address-picker.shipping-related");
     this._shippingRelatedEls = contents.querySelectorAll(".shipping-related");
     this._payerRelatedEls = contents.querySelectorAll(".payer-related");
     this._payerAddressPicker = contents.querySelector("address-picker.payer-related");
 
     this._header = contents.querySelector("header");
 
-    this._errorText = contents.querySelector("#error-text");
+    this._errorText = contents.querySelector("header > .page-error");
 
     this._disabledOverlay = contents.getElementById("disabled-overlay");
 
     this.appendChild(contents);
 
     super.connectedCallback();
   }
 
--- a/browser/components/payments/res/debugging.html
+++ b/browser/components/payments/res/debugging.html
@@ -54,16 +54,17 @@
           <label class="block"><input type="radio" name="setCompleteStatus" value="timeout">Timeout</label>
         </fieldset>
         <label class="block"><input type="checkbox" id="setChangesPrevented">Prevent changes</label>
 
 
         <section class="group">
           <fieldset>
             <legend>User Data Errors</legend>
+            <button id="saveVisibleForm" title="Bypasses field validation">Save Visible Form</button>
             <button id="setShippingError">Shipping Error</button>
             <button id="setAddressErrors">Address Errors</button>
           </fieldset>
         </section>
       </section>
     </div>
   </body>
 </html>
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -328,16 +328,21 @@ let buttonActions = {
   refresh() {
     window.parent.location.reload(true);
   },
 
   rerender() {
     requestStore.setState({});
   },
 
+  saveVisibleForm() {
+    // Bypasses field validation which is useful to test error handling.
+    paymentDialog.querySelector("#main-container > .page:not([hidden])").saveRecord();
+  },
+
   setAddresses1() {
     paymentDialog.setStateFromParent({savedAddresses: ADDRESSES_1});
   },
 
   setDupesAddresses() {
     paymentDialog.setStateFromParent({savedAddresses: DUPED_ADDRESSES});
   },
 
--- a/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -15,26 +15,26 @@ export let requestStore = new PaymentsSt
   changesPrevented: false,
   orderDetailsShowing: false,
   "basic-card-page": {
     guid: null,
     // preserveFieldValues: true,
   },
   "address-page": {
     guid: null,
+    selectedStateKey: null,
     title: "",
   },
   "payment-summary": {
   },
   page: {
     id: "payment-summary",
     previousId: null,
     // onboardingWizard: true,
     // error: "",
-    // selectedStateKey: "",
   },
   request: {
     completeStatus: "",
     tabId: null,
     topLevelPrincipal: {URI: {displayHost: null}},
     requestId: null,
     paymentMethods: [],
     paymentDetails: {
--- a/browser/components/payments/res/paymentRequest.css
+++ b/browser/components/payments/res/paymentRequest.css
@@ -40,21 +40,35 @@ payment-dialog > header,
 .page > .page-body,
 .page > footer {
   padding: 0 10%;
 }
 
 payment-dialog > header {
   border-bottom: 1px solid rgba(0,0,0,0.1);
   display: flex;
+  /* Wrap so that the error text appears full-width above the rest of the contents */
+  flex-wrap: wrap;
   /* from visual spec: */
   padding-bottom: 19px;
   padding-top: 19px;
 }
 
+payment-dialog > header > .page-error:empty {
+  display: none;
+}
+
+payment-dialog > header > .page-error {
+  background: #D70022;
+  border-radius: 3px;
+  color: white;
+  padding: 6px;
+  width: 100%;
+}
+
 #main-container {
   display: flex;
   grid-area: main;
   position: relative;
   max-height: 100%;
   padding-top: 18px;
 }
 
@@ -73,29 +87,29 @@ payment-dialog > header {
   /* The area above the footer should scroll, if necessary. */
   overflow: auto;
 }
 
 .page > .page-body > h2:empty {
   display: none;
 }
 
+.page-error {
+  color: #D70022;
+}
+
 .page > footer {
   align-items: center;
   background-color: #eaeaee;
   display: flex;
   /* from visual spec: */
   padding-top: 20px;
   padding-bottom: 18px;
 }
 
-#error-text {
-  text-align: center;
-}
-
 #order-details-overlay {
   background-color: var(--in-content-page-background);
   overflow: auto;
   position: absolute;
   top: 0;
   right: 0;
   bottom: 0;
   left: 0;
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -6,16 +6,17 @@
  * Loaded in the unprivileged frame of each payment dialog.
  *
  * Communicates with privileged code via DOM Events.
  */
 
 /* import-globals-from unprivileged-fallbacks.js */
 
 var paymentRequest = {
+  _nextMessageID: 1,
   domReadyPromise: null,
 
   init() {
     // listen to content
     window.addEventListener("paymentChromeToContent", this);
 
     window.addEventListener("keydown", this);
 
@@ -49,25 +50,33 @@ var paymentRequest = {
         break;
       }
       default: {
         throw new Error("Unexpected event type");
       }
     }
   },
 
+  /**
+   * @param {string} messageType
+   * @param {[object]} detail
+   * @returns {number} message ID to be able to identify a reply (where applicable).
+   */
   sendMessageToChrome(messageType, detail = {}) {
-    log.debug("sendMessageToChrome:", messageType, detail);
+    let messageID = this._nextMessageID++;
+    log.debug("sendMessageToChrome:", messageType, messageID, detail);
     let event = new CustomEvent("paymentContentToChrome", {
       bubbles: true,
       detail: Object.assign({
         messageType,
+        messageID,
       }, detail),
     });
     document.dispatchEvent(event);
+    return messageID;
   },
 
   toggleDebuggingConsole() {
     let debuggingConsole = document.getElementById("debugging-console");
     if (debuggingConsole.hidden && !debuggingConsole.src) {
       debuggingConsole.src = "debugging.html";
     }
     debuggingConsole.hidden = !debuggingConsole.hidden;
@@ -138,24 +147,24 @@ var paymentRequest = {
 
       state["address-page"] = {
         addressFields: null,
         guid: null,
       };
 
       if (shippingRequested) {
         Object.assign(state["address-page"], {
+          selectedStateKey: ["selectedShippingAddress"],
           title: paymentDialog.dataset.shippingAddressTitleAdd,
         });
-        state.page.selectedStateKey = ["selectedShippingAddress"];
       } else {
         Object.assign(state["address-page"], {
+          selectedStateKey: ["basic-card-page", "billingAddressGUID"],
           title: paymentDialog.dataset.billingAddressTitleAdd,
         });
-        state.page.selectedStateKey = ["basic-card-page", "billingAddressGUID"];
       }
     } else if (!hasSavedCards) {
       state.page = {
         id: "basic-card-page",
         onboardingWizard: true,
       };
     }
 
@@ -184,31 +193,40 @@ var paymentRequest = {
 
   /**
    * Add/update an autofill storage record.
    *
    * If the the `guid` argument is provided update the record; otherwise, add it.
    * @param {string} collectionName The autofill collection that record belongs to.
    * @param {object} record The autofill record to add/update
    * @param {string} [guid] The guid of the autofill record to update
+   * @returns {Promise} when the update response is received
    */
-  updateAutofillRecord(collectionName, record, guid, {
-    errorStateChange,
-    preserveOldProperties,
-    selectedStateKey,
-    successStateChange,
-  }) {
-    this.sendMessageToChrome("updateAutofillRecord", {
-      collectionName,
-      guid,
-      record,
-      errorStateChange,
-      preserveOldProperties,
-      selectedStateKey,
-      successStateChange,
+  updateAutofillRecord(collectionName, record, guid) {
+    return new Promise((resolve, reject) => {
+      let messageID = this.sendMessageToChrome("updateAutofillRecord", {
+        collectionName,
+        guid,
+        record,
+      });
+
+      window.addEventListener("paymentChromeToContent", function onMsg({detail}) {
+        if (detail.messageType != "updateAutofillRecord:Response"
+            || detail.messageID != messageID) {
+          return;
+        }
+        log.debug("updateAutofillRecord: response:", detail);
+        window.removeEventListener("paymentChromeToContent", onMsg);
+        document.querySelector("payment-dialog").setStateFromParent(detail.stateChange);
+        if (detail.error) {
+          reject(detail);
+        } else {
+          resolve(detail);
+        }
+      });
     });
   },
 
   /**
    * @param {object} state object representing the UI state
    * @param {string} methodID (GUID) uniquely identifying the selected payment method
    * @returns {object?} the applicable modifier for the payment method
    */
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -89,29 +89,29 @@
 
   <script src="formautofill/autofillEditForms.js"></script>
 
   <script type="module" src="containers/payment-dialog.js"></script>
   <script type="module" src="paymentRequest.js"></script>
 
   <template id="payment-dialog-template">
     <header>
+      <div class="page-error" aria-live="polite"></div>
       <div id="total">
         <currency-amount display-code="display-code"></currency-amount>
         <div>&header.payTo; <span id="host-name"></span></div>
       </div>
       <div id="top-buttons" hidden="hidden">
         <button id="view-all" class="closed">&viewAllItems;</button>
       </div>
     </header>
 
     <div id="main-container">
       <payment-request-page id="payment-summary">
         <div class="page-body">
-          <div id="error-text"></div>
           <address-picker class="shipping-related"
                           data-add-link-label="&address.addLink.label;"
                           data-edit-link-label="&address.editLink.label;"
                           data-shipping-address-label="&shippingAddressLabel;"
                           data-delivery-address-label="&deliveryAddressLabel;"
                           data-pickup-address-label="&pickupAddressLabel;"
                           selected-state-key="selectedShippingAddress"></address-picker>
 
@@ -123,17 +123,16 @@
                                  data-edit-link-label="&basicCard.editLink.label;"
                                  label="&paymentMethodsLabel;">
           </payment-method-picker>
           <address-picker class="payer-related"
                           label="&payerLabel;"
                           data-add-link-label="&payer.addLink.label;"
                           data-edit-link-label="&payer.editLink.label;"
                           selected-state-key="selectedPayerAddress"></address-picker>
-          <div id="error-text"></div>
         </div>
 
         <footer>
           <button id="cancel">&cancelPaymentButton.label;</button>
           <button id="pay"
                   class="primary"
                   data-label="&approvePaymentButton.label;"
                   data-processing-label="&processingPaymentButton.label;"
--- a/browser/components/payments/res/unprivileged-fallbacks.js
+++ b/browser/components/payments/res/unprivileged-fallbacks.js
@@ -21,17 +21,17 @@ var log = {
   debug: console.debug.bind(console, "paymentRequest.xhtml:"),
 };
 
 var PaymentDialogUtils = {
   getAddressLabel(address) {
     return `${address.name} (${address.guid})`;
   },
   isCCNumber(str) {
-    return str.length > 0;
+    return !!str.replace(/[-\s]/g, "").match(/^\d{9,}$/);
   },
   DEFAULT_REGION: "US",
   supportedCountries: ["US", "CA"],
   getFormFormat(country) {
     return {
       "addressLevel1Label": country == "US" ? "state" : "province",
       "postalCodeLabel": country == "US" ? "zip" : "postalCode",
       "fieldsOrder": [
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -167,16 +167,17 @@ var PaymentTestUtils = {
      * Don't await on this method from a ContentTask when expecting the dialog to close
      *
      * @returns {undefined}
      */
     clickPrimaryButton: () => {
       let {requestStore} = Cu.waiveXrays(content.document.querySelector("payment-dialog"));
       let {page} = requestStore.getState();
       let button = content.document.querySelector(`#${page.id} button.primary`);
+      ok(!button.disabled, "Primary button should not be disabled when clicking it");
       button.click();
     },
 
     /**
      * Click the cancel button
      *
      * Don't await on this task since the cancel can close the dialog before
      * ContentTask can resolve the promise.
--- a/browser/components/payments/test/browser/browser_card_edit.js
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -108,23 +108,23 @@ async function add_link(aOptions = {}) {
     }, aOptions);
 
     await navigateToAddAddressPage(frame, addressOptions);
 
     await fillInAddressForm(frame, PTU.Addresses.TimBL2, addressOptions);
 
     await verifyPersistCheckbox(frame, addressOptions);
 
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
     await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
-      content.document.querySelector("address-form button:last-of-type").click();
-
       let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
       }, "Check address was added and we're back on basic-card page (add)");
 
       let addressCount = Object.keys(state.savedAddresses).length +
                          Object.keys(state.tempAddresses).length;
       is(addressCount, 2, "Check address was added");
 
@@ -147,28 +147,29 @@ async function add_link(aOptions = {}) {
       is(selectedAddressGuid, lastAddress.guid, "The select should have the new address selected");
     }, aOptions);
 
     await fillInCardForm(frame, PTU.BasicCards.JaneMasterCard, {
       isTemporary: aOptions.isPrivate,
       checkboxSelector: "basic-card-form .persist-checkbox",
     });
 
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
     await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
-      content.document.querySelector("basic-card-form button:last-of-type").click();
-
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "payment-summary";
-      }, "Check we are back on the sumamry page");
+      }, "Check we are back on the summary page");
     });
 
+
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "123",
     });
 
     await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
@@ -365,26 +366,26 @@ add_task(async function test_edit_link()
       let field = content.document.getElementById(key);
       if (!field) {
         ok(false, `${key} field not found`);
       }
       field.value = val.slice(0, -1) + "7";
       ok(!field.disabled, `Field #${key} shouldn't be disabled`);
     }
 
-    content.document.querySelector("address-form button:last-of-type").click();
+    content.document.querySelector("address-form button.save-button").click();
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
              Object.keys(state.savedAddresses).length == 1;
     }, "Check still only one address and we're back on basic-card page");
 
     is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel.slice(0, -1) + "7",
        "Check that address was edited and saved");
 
-    content.document.querySelector("basic-card-form button:last-of-type").click();
+    content.document.querySelector("basic-card-form button.save-button").click();
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       let cards = Object.entries(state.savedBasicCards);
       return cards.length == 1 &&
              cards[0][1]["cc-name"] == card["cc-name"];
     }, "Check card was edited");
 
     let cardGUIDs = Object.keys(state.savedBasicCards);
@@ -398,70 +399,78 @@ add_task(async function test_edit_link()
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "payment-summary";
     }, "Switched back to payment-summary");
   }, args);
 });
 
 add_task(async function test_private_card_adding() {
   await setup([PTU.Addresses.TimBL], [PTU.BasicCards.JohnDoe]);
-  const args = {
-    methodData: [PTU.MethodData.basicCard],
-    details: PTU.Details.total60USD,
-  };
   let privateWin = await BrowserTestUtils.openNewBrowserWindow({private: true});
-  await spawnInDialogForMerchantTask(PTU.ContentTasks.createAndShowRequest, async function check() {
-    let {
-      PaymentTestUtils: PTU,
-    } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+  await BrowserTestUtils.withNewTab({
+    gBrowser: privateWin.gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    let {win, frame} = await setupPaymentDialog(browser, {
+      methodData: [PTU.MethodData.basicCard],
+      details: PTU.Details.total60USD,
+      merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+    });
 
-    let addLink = content.document.querySelector("payment-method-picker .add-link");
-    is(addLink.textContent, "Add", "Add link text");
+    await spawnPaymentDialogTask(frame, async function check() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
-    addLink.click();
+      let addLink = content.document.querySelector("payment-method-picker .add-link");
+      is(addLink.textContent, "Add", "Add link text");
 
-    let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
-    },
-                                                          "Check add page state");
+      addLink.click();
 
-    let savedCardCount = Object.keys(state.savedBasicCards).length;
-    let tempCardCount = Object.keys(state.tempBasicCards).length;
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
+      },
+                                                "Check card page state");
+    });
 
-    let card = Object.assign({}, PTU.BasicCards.JohnDoe);
+    await fillInCardForm(frame, PTU.BasicCards.JohnDoe);
 
-    info("filling fields");
-    for (let [key, val] of Object.entries(card)) {
-      let field = content.document.getElementById(key);
-      field.value = val;
-      ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-    }
+    await spawnPaymentDialogTask(frame, async function() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
-    content.document.querySelector("basic-card-form button:last-of-type").click();
+      let card = Object.assign({}, PTU.BasicCards.JohnDoe);
+      let state = await PTU.DialogContentUtils.getCurrentState(content);
+      let savedCardCount = Object.keys(state.savedBasicCards).length;
+      let tempCardCount = Object.keys(state.tempBasicCards).length;
+      content.document.querySelector("basic-card-form button.save-button").click();
 
-    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return Object.keys(state.tempBasicCards).length > tempCardCount;
-    },
-                                                      "Check card was added to temp collection");
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return Object.keys(state.tempBasicCards).length > tempCardCount;
+      },
+                                                        "Check card was added to temp collection");
 
-    is(savedCardCount, Object.keys(state.savedBasicCards).length, "No card was saved in state");
-    is(Object.keys(state.tempBasicCards).length, 1, "Card was added temporarily");
+      is(savedCardCount, Object.keys(state.savedBasicCards).length, "No card was saved in state");
+      is(Object.keys(state.tempBasicCards).length, 1, "Card was added temporarily");
 
-    let cardGUIDs = Object.keys(state.tempBasicCards);
-    is(cardGUIDs.length, 1, "Check there is one card");
+      let cardGUIDs = Object.keys(state.tempBasicCards);
+      is(cardGUIDs.length, 1, "Check there is one card");
 
-    let tempCard = state.tempBasicCards[cardGUIDs[0]];
-    // Card number should be masked, so skip cc-number in the compare loop below
-    delete card["cc-number"];
-    for (let [key, val] of Object.entries(card)) {
-      is(tempCard[key], val, "Check " + key + ` ${tempCard[key]} matches ${val}`);
-    }
-    // check computed fields
-    is(tempCard["cc-number"], "************1111", "cc-number is masked");
-    is(tempCard["cc-given-name"], "John", "cc-given-name was computed");
-    is(tempCard["cc-family-name"], "Doe", "cc-family-name was computed");
-    ok(tempCard["cc-exp"], "cc-exp was computed");
-    ok(tempCard["cc-number-encrypted"], "cc-number-encrypted was computed");
-  }, args, {
-    browser: privateWin.gBrowser,
+      let tempCard = state.tempBasicCards[cardGUIDs[0]];
+      // Card number should be masked, so skip cc-number in the compare loop below
+      delete card["cc-number"];
+      for (let [key, val] of Object.entries(card)) {
+        is(tempCard[key], val, "Check " + key + ` ${tempCard[key]} matches ${val}`);
+      }
+      // check computed fields
+      is(tempCard["cc-number"], "************1111", "cc-number is masked");
+      is(tempCard["cc-given-name"], "John", "cc-given-name was computed");
+      is(tempCard["cc-family-name"], "Doe", "cc-family-name was computed");
+      ok(tempCard["cc-exp"], "cc-exp was computed");
+      ok(tempCard["cc-number-encrypted"], "cc-number-encrypted was computed");
+    });
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
   await BrowserTestUtils.closeWindow(privateWin);
 });
--- a/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
+++ b/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
@@ -32,17 +32,17 @@ add_task(async function test_onboarding_
 
     await spawnPaymentDialogTask(frame, async function() {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" &&
-               state.page.selectedStateKey[0] == "selectedShippingAddress";
+               state["address-page"].selectedStateKey[0] == "selectedShippingAddress";
       }, "Address page is shown first during on-boarding if there are no saved addresses");
 
       info("Checking if the address page has been rendered");
       let addressSaveButton = content.document.querySelector("address-form .save-button");
       ok(content.isVisible(addressSaveButton), "Address save button is rendered");
 
       info("Check if the total header is visible on the address page during on-boarding");
       let header = content.document.querySelector("header");
@@ -81,31 +81,33 @@ add_task(async function test_onboarding_
       ok(content.isVisible(basicCardTitle), "Basic card page title is visible");
       is(basicCardTitle.textContent, "Add Credit Card", "Basic card page title is correctly shown");
 
       info("Check if the correct billing address is selected in the basic card page");
       PTU.DialogContentUtils.waitForState(content, (state) => {
         let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
         return state.selectedShippingAddress == billingAddressSelect.value;
       }, "Shipping address is selected as the billing address");
+    });
 
-      for (let [key, val] of Object.entries(PTU.BasicCards.JohnDoe)) {
-        let field = content.document.getElementById(key);
-        field.value = val;
-        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-      }
-      content.document.querySelector("basic-card-form .save-button").click();
+    await fillInCardForm(frame, PTU.BasicCards.JohnDoe);
+
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
+    await spawnPaymentDialogTask(frame, async function() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "payment-summary";
       }, "Payment summary page is shown after the basic card page during on boarding");
 
       let cancelButton = content.document.querySelector("#cancel");
-      ok(content.isVisible(cancelButton),
-         "Payment summary page is rendered");
+      ok(content.isVisible(cancelButton), "Payment summary page is rendered");
     });
 
     info("Closing the payment dialog");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
@@ -312,18 +314,18 @@ add_task(async function test_onboarding_
 
     await spawnPaymentDialogTask(frame, async function() {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" &&
-               state.page.selectedStateKey[0] == "basic-card-page" &&
-               state.page.selectedStateKey[1] == "billingAddressGUID";
+               state["address-page"].selectedStateKey[0] == "basic-card-page" &&
+               state["address-page"].selectedStateKey[1] == "billingAddressGUID";
       // eslint-disable-next-line max-len
       }, "Billing address page is shown first during on-boarding if requestShipping is turned off");
 
       info("Checking if the billing address page has been rendered");
       let addressSaveButton = content.document.querySelector("address-form .save-button");
       ok(content.isVisible(addressSaveButton),
          "Address save button is rendered");
 
@@ -351,23 +353,26 @@ add_task(async function test_onboarding_
       let cardSaveButton = content.document.querySelector("basic-card-form .save-button");
       ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
 
       info("Check if the correct billing address is selected in the basic card page");
       PTU.DialogContentUtils.waitForState(content, (state) => {
         let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
         return state["basic-card-page"].billingAddressGUID == billingAddressSelect.value;
       }, "Billing Address is correctly shown");
+    });
 
-      for (let [key, val] of Object.entries(PTU.BasicCards.JohnDoe)) {
-        let field = content.document.getElementById(key);
-        field.value = val;
-        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-      }
-      content.document.querySelector("basic-card-form .save-button").click();
+    await fillInCardForm(frame, PTU.BasicCards.JohnDoe);
+
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
+    await spawnPaymentDialogTask(frame, async function() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "payment-summary";
       }, "payment-summary is shown after the basic card page during on boarding");
 
       let cancelButton = content.document.querySelector("#cancel");
       ok(content.isVisible(cancelButton), "Payment summary page is rendered");
     });
@@ -428,18 +433,18 @@ add_task(async function test_back_button
 
     await spawnPaymentDialogTask(frame, async function() {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page";
-      }, "Address page is shown first if there are saved addresses during on boarding");
-
+      }, "Billing address page is shown first if there are no saved addresses " +
+         "and requestShipping is false during on boarding");
       info("Checking if the address page has been rendered");
       let addressSaveButton = content.document.querySelector("address-form .save-button");
       ok(content.isVisible(addressSaveButton), "Address save button is rendered");
 
       for (let [key, val] of Object.entries(PTU.Addresses.TimBL2)) {
         let field = content.document.getElementById(key);
         if (!field) {
           ok(false, `${key} field not found`);
--- a/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
+++ b/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
@@ -32,17 +32,17 @@ add_task(async function test_show_error_
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.selectShippingOptionById, "1");
 
     info("awaiting the shippingoptionchange event");
     await ContentTask.spawn(browser, {
       eventName: "shippingoptionchange",
     }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
 
     await spawnPaymentDialogTask(frame, expectedText => {
-      let errorText = content.document.querySelector("#error-text");
+      let errorText = content.document.querySelector("header .page-error");
       is(errorText.textContent, expectedText, "Error text should be on dialog");
       ok(content.isVisible(errorText), "Error text should be visible");
     }, PTU.Details.genericShippingError.error);
 
     info("setting up the event handler for shippingaddresschange");
     await ContentTask.spawn(browser, {
       eventName: "shippingaddresschange",
       details: Object.assign({},
@@ -54,17 +54,17 @@ add_task(async function test_show_error_
     await selectPaymentDialogShippingAddressByCountry(frame, "DE");
 
     info("awaiting the shippingaddresschange event");
     await ContentTask.spawn(browser, {
       eventName: "shippingaddresschange",
     }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
 
     await spawnPaymentDialogTask(frame, () => {
-      let errorText = content.document.querySelector("#error-text");
+      let errorText = content.document.querySelector("header .page-error");
       is(errorText.textContent, "", "Error text should not be on dialog");
       ok(content.isHidden(errorText), "Error text should not be visible");
     });
 
     info("clicking cancel");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
@@ -107,17 +107,17 @@ add_task(async function test_show_field_
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return Object.keys(state.request.paymentDetails.shippingAddressErrors).length;
       }, "Check that there are shippingAddressErrors");
 
-      is(content.document.querySelector("#error-text").textContent,
+      is(content.document.querySelector("header .page-error").textContent,
          PTU.Details.fieldSpecificErrors.error,
          "Error text should be present on dialog");
 
       info("click the Edit link");
       content.document.querySelector("address-picker .edit-link").click();
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" && state["address-page"].guid;
@@ -160,17 +160,17 @@ add_task(async function test_show_field_
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "payment-summary";
       }, "Check we're back on summary view");
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return !Object.keys(state.request.paymentDetails.shippingAddressErrors).length;
       }, "Check that there are no more shippingAddressErrors");
 
-      is(content.document.querySelector("#error-text").textContent,
+      is(content.document.querySelector("header .page-error").textContent,
          "", "Error text should not be present on dialog");
 
       info("click the Edit link again");
       content.document.querySelector("address-picker .edit-link").click();
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" && state["address-page"].guid;
       }, "Check edit page state");
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -317,17 +317,17 @@ add_task(async function setup_head() {
     if (msg.isWarning || !msg.errorMessage) {
       // Ignore warnings and non-errors.
       return;
     }
     if (msg.category == "CSP_CSPViolationWithURI" && msg.errorMessage.includes("at inline")) {
       // Ignore unknown CSP error.
       return;
     }
-    if (msg.errorMessage.match(/docShell is null.*BrowserUtils.jsm/)) {
+    if (msg.message.match(/docShell is null.*BrowserUtils.jsm/)) {
       // Bug 1478142 - Console spam from the Find Toolbar.
       return;
     }
     ok(false, msg.message || msg.errorMessage);
   });
   await setupFormAutofillStorage();
   registerCleanupFunction(function cleanup() {
     paymentSrv.cleanup();
@@ -493,18 +493,27 @@ async function fillInCardForm(frame, aCa
 
     // fill the form
     info("fillInCardForm: fill the form with card: " + JSON.stringify(card));
     for (let [key, val] of Object.entries(card)) {
       let field = content.document.getElementById(key);
       if (!field) {
         ok(false, `${key} field not found`);
       }
-      field.value = val;
+      ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+      field.value = "";
+      field.focus();
+      // cc-exp-* fields are numbers so convert to strings and pad left with 0
+      let fillValue = val.toString().padStart(2, "0");
+      EventUtils.synthesizeKey(fillValue, {}, content.window);
+      ok(field.value, fillValue, `${key} value is correct after synthesizeKey`);
     }
+
+    info([...content.document.getElementById("cc-exp-year").options].map(op => op.label).join(","));
+
     let persistCheckbox = content.document.querySelector(options.checkboxSelector);
     // only touch the checked state if explicitly told to in the options
     if (options.hasOwnProperty("isTemporary")) {
       Cu.waiveXrays(persistCheckbox).checked = !options.isTemporary;
     }
   }, {card: aCard, options: aOptions});
 }
 
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -135,52 +135,36 @@ add_task(async function test_saveButton(
   sendString("+15555551212");
 
   let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
   is(form.saveButton.textContent, "Save", "Check label");
   form.saveButton.scrollIntoView();
   synthesizeMouseAtCenter(form.saveButton, {});
 
   let details = await messagePromise;
+  ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type");
+  delete details.messageID;
   is(details.collectionName, "addresses", "Check collectionName");
   isDeeply(details, {
     collectionName: "addresses",
-    errorStateChange: {
-      page: {
-        id: "address-page",
-        error: "Generic error",
-        onboardingWizard: undefined,
-      },
-      "address-page": {
-        title: "Sample page title",
-      },
-    },
     guid: undefined,
     messageType: "updateAutofillRecord",
-    preserveOldProperties: true,
     record: {
       "given-name": "Jaws",
       "family-name": "Swaj",
       "additional-name": "",
       "organization": "Allizom",
       "street-address": "404 Internet Super Highway",
       "address-level2": "Firefoxity City",
       "address-level1": "CA",
       "postal-code": "00001",
       "country": "US",
       "email": "test@example.com",
       "tel": "+15555551212",
     },
-    selectedStateKey: undefined,
-    successStateChange: {
-      page: {
-        id: "payment-summary",
-        onboardingWizard: undefined,
-      },
-    },
   }, "Check event details for the message to chrome");
   form.remove();
 });
 
 add_task(async function test_genericError() {
   let form = new AddressForm();
   await form.requestStore.setState({
     page: {
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -91,56 +91,51 @@ add_task(async function test_backButton(
 add_task(async function test_saveButton() {
   let form = new BasicCardForm();
   form.dataset.saveButtonLabel = "Save";
   form.dataset.errorGenericSave = "Generic error";
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
+  ok(form.saveButton.disabled, "Save button should initially be disabled");
   form.form.querySelector("#cc-number").focus();
-  sendString("4111111111111111");
+  sendString("4111 1111-1111 1111");
   form.form.querySelector("#cc-name").focus();
+  // Check .disabled after .focus() so that it's after both "input" and "change" events.
+  ok(form.saveButton.disabled, "Save button should still be disabled without a name");
   sendString("J. Smith");
   form.form.querySelector("#cc-exp-month").focus();
   sendString("11");
   form.form.querySelector("#cc-exp-year").focus();
   let year = (new Date()).getFullYear().toString();
   sendString(year);
+  form.saveButton.focus();
+  ok(!form.saveButton.disabled,
+     "Save button should be enabled since the required fields are filled");
 
   let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
   is(form.saveButton.textContent, "Save", "Check label");
   synthesizeMouseAtCenter(form.saveButton, {});
 
   let details = await messagePromise;
+  ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type");
+  delete details.messageID;
   is(details.collectionName, "creditCards", "Check collectionName");
   isDeeply(details, {
     collectionName: "creditCards",
-    errorStateChange: {
-      page: {
-        id: "basic-card-page",
-        error: "Generic error",
-      },
-    },
     guid: undefined,
     messageType: "updateAutofillRecord",
-    preserveOldProperties: true,
     record: {
       "cc-exp-month": "11",
       "cc-exp-year": year,
       "cc-name": "J. Smith",
-      "cc-number": "4111111111111111",
+      "cc-number": "4111 1111-1111 1111",
       "billingAddressGUID": "",
     },
-    selectedStateKey: ["selectedPaymentCard"],
-    successStateChange: {
-      page: {
-        id: "payment-summary",
-      },
-    },
   }, "Check event details for the message to chrome");
   form.remove();
 });
 
 add_task(async function test_genericError() {
   let form = new BasicCardForm();
   await form.requestStore.setState({
     page: {
--- a/browser/extensions/formautofill/FormAutofillStorage.jsm
+++ b/browser/extensions/formautofill/FormAutofillStorage.jsm
@@ -322,16 +322,20 @@ class AutofillRecords {
         if (existing.deleted) {
           this._data.splice(index, 1);
         } else {
           throw new Error(`Record ${recordToSave.guid} already exists`);
         }
       }
     } else if (!recordToSave.deleted) {
       this._normalizeRecord(recordToSave);
+      // _normalizeRecord shouldn't do any validation (throw) because in the
+      // `update` case it is called with partial records whereas
+      // `_validateFields` is called with a complete one.
+      this._validateFields(recordToSave);
 
       recordToSave.guid = this._generateGUID();
       recordToSave.version = this.version;
 
       // Metadata
       let now = Date.now();
       recordToSave.timeCreated = now;
       recordToSave.timeLastModified = now;
@@ -433,16 +437,23 @@ class AutofillRecords {
 
       this._maybeStoreLastSyncedField(recordFound, field, oldValue);
     }
 
     if (!hasValidField) {
       throw new Error("Record contains no valid field.");
     }
 
+    // _normalizeRecord above is called with the `record` argument provided to
+    // `update` which may not contain all resulting fields when
+    // `preserveOldProperties` is used. This means we need to validate for
+    // missing fields after we compose the record (`recordFound`) with the stored
+    // record like we do in the loop above.
+    this._validateFields(recordFound);
+
     recordFound.timeLastModified = Date.now();
     let syncMetadata = this._getSyncMetaData(recordFound);
     if (syncMetadata) {
       syncMetadata.changeCounter += 1;
     }
 
     this.computeFields(recordFound);
     this._data[recordFoundIndex] = recordFound;
@@ -1215,18 +1226,38 @@ class AutofillRecords {
   }
 
   // An interface to be inherited.
   _recordReadProcessor(record) {}
 
   // An interface to be inherited.
   computeFields(record) {}
 
-  // An interface to be inherited.
-  _normalizeFields(record) {}
+  /**
+  * An interface to be inherited to mutate the argument to normalize it.
+  *
+  * @param {object} partialRecord containing the record passed by the consumer of
+  *                               storage and in the case of `update` with
+  *                               `preserveOldProperties` will only include the
+  *                               properties that the user is changing so the
+  *                               lack of a field doesn't mean that the record
+  *                               won't have that field.
+  */
+  _normalizeFields(partialRecord) {}
+
+  /**
+   * An interface to be inherited to validate that the complete record is
+   * consistent and isn't missing required fields. Overrides should throw for
+   * invalid records.
+   *
+   * @param {object} record containing the complete record that would be stored
+   *                        if this doesn't throw due to an error.
+   * @throws
+   */
+  _validateFields(record) {}
 
   // An interface to be inherited.
   mergeIfPossible(guid, record, strict) {}
 }
 
 class Addresses extends AutofillRecords {
   constructor(store) {
     super(store, "addresses", VALID_ADDRESS_FIELDS, VALID_ADDRESS_COMPUTED_FIELDS, ADDRESS_SCHEMA_VERSION);
@@ -1574,17 +1605,17 @@ class CreditCards extends AutofillRecord
     delete creditCard["cc-additional-name"];
     delete creditCard["cc-family-name"];
   }
 
   _normalizeCCNumber(creditCard) {
     if (creditCard["cc-number"]) {
       let card = new CreditCard({number: creditCard["cc-number"]});
       creditCard["cc-number"] = card.number;
-      if (!creditCard["cc-number"]) {
+      if (!card.isValidNumber()) {
         delete creditCard["cc-number"];
       }
     }
   }
 
   _normalizeCCExpirationDate(creditCard) {
     let card = new CreditCard({
       expirationMonth: creditCard["cc-exp-month"],
@@ -1599,16 +1630,22 @@ class CreditCards extends AutofillRecord
     if (card.expirationYear) {
       creditCard["cc-exp-year"] = card.expirationYear;
     } else {
       delete creditCard["cc-exp-year"];
     }
     delete creditCard["cc-exp"];
   }
 
+  _validateFields(creditCard) {
+    if (!creditCard["cc-number"]) {
+      throw new Error("Missing/invalid cc-number");
+    }
+  }
+
   /**
    * Normalize the given record and return the first matched guid if storage has the same record.
    * @param {Object} targetCreditCard
    *        The credit card for duplication checking.
    * @returns {string|null}
    *          Return the first guid if storage has the same credit card and null otherwise.
    */
   getDuplicateGuid(targetCreditCard) {
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -216,17 +216,17 @@ this.FormAutofillUtils = {
   },
 
   isCreditCardField(fieldName) {
     return this._fieldNameInfo[fieldName] == "creditCard";
   },
 
   isCCNumber(ccNumber) {
     let card = new CreditCard({number: ccNumber});
-    return !!card.number;
+    return card.isValidNumber();
   },
 
   getCategoryFromFieldName(fieldName) {
     return this._fieldNameInfo[fieldName];
   },
 
   getCategoriesFromFieldNames(fieldNames) {
     let categories = new Set();
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -203,16 +203,17 @@ class EditCreditCard extends EditAutofil
    */
   constructor(elements, record, addresses, config) {
     super(elements);
 
     this._addresses = addresses;
     Object.assign(this, config);
     Object.assign(this._elements, {
       ccNumber: this._elements.form.querySelector("#cc-number"),
+      invalidCardNumberStringElement: this._elements.form.querySelector("#invalidCardNumberString"),
       year: this._elements.form.querySelector("#cc-exp-year"),
       billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
       billingAddressRow: this._elements.form.querySelector(".billingAddressRow"),
     });
 
     this.loadRecord(record, addresses);
     this.attachEventListeners();
   }
@@ -232,16 +233,19 @@ class EditCreditCard extends EditAutofil
   generateYears() {
     const count = 11;
     const currentYear = new Date().getFullYear();
     const ccExpYear = this._record && this._record["cc-exp-year"];
 
     // Clear the list
     this._elements.year.textContent = "";
 
+    // Provide an empty year option
+    this._elements.year.appendChild(new Option());
+
     if (ccExpYear && ccExpYear < currentYear) {
       this._elements.year.appendChild(new Option(ccExpYear));
     }
 
     for (let i = 0; i < count; i++) {
       let year = currentYear + i;
       let option = new Option(year);
       this._elements.year.appendChild(option);
@@ -281,17 +285,18 @@ class EditCreditCard extends EditAutofil
     if (event.target != this._elements.ccNumber) {
       return;
     }
 
     let ccNumberField = this._elements.ccNumber;
 
     // Mark the cc-number field as invalid if the number is empty or invalid.
     if (!this.isCCNumber(ccNumberField.value)) {
-      ccNumberField.setCustomValidity(true);
+      let invalidCardNumberString = this._elements.invalidCardNumberStringElement.textContent;
+      ccNumberField.setCustomValidity(invalidCardNumberString || " ");
     }
   }
 
   handleInput(event) {
     // Clear the error message if cc-number is valid
     if (event.target == this._elements.ccNumber &&
         this.isCCNumber(this._elements.ccNumber.value)) {
       this._elements.ccNumber.setCustomValidity("");
--- a/browser/extensions/formautofill/content/editCreditCard.xhtml
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -15,21 +15,22 @@
   <script src="chrome://formautofill/content/l10n.js"></script>
   <script src="chrome://formautofill/content/editDialog.js"></script>
   <script src="chrome://formautofill/content/autofillEditForms.js"></script>
 </head>
 <body dir="&locale.dir;">
   <form id="form" autocomplete="off">
     <label>
       <span data-localization="cardNumber"/>
-      <input id="cc-number" type="text"/>
+      <span id="invalidCardNumberString" hidden="hidden" data-localization="invalidCardNumber"></span>
+      <input id="cc-number" type="text" required="required" minlength="9" pattern="[- 0-9]+"/>
     </label>
     <label>
       <span data-localization="nameOnCard"/>
-      <input id="cc-name" type="text"/>
+      <input id="cc-name" type="text" required="required"/>
     </label>
     <div>
       <span data-localization="cardExpires"/>
       <select id="cc-exp-month">
         <option/>
         <option value="1">01</option>
         <option value="2">02</option>
         <option value="3">03</option>
--- a/browser/extensions/formautofill/content/editDialog.js
+++ b/browser/extensions/formautofill/content/editDialog.js
@@ -168,20 +168,24 @@ class EditCreditCardDialog extends Autof
   localizeDocument() {
     if (this._record) {
       this._elements.title.dataset.localization = "editCreditCardTitle";
     }
   }
 
   async handleSubmit() {
     let creditCard = this._elements.fieldContainer.buildFormObject();
-    if (!this._elements.fieldContainer._elements.form.checkValidity()) {
+    if (!this._elements.fieldContainer._elements.form.reportValidity()) {
       return;
     }
 
     // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
     // APIs are refactored to be async functions (bug 1399367).
     if (await MasterPassword.ensureLoggedIn()) {
-      await this.saveRecord(creditCard, this._record ? this._record.guid : null);
+      try {
+        await this.saveRecord(creditCard, this._record ? this._record.guid : null);
+        window.close();
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
     }
-    window.close();
   }
 }
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -131,11 +131,12 @@ cancelBtnLabel = Cancel
 saveBtnLabel = Save
 countryWarningMessage2 = Form Autofill is currently available only for certain countries.
 
 # LOCALIZATION NOTE (addNewCreditCardTitle, editCreditCardTitle): The dialog title for creating or editing
 # credit cards in browser preferences.
 addNewCreditCardTitle = Add New Credit Card
 editCreditCardTitle = Edit Credit Card
 cardNumber = Card Number
+invalidCardNumber = Please enter a valid card number
 nameOnCard = Name on Card
 cardExpires = Expires
 billingAddress = Billing Address
--- a/browser/extensions/formautofill/test/browser/browser_editCreditCardDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_editCreditCardDialog.js
@@ -174,21 +174,26 @@ add_task(async function test_editCreditC
 
 add_task(async function test_addInvalidCreditCard() {
   await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
     const unloadHandler = () => ok(false, "Edit credit card dialog shouldn't be closed");
     win.addEventListener("unload", unloadHandler);
 
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey("test", {}, win);
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
+    EventUtils.synthesizeKey("test name", {}, win);
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeMouseAtCenter(win.document.querySelector("#save"), {}, win);
 
     is(win.document.querySelector("form").checkValidity(), false, "cc-number is invalid");
     SimpleTest.requestFlakyTimeout("Ensure the window remains open after save attempt");
     setTimeout(() => {
       win.removeEventListener("unload", unloadHandler);
+      info("closing");
       win.close();
     }, 500);
   });
+  info("closed");
   let creditCards = await getCreditCards();
 
   is(creditCards.length, 0, "Credit card storage is empty");
 });
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -8,16 +8,17 @@
             sleep, expectPopupOpen, openPopupOn, expectPopupClose, closePopup, clickDoorhangerButton,
             getAddresses, saveAddress, removeAddresses, saveCreditCard,
             getDisplayedPopupItems, getDoorhangerCheckbox, waitForMasterPasswordDialog,
             getNotification, getDoorhangerButton, removeAllRecords, testDialog */
 
 "use strict";
 
 ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+ChromeUtils.import("resource://formautofill/MasterPassword.jsm", this);
 
 const MANAGE_ADDRESSES_DIALOG_URL = "chrome://formautofill/content/manageAddresses.xhtml";
 const MANAGE_CREDIT_CARDS_DIALOG_URL = "chrome://formautofill/content/manageCreditCards.xhtml";
 const EDIT_ADDRESS_DIALOG_URL = "chrome://formautofill/content/editAddress.xhtml";
 const EDIT_CREDIT_CARD_DIALOG_URL = "chrome://formautofill/content/editCreditCard.xhtml";
 const BASE_URL = "http://mochi.test:8888/browser/browser/extensions/formautofill/test/browser/";
 const FORM_URL = "http://mochi.test:8888/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html";
 const CREDITCARD_FORM_URL =
@@ -339,16 +340,21 @@ async function removeAllRecords() {
 async function waitForFocusAndFormReady(win) {
   return Promise.all([
     new Promise(resolve => waitForFocus(resolve, win)),
     BrowserTestUtils.waitForEvent(win, "FormReady"),
   ]);
 }
 
 async function testDialog(url, testFn, arg = undefined) {
+  if (url == EDIT_CREDIT_CARD_DIALOG_URL && arg) {
+    arg = Object.assign({}, arg, {
+      "cc-number": await MasterPassword.decrypt(arg["cc-number-encrypted"]),
+    });
+  }
   let win = window.openDialog(url, null, "width=600,height=600", arg);
   await waitForFocusAndFormReady(win);
   let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
   await testFn(win);
   return unloadPromise;
 }
 
 registerCleanupFunction(removeAllRecords);
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -241,28 +241,28 @@ const TESTCASES = [
                <input id="cc-number" autocomplete="cc-number">
                <input id="cc-name" autocomplete="cc-name">
                <input id="cc-exp-month" autocomplete="cc-exp-month">
                <input id="cc-exp-year" autocomplete="cc-exp-year">
                </form>`,
     focusedInputId: "cc-number",
     profileData: {
       "guid": "123",
-      "cc-number": "1234000056780000",
+      "cc-number": "4111111111111111",
       "cc-name": "test name",
       "cc-exp-month": "06",
       "cc-exp-year": "25",
     },
     expectedResult: {
       "street-addr": "",
       "city": "",
       "country": "",
       "email": "",
       "tel": "",
-      "cc-number": "1234000056780000",
+      "cc-number": "4111111111111111",
       "cc-name": "test name",
       "cc-exp-month": "06",
       "cc-exp-year": "25",
     },
   },
 
 
 ];
--- a/browser/extensions/formautofill/test/unit/test_createRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_createRecords.js
@@ -230,41 +230,41 @@ const TESTCASES = [
   {
     description: "A credit card form with the value of cc-number, cc-exp, and cc-name.",
     document: `<form>
                 <input id="cc-number" autocomplete="cc-number">
                 <input id="cc-name" autocomplete="cc-name">
                 <input id="cc-exp" autocomplete="cc-exp">
                </form>`,
     formValue: {
-      "cc-number": "4444000022220000",
+      "cc-number": "5105105105105100",
       "cc-name": "Foo Bar",
       "cc-exp": "2022-06",
     },
     expectedRecord: {
       address: [],
       creditCard: [{
-        "cc-number": "4444000022220000",
+        "cc-number": "5105105105105100",
         "cc-name": "Foo Bar",
         "cc-exp": "2022-06",
       }],
     },
   },
   {
     description: "A credit card form with cc-number value only.",
     document: `<form>
                 <input id="cc-number" autocomplete="cc-number">
                </form>`,
     formValue: {
-      "cc-number": "4444000022220000",
+      "cc-number": "4111111111111111",
     },
     expectedRecord: {
       address: [],
       creditCard: [{
-        "cc-number": "4444000022220000",
+        "cc-number": "4111111111111111",
       }],
     },
   },
   {
     description: "A credit card form must have cc-number value.",
     document: `<form>
                 <input id="cc-number" autocomplete="cc-number">
                 <input id="cc-name" autocomplete="cc-name">
@@ -327,20 +327,20 @@ const TESTCASES = [
       "family-name-shipping": "Doe",
       "organization-shipping": "Mozilla",
       "country-shipping": "US",
 
       "given-name-billing": "Foo",
       "organization-billing": "Bar",
       "country-billing": "US",
 
-      "cc-number-section-one": "4444000022220000",
+      "cc-number-section-one": "4111111111111111",
       "cc-name-section-one": "John",
 
-      "cc-number-section-two": "4444000022221111",
+      "cc-number-section-two": "5105105105105100",
       "cc-name-section-two": "Foo Bar",
       "cc-exp-section-two": "2026-26",
     },
     expectedRecord: {
       address: [{
         "given-name": "Bar",
         "organization": "Foo",
         "country": "US",
@@ -350,20 +350,20 @@ const TESTCASES = [
         "organization": "Mozilla",
         "country": "US",
       }, {
         "given-name": "Foo",
         "organization": "Bar",
         "country": "US",
       }],
       creditCard: [{
-        "cc-number": "4444000022220000",
+        "cc-number": "4111111111111111",
         "cc-name": "John",
       }, {
-        "cc-number": "4444000022221111",
+        "cc-number": "5105105105105100",
         "cc-name": "Foo Bar",
         "cc-exp": "2026-26",
       }],
     },
   },
 
 ];
 
--- a/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
+++ b/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
@@ -77,45 +77,45 @@ const TESTCASES = [
         creditCard: [],
       },
     },
   },
   {
     description: "Trigger credit card saving",
     formValue: {
       "cc-name": "John Doe",
-      "cc-number": "1234567812345678",
+      "cc-number": "5105105105105100",
       "cc-exp-month": 12,
       "cc-exp-year": 2000,
     },
     expectedResult: {
       formSubmission: true,
       records: {
         address: [],
         creditCard: [{
           guid: null,
           record: {
             "cc-name": "John Doe",
-            "cc-number": "1234567812345678",
+            "cc-number": "5105105105105100",
             "cc-exp-month": 12,
             "cc-exp-year": 2000,
           },
           untouchedFields: [],
         }],
       },
     },
   },
   {
     description: "Trigger address and credit card saving",
     formValue: {
       "street-addr": "331 E. Evelyn Avenue",
       "country": "USA",
       "tel": "1-650-903-0800",
       "cc-name": "John Doe",
-      "cc-number": "1234567812345678",
+      "cc-number": "5105105105105100",
       "cc-exp-month": 12,
       "cc-exp-year": 2000,
     },
     expectedResult: {
       formSubmission: true,
       records: {
         address: [{
           guid: null,
@@ -128,17 +128,17 @@ const TESTCASES = [
             "tel": "1-650-903-0800",
           },
           untouchedFields: [],
         }],
         creditCard: [{
           guid: null,
           record: {
             "cc-name": "John Doe",
-            "cc-number": "1234567812345678",
+            "cc-number": "5105105105105100",
             "cc-exp-month": 12,
             "cc-exp-year": 2000,
           },
           untouchedFields: [],
         }],
       },
     },
   },
--- a/browser/extensions/formautofill/test/unit/test_transformFields.js
+++ b/browser/extensions/formautofill/test/unit/test_transformFields.js
@@ -521,19 +521,21 @@ const ADDRESS_NORMALIZE_TESTCASES = [
 ];
 
 const CREDIT_CARD_COMPUTE_TESTCASES = [
   // Name
   {
     description: "Has \"cc-name\"",
     creditCard: {
       "cc-name": "Timothy John Berners-Lee",
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-name": "Timothy John Berners-Lee",
+      "cc-number": "************1045",
       "cc-given-name": "Timothy",
       "cc-additional-name": "John",
       "cc-family-name": "Berners-Lee",
     },
   },
 
   // Card Number
   {
@@ -547,66 +549,76 @@ const CREDIT_CARD_COMPUTE_TESTCASES = [
   },
 
   // Expiration Date
   {
     description: "Has \"cc-exp-year\" and \"cc-exp-month\"",
     creditCard: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
       "cc-exp": "2022-12",
+      "cc-number": "************1045",
     },
   },
   {
     description: "Has only \"cc-exp-month\"",
     creditCard: {
       "cc-exp-month": 12,
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp": undefined,
+      "cc-number": "************1045",
     },
   },
   {
     description: "Has only \"cc-exp-year\"",
     creditCard: {
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-year": 2022,
       "cc-exp": undefined,
+      "cc-number": "************1045",
     },
   },
 ];
 
 const CREDIT_CARD_NORMALIZE_TESTCASES = [
   // Name
   {
     description: "Has both \"cc-name\" and the split name fields",
     creditCard: {
       "cc-name": "Timothy John Berners-Lee",
       "cc-given-name": "John",
       "cc-family-name": "Doe",
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-name": "Timothy John Berners-Lee",
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has only the split name fields",
     creditCard: {
       "cc-given-name": "John",
       "cc-family-name": "Doe",
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-name": "John Doe",
+      "cc-number": "4929001587121045",
     },
   },
 
   // Card Number
   {
     description: "Regular number",
     creditCard: {
       "cc-number": "4929001587121045",
@@ -633,161 +645,191 @@ const CREDIT_CARD_NORMALIZE_TESTCASES = 
       "cc-number": "4111111111111111",
     },
   },
 
   // Expiration Date
   {
     description: "Has \"cc-exp\" formatted \"yyyy-mm\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "2022-12",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"yyyy/mm\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "2022/12",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"yyyy-m\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "2022-3",
     },
     expectedResult: {
       "cc-exp-month": 3,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"yyyy/m\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "2022/3",
     },
     expectedResult: {
       "cc-exp-month": 3,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"mm-yyyy\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "12-2022",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"mm/yyyy\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "12/2022",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"m-yyyy\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "3-2022",
     },
     expectedResult: {
       "cc-exp-month": 3,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"m/yyyy\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "3/2022",
     },
     expectedResult: {
       "cc-exp-month": 3,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"mm-yy\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "12-22",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"mm/yy\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "12/22",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"yy-mm\"",
     creditCard: {
+      "cc-number": "4929001587121045",
       "cc-exp": "22-12",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"yy/mm\"",
     creditCard: {
       "cc-exp": "22/12",
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"mmyy\"",
     creditCard: {
       "cc-exp": "1222",
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" formatted \"yymm\"",
     creditCard: {
       "cc-exp": "2212",
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has \"cc-exp\" with spaces",
     creditCard: {
       "cc-exp": "  2033-11  ",
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 11,
       "cc-exp-year": 2033,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has invalid \"cc-exp\"",
     creditCard: {
       "cc-number": "4111111111111111", // Make sure it won't be an empty record.
       "cc-exp": "99-9999",
     },
@@ -797,42 +839,48 @@ const CREDIT_CARD_NORMALIZE_TESTCASES = 
     },
   },
   {
     description: "Has both \"cc-exp-*\" and \"cc-exp\"",
     creditCard: {
       "cc-exp": "2022-12",
       "cc-exp-month": 3,
       "cc-exp-year": 2030,
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 3,
       "cc-exp-year": 2030,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has only \"cc-exp-year\" and \"cc-exp\"",
     creditCard: {
       "cc-exp": "2022-12",
       "cc-exp-year": 2030,
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
   {
     description: "Has only \"cc-exp-month\" and \"cc-exp\"",
     creditCard: {
       "cc-exp": "2022-12",
       "cc-exp-month": 3,
+      "cc-number": "4929001587121045",
     },
     expectedResult: {
       "cc-exp-month": 12,
       "cc-exp-year": 2022,
+      "cc-number": "4929001587121045",
     },
   },
 ];
 
 let do_check_record_matches = (expectedRecord, record) => {
   for (let key in expectedRecord) {
     Assert.equal(expectedRecord[key], record[key]);
   }
--- a/browser/modules/AboutNewTab.jsm
+++ b/browser/modules/AboutNewTab.jsm
@@ -6,17 +6,17 @@
 
 var EXPORTED_SYMBOLS = [ "AboutNewTab" ];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ActivityStream: "resource://activity-stream/lib/ActivityStream.jsm",
-  RemotePages: "resource://gre/modules/RemotePageManager.jsm"
+  RemotePages: "resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm"
 });
 
 const BROWSER_READY_NOTIFICATION = "sessionstore-windows-restored";
 
 var AboutNewTab = {
   QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
   // AboutNewTab
--- a/browser/modules/ContentCrashHandlers.jsm
+++ b/browser/modules/ContentCrashHandlers.jsm
@@ -12,17 +12,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   clearTimeout: "resource://gre/modules/Timer.jsm",
   CrashSubmit: "resource://gre/modules/CrashSubmit.jsm",
   PluralForm: "resource://gre/modules/PluralForm.jsm",
-  RemotePages: "resource://gre/modules/RemotePageManager.jsm",
+  RemotePages: "resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm"
 });
 
 XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
   const url = "chrome://browser/locale/browser.properties";
   return Services.strings.createBundle(url);
 });
--- a/devtools/client/framework/test/browser_toolbox_options_panel_toggle.js
+++ b/devtools/client/framework/test/browser_toolbox_options_panel_toggle.js
@@ -39,19 +39,20 @@ add_task(async function() {
 async function sendOptionsKeyEvent(toolbox) {
   const onReady = toolbox.once("select");
   EventUtils.synthesizeKey("VK_F1", {}, toolbox.win);
   await onReady;
 }
 
 async function clickSettingsMenu(toolbox) {
   const onPopupShown = () => {
-    toolbox.doc.removeEventListener("popuphidden", onPopupShown);
+    toolbox.doc.removeEventListener("popupshown", onPopupShown);
     const menuItem = toolbox.doc.getElementById("toolbox-meatball-menu-settings");
     EventUtils.synthesizeMouseAtCenter(menuItem, {}, menuItem.ownerGlobal);
   };
   toolbox.doc.addEventListener("popupshown", onPopupShown);
 
   const button = toolbox.doc.getElementById("toolbox-meatball-menu-button");
+  await waitUntil(() => button.style.pointerEvents !== "none");
   EventUtils.synthesizeMouseAtCenter(button, {}, button.ownerGlobal);
 
   await toolbox.once("select");
 }
--- a/devtools/client/shared/components/menu/MenuButton.js
+++ b/devtools/client/shared/components/menu/MenuButton.js
@@ -5,16 +5,17 @@
 /* eslint-env browser */
 "use strict";
 
 // A button that toggles a doorhanger menu.
 
 const { PureComponent } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const Services = require("Services");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { button } = dom;
 const {
   HTMLTooltip,
 } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 
 class MenuButton extends PureComponent {
   static get propTypes() {
@@ -162,26 +163,54 @@ class MenuButton extends PureComponent {
     this.tooltip.updateContainerBounds(this.buttonRef, {
       position: this.props.menuPosition,
       y: this.props.menuOffset,
     });
   }
 
   onHidden() {
     this.setState({ expanded: false });
+    // While the menu is open, if we click _anywhere_ outside the menu, it will
+    // automatically close. This is performed by the XUL wrapper before we get
+    // any chance to see any event. To avoid immediately re-opening the menu
+    // when we process the subsequent click event on this button, we set
+    // 'pointer-events: none' on the button while the menu is open.
+    //
+    // After the menu is closed we need to remove the pointer-events style (so
+    // the button works again) but we don't want to do it immediately since the
+    // "popuphidden" event which triggers this callback might be dispatched
+    // before the "click" event that we want to ignore.  As a result, we queue
+    // up a task using setTimeout() to run after the "click" event.
+    this.state.win.setTimeout(() => {
+      if (this.buttonRef) {
+        this.buttonRef.style.pointerEvents = "auto";
+      }
+    }, 0);
   }
 
   async onClick(e) {
     if (e.target === this.buttonRef) {
       if (this.props.onClick) {
         this.props.onClick(e);
       }
 
       if (!e.defaultPrevented) {
         const wasKeyboardEvent = e.screenX === 0 && e.screenY === 0;
+        // If the popup menu will be shown, disable this button in order to
+        // prevent reopening the popup menu. See extended comment in onHidden().
+        // above.
+        //
+        // Also, we should _not_ set 'pointer-events: none' if
+        // ui.popup.disable_autohide pref is in effect since, in that case,
+        // there's no redundant hiding behavior and we actually want clicking
+        // the button to close the menu.
+        if (!this.state.expanded &&
+            !Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
+          this.buttonRef.style.pointerEvents = "none";
+        }
         await this.toggleMenu(e.target);
         // If the menu was activated by keyboard, focus the first item.
         if (wasKeyboardEvent && this.tooltip) {
           this.tooltip.focus();
         }
       }
     // If we clicked one of the menu items, then, by default, we should
     // auto-collapse the menu.
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -10,16 +10,17 @@ support-files =
   storage-empty-objectstores.html
   storage-file-url.html
   storage-idb-delete-blocked.html
   storage-indexeddb-duplicate-names.html
   storage-listings.html
   storage-listings-usercontextid.html
   storage-listings-with-fragment.html
   storage-localstorage.html
+  storage-overflow-indexeddb.html
   storage-overflow.html
   storage-search.html
   storage-secured-iframe.html
   storage-secured-iframe-usercontextid.html
   storage-sessionstorage.html
   storage-unsecured-iframe.html
   storage-unsecured-iframe-usercontextid.html
   storage-updates.html
@@ -52,16 +53,17 @@ tags = usercontextid
 [browser_storage_dynamic_updates_cookies.js]
 [browser_storage_dynamic_updates_localStorage.js]
 [browser_storage_dynamic_updates_sessionStorage.js]
 [browser_storage_empty_objectstores.js]
 [browser_storage_file_url.js]
 [browser_storage_indexeddb_delete.js]
 [browser_storage_indexeddb_delete_blocked.js]
 [browser_storage_indexeddb_duplicate_names.js]
+[browser_storage_indexeddb_overflow.js]
 [browser_storage_localstorage_add.js]
 [browser_storage_localstorage_edit.js]
 [browser_storage_localstorage_error.js]
 [browser_storage_localstorage_rapid_add_remove.js]
 [browser_storage_overflow.js]
 [browser_storage_search.js]
 [browser_storage_search_keyboard_trap.js]
 [browser_storage_sessionstorage_add.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_overflow.js
@@ -0,0 +1,35 @@
+/* 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/. */
+
+// Test endless scrolling when a lot of items are present in the storage
+// inspector table for IndexedDB.
+"use strict";
+
+const ITEMS_PER_PAGE = 50;
+
+add_task(async function() {
+  await openTabAndSetupStorage(MAIN_DOMAIN + "storage-overflow-indexeddb.html");
+
+  info("Run the tests with short DevTools");
+  await runTests();
+
+  info("Close Toolbox");
+  const target = TargetFactory.forTab(gBrowser.selectedTab);
+  await gDevTools.closeToolbox(target);
+
+  await finishTests();
+});
+
+async function runTests() {
+  gUI.tree.expandAll();
+
+  await selectTreeItem(["indexedDB",
+                        "http://test1.example.org",
+                        "database (default)",
+                        "store"]);
+  checkCellLength(ITEMS_PER_PAGE);
+
+  await scroll();
+  checkCellLength(ITEMS_PER_PAGE * 2);
+}
--- a/devtools/client/storage/test/browser_storage_overflow.js
+++ b/devtools/client/storage/test/browser_storage_overflow.js
@@ -1,8 +1,12 @@
+/* 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/. */
+
 // Test endless scrolling when a lot of items are present in the storage
 // inspector table.
 "use strict";
 
 const ITEMS_PER_PAGE = 50;
 
 add_task(async function() {
   await openTabAndSetupStorage(MAIN_DOMAIN + "storage-overflow.html");
@@ -42,35 +46,16 @@ async function runTests() {
 
   // Sort descending.
   clickColumnHeader("name");
 
   // Check that the columns are sorted in a human readable way (descending).
   checkCellValues("DEC");
 }
 
-function checkCellLength(len) {
-  const cells = gPanelWindow.document
-                          .querySelectorAll("#name .table-widget-cell");
-  const msg = `Table should initially display ${len} items`;
-
-  is(cells.length, len, msg);
-}
-
 function checkCellValues(order) {
   const cells = [...gPanelWindow.document
                               .querySelectorAll("#name .table-widget-cell")];
   cells.forEach(function(cell, index, arr) {
     const i = order === "ASC" ? index + 1 : arr.length - index;
     is(cell.value, `item-${i}`, `Cell value is correct (${order}).`);
   });
 }
-
-async function scroll() {
-  const $ = id => gPanelWindow.document.querySelector(id);
-  const table = $("#storage-table .table-widget-body");
-  const cell = $("#name .table-widget-cell");
-  const cellHeight = cell.getBoundingClientRect().height;
-
-  const onStoresUpdate = gUI.once("store-objects-updated");
-  table.scrollTop += cellHeight * 50;
-  await onStoresUpdate;
-}
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -998,8 +998,26 @@ async function performAdd(store) {
   const rowId = await eventEdit;
   await eventWait;
 
   const key = type === "cookies" ? "uniqueKey" : "name";
   const value = getCellValue(rowId, key);
 
   is(rowId, value, `Row '${rowId}' was successfully added.`);
 }
+
+function checkCellLength(len) {
+  const cells = gPanelWindow.document.querySelectorAll("#name .table-widget-cell");
+  const msg = `Table should initially display ${len} items`;
+
+  is(cells.length, len, msg);
+}
+
+async function scroll() {
+  const $ = id => gPanelWindow.document.querySelector(id);
+  const table = $("#storage-table .table-widget-body");
+  const cell = $("#name .table-widget-cell");
+  const cellHeight = cell.getBoundingClientRect().height;
+
+  const onStoresUpdate = gUI.once("store-objects-updated");
+  table.scrollTop += cellHeight * 50;
+  await onStoresUpdate;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/storage-overflow-indexeddb.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1171903 - Storage Inspector endless scrolling
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Storage inspector endless scrolling test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+window.setup = async function() {
+  await new Promise(resolve => {
+    const open = indexedDB.open("database", 1);
+    open.onupgradeneeded = function() {
+      const db = open.result;
+      const store = db.createObjectStore("store", {keyPath: "id"});
+      store.transaction.oncomplete = () => {
+        const transaction = db.transaction(["store"], "readwrite");
+        for (let i = 1; i < 150; i++) {
+          transaction.objectStore("store").add({id: i});
+        }
+
+        transaction.oncomplete = function() {
+          db.close();
+          resolve();
+        };
+      };
+    };
+  });
+};
+
+function deleteDB(dbName, storage) {
+  return new Promise(resolve => {
+    indexedDB.deleteDatabase(dbName, { storage: storage }).onsuccess = resolve;
+  });
+}
+
+window.clear = async function() {
+  await deleteDB("database", "temporary");
+  await deleteDB("database", "default");
+  await deleteDB("database", "persistent");
+
+  dump(`removed indexedDB data from ${document.location}\n`);
+};
+
+</script>
+</body>
+</html>
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -304,25 +304,16 @@ class JSTerm extends Component {
               ) {
                 return null;
               }
 
               this.clearCompletion();
               return "CodeMirror.Pass";
             },
 
-            "Esc": () => {
-              if (this.autocompletePopup.isOpen) {
-                this.clearCompletion();
-                return null;
-              }
-
-              return "CodeMirror.Pass";
-            },
-
             "PageUp": () => {
               if (this.autocompletePopup.isOpen) {
                 this.complete(this.COMPLETE_PAGEUP);
                 return null;
               }
 
               return "CodeMirror.Pass";
             },
@@ -348,25 +339,35 @@ class JSTerm extends Component {
             "End": () => {
               if (this.autocompletePopup.isOpen) {
                 this.autocompletePopup.selectedIndex =
                   this.autocompletePopup.itemCount - 1;
                 return null;
               }
 
               return "CodeMirror.Pass";
-            }
+            },
+
+            "Esc": false,
           }
         });
 
         this.editor.on("change", this._inputEventHandler);
         this.editor.appendToLocalElement(this.node);
         const cm = this.editor.codeMirror;
         cm.on("paste", (_, event) => this.props.onPaste(event));
         cm.on("drop", (_, event) => this.props.onPaste(event));
+
+        this.node.addEventListener("keydown", event => {
+          if (event.keyCode === KeyCodes.DOM_VK_ESCAPE && this.autocompletePopup.isOpen) {
+            this.clearCompletion();
+            event.preventDefault();
+            event.stopPropagation();
+          }
+        });
       }
     } else if (this.inputNode) {
       this.inputNode.addEventListener("keypress", this._keyPress);
       this.inputNode.addEventListener("input", this._inputEventHandler);
       this.inputNode.addEventListener("keyup", this._inputEventHandler);
       this.focus();
     }
 
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_split.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_split.js
@@ -7,21 +7,30 @@
 
 const TEST_URI = "data:text/html;charset=utf-8,Web Console test for splitting";
 const {Toolbox} = require("devtools/client/framework/toolbox");
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N =
   new LocalizationHelper("devtools/client/locales/toolbox.properties");
 
 // Test is slow on Linux EC2 instances - Bug 962931
-requestLongerTimeout(2);
+requestLongerTimeout(4);
 
 add_task(async function() {
+  // Run test with legacy JsTerm
+  await pushPref("devtools.webconsole.jsterm.codeMirror", false);
+  await performTests();
+  // And then run it with the CodeMirror-powered one.
+  await pushPref("devtools.webconsole.jsterm.codeMirror", true);
+  await performTests();
+});
+
+async function performTests() {
   let toolbox;
-
+  await pushPref("devtools.webconsole.jsterm.codeMirror", true);
   await addTab(TEST_URI);
   await testConsoleLoadOnDifferentPanel();
   await testKeyboardShortcuts();
   await checkAllTools();
 
   info("Testing host types");
   checkHostType(Toolbox.HostType.BOTTOM);
   await checkToolboxUI();
@@ -244,9 +253,9 @@ add_task(async function() {
   }
 
   function checkHostType(hostType) {
     is(toolbox.hostType, hostType, "host type is " + hostType);
 
     const pref = Services.prefs.getCharPref("devtools.toolbox.host");
     is(pref, hostType, "host pref is " + hostType);
   }
-});
+}
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -375,35 +375,51 @@ const proto = {
           // The above can throw if the cache becomes stale.
         }
         if (!getter) {
           obj._safeGetters = null;
           continue;
         }
 
         const result = getter.call(this.obj);
-        if (result && !("throw" in result)) {
-          let getterValue = undefined;
-          if ("return" in result) {
-            getterValue = result.return;
-          } else if ("yield" in result) {
-            getterValue = result.yield;
+        if (!result || "throw" in result) {
+          continue;
+        }
+
+        let getterValue = undefined;
+        if ("return" in result) {
+          getterValue = result.return;
+        } else if ("yield" in result) {
+          getterValue = result.yield;
+        }
+
+        // Treat an already-rejected Promise as we would a thrown exception
+        // by not including it as a safe getter value (see Bug 1477765).
+        if (getterValue && (getterValue.class == "Promise" &&
+                            getterValue.promiseState == "rejected")) {
+          // Until we have a good way to handle Promise rejections through the
+          // debugger API (Bug 1478076), call `catch` when it's safe to do so.
+          const raw = getterValue.unsafeDereference();
+          if (DevToolsUtils.isSafeJSObject(raw)) {
+            raw.catch(e=>e);
           }
-          // WebIDL attributes specified with the LenientThis extended attribute
-          // return undefined and should be ignored.
-          if (getterValue !== undefined) {
-            safeGetterValues[name] = {
-              getterValue: this.hooks.createValueGrip(getterValue),
-              getterPrototypeLevel: level,
-              enumerable: desc.enumerable,
-              writable: level == 0 ? desc.writable : true,
-            };
-            if (limit && ++i == limit) {
-              break;
-            }
+          continue;
+        }
+
+        // WebIDL attributes specified with the LenientThis extended attribute
+        // return undefined and should be ignored.
+        if (getterValue !== undefined) {
+          safeGetterValues[name] = {
+            getterValue: this.hooks.createValueGrip(getterValue),
+            getterPrototypeLevel: level,
+            enumerable: desc.enumerable,
+            writable: level == 0 ? desc.writable : true,
+          };
+          if (limit && ++i == limit) {
+            break;
           }
         }
       }
       if (limit && i == limit) {
         break;
       }
 
       obj = obj.proto;
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -332,17 +332,17 @@ StorageActors.defaults = function(typeNa
      * @return {object} An object containing following properties:
      *          - offset - The actual offset of the returned array. This might
      *                     be different from the requested offset if that was
      *                     invalid
      *          - total - The total number of entries possible.
      *          - data - The requested values.
      */
     async getStoreObjects(host, names, options = {}) {
-      let offset = options.offset || 0;
+      const offset = options.offset || 0;
       let size = options.size || MAX_STORE_OBJECT_COUNT;
       if (size > MAX_STORE_OBJECT_COUNT) {
         size = MAX_STORE_OBJECT_COUNT;
       }
       const sortOn = options.sortOn || "name";
 
       const toReturn = {
         offset: offset,
@@ -376,50 +376,46 @@ StorageActors.defaults = function(typeNa
           } else if (objectStores) {
             toReturn.data.push(...objectStores);
           } else {
             toReturn.data.push(...values);
           }
         }
 
         toReturn.total = this.getObjectsSize(host, names, options);
-
-        if (offset > toReturn.total) {
-          // In this case, toReturn.data is an empty array.
-          toReturn.offset = toReturn.total;
-          toReturn.data = [];
-        } else {
-          // We need to use natural sort before slicing.
-          const sorted = toReturn.data.sort((a, b) => {
-            return naturalSortCaseInsensitive(a[sortOn], b[sortOn]);
-          });
-          const sliced = sorted.slice(offset, offset + size);
-          toReturn.data = sliced.map(a => this.toStoreObject(a));
-        }
       } else {
         let obj = await this.getValuesForHost(host, undefined, undefined,
                                               this.hostVsStores, principal);
         if (obj.dbs) {
           obj = obj.dbs;
         }
 
         toReturn.total = obj.length;
-
-        if (offset > toReturn.total) {
-          // In this case, toReturn.data is an empty array.
-          toReturn.offset = offset = toReturn.total;
-          toReturn.data = [];
+        toReturn.data = obj;
+      }
+
+      if (offset > toReturn.total) {
+        // In this case, toReturn.data is an empty array.
+        toReturn.offset = toReturn.total;
+        toReturn.data = [];
+      } else {
+        // We need to use natural sort before slicing.
+        const sorted = toReturn.data.sort((a, b) => {
+          return naturalSortCaseInsensitive(a[sortOn], b[sortOn]);
+        });
+        let sliced;
+        if (this.typeName === "indexedDB") {
+          // indexedDB's getValuesForHost never returns *all* values available but only
+          // a slice, starting at the expected offset. Therefore the result is already
+          // sliced as expected.
+          sliced = sorted;
         } else {
-          // We need to use natural sort before slicing.
-          const sorted = obj.sort((a, b) => {
-            return naturalSortCaseInsensitive(a[sortOn], b[sortOn]);
-          });
-          const sliced = sorted.slice(offset, offset + size);
-          toReturn.data = sliced.map(object => this.toStoreObject(object));
+          sliced = sorted.slice(offset, offset + size);
         }
+        toReturn.data = sliced.map(a => this.toStoreObject(a));
       }
 
       return toReturn;
     },
 
     getPrincipal(win) {
       if (win) {
         return win.document.nodePrincipal;
@@ -2353,25 +2349,25 @@ var indexedDBHelpers = {
       return this.backToChild("getValuesForHost", {objectStores: objectStores});
     }
     // Get either all entries from the object store, or a particular id
     const storage = hostVsStores.get(host).get(db2).storage;
     const result = await this.getObjectStoreData(host, principal, db2, storage, {
       objectStore: objectStore,
       id: id,
       index: options.index,
-      offset: 0,
+      offset: options.offset,
       size: options.size
     });
     return this.backToChild("getValuesForHost", {result: result});
   },
 
   /**
-   * Returns all or requested entries from a particular objectStore from the db
-   * in the given host.
+   * Returns requested entries (or at most MAX_STORE_OBJECT_COUNT) from a particular
+   * objectStore from the db in the given host.
    *
    * @param {string} host
    *        The given host.
    * @param {nsIPrincipal} principal
    *        The principal of the given document.
    * @param {string} dbName
    *        The name of the indexed db from the above host.
    * @param {String} storage
--- a/devtools/shared/webconsole/test/test_bug819670_getter_throws.html
+++ b/devtools/shared/webconsole/test/test_bug819670_getter_throws.html
@@ -48,17 +48,16 @@ function onEvaluate(aState, aResponse)
 function onInspect(aState, aResponse)
 {
   ok(!aResponse.error, "no response error");
 
   let expectedProps = {
     "addBroadcastListenerFor": { value: { type: "object" } },
     "commandDispatcher": { get: { type: "object" } },
     "getBoxObjectFor": { value: { type: "object" } },
-    "getElementsByAttribute": { value: { type: "object" } },
   };
 
   let props = aResponse.ownProperties;
   ok(props, "response properties available");
 
   if (props) {
     ok(Object.keys(props).length > Object.keys(expectedProps).length,
        "number of enumerable properties");
--- a/devtools/shared/webconsole/test/test_object_actor_native_getters_lenient_this.html
+++ b/devtools/shared/webconsole/test/test_object_actor_native_getters_lenient_this.html
@@ -61,21 +61,17 @@ function onConsoleCall(aState, aType, aP
 function onProperties(aState, aResponse)
 {
   let props = aResponse.ownProperties;
   let keys = Object.keys(props);
   info(keys.length + " ownProperties: " + keys);
 
   is(keys.length, 0, "number of properties");
   keys = Object.keys(aResponse.safeGetterValues);
-  // There is one "safe getter" as far as the code in _findSafeGetterValues is
-  // concerned, because it treats any Promise-returning attribute as a "safe
-  // getter".  See bug 1438015.
-  is(keys.length, 1, "number of safe getters");
-  is(keys[0], "documentReadyForIdle", "Unexpected safe getter");
+  is(keys.length, 0, "number of safe getters");
 
   closeDebugger(aState, function() {
     SimpleTest.finish();
   });
 }
 
 addEventListener("load", startTest);
 </script>
--- a/dom/base/nsAttrValue.cpp
+++ b/dom/base/nsAttrValue.cpp
@@ -29,16 +29,46 @@
 #include "nsIDocument.h"
 #include <algorithm>
 
 using namespace mozilla;
 
 #define MISC_STR_PTR(_cont) \
   reinterpret_cast<void*>((_cont)->mStringBits & NS_ATTRVALUE_POINTERVALUE_MASK)
 
+/* static */ MiscContainer*
+nsAttrValue::AllocMiscContainer()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MiscContainer* cont = nullptr;
+  Swap(cont, sMiscContainerCache);
+
+  if (cont) {
+    return new (cont) MiscContainer;
+  }
+
+  return new MiscContainer;
+}
+
+/* static */ void
+nsAttrValue::DeallocMiscContainer(MiscContainer* aCont)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!aCont) {
+    return;
+  }
+
+  if (!sMiscContainerCache) {
+    aCont->~MiscContainer();
+    sMiscContainerCache = aCont;
+  } else {
+    delete aCont;
+  }
+}
+
 bool
 MiscContainer::GetString(nsAString& aString) const
 {
   void* ptr = MISC_STR_PTR(this);
 
   if (!ptr) {
     return false;
   }
@@ -117,16 +147,17 @@ MiscContainer::Evict()
   DebugOnly<bool> gotString = GetString(str);
   MOZ_ASSERT(gotString);
 
   sheet->EvictStyleAttr(str, this);
   mValue.mCached = 0;
 }
 
 nsTArray<const nsAttrValue::EnumTable*>* nsAttrValue::sEnumTableArray = nullptr;
+MiscContainer* nsAttrValue::sMiscContainerCache = nullptr;
 
 nsAttrValue::nsAttrValue()
     : mBits(0)
 {
 }
 
 nsAttrValue::nsAttrValue(const nsAttrValue& aOther)
     : mBits(0)
@@ -172,18 +203,24 @@ nsAttrValue::Init()
   sEnumTableArray = new nsTArray<const EnumTable*>;
   return NS_OK;
 }
 
 /* static */
 void
 nsAttrValue::Shutdown()
 {
+  MOZ_ASSERT(NS_IsMainThread());
   delete sEnumTableArray;
   sEnumTableArray = nullptr;
+  // The MiscContainer pointed to by sMiscContainerCache has already
+  // be destructed so `delete sMiscContainerCache` is
+  // dangerous. Invoke `operator delete` to free the memory.
+  ::operator delete(sMiscContainerCache);
+  sMiscContainerCache = nullptr;
 }
 
 void
 nsAttrValue::Reset()
 {
   switch(BaseType()) {
     case eStringBase:
     {
@@ -197,17 +234,17 @@ nsAttrValue::Reset()
     case eOtherBase:
     {
       MiscContainer* cont = GetMiscContainer();
       if (cont->IsRefCounted() && cont->mValue.mRefCount > 1) {
         NS_RELEASE(cont);
         break;
       }
 
-      delete ClearMiscContainer();
+      DeallocMiscContainer(ClearMiscContainer());
 
       break;
     }
     case eAtomBase:
     {
       nsAtom* atom = GetAtomValue();
       NS_RELEASE(atom);
 
@@ -257,17 +294,17 @@ nsAttrValue::SetTo(const nsAttrValue& aO
       ResetIfSet();
       mBits = aOther.mBits;
       return;
     }
   }
 
   MiscContainer* otherCont = aOther.GetMiscContainer();
   if (otherCont->IsRefCounted()) {
-    delete ClearMiscContainer();
+    DeallocMiscContainer(ClearMiscContainer());
     NS_ADDREF(otherCont);
     SetPtrValueAndType(otherCont, eOtherBase);
     return;
   }
 
   MiscContainer* cont = EnsureEmptyMiscContainer();
   switch (otherCont->mType) {
     case eInteger:
@@ -1806,17 +1843,17 @@ nsAttrValue::ClearMiscContainer()
 {
   MiscContainer* cont = nullptr;
   if (BaseType() == eOtherBase) {
     cont = GetMiscContainer();
     if (cont->IsRefCounted() && cont->mValue.mRefCount > 1) {
       // This MiscContainer is shared, we need a new one.
       NS_RELEASE(cont);
 
-      cont = new MiscContainer;
+      cont = AllocMiscContainer();
       SetPtrValueAndType(cont, eOtherBase);
     }
     else {
       switch (cont->mType) {
         case eCSSDeclaration:
         {
           MOZ_ASSERT(cont->mValue.mRefCount == 1);
           cont->Release();
@@ -1859,17 +1896,17 @@ nsAttrValue::EnsureEmptyMiscContainer()
 {
   MiscContainer* cont = ClearMiscContainer();
   if (cont) {
     MOZ_ASSERT(BaseType() == eOtherBase);
     ResetMiscAtomOrString();
     cont = GetMiscContainer();
   }
   else {
-    cont = new MiscContainer;
+    cont = AllocMiscContainer();
     SetPtrValueAndType(cont, eOtherBase);
   }
 
   return cont;
 }
 
 bool
 nsAttrValue::EnsureEmptyAtomArray()
--- a/dom/base/nsAttrValue.h
+++ b/dom/base/nsAttrValue.h
@@ -473,17 +473,21 @@ private:
   bool EnsureEmptyAtomArray();
   already_AddRefed<nsStringBuffer>
     GetStringBuffer(const nsAString& aValue) const;
   // Given an enum table and a particular entry in that table, return
   // the actual integer value we should store.
   int32_t EnumTableEntryToValue(const EnumTable* aEnumTable,
                                 const EnumTable* aTableEntry);
 
+  static MiscContainer* AllocMiscContainer();
+  static void DeallocMiscContainer(MiscContainer* aCont);
+
   static nsTArray<const EnumTable*>* sEnumTableArray;
+  static MiscContainer* sMiscContainerCache;
 
   uintptr_t mBits;
 };
 
 inline const nsAttrValue&
 nsAttrValue::operator=(const nsAttrValue& aOther)
 {
   SetTo(aOther);
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -3546,30 +3546,28 @@ nsDOMWindowUtils::AllowScriptsToClose()
   NS_ENSURE_STATE(window);
   nsGlobalWindowOuter::Cast(window)->AllowScriptsToClose();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDOMWindowUtils::GetIsParentWindowMainWidgetVisible(bool* aIsVisible)
 {
+  if (!XRE_IsParentProcess()) {
+    MOZ_CRASH("IsParentWindowMainWidgetVisible is only available in the parent process");
+  }
+
   // this should reflect the "is parent window visible" logic in
   // nsWindowWatcher::OpenWindowInternal()
   nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryReferent(mWindow);
   NS_ENSURE_STATE(window);
 
   nsCOMPtr<nsIWidget> parentWidget;
   nsIDocShell *docShell = window->GetDocShell();
   if (docShell) {
-    if (TabChild *tabChild = TabChild::GetFrom(docShell)) {
-      if (!tabChild->SendIsParentWindowMainWidgetVisible(aIsVisible))
-        return NS_ERROR_FAILURE;
-      return NS_OK;
-    }
-
     nsCOMPtr<nsIDocShellTreeOwner> parentTreeOwner;
     docShell->GetTreeOwner(getter_AddRefs(parentTreeOwner));
     nsCOMPtr<nsIBaseWindow> parentWindow(do_GetInterface(parentTreeOwner));
     if (parentWindow) {
         parentWindow->GetMainWidget(getter_AddRefs(parentWidget));
     }
   }
   if (!parentWidget) {
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -1654,16 +1654,103 @@ nsINode::GetLastElementChild() const
     if (child->IsElement()) {
       return child->AsElement();
     }
   }
 
   return nullptr;
 }
 
+static
+bool MatchAttribute(Element* aElement,
+                    int32_t aNamespaceID,
+                    nsAtom* aAttrName,
+                    void* aData)
+{
+  MOZ_ASSERT(aElement, "Must have content node to work with!");
+  nsString* attrValue = static_cast<nsString*>(aData);
+  if (aNamespaceID != kNameSpaceID_Unknown &&
+      aNamespaceID != kNameSpaceID_Wildcard) {
+    return attrValue->EqualsLiteral("*") ?
+      aElement->HasAttr(aNamespaceID, aAttrName) :
+      aElement->AttrValueIs(aNamespaceID, aAttrName, *attrValue,
+                            eCaseMatters);
+  }
+
+  // Qualified name match. This takes more work.
+  uint32_t count = aElement->GetAttrCount();
+  for (uint32_t i = 0; i < count; ++i) {
+    const nsAttrName* name = aElement->GetAttrNameAt(i);
+    bool nameMatch;
+    if (name->IsAtom()) {
+      nameMatch = name->Atom() == aAttrName;
+    } else if (aNamespaceID == kNameSpaceID_Wildcard) {
+      nameMatch = name->NodeInfo()->Equals(aAttrName);
+    } else {
+      nameMatch = name->NodeInfo()->QualifiedNameEquals(aAttrName);
+    }
+
+    if (nameMatch) {
+      return attrValue->EqualsLiteral("*") ||
+        aElement->AttrValueIs(name->NamespaceID(), name->LocalName(),
+                              *attrValue, eCaseMatters);
+    }
+  }
+
+  return false;
+}
+
+already_AddRefed<nsIHTMLCollection>
+nsINode::GetElementsByAttribute(const nsAString& aAttribute,
+                                const nsAString& aValue)
+{
+  RefPtr<nsAtom> attrAtom(NS_Atomize(aAttribute));
+  nsAutoPtr<nsString> attrValue(new nsString(aValue));
+  RefPtr<nsContentList> list = new nsContentList(this,
+                                          MatchAttribute,
+                                          nsContentUtils::DestroyMatchString,
+                                          attrValue.forget(),
+                                          true,
+                                          attrAtom,
+                                          kNameSpaceID_Unknown);
+
+  return list.forget();
+}
+
+already_AddRefed<nsIHTMLCollection>
+nsINode::GetElementsByAttributeNS(const nsAString& aNamespaceURI,
+                                  const nsAString& aAttribute,
+                                  const nsAString& aValue,
+                                  ErrorResult& aRv)
+{
+  RefPtr<nsAtom> attrAtom(NS_Atomize(aAttribute));
+  nsAutoPtr<nsString> attrValue(new nsString(aValue));
+
+  int32_t nameSpaceId = kNameSpaceID_Wildcard;
+  if (!aNamespaceURI.EqualsLiteral("*")) {
+    nsresult rv =
+      nsContentUtils::NameSpaceManager()->RegisterNameSpace(aNamespaceURI,
+                                                            nameSpaceId);
+    if (NS_FAILED(rv)) {
+      aRv.Throw(rv);
+      return nullptr;
+    }
+  }
+
+  RefPtr<nsContentList> list = new nsContentList(this,
+                                          MatchAttribute,
+                                          nsContentUtils::DestroyMatchString,
+                                          attrValue.forget(),
+                                          true,
+                                          attrAtom,
+                                          nameSpaceId);
+  return list.forget();
+}
+
+
 void
 nsINode::Prepend(const Sequence<OwningNodeOrString>& aNodes,
                  ErrorResult& aRv)
 {
   nsCOMPtr<nsIDocument> doc = OwnerDoc();
   nsCOMPtr<nsINode> node = ConvertNodesOrStringsIntoNode(aNodes, doc, aRv);
   if (aRv.Failed()) {
     return;
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -37,16 +37,17 @@
 
 class nsAttrAndChildArray;
 class nsAttrChildContentList;
 class nsDOMAttributeMap;
 class nsIAnimationObserver;
 class nsIContent;
 class nsIDocument;
 class nsIFrame;
+class nsIHTMLCollection;
 class nsIMutationObserver;
 class nsINode;
 class nsINodeList;
 class nsIPresShell;
 class nsIPrincipal;
 class nsIURI;
 class nsNodeSupportsWeakRefTearoff;
 class nsDOMMutationObserver;
@@ -1854,16 +1855,26 @@ public:
    * Remove this node from its parent, if any.
    */
   void Remove();
 
   // ParentNode methods
   mozilla::dom::Element* GetFirstElementChild() const;
   mozilla::dom::Element* GetLastElementChild() const;
 
+  already_AddRefed<nsIHTMLCollection>
+    GetElementsByAttribute(const nsAString& aAttribute,
+                           const nsAString& aValue);
+  already_AddRefed<nsIHTMLCollection>
+    GetElementsByAttributeNS(const nsAString& aNamespaceURI,
+                             const nsAString& aAttribute,
+                             const nsAString& aValue,
+                             ErrorResult& aRv);
+
+
   MOZ_CAN_RUN_SCRIPT void Prepend(const Sequence<OwningNodeOrString>& aNodes,
                                   ErrorResult& aRv);
   MOZ_CAN_RUN_SCRIPT void Append(const Sequence<OwningNodeOrString>& aNodes,
                                  ErrorResult& aRv);
 
   void GetBoxQuads(const BoxQuadOptions& aOptions,
                    nsTArray<RefPtr<DOMQuad> >& aResult,
                    CallerType aCallerType,
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -354,18 +354,16 @@ parent:
                                 nsCString[] enabledCommands,
                                 nsCString[] disabledCommands);
 
     nested(inside_cpow) sync GetInputContext() returns (IMEState state);
 
     nested(inside_cpow) async SetInputContext(InputContext context,
                                               InputContextAction action);
 
-    sync IsParentWindowMainWidgetVisible() returns (bool visible);
-
     /**
      * Set the native cursor.
      * @param value
      *   The widget cursor to set.
      * @param force
      *   Invalidate any locally cached cursor settings and force an
      *   update.
      */
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -2486,36 +2486,16 @@ mozilla::ipc::IPCResult
 TabParent::RecvSetInputContext(
   const InputContext& aContext,
   const InputContextAction& aAction)
 {
   IMEStateManager::SetInputContextForChildProcess(this, aContext, aAction);
   return IPC_OK();
 }
 
-mozilla::ipc::IPCResult
-TabParent::RecvIsParentWindowMainWidgetVisible(bool* aIsVisible)
-{
-  // XXXbz This looks unused; can we just remove it?
-  nsCOMPtr<nsIContent> frame = do_QueryInterface(mFrameElement);
-  if (!frame)
-    return IPC_OK();
-  nsGlobalWindowOuter* outer =
-    nsGlobalWindowOuter::Cast(frame->OwnerDoc()->GetWindow());
-  if (!outer)
-    return IPC_OK();
-
-  nsCOMPtr<nsIDOMWindowUtils> windowUtils = outer->WindowUtils();
-  nsresult rv = windowUtils->GetIsParentWindowMainWidgetVisible(aIsVisible);
-  if (NS_FAILED(rv)) {
-    return IPC_FAIL_NO_REASON(this);
-  }
-  return IPC_OK();
-}
-
 already_AddRefed<nsIWidget>
 TabParent::GetTopLevelWidget()
 {
   nsCOMPtr<nsIContent> content = do_QueryInterface(mFrameElement);
   if (content) {
     nsIPresShell* shell = content->OwnerDoc()->GetShell();
     if (shell) {
       nsViewManager* vm = shell->GetViewManager();
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -278,18 +278,16 @@ public:
                                                       const gfx::SurfaceFormat& aFormat,
                                                       const uint32_t& aHotspotX,
                                                       const uint32_t& aHotspotY,
                                                       const bool& aForce) override;
 
   virtual mozilla::ipc::IPCResult RecvSetStatus(const uint32_t& aType,
                                                 const nsString& aStatus) override;
 
-  virtual mozilla::ipc::IPCResult RecvIsParentWindowMainWidgetVisible(bool* aIsVisible) override;
-
   virtual mozilla::ipc::IPCResult RecvShowTooltip(const uint32_t& aX,
                                                   const uint32_t& aY,
                                                   const nsString& aTooltip,
                                                   const nsString& aDirection) override;
 
   virtual mozilla::ipc::IPCResult RecvHideTooltip() override;
 
 
--- a/dom/webidl/ParentNode.webidl
+++ b/dom/webidl/ParentNode.webidl
@@ -13,13 +13,20 @@ interface ParentNode {
   readonly attribute HTMLCollection children;
   [Pure]
   readonly attribute Element? firstElementChild;
   [Pure]
   readonly attribute Element? lastElementChild;
   [Pure]
   readonly attribute unsigned long childElementCount;
 
+  [Func="IsChromeOrXBL"]
+  HTMLCollection getElementsByAttribute(DOMString name,
+                                        [TreatNullAs=EmptyString] DOMString value);
+  [Throws, Func="IsChromeOrXBL"]
+  HTMLCollection getElementsByAttributeNS(DOMString? namespaceURI, DOMString name,
+                                          [TreatNullAs=EmptyString] DOMString value);
+
   [CEReactions, Throws, Unscopable]
   void prepend((Node or DOMString)... nodes);
   [CEReactions, Throws, Unscopable]
   void append((Node or DOMString)... nodes);
 };
--- a/dom/webidl/XULDocument.webidl
+++ b/dom/webidl/XULDocument.webidl
@@ -21,22 +21,16 @@ interface XULDocument : Document {
   readonly attribute Node? popupRangeParent;
   [Throws, ChromeOnly]
   readonly attribute long  popupRangeOffset;
 
            attribute Node? tooltipNode;
 
   readonly attribute XULCommandDispatcher? commandDispatcher;
 
-  NodeList getElementsByAttribute(DOMString name,
-                                  [TreatNullAs=EmptyString] DOMString value);
-  [Throws]
-  NodeList getElementsByAttributeNS(DOMString? namespaceURI, DOMString name,
-                                    [TreatNullAs=EmptyString] DOMString value);
-
   [Throws]
   void addBroadcastListenerFor(Element broadcaster, Element observer,
                                DOMString attr);
   void removeBroadcastListenerFor(Element broadcaster, Element observer,
                                   DOMString attr);
 
   [Throws]
   BoxObject? getBoxObjectFor(Element? element);
--- a/dom/webidl/XULElement.webidl
+++ b/dom/webidl/XULElement.webidl
@@ -78,23 +78,15 @@ interface XULElement : Element {
   [Throws]
   void                      focus();
   [Throws]
   void                      blur();
   [NeedsCallerType]
   void                      click();
   void                      doCommand();
 
-  // XXXbz this isn't really a nodelist!  See bug 818548
-  NodeList            getElementsByAttribute(DOMString name,
-                                             DOMString value);
-  // XXXbz this isn't really a nodelist!  See bug 818548
-  [Throws]
-  NodeList            getElementsByAttributeNS(DOMString namespaceURI,
-                                               DOMString name,
-                                               DOMString value);
   [Constant]
   readonly attribute CSSStyleDeclaration style;
 };
 
 XULElement implements GlobalEventHandlers;
 XULElement implements TouchEventHandlers;
 XULElement implements OnErrorEventHandlerForNodes;
--- a/dom/xul/XULDocument.cpp
+++ b/dom/xul/XULDocument.cpp
@@ -946,62 +946,16 @@ XULDocument::ResolveForwardReferences()
     return NS_OK;
 }
 
 //----------------------------------------------------------------------
 //
 // nsIDocument interface
 //
 
-already_AddRefed<nsINodeList>
-XULDocument::GetElementsByAttribute(const nsAString& aAttribute,
-                                    const nsAString& aValue)
-{
-    RefPtr<nsAtom> attrAtom(NS_Atomize(aAttribute));
-    nsAutoPtr<nsString> attrValue(new nsString(aValue));
-    RefPtr<nsContentList> list = new nsContentList(this,
-                                            MatchAttribute,
-                                            nsContentUtils::DestroyMatchString,
-                                            attrValue.forget(),
-                                            true,
-                                            attrAtom,
-                                            kNameSpaceID_Unknown);
-
-    return list.forget();
-}
-
-already_AddRefed<nsINodeList>
-XULDocument::GetElementsByAttributeNS(const nsAString& aNamespaceURI,
-                                      const nsAString& aAttribute,
-                                      const nsAString& aValue,
-                                      ErrorResult& aRv)
-{
-    RefPtr<nsAtom> attrAtom(NS_Atomize(aAttribute));
-    nsAutoPtr<nsString> attrValue(new nsString(aValue));
-
-    int32_t nameSpaceId = kNameSpaceID_Wildcard;
-    if (!aNamespaceURI.EqualsLiteral("*")) {
-      nsresult rv =
-        nsContentUtils::NameSpaceManager()->RegisterNameSpace(aNamespaceURI,
-                                                              nameSpaceId);
-      if (NS_FAILED(rv)) {
-          aRv.Throw(rv);
-          return nullptr;
-      }
-    }
-
-    RefPtr<nsContentList> list = new nsContentList(this,
-                                            MatchAttribute,
-                                            nsContentUtils::DestroyMatchString,
-                                            attrValue.forget(),
-                                            true,
-                                            attrAtom,
-                                            nameSpaceId);
-    return list.forget();
-}
 
 void
 XULDocument::Persist(Element* aElement, int32_t aNameSpaceID,
                      nsAtom* aAttribute)
 {
     // For non-chrome documents, persistance is simply broken
     if (!nsContentUtils::IsSystemPrincipal(NodePrincipal()))
         return;
@@ -1365,57 +1319,16 @@ XULDocument::StartLayout(void)
 
         nsresult rv = shell->Initialize();
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
     return NS_OK;
 }
 
-/* static */
-bool
-XULDocument::MatchAttribute(Element* aElement,
-                            int32_t aNamespaceID,
-                            nsAtom* aAttrName,
-                            void* aData)
-{
-    MOZ_ASSERT(aElement, "Must have content node to work with!");
-    nsString* attrValue = static_cast<nsString*>(aData);
-    if (aNamespaceID != kNameSpaceID_Unknown &&
-        aNamespaceID != kNameSpaceID_Wildcard) {
-        return attrValue->EqualsLiteral("*") ?
-            aElement->HasAttr(aNamespaceID, aAttrName) :
-            aElement->AttrValueIs(aNamespaceID, aAttrName, *attrValue,
-                                  eCaseMatters);
-    }
-
-    // Qualified name match. This takes more work.
-
-    uint32_t count = aElement->GetAttrCount();
-    for (uint32_t i = 0; i < count; ++i) {
-        const nsAttrName* name = aElement->GetAttrNameAt(i);
-        bool nameMatch;
-        if (name->IsAtom()) {
-            nameMatch = name->Atom() == aAttrName;
-        } else if (aNamespaceID == kNameSpaceID_Wildcard) {
-            nameMatch = name->NodeInfo()->Equals(aAttrName);
-        } else {
-            nameMatch = name->NodeInfo()->QualifiedNameEquals(aAttrName);
-        }
-
-        if (nameMatch) {
-            return attrValue->EqualsLiteral("*") ||
-                aElement->AttrValueIs(name->NamespaceID(), name->LocalName(),
-                                      *attrValue, eCaseMatters);
-        }
-    }
-
-    return false;
-}
-
 nsresult
 XULDocument::PrepareToLoadPrototype(nsIURI* aURI, const char* aCommand,
                                     nsIPrincipal* aDocumentPrincipal,
                                     nsIParser** aResult)
 {
     nsresult rv;
 
     // Create a new prototype document.
--- a/dom/xul/XULDocument.h
+++ b/dom/xul/XULDocument.h
@@ -127,45 +127,31 @@ public:
 
     virtual nsIDocument::DocumentTheme GetDocumentLWTheme() override;
     virtual nsIDocument::DocumentTheme ThreadSafeGetDocumentLWTheme() const override;
 
     void ResetDocumentLWTheme() { mDocLWTheme = Doc_Theme_Uninitialized; }
 
     NS_IMETHOD OnScriptCompileComplete(JSScript* aScript, nsresult aStatus) override;
 
-    static bool
-    MatchAttribute(Element* aContent,
-                   int32_t aNameSpaceID,
-                   nsAtom* aAttrName,
-                   void* aData);
-
     NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULDocument, XMLDocument)
 
     void TraceProtos(JSTracer* aTrc);
 
     // WebIDL API
     already_AddRefed<nsINode> GetPopupNode();
     void SetPopupNode(nsINode* aNode);
     nsINode* GetPopupRangeParent(ErrorResult& aRv);
     int32_t GetPopupRangeOffset(ErrorResult& aRv);
     already_AddRefed<nsINode> GetTooltipNode();
     void SetTooltipNode(nsINode* aNode) { /* do nothing */ }
     nsIDOMXULCommandDispatcher* GetCommandDispatcher() const
     {
         return mCommandDispatcher;
     }
-    already_AddRefed<nsINodeList>
-      GetElementsByAttribute(const nsAString& aAttribute,
-                             const nsAString& aValue);
-    already_AddRefed<nsINodeList>
-      GetElementsByAttributeNS(const nsAString& aNamespaceURI,
-                               const nsAString& aAttribute,
-                               const nsAString& aValue,
-                               ErrorResult& aRv);
     void AddBroadcastListenerFor(Element& aBroadcaster, Element& aListener,
                                  const nsAString& aAttr, ErrorResult& aRv);
     void RemoveBroadcastListenerFor(Element& aBroadcaster, Element& aListener,
                                     const nsAString& aAttr);
     using nsDocument::GetBoxObjectFor;
 
 protected:
     virtual ~XULDocument();
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -368,64 +368,16 @@ nsXULElement::Clone(mozilla::dom::NodeIn
     }
 
     element.forget(aResult);
     return rv;
 }
 
 //----------------------------------------------------------------------
 
-already_AddRefed<nsINodeList>
-nsXULElement::GetElementsByAttribute(const nsAString& aAttribute,
-                                     const nsAString& aValue)
-{
-    RefPtr<nsAtom> attrAtom(NS_Atomize(aAttribute));
-    void* attrValue = new nsString(aValue);
-    RefPtr<nsContentList> list =
-        new nsContentList(this,
-                          XULDocument::MatchAttribute,
-                          nsContentUtils::DestroyMatchString,
-                          attrValue,
-                          true,
-                          attrAtom,
-                          kNameSpaceID_Unknown);
-    return list.forget();
-}
-
-already_AddRefed<nsINodeList>
-nsXULElement::GetElementsByAttributeNS(const nsAString& aNamespaceURI,
-                                       const nsAString& aAttribute,
-                                       const nsAString& aValue,
-                                       ErrorResult& rv)
-{
-    RefPtr<nsAtom> attrAtom(NS_Atomize(aAttribute));
-
-    int32_t nameSpaceId = kNameSpaceID_Wildcard;
-    if (!aNamespaceURI.EqualsLiteral("*")) {
-      rv =
-        nsContentUtils::NameSpaceManager()->RegisterNameSpace(aNamespaceURI,
-                                                              nameSpaceId);
-      if (rv.Failed()) {
-          return nullptr;
-      }
-    }
-
-    void* attrValue = new nsString(aValue);
-    RefPtr<nsContentList> list =
-        new nsContentList(this,
-                          XULDocument::MatchAttribute,
-                          nsContentUtils::DestroyMatchString,
-                          attrValue,
-                          true,
-                          attrAtom,
-                          nameSpaceId);
-
-    return list.forget();
-}
-
 EventListenerManager*
 nsXULElement::GetEventListenerManagerForAttr(nsAtom* aAttrName, bool* aDefer)
 {
     // XXXbz sXBL/XBL2 issue: should we instead use GetComposedDoc()
     // here, override BindToTree for those classes and munge event
     // listeners there?
     nsIDocument* doc = OwnerDoc();
 
--- a/dom/xul/nsXULElement.h
+++ b/dom/xul/nsXULElement.h
@@ -612,24 +612,16 @@ public:
         SetXULBoolAttr(nsGkAtoms::allowevents, aAllowEvents);
     }
     nsIControllers* GetControllers(mozilla::ErrorResult& rv);
     // Note: this can only fail if the do_CreateInstance for the boxobject
     // contact fails for some reason.
     already_AddRefed<mozilla::dom::BoxObject> GetBoxObject(mozilla::ErrorResult& rv);
     void Click(mozilla::dom::CallerType aCallerType);
     void DoCommand();
-    already_AddRefed<nsINodeList>
-      GetElementsByAttribute(const nsAString& aAttribute,
-                             const nsAString& aValue);
-    already_AddRefed<nsINodeList>
-      GetElementsByAttributeNS(const nsAString& aNamespaceURI,
-                               const nsAString& aAttribute,
-                               const nsAString& aValue,
-                               mozilla::ErrorResult& rv);
     // Style() inherited from nsStyledElement
 
     nsINode* GetScopeChainParent() const override
     {
         // For XUL, the parent is the parent element, if any
         Element* parent = GetParentElement();
         return parent ? parent : nsStyledElement::GetScopeChainParent();
     }
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -2579,16 +2579,38 @@ gfxPlatform::InitWebRenderConfig()
     if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
       featureWebRenderQualified.Disable(FeatureStatus::Blocked,
                                          "No qualified hardware",
                                          failureId);
     } else if (HasBattery()) {
       featureWebRenderQualified.Disable(FeatureStatus::Blocked,
                                          "Has battery",
                                          NS_LITERAL_CSTRING("FEATURE_FAILURE_WR_HAS_BATTERY"));
+    } else {
+      nsAutoString adapterVendorID;
+      gfxInfo->GetAdapterVendorID(adapterVendorID);
+      if (adapterVendorID != u"0x10de") {
+        featureWebRenderQualified.Disable(FeatureStatus::Blocked,
+                                         "Not Nvidia",
+                                         NS_LITERAL_CSTRING("FEATURE_FAILURE_NOT_NVIDIA"));
+      } else {
+        nsAutoString adapterDeviceID;
+        gfxInfo->GetAdapterDeviceID(adapterDeviceID);
+        nsresult valid;
+        int32_t deviceID = adapterDeviceID.ToInteger(&valid, 16);
+        if (valid != NS_OK) {
+          featureWebRenderQualified.Disable(FeatureStatus::Blocked,
+                                            "Bad device id",
+                                            NS_LITERAL_CSTRING("FEATURE_FAILURE_BAD_DEVICE_ID"));
+        } else if (deviceID < 1000) { // > 1000 or 0x3e8 roughly corresponds to Tesla and newer
+          featureWebRenderQualified.Disable(FeatureStatus::Blocked,
+                                            "Device too old",
+                                            NS_LITERAL_CSTRING("FEATURE_FAILURE_DEVICE_TOO_OLD"));
+        }
+      }
     }
   } else {
     featureWebRenderQualified.Disable(FeatureStatus::Blocked,
                                        "gfxInfo is broken",
                                        NS_LITERAL_CSTRING("FEATURE_FAILURE_WR_NO_GFX_INFO"));
   }
 
   FeatureState& featureWebRender = gfxConfig::GetFeature(Feature::WEBRENDER);
--- a/ipc/ipdl/sync-messages.ini
+++ b/ipc/ipdl/sync-messages.ini
@@ -825,19 +825,16 @@ description =
 [PBrowser::NotifyIMEMouseButtonEvent]
 description =
 [PBrowser::RequestIMEToCommitComposition]
 description =
 [PBrowser::StartPluginIME]
 description =
 [PBrowser::GetInputContext]
 description =
-[PBrowser::IsParentWindowMainWidgetVisible]
-description =
-description =
 [PBrowser::RequestNativeKeyBindings]
 description =
 [PBrowser::DispatchWheelEvent]
 description =
 [PBrowser::DispatchMouseEvent]
 description =
 [PBrowser::DispatchKeyboardEvent]
 description =
--- a/layout/forms/nsFieldSetFrame.cpp
+++ b/layout/forms/nsFieldSetFrame.cpp
@@ -311,28 +311,32 @@ nsFieldSetFrame::PaintBorder(
 }
 
 nscoord
 nsFieldSetFrame::GetIntrinsicISize(gfxContext* aRenderingContext,
                                    nsLayoutUtils::IntrinsicISizeType aType)
 {
   nscoord legendWidth = 0;
   nscoord contentWidth = 0;
-  if (nsIFrame* legend = GetLegend()) {
-    legendWidth =
-      nsLayoutUtils::IntrinsicForContainer(aRenderingContext, legend, aType);
-  }
+  if (!StyleDisplay()->IsContainSize()) {
+    // Both inner and legend are children, and if the fieldset is
+    // size-contained they should not contribute to the intrinsic size.
+    if (nsIFrame* legend = GetLegend()) {
+      legendWidth =
+        nsLayoutUtils::IntrinsicForContainer(aRenderingContext, legend, aType);
+    }
 
-  if (nsIFrame* inner = GetInner()) {
-    // Ignore padding on the inner, since the padding will be applied to the
-    // outer instead, and the padding computed for the inner is wrong
-    // for percentage padding.
-    contentWidth =
-      nsLayoutUtils::IntrinsicForContainer(aRenderingContext, inner, aType,
-                                           nsLayoutUtils::IGNORE_PADDING);
+    if (nsIFrame* inner = GetInner()) {
+      // Ignore padding on the inner, since the padding will be applied to the
+      // outer instead, and the padding computed for the inner is wrong
+      // for percentage padding.
+      contentWidth =
+        nsLayoutUtils::IntrinsicForContainer(aRenderingContext, inner, aType,
+                                             nsLayoutUtils::IGNORE_PADDING);
+    }
   }
 
   return std::max(legendWidth, contentWidth);
 }
 
 
 nscoord
 nsFieldSetFrame::GetMinISize(gfxContext* aRenderingContext)
@@ -583,16 +587,31 @@ nsFieldSetFrame::Reflow(nsPresContext*  
     nsContainerFrame::PositionFrameView(legend);
     nsContainerFrame::PositionChildViews(legend);
   }
 
   // Return our size and our result.
   LogicalSize finalSize(wm, contentRect.ISize(wm) + border.IStartEnd(wm),
                         mLegendSpace + border.BStartEnd(wm) +
                         (inner ? inner->BSize(wm) : 0));
+  if (aReflowInput.mStyleDisplay->IsContainSize()) {
+    // If we're size-contained, then we must set finalSize to be what
+    // it'd be if we had no children (i.e. if we had no legend and if
+    // 'inner' were empty).  Note: normally the fieldset's own padding
+    // (which we still must honor) would be accounted for as part of
+    // inner's size (see kidReflowInput.Init() call above).  So: since
+    // we're disregarding sizing information from 'inner', we need to
+    // account for that padding ourselves here.
+    nscoord contentBoxBSize =
+      aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE
+      ? aReflowInput.ApplyMinMaxBSize(0)
+      : aReflowInput.ComputedBSize();
+    finalSize.BSize(wm) = contentBoxBSize +
+      aReflowInput.ComputedLogicalBorderPadding().BStartEnd(wm);
+  }
   aDesiredSize.SetSize(wm, finalSize);
   aDesiredSize.SetOverflowAreasToDesiredBounds();
 
   if (legend) {
     ConsiderChildOverflow(aDesiredSize.mOverflowAreas, legend);
   }
   if (inner) {
     ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inner);
@@ -664,31 +683,41 @@ nsFieldSetFrame::GetLogicalBaseline(Writ
                                           AlignmentContext::eInline);
   }
 }
 
 bool
 nsFieldSetFrame::GetVerticalAlignBaseline(WritingMode aWM,
                                           nscoord* aBaseline) const
 {
+  if (StyleDisplay()->IsContainSize()) {
+    // If we are size-contained, our child 'inner' should not
+    // affect how we calculate our baseline.
+    return false;
+  }
   nsIFrame* inner = GetInner();
   MOZ_ASSERT(!inner->GetWritingMode().IsOrthogonalTo(aWM));
   if (!inner->GetVerticalAlignBaseline(aWM, aBaseline)) {
     return false;
   }
   nscoord innerBStart = inner->BStart(aWM, GetSize());
   *aBaseline += innerBStart;
   return true;
 }
 
 bool
 nsFieldSetFrame::GetNaturalBaselineBOffset(WritingMode          aWM,
                                            BaselineSharingGroup aBaselineGroup,
                                            nscoord*             aBaseline) const
 {
+  if (StyleDisplay()->IsContainSize()) {
+    // If we are size-contained, our child 'inner' should not
+    // affect how we calculate our baseline.
+    return false;
+  }
   nsIFrame* inner = GetInner();
   MOZ_ASSERT(!inner->GetWritingMode().IsOrthogonalTo(aWM));
   if (!inner->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aBaseline)) {
     return false;
   }
   nscoord innerBStart = inner->BStart(aWM, GetSize());
   if (aBaselineGroup == BaselineSharingGroup::eFirst) {
     *aBaseline += innerBStart;
new file mode 100644
--- /dev/null
+++ b/layout/reftests/w3c-css/submitted/contain/contain-size-fieldset-001-ref.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>CSS Reftest Reference</title>
+  <link rel="author" title="Morgan Rae Reschenberg" href="mailto:mreschenberg@berkeley.edu">
+  <style>
+  .basic {
+    visibility: hidden;
+    border: none;
+  }
+  .container {
+    border: 10px solid green;
+    display: inline-block;
+  }
+  .height {
+    height: 30px;
+  }
+  .width {
+    width: 30px;
+  }
+  </style>
+</head>
+<body>
+  <div class="container"><fieldset class="basic"></fieldset></div>
+  <br>
+
+  <div class="container"><fieldset class="basic height"></fieldset></div>
+  <br>
+
+  <div class="container"><fieldset class="basic height"></fieldset></div>
+  <br>
+
+  <div class="container"><fieldset class="basic width"></fieldset></div>
+  <br>
+
+  <div class="container"><fieldset class="basic width"></fieldset></div>
+  <br>
+
+  <fieldset class="height"><legend>legend</legend></fieldset>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/w3c-css/submitted/contain/contain-size-fieldset-001.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>CSS Test: 'contain: size' on fieldset elements should cause them to be sized as if they had no contents.</title>
+  <link rel="author" title="Morgan Rae Reschenberg" href="mailto:mreschenberg@berkeley.edu">
+  <link rel="help" href="https://drafts.csswg.org/css-contain/#containment-size">
+  <link rel="match" href="contain-size-fieldset-001-ref.html">
+  <style>
+  .contain {
+    contain: size;
+    visibility: hidden;
+    border: none;
+    color: transparent;
+  }
+  .container {
+    border: 10px solid green;
+    display: inline-block;
+  }
+  .innerContents {
+    height: 50px;
+    width: 50px;
+  }
+  .minHeight {
+    min-height: 30px;
+  }
+  .height {
+    height: 30px;
+  }
+  .minWidth {
+    min-width: 30px;
+  }
+  .width {
+    width: 30px;
+  }
+  </style>
+</head>
+<body>
+  <!--Note: The following .container class is used to help test if size-contained
+  fieldsets and non-contained fieldsets have the same size. Normally, we'd test
+  that a fieldset with children and size-containment is drawn identically to a
+  fieldset without containment or children. However, when we have a legend as
+  a child, border placement and padding of the fieldset are changed.
+  To check the dimensions between the ref-case and test-case without
+  failing because of the border/padding differences, we make the fieldset
+  {visibility:hidden; border:none;} and add a .container wrapper div.-->
+
+  <!--CSS Test: A size-contained fieldset element with no specified size should size itself as if it had no contents.-->
+  <div class="container">
+  <fieldset class="contain">
+    <legend>legend</legend>
+    <div class="innerContents">inner</div>
+  </fieldset>
+  </div>
+  <br>
+
+  <!--CSS Test: A size-contained fieldset element with specified min-height should size itself as if it had no contents.-->
+  <div class="container">
+  <fieldset class="contain minHeight">
+    <legend>legend</legend>
+    <div class="innerContents">inner</div>
+  </fieldset>
+  </div>
+  <br>
+
+  <!--CSS Test: A size-contained fieldset element with specified height should size itself as if it had no contents.-->
+  <div class="container">
+  <fieldset class="contain height">
+    <legend>legend</legend>
+    <div class="innerContents">inner</div>
+  </fieldset>
+  </div>
+  <br>
+
+  <!--CSS Test: A size-contained fieldset element with specified min-width should size itself as if it had no contents.-->
+  <div class="container">
+  <fieldset class="contain minWidth">
+    <legend>legend</legend>
+    <div class="innerContents">inner</div>
+  </fieldset>
+  </div>
+  <br>
+
+  <!--CSS Test: A size-contained fieldset element with specified width should size itself as if it had no contents.-->
+  <div class="container">
+  <fieldset class="contain width">
+    <legend>legend</legend>
+    <div class="innerContents">inner</div>
+  </fieldset>
+  </div>
+  <br>
+
+  <!--CSS Test: A size contained fieldset element with a legend should draw its legend and border in the same way as a non-contained fieldset element-->
+  <fieldset class="height" style="contain:size;">
+    <legend>legend</legend>
+  </fieldset>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/w3c-css/submitted/contain/contain-size-fieldset-002-ref.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>CSS Reftest Reference</title>
+  <link rel="author" title="Morgan Rae Reschenberg" href="mailto:mreschenberg@berkeley.edu">
+  <style>
+  .basic {
+    border: none;
+    visibility: hidden;
+  }
+  .container {
+    border: 10px solid green;
+    display: inline-block;
+  }
+  .floatLBasic-ref {
+    float: left;
+  }
+  .floatLWidth-ref {
+    float: left;
+    width: 30px;
+  }
+  .flexBaselineCheck {
+    display: flex;
+    align-items: baseline;
+  }
+  </style>
+</head>
+<body>
+  <div class="flexBaselineCheck">
+  outside before<fieldset class="basic"></fieldset>outside after
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/w3c-css/submitted/contain/contain-size-fieldset-002.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>CSS Test: 'contain: size' on fieldset elements should cause them to be baseline-aligned as if they had no contents.</title>
+  <link rel="author" title="Morgan Rae Reschenberg" href="mailto:mreschenberg@berkeley.edu">
+  <link rel="help" href="https://drafts.csswg.org/css-contain/#containment-size">
+  <link rel="match" href="contain-size-fieldset-002-ref.html">
+  <style>
+  .contain {
+    contain: size;
+    border: none;
+    color: transparent;
+    visibility: hidden;
+  }
+  .innerContents {
+    height: 50px;
+    width: 50px;
+  }
+  .flexBaselineCheck {
+    display: flex;
+    align-items: baseline;
+  }
+  </style>
+</head>
+<body>
+  <!--CSS Test: A size-contained fieldset element should perform baseline alignment as if the container were empty.-->
+  <div class="flexBaselineCheck">
+  outside before<fieldset class="contain">
+    <legend>legend</legend>
+    <div class="innerContents">inner</div>
+  </fieldset>outside after
+  </div>
+</body>
+</html>
--- a/layout/reftests/w3c-css/submitted/contain/reftest.list
+++ b/layout/reftests/w3c-css/submitted/contain/reftest.list
@@ -18,10 +18,12 @@ pref(layout.css.overflow-clip-box.enable
 == contain-paint-stacking-context-001a.html contain-paint-stacking-context-001-ref.html
 == contain-paint-stacking-context-001b.html contain-paint-stacking-context-001-ref.html
 == contain-size-button-001.html contain-size-button-001-ref.html
 == contain-size-block-001.html contain-size-block-001-ref.html
 == contain-size-inline-block-001.html contain-size-inline-block-001-ref.html
 == contain-size-flex-001.html contain-size-flex-001-ref.html
 fuzzy-if(webrender&&winWidget,3,2) == contain-size-inline-flex-001.html contain-size-inline-flex-001-ref.html # bug 1474093
 == contain-size-multicol-001.html contain-size-multicol-001-ref.html
+== contain-size-fieldset-001.html contain-size-fieldset-001-ref.html
+== contain-size-fieldset-002.html contain-size-fieldset-002-ref.html
 == contain-size-multicol-002.html contain-size-multicol-002-ref.html
 == contain-size-multicol-003.html contain-size-multicol-003-ref.html
--- a/media/webrtc/signaling/src/peerconnection/TransceiverImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/TransceiverImpl.cpp
@@ -236,17 +236,17 @@ TransceiverImpl::UpdateConduit()
 
   if(!mConduit->SetLocalSSRCs(mJsepTransceiver->mSendTrack.GetSsrcs())) {
     MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: " << __FUNCTION__ <<
                         " SetLocalSSRCs failed");
     return NS_ERROR_FAILURE;
   }
 
   mConduit->SetLocalCNAME(mJsepTransceiver->mSendTrack.GetCNAME().c_str());
-  mConduit->SetLocalMID(mJsepTransceiver->mTransport->mTransportId);
+  mConduit->SetLocalMID(mMid);
 
   nsresult rv;
 
   if (IsVideo()) {
     rv = UpdateVideoConduit();
   } else {
     rv = UpdateAudioConduit();
   }
--- a/media/webrtc/signaling/src/sdp/RsdparsaSdpAttributeList.cpp
+++ b/media/webrtc/signaling/src/sdp/RsdparsaSdpAttributeList.cpp
@@ -429,16 +429,17 @@ RsdparsaSdpAttributeList::LoadAttribute(
       case SdpAttribute::kFmtpAttribute:
         LoadFmtp(attributeList);
         return;
       case SdpAttribute::kPtimeAttribute:
         LoadPtime(attributeList);
         return;
       case SdpAttribute::kIceLiteAttribute:
       case SdpAttribute::kRtcpMuxAttribute:
+      case SdpAttribute::kRtcpRsizeAttribute:
       case SdpAttribute::kBundleOnlyAttribute:
       case SdpAttribute::kEndOfCandidatesAttribute:
         LoadFlags(attributeList);
         return;
       case SdpAttribute::kMaxMessageSizeAttribute:
         LoadMaxMessageSize(attributeList);
         return ;
       case SdpAttribute::kMidAttribute:
@@ -484,25 +485,21 @@ RsdparsaSdpAttributeList::LoadAttribute(
         LoadSimulcast(attributeList);
         return;
       case SdpAttribute::kMaxptimeAttribute:
         LoadMaxPtime(attributeList);
         return;
       case SdpAttribute::kCandidateAttribute:
         LoadCandidate(attributeList);
         return;
-
-
-      case SdpAttribute::kLabelAttribute:
       case SdpAttribute::kSsrcGroupAttribute:
-      case SdpAttribute::kRtcpRsizeAttribute:
       case SdpAttribute::kConnectionAttribute:
       case SdpAttribute::kIceMismatchAttribute:
-        // TODO: Not implemented, or not applicable.
-        // Sort this out in Bug 1437165.
+      case SdpAttribute::kLabelAttribute:
+        // These attributes are unused
         return;
     }
   }
 }
 
 void
 RsdparsaSdpAttributeList::LoadAll(RustAttributeList *attributeList)
 {
@@ -824,16 +821,19 @@ RsdparsaSdpAttributeList::LoadFlags(Rust
 {
   RustSdpAttributeFlags flags = sdp_get_attribute_flags(attributeList);
   if (flags.iceLite) {
     SetAttribute(new SdpFlagAttribute(SdpAttribute::kIceLiteAttribute));
   }
   if (flags.rtcpMux) {
     SetAttribute(new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
   }
+  if (flags.rtcpRsize) {
+    SetAttribute(new SdpFlagAttribute(SdpAttribute::kRtcpRsizeAttribute));
+  }
   if (flags.bundleOnly) {
     SetAttribute(new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute));
   }
   if (flags.endOfCandidates) {
     SetAttribute(new SdpFlagAttribute(SdpAttribute::kEndOfCandidatesAttribute));
   }
 }
 
--- a/media/webrtc/signaling/src/sdp/RsdparsaSdpInc.h
+++ b/media/webrtc/signaling/src/sdp/RsdparsaSdpInc.h
@@ -215,16 +215,17 @@ struct RustSdpAttributeFmtp {
   uint8_t payloadType;
   StringView codecName;
   RustSdpAttributeFmtpParameters parameters;
 };
 
 struct RustSdpAttributeFlags {
   bool iceLite;
   bool rtcpMux;
+  bool rtcpRsize;
   bool bundleOnly;
   bool endOfCandidates;
 };
 
 struct RustSdpAttributeMsid {
   StringView id;
   StringView appdata;
 };
--- a/media/webrtc/signaling/src/sdp/rsdparsa_capi/src/attribute.rs
+++ b/media/webrtc/signaling/src/sdp/rsdparsa_capi/src/attribute.rs
@@ -509,34 +509,38 @@ pub unsafe extern "C" fn sdp_get_sctp_po
     -1
 }
 
 #[repr(C)]
 #[derive(Clone, Copy)]
 pub struct RustSdpAttributeFlags {
     pub ice_lite: bool,
     pub rtcp_mux: bool,
+    pub rtcp_rsize: bool,
     pub bundle_only: bool,
-    pub end_of_candidates: bool
+    pub end_of_candidates: bool,
 }
 
 
 #[no_mangle]
 pub unsafe extern "C" fn sdp_get_attribute_flags(attributes: *const Vec<SdpAttribute>) -> RustSdpAttributeFlags {
     let mut ret = RustSdpAttributeFlags {
         ice_lite: false,
         rtcp_mux: false,
+        rtcp_rsize: false,
         bundle_only: false,
         end_of_candidates: false
     };
     for attribute in (*attributes).iter() {
         if let SdpAttribute::IceLite = *attribute {
             ret.ice_lite = true;
         } else if let SdpAttribute::RtcpMux = *attribute {
             ret.rtcp_mux = true;
+        } else if let SdpAttribute::RtcpRsize = *attribute {
+            ret.rtcp_rsize = true;
         } else if let SdpAttribute::BundleOnly = *attribute {
             ret.bundle_only = true;
         } else if let SdpAttribute::EndOfCandidates = *attribute {
             ret.end_of_candidates = true;
         }
     }
     ret
 }
--- a/mobile/android/modules/LightweightThemeConsumer.jsm
+++ b/mobile/android/modules/LightweightThemeConsumer.jsm
@@ -6,41 +6,48 @@ var EXPORTED_SYMBOLS = ["LightweightThem
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/LightweightThemeManager.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "EventDispatcher",
                                "resource://gre/modules/Messaging.jsm");
 
+const DEFAULT_THEME_ID = "default-theme@mozilla.org";
+
 function LightweightThemeConsumer(aDocument) {
   this._doc = aDocument;
   Services.obs.addObserver(this, "lightweight-theme-styling-update");
   Services.obs.addObserver(this, "lightweight-theme-apply");
 
   this._update(LightweightThemeManager.currentThemeForDisplay);
 }
 
 LightweightThemeConsumer.prototype = {
   observe: function(aSubject, aTopic, aData) {
-    if (aTopic == "lightweight-theme-styling-update")
-      this._update(JSON.parse(aData));
-    else if (aTopic == "lightweight-theme-apply")
+    if (aTopic == "lightweight-theme-styling-update") {
+      let parsedData = JSON.parse(aData);
+      if (!parsedData) {
+        parsedData = { theme: null };
+      }
+      this._update(parsedData.theme);
+    } else if (aTopic == "lightweight-theme-apply") {
       this._update(LightweightThemeManager.currentThemeForDisplay);
+    }
   },
 
   destroy: function() {
     Services.obs.removeObserver(this, "lightweight-theme-styling-update");
     Services.obs.removeObserver(this, "lightweight-theme-apply");
     this._doc = null;
   },
 
   _update: function(aData) {
-    if (!aData)
-      aData = { headerURL: "", footerURL: "", textcolor: "", accentcolor: "" };
-
-    let active = !!aData.headerURL;
+    let active = aData && aData.id !== DEFAULT_THEME_ID;
+    if (!aData) {
+      aData = {};
+    }
 
     let msg = active ? { type: "LightweightTheme:Update", data: aData } :
                        { type: "LightweightTheme:Disable" };
     EventDispatcher.instance.sendRequest(msg);
   }
 };
--- a/other-licenses/ia2/AccessibleRole.idl
+++ b/other-licenses/ia2/AccessibleRole.idl
@@ -1,132 +1,132 @@
 /*************************************************************************
  *
  *  File Name (AccessibleRole.idl)
- * 
- *  IAccessible2 IDL Specification 
- * 
- *  Copyright (c) 2007, 2013 Linux Foundation 
- *  Copyright (c) 2006 IBM Corporation 
- *  Copyright (c) 2000, 2006 Sun Microsystems, Inc. 
- *  All rights reserved. 
- *   
- *   
- *  Redistribution and use in source and binary forms, with or without 
- *  modification, are permitted provided that the following conditions 
- *  are met: 
- *   
- *   1. Redistributions of source code must retain the above copyright 
- *      notice, this list of conditions and the following disclaimer. 
- *   
- *   2. Redistributions in binary form must reproduce the above 
- *      copyright notice, this list of conditions and the following 
- *      disclaimer in the documentation and/or other materials 
- *      provided with the distribution. 
+ *
+ *  IAccessible2 IDL Specification
+ *
+ *  Copyright (c) 2007-2018 Linux Foundation
+ *  Copyright (c) 2006 IBM Corporation
+ *  Copyright (c) 2000, 2006 Sun Microsystems, Inc.
+ *  All rights reserved.
+ *
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions
+ *  are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer in the documentation and/or other materials
+ *      provided with the distribution.
  *
- *   3. Neither the name of the Linux Foundation nor the names of its 
- *      contributors may be used to endorse or promote products 
- *      derived from this software without specific prior written 
- *      permission. 
- *   
- *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
- *  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
- *  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
- *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
- *  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
- *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
- *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
- *  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
- *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
- *  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
- *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
- *  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
- *  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
- *   
- *  This BSD License conforms to the Open Source Initiative "Simplified 
- *  BSD License" as published at: 
- *  http://www.opensource.org/licenses/bsd-license.php 
- *   
- *  IAccessible2 is a trademark of the Linux Foundation. The IAccessible2 
- *  mark may be used in accordance with the Linux Foundation Trademark 
- *  Policy to indicate compliance with the IAccessible2 specification. 
- * 
- ************************************************************************/ 
+ *   3. Neither the name of the Linux Foundation nor the names of its
+ *      contributors may be used to endorse or promote products
+ *      derived from this software without specific prior written
+ *      permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ *  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ *  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ *  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ *  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ *  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ *  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ *  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ *  This BSD License conforms to the Open Source Initiative "Simplified
+ *  BSD License" as published at:
+ *  http://www.opensource.org/licenses/bsd-license.php
+ *
+ *  IAccessible2 is a trademark of the Linux Foundation. The IAccessible2
+ *  mark may be used in accordance with the Linux Foundation Trademark
+ *  Policy to indicate compliance with the IAccessible2 specification.
+ *
+ ************************************************************************/
 
 import "objidl.idl";
 
 /** Collection of roles
 
   This enumerator defines an extended set of accessible roles of objects implementing
   the %IAccessible2 interface. These roles are in addition to the MSAA roles obtained
-  through the MSAA get_accRole method.  Examples are 'footnote', 'heading', and 
+  through the MSAA get_accRole method.  Examples are 'footnote', 'heading', and
   'label'. You obtain an object's %IAccessible2 roles by calling IAccessible2::role.
 */
 enum IA2Role {
 
-  /** Unknown role. The object contains some Accessible information, but its 
+  /** Unknown role. The object contains some Accessible information, but its
    role is not known.
   */
   IA2_ROLE_UNKNOWN = 0,
 
   /** An object that can be drawn into and to manage events from the objects
    drawn into it.  Also refer to ::IA2_ROLE_FRAME,
-   ::IA2_ROLE_GLASS_PANE, and ::IA2_ROLE_LAYERED_PANE. 
+   ::IA2_ROLE_GLASS_PANE, and ::IA2_ROLE_LAYERED_PANE.
   */
   IA2_ROLE_CANVAS = 0x401,
 
   /// A caption describing another object.
   IA2_ROLE_CAPTION,
 
   /// Used for check buttons that are menu items.
   IA2_ROLE_CHECK_MENU_ITEM,
 
   /// A specialized dialog that lets the user choose a color.
   IA2_ROLE_COLOR_CHOOSER,
 
   /// A date editor.
   IA2_ROLE_DATE_EDITOR,
 
-  /** An iconified internal frame in an ::IA2_ROLE_DESKTOP_PANE. 
+  /** An iconified internal frame in an ::IA2_ROLE_DESKTOP_PANE.
    Also refer to ::IA2_ROLE_INTERNAL_FRAME.
   */
   IA2_ROLE_DESKTOP_ICON,
 
-  /** A desktop pane. A pane that supports internal frames and iconified 
+  /** A desktop pane. A pane that supports internal frames and iconified
    versions of those internal frames.  Also refer to ::IA2_ROLE_INTERNAL_FRAME.
   */
   IA2_ROLE_DESKTOP_PANE,
 
-  /** A directory pane. A pane that allows the user to navigate through 
-   and select the contents of a directory. May be used by a file chooser.   
+  /** A directory pane. A pane that allows the user to navigate through
+   and select the contents of a directory. May be used by a file chooser.
    Also refer to ::IA2_ROLE_FILE_CHOOSER.
   */
   IA2_ROLE_DIRECTORY_PANE,
 
   /** An editable text object in a toolbar. <b>Deprecated.</b>
    The edit bar role was meant for a text area in a tool bar. However, to detect
    a text area in a tool bar the AT can query the parent.
   */
   IA2_ROLE_EDITBAR,
 
   /// Embedded (OLE) object.
   IA2_ROLE_EMBEDDED_OBJECT,
 
   /// Text that is used as an endnote (footnote at the end of a chapter or section).
   IA2_ROLE_ENDNOTE,
 
-  /** A file chooser. A specialized dialog that displays the files in the 
-   directory and lets the user select a file, browse a different directory, 
-   or specify a filename. May use the directory pane to show the contents of 
-   a directory.  
+  /** A file chooser. A specialized dialog that displays the files in the
+   directory and lets the user select a file, browse a different directory,
+   or specify a filename. May use the directory pane to show the contents of
+   a directory.
    Also refer to ::IA2_ROLE_DIRECTORY_PANE.
   */
   IA2_ROLE_FILE_CHOOSER,
 
-  /** A font chooser. A font chooser is a component that lets the user pick 
+  /** A font chooser. A font chooser is a component that lets the user pick
    various attributes for fonts.
   */
   IA2_ROLE_FONT_CHOOSER,
 
   /** Footer of a document page.
    Also refer to ::IA2_ROLE_HEADER.
   */
   IA2_ROLE_FOOTER,
@@ -134,17 +134,17 @@ enum IA2Role {
   /// Text that is used as a footnote.  Also refer to ::IA2_ROLE_ENDNOTE.
   IA2_ROLE_FOOTNOTE,
 
   /** A container of form controls.  An example of the use of this role is to
    represent an HTML FORM tag.
   */
   IA2_ROLE_FORM,
 
-  /** Frame role. A top level window with a title bar, border, menu bar, etc.  
+  /** Frame role. A top level window with a title bar, border, menu bar, etc.
    It is often used as the primary window for an application.  Also refer to
    ::IA2_ROLE_CANVAS and the MSAA roles of dialog and window.
   */
   IA2_ROLE_FRAME,
 
   /** A glass pane. A pane that is guaranteed to be painted on top of all panes
    beneath it.  Also refer to ::IA2_ROLE_CANVAS, ::IA2_ROLE_INTERNAL_FRAME, and
    ::IA2_ROLE_ROOT_PANE.
@@ -157,40 +157,40 @@ enum IA2Role {
   IA2_ROLE_HEADER,
 
   /// Heading.  Use the IAccessible2::attributes level attribute to determine the heading level.
   IA2_ROLE_HEADING,
 
   /// A small fixed size picture, typically used to decorate components.
   IA2_ROLE_ICON,
 
-  /** An image map object.  Usually a graphic with multiple hotspots, where 
+  /** An image map object.  Usually a graphic with multiple hotspots, where
    each hotspot can be activated resulting in the loading of another document
    or section of a document.
   */
   IA2_ROLE_IMAGE_MAP,
 
   /** An object which is used to allow input of characters not found on a keyboard,
    such as the input of Chinese characters on a Western keyboard.
   */
   IA2_ROLE_INPUT_METHOD_WINDOW,
 
-  /** An internal frame. A frame-like object that is clipped by a desktop pane.  
-   The desktop pane, internal frame, and desktop icon objects are often used to 
+  /** An internal frame. A frame-like object that is clipped by a desktop pane.
+   The desktop pane, internal frame, and desktop icon objects are often used to
    create multiple document interfaces within an application.
    Also refer to ::IA2_ROLE_DESKTOP_ICON, ::IA2_ROLE_DESKTOP_PANE, and ::IA2_ROLE_FRAME.
   */
   IA2_ROLE_INTERNAL_FRAME,
 
   /// An object used to present an icon or short string in an interface.
   IA2_ROLE_LABEL,
 
-  /** A layered pane. A specialized pane that allows its children to be drawn 
-   in layers, providing a form of stacking order. This is usually the pane that 
-   holds the menu bar as  well as the pane that contains most of the visual 
+  /** A layered pane. A specialized pane that allows its children to be drawn
+   in layers, providing a form of stacking order. This is usually the pane that
+   holds the menu bar as  well as the pane that contains most of the visual
    components in a window.
    Also refer to ::IA2_ROLE_CANVAS, ::IA2_ROLE_GLASS_PANE, and ::IA2_ROLE_ROOT_PANE.
   */
   IA2_ROLE_LAYERED_PANE,
 
   /** A section whose content is parenthetic or ancillary to the main content
    of the resource.
   */
@@ -214,88 +214,106 @@ enum IA2Role {
   */
   IA2_ROLE_RADIO_MENU_ITEM,
 
   /** An object which is redundant with another object in the accessible hierarchy.
    ATs typically ignore objects with this role.
   */
   IA2_ROLE_REDUNDANT_OBJECT,
 
-  /** A root pane. A specialized pane that has a glass pane and a layered pane 
+  /** A root pane. A specialized pane that has a glass pane and a layered pane
    as its children.
    Also refer to ::IA2_ROLE_GLASS_PANE and ::IA2_ROLE_LAYERED_PANE
   */
   IA2_ROLE_ROOT_PANE,
 
   /** A ruler such as those used in word processors.
   */
   IA2_ROLE_RULER,
 
-  /** A scroll pane. An object that allows a user to incrementally view a large 
+  /** A scroll pane. An object that allows a user to incrementally view a large
    amount of information.  Its children can include scroll bars and a viewport.
    Also refer to ::IA2_ROLE_VIEW_PORT and MSAA's scroll bar role.
   */
   IA2_ROLE_SCROLL_PANE,
 
   /** A container of document content.  An example of the use of this role is to
-   represent an HTML DIV tag.  A section may be used as a region.  A region is a 
-   group of elements that together form a perceivable unit.  A region does not 
-   necessarily follow the logical structure of the content, but follows the 
-   perceivable structure of the page.  A region may have an attribute in the set 
-   of IAccessible2::attributes which indicates that it is "live".  A live region 
-   is content that is likely to change in response to a timed change, a user 
+   represent an HTML DIV tag.  A section may be used as a region.  A region is a
+   group of elements that together form a perceivable unit.  A region does not
+   necessarily follow the logical structure of the content, but follows the
+   perceivable structure of the page.  A region may have an attribute in the set
+   of IAccessible2::attributes which indicates that it is "live".  A live region
+   is content that is likely to change in response to a timed change, a user
    event, or some other programmed logic or event.
   */
   IA2_ROLE_SECTION,
 
   /// Object with graphical representation used to represent content on draw pages.
   IA2_ROLE_SHAPE,
 
-  /** A split pane. A specialized panel that presents two other panels at the 
-   same time. Between the two panels is a divider the user can manipulate to make 
+  /** A split pane. A specialized panel that presents two other panels at the
+   same time. Between the two panels is a divider the user can manipulate to make
    one panel larger and the other panel smaller.
   */
   IA2_ROLE_SPLIT_PANE,
 
-  /** An object that forms part of a menu system but which can be "undocked" 
+  /** An object that forms part of a menu system but which can be "undocked"
    from or "torn off" the menu system to exist as a separate window.
   */
   IA2_ROLE_TEAR_OFF_MENU,
 
   /// An object used as a terminal emulator.
   IA2_ROLE_TERMINAL,
 
   /// Collection of objects that constitute a logical text entity.
   IA2_ROLE_TEXT_FRAME,
 
-  /** A toggle button. A specialized push button that can be checked or unchecked, 
+  /** A toggle button. A specialized push button that can be checked or unchecked,
    but does not provide a separate indicator for the current state.
    Also refer to MSAA's roles of push button, check box, and radio button.
-   <BR><B>Note:</B> IA2_ROLE_TOGGLE_BUTTON should not be used.  Instead, use MSAA's 
+   <BR><B>Note:</B> IA2_ROLE_TOGGLE_BUTTON should not be used.  Instead, use MSAA's
    ROLE_SYSTEM_PUSHBUTTON and STATE_SYSTEM_PRESSED.
   */
   IA2_ROLE_TOGGLE_BUTTON,
 
-  /** A viewport. An object usually used in a scroll pane. It represents the 
-   portion of the entire data that the user can see. As the user manipulates 
+  /** A viewport. An object usually used in a scroll pane. It represents the
+   portion of the entire data that the user can see. As the user manipulates
    the scroll bars, the contents of the viewport can change.
    Also refer to ::IA2_ROLE_SCROLL_PANE.
   */
   IA2_ROLE_VIEW_PORT,
 
   /** An object containing content which is complementary to the main content of
    a document, but remains meaningful when separated from the main content.  There
    are various types of content that would appropriately have this role.  For example,
    in the case where content is delivered via a web portal to a web browser, this may
    include but not be limited to show times, current weather, related articles, or
    stocks to watch.  The complementary role indicates that contained content is relevant
    to the main content.  If the complementary content is completely separable main
    content, it may be appropriate to use a more general role.
   */
   IA2_ROLE_COMPLEMENTARY_CONTENT,
-  
+
   /** An object representing a navigational landmark, a region on a page to
-   which the user may want quick access, such as navigating, searching,
-   perusing the primary content.
+   which the user may want quick access, such as a navigation area, a search
+   facility or the main content of a page.
   */
-  IA2_ROLE_LANDMARK
+  IA2_ROLE_LANDMARK,
+
+  /**
+   * A bar that serves as a level indicator to, for instance, show
+   * the strength of a password or the charge of a battery.
+   */
+  IA2_ROLE_LEVEL_BAR,
 
+  /** Content previously deleted or proposed for deletion, e.g. in revision
+   history or a content view providing suggestions from reviewers.
+  */
+  IA2_ROLE_CONTENT_DELETION,
+
+  /** Content previously inserted or proposed for insertion, e.g. in revision
+   history or a content view providing suggestions from reviewers.
+  */
+  IA2_ROLE_CONTENT_INSERTION,
+
+  /// A section of content that is quoted from another source.
+  IA2_ROLE_BLOCK_QUOTE
 };
--- a/python/mozboot/mozboot/android.py
+++ b/python/mozboot/mozboot/android.py
@@ -224,32 +224,54 @@ def ensure_android_sdk_and_ndk(mozbuild_
     elif os.path.isdir(sdk_path):
         raise NotImplementedError(ANDROID_SDK_TOO_OLD % sdk_path)
     else:
         # The SDK archive used to include a top-level
         # android-sdk-$OS_NAME directory; it no longer does so.  We
         # preserve the old convention to smooth detecting existing SDK
         # installations.
         install_mobile_android_sdk_or_ndk(sdk_url, os.path.join(mozbuild_path,
-                                          'android-sdk-{0}'.format(os_name)))
+                                                                'android-sdk-{0}'.format(os_name)))
+
+
+def get_packages_to_install(packages_file_name):
+    """
+    sdkmanager version 26.1.1 (current) and some versions below have a bug that makes
+    the following command fail:
+        args = [sdkmanager_tool, '--package_file={0}'.format(package_file_name)]
+        subprocess.check_call(args)
+    The error is in the sdkmanager, where the --package_file param isn't recognized.
+        The error is being tracked here https://issuetracker.google.com/issues/66465833
+    Meanwhile, this workaround achives installing all required Android packages by reading
+    them out of the same file that --package_file would have used, and passing them as strings.
+    So from here: https://developer.android.com/studio/command-line/sdkmanager
+    Instead of:
+        sdkmanager --package_file=package_file [options]
+    We're doing:
+        sdkmanager "platform-tools" "platforms;android-26"
+    """
+    with open(packages_file_name) as package_file:
+        return map(lambda package: package.strip(), package_file.readlines())
 
 
 def ensure_android_packages(sdkmanager_tool, packages=None, no_interactive=False):
     '''
     Use the given sdkmanager tool (like 'sdkmanager') to install required
     Android packages.
     '''
 
     # This tries to install all the required Android packages.  The user
     # may be prompted to agree to the Android license.
     package_file_name = os.path.abspath(os.path.join(os.path.dirname(__file__),
-                                        'android-packages.txt'))
+                                                     'android-packages.txt'))
     print(INSTALLING_ANDROID_PACKAGES % open(package_file_name, 'rt').read())
 
-    args = [sdkmanager_tool, '--package_file={0}'.format(package_file_name)]
+    args = [sdkmanager_tool]
+    args.extend(get_packages_to_install(package_file_name))
+
     if not no_interactive:
         subprocess.check_call(args)
         return
 
     # Emulate yes.  For a discussion of passing input to check_output,
     # see https://stackoverflow.com/q/10103551.
     yes = '\n'.join(['y']*100)
     proc = subprocess.Popen(args,
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/analyze/graph.py
@@ -0,0 +1,116 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sqlite3 as lite
+import os
+import time
+
+class Node(object):
+
+    def __init__(self, graph, node_id):
+        sql_result = graph.query_arg('SELECT id, dir, type, mtime, name \
+            FROM node WHERE id=?', (node_id,)).fetchone()
+        self.id, self.dir, self.type, self.mtime, self.name = sql_result
+        children = graph.query_arg('SELECT to_id FROM \
+            normal_link WHERE from_id=?', (self.id,)).fetchall()
+        self.children = [graph.get_node(x) for (x,) in children]
+        self.cmds = list(set(self.get_cmd_nodes()))
+        self.path = self.get_path(graph) if self.type == 0 else ''
+        self.cost = self.calculate_mtime()
+        graph.add_node(self.id, self)
+
+    @property
+    def num_cmds(self):
+        return len(self.cmds)
+
+    def get_cmd_nodes(self):
+        res = []
+        if self.type == 1:
+            res += [self]
+        return res + [c for x in self.children for c in x.cmds]
+
+    def get_cmd_ids(self):
+        return [x.id for x in self.cmds]
+
+    def get_path(self, graph):
+        if self.dir == 1:
+            return self.name
+        parent = graph.get_node(self.dir)
+        return os.path.join(parent.get_path(graph), self.name)
+
+    def calculate_mtime(self):
+        if self.type == 0: # only files have meaningful costs
+            return sum(x.mtime for x in self.cmds)
+        else:
+            return None
+
+class Graph(object):
+
+    def __init__(self, path=None, connect=None):
+        self.connect = connect
+        if path is not None:
+            self.connect = lite.connect(path)
+        elif self.connect is None:
+            raise Exception
+        if not self.table_check():
+            print ('\n Tup db does not have the necessary tables.')
+            raise Exception
+        self.node_dict = {}
+
+    def table_check(self):
+        tables = [x[0] for x in self.query_arg('SELECT name \
+            FROM sqlite_master WHERE type=?', ('table',)).fetchall()]
+        return ('node' in tables and 'normal_link' in tables)
+
+    def close(self):
+        self.connect.close()
+
+    def query_arg(self, q, arg):
+        assert isinstance(arg, tuple) #execute() requires tuple argument
+        cursor = self.connect.cursor()
+        cursor.execute(q, arg)
+        return cursor
+
+    def query(self, q):
+        cursor = self.connect.cursor()
+        cursor.execute(q)
+        return cursor
+
+    @property
+    def nodes(self):
+        return self.node_dict
+
+    def add_node(self, k, v):
+        self.node_dict[k] = v
+
+    def get_id(self, filepath):
+        nodeid = 1
+        for part in filepath.split('/'):
+            ret = self.query_arg('SELECT id FROM node \
+                WHERE dir=? AND name=?', (nodeid, part)).fetchone()
+            # fetchone should be ok bc dir and and name combo is unique
+            if ret == None:
+                print ("\nCould not find id number for '%s'" % filepath)
+                return None
+            nodeid = ret[0]
+        return nodeid
+
+    def get_node(self, node_id):
+        if node_id is not None:
+            node = self.node_dict.get(node_id)
+            if node is None:
+                return Node(self, node_id)
+            else:
+                return node
+
+    def file_summaries(self, files):
+        for f in files:
+            node = self.get_node(self.get_id(f))
+            if node is not None:
+                sec = node.cost / 1000.0
+                m, s = sec / 60, sec % 60
+                print ("\n------ Summary for %s ------\
+                    \nTotal cost (mm:ss) = %d:%d\nNum Downstream Commands = %d"
+                    % (f, m, s, node.num_cmds))
+
--- a/python/mozbuild/mozbuild/backend/tup.py
+++ b/python/mozbuild/mozbuild/backend/tup.py
@@ -261,17 +261,17 @@ class TupBackend(CommonBackend):
         # dependencies that aren't specified by moz.build and cause errors
         # in Tup. Express these as a group dependency.
         self._early_generated_files = '$(MOZ_OBJ_ROOT)/<early-generated-files>'
 
         self._shlibs = '$(MOZ_OBJ_ROOT)/<shlibs>'
         self._gtests = '$(MOZ_OBJ_ROOT)/<gtest>'
         self._default_group = '$(MOZ_OBJ_ROOT)/<default>'
 
-        self._rust_outputs = set()
+        self._rust_cmds = set()
 
         self._built_in_addons = set()
         self._built_in_addons_file = 'dist/bin/browser/chrome/browser/content/browser/built_in_addons.json'
 
 
     def _get_mozconfig_env(self, config):
         env = {}
         loader = MozconfigLoader(config.topsrcdir)
@@ -795,34 +795,33 @@ class TupBackend(CommonBackend):
                 header = 'RUSTC'
             else:
                 inputs.add(invocation['program'])
                 header = 'RUN'
 
             invocation['full-deps'] = set(inputs)
             invocation['full-deps'].update(invocation['outputs'])
 
-            output_key = tuple(outputs)
-            if output_key not in self._rust_outputs:
+            cmd_key = ' '.join(command)
+            if cmd_key not in self._rust_cmds:
+                self._rust_cmds.add(cmd_key)
                 # The two rust libraries in the tree share many prerequisites,
                 # so we need to prune common dependencies and therefore build
                 # all rust from the same Tupfile.
                 rust_backend_file = self._get_backend_file('toolkit/library/rust')
-                self._rust_outputs.add(output_key)
                 rust_backend_file.rule(
                     command,
                     inputs=sorted(inputs),
                     outputs=outputs,
                     output_group=self._rust_libs,
                     extra_inputs=[self._installed_files],
                     display='%s %s' % (header, display_name(invocation)),
                 )
 
                 for dst, link in invocation['links'].iteritems():
-                    self._rust_outputs.add(output_key)
                     rust_backend_file.symlink_rule(link, dst, self._rust_libs)
 
         for val in enumerate(invocations):
             _process(*val)
 
 
     def _gen_rust_rules(self, obj, backend_file):
         cargo_flags = self._get_cargo_flags(obj)
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -2472,8 +2472,30 @@ class Repackage(MachCommandBase):
         help='Input filename')
     @CommandArgument('--mar', type=str, required=True,
         help='Mar binary path')
     @CommandArgument('--output', '-o', type=str, required=True,
         help='Output filename')
     def repackage_mar(self, input, mar, output):
         from mozbuild.repackaging.mar import repackage_mar
         repackage_mar(self.topsrcdir, input, mar, output)
+
+@CommandProvider
+class Analyze(MachCommandBase):
+    """ Get information about a file in the build graph """
+    @Command('summarize', category='misc',
+        description='Get incremental build cost for a file (or files) from the tup database.')
+    @CommandArgument('--path', help='Path to tup db',
+        default=None)
+    @CommandArgument('files', nargs='*', help='Files to summarize')
+    def summarize(self, path, files):
+        from mozbuild.analyze.graph import Graph
+        if path is None:
+            path = mozpath.join(self.topsrcdir, '.tup', 'db')
+        if os.path.isfile(path):
+            g = Graph(path)
+            g.file_summaries(files)
+            g.close()
+        else:
+            res = 'Please make sure you have a local tup db *or* specify the location with --path.'
+            print ('Could not find a valid tup db in ' + path, res, sep='\n')
+            return 1
+
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/analyze/test_graph.py
@@ -0,0 +1,135 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import unittest
+import sqlite3 as lite
+import mozunit
+
+from mozbuild.analyze.graph import Graph
+
+CREATE_NODE = """CREATE TABLE node
+        (id integer PRIMARY KEY NOT NULL,
+        dir integer NOT NULL,
+        type integer NOT NULL,
+        mtime integer NOT NULL,
+        name varchar(4096) NOT NULL,
+        unique(dir, name));"""
+
+CREATE_NORMAL_LINK = """CREATE TABLE normal_link
+        (from_id integer,
+        to_id integer, unique(from_id, to_id));"""
+
+NODE_DATA = [(1, 0 ,2, -1, '.'),
+        (2, 100, 0, 1, 'Base64.cpp'),
+        (3, 200, 0, 1, 'nsArray.cpp'),
+        (4, 100, 0, 1, 'nsWildCard.h'),
+        (5, -1, 1, 9426, 'CDD Unified_cpp_xpcom_io0.cpp'),
+        (6, -1, 1, 5921, 'CXX Unified_cpp_xpcom_ds0.cpp'),
+        (7, -1, 1, 11077, 'CXX /builds/worker/workspace/build/src/dom/\
+            plugins/base/snNPAPIPlugin.cpp'),
+        (8, -1, 1, 7677, 'CXX Unified_cpp_xpcom_io1.cpp'),
+        (9, -1, 1, 8672, 'CXX Unified_cpp_modules_libjar0.cpp'),
+        (10, -1, 4, 1, 'Unified_cpp_xpcom_io0.o'),
+        (11, -1, 4, 1, 'Unified_cpp_xpcom_dso.o'),
+        (12, -1, 4, 1, 'nsNPAPIPlugin.o'),
+        (13, -1, 4, 1, 'Unified_cpp_xpcom_io1.o'),
+        (14, -1, 4, 1, 'Unified_cpp_modules_libjar0.o'),
+        (15, -1, 1, 52975, 'LINK libxul.so'),
+        (16, -1, 4, 1, 'libxul.so'),
+        (17, -1, 1, 180, 'LINK libtestcrasher.so'),
+        (18, -1, 1, 944, 'python /builds/worker/workspace/build/src/toolkit/\
+            library/dependentlibs.py:gen_list -> [dependentlibs.list, \
+            dependentlibs.list.gtest, dependentlibs.list.pp]'),
+        (19, -1, 1, 348, 'LINK ../../dist/bin/plugin-container'),
+        (20, -1, 1, 342, 'LINK ../../../dist/bin/xpcshell'),
+        (21, -1, 4, 1, 'libtestcrasher.so'),
+        (22, -1, 4, 1, 'dependentlibs.list'),
+        (23, -1, 4, 1, 'dependentlibs.list.gtest'),
+        (24, -1, 4, 1, 'dependentlibs.list.pp'),
+        (25, -1, 4, 1, 'plugin-container'),
+        (26, -1, 4, 1, 'xpcshell'),
+        (27, -1, 6, 1, '<shlibs>'),
+        (28, 1, 0, 1, 'dummy node'),
+        (100, 300, 2, -1, 'io'),
+        (200, 300, 2, -1, 'ds'),
+        (300, 1, 2, -1, 'xpcom')]
+
+NORMAL_LINK_DATA = [(2, 5), (3, 6), (4, 7), (4, 8), (4, 9), (5, 10), (6, 11),
+                    (7, 12), (8, 13), (9, 14), (10, 15), (11, 15), (12, 15),
+                    (13, 15), (14, 15), (15, 16), (15, 27), (16, 17), (16, 18),
+                    (16, 19), (16, 20), (17, 21), (17, 27), (18, 22), (18, 23),
+                    (18, 24), (19, 25), (20, 26), (21, 27)]
+
+PATH_TO_TEST_DB = ':memory:'
+
+class TestGraph(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.connect = lite.connect(PATH_TO_TEST_DB)
+        cls.connect.text_factory = str
+        cls.cursor = cls.connect.cursor()
+        # create tables in memory
+        cls.cursor.execute(CREATE_NODE)
+        cls.cursor.execute(CREATE_NORMAL_LINK)
+        # insert data to tables
+        for d in NODE_DATA:
+            cls.cursor.execute("""INSERT INTO node \
+                (id, dir, type, mtime, name) values(?, ?, ?, ?, ?)""", d)
+        for d in NORMAL_LINK_DATA:
+            cls.cursor.execute("""INSERT INTO normal_link \
+                (from_id, to_id) values(?, ?)""", d)
+        cls.connect.commit()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.connect.close()
+
+    def test_graph_cmds(self):
+        g = Graph(connect=self.connect)
+        libxul = [15, 17, 18, 19, 20]
+        # no commands
+        self.assertEqual(len(g.get_node(27).cmds), 0)
+        self.assertEqual(len(g.get_node(21).cmds), 0)
+        self.assertEqual(len(g.get_node(28).cmds), 0)
+        # one immediate command child
+        self.assertItemsEqual(g.get_node(2).get_cmd_ids(),[5] + libxul)
+        self.assertItemsEqual(g.get_node(3).get_cmd_ids(),[6] + libxul)
+        # multiple immediate command children
+        self.assertItemsEqual(g.get_node(4).get_cmd_ids(),[7, 8, 9] + libxul)
+        # node is not a file or command
+        self.assertItemsEqual(g.get_node(16).get_cmd_ids(), libxul[1:])
+        self.assertItemsEqual(g.get_node(11).get_cmd_ids(), libxul)
+        # node is the command
+        self.assertItemsEqual(g.get_node(19).get_cmd_ids(), [19])
+        self.assertItemsEqual(g.get_node(7).get_cmd_ids(), [7] + libxul)
+        self.assertItemsEqual(g.get_node(15).get_cmd_ids(), libxul)
+
+    def test_mtime_calc(self):
+        g = Graph(connect=self.connect)
+        # one immediate child
+        self.assertEqual(g.get_node(2).cost, 64215)
+        self.assertEqual(g.get_node(3).cost, 60710)
+        # multiple immediate children
+        self.assertEqual(g.get_node(4).cost, 82215)
+        # no children
+        self.assertEqual(g.get_node(23).cost, None)
+        # not a file
+        self.assertEqual(g.get_node(15).cost, None)
+        # file with no commands
+        self.assertEqual(g.get_node(28).cost, 0)
+
+    def test_path(self):
+        g = Graph(connect=self.connect)
+        # node is not a file
+        self.assertEqual(g.get_node(15).path, '')
+        self.assertEqual(g.get_node(16).path, '')
+        self.assertEqual(g.get_node(27).path, '')
+        # node is a file
+        self.assertEqual(g.get_node(2).path, 'xpcom/io/Base64.cpp')
+        self.assertEqual(g.get_node(3).path, 'xpcom/ds/nsArray.cpp')
+        self.assertEqual(g.get_node(4).path, 'xpcom/io/nsWildCard.h')
+        self.assertEqual(g.get_node(28).path, 'dummy node')
+
+if __name__ == '__main__':
+  mozunit.main()
\ No newline at end of file
--- a/python/mozbuild/mozbuild/test/python.ini
+++ b/python/mozbuild/mozbuild/test/python.ini
@@ -1,15 +1,17 @@
 [DEFAULT]
 skip-if = python == 3
 
 [action/test_buildlist.py]
 [action/test_langpack_manifest.py]
 [action/test_process_install_manifest.py]
 [action/test_package_fennec_apk.py]
+[analyze/test_graph.py]
+skip-if = (os == "win")
 [backend/test_build.py]
 [backend/test_configenvironment.py]
 [backend/test_fastermake.py]
 [backend/test_gn_processor.py]
 [backend/test_partialconfigenvironment.py]
 [backend/test_recursivemake.py]
 [backend/test_test_manifest.py]
 [backend/test_visualstudio.py]
--- a/testing/talos/talos/tests/tabswitch/api.js
+++ b/testing/talos/talos/tests/tabswitch/api.js
@@ -1,15 +1,15 @@
 // -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
 
 /* globals ExtensionAPI */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://gre/modules/RemotePageManager.jsm");
+ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm");
 
 let context = {};
 let TalosParentProfiler;
 
 /**
  * Returns a Promise that resolves when browser-delayed-startup-finished
  * fires for a given window
  *
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -51,16 +51,17 @@ DIRS += [
     'places',
     'privatebrowsing',
     'processsingleton',
     'promiseworker',
     'prompts',
     'protobuf',
     'reader',
     'remotebrowserutils',
+    'remotepagemanager',
     'reflect',
     'reputationservice',
     'resistfingerprinting',
     'securityreporter',
     'sessionstore',
     'startup',
     'statusfilter',
     'telemetry',
rename from toolkit/modules/RemotePageManager.jsm
rename to toolkit/components/remotepagemanager/MessagePort.jsm
--- a/toolkit/modules/RemotePageManager.jsm
+++ b/toolkit/components/remotepagemanager/MessagePort.jsm
@@ -1,40 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["RemotePages", "RemotePageManager", "PageListener"];
-
-/*
- * Using the RemotePageManager:
- * * Create a new page listener by calling 'new RemotePages(URI)' which
- *   then injects functions like RPMGetBoolPref() into the registered page.
- *   One can then use those exported functions to communicate between
- *   child and parent.
- *
- * * When adding a new consumer of RPM that relies on other functionality
- *   then simple message passing provided by the RPM, then one has to
- *   whitelist permissions for the new URI within the RPMAccessManager.
- *
- * Please note that prefs that one wants to update need to be whitelisted
- * within AsyncPrefs.jsm.
- */
+var EXPORTED_SYMBOLS = ["MessagePort", "MessageListener"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.defineModuleGetter(this, "AsyncPrefs",
   "resource://gre/modules/AsyncPrefs.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 /*
  * Used for all kinds of permissions checks which requires explicit
  * whitelisting of specific permissions granted through RPM.
+ *
+ * Please note that prefs that one wants to update need to be
+ * whitelisted within AsyncPrefs.jsm.
  */
 let RPMAccessManager = {
   accessMap: {
     "about:privatebrowsing": {
       // "sendAsyncMessage": handled within AboutPrivateBrowsingHandler.jsm
       // "setBoolPref": handled within AsyncPrefs.jsm and uses the pref
       //                ["privacy.trackingprotection.pbmode.enabled"],
       "getBoolPref": ["privacy.trackingprotection.enabled",
@@ -118,181 +106,16 @@ MessageListener.prototype = {
   removeMessageListener(name, callback) {
     if (!this.listeners.has(name))
       return;
 
     this.listeners.get(name).delete(callback);
   },
 };
 
-
-/**
- * Creates a RemotePages object which listens for new remote pages of some
- * particular URLs. A "RemotePage:Init" message will be dispatched to this
- * object for every page loaded. Message listeners added to this object receive
- * messages from all loaded pages from the requested urls.
- */
-var RemotePages = function(urls) {
-  this.urls = Array.isArray(urls) ? urls : [urls];
-  this.messagePorts = new Set();
-  this.listener = new MessageListener();
-  this.destroyed = false;
-
-  this.portCreated = this.portCreated.bind(this);
-  this.portMessageReceived = this.portMessageReceived.bind(this);
-
-  for (const url of this.urls) {
-    RemotePageManager.addRemotePageListener(url, this.portCreated);
-  }
-};
-
-RemotePages.prototype = {
-  urls: null,
-  messagePorts: null,
-  listener: null,
-  destroyed: null,
-
-  destroy() {
-    for (const url of this.urls) {
-      RemotePageManager.removeRemotePageListener(url);
-    }
-
-    for (let port of this.messagePorts.values()) {
-      this.removeMessagePort(port);
-    }
-
-    this.messagePorts = null;
-    this.listener = null;
-    this.destroyed = true;
-  },
-
-  // Called when a page matching one of the urls has loaded in a frame.
-  portCreated(port) {
-    this.messagePorts.add(port);
-
-    port.loaded = false;
-    port.addMessageListener("RemotePage:Load", this.portMessageReceived);
-    port.addMessageListener("RemotePage:Unload", this.portMessageReceived);
-
-    for (let name of this.listener.keys()) {
-      this.registerPortListener(port, name);
-    }
-
-    this.listener.callListeners({ target: port, name: "RemotePage:Init" });
-  },
-
-  // A message has been received from one of the pages
-  portMessageReceived(message) {
-    switch (message.name) {
-      case "RemotePage:Load":
-        message.target.loaded = true;
-        break;
-      case "RemotePage:Unload":
-        message.target.loaded = false;
-        this.removeMessagePort(message.target);
-        break;
-    }
-
-    this.listener.callListeners(message);
-  },
-
-  // A page has closed
-  removeMessagePort(port) {
-    for (let name of this.listener.keys()) {
-      port.removeMessageListener(name, this.portMessageReceived);
-    }
-
-    port.removeMessageListener("RemotePage:Load", this.portMessageReceived);
-    port.removeMessageListener("RemotePage:Unload", this.portMessageReceived);
-    this.messagePorts.delete(port);
-  },
-
-  registerPortListener(port, name) {
-    port.addMessageListener(name, this.portMessageReceived);
-  },
-
-  // Sends a message to all known pages
-  sendAsyncMessage(name, data = null) {
-    for (let port of this.messagePorts.values()) {
-      try {
-        port.sendAsyncMessage(name, data);
-      } catch (e) {
-        // Unless the port is in the process of unloading, something strange
-        // happened but allow other ports to receive the message
-        if (e.result !== Cr.NS_ERROR_NOT_INITIALIZED)
-          Cu.reportError(e);
-      }
-    }
-  },
-
-  addMessageListener(name, callback) {
-    if (this.destroyed) {
-      throw new Error("RemotePages has been destroyed");
-    }
-
-    if (!this.listener.has(name)) {
-      for (let port of this.messagePorts.values()) {
-        this.registerPortListener(port, name);
-      }
-    }
-
-    this.listener.addMessageListener(name, callback);
-  },
-
-  removeMessageListener(name, callback) {
-    if (this.destroyed) {
-      throw new Error("RemotePages has been destroyed");
-    }
-
-    this.listener.removeMessageListener(name, callback);
-  },
-
-  portsForBrowser(browser) {
-    return [...this.messagePorts].filter(port => port.browser == browser);
-  },
-};
-
-
-// Only exposes the public properties of the MessagePort
-function publicMessagePort(port) {
-  let properties = ["addMessageListener", "removeMessageListener",
-                    "sendAsyncMessage", "destroy"];
-
-  let clean = {};
-  for (let property of properties) {
-    clean[property] = port[property].bind(port);
-  }
-
-  Object.defineProperty(clean, "portID", {
-    enumerable: true,
-    get() {
-      return port.portID;
-    }
-  });
-
-  if (port instanceof ChromeMessagePort) {
-    Object.defineProperty(clean, "browser", {
-      enumerable: true,
-      get() {
-        return port.browser;
-      }
-    });
-
-    Object.defineProperty(clean, "url", {
-      enumerable: true,
-      get() {
-        return port.url;
-      }
-    });
-  }
-
-  return clean;
-}
-
-
 /*
  * A message port sits on each side of the process boundary for every remote
  * page. Each has a port ID that is unique to the message manager it talks
  * through.
  *
  * We roughly implement the same contract as nsIMessageSender and
  * nsIMessageListenerManager
  */
@@ -403,285 +226,8 @@ MessagePort.prototype = {
   isWindowPrivate() {
     let principal = this.window.document.nodePrincipal;
     if (!RPMAccessManager.checkAllowAccess(principal, "isWindowPrivate", "yes")) {
       throw new Error("RPMAccessManager does not allow access to isWindowPrivate");
     }
     return PrivateBrowsingUtils.isContentWindowPrivate(this.window);
   },
 };
-
-
-// The chome side of a message port
-function ChromeMessagePort(browser, portID, url) {
-  MessagePort.call(this, browser.messageManager, portID);
-
-  this._browser = browser;
-  this._permanentKey = browser.permanentKey;
-  this._url = url;
-
-  Services.obs.addObserver(this, "message-manager-disconnect");
-  this.publicPort = publicMessagePort(this);
-
-  this.swapBrowsers = this.swapBrowsers.bind(this);
-  this._browser.addEventListener("SwapDocShells", this.swapBrowsers);
-}
-
-ChromeMessagePort.prototype = Object.create(MessagePort.prototype);
-
-Object.defineProperty(ChromeMessagePort.prototype, "browser", {
-  get() {
-    return this._browser;
-  }
-});
-
-Object.defineProperty(ChromeMessagePort.prototype, "url", {
-  get() {
-    return this._url;
-  }
-});
-
-// Called when the docshell is being swapped with another browser. We have to
-// update to use the new browser's message manager
-ChromeMessagePort.prototype.swapBrowsers = function({ detail: newBrowser }) {
-  // We can see this event for the new browser before the swap completes so
-  // check that the browser we're tracking has our permanentKey.
-  if (this._browser.permanentKey != this._permanentKey)
-    return;
-
-  this._browser.removeEventListener("SwapDocShells", this.swapBrowsers);
-
-  this._browser = newBrowser;
-  this.swapMessageManager(newBrowser.messageManager);
-
-  this._browser.addEventListener("SwapDocShells", this.swapBrowsers);
-};
-
-// Called when a message manager has been disconnected indicating that the
-// tab has closed or crashed
-ChromeMessagePort.prototype.observe = function(messageManager) {
-  if (messageManager != this.messageManager)
-    return;
-
-  this.listener.callListeners({
-    target: this.publicPort,
-    name: "RemotePage:Unload",
-    data: null,
-  });
-  this.destroy();
-};
-
-// Called when a message is received from the message manager. This could
-// have come from any port in the message manager so verify the port ID.
-ChromeMessagePort.prototype.message = function({ data: messagedata }) {
-  if (this.destroyed || (messagedata.portID != this.portID)) {
-    return;
-  }
-
-  let message = {
-    target: this.publicPort,
-    name: messagedata.name,
-    data: messagedata.data,
-  };
-  this.listener.callListeners(message);
-
-  if (messagedata.name == "RemotePage:Unload")
-    this.destroy();
-};
-
-ChromeMessagePort.prototype.destroy = function() {
-  try {
-    this._browser.removeEventListener(
-        "SwapDocShells", this.swapBrowsers);
-  } catch (e) {
-    // It's possible the browser instance is already dead so we can just ignore
-    // this error.
-  }
-
-  this._browser = null;
-  Services.obs.removeObserver(this, "message-manager-disconnect");
-  MessagePort.prototype.destroy.call(this);
-};
-
-
-// The content side of a message port
-function ChildMessagePort(contentFrame, window) {
-  let portID = Services.appinfo.processID + ":" + ChildMessagePort.prototype.nextPortID++;
-  MessagePort.call(this, contentFrame, portID);
-
-  this.window = window;
-
-  // Add functionality to the content page
-  Cu.exportFunction(this.sendAsyncMessage.bind(this), window, {
-    defineAs: "RPMSendAsyncMessage",
-  });
-  Cu.exportFunction(this.addMessageListener.bind(this), window, {
-    defineAs: "RPMAddMessageListener",
-    allowCallbacks: true,
-  });
-  Cu.exportFunction(this.removeMessageListener.bind(this), window, {
-    defineAs: "RPMRemoveMessageListener",
-    allowCallbacks: true,
-  });
-  Cu.exportFunction(this.getBoolPref.bind(this), window, {
-    defineAs: "RPMGetBoolPref",
-  });
-  Cu.exportFunction(this.setBoolPref.bind(this), window, {
-    defineAs: "RPMSetBoolPref",
-  });
-  Cu.exportFunction(this.getFormatURLPref.bind(this), window, {
-    defineAs: "RPMGetFormatURLPref",
-  });
-  Cu.exportFunction(this.isWindowPrivate.bind(this), window, {
-    defineAs: "RPMIsWindowPrivate",
-  });
-
-  // Send a message for load events
-  let loadListener = () => {
-    this.sendAsyncMessage("RemotePage:Load");
-    window.removeEventListener("load", loadListener);
-  };
-  window.addEventListener("load", loadListener);
-
-  // Destroy the port when the window is unloaded
-  window.addEventListener("unload", () => {
-    try {
-      this.sendAsyncMessage("RemotePage:Unload");
-    } catch (e) {
-      // If the tab has been closed the frame message manager has already been
-      // destroyed
-    }
-    this.destroy();
-  });
-
-  // Tell the main process to set up its side of the message pipe.
-  this.messageManager.sendAsyncMessage("RemotePage:InitPort", {
-    portID,
-    url: window.document.documentURI.replace(/[\#|\?].*$/, ""),
-  });
-}
-
-ChildMessagePort.prototype = Object.create(MessagePort.prototype);
-
-ChildMessagePort.prototype.nextPortID = 0;
-
-// Called when a message is received from the message manager. This could
-// have come from any port in the message manager so verify the port ID.
-ChildMessagePort.prototype.message = function({ data: messagedata }) {
-  if (this.destroyed || (messagedata.portID != this.portID)) {
-    return;
-  }
-
-  let message = {
-    name: messagedata.name,
-    data: messagedata.data,
-  };
-  this.listener.callListeners(Cu.cloneInto(message, this.window));
-};
-
-ChildMessagePort.prototype.destroy = function() {
-  this.window = null;
-  MessagePort.prototype.destroy.call(this);
-};
-
-// Allows callers to register to connect to specific content pages. Registration
-// is done through the addRemotePageListener method
-var RemotePageManagerInternal = {
-  // The currently registered remote pages
-  pages: new Map(),
-
-  // Initialises all the needed listeners
-  init() {
-    Services.mm.addMessageListener("RemotePage:InitPort", this.initPort.bind(this));
-    this.updateProcessUrls();
-  },
-
-  updateProcessUrls() {
-    Services.ppmm.initialProcessData["RemotePageManager:urls"] = Array.from(this.pages.keys());
-  },
-
-  // Registers interest in a remote page. A callback is called with a port for
-  // the new page when loading begins (i.e. the page hasn't actually loaded yet).
-  // Only one callback can be registered per URL.
-  addRemotePageListener(url, callback) {
-    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-      throw new Error("RemotePageManager can only be used in the main process.");
-
-    if (this.pages.has(url)) {
-      throw new Error("Remote page already registered: " + url);
-    }
-
-    this.pages.set(url, callback);
-    this.updateProcessUrls();
-
-    // Notify all the frame scripts of the new registration
-    Services.ppmm.broadcastAsyncMessage("RemotePage:Register", { urls: [url] });
-  },
-
-  // Removes any interest in a remote page.
-  removeRemotePageListener(url) {
-    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-      throw new Error("RemotePageManager can only be used in the main process.");
-
-    if (!this.pages.has(url)) {
-      throw new Error("Remote page is not registered: " + url);
-    }
-
-    // Notify all the frame scripts of the removed registration
-    Services.ppmm.broadcastAsyncMessage("RemotePage:Unregister", { urls: [url] });
-    this.pages.delete(url);
-    this.updateProcessUrls();
-  },
-
-  // A remote page has been created and a port is ready in the content side
-  initPort({ target: browser, data: { url, portID } }) {
-    let callback = this.pages.get(url);
-    if (!callback) {
-      Cu.reportError("Unexpected remote page load: " + url);
-      return;
-    }
-
-    let port = new ChromeMessagePort(browser, portID, url);
-    callback(port.publicPort);
-  }
-};
-
-if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-  RemotePageManagerInternal.init();
-
-// The public API for the above object
-var RemotePageManager = {
-  addRemotePageListener: RemotePageManagerInternal.addRemotePageListener.bind(RemotePageManagerInternal),
-  removeRemotePageListener: RemotePageManagerInternal.removeRemotePageListener.bind(RemotePageManagerInternal),
-};
-
-// Listen for pages in any process we're loaded in
-var registeredURLs = new Set(Services.cpmm.initialProcessData["RemotePageManager:urls"]);
-
-var observer = (window) => {
-  // Strip the hash from the URL, because it's not part of the origin.
-  let url = window.document.documentURI.replace(/[\#|\?].*$/, "");
-  if (!registeredURLs.has(url))
-    return;
-
-  // Get the frame message manager for this window so we can associate this
-  // page with a browser element
-  let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDocShell)
-                             .QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIContentFrameMessageManager);
-  // Set up the child side of the message port
-  new ChildMessagePort(messageManager, window);
-};
-Services.obs.addObserver(observer, "chrome-document-global-created");
-Services.obs.addObserver(observer, "content-document-global-created");
-
-// A message from chrome telling us what pages to listen for
-Services.cpmm.addMessageListener("RemotePage:Register", ({ data }) => {
-  for (let url of data.urls)
-    registeredURLs.add(url);
-});
-
-// A message from chrome telling us what pages to stop listening for
-Services.cpmm.addMessageListener("RemotePage:Unregister", ({ data }) => {
-  for (let url of data.urls)
-    registeredURLs.delete(url);
-});
copy from toolkit/modules/RemotePageManager.jsm
copy to toolkit/components/remotepagemanager/RemotePageManagerChild.jsm
--- a/toolkit/modules/RemotePageManager.jsm
+++ b/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm
@@ -1,511 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["RemotePages", "RemotePageManager", "PageListener"];
-
-/*
- * Using the RemotePageManager:
- * * Create a new page listener by calling 'new RemotePages(URI)' which
- *   then injects functions like RPMGetBoolPref() into the registered page.
- *   One can then use those exported functions to communicate between
- *   child and parent.
- *
- * * When adding a new consumer of RPM that relies on other functionality
- *   then simple message passing provided by the RPM, then one has to
- *   whitelist permissions for the new URI within the RPMAccessManager.
- *
- * Please note that prefs that one wants to update need to be whitelisted
- * within AsyncPrefs.jsm.
- */
+var EXPORTED_SYMBOLS = ["ChildMessagePort"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.defineModuleGetter(this, "AsyncPrefs",
-  "resource://gre/modules/AsyncPrefs.jsm");
-ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
-  "resource://gre/modules/PrivateBrowsingUtils.jsm");
-
-/*
- * Used for all kinds of permissions checks which requires explicit
- * whitelisting of specific permissions granted through RPM.
- */
-let RPMAccessManager = {
-  accessMap: {
-    "about:privatebrowsing": {
-      // "sendAsyncMessage": handled within AboutPrivateBrowsingHandler.jsm
-      // "setBoolPref": handled within AsyncPrefs.jsm and uses the pref
-      //                ["privacy.trackingprotection.pbmode.enabled"],
-      "getBoolPref": ["privacy.trackingprotection.enabled",
-                      "privacy.trackingprotection.pbmode.enabled"],
-      "getFormatURLPref": ["privacy.trackingprotection.introURL",
-                           "app.support.baseURL"],
-      "isWindowPrivate": ["yes"],
-    },
-  },
-
-  checkAllowAccess(aPrincipal, aFeature, aValue) {
-    // if there is no content principal; deny access
-    if (!aPrincipal || !aPrincipal.URI) {
-      return false;
-    }
-    let uri = aPrincipal.URI.asciiSpec;
-
-    // check if there is an entry for that requestying URI in the accessMap;
-    // if not, deny access.
-    let accessMapForURI = this.accessMap[uri];
-    if (!accessMapForURI) {
-      Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
-      return false;
-    }
-
-    // check if the feature is allowed to be accessed for that URI;
-    // if not, deny access.
-    let accessMapForFeature = accessMapForURI[aFeature];
-    if (!accessMapForFeature) {
-      Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
-      return false;
-    }
-
-    // if the actual value is in the whitelist for that feature;
-    // allow access
-    if (accessMapForFeature.includes(aValue)) {
-      return true;
-    }
-
-    // otherwise deny access
-    Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
-    return false;
-  },
-};
-
-function MessageListener() {
-  this.listeners = new Map();
-}
-
-MessageListener.prototype = {
-  keys() {
-    return this.listeners.keys();
-  },
-
-  has(name) {
-    return this.listeners.has(name);
-  },
-
-  callListeners(message) {
-    let listeners = this.listeners.get(message.name);
-    if (!listeners) {
-      return;
-    }
-
-    for (let listener of listeners.values()) {
-      try {
-        listener(message);
-      } catch (e) {
-        Cu.reportError(e);
-      }
-    }
-  },
-
-  addMessageListener(name, callback) {
-    if (!this.listeners.has(name))
-      this.listeners.set(name, new Set([callback]));
-    else
-      this.listeners.get(name).add(callback);
-  },
-
-  removeMessageListener(name, callback) {
-    if (!this.listeners.has(name))
-      return;
-
-    this.listeners.get(name).delete(callback);
-  },
-};
-
-
-/**
- * Creates a RemotePages object which listens for new remote pages of some
- * particular URLs. A "RemotePage:Init" message will be dispatched to this
- * object for every page loaded. Message listeners added to this object receive
- * messages from all loaded pages from the requested urls.
- */
-var RemotePages = function(urls) {
-  this.urls = Array.isArray(urls) ? urls : [urls];
-  this.messagePorts = new Set();
-  this.listener = new MessageListener();
-  this.destroyed = false;
-
-  this.portCreated = this.portCreated.bind(this);
-  this.portMessageReceived = this.portMessageReceived.bind(this);
-
-  for (const url of this.urls) {
-    RemotePageManager.addRemotePageListener(url, this.portCreated);
-  }
-};
-
-RemotePages.prototype = {
-  urls: null,
-  messagePorts: null,
-  listener: null,
-  destroyed: null,
-
-  destroy() {
-    for (const url of this.urls) {
-      RemotePageManager.removeRemotePageListener(url);
-    }
-
-    for (let port of this.messagePorts.values()) {
-      this.removeMessagePort(port);
-    }
-
-    this.messagePorts = null;
-    this.listener = null;
-    this.destroyed = true;
-  },
-
-  // Called when a page matching one of the urls has loaded in a frame.
-  portCreated(port) {
-    this.messagePorts.add(port);
-
-    port.loaded = false;
-    port.addMessageListener("RemotePage:Load", this.portMessageReceived);
-    port.addMessageListener("RemotePage:Unload", this.portMessageReceived);
-
-    for (let name of this.listener.keys()) {
-      this.registerPortListener(port, name);
-    }
-
-    this.listener.callListeners({ target: port, name: "RemotePage:Init" });
-  },
-
-  // A message has been received from one of the pages
-  portMessageReceived(message) {
-    switch (message.name) {
-      case "RemotePage:Load":
-        message.target.loaded = true;
-        break;
-      case "RemotePage:Unload":
-        message.target.loaded = false;
-        this.removeMessagePort(message.target);
-        break;
-    }
-
-    this.listener.callListeners(message);
-  },
-
-  // A page has closed
-  removeMessagePort(port) {
-    for (let name of this.listener.keys()) {
-      port.removeMessageListener(name, this.portMessageReceived);
-    }
-
-    port.removeMessageListener("RemotePage:Load", this.portMessageReceived);
-    port.removeMessageListener("RemotePage:Unload", this.portMessageReceived);
-    this.messagePorts.delete(port);
-  },
-
-  registerPortListener(port, name) {
-    port.addMessageListener(name, this.portMessageReceived);
-  },
-
-  // Sends a message to all known pages
-  sendAsyncMessage(name, data = null) {
-    for (let port of this.messagePorts.values()) {
-      try {
-        port.sendAsyncMessage(name, data);
-      } catch (e) {
-        // Unless the port is in the process of unloading, something strange
-        // happened but allow other ports to receive the message
-        if (e.result !== Cr.NS_ERROR_NOT_INITIALIZED)
-          Cu.reportError(e);
-      }
-    }
-  },
-
-  addMessageListener(name, callback) {
-    if (this.destroyed) {
-      throw new Error("RemotePages has been destroyed");
-    }
-
-    if (!this.listener.has(name)) {
-      for (let port of this.messagePorts.values()) {
-        this.registerPortListener(port, name);
-      }
-    }
-
-    this.listener.addMessageListener(name, callback);
-  },
-
-  removeMessageListener(name, callback) {
-    if (this.destroyed) {
-      throw new Error("RemotePages has been destroyed");
-    }
-
-    this.listener.removeMessageListener(name, callback);
-  },
-
-  portsForBrowser(browser) {
-    return [...this.messagePorts].filter(port => port.browser == browser);
-  },
-};
-
-
-// Only exposes the public properties of the MessagePort
-function publicMessagePort(port) {
-  let properties = ["addMessageListener", "removeMessageListener",
-                    "sendAsyncMessage", "destroy"];
-
-  let clean = {};
-  for (let property of properties) {
-    clean[property] = port[property].bind(port);
-  }
-
-  Object.defineProperty(clean, "portID", {
-    enumerable: true,
-    get() {
-      return port.portID;
-    }
-  });
-
-  if (port instanceof ChromeMessagePort) {
-    Object.defineProperty(clean, "browser", {
-      enumerable: true,
-      get() {
-        return port.browser;
-      }
-    });
-
-    Object.defineProperty(clean, "url", {
-      enumerable: true,
-      get() {
-        return port.url;
-      }
-    });
-  }
-
-  return clean;
-}
-
-
-/*
- * A message port sits on each side of the process boundary for every remote
- * page. Each has a port ID that is unique to the message manager it talks
- * through.
- *
- * We roughly implement the same contract as nsIMessageSender and
- * nsIMessageListenerManager
- */
-function MessagePort(messageManager, portID) {
-  this.messageManager = messageManager;
-  this.portID = portID;
-  this.destroyed = false;
-  this.listener = new MessageListener();
-
-  this.message = this.message.bind(this);
-  this.messageManager.addMessageListener("RemotePage:Message", this.message);
-}
-
-MessagePort.prototype = {
-  messageManager: null,
-  portID: null,
-  destroyed: null,
-  listener: null,
-  _browser: null,
-  remotePort: null,
-
-  // Called when the message manager used to connect to the other process has
-  // changed, i.e. when a tab is detached.
-  swapMessageManager(messageManager) {
-    this.messageManager.removeMessageListener("RemotePage:Message", this.message);
-
-    this.messageManager = messageManager;
-
-    this.messageManager.addMessageListener("RemotePage:Message", this.message);
-  },
-
-  /* Adds a listener for messages. Many callbacks can be registered for the
-   * same message if necessary. An attempt to register the same callback for the
-   * same message twice will be ignored. When called the callback is passed an
-   * object with these properties:
-   *   target: This message port
-   *   name:   The message name
-   *   data:   Any data sent with the message
-   */
-  addMessageListener(name, callback) {
-    if (this.destroyed) {
-      throw new Error("Message port has been destroyed");
-    }
-
-    this.listener.addMessageListener(name, callback);
-  },
-
-  /*
-   * Removes a listener for messages.
-   */
-  removeMessageListener(name, callback) {
-    if (this.destroyed) {
-      throw new Error("Message port has been destroyed");
-    }
-
-    this.listener.removeMessageListener(name, callback);
-  },
-
-  // Sends a message asynchronously to the other process
-  sendAsyncMessage(name, data = null) {
-    if (this.destroyed) {
-      throw new Error("Message port has been destroyed");
-    }
-
-    this.messageManager.sendAsyncMessage("RemotePage:Message", {
-      portID: this.portID,
-      name,
-      data,
-    });
-  },
-
-  // Called to destroy this port
-  destroy() {
-    try {
-      // This can fail in the child process if the tab has already been closed
-      this.messageManager.removeMessageListener("RemotePage:Message", this.message);
-    } catch (e) { }
-    this.messageManager = null;
-    this.destroyed = true;
-    this.portID = null;
-    this.listener = null;
-  },
-
-  getBoolPref(aPref) {
-    let principal = this.window.document.nodePrincipal;
-    if (!RPMAccessManager.checkAllowAccess(principal, "getBoolPref", aPref)) {
-      throw new Error("RPMAccessManager does not allow access to getBoolPref");
-    }
-    return Services.prefs.getBoolPref(aPref);
-  },
-
-  setBoolPref(aPref, aVal) {
-    return new this.window.Promise(function(resolve) {
-      AsyncPrefs.set(aPref, aVal).then(function() {
-        resolve();
-      });
-    });
-  },
-
-  getFormatURLPref(aFormatURL) {
-    let principal = this.window.document.nodePrincipal;
-    if (!RPMAccessManager.checkAllowAccess(principal, "getFormatURLPref", aFormatURL)) {
-      throw new Error("RPMAccessManager does not allow access to getFormatURLPref");
-    }
-    return Services.urlFormatter.formatURLPref(aFormatURL);
-  },
-
-  isWindowPrivate() {
-    let principal = this.window.document.nodePrincipal;
-    if (!RPMAccessManager.checkAllowAccess(principal, "isWindowPrivate", "yes")) {
-      throw new Error("RPMAccessManager does not allow access to isWindowPrivate");
-    }
-    return PrivateBrowsingUtils.isContentWindowPrivate(this.window);
-  },
-};
-
-
-// The chome side of a message port
-function ChromeMessagePort(browser, portID, url) {
-  MessagePort.call(this, browser.messageManager, portID);
-
-  this._browser = browser;
-  this._permanentKey = browser.permanentKey;
-  this._url = url;
-
-  Services.obs.addObserver(this, "message-manager-disconnect");
-  this.publicPort = publicMessagePort(this);
-
-  this.swapBrowsers = this.swapBrowsers.bind(this);
-  this._browser.addEventListener("SwapDocShells", this.swapBrowsers);
-}
-
-ChromeMessagePort.prototype = Object.create(MessagePort.prototype);
-
-Object.defineProperty(ChromeMessagePort.prototype, "browser", {
-  get() {
-    return this._browser;
-  }
-});
-
-Object.defineProperty(ChromeMessagePort.prototype, "url", {
-  get() {
-    return this._url;
-  }
-});
-
-// Called when the docshell is being swapped with another browser. We have to
-// update to use the new browser's message manager
-ChromeMessagePort.prototype.swapBrowsers = function({ detail: newBrowser }) {
-  // We can see this event for the new browser before the swap completes so
-  // check that the browser we're tracking has our permanentKey.
-  if (this._browser.permanentKey != this._permanentKey)
-    return;
-
-  this._browser.removeEventListener("SwapDocShells", this.swapBrowsers);
-
-  this._browser = newBrowser;
-  this.swapMessageManager(newBrowser.messageManager);
-
-  this._browser.addEventListener("SwapDocShells", this.swapBrowsers);
-};
-
-// Called when a message manager has been disconnected indicating that the
-// tab has closed or crashed
-ChromeMessagePort.prototype.observe = function(messageManager) {
-  if (messageManager != this.messageManager)
-    return;
-
-  this.listener.callListeners({
-    target: this.publicPort,
-    name: "RemotePage:Unload",
-    data: null,
-  });
-  this.destroy();
-};
-
-// Called when a message is received from the message manager. This could
-// have come from any port in the message manager so verify the port ID.
-ChromeMessagePort.prototype.message = function({ data: messagedata }) {
-  if (this.destroyed || (messagedata.portID != this.portID)) {
-    return;
-  }
-
-  let message = {
-    target: this.publicPort,
-    name: messagedata.name,
-    data: messagedata.data,
-  };
-  this.listener.callListeners(message);
-
-  if (messagedata.name == "RemotePage:Unload")
-    this.destroy();
-};
-
-ChromeMessagePort.prototype.destroy = function() {
-  try {
-    this._browser.removeEventListener(
-        "SwapDocShells", this.swapBrowsers);
-  } catch (e) {
-    // It's possible the browser instance is already dead so we can just ignore
-    // this error.
-  }
-
-  this._browser = null;
-  Services.obs.removeObserver(this, "message-manager-disconnect");
-  MessagePort.prototype.destroy.call(this);
-};
-
+ChromeUtils.import("resource://gre/modules/remotepagemanager/MessagePort.jsm");
 
 // The content side of a message port
 function ChildMessagePort(contentFrame, window) {
   let portID = Services.appinfo.processID + ":" + ChildMessagePort.prototype.nextPortID++;
   MessagePort.call(this, contentFrame, portID);
 
   this.window = window;
 
@@ -576,112 +83,8 @@ ChildMessagePort.prototype.message = fun
   };
   this.listener.callListeners(Cu.cloneInto(message, this.window));
 };
 
 ChildMessagePort.prototype.destroy = function() {
   this.window = null;
   MessagePort.prototype.destroy.call(this);
 };
-
-// Allows callers to register to connect to specific content pages. Registration
-// is done through the addRemotePageListener method
-var RemotePageManagerInternal = {
-  // The currently registered remote pages
-  pages: new Map(),
-
-  // Initialises all the needed listeners
-  init() {
-    Services.mm.addMessageListener("RemotePage:InitPort", this.initPort.bind(this));
-    this.updateProcessUrls();
-  },
-
-  updateProcessUrls() {
-    Services.ppmm.initialProcessData["RemotePageManager:urls"] = Array.from(this.pages.keys());
-  },
-
-  // Registers interest in a remote page. A callback is called with a port for
-  // the new page when loading begins (i.e. the page hasn't actually loaded yet).
-  // Only one callback can be registered per URL.
-  addRemotePageListener(url, callback) {
-    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-      throw new Error("RemotePageManager can only be used in the main process.");
-
-    if (this.pages.has(url)) {
-      throw new Error("Remote page already registered: " + url);
-    }
-
-    this.pages.set(url, callback);
-    this.updateProcessUrls();
-
-    // Notify all the frame scripts of the new registration
-    Services.ppmm.broadcastAsyncMessage("RemotePage:Register", { urls: [url] });
-  },
-
-  // Removes any interest in a remote page.
-  removeRemotePageListener(url) {
-    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-      throw new Error("RemotePageManager can only be used in the main process.");
-
-    if (!this.pages.has(url)) {
-      throw new Error("Remote page is not registered: " + url);
-    }
-
-    // Notify all the frame scripts of the removed registration
-    Services.ppmm.broadcastAsyncMessage("RemotePage:Unregister", { urls: [url] });
-    this.pages.delete(url);
-    this.updateProcessUrls();
-  },
-
-  // A remote page has been created and a port is ready in the content side
-  initPort({ target: browser, data: { url, portID } }) {
-    let callback = this.pages.get(url);
-    if (!callback) {
-      Cu.reportError("Unexpected remote page load: " + url);
-      return;
-    }
-
-    let port = new ChromeMessagePort(browser, portID, url);
-    callback(port.publicPort);
-  }
-};
-
-if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-  RemotePageManagerInternal.init();
-
-// The public API for the above object
-var RemotePageManager = {
-  addRemotePageListener: RemotePageManagerInternal.addRemotePageListener.bind(RemotePageManagerInternal),
-  removeRemotePageListener: RemotePageManagerInternal.removeRemotePageListener.bind(RemotePageManagerInternal),
-};
-
-// Listen for pages in any process we're loaded in
-var registeredURLs = new Set(Services.cpmm.initialProcessData["RemotePageManager:urls"]);
-
-var observer = (window) => {
-  // Strip the hash from the URL, because it's not part of the origin.
-  let url = window.document.documentURI.replace(/[\#|\?].*$/, "");
-  if (!registeredURLs.has(url))
-    return;
-
-  // Get the frame message manager for this window so we can associate this
-  // page with a browser element
-  let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDocShell)
-                             .QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIContentFrameMessageManager);
-  // Set up the child side of the message port
-  new ChildMessagePort(messageManager, window);
-};
-Services.obs.addObserver(observer, "chrome-document-global-created");
-Services.obs.addObserver(observer, "content-document-global-created");
-
-// A message from chrome telling us what pages to listen for
-Services.cpmm.addMessageListener("RemotePage:Register", ({ data }) => {
-  for (let url of data.urls)
-    registeredURLs.add(url);
-});
-
-// A message from chrome telling us what pages to stop listening for
-Services.cpmm.addMessageListener("RemotePage:Unregister", ({ data }) => {
-  for (let url of data.urls)
-    registeredURLs.delete(url);
-});
copy from toolkit/modules/RemotePageManager.jsm
copy to toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
--- a/toolkit/modules/RemotePageManager.jsm
+++ b/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
@@ -1,133 +1,31 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["RemotePages", "RemotePageManager", "PageListener"];
+var EXPORTED_SYMBOLS = ["RemotePages", "RemotePageManager"];
 
 /*
  * Using the RemotePageManager:
  * * Create a new page listener by calling 'new RemotePages(URI)' which
  *   then injects functions like RPMGetBoolPref() into the registered page.
  *   One can then use those exported functions to communicate between
  *   child and parent.
  *
  * * When adding a new consumer of RPM that relies on other functionality
  *   then simple message passing provided by the RPM, then one has to
- *   whitelist permissions for the new URI within the RPMAccessManager.
- *
- * Please note that prefs that one wants to update need to be whitelisted
- * within AsyncPrefs.jsm.
+ *   whitelist permissions for the new URI within the RPMAccessManager
+ *   from MessagePort.jsm.
  */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.defineModuleGetter(this, "AsyncPrefs",
-  "resource://gre/modules/AsyncPrefs.jsm");
-ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
-  "resource://gre/modules/PrivateBrowsingUtils.jsm");
-
-/*
- * Used for all kinds of permissions checks which requires explicit
- * whitelisting of specific permissions granted through RPM.
- */
-let RPMAccessManager = {
-  accessMap: {
-    "about:privatebrowsing": {
-      // "sendAsyncMessage": handled within AboutPrivateBrowsingHandler.jsm
-      // "setBoolPref": handled within AsyncPrefs.jsm and uses the pref
-      //                ["privacy.trackingprotection.pbmode.enabled"],
-      "getBoolPref": ["privacy.trackingprotection.enabled",
-                      "privacy.trackingprotection.pbmode.enabled"],
-      "getFormatURLPref": ["privacy.trackingprotection.introURL",
-                           "app.support.baseURL"],
-      "isWindowPrivate": ["yes"],
-    },
-  },
-
-  checkAllowAccess(aPrincipal, aFeature, aValue) {
-    // if there is no content principal; deny access
-    if (!aPrincipal || !aPrincipal.URI) {
-      return false;
-    }
-    let uri = aPrincipal.URI.asciiSpec;
-
-    // check if there is an entry for that requestying URI in the accessMap;
-    // if not, deny access.
-    let accessMapForURI = this.accessMap[uri];
-    if (!accessMapForURI) {
-      Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
-      return false;
-    }
-
-    // check if the feature is allowed to be accessed for that URI;
-    // if not, deny access.
-    let accessMapForFeature = accessMapForURI[aFeature];
-    if (!accessMapForFeature) {
-      Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
-      return false;
-    }
-
-    // if the actual value is in the whitelist for that feature;
-    // allow access
-    if (accessMapForFeature.includes(aValue)) {
-      return true;
-    }
-
-    // otherwise deny access
-    Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
-    return false;
-  },
-};
-
-function MessageListener() {
-  this.listeners = new Map();
-}
-
-MessageListener.prototype = {
-  keys() {
-    return this.listeners.keys();
-  },
-
-  has(name) {
-    return this.listeners.has(name);
-  },
-
-  callListeners(message) {
-    let listeners = this.listeners.get(message.name);
-    if (!listeners) {
-      return;
-    }
-
-    for (let listener of listeners.values()) {
-      try {
-        listener(message);
-      } catch (e) {
-        Cu.reportError(e);
-      }
-    }
-  },
-
-  addMessageListener(name, callback) {
-    if (!this.listeners.has(name))
-      this.listeners.set(name, new Set([callback]));
-    else
-      this.listeners.get(name).add(callback);
-  },
-
-  removeMessageListener(name, callback) {
-    if (!this.listeners.has(name))
-      return;
-
-    this.listeners.get(name).delete(callback);
-  },
-};
-
+ChromeUtils.import("resource://gre/modules/remotepagemanager/MessagePort.jsm");
 
 /**
  * Creates a RemotePages object which listens for new remote pages of some
  * particular URLs. A "RemotePage:Init" message will be dispatched to this
  * object for every page loaded. Message listeners added to this object receive
  * messages from all loaded pages from the requested urls.
  */
 var RemotePages = function(urls) {
@@ -282,139 +180,16 @@ function publicMessagePort(port) {
         return port.url;
       }
     });
   }
 
   return clean;
 }
 
-
-/*
- * A message port sits on each side of the process boundary for every remote
- * page. Each has a port ID that is unique to the message manager it talks
- * through.
- *
- * We roughly implement the same contract as nsIMessageSender and
- * nsIMessageListenerManager
- */
-function MessagePort(messageManager, portID) {
-  this.messageManager = messageManager;
-  this.portID = portID;
-  this.destroyed = false;
-  this.listener = new MessageListener();
-
-  this.message = this.message.bind(this);
-  this.messageManager.addMessageListener("RemotePage:Message", this.message);
-}
-
-MessagePort.prototype = {
-  messageManager: null,
-  portID: null,
-  destroyed: null,
-  listener: null,
-  _browser: null,
-  remotePort: null,
-
-  // Called when the message manager used to connect to the other process has
-  // changed, i.e. when a tab is detached.
-  swapMessageManager(messageManager) {
-    this.messageManager.removeMessageListener("RemotePage:Message", this.message);
-
-    this.messageManager = messageManager;
-
-    this.messageManager.addMessageListener("RemotePage:Message", this.message);
-  },
-
-  /* Adds a listener for messages. Many callbacks can be registered for the
-   * same message if necessary. An attempt to register the same callback for the
-   * same message twice will be ignored. When called the callback is passed an
-   * object with these properties:
-   *   target: This message port
-   *   name:   The message name
-   *   data:   Any data sent with the message
-   */
-  addMessageListener(name, callback) {
-    if (this.destroyed) {
-      throw new Error("Message port has been destroyed");
-    }
-
-    this.listener.addMessageListener(name, callback);
-  },
-
-  /*
-   * Removes a listener for messages.
-   */
-  removeMessageListener(name, callback) {
-    if (this.destroyed) {
-      throw new Error("Message port has been destroyed");
-    }
-
-    this.listener.removeMessageListener(name, callback);
-  },
-
-  // Sends a message asynchronously to the other process
-  sendAsyncMessage(name, data = null) {
-    if (this.destroyed) {
-      throw new Error("Message port has been destroyed");
-    }
-
-    this.messageManager.sendAsyncMessage("RemotePage:Message", {
-      portID: this.portID,
-      name,
-      data,
-    });
-  },
-
-  // Called to destroy this port
-  destroy() {
-    try {
-      // This can fail in the child process if the tab has already been closed
-      this.messageManager.removeMessageListener("RemotePage:Message", this.message);
-    } catch (e) { }
-    this.messageManager = null;
-    this.destroyed = true;
-    this.portID = null;
-    this.listener = null;
-  },
-
-  getBoolPref(aPref) {
-    let principal = this.window.document.nodePrincipal;
-    if (!RPMAccessManager.checkAllowAccess(principal, "getBoolPref", aPref)) {
-      throw new Error("RPMAccessManager does not allow access to getBoolPref");
-    }
-    return Services.prefs.getBoolPref(aPref);
-  },
-
-  setBoolPref(aPref, aVal) {
-    return new this.window.Promise(function(resolve) {
-      AsyncPrefs.set(aPref, aVal).then(function() {
-        resolve();
-      });
-    });
-  },
-
-  getFormatURLPref(aFormatURL) {
-    let principal = this.window.document.nodePrincipal;
-    if (!RPMAccessManager.checkAllowAccess(principal, "getFormatURLPref", aFormatURL)) {
-      throw new Error("RPMAccessManager does not allow access to getFormatURLPref");
-    }
-    return Services.urlFormatter.formatURLPref(aFormatURL);
-  },
-
-  isWindowPrivate() {
-    let principal = this.window.document.nodePrincipal;
-    if (!RPMAccessManager.checkAllowAccess(principal, "isWindowPrivate", "yes")) {
-      throw new Error("RPMAccessManager does not allow access to isWindowPrivate");
-    }
-    return PrivateBrowsingUtils.isContentWindowPrivate(this.window);
-  },
-};
-
-
 // The chome side of a message port
 function ChromeMessagePort(browser, portID, url) {
   MessagePort.call(this, browser.messageManager, portID);
 
   this._browser = browser;
   this._permanentKey = browser.permanentKey;
   this._url = url;
 
@@ -497,141 +272,51 @@ ChromeMessagePort.prototype.destroy = fu
   }
 
   this._browser = null;
   Services.obs.removeObserver(this, "message-manager-disconnect");
   MessagePort.prototype.destroy.call(this);
 };
 
 
-// The content side of a message port
-function ChildMessagePort(contentFrame, window) {
-  let portID = Services.appinfo.processID + ":" + ChildMessagePort.prototype.nextPortID++;
-  MessagePort.call(this, contentFrame, portID);
-
-  this.window = window;
-
-  // Add functionality to the content page
-  Cu.exportFunction(this.sendAsyncMessage.bind(this), window, {
-    defineAs: "RPMSendAsyncMessage",
-  });
-  Cu.exportFunction(this.addMessageListener.bind(this), window, {
-    defineAs: "RPMAddMessageListener",
-    allowCallbacks: true,
-  });
-  Cu.exportFunction(this.removeMessageListener.bind(this), window, {
-    defineAs: "RPMRemoveMessageListener",
-    allowCallbacks: true,
-  });
-  Cu.exportFunction(this.getBoolPref.bind(this), window, {
-    defineAs: "RPMGetBoolPref",
-  });
-  Cu.exportFunction(this.setBoolPref.bind(this), window, {
-    defineAs: "RPMSetBoolPref",
-  });
-  Cu.exportFunction(this.getFormatURLPref.bind(this), window, {
-    defineAs: "RPMGetFormatURLPref",
-  });
-  Cu.exportFunction(this.isWindowPrivate.bind(this), window, {
-    defineAs: "RPMIsWindowPrivate",
-  });
-
-  // Send a message for load events
-  let loadListener = () => {
-    this.sendAsyncMessage("RemotePage:Load");
-    window.removeEventListener("load", loadListener);
-  };
-  window.addEventListener("load", loadListener);
-
-  // Destroy the port when the window is unloaded
-  window.addEventListener("unload", () => {
-    try {
-      this.sendAsyncMessage("RemotePage:Unload");
-    } catch (e) {
-      // If the tab has been closed the frame message manager has already been
-      // destroyed
-    }
-    this.destroy();
-  });
-
-  // Tell the main process to set up its side of the message pipe.
-  this.messageManager.sendAsyncMessage("RemotePage:InitPort", {
-    portID,
-    url: window.document.documentURI.replace(/[\#|\?].*$/, ""),
-  });
-}
-
-ChildMessagePort.prototype = Object.create(MessagePort.prototype);
-
-ChildMessagePort.prototype.nextPortID = 0;
-
-// Called when a message is received from the message manager. This could
-// have come from any port in the message manager so verify the port ID.
-ChildMessagePort.prototype.message = function({ data: messagedata }) {
-  if (this.destroyed || (messagedata.portID != this.portID)) {
-    return;
-  }
-
-  let message = {
-    name: messagedata.name,
-    data: messagedata.data,
-  };
-  this.listener.callListeners(Cu.cloneInto(message, this.window));
-};
-
-ChildMessagePort.prototype.destroy = function() {
-  this.window = null;
-  MessagePort.prototype.destroy.call(this);
-};
-
 // Allows callers to register to connect to specific content pages. Registration
 // is done through the addRemotePageListener method
 var RemotePageManagerInternal = {
   // The currently registered remote pages
   pages: new Map(),
 
   // Initialises all the needed listeners
   init() {
     Services.mm.addMessageListener("RemotePage:InitPort", this.initPort.bind(this));
     this.updateProcessUrls();
   },
 
   updateProcessUrls() {
-    Services.ppmm.initialProcessData["RemotePageManager:urls"] = Array.from(this.pages.keys());
+    Services.ppmm.sharedData.set("RemotePageManager:urls", new Set(this.pages.keys()));
+    Services.ppmm.sharedData.flush();
   },
 
   // Registers interest in a remote page. A callback is called with a port for
   // the new page when loading begins (i.e. the page hasn't actually loaded yet).
   // Only one callback can be registered per URL.
   addRemotePageListener(url, callback) {
-    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-      throw new Error("RemotePageManager can only be used in the main process.");
-
     if (this.pages.has(url)) {
       throw new Error("Remote page already registered: " + url);
     }
 
     this.pages.set(url, callback);
     this.updateProcessUrls();
-
-    // Notify all the frame scripts of the new registration
-    Services.ppmm.broadcastAsyncMessage("RemotePage:Register", { urls: [url] });
   },
 
   // Removes any interest in a remote page.
   removeRemotePageListener(url) {
-    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-      throw new Error("RemotePageManager can only be used in the main process.");
-
     if (!this.pages.has(url)) {
       throw new Error("Remote page is not registered: " + url);
     }
 
-    // Notify all the frame scripts of the removed registration
-    Services.ppmm.broadcastAsyncMessage("RemotePage:Unregister", { urls: [url] });
     this.pages.delete(url);
     this.updateProcessUrls();
   },
 
   // A remote page has been created and a port is ready in the content side
   initPort({ target: browser, data: { url, portID } }) {
     let callback = this.pages.get(url);
     if (!callback) {
@@ -639,49 +324,20 @@ var RemotePageManagerInternal = {
       return;
     }
 
     let port = new ChromeMessagePort(browser, portID, url);
     callback(port.publicPort);
   }
 };
 
-if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-  RemotePageManagerInternal.init();
+if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+  throw new Error("RemotePageManager can only be used in the main process.");
+}
+
+RemotePageManagerInternal.init();
 
 // The public API for the above object
 var RemotePageManager = {
   addRemotePageListener: RemotePageManagerInternal.addRemotePageListener.bind(RemotePageManagerInternal),
   removeRemotePageListener: RemotePageManagerInternal.removeRemotePageListener.bind(RemotePageManagerInternal),
 };
 
-// Listen for pages in any process we're loaded in
-var registeredURLs = new Set(Services.cpmm.initialProcessData["RemotePageManager:urls"]);
-
-var observer = (window) => {
-  // Strip the hash from the URL, because it's not part of the origin.
-  let url = window.document.documentURI.replace(/[\#|\?].*$/, "");
-  if (!registeredURLs.has(url))
-    return;
-
-  // Get the frame message manager for this window so we can associate this
-  // page with a browser element
-  let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDocShell)
-                             .QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIContentFrameMessageManager);
-  // Set up the child side of the message port
-  new ChildMessagePort(messageManager, window);
-};
-Services.obs.addObserver(observer, "chrome-document-global-created");
-Services.obs.addObserver(observer, "content-document-global-created");
-
-// A message from chrome telling us what pages to listen for
-Services.cpmm.addMessageListener("RemotePage:Register", ({ data }) => {
-  for (let url of data.urls)
-    registeredURLs.add(url);
-});
-
-// A message from chrome telling us what pages to stop listening for
-Services.cpmm.addMessageListener("RemotePage:Unregister", ({ data }) => {
-  for (let url of data.urls)
-    registeredURLs.delete(url);
-});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/remotepagemanager/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files('**'):
+    BUG_COMPONENT = ('Toolkit', 'General')
+
+EXTRA_JS_MODULES.remotepagemanager = [
+    'MessagePort.jsm',
+    'RemotePageManagerChild.jsm',
+    'RemotePageManagerParent.jsm',
+]
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
copy from toolkit/modules/tests/browser/.eslintrc.js
copy to toolkit/components/remotepagemanager/tests/browser/.eslintrc.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/remotepagemanager/tests/browser/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+  testremotepagemanager.html
+  testremotepagemanager2.html
+
+[browser_RemotePageManager.js]
rename from toolkit/modules/tests/browser/browser_RemotePageManager.js
rename to toolkit/components/remotepagemanager/tests/browser/browser_RemotePageManager.js
--- a/toolkit/modules/tests/browser/browser_RemotePageManager.js
+++ b/toolkit/components/remotepagemanager/tests/browser/browser_RemotePageManager.js
@@ -1,15 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
-const TEST_URL = "http://www.example.com/browser/toolkit/modules/tests/browser/testremotepagemanager.html";
+const TEST_URL = "http://www.example.com/browser/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager.html";
 
-var { RemotePages, RemotePageManager } = ChromeUtils.import("resource://gre/modules/RemotePageManager.jsm", {});
+var { RemotePages, RemotePageManager } =
+  ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm", {});
 
 function failOnMessage(message) {
   ok(false, "Should not have seen message " + message.name);
 }
 
 function waitForMessage(port, message, expectedPort = port) {
   return new Promise((resolve) => {
     function listener(message) {
@@ -54,19 +55,19 @@ function swapDocShells(browser1, browser
   browser1.swapDocShells(browser2);
 
   // Swap permanentKeys.
   let tmp = browser1.permanentKey;
   browser1.permanentKey = browser2.permanentKey;
   browser2.permanentKey = tmp;
 }
 
-add_task(async function initialProcessData() {
+add_task(async function sharedData_aka_initialProcessData() {
   const includesTest = () => Services.cpmm.
-    initialProcessData["RemotePageManager:urls"].includes(TEST_URL);
+    sharedData.get("RemotePageManager:urls").has(TEST_URL);
   is(includesTest(), false, "Shouldn't have test url in initial process data yet");
 
   const loadedPort = waitForPort(TEST_URL);
   is(includesTest(), true, "Should have test url when waiting for it to load");
 
   await loadedPort;
   is(includesTest(), false, "Should have test url removed when done listening");
 
rename from toolkit/modules/tests/browser/testremotepagemanager.html
rename to toolkit/components/remotepagemanager/tests/browser/testremotepagemanager.html
rename from toolkit/modules/tests/browser/testremotepagemanager2.html
rename to toolkit/components/remotepagemanager/tests/browser/testremotepagemanager2.html
--- a/toolkit/content/process-content.js
+++ b/toolkit/content/process-content.js
@@ -2,61 +2,88 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // Creates a new PageListener for this process. This will listen for page loads
 // and for those that match URLs provided by the parent process will set up
 // a dedicated message port and notify the parent process.
-ChromeUtils.import("resource://gre/modules/RemotePageManager.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
 
 Services.cpmm.addMessageListener("gmp-plugin-crash", msg => {
   let gmpservice = Cc["@mozilla.org/gecko-media-plugin-service;1"]
                      .getService(Ci.mozIGeckoMediaPluginService);
 
   gmpservice.RunPluginCrashCallbacks(msg.data.pluginID, msg.data.pluginName);
 });
 
+let TOPICS = [
+  "chrome-document-global-created",
+  "content-document-global-created",
+];
+
 if (gInContentProcess) {
-  let ProcessObserver = {
-    TOPICS: [
-      "inner-window-destroyed",
-      "xpcom-shutdown",
-    ],
+  TOPICS.push("inner-window-destroyed");
+  TOPICS.push("xpcom-shutdown");
+}
+
+let ProcessObserver = {
+  init() {
+    for (let topic of TOPICS) {
+      Services.obs.addObserver(this, topic);
+    }
+  },
 
-    init() {
-      for (let topic of this.TOPICS) {
-        Services.obs.addObserver(this, topic);
-      }
-    },
+  uninit() {
+    for (let topic of TOPICS) {
+      Services.obs.removeObserver(this, topic);
+    }
+  },
 
-    uninit() {
-      for (let topic of this.TOPICS) {
-        Services.obs.removeObserver(this, topic);
-      }
-    },
+  observe(subject, topic, data) {
+    switch (topic) {
+      case "chrome-document-global-created":
+      case "content-document-global-created": {
+
+        // Strip the hash from the URL, because it's not part of the origin.
+        let window = subject;
+        let url = window.document.documentURI.replace(/[\#|\?].*$/, "");
+
+        let registeredURLs = Services.cpmm.sharedData.get("RemotePageManager:urls");
 
-    observe(subject, topic, data) {
-      switch (topic) {
-        case "inner-window-destroyed": {
-          // Forward inner-window-destroyed notifications with the
-          // inner window ID, so that code in the parent that should
-          // do something when content windows go away can do it
-          let innerWindowID =
-            subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-          Services.cpmm.sendAsyncMessage("Toolkit:inner-window-destroyed",
-                                         innerWindowID);
-          break;
-        }
-        case "xpcom-shutdown": {
-          this.uninit();
-          break;
-        }
+        if (!registeredURLs.has(url))
+          return;
+
+        // Get the frame message manager for this window so we can associate this
+        // page with a browser element
+        let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                                   .getInterface(Ci.nsIDocShell)
+                                   .QueryInterface(Ci.nsIInterfaceRequestor)
+                                   .getInterface(Ci.nsIContentFrameMessageManager);
+
+        let { ChildMessagePort } =
+          ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerChild.jsm", {});
+        // Set up the child side of the message port
+        new ChildMessagePort(messageManager, window);
+        break;
       }
-    },
-  };
+      case "inner-window-destroyed": {
+        // Forward inner-window-destroyed notifications with the
+        // inner window ID, so that code in the parent that should
+        // do something when content windows go away can do it
+        let innerWindowID =
+          subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+        Services.cpmm.sendAsyncMessage("Toolkit:inner-window-destroyed",
+                                       innerWindowID);
+        break;
+      }
+      case "xpcom-shutdown": {
+        this.uninit();
+        break;
+      }
+    }
+  },
+};
 
-  ProcessObserver.init();
-}
+ProcessObserver.init();
--- a/toolkit/modules/LightweightThemeConsumer.jsm
+++ b/toolkit/modules/LightweightThemeConsumer.jsm
@@ -178,17 +178,17 @@ LightweightThemeConsumer.prototype = {
   },
 
   _update(aData) {
     this._lastData = aData;
     if (aData) {
       aData = LightweightThemeImageOptimizer.optimize(aData, this._win.screen);
     }
 
-    let active = this._active = !!aData && aData.id !== DEFAULT_THEME_ID;
+    let active = this._active = aData && aData.id !== DEFAULT_THEME_ID;
 
     if (!aData) {
       aData = {};
     }
 
     let root = this._doc.documentElement;
 
     if (active && aData.headerURL) {
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -229,17 +229,16 @@ EXTRA_JS_MODULES += [
     'PrivateBrowsingUtils.jsm',
     'ProfileAge.jsm',
     'Promise-backend.js',
     'Promise.jsm',
     'PromiseMessage.jsm',
     'PromiseUtils.jsm',
     'RemoteController.js',
     'RemoteFinder.jsm',
-    'RemotePageManager.jsm',
     'RemoteSecurityUI.jsm',
     'RemoteWebProgress.jsm',
     'ResetProfile.jsm',
     'ResponsivenessMonitor.jsm',
     'SelectContentHelper.jsm',
     'SelectionSourceContent.jsm',
     'SelectParentHelper.jsm',
     'ServiceRequest.jsm',
--- a/toolkit/modules/tests/browser/browser.ini
+++ b/toolkit/modules/tests/browser/browser.ini
@@ -1,14 +1,12 @@
 [DEFAULT]
 support-files =
   dummy_page.html
   metadata_*.html
-  testremotepagemanager.html
-  testremotepagemanager2.html
   file_FinderIframeTest.html
   file_FinderSample.html
   file_WebNavigation_page1.html
   file_WebNavigation_page2.html
   file_WebNavigation_page3.html
   file_WebRequest_page1.html
   file_WebRequest_page2.html
   file_getSelectionDetails_inputs.html
@@ -50,10 +48,9 @@ skip-if = debug || os = "linux"
 skip-if = true # Superseded by WebExtension tests
 [browser_WebRequest.js]
 skip-if = (verify && debug && (os == 'mac'))
 [browser_WebRequest_ancestors.js]
 [browser_WebRequest_cookies.js]
 [browser_WebRequest_filtering.js]
 [browser_PageMetadata.js]
 [browser_PromiseMessage.js]
-[browser_RemotePageManager.js]
 [browser_Troubleshoot.js]
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -816,17 +816,18 @@ var AddonManagerInternal = {
       // If this is a new profile just pretend that there were no changes
       if (appChanged === undefined) {
         for (let type in this.startupChanges)
           delete this.startupChanges[type];
       }
 
       // Support for remote about:plugins. Note that this module isn't loaded
       // at the top because Services.appinfo is defined late in tests.
-      let { RemotePages } = ChromeUtils.import("resource://gre/modules/RemotePageManager.jsm", {});
+      let { RemotePages } =
+        ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm", {});
 
       gPluginPageListener = new RemotePages("about:plugins");
       gPluginPageListener.addMessageListener("RequestPlugins", this.requestPlugins);
 
       gStartupComplete = true;
       this.recordTimestamp("AMI_startup_end");
     } catch (e) {
       logger.error("startup failed", e);
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -119,16 +119,17 @@
   "logmanager.js": ["LogManager"],
   "lz4.js": ["Lz4"],
   "lz4_internal.js": ["Primitives"],
   "main.js": ["Weave"],
   "MatchURLFilters.jsm": ["MatchURLFilters"],
   "mcc_iso3166_table.jsm": ["MCC_ISO3166_TABLE"],
   "message.js": ["Command", "Message", "MessageOrigin", "Response"],
   "MessageContext.jsm": ["MessageContext"],
+  "MessagePort.jsm": ["MessagePort", "MessageListener"],
   "Messaging.jsm": ["sendMessageToJava", "Messaging", "EventDispatcher"],
   "microformat-shiv.js": ["Microformats"],
   "MigrationUtils.jsm": ["MigrationUtils", "MigratorPrototype"],
   "MulticastDNSAndroid.jsm": ["MulticastDNS"],
   "NativeMessaging.jsm": ["NativeApp"],
   "NotificationDB.jsm": [],
   "nsFormAutoCompleteResult.jsm": ["FormAutoCompleteResult"],
   "observers.js": ["Observers"],
@@ -162,17 +163,18 @@
   "PushCrypto.jsm": ["PushCrypto", "concatArray"],
   "quit.js": ["goQuitApplication"],
   "Readability.js": ["Readability"],
   "record.js": ["WBORecord", "RecordManager", "CryptoWrapper", "CollectionKeyManager", "Collection"],
   "recursive_importA.jsm": ["foo", "bar"],
   "recursive_importB.jsm": ["baz", "qux"],
   "reflect.jsm": ["Reflect"],
   "RemoteFinder.jsm": ["RemoteFinder", "RemoteFinderListener"],
-  "RemotePageManager.jsm": ["RemotePages", "RemotePageManager", "PageListener"],
+  "RemotePageManagerChild.jsm": ["ChildMessagePort"],
+  "RemotePageManagerParent.jsm": ["RemotePages", "RemotePageManager"],
   "RemoteWebProgress.jsm": ["RemoteWebProgressManager"],
   "resource.js": ["AsyncResource", "Resource"],
   "rest.js": ["RESTRequest", "RESTResponse", "TokenAuthenticatedRESTRequest"],
   "rotaryengine.js": ["RotaryEngine", "RotaryRecord", "RotaryStore", "RotaryTracker"],
   "require.js": ["require"],
   "RTCStatsReport.jsm": ["convertToRTCStatsReport"],
   "Schemas.jsm": ["SchemaRoot", "Schemas"],
   "scratchpad-manager.jsm": ["ScratchpadManager"],
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -5308,18 +5308,19 @@ nsWindow::ProcessMessage(UINT msg, WPARA
         fontEnum->UpdateFontList(&didChange);
         ForceFontUpdate();
       } //if (NS_SUCCEEDED(rv))
     }
     break;
 
     case WM_SETTINGCHANGE:
     {
-      if (wParam == SPI_SETKEYBOARDDELAY) {
-        // CaretBlinkTime is cached in nsLookAndFeel
+      if (wParam == SPI_SETCLIENTAREAANIMATION ||
+          // CaretBlinkTime is cached in nsLookAndFeel
+          wParam == SPI_SETKEYBOARDDELAY) {
         NotifyThemeChanged();
         break;
       }
       if (lParam) {
         auto lParamString = reinterpret_cast<const wchar_t*>(lParam);
         if (!wcscmp(lParamString, L"ImmersiveColorSet")) {
           // This might be the Win10 dark mode setting; only way to tell
           // is to actually force a theme change, since we don't get