Merge mozilla-central to autoland. a=merge CLOSED TREE
authorMargareta Eliza Balazs <ebalazs@mozilla.com>
Thu, 18 Oct 2018 13:25:55 +0300
changeset 490243 704e640a83c2a829f674ac4da73faec6fb1c04d7
parent 490242 695366afa355ad61c17dedd9498ac2feba7b0ecd (current diff)
parent 490220 8f709fd4aa463ecfc38deda95ac9cc68b5095356 (diff)
child 490244 f2d7d033c14fe7a9e94c6c5ddec4e5dcf165be7c
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersmerge
milestone64.0a1
Merge mozilla-central to autoland. a=merge CLOSED TREE
browser/extensions/formautofill/OSKeyStore.jsm
browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
browser/extensions/formautofill/test/fixtures/OSKeyStoreTestUtils.jsm
browser/extensions/formautofill/test/unit/test_osKeyStore.js
testing/web-platform/meta/infrastructure/reftest/reftest_or_0.html.ini
testing/web-platform/meta/svg/shapes/ellipse-04.svg.ini
testing/web-platform/meta/wasm/idlharness.any.js.ini
testing/web-platform/tests/wasm/idlharness.any.js
--- a/browser/app/firefox.exe.manifest
+++ b/browser/app/firefox.exe.manifest
@@ -14,16 +14,26 @@
                         name="Microsoft.Windows.Common-Controls"
                         version="6.0.0.0"
                         processorArchitecture="*"
                         publicKeyToken="6595b64144ccf1df"
                         language="*"
                 />
         </dependentAssembly>
 </dependency>
+<dependency>
+        <dependentAssembly>
+                <assemblyIdentity
+                        type="win32"
+                        name="mozglue"
+                        version="1.0.0.0"
+                        language="*"
+                />
+        </dependentAssembly>
+</dependency>
 <ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
   <ms_asmv3:security>
     <ms_asmv3:requestedPrivileges>
       <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
     </ms_asmv3:requestedPrivileges>
   </ms_asmv3:security>
 </ms_asmv3:trustInfo>
   <ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -11,18 +11,18 @@
 
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "OSKeyStore",
-                               "resource://formautofill/OSKeyStore.jsm");
+ChromeUtils.defineModuleGetter(this, "MasterPassword",
+                               "resource://formautofill/MasterPassword.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "formAutofillStorage", () => {
   let storage;
   try {
     storage = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {})
                          .formAutofillStorage;
@@ -144,30 +144,30 @@ var paymentDialogWrapper = {
     });
 
     return address;
   },
 
   /**
    * @param {string} guid The GUID of the basic card record from storage.
    * @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
-   * @throws If there is an error decrypting
+   * @throws if the user cancels entering their master password or an error decrypting
    * @returns {nsIBasicCardResponseData?} returns response data or null (if the
    *                                      master password dialog was cancelled);
    */
   async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) {
     let cardData = this.temporaryStore.creditCards.get(guid) ||
                    await formAutofillStorage.creditCards.get(guid);
     if (!cardData) {
       throw new Error(`Basic card not found in storage: ${guid}`);
     }
 
     let cardNumber;
     try {
-      cardNumber = await OSKeyStore.decrypt(cardData["cc-number-encrypted"], true);
+      cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
     } catch (ex) {
       if (ex.result != Cr.NS_ERROR_ABORT) {
         throw ex;
       }
       // User canceled master password entry
       return null;
     }
 
@@ -498,26 +498,18 @@ var paymentDialogWrapper = {
     window.close();
   },
 
   async onPay({
     selectedPayerAddressGUID: payerGUID,
     selectedPaymentCardGUID: paymentCardGUID,
     selectedPaymentCardSecurityCode: cardSecurityCode,
   }) {
-    let methodData;
-    try {
-      methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
-                                                                          cardSecurityCode);
-    } catch (ex) {
-      // TODO (Bug 1498403): Some kind of "credit card storage error" here, perhaps asking user
-      // to re-enter credit card # from management UI.
-      Cu.reportError(ex);
-      return;
-    }
+    let methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
+                                                                            cardSecurityCode);
 
     if (!methodData) {
       // TODO (Bug 1429265/Bug 1429205): Handle when a user hits cancel on the
       // Master Password dialog.
       Cu.reportError("Bug 1429265/Bug 1429205: User canceled master password entry");
       return;
     }
 
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -313,17 +313,17 @@ let DUPED_ADDRESSES = {
 };
 
 let BASIC_CARDS_1 = {
   "53f9d009aed2": {
     billingAddressGUID: "68gjdh354j",
     methodName: "basic-card",
     "cc-number": "************5461",
     "guid": "53f9d009aed2",
-    "version": 2,
+    "version": 1,
     "timeCreated": 1505240896213,
     "timeLastModified": 1515609524588,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "John Smith",
     "cc-exp-month": 6,
     "cc-exp-year": 2024,
     "cc-type": "visa",
@@ -331,17 +331,17 @@ let BASIC_CARDS_1 = {
     "cc-additional-name": "",
     "cc-family-name": "Smith",
     "cc-exp": "2024-06",
   },
   "9h5d4h6f4d1s": {
     methodName: "basic-card",
     "cc-number": "************0954",
     "guid": "9h5d4h6f4d1s",
-    "version": 2,
+    "version": 1,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "Jane Doe",
     "cc-exp-month": 5,
     "cc-exp-year": 2023,
     "cc-type": "mastercard",
@@ -349,17 +349,17 @@ let BASIC_CARDS_1 = {
     "cc-additional-name": "",
     "cc-family-name": "Doe",
     "cc-exp": "2023-05",
   },
   "123456789abc": {
     methodName: "basic-card",
     "cc-number": "************1234",
     "guid": "123456789abc",
-    "version": 2,
+    "version": 1,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "Jane Fields",
     "cc-given-name": "Jane",
     "cc-additional-name": "",
     "cc-family-name": "Fields",
@@ -383,17 +383,17 @@ let BASIC_CARDS_1 = {
     "cc-exp-month": 6,
     "cc-exp-year": 2023,
     "cc-exp": "2023-06",
   },
   "missing-cc-name": {
     methodName: "basic-card",
     "cc-number": "************8563",
     "guid": "missing-cc-name",
-    "version": 2,
+    "version": 1,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-exp-month": 8,
     "cc-exp-year": 2024,
     "cc-exp": "2024-08",
   },
--- a/browser/config/mozconfigs/win32/mingwclang
+++ b/browser/config/mozconfigs/win32/mingwclang
@@ -25,16 +25,17 @@ unset MAKECAB
 #    CARGO
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 # MinGW Stuff
 ac_add_options --target=i686-w64-mingw32
 ac_add_options --with-toolchain-prefix=i686-w64-mingw32-
 
 ac_add_options --disable-warnings-as-errors
+MOZ_COPY_PDBS=1
 
 # Temporary config settings until we get these working on mingw
 ac_add_options --disable-accessibility # https://sourceforge.net/p/mingw-w64/bugs/648/
 
 # For now, we'll disable the sandbox, until we get get Bug 1461421 figured out
 ac_add_options --disable-sandbox
 
 # These aren't supported on mingw at this time
--- a/browser/config/mozconfigs/win64/mingwclang
+++ b/browser/config/mozconfigs/win64/mingwclang
@@ -25,16 +25,17 @@ unset MAKECAB
 #    CARGO
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 # MinGW Stuff
 ac_add_options --target=x86_64-w64-mingw32
 ac_add_options --with-toolchain-prefix=x86_64-w64-mingw32-
 
 ac_add_options --disable-warnings-as-errors
+MOZ_COPY_PDBS=1
 
 # Temporary config settings until we get these working on mingw
 ac_add_options --disable-accessibility # https://sourceforge.net/p/mingw-w64/bugs/648/
 
 # For now, we'll disable the sandbox, until we get get Bug 1461421 figured out
 ac_add_options --disable-sandbox
 
 # These aren't supported on mingw at this time
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -37,17 +37,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   CreditCard: "resource://gre/modules/CreditCard.jsm",
   FormAutofillPreferences: "resource://formautofill/FormAutofillPreferences.jsm",
   FormAutofillDoorhanger: "resource://formautofill/FormAutofillDoorhanger.jsm",
   FormAutofillUtils: "resource://formautofill/FormAutofillUtils.jsm",
-  OSKeyStore: "resource://formautofill/OSKeyStore.jsm",
+  MasterPassword: "resource://formautofill/MasterPassword.jsm",
 });
 
 this.log = null;
 FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
 
 const {
   ENABLED_AUTOFILL_ADDRESSES_PREF,
   ENABLED_AUTOFILL_CREDITCARDS_PREF,
@@ -220,18 +220,18 @@ FormAutofillParent.prototype = {
         if (data.guid) {
           await this.formAutofillStorage.addresses.update(data.guid, data.address);
         } else {
           await this.formAutofillStorage.addresses.add(data.address);
         }
         break;
       }
       case "FormAutofill:SaveCreditCard": {
-        if (!await OSKeyStore.ensureLoggedIn()) {
-          log.warn("User canceled encryption login");
+        if (!await MasterPassword.ensureLoggedIn()) {
+          log.warn("User canceled master password entry");
           return;
         }
         await this.formAutofillStorage.creditCards.add(data.creditcard);
         break;
       }
       case "FormAutofill:RemoveAddresses": {
         data.guids.forEach(guid => this.formAutofillStorage.addresses.remove(guid));
         break;
@@ -248,22 +248,22 @@ FormAutofillParent.prototype = {
         const win = BrowserWindowTracker.getTopWindow();
         win.openPreferences("privacy-form-autofill", {origin: "autofillFooter"});
         break;
       }
       case "FormAutofill:GetDecryptedString": {
         let {cipherText, reauth} = data;
         let string;
         try {
-          string = await OSKeyStore.decrypt(cipherText, reauth);
+          string = await MasterPassword.decrypt(cipherText, reauth);
         } catch (e) {
           if (e.result != Cr.NS_ERROR_ABORT) {
             throw e;
           }
-          log.warn("User canceled encryption login");
+          log.warn("User canceled master password entry");
         }
         target.sendAsyncMessage("FormAutofill:DecryptedString", string);
         break;
       }
     }
   },
 
   /**
@@ -287,17 +287,17 @@ FormAutofillParent.prototype = {
       Services.ppmm.removeMessageListener("FormAutofill:GetDecryptedString", this);
       Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
     }
   },
 
   /**
    * Get the records from profile store and return results back to content
    * process. It will decrypt the credit card number and append
-   * "cc-number-decrypted" to each record if OSKeyStore isn't set.
+   * "cc-number-decrypted" to each record if MasterPassword isn't set.
    *
    * @private
    * @param  {string} data.collectionName
    *         The name used to specify which collection to retrieve records.
    * @param  {string} data.searchString
    *         The typed string for filtering out the matched records.
    * @param  {string} data.info
    *         The input autocomplete property's information.
@@ -312,33 +312,44 @@ FormAutofillParent.prototype = {
     }
 
     let recordsInCollection = await collection.getAll();
     if (!info || !info.fieldName || !recordsInCollection.length) {
       target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
       return;
     }
 
-    let isCC = collectionName == CREDITCARDS_COLLECTION_NAME;
-    // We don't filter "cc-number"
-    if (isCC && info.fieldName == "cc-number") {
+    let isCCAndMPEnabled = collectionName == CREDITCARDS_COLLECTION_NAME && MasterPassword.isEnabled;
+    // We don't filter "cc-number" when MasterPassword is set.
+    if (isCCAndMPEnabled && info.fieldName == "cc-number") {
       recordsInCollection = recordsInCollection.filter(record => !!record["cc-number"]);
       target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
       return;
     }
 
     let records = [];
     let lcSearchString = searchString.toLowerCase();
 
     for (let record of recordsInCollection) {
       let fieldValue = record[info.fieldName];
       if (!fieldValue) {
         continue;
       }
 
+      // Cache the decrypted "cc-number" in each record for content to preview
+      // when MasterPassword isn't set.
+      if (!isCCAndMPEnabled && record["cc-number-encrypted"]) {
+        record["cc-number-decrypted"] = await MasterPassword.decrypt(record["cc-number-encrypted"]);
+      }
+
+      // Filter "cc-number" based on the decrypted one.
+      if (info.fieldName == "cc-number") {
+        fieldValue = record["cc-number-decrypted"];
+      }
+
       if (collectionName == ADDRESSES_COLLECTION_NAME && record.country
           && !FormAutofill.supportedCountries.includes(record.country)) {
         // Address autofill isn't supported for the record's country so we don't
         // want to attempt to potentially incorrectly fill the address fields.
         continue;
       }
 
       if (lcSearchString && !String(fieldValue).toLowerCase().startsWith(lcSearchString)) {
@@ -522,18 +533,18 @@ FormAutofillParent.prototype = {
         return;
       }
 
       if (state == "disable") {
         Services.prefs.setBoolPref("extensions.formautofill.creditCards.enabled", false);
         return;
       }
 
-      if (!await OSKeyStore.ensureLoggedIn()) {
-        log.warn("User canceled encryption login");
+      if (!await MasterPassword.ensureLoggedIn()) {
+        log.warn("User canceled master password entry");
         return;
       }
 
       let changedGUIDs = [];
       if (creditCard.guid) {
         if (state == "update") {
           await this.formAutofillStorage.creditCards.update(creditCard.guid, creditCard.record, true);
           changedGUIDs.push(creditCard.guid);
--- a/browser/extensions/formautofill/FormAutofillStorage.jsm
+++ b/browser/extensions/formautofill/FormAutofillStorage.jsm
@@ -137,36 +137,33 @@ ChromeUtils.import("resource://formautof
 ChromeUtils.defineModuleGetter(this, "CreditCard",
                                "resource://gre/modules/CreditCard.jsm");
 ChromeUtils.defineModuleGetter(this, "JSONFile",
                                "resource://gre/modules/JSONFile.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillNameUtils",
                                "resource://formautofill/FormAutofillNameUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                                "resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "OSKeyStore",
-                               "resource://formautofill/OSKeyStore.jsm");
+ChromeUtils.defineModuleGetter(this, "MasterPassword",
+                               "resource://formautofill/MasterPassword.jsm");
 ChromeUtils.defineModuleGetter(this, "PhoneNumber",
                                "resource://formautofill/phonenumberutils/PhoneNumber.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
-                                   "@mozilla.org/login-manager/crypto/SDR;1",
-                                   Ci.nsILoginManagerCrypto);
 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
                                           "nsICryptoHash", "initWithString");
 
 const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
 
 const STORAGE_SCHEMA_VERSION = 1;
 const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 2;
+const CREDIT_CARD_SCHEMA_VERSION = 1;
 
 const VALID_ADDRESS_FIELDS = [
   "given-name",
   "additional-name",
   "family-name",
   "organization",
   "street-address",
   "address-level3",
@@ -262,24 +259,23 @@ class AutofillRecords {
 
     this.VALID_FIELDS = validFields;
     this.VALID_COMPUTED_FIELDS = validComputedFields;
 
     this._store = store;
     this._collectionName = collectionName;
     this._schemaVersion = schemaVersion;
 
-    this._initializePromise =
-      Promise.all(this._data.map(async (record, index) => this._migrateRecord(record, index)))
-        .then(hasChangesArr => {
-          let dataHasChanges = hasChangesArr.includes(true);
-          if (dataHasChanges) {
-            this._store.saveSoon();
-          }
-        });
+    Promise.all(this._data.map(record => this._migrateRecord(record)))
+      .then(hasChangesArr => {
+        let dataHasChanges = hasChangesArr.find(hasChanges => hasChanges);
+        if (dataHasChanges) {
+          this._store.saveSoon();
+        }
+      });
   }
 
   /**
    * Gets the schema version number.
    *
    * @returns {number}
    *          The current schema version number.
    */
@@ -303,24 +299,16 @@ class AutofillRecords {
   _ensureMatchingVersion(record) {
     if (record.version != this.version) {
       throw new Error(`Got unknown record version ${
         record.version}; want ${this.version}`);
     }
   }
 
   /**
-   * Initialize the records in the collection, resolves when the migration completes.
-   * @returns {Promise}
-   */
-  initialize() {
-    return this._initializePromise;
-  }
-
-  /**
    * Adds a new record.
    *
    * @param {Object} record
    *        The new record for saving.
    * @param {boolean} [options.sourceSync = false]
    *        Did sync generate this addition?
    * @returns {Promise<string>}
    *          The GUID of the newly added item..
@@ -1183,47 +1171,36 @@ class AutofillRecords {
   }
 
   _findIndexByGUID(guid, {includeDeleted = false} = {}) {
     return this._data.findIndex(record => {
       return record.guid == guid && (!record.deleted || includeDeleted);
     });
   }
 
-  async _migrateRecord(record, index) {
+  async _migrateRecord(record) {
     let hasChanges = false;
 
     if (record.deleted) {
       return hasChanges;
     }
 
     if (!record.version || isNaN(record.version) || record.version < 1) {
       this.log.warn("Invalid record version:", record.version);
 
       // Force to run the migration.
       record.version = 0;
     }
 
     if (record.version < this.version) {
       hasChanges = true;
-
-      record = await this._computeMigratedRecord(record);
+      record.version = this.version;
 
-      if (record.deleted) {
-        // record is deleted by _computeMigratedRecord(),
-        // go ahead and put it in the store.
-        this._data[index] = record;
-        return hasChanges;
-      }
-
-      // Compute the computed fields before putting it to store.
-      await this.computeFields(record);
-      this._data[index] = record;
-
-      return hasChanges;
+      // Force to recompute fields if we upgrade the schema.
+      await this._stripComputedFields(record);
     }
 
     hasChanges |= await this.computeFields(record);
     return hasChanges;
   }
 
   _normalizeRecord(record, preserveEmptyFields = false) {
     this._normalizeFields(record);
@@ -1274,34 +1251,16 @@ class AutofillRecords {
     this._store.data[this._collectionName] = [];
     this._store.saveSoon();
     Services.obs.notifyObservers({wrappedJSObject: {
       sourceSync,
       collectionName: this._collectionName,
     }}, "formautofill-storage-changed", "removeAll");
   }
 
-  /**
-   * Strip the computed fields based on the record version.
-   * @param   {Object} record      The record to migrate
-   * @returns {Object}             Migrated record.
-   *                               Record is always cloned, with version updated,
-   *                               with computed fields stripped.
-   *                               Could be a tombstone record, if the record
-   *                               should be discorded.
-   */
-  async _computeMigratedRecord(record) {
-    if (!record.deleted) {
-      record = this._clone(record);
-      await this._stripComputedFields(record);
-      record.version = this.version;
-    }
-    return record;
-  }
-
   async _stripComputedFields(record) {
     this.VALID_COMPUTED_FIELDS.forEach(field => delete record[field]);
   }
 
   // An interface to be inherited.
   _recordReadProcessor(record) {}
 
   // An interface to be inherited.
@@ -1640,82 +1599,28 @@ class CreditCards extends AutofillRecord
       hasNewComputedFields = true;
     }
 
     // Encrypt credit card number
     if (!("cc-number-encrypted" in creditCard)) {
       if ("cc-number" in creditCard) {
         let ccNumber = creditCard["cc-number"];
         creditCard["cc-number"] = CreditCard.getLongMaskedNumber(ccNumber);
-        creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
+        creditCard["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
       } else {
         creditCard["cc-number-encrypted"] = "";
       }
     }
 
     return hasNewComputedFields;
   }
 
-  async _computeMigratedRecord(creditCard) {
-    if (creditCard["cc-number-encrypted"]) {
-      switch (creditCard.version) {
-        case 1: {
-          if (!cryptoSDR.isLoggedIn) {
-            // We cannot decrypt the data, so silently remove the record for
-            // the user.
-            if (creditCard.deleted) {
-              break;
-            }
-
-            this.log.warn("Removing version 1 credit card record to migrate to new encryption:", creditCard.guid);
-
-            // Replace the record with a tombstone record here,
-            // regardless of existence of sync metadata.
-            let existingSync = this._getSyncMetaData(creditCard);
-            creditCard = {
-              guid: creditCard.guid,
-              timeLastModified: Date.now(),
-              deleted: true,
-            };
-
-            if (existingSync) {
-              creditCard._sync = existingSync;
-              existingSync.changeCounter++;
-            }
-            break;
-          }
-
-          creditCard = this._clone(creditCard);
-
-          // Decrypt the cc-number using version 1 encryption.
-          let ccNumber = cryptoSDR.decrypt(creditCard["cc-number-encrypted"]);
-          // Re-encrypt the cc-number with version 2 encryption.
-          creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
-          break;
-        }
-
-        default:
-          throw new Error("Unknown credit card version to migrate: " + creditCard.version);
-      }
-    }
-    return super._computeMigratedRecord(creditCard);
-  }
-
   async _stripComputedFields(creditCard) {
     if (creditCard["cc-number-encrypted"]) {
-      try {
-        creditCard["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
-      } catch (ex) {
-        if (ex.result == Cr.NS_ERROR_ABORT) {
-          throw ex;
-        }
-        // Quietly recover from encryption error,
-        // so existing credit card entry with undecryptable number
-        // can be updated.
-      }
+      creditCard["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
     }
     await super._stripComputedFields(creditCard);
   }
 
   _normalizeFields(creditCard) {
     this._normalizeCCName(creditCard);
     this._normalizeCCNumber(creditCard);
     this._normalizeCCExpirationDate(creditCard);
@@ -1766,62 +1671,38 @@ class CreditCards extends AutofillRecord
   }
 
   _validateFields(creditCard) {
     if (!creditCard["cc-number"]) {
       throw new Error("Missing/invalid cc-number");
     }
   }
 
-  _ensureMatchingVersion(record) {
-    if (!record.version || isNaN(record.version) || record.version < 1) {
-      throw new Error(`Got invalid record version ${
-        record.version}; want ${this.version}`);
-    }
-
-    if (record.version < this.version) {
-      switch (record.version) {
-        case 1:
-          // The difference between version 1 and 2 is only about the encryption
-          // method used for the cc-number-encrypted field.
-          // As long as the record is already decrypted, it is safe to bump the
-          // version directly.
-          if (!record["cc-number-encrypted"]) {
-            record.version = this.version;
-          } else {
-            throw new Error("Unexpected record migration path.");
-          }
-          break;
-        default:
-          throw new Error("Unknown credit card version to match: " + record.version);
-      }
-    }
-
-    return super._ensureMatchingVersion(record);
-  }
-
   /**
    * 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 {Promise<string|null>}
    *          Return the first guid if storage has the same credit card and null otherwise.
    */
   async getDuplicateGuid(targetCreditCard) {
     let clonedTargetCreditCard = this._clone(targetCreditCard);
     this._normalizeRecord(clonedTargetCreditCard);
     for (let creditCard of this._data) {
       let isDuplicate = await Promise.all(this.VALID_FIELDS.map(async field => {
         if (!clonedTargetCreditCard[field]) {
           return !creditCard[field];
         }
         if (field == "cc-number" && creditCard[field]) {
-          // Compare the masked numbers instead when decryption requires a password
-          // because we don't want to leak the credit card number.
-          return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
+          if (MasterPassword.isEnabled) {
+            // Compare the masked numbers instead when the master password is
+            // enabled because we don't want to leak the credit card number.
+            return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
+          }
+          return (clonedTargetCreditCard[field] == await MasterPassword.decrypt(creditCard["cc-number-encrypted"]));
         }
         return clonedTargetCreditCard[field] == creditCard[field];
       })).then(fieldResults => fieldResults.every(result => result));
       if (isDuplicate) {
         return creditCard.guid;
       }
     }
     return null;
@@ -1919,20 +1800,17 @@ FormAutofillStorage.prototype = {
    * @rejects  JavaScript exception.
    */
   initialize() {
     if (!this._initializePromise) {
       this._store = new JSONFile({
         path: this._path,
         dataPostProcessor: this._dataPostProcessor.bind(this),
       });
-      this._initializePromise = this._store.load()
-        .then(() => Promise.all([
-          this.addresses.initialize(),
-          this.creditCards.initialize()]));
+      this._initializePromise = this._store.load();
     }
     return this._initializePromise;
   },
 
   _dataPostProcessor(data) {
     data.version = this.version;
     if (!data.addresses) {
       data.addresses = [];
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -12,17 +12,17 @@ const ADDRESS_REFERENCES_EXT = "addressR
 
 const ADDRESSES_COLLECTION_NAME = "addresses";
 const CREDITCARDS_COLLECTION_NAME = "creditCards";
 const MANAGE_ADDRESSES_KEYWORDS = ["manageAddressesTitle", "addNewAddressTitle"];
 const EDIT_ADDRESS_KEYWORDS = [
   "givenName", "additionalName", "familyName", "organization2", "streetAddress",
   "state", "province", "city", "country", "zip", "postalCode", "email", "tel",
 ];
-const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle"];
+const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle", "showCreditCardsBtnLabel"];
 const EDIT_CREDITCARD_KEYWORDS = ["cardNumber", "nameOnCard", "cardExpiresMonth", "cardExpiresYear", "cardNetwork"];
 const FIELD_STATES = {
   NORMAL: "NORMAL",
   AUTO_FILLED: "AUTO_FILLED",
   PREVIEW: "PREVIEW",
 };
 const SECTION_TYPES = {
   ADDRESS: "address",
rename from browser/extensions/formautofill/OSKeyStore.jsm
rename to browser/extensions/formautofill/MasterPassword.jsm
--- a/browser/extensions/formautofill/OSKeyStore.jsm
+++ b/browser/extensions/formautofill/MasterPassword.jsm
@@ -1,251 +1,184 @@
 /* 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/. */
 
 /**
- * Helpers for using OS Key Store.
+ * Helpers for the Master Password Dialog.
+ * In the future the Master Password implementation may move here.
  */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
-  "OSKeyStore",
+  "MasterPassword",
 ];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "nativeOSKeyStore",
-                                   "@mozilla.org/security/oskeystore;1", Ci.nsIOSKeyStore);
-
-// Skip reauth during tests, only works in non-official builds.
-const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
-
-var OSKeyStore = {
-  /**
-   * On macOS this becomes part of the name label visible on Keychain Acesss as
-   * "org.mozilla.nss.keystore.firefox" (where "firefox" is the MOZ_APP_NAME).
-   */
-  STORE_LABEL: AppConstants.MOZ_APP_NAME,
+XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
+                                   "@mozilla.org/login-manager/crypto/SDR;1",
+                                   Ci.nsILoginManagerCrypto);
 
-  /**
-   * Consider the module is initialized as locked. OS might unlock without a
-   * prompt.
-   * @type {Boolean}
-   */
-  _isLocked: true,
-
-  _pendingUnlockPromise: null,
-
-  /**
-   * @returns {boolean} True if logged in (i.e. decrypt(reauth = false) will
-   *                    not retrigger a dialog) and false if not.
-   *                    User might log out elsewhere in the OS, so even if this
-   *                    is true a prompt might still pop up.
-   */
-  get isLoggedIn() {
-    return !this._isLocked;
+var MasterPassword = {
+  get _token() {
+    let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB);
+    return tokendb.getInternalKeyToken();
   },
 
   /**
-   * @returns {boolean} True if there is another login dialog existing and false
-   *                    otherwise.
+   * @returns {boolean} True if a master password is set and false otherwise.
    */
-  get isUIBusy() {
-    return !!this._pendingUnlockPromise;
+  get isEnabled() {
+    return this._token.hasPassword;
   },
 
   /**
-   * If the test pref exist and applicable,
-   * this method will dispatch a observer message and return
-   * to simulate successful reauth, or throw to simulate
-   * failed reauth.
-   *
-   * @returns {boolean} True when reauth should NOT be skipped,
-   *                    false when reauth has been skipped.
-   * @throws            If it needs to simulate reauth login failure.
+   * @returns {boolean} True if master password is logged in and false if not.
    */
-  _maybeSkipReauthForTest() {
-    // Don't take test reauth pref in the following configurations.
-    if (nativeOSKeyStore.isNSSKeyStore ||
-        AppConstants.MOZILLA_OFFICIAL ||
-        !this._testReauth) {
-      return true;
-    }
-
-    // Skip this reauth because there is no way to mock the
-    // native dialog in the testing environment, for now.
-    log.debug("_ensureReauth: _testReauth: ", this._testReauth);
-    switch (this._testReauth) {
-      case "pass":
-        Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "pass");
-        return false;
-      case "cancel":
-        Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "cancel");
-        throw new Components.Exception("Simulating user cancelling login dialog", Cr.NS_ERROR_FAILURE);
-      default:
-        throw new Components.Exception("Unknown test pref value", Cr.NS_ERROR_FAILURE);
-    }
+  get isLoggedIn() {
+    return Services.logins.isLoggedIn;
   },
 
   /**
-   * Ensure the store in use is logged in. It will display the OS login
-   * login prompt or do nothing if it's logged in already. If an existing login
+   * @returns {boolean} True if there is another master password login dialog
+   *                    existing and false otherwise.
+   */
+  get isUIBusy() {
+    return Services.logins.uiBusy;
+  },
+
+  /**
+   * Ensure the master password is logged in. It will display the master password
+   * login prompt or do nothing if it's logged in already. If an existing MP
    * prompt is already prompted, the result from it will be used instead.
    *
-   * Note: This method must set _pendingUnlockPromise before returning the
-   * promise (i.e. the first |await|), otherwise we'll risk re-entry.
-   * This is why there aren't an |await| in the method. The method is marked as
-   * |async| to communicate that it's async.
-   *
    * @param   {boolean} reauth Prompt the login dialog no matter it's logged in
    *                           or not if it's set to true.
    * @returns {Promise<boolean>} True if it's logged in or no password is set
    *                             and false if it's still not logged in (prompt
    *                             canceled or other error).
    */
   async ensureLoggedIn(reauth = false) {
-    if (this._pendingUnlockPromise) {
-      log.debug("ensureLoggedIn: Has a pending unlock operation");
-      return this._pendingUnlockPromise;
+    if (!this.isEnabled) {
+      return true;
     }
-    log.debug("ensureLoggedIn: Creating new pending unlock promise. reauth: ", reauth);
-
-    // TODO: Implementing re-auth by passing this value to the native implementation
-    // in some way. Set this to false for now to ignore the reauth request (bug 1429265).
-    reauth = false;
-
-    let unlockPromise = Promise.resolve().then(async () => {
-      if (reauth) {
-        reauth = this._maybeSkipReauthForTest();
-      }
 
-      if (!await nativeOSKeyStore.asyncSecretAvailable(this.STORE_LABEL)) {
-        log.debug("ensureLoggedIn: Secret unavailable, attempt to generate new secret.");
-        let recoveryPhrase = await nativeOSKeyStore.asyncGenerateSecret(this.STORE_LABEL);
-        // TODO We should somehow have a dialog to ask the user to write this down,
-        // and another dialog somewhere for the user to restore the secret with it.
-        // (Intentionally not printing it out in the console)
-        log.debug("ensureLoggedIn: Secret generated. Recovery phrase length: " + recoveryPhrase.length);
-      }
-    });
+    if (this.isLoggedIn && !reauth) {
+      return true;
+    }
 
-    if (nativeOSKeyStore.isNSSKeyStore) {
-      // Workaround bug 1492305: NSS-implemented methods don't reject when user cancels.
-      unlockPromise = unlockPromise.then(() => {
-        log.debug("ensureLoggedIn: isNSSKeyStore: ", reauth, Services.logins.isLoggedIn);
-        // User has hit the cancel button on the master password prompt.
-        // We must reject the promise chain here.
-        if (!Services.logins.isLoggedIn) {
-          throw Components.Exception("User canceled OS unlock entry (Workaround)", Cr.NS_ERROR_FAILURE);
-        }
-      });
+    // If a prompt is already showing then wait for and focus it.
+    if (this.isUIBusy) {
+      return this.waitForExistingDialog();
     }
 
-    unlockPromise = unlockPromise.then(() => {
-      log.debug("ensureLoggedIn: Logged in");
-      this._pendingUnlockPromise = null;
-      this._isLocked = false;
+    let token = this._token;
+    try {
+      // 'true' means always prompt for token password. User will be prompted until
+      // clicking 'Cancel' or entering the correct password.
+      token.login(true);
+    } catch (e) {
+      // An exception will be thrown if the user cancels the login prompt dialog.
+      // User is also logged out.
+    }
 
-      return true;
-    }, (err) => {
-      log.debug("ensureLoggedIn: Not logged in", err);
-      this._pendingUnlockPromise = null;
-      this._isLocked = true;
+    // If we triggered a master password prompt, notify observers.
+    if (token.isLoggedIn()) {
+      Services.obs.notifyObservers(null, "passwordmgr-crypto-login");
+    } else {
+      Services.obs.notifyObservers(null, "passwordmgr-crypto-loginCanceled");
+    }
 
-      return false;
-    });
-
-    this._pendingUnlockPromise = unlockPromise;
-
-    return this._pendingUnlockPromise;
+    return token.isLoggedIn();
   },
 
   /**
    * Decrypts cipherText.
    *
-   * Note: In the event of an rejection, check the result property of the Exception
-   *       object. Handles NS_ERROR_ABORT as user has cancelled the action (e.g.,
-   *       don't show that dialog), apart from other errors (e.g., gracefully
-   *       recover from that and still shows the dialog.)
-   *
    * @param   {string} cipherText Encrypted string including the algorithm details.
    * @param   {boolean} reauth True if we want to force the prompt to show up
    *                    even if the user is already logged in.
    * @returns {Promise<string>} resolves to the decrypted string, or rejects otherwise.
    */
   async decrypt(cipherText, reauth = false) {
     if (!await this.ensureLoggedIn(reauth)) {
-      throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
+      throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
     }
-    let bytes = await nativeOSKeyStore.asyncDecryptBytes(this.STORE_LABEL, cipherText);
-    return String.fromCharCode.apply(String, bytes);
+    return cryptoSDR.decrypt(cipherText);
   },
 
   /**
    * Encrypts a string and returns cipher text containing algorithm information used for decryption.
    *
    * @param   {string} plainText Original string without encryption.
    * @returns {Promise<string>} resolves to the encrypted string (with algorithm), otherwise rejects.
    */
   async encrypt(plainText) {
     if (!await this.ensureLoggedIn()) {
-      throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
+      throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
     }
 
-    // Convert plain text into a UTF-8 binary string
-    plainText = unescape(encodeURIComponent(plainText));
-
-    // Convert it to an array
-    let textArr = [];
-    for (let char of plainText) {
-      textArr.push(char.charCodeAt(0));
-    }
-
-    let rawEncryptedText = await nativeOSKeyStore.asyncEncryptBytes(this.STORE_LABEL, textArr.length, textArr);
-
-    // Mark the output with a version number.
-    return rawEncryptedText;
+    return cryptoSDR.encrypt(plainText);
   },
 
   /**
-   * Resolve when the login dialogs are closed, immediately if none are open.
+   * Resolve when master password dialogs are closed, immediately if none are open.
    *
    * An existing MP dialog will be focused and will request attention.
    *
    * @returns {Promise<boolean>}
    *          Resolves with whether the user is logged in to MP.
    */
   async waitForExistingDialog() {
-    if (this.isUIBusy) {
-      return this._pendingUnlockPromise;
+    if (!this.isUIBusy) {
+      log.debug("waitForExistingDialog: Dialog isn't showing. isLoggedIn:", this.isLoggedIn);
+      return this.isLoggedIn;
     }
-    return this.isLoggedIn;
-  },
+
+    return new Promise((resolve) => {
+      log.debug("waitForExistingDialog: Observing the open dialog");
+      let observer = {
+        QueryInterface: ChromeUtils.generateQI([
+          Ci.nsIObserver,
+          Ci.nsISupportsWeakReference,
+        ]),
 
-  /**
-   * Remove the store. For tests.
-   */
-  async cleanup() {
-    return nativeOSKeyStore.asyncDeleteSecret(this.STORE_LABEL);
-  },
+        observe(subject, topic, data) {
+          log.debug("waitForExistingDialog: Got notification:", topic);
+          // Only run observer once.
+          Services.obs.removeObserver(this, "passwordmgr-crypto-login");
+          Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
+          if (topic == "passwordmgr-crypto-loginCanceled") {
+            resolve(false);
+            return;
+          }
+
+          resolve(true);
+        },
+      };
 
-  /**
-   * Check if the implementation is using the NSS key store.
-   * If so, tests will be able to handle the reauth dialog.
-   */
-  get isNSSKeyStore() {
-    return nativeOSKeyStore.isNSSKeyStore;
+      // Possible leak: it's possible that neither of these notifications
+      // will fire, and if that happens, we'll leak the observer (and
+      // never return). We should guarantee that at least one of these
+      // will fire.
+      // See bug XXX.
+      Services.obs.addObserver(observer, "passwordmgr-crypto-login");
+      Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled");
+
+      // Focus and draw attention to the existing master password dialog for the
+      // occassions where it's not attached to the current window.
+      let promptWin = Services.wm.getMostRecentWindow("prompt:promptPassword");
+      promptWin.focus();
+      promptWin.getAttention();
+    });
   },
 };
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
   return new ConsoleAPI({
-    maxLogLevelPref: "extensions.formautofill.loglevel",
-    prefix: "OSKeyStore",
+    maxLogLevelPref: "masterPassword.loglevel",
+    prefix: "Master Password",
   });
 });
-
-XPCOMUtils.defineLazyPreferenceGetter(OSKeyStore, "_testReauth", TEST_ONLY_REAUTH, "");
--- a/browser/extensions/formautofill/content/manageCreditCards.xhtml
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -15,27 +15,29 @@
 </head>
 <body dir="&locale.dir;">
   <fieldset>
     <legend data-localization="creditCardsListHeader"/>
     <select id="credit-cards" size="9" multiple="multiple"/>
   </fieldset>
   <div id="controls-container">
     <button id="remove" disabled="disabled" data-localization="removeBtnLabel"/>
+    <button id="show-hide-credit-cards" data-localization="showCreditCardsBtnLabel"/>
     <!-- Wrapper is used to properly compute the search tooltip position -->
     <div>
       <button id="add" data-localization="addBtnLabel"/>
     </div>
     <button id="edit" disabled="disabled" data-localization="editBtnLabel"/>
   </div>
   <script type="application/javascript">
     "use strict";
     /* global ManageCreditCards */
     new ManageCreditCards({
       records: document.getElementById("credit-cards"),
       controlsContainer: document.getElementById("controls-container"),
       remove: document.getElementById("remove"),
+      showHideCreditCards: document.getElementById("show-hide-credit-cards"),
       add: document.getElementById("add"),
       edit: document.getElementById("edit"),
     });
   </script>
 </body>
 </html>
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -14,18 +14,18 @@ ChromeUtils.import("resource://formautof
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 ChromeUtils.defineModuleGetter(this, "CreditCard",
                                "resource://gre/modules/CreditCard.jsm");
 ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
                                "resource://formautofill/FormAutofillStorage.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                                "resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "OSKeyStore",
-                               "resource://formautofill/OSKeyStore.jsm");
+ChromeUtils.defineModuleGetter(this, "MasterPassword",
+                               "resource://formautofill/MasterPassword.jsm");
 
 this.log = null;
 FormAutofill.defineLazyLogGetter(this, "manageAddresses");
 
 class ManageRecords {
   constructor(subStorageName, elements) {
     this._storageInitPromise = formAutofillStorage.initialize();
     this._subStorageName = subStorageName;
@@ -308,43 +308,35 @@ class ManageAddresses extends ManageReco
 }
 
 class ManageCreditCards extends ManageRecords {
   constructor(elements) {
     super("creditCards", elements);
     elements.add.setAttribute("searchkeywords", FormAutofillUtils.EDIT_CREDITCARD_KEYWORDS
                                                   .map(key => FormAutofillUtils.stringBundle.GetStringFromName(key))
                                                   .join("\n"));
+    this._hasMasterPassword = MasterPassword.isEnabled;
     this._isDecrypted = false;
+    if (this._hasMasterPassword) {
+      elements.showHideCreditCards.setAttribute("hidden", true);
+    }
   }
 
   /**
    * Open the edit address dialog to create/edit a credit card.
    *
    * @param  {object} creditCard [optional]
    */
   async openEditDialog(creditCard) {
-    // Ask for reauth if user is trying to edit an existing credit card.
-    if (!creditCard || await OSKeyStore.ensureLoggedIn(true)) {
+    // If master password is set, ask for password if user is trying to edit an
+    // existing credit card.
+    if (!creditCard || !this._hasMasterPassword || await MasterPassword.ensureLoggedIn(true)) {
       let decryptedCCNumObj = {};
       if (creditCard) {
-        try {
-          decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
-        } catch (ex) {
-          if (ex.result == Cr.NS_ERROR_ABORT) {
-            // User shouldn't be ask to reauth here, but it could happen.
-            // Return here and skip opening the dialog.
-            return;
-          }
-          // We've got ourselves a real error.
-          // Recover from encryption error so the user gets a chance to re-enter
-          // unencrypted credit card number.
-          decryptedCCNumObj["cc-number"] = "";
-          Cu.reportError(ex);
-        }
+        decryptedCCNumObj["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
       }
       let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
       this.prefWin.gSubDialog.open(EDIT_CREDIT_CARD_URL, "resizable=no", {
         record: decryptedCreditCard,
       });
     }
   }
 
@@ -362,16 +354,22 @@ class ManageCreditCards extends ManageRe
       encryptedNumber: creditCard["cc-number-encrypted"],
       number: creditCard["cc-number"],
       name: creditCard["cc-name"],
       network: creditCard["cc-type"],
     });
     return cardObj.getLabel({showNumbers: showCreditCards});
   }
 
+  async toggleShowHideCards(options) {
+    this._isDecrypted = !this._isDecrypted;
+    this.updateShowHideButtonState();
+    await this.updateLabels(options, this._isDecrypted);
+  }
+
   async updateLabels(options, isDecrypted) {
     for (let option of options) {
       option.text = await this.getLabel(option.record, isDecrypted);
     }
     // For testing only: Notify when credit cards labels have been updated
     this._elements.records.dispatchEvent(new CustomEvent("LabelsUpdated"));
   }
 
@@ -389,15 +387,30 @@ class ManageCreditCards extends ManageRe
         option.setAttribute("cc-type", record["cc-type"]);
       } else {
         option.removeAttribute("cc-type");
       }
     }
   }
 
   updateButtonsStates(selectedCount) {
+    this.updateShowHideButtonState();
     super.updateButtonsStates(selectedCount);
   }
 
+  updateShowHideButtonState() {
+    if (this._elements.records.length) {
+      this._elements.showHideCreditCards.removeAttribute("disabled");
+    } else {
+      this._elements.showHideCreditCards.setAttribute("disabled", true);
+    }
+    this._elements.showHideCreditCards.textContent =
+      this._isDecrypted ? FormAutofillUtils.stringBundle.GetStringFromName("hideCreditCardsBtnLabel") :
+                          FormAutofillUtils.stringBundle.GetStringFromName("showCreditCardsBtnLabel");
+  }
+
   handleClick(event) {
+    if (event.target == this._elements.showHideCreditCards) {
+      this.toggleShowHideCards(this._elements.records.options);
+    }
     super.handleClick(event);
   }
 }
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -99,16 +99,18 @@ savedCreditCardsBtnLabel = Saved Credit Cards…
 # LOCALIZATION NOTE (manageAddressesTitle, manageCreditCardsTitle): The dialog title for the list of addresses or
 # credit cards in browser preferences.
 manageAddressesTitle = Saved Addresses
 manageCreditCardsTitle = Saved Credit Cards
 # LOCALIZATION NOTE (addressesListHeader, creditCardsListHeader): The header for the list of addresses or credit cards
 # in browser preferences.
 addressesListHeader = Addresses
 creditCardsListHeader = Credit Cards
+showCreditCardsBtnLabel = Show Credit Cards
+hideCreditCardsBtnLabel = Hide Credit Cards
 removeBtnLabel = Remove
 addBtnLabel = Add…
 editBtnLabel = Edit…
 # LOCALIZATION NOTE (manageDialogsWidth): This strings sets the default width for windows used to manage addresses and
 # credit cards.
 manageDialogsWidth = 560px
 
 # LOCALIZATION NOTE (addNewAddressTitle, editAddressTitle): The dialog title for creating or editing addresses
--- a/browser/extensions/formautofill/moz.build
+++ b/browser/extensions/formautofill/moz.build
@@ -27,18 +27,16 @@ elif CONFIG['OS_ARCH'] == 'Darwin':
         'skin/osx/editDialog.css',
     ]
 elif CONFIG['OS_ARCH'] == 'WINNT':
     FINAL_TARGET_FILES.features['formautofill@mozilla.org'].chrome.res += [
         'skin/windows/autocomplete-item.css',
         'skin/windows/editDialog.css',
     ]
 
-TESTING_JS_MODULES += ['test/fixtures/OSKeyStoreTestUtils.jsm']
-
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
--- a/browser/extensions/formautofill/test/browser/browser.ini
+++ b/browser/extensions/formautofill/test/browser/browser.ini
@@ -10,18 +10,17 @@ support-files =
 [browser_autocomplete_footer.js]
 skip-if = verify
 [browser_autocomplete_marked_back_forward.js]
 [browser_autocomplete_marked_detached_tab.js]
 skip-if = (verify && (os == 'win' || os == 'mac'))
 [browser_check_installed.js]
 [browser_creditCard_doorhanger.js]
 skip-if = (os == "linux") || (os == "mac" && debug) || (os == "win") # bug 1425884
-[browser_creditCard_fill_cancel_login.js]
-skip-if = true # Re-auth is not implemented, cannot cancel OS key store login (bug 1429265)
+[browser_creditCard_fill_master_password.js]
 [browser_dropdown_layout.js]
 [browser_editAddressDialog.js]
 [browser_editCreditCardDialog.js]
 skip-if = (verify && (os == 'linux'))
 [browser_first_time_use_doorhanger.js]
 skip-if = verify
 [browser_insecure_form.js]
 skip-if = (os == 'linux' && !debug) || (os == 'win') # bug 1456284
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
@@ -46,17 +46,16 @@ add_task(async function test_submit_cred
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
-      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
         name.setUserInput("User 1");
 
         form.querySelector("#cc-number").setUserInput("5038146897157463");
         form.querySelector("#cc-exp-month").setUserInput("12");
@@ -65,66 +64,54 @@ add_task(async function test_submit_cred
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
-      await onChanged;
     }
   );
 
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
   is(creditCards[0]["cc-type"], "mastercard", "Verify the cc-type field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_submit_untouched_creditCard_form() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
-    return;
-  }
-
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
-
-  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
-  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
-                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
-      await osKeyStoreLoginShown;
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await sleep(1000);
       is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
     }
   );
-  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0].timesUsed, 1, "timesUsed field set to 1");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
@@ -133,19 +120,16 @@ add_task(async function test_submit_chan
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
-
-  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
-                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
 
@@ -160,17 +144,16 @@ add_task(async function test_submit_chan
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
-  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card in storage");
   is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"], "name field still exists");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
@@ -286,16 +269,88 @@ add_task(async function test_submit_cred
   let creditCardPref = SpecialPowers.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
   is(creditCards.length, 0, "No credit card in storage");
   is(creditCardPref, false, "Credit card is disabled");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
 });
 
+add_task(async function test_submit_creditCard_saved_with_mp_enabled() {
+  LoginTestUtils.masterPassword.enable();
+  // Login with the masterPassword in LoginTestUtils.
+  let masterPasswordDialogShown = waitForMasterPasswordDialog(true);
+  await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
+    async function(browser) {
+      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+                                                       "popupshown");
+      await ContentTask.spawn(browser, null, async function() {
+        let form = content.document.getElementById("form");
+        let name = form.querySelector("#cc-name");
+        name.focus();
+        await new Promise(resolve => setTimeout(resolve, 1000));
+        name.setUserInput("User 0");
+
+        let number = form.querySelector("#cc-number");
+        number.setUserInput("6387060366272981");
+
+        // Wait 1000ms before submission to make sure the input value applied
+        await new Promise(resolve => setTimeout(resolve, 1000));
+        form.querySelector("input[type=submit]").click();
+      });
+
+      await promiseShown;
+      await clickDoorhangerButton(MAIN_BUTTON);
+      await masterPasswordDialogShown;
+      await TestUtils.topicObserved("formautofill-storage-changed");
+    }
+  );
+
+  let creditCards = await getCreditCards();
+  is(creditCards.length, 1, "1 credit card in storage");
+  is(creditCards[0]["cc-name"], "User 0", "Verify the name field");
+  is(creditCards[0]["cc-number"], "************2981", "Verify the card number field");
+  LoginTestUtils.masterPassword.disable();
+  await removeAllRecords();
+});
+
+add_task(async function test_submit_creditCard_saved_with_mp_enabled_but_canceled() {
+  LoginTestUtils.masterPassword.enable();
+  let masterPasswordDialogShown = waitForMasterPasswordDialog();
+  await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
+    async function(browser) {
+      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+                                                       "popupshown");
+      await ContentTask.spawn(browser, null, async function() {
+        let form = content.document.getElementById("form");
+        let name = form.querySelector("#cc-name");
+        name.focus();
+        await new Promise(resolve => setTimeout(resolve, 1000));
+        name.setUserInput("User 2");
+
+        let number = form.querySelector("#cc-number");
+        number.setUserInput("5471839082338112");
+
+        // Wait 1000ms before submission to make sure the input value applied
+        await new Promise(resolve => setTimeout(resolve, 1000));
+        form.querySelector("input[type=submit]").click();
+      });
+
+      await promiseShown;
+      await clickDoorhangerButton(MAIN_BUTTON);
+      await masterPasswordDialogShown;
+    }
+  );
+
+  await sleep(1000);
+  let creditCards = await getCreditCards();
+  is(creditCards.length, 0, "No credit cards in storage");
+  LoginTestUtils.masterPassword.disable();
+});
+
 add_task(async function test_submit_creditCard_with_sync_account() {
   await SpecialPowers.pushPrefEnv({
     "set": [
       [SYNC_USERNAME_PREF, "foo@bar.com"],
       [SYNC_CREDITCARDS_AVAILABLE_PREF, true],
     ],
   });
 
@@ -385,18 +440,16 @@ add_task(async function test_submit_manu
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_3);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
-  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
-                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
@@ -409,255 +462,195 @@ add_task(async function test_submit_manu
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
-  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 3", "Verify the name field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_autofill_form_name() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
-    return;
-  }
-
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
-  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
-  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
-                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
-      await osKeyStoreLoginShown;
       await ContentTask.spawn(browser, null, async function() {
-        await ContentTaskUtils.waitForCondition(() => {
-          let form = content.document.getElementById("form");
-          let name = form.querySelector("#cc-name");
-          return name.value == "John Doe";
-        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.setUserInput("User 1");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
+
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
-  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0]["cc-name"], "User 1", "cc-name field is updated");
   is(creditCards[0]["cc-number"], "************1111", "Verify the card number field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_autofill_form_exp_date() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
-    return;
-  }
-
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
-  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
-  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
-                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
-        await ContentTaskUtils.waitForCondition(() => {
-          let form = content.document.getElementById("form");
-          let name = form.querySelector("#cc-name");
-          return name.value == "John Doe";
-        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let year = form.querySelector("#cc-exp-year");
         year.setUserInput("2020");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
-      await osKeyStoreLoginShown;
     }
   );
-  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0]["cc-exp-year"], "2020", "cc-exp-year field is updated");
   is(creditCards[0]["cc-number"], "************1111", "Verify the card number field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_create_new_autofill_form() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
-    return;
-  }
-
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
-  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
-      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
-        await ContentTaskUtils.waitForCondition(() => {
-          let form = content.document.getElementById("form");
-          let name = form.querySelector("#cc-name");
-          return name.value == "John Doe";
-        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.setUserInput("User 1");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(SECONDARY_BUTTON);
-      await osKeyStoreLoginShown;
-      await onChanged;
     }
   );
 
   creditCards = await getCreditCards();
   is(creditCards.length, 2, "2 credit cards in storage");
   is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"],
      "Original record's cc-name field is unchanged");
   is(creditCards[1]["cc-name"], "User 1", "cc-name field in the new record");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_duplicate_autofill_form() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
-    return;
-  }
-
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard({
     "cc-number": "6387060366272981",
   });
   await saveCreditCard({
     "cc-number": "5038146897157463",
   });
   let creditCards = await getCreditCards();
   is(creditCards.length, 2, "2 credit card in storage");
-  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
-  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
-                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "form #cc-number");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
-        await ContentTaskUtils.waitForCondition(() => {
-          let form = content.document.getElementById("form");
-          let number = form.querySelector("#cc-number");
-          return number.value == "6387060366272981";
-        }, "Should be the first credit card number");
-
-        // Change number to the second credit card number
         let form = content.document.getElementById("form");
         let number = form.querySelector("#cc-number");
+        is(number.value, "6387060366272981", "Should be the first credit card number");
+        // Change number to the second credit card number
         number.setUserInput("5038146897157463");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await sleep(1000);
       is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
-      await osKeyStoreLoginShown;
     }
   );
-  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 2, "Still 2 credit card");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 1,
     "User neither sees the doorhanger nor uses autofill but somehow has a record in the storage");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_submit_creditCard_with_invalid_network() {
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
-      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
         name.setUserInput("User 1");
 
         form.querySelector("#cc-number").setUserInput("5038146897157463");
         form.querySelector("#cc-exp-month").setUserInput("12");
@@ -666,17 +659,16 @@ add_task(async function test_submit_cred
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
-      await onChanged;
     }
   );
 
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
   is(creditCards[0]["cc-type"], undefined, "Invalid network/cc-type was not saved");
 
rename from browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
rename to browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
@@ -1,25 +1,25 @@
 "use strict";
 
-add_task(async function test_fill_creditCard_but_cancel_login() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
-    return;
-  }
-
+add_task(async function test_fill_creditCard_with_mp_enabled_but_canceled() {
   await saveCreditCard(TEST_CREDIT_CARD_2);
 
-  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false); // cancel
+  LoginTestUtils.masterPassword.enable();
+  registerCleanupFunction(() => {
+    LoginTestUtils.masterPassword.disable();
+  });
+
+  let masterPasswordDialogShown = waitForMasterPasswordDialog(false); // cancel
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "#cc-name");
       const ccItem = getDisplayedPopupItems(browser)[0];
       await EventUtils.synthesizeMouseAtCenter(ccItem, {});
-      await Promise.all([osKeyStoreLoginShown, expectPopupClose(browser)]);
+      await Promise.all([masterPasswordDialogShown, expectPopupClose(browser)]);
 
       await ContentTask.spawn(browser, {}, async function() {
         is(content.document.querySelector("#cc-name").value, "", "Check name");
         is(content.document.querySelector("#cc-number").value, "", "Check number");
       });
     }
   );
 });
--- a/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
@@ -1,29 +1,32 @@
 "use strict";
 
 const TEST_SELECTORS = {
   selRecords: "#credit-cards",
   btnRemove: "#remove",
+  btnShowHideCreditCards: "#show-hide-credit-cards",
   btnAdd: "#add",
   btnEdit: "#edit",
 };
 
 const DIALOG_SIZE = "width=600,height=400";
 
 add_task(async function test_manageCreditCardsInitialState() {
   await BrowserTestUtils.withNewTab({gBrowser, url: MANAGE_CREDIT_CARDS_DIALOG_URL}, async function(browser) {
     await ContentTask.spawn(browser, TEST_SELECTORS, (args) => {
       let selRecords = content.document.querySelector(args.selRecords);
       let btnRemove = content.document.querySelector(args.btnRemove);
+      let btnShowHideCreditCards = content.document.querySelector(args.btnShowHideCreditCards);
       let btnAdd = content.document.querySelector(args.btnAdd);
       let btnEdit = content.document.querySelector(args.btnEdit);
 
       is(selRecords.length, 0, "No credit card");
       is(btnRemove.disabled, true, "Remove button disabled");
+      is(btnShowHideCreditCards.disabled, true, "Show Credit Cards button disabled");
       is(btnAdd.disabled, false, "Add button enabled");
       is(btnEdit.disabled, true, "Edit button disabled");
     });
   });
 });
 
 add_task(async function test_cancelManageCreditCardsDialogWithESC() {
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL);
@@ -99,16 +102,67 @@ add_task(async function test_creditCards
   is(selRecords.length, 1, "One credit card is shown");
 
   await removeCreditCards([selRecords.options[0].value]);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
   is(selRecords.length, 0, "Credit card is removed");
   win.close();
 });
 
+add_task(async function test_showCreditCards() {
+  await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
+  await saveCreditCard(TEST_CREDIT_CARD_1);
+  await saveCreditCard(TEST_CREDIT_CARD_2);
+  await saveCreditCard(TEST_CREDIT_CARD_3);
+
+  let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
+  await waitForFocusAndFormReady(win);
+
+  let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+  let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
+
+  is(btnShowHideCreditCards.disabled, false, "Show credit cards button enabled");
+  is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
+
+  // Show credit card numbers
+  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
+  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
+  is(selRecords[0].text, "5103059495477870", "Decrypted credit card 3");
+  is(selRecords[1].text, "4929001587121045, Timothy Berners-Lee", "Decrypted credit card 2");
+  is(selRecords[2].text, "4111111111111111, John Doe", "Decrypted credit card 1");
+  is(btnShowHideCreditCards.textContent, "Hide Credit Cards", "Label should be 'Hide Credit Cards'");
+
+  // Hide credit card numbers
+  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
+  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
+  is(selRecords[0].text, "**** 7870", "Masked credit card 3");
+  is(selRecords[1].text, "**** 1045, Timothy Berners-Lee", "Masked credit card 2");
+  is(selRecords[2].text, "**** 1111, John Doe", "Masked credit card 1");
+  is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
+
+  // Show credit card numbers again to test if they revert back to masked form when reloaded
+  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
+  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
+  // Ensure credit card numbers are shown again
+  is(selRecords[0].text, "5103059495477870", "Decrypted credit card 3");
+  // Remove a card to trigger reloading
+  await removeCreditCards([selRecords.options[2].value]);
+  await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+  is(selRecords[0].text, "**** 7870", "Masked credit card 3");
+  is(selRecords[1].text, "**** 1045, Timothy Berners-Lee", "Masked credit card 2");
+
+  // Remove the rest of the cards
+  await removeCreditCards([selRecords.options[1].value]);
+  await removeCreditCards([selRecords.options[0].value]);
+  await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+  is(btnShowHideCreditCards.disabled, true, "Show credit cards button is disabled when there is no card");
+
+  win.close();
+});
+
 add_task(async function test_showCreditCardIcons() {
   await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let unknownCard = Object.assign({}, TEST_CREDIT_CARD_3, {"cc-type": "gringotts"});
   await saveCreditCard(unknownCard);
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await waitForFocusAndFormReady(win);
@@ -134,51 +188,45 @@ add_task(async function test_showCreditC
   }
 
   await removeCreditCards([option0.value, option1.value]);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
   is(selRecords.length, 0, "Credit card is removed");
   win.close();
 });
 
-add_task(async function test_hasEditLoginPrompt() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
-    return;
-  }
 
+add_task(async function test_hasMasterPassword() {
   await saveCreditCard(TEST_CREDIT_CARD_1);
+  LoginTestUtils.masterPassword.enable();
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await waitForFocusAndFormReady(win);
 
   let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
   let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
+  let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
   let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd);
-  // let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
+  let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
+  let masterPasswordDialogShown = waitForMasterPasswordDialog();
 
-  EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+  is(btnShowHideCreditCards.hidden, true, "Show credit cards button is hidden");
 
-  // Login dialog should show when trying to edit a credit card record.
-  // TODO: test disabled because re-auth is not implemented yet (bug 1429265).
-  /*
-  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(); // cancel
+  // Master password dialog should show when trying to edit a credit card record.
+  EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
   EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win);
-  await osKeyStoreLoginShown;
-  await new Promise(resolve => waitForFocus(resolve, win));
-  await new Promise(resolve => executeSoon(resolve));
-  */
+  await masterPasswordDialogShown;
 
-  // Login is not required for removing credit cards.
+  // Master password is not required for removing credit cards.
   EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
   is(selRecords.length, 0, "Credit card is removed");
 
   // gSubDialog.open should be called when trying to add a credit card,
-  // no OS login dialog is required.
+  // no master password is required.
   window.gSubDialog = {
     open: url => is(url, EDIT_CREDIT_CARD_DIALOG_URL, "Edit credit card dialog is called"),
   };
   EventUtils.synthesizeMouseAtCenter(btnAdd, {}, win);
   delete window.gSubDialog;
 
   win.close();
 });
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -3,31 +3,32 @@
             TEST_ADDRESS_IE_1,
             TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2, TEST_CREDIT_CARD_3, FORM_URL, CREDITCARD_FORM_URL,
             FTU_PREF, ENABLED_AUTOFILL_ADDRESSES_PREF, AUTOFILL_CREDITCARDS_AVAILABLE_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF,
             SUPPORTED_COUNTRIES_PREF,
             SYNC_USERNAME_PREF, SYNC_ADDRESSES_PREF, SYNC_CREDITCARDS_PREF, SYNC_CREDITCARDS_AVAILABLE_PREF, CREDITCARDS_USED_STATUS_PREF,
             DEFAULT_REGION_PREF,
             sleep, expectPopupOpen, openPopupOn, expectPopupClose, closePopup, clickDoorhangerButton,
             getAddresses, saveAddress, removeAddresses, saveCreditCard,
-            getDisplayedPopupItems, getDoorhangerCheckbox,
+            getDisplayedPopupItems, getDoorhangerCheckbox, waitForMasterPasswordDialog,
             getNotification, getDoorhangerButton, removeAllRecords, testDialog */
 
 "use strict";
 
-ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
-ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", this);
+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 = "https://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_creditcard_basic.html";
+const CREDITCARD_FORM_URL =
+  "https://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_creditcard_basic.html";
 const FTU_PREF = "extensions.formautofill.firstTimeUse";
 const CREDITCARDS_USED_STATUS_PREF = "extensions.formautofill.creditCards.used";
 const ENABLED_AUTOFILL_ADDRESSES_PREF = "extensions.formautofill.addresses.enabled";
 const AUTOFILL_CREDITCARDS_AVAILABLE_PREF = "extensions.formautofill.creditCards.available";
 const ENABLED_AUTOFILL_CREDITCARDS_PREF = "extensions.formautofill.creditCards.enabled";
 const SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.supportedCountries";
 const SYNC_USERNAME_PREF = "services.sync.username";
 const SYNC_ADDRESSES_PREF = "services.sync.engine.addresses";
@@ -320,16 +321,34 @@ async function clickDoorhangerButton(but
 function getDoorhangerCheckbox() {
   return getNotification().checkbox;
 }
 
 function getDoorhangerButton(button) {
   return getNotification()[button];
 }
 
+
+// Wait for the master password dialog to popup and enter the password to log in
+// if "login" is "true" or dismiss it directly if otherwise.
+function waitForMasterPasswordDialog(login = false) {
+  info("expecting master password dialog loaded");
+  let dialogShown = TestUtils.topicObserved("common-dialog-loaded");
+  return dialogShown.then(([subject]) => {
+    let dialog = subject.Dialog;
+    is(dialog.args.title, "Password Required", "Master password dialog shown");
+    if (login) {
+      dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
+      dialog.ui.button0.click();
+    } else {
+      dialog.ui.button1.click();
+    }
+  });
+}
+
 async function removeAllRecords() {
   let addresses = await getAddresses();
   if (addresses.length) {
     await removeAddresses(addresses.map(address => address.guid));
   }
 
   let creditCards = await getCreditCards();
   if (creditCards.length) {
@@ -342,26 +361,19 @@ async function waitForFocusAndFormReady(
     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.record) {
     arg.record = Object.assign({}, arg.record, {
-      "cc-number": await OSKeyStore.decrypt(arg.record["cc-number-encrypted"]),
+      "cc-number": await MasterPassword.decrypt(arg.record["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;
 }
 
-add_task(function setup() {
-  OSKeyStoreTestUtils.setup();
-});
-
 registerCleanupFunction(removeAllRecords);
-registerCleanupFunction(async () => {
-  await OSKeyStoreTestUtils.cleanup();
-});
deleted file mode 100644
--- a/browser/extensions/formautofill/test/fixtures/OSKeyStoreTestUtils.jsm
+++ /dev/null
@@ -1,93 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = [
-  "OSKeyStoreTestUtils",
-];
-
-ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
-// TODO: Consider AppConstants.MOZILLA_OFFICIAL to decide if we could test re-auth (bug 1429265).
-/*
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-*/
-ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
-ChromeUtils.import("resource://testing-common/TestUtils.jsm");
-
-var OSKeyStoreTestUtils = {
-  /*
-  TEST_ONLY_REAUTH: "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin",
-  */
-
-  setup() {
-    // TODO: run tests with master password enabled to ensure NSS-implemented
-    // key store prompts on re-auth (bug 1429265)
-    /*
-    LoginTestUtils.masterPassword.enable();
-    */
-
-    this.ORIGINAL_STORE_LABEL = OSKeyStore.STORE_LABEL;
-    OSKeyStore.STORE_LABEL = "test-" + Math.random().toString(36).substr(2);
-  },
-
-  async cleanup() {
-    // TODO: run tests with master password enabled to ensure NSS-implemented
-    // key store prompts on re-auth (bug 1429265)
-    /*
-    LoginTestUtils.masterPassword.disable();
-    */
-
-    await OSKeyStore.cleanup();
-    OSKeyStore.STORE_LABEL = this.ORIGINAL_STORE_LABEL;
-  },
-
-  canTestOSKeyStoreLogin() {
-    // TODO: return true based on whether or not we could test the prompt on
-    // the platform (bug 1429265).
-    /*
-    return OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
-    */
-    return true;
-  },
-
-  // Wait for the master password dialog to popup and enter the password to log in
-  // if "login" is "true" or dismiss it directly if otherwise.
-  async waitForOSKeyStoreLogin(login = false) {
-    // TODO: Always resolves for now, because we are skipping re-auth on all
-    // platforms (bug 1429265).
-    /*
-    if (OSKeyStore.isNSSKeyStore) {
-      await this.waitForMasterPasswordDialog(login);
-      return;
-    }
-
-    const str = login ? "pass" : "cancel";
-
-    Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, str);
-
-    await TestUtils.topicObserved("oskeystore-testonly-reauth",
-      (subject, data) => data == str);
-
-    Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, "");
-    */
-  },
-
-  async waitForMasterPasswordDialog(login = false) {
-    let [subject] = await TestUtils.topicObserved("common-dialog-loaded");
-
-    let dialog = subject.Dialog;
-    if (dialog.args.title !== "Password Required") {
-      throw new Error("Incorrect master password dialog title");
-    }
-
-    if (login) {
-      dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
-      dialog.ui.button0.click();
-    } else {
-      dialog.ui.button1.click();
-    }
-    await TestUtils.waitForTick();
-  },
-};
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -1,11 +1,10 @@
 /* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */
 /* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
-/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/AddTask.js */
 /* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
 /* eslint-disable no-unused-vars */
 
 "use strict";
 
 let formFillChromeScript;
 let defaultTextColor;
 let expectingPopup = null;
@@ -197,25 +196,16 @@ async function cleanUpCreditCards() {
   return invokeAsyncChromeTask("FormAutofillTest:CleanUpCreditCards", "FormAutofillTest:CreditCardsCleanedUp");
 }
 
 async function cleanUpStorage() {
   await cleanUpAddresses();
   await cleanUpCreditCards();
 }
 
-async function canTestOSKeyStoreLogin() {
-  let {canTest} = await invokeAsyncChromeTask("FormAutofillTest:CanTestOSKeyStoreLogin", "FormAutofillTest:CanTestOSKeyStoreLoginResult");
-  return canTest;
-}
-
-async function waitForOSKeyStoreLogin(login = false) {
-  await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", "FormAutofillTest:OSKeyStoreLoggedIn", {login});
-}
-
 function patchRecordCCNumber(record) {
   const number = record["cc-number"];
   const ccNumberFmt = {
     affix: "****",
     label: number.substr(-4),
   };
 
   return Object.assign({}, record, {ccNumberFmt});
@@ -268,22 +258,16 @@ function formAutoFillCommonSetup() {
   formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
   formFillChromeScript.addMessageListener("onpopupshown", ({results}) => {
     gLastAutoCompleteResults = results;
     if (gPopupShownListener) {
       gPopupShownListener({results});
     }
   });
 
-  add_task(async function setup() {
-    formFillChromeScript.sendAsyncMessage("setup");
-    info(`expecting the storage setup`);
-    await formFillChromeScript.promiseOneMessage("setup-finished");
-  });
-
   SimpleTest.registerCleanupFunction(async () => {
     formFillChromeScript.sendAsyncMessage("cleanup");
     info(`expecting the storage cleanup`);
     await formFillChromeScript.promiseOneMessage("cleanup-finished");
 
     formFillChromeScript.destroy();
     expectingPopup = null;
   });
--- a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
@@ -1,19 +1,16 @@
 // assert is available to chrome scripts loaded via SpecialPowers.loadChromeScript.
 /* global assert */
 /* eslint-env mozilla/frame-script */
 
 "use strict";
 
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
-ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm");
 
 let {formAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {});
 
 const {ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME} = FormAutofillUtils;
 
 var ParentUtils = {
   async _getRecords(collectionName) {
     return new Promise(resolve => {
@@ -112,25 +109,19 @@ var ParentUtils = {
     if (guids.length == 0) {
       sendAsyncMessage("FormAutofillTest:CreditCardsCleanedUp");
       return;
     }
 
     await this.operateCreditCard("remove", {guids}, "FormAutofillTest:CreditCardsCleanedUp");
   },
 
-  setup() {
-    OSKeyStoreTestUtils.setup();
-  },
-
   async cleanup() {
     await this.cleanUpAddresses();
     await this.cleanUpCreditCards();
-    await OSKeyStoreTestUtils.cleanup();
-
     Services.obs.removeObserver(this, "formautofill-storage-changed");
   },
 
   _areRecordsMatching(recordA, recordB, collectionName) {
     for (let field of formAutofillStorage[collectionName].VALID_FIELDS) {
       if (recordA[field] !== recordB[field]) {
         return false;
       }
@@ -219,28 +210,13 @@ addMessageListener("FormAutofillTest:Rem
 addMessageListener("FormAutofillTest:CheckCreditCards", (msg) => {
   ParentUtils.checkCreditCards(msg);
 });
 
 addMessageListener("FormAutofillTest:CleanUpCreditCards", (msg) => {
   ParentUtils.cleanUpCreditCards();
 });
 
-addMessageListener("FormAutofillTest:CanTestOSKeyStoreLogin", (msg) => {
-  sendAsyncMessage("FormAutofillTest:CanTestOSKeyStoreLoginResult",
-    {canTest: OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL});
-});
-
-addMessageListener("FormAutofillTest:OSKeyStoreLogin", async (msg) => {
-  await OSKeyStoreTestUtils.waitForOSKeyStoreLogin(msg.login);
-  sendAsyncMessage("FormAutofillTest:OSKeyStoreLoggedIn");
+addMessageListener("cleanup", () => {
+  ParentUtils.cleanup().then(() => {
+    sendAsyncMessage("cleanup-finished", {});
+  });
 });
-
-addMessageListener("setup", async () => {
-  ParentUtils.setup();
-  sendAsyncMessage("setup-finished", {});
-});
-
-addMessageListener("cleanup", async () => {
-  await ParentUtils.cleanup();
-
-  sendAsyncMessage("cleanup-finished", {});
-});
--- a/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
@@ -145,60 +145,41 @@ add_task(async function check_search_res
   await setInput("#cc-name", "");
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(["John Smith"], false);
 
   await SpecialPowers.popPrefEnv();
 });
 
-let canTest;
-
 // Autofill the credit card from dropdown menu.
 add_task(async function check_fields_after_form_autofill() {
-  canTest = await canTestOSKeyStoreLogin();
-  if (!canTest) {
-    todo(canTest, "Cannot test OS key store login on official builds.");
-    return;
-  }
-
   await setInput("#cc-exp-year", 202);
 
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.slice(1).map(patchRecordCCNumber).map(cc => JSON.stringify({
     primary: cc["cc-exp-year"],
     secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
   })));
 
   synthesizeKey("KEY_ArrowDown");
-  let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
-  await new Promise(resolve => SimpleTest.executeSoon(resolve));
   await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]);
-  await osKeyStoreLoginShown;
 });
 
 // Fallback to history search after autofill address.
 add_task(async function check_fallback_after_form_autofill() {
-  if (!canTest) {
-    return;
-  }
-
   await setInput("#cc-name", "", true);
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(["John Smith"], false);
 });
 
 // Resume form autofill once all the autofilled fileds are changed.
 add_task(async function check_form_autofill_resume() {
-  if (!canTest) {
-    return;
-  }
-
   document.querySelector("#cc-name").blur();
   document.querySelector("#form1").reset();
 
   await setInput("#cc-name", "");
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
     primary: cc["cc-name"],
--- a/browser/extensions/formautofill/test/mochitest/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_clear_form.html
@@ -98,26 +98,19 @@ add_task(async function clear_modified_f
   await setInput("#tel", "+1111111111", true);
 
   await triggerPopupAndHoverItem("#street-address", 0);
   await confirmClear("#street-address");
   checkIsFormCleared({tel: "+1111111111"});
 });
 
 add_task(async function clear_distinct_section() {
-  if (!(await canTestOSKeyStoreLogin())) {
-    todo(false, "Cannot test OS key store login on official builds.");
-    return;
-  }
-
   document.getElementById("form1").reset();
   await triggerPopupAndHoverItem("#cc-name", 0);
-  let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
   await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE[0]);
-  await osKeyStoreLoginShown;
 
   await triggerPopupAndHoverItem("#organization", 0);
   await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
   await triggerPopupAndHoverItem("#street-address", 0);
   await confirmClear("#street-address");
 
   for (const [id, val] of Object.entries(MOCK_CC_STORAGE[0])) {
     const element = document.getElementById(id);
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -94,17 +94,17 @@ async function initProfileStorage(fileNa
 
   let onChanged = TestUtils.topicObserved(
     "formautofill-storage-changed",
     (subject, data) =>
       data == "add" &&
       subject.wrappedJSObject.collectionName == collectionName
   );
   for (let record of records) {
-    Assert.ok(await profileStorage[collectionName].add(record));
+    Assert.ok(profileStorage[collectionName].add(record));
     await onChanged;
   }
   await profileStorage._saveImmediately();
   return profileStorage;
 }
 
 function verifySectionFieldDetails(sections, expectedResults) {
   Assert.equal(sections.length, expectedResults.length, "Expected section count.");
@@ -218,18 +218,8 @@ add_task(async function head_initialize(
     Services.prefs.clearUserPref("extensions.formautofill.creditCards.available");
     Services.prefs.clearUserPref("extensions.formautofill.heuristics.enabled");
     Services.prefs.clearUserPref("extensions.formautofill.section.enabled");
     Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
   });
 
   await loadExtension();
 });
-
-let OSKeyStoreTestUtils;
-add_task(async function os_key_store_setup() {
-  ({OSKeyStoreTestUtils} =
-    ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", {}));
-  OSKeyStoreTestUtils.setup();
-  registerCleanupFunction(async function cleanup() {
-    await OSKeyStoreTestUtils.cleanup();
-  });
-});
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -1,18 +1,19 @@
 /*
  * Test for form auto fill content helper fill all inputs function.
  */
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 
 "use strict";
 
+let MasterPassword;
 add_task(async function setup() {
   ChromeUtils.import("resource://formautofill/FormAutofillHandler.jsm");
-  ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
+  ({MasterPassword} = ChromeUtils.import("resource://formautofill/MasterPassword.jsm", {}));
 });
 
 const TESTCASES = [
   {
     description: "Form without autocomplete property",
     document: `<form><input id="given-name"><input id="family-name">
                <input id="street-addr"><input id="city"><select id="country"></select>
                <input id='email'><input id="tel"></form>`,
@@ -471,31 +472,38 @@ const TESTCASES_FILL_SELECT = [
 function do_test(testcases, testFn) {
   for (let tc of testcases) {
     (function() {
       let testcase = tc;
       add_task(async function() {
         info("Starting testcase: " + testcase.description);
         let ccNumber = testcase.profileData["cc-number"];
         if (ccNumber) {
-          testcase.profileData["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
+          testcase.profileData["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
           delete testcase.profileData["cc-number"];
         }
 
         let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                                   testcase.document);
         let form = doc.querySelector("form");
         let formLike = FormLikeFactory.createFromForm(form);
         let handler = new FormAutofillHandler(formLike);
         let promises = [];
-        // Replace the internal decrypt method with OSKeyStore API,
-        // but don't pass the reauth parameter to avoid triggering
-        // reauth login dialog in these tests.
+        // Replace the interal decrypt method with MasterPassword API
         let decryptHelper = async (cipherText, reauth) => {
-          return OSKeyStore.decrypt(cipherText, false);
+          let string;
+          try {
+            string = await MasterPassword.decrypt(cipherText, reauth);
+          } catch (e) {
+            if (e.result != Cr.NS_ERROR_ABORT) {
+              throw e;
+            }
+            info("User canceled master password entry");
+          }
+          return string;
         };
 
         handler.collectFormFields();
 
         let focusedInput = doc.getElementById(testcase.focusedInputId);
         handler.focusedInput = focusedInput;
 
         for (let section of handler.sections) {
--- a/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
@@ -311,17 +311,17 @@ add_task(async function test_add() {
   let creditCards = await profileStorage.creditCards.getAll();
 
   Assert.equal(creditCards.length, 2);
 
   do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
   do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
 
   Assert.notEqual(creditCards[0].guid, undefined);
-  Assert.equal(creditCards[0].version, 2);
+  Assert.equal(creditCards[0].version, 1);
   Assert.notEqual(creditCards[0].timeCreated, undefined);
   Assert.equal(creditCards[0].timeLastModified, creditCards[0].timeCreated);
   Assert.equal(creditCards[0].timeLastUsed, 0);
   Assert.equal(creditCards[0].timesUsed, 0);
 
   // Empty string should be deleted before saving.
   await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
   let creditCard = profileStorage.creditCards._data[2];
@@ -417,24 +417,16 @@ add_task(async function test_update() {
   creditCard = profileStorage.creditCards._data[0];
   Assert.equal(creditCard["cc-number"],
     CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
   await profileStorage.creditCards.update(profileStorage.creditCards._data[1].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, true);
   creditCard = profileStorage.creditCards._data[1];
   Assert.equal(creditCard["cc-number"],
     CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
 
-  // Decryption failure of existing record should not prevent it from being updated.
-  creditCard = profileStorage.creditCards._data[0];
-  creditCard["cc-number-encrypted"] = "INVALID";
-  await profileStorage.creditCards.update(profileStorage.creditCards._data[0].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, false);
-  creditCard = profileStorage.creditCards._data[0];
-  Assert.equal(creditCard["cc-number"],
-    CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
-
   await Assert.rejects(profileStorage.creditCards.update("INVALID_GUID", TEST_CREDIT_CARD_3),
     /No matching record\./
   );
 
   await Assert.rejects(profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_WITH_INVALID_FIELD),
     /"invalidField" is not a valid field\./
   );
 
@@ -658,17 +650,23 @@ add_task(async function test_getDuplicat
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 
   // Numbers with the same last 4 digits shouldn't be treated as a duplicate.
   record = Object.assign({}, TEST_CREDIT_CARD_3);
   let last4Digits = record["cc-number"].substr(-4);
   // This number differs from TEST_CREDIT_CARD_3 by swapping the order of the
   // 09 and 90 adjacent digits, which is still a valid credit card number.
   record["cc-number"] = "358999378390" + last4Digits;
+  Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 
-  // We treat numbers with the same last 4 digits as a duplicate.
+  // ... However, we treat numbers with the same last 4 digits as a duplicate if
+  // the master password is enabled.
+  let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB);
+  let token = tokendb.getInternalKeyToken();
+  token.reset();
+  token.initPassword("password");
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), guid);
 
-  // Even though the last 4 digits are the same, an invalid credit card number
-  // should never be treated as a duplicate.
+  // ... Even though the master password is enabled and the last 4 digits are the
+  // same, an invalid credit card number should never be treated as a duplicate.
   record["cc-number"] = "************" + last4Digits;
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 });
--- a/browser/extensions/formautofill/test/unit/test_getRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_getRecords.js
@@ -2,20 +2,19 @@
  * Test for make sure getRecords can retrieve right collection from storage.
  */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
 
 let FormAutofillParent;
-let OSKeyStore;
 add_task(async function setup() {
   ({FormAutofillParent} = ChromeUtils.import("resource://formautofill/FormAutofillParent.jsm", {}));
-  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
+  ChromeUtils.import("resource://formautofill/MasterPassword.jsm");
 });
 
 const TEST_ADDRESS_1 = {
   "given-name": "Timothy",
   "additional-name": "John",
   "family-name": "Berners-Lee",
   organization: "World Wide Web Consortium",
   "street-address": "32 Vassar Street\nMIT Room 32-G524",
@@ -172,63 +171,84 @@ add_task(async function test_getRecords_
   let formAutofillParent = new FormAutofillParent();
 
   await formAutofillParent.init();
   await formAutofillParent.formAutofillStorage.initialize();
   let collection = formAutofillParent.formAutofillStorage.creditCards;
   let encryptedCCRecords = await Promise.all([TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2].map(async record => {
     let clonedRecord = Object.assign({}, record);
     clonedRecord["cc-number"] = CreditCard.getLongMaskedNumber(record["cc-number"]);
-    clonedRecord["cc-number-encrypted"] = await OSKeyStore.encrypt(record["cc-number"]);
+    clonedRecord["cc-number-encrypted"] = await MasterPassword.encrypt(record["cc-number"]);
     return clonedRecord;
   }));
   sinon.stub(collection, "getAll", () =>
     Promise.resolve([Object.assign({}, encryptedCCRecords[0]), Object.assign({}, encryptedCCRecords[1])]));
+  let CreditCardsWithDecryptedNumber = [
+    Object.assign({}, encryptedCCRecords[0], {"cc-number-decrypted": TEST_CREDIT_CARD_1["cc-number"]}),
+    Object.assign({}, encryptedCCRecords[1], {"cc-number-decrypted": TEST_CREDIT_CARD_2["cc-number"]}),
+  ];
 
   let testCases = [
     {
-      description: "If the search string could match multiple creditCards",
+      description: "If the search string could match 1 creditCard (without masterpassword)",
+      filter: {
+        collectionName: "creditCards",
+        info: {fieldName: "cc-name"},
+        searchString: "John Doe",
+      },
+      expectedResult: CreditCardsWithDecryptedNumber.slice(0, 1),
+    },
+    {
+      description: "If the search string could match multiple creditCards (without masterpassword)",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "John",
       },
-      expectedResult: encryptedCCRecords,
+      expectedResult: CreditCardsWithDecryptedNumber,
     },
     {
-      description: "If the search string could not match any creditCard",
+      description: "If the search string could not match any creditCard (without masterpassword)",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "T",
       },
       expectedResult: [],
     },
     {
-      description: "Return all creditCards if focused field is cc number; " +
-        "if the search string could match multiple creditCards",
+      description: "If the search number string could match 1 creditCard (without masterpassword)",
+      filter: {
+        collectionName: "creditCards",
+        info: {fieldName: "cc-number"},
+        searchString: "411",
+      },
+      expectedResult: CreditCardsWithDecryptedNumber.slice(0, 1),
+    },
+    {
+      description: "If the search string could match multiple creditCards (without masterpassword)",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-number"},
         searchString: "4",
       },
-      expectedResult: encryptedCCRecords,
+      expectedResult: CreditCardsWithDecryptedNumber,
     },
     {
-      description: "If the search string could match 1 creditCard",
+      description: "If the search string could match 1 creditCard (with masterpassword)",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "John Doe",
       },
       mpEnabled: true,
       expectedResult: encryptedCCRecords.slice(0, 1),
     },
     {
-      description: "Return all creditCards if focused field is cc number",
+      description: "Return all creditCards if focused field is cc number (with masterpassword)",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-number"},
         searchString: "411",
       },
       mpEnabled: true,
       expectedResult: encryptedCCRecords,
     },
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_masterPassword.js
@@ -0,0 +1,119 @@
+/**
+ * Tests of MasterPassword.jsm
+ */
+
+"use strict";
+const {MockRegistrar} =
+  ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", {});
+
+let MasterPassword;
+add_task(async function setup() {
+  ({MasterPassword} = ChromeUtils.import("resource://formautofill/MasterPassword.jsm", {}));
+});
+
+const TESTCASES = [{
+  description: "With master password set",
+  masterPassword: "fakemp",
+  mpEnabled: true,
+},
+{
+  description: "Without master password set",
+  masterPassword: "", // "" means no master password
+  mpEnabled: false,
+}];
+
+
+// Tests that PSM can successfully ask for a password from the user and relay it
+// back to NSS. Does so by mocking out the actual dialog and "filling in" the
+// password. Also tests that providing an incorrect password will fail (well,
+// technically the user will just get prompted again, but if they then cancel
+// the dialog the overall operation will fail).
+
+let gMockPrompter = {
+  passwordToTry: null,
+  numPrompts: 0,
+
+  // This intentionally does not use arrow function syntax to avoid an issue
+  // where in the context of the arrow function, |this != gMockPrompter| due to
+  // how objects get wrapped when going across xpcom boundaries.
+  promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
+    this.numPrompts++;
+    if (this.numPrompts > 1) { // don't keep retrying a bad password
+      return false;
+    }
+    equal(text,
+          "Please enter your master password.",
+          "password prompt text should be as expected");
+    equal(checkMsg, null, "checkMsg should be null");
+    ok(this.passwordToTry, "passwordToTry should be non-null");
+    password.value = this.passwordToTry;
+    return true;
+  },
+
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
+};
+
+// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
+// to call promptPassword. We return the mock one, above.
+let gWindowWatcher = {
+  getNewPrompter: () => gMockPrompter,
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
+};
+
+// Ensure that the appropriate initialization has happened.
+do_get_profile();
+
+let windowWatcherCID =
+  MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
+                         gWindowWatcher);
+registerCleanupFunction(() => {
+  MockRegistrar.unregister(windowWatcherCID);
+});
+
+TESTCASES.forEach(testcase => {
+  add_task(async function test_encrypt_decrypt() {
+    info("Starting testcase: " + testcase.description);
+
+    let token = MasterPassword._token;
+    token.initPassword(testcase.masterPassword);
+
+    // Test only: Force the token login without asking for master password
+    token.login(/* force */ false);
+    Assert.equal(testcase.mpEnabled, token.isLoggedIn(), "Token should now be logged into");
+    Assert.equal(MasterPassword.isEnabled, testcase.mpEnabled);
+
+    let testText = "test string";
+    let cipherText = await MasterPassword.encrypt(testText);
+    Assert.notEqual(testText, cipherText);
+    let plainText = await MasterPassword.decrypt(cipherText);
+    Assert.equal(testText, plainText);
+    if (token.isLoggedIn()) {
+      // Reset state.
+      gMockPrompter.numPrompts = 0;
+      token.logoutSimple();
+
+      ok(!token.isLoggedIn(),
+         "Token should be logged out after calling logoutSimple()");
+
+      // Try with the correct password.
+      gMockPrompter.passwordToTry = testcase.masterPassword;
+      await MasterPassword.encrypt(testText);
+      Assert.equal(gMockPrompter.numPrompts, 1, "should have prompted for encryption");
+
+      // Reset state.
+      gMockPrompter.numPrompts = 0;
+      token.logoutSimple();
+
+      try {
+        // Try with the incorrect password.
+        gMockPrompter.passwordToTry = "XXX";
+        await MasterPassword.decrypt(cipherText);
+        throw new Error("Not receiving canceled master password error");
+      } catch (e) {
+        Assert.equal(e.message, "User canceled master password entry");
+      }
+    }
+
+    token.reset();
+  });
+});
--- a/browser/extensions/formautofill/test/unit/test_migrateRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_migrateRecords.js
@@ -1,27 +1,24 @@
 /**
  * Tests the migration algorithm in profileStorage.
  */
 
 "use strict";
 
-ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+let FormAutofillStorage;
 
-let FormAutofillStorage;
-let OSKeyStore;
 add_task(async function setup() {
   ({FormAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {}));
-  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
 });
 
 const TEST_STORE_FILE_NAME = "test-profile.json";
 
 const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 2;
+const CREDIT_CARD_SCHEMA_VERSION = 1;
 
 const ADDRESS_TESTCASES = [
   {
     description: "The record version is equal to the current version. The migration shouldn't be invoked.",
     record: {
       guid: "test-guid",
       version: ADDRESS_SCHEMA_VERSION,
       "given-name": "Timothy",
@@ -244,98 +241,27 @@ let do_check_record_matches = (expectedR
 };
 
 add_task(async function test_migrateAddressRecords() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
-  for (let testcase of ADDRESS_TESTCASES) {
+  await Promise.all(ADDRESS_TESTCASES.map(async testcase => {
     info(testcase.description);
-    profileStorage._store.data.addresses = [testcase.record];
-    await profileStorage.addresses._migrateRecord(testcase.record, 0);
-    do_check_record_matches(testcase.expectedResult, profileStorage.addresses._data[0]);
-  }
+    await profileStorage.addresses._migrateRecord(testcase.record);
+    do_check_record_matches(testcase.expectedResult, testcase.record);
+  }));
 });
 
 add_task(async function test_migrateCreditCardRecords() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
-  for (let testcase of CREDIT_CARD_TESTCASES) {
+  await Promise.all(CREDIT_CARD_TESTCASES.map(async testcase => {
     info(testcase.description);
-    profileStorage._store.data.creditCards = [testcase.record];
-    await profileStorage.creditCards._migrateRecord(testcase.record, 0);
-    do_check_record_matches(testcase.expectedResult, profileStorage.creditCards._data[0]);
-  }
+    await profileStorage.creditCards._migrateRecord(testcase.record);
+    do_check_record_matches(testcase.expectedResult, testcase.record);
+  }));
 });
-
-add_task(async function test_migrateEncryptedCreditCardNumber() {
-  let path = getTempFile(TEST_STORE_FILE_NAME).path;
-
-  let profileStorage = new FormAutofillStorage(path);
-  await profileStorage.initialize();
-
-  const ccNumber = "4111111111111111";
-  let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"]
-    .createInstance(Ci.nsILoginManagerCrypto);
-
-  info("Encrypted credit card should be migrated from v1 to v2");
-
-  let record = {
-    guid: "test-guid",
-    version: 1,
-    "cc-name": "Timothy",
-    "cc-number-encrypted": cryptoSDR.encrypt(ccNumber),
-  };
-
-  let expectedRecord = {
-    guid: "test-guid",
-    version: CREDIT_CARD_SCHEMA_VERSION,
-    "cc-name": "Timothy",
-    "cc-given-name": "Timothy",
-  };
-  profileStorage._store.data.creditCards = [record];
-  await profileStorage.creditCards._migrateRecord(record, 0);
-  record = profileStorage.creditCards._data[0];
-
-  Assert.equal(expectedRecord.guid, record.guid);
-  Assert.equal(expectedRecord.version, record.version);
-  Assert.equal(expectedRecord["cc-name"], record["cc-name"]);
-  Assert.equal(expectedRecord["cc-given-name"], record["cc-given-name"]);
-
-  // Ciphertext of OS Key Store is not stable, must compare decrypted text here.
-  Assert.equal(ccNumber, await OSKeyStore.decrypt(record["cc-number-encrypted"]));
-});
-
-add_task(async function test_migrateEncryptedCreditCardNumberWithMP() {
-  LoginTestUtils.masterPassword.enable();
-
-  let path = getTempFile(TEST_STORE_FILE_NAME).path;
-
-  let profileStorage = new FormAutofillStorage(path);
-  await profileStorage.initialize();
-
-  info("Encrypted credit card should be migrated a tombstone if MP is enabled");
-
-  let record = {
-    guid: "test-guid",
-    version: 1,
-    "cc-name": "Timothy",
-    "cc-number-encrypted": "(encrypted to be discarded)",
-  };
-
-  profileStorage._store.data.creditCards = [record];
-  await profileStorage.creditCards._migrateRecord(record, 0);
-  record = profileStorage.creditCards._data[0];
-
-  Assert.equal(record.guid, "test-guid");
-  Assert.equal(record.deleted, true);
-  Assert.equal(typeof record.version, "undefined");
-  Assert.equal(typeof record["cc-name"], "undefined");
-  Assert.equal(typeof record["cc-number-encrypted"], "undefined");
-
-  LoginTestUtils.masterPassword.disable();
-});
-
deleted file mode 100644
--- a/browser/extensions/formautofill/test/unit/test_osKeyStore.js
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * Tests of OSKeyStore.jsm
- */
-
-"use strict";
-
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-ChromeUtils.import("resource://testing-common/MockRegistrar.jsm");
-
-let OSKeyStore;
-add_task(async function setup() {
-  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
-});
-
-// Ensure that the appropriate initialization has happened.
-do_get_profile();
-
-// For NSS key store, mocking out the dialog and control it from here.
-let gMockPrompter = {
-  passwordToTry: "hunter2",
-  resolve: null,
-  login: undefined,
-
-  // This intentionally does not use arrow function syntax to avoid an issue
-  // where in the context of the arrow function, |this != gMockPrompter| due to
-  // how objects get wrapped when going across xpcom boundaries.
-  promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
-    equal(text,
-          "Please enter your master password.",
-          "password prompt text should be as expected");
-    equal(checkMsg, null, "checkMsg should be null");
-    if (this.login) {
-      password.value = this.passwordToTry;
-    }
-    this.resolve();
-    this.resolve = null;
-
-    return this.login;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
-};
-
-// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
-// to call promptPassword. We return the mock one, above.
-let gWindowWatcher = {
-  getNewPrompter: () => gMockPrompter,
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
-};
-
-let nssToken;
-
-const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
-
-async function waitForReauth(login = false) {
-  if (OSKeyStore.isNSSKeyStore) {
-    gMockPrompter.login = login;
-    await new Promise(resolve => { gMockPrompter.resolve = resolve; });
-
-    return;
-  }
-
-  let value = login ? "pass" : "cancel";
-  Services.prefs.setStringPref(TEST_ONLY_REAUTH, value);
-  await TestUtils.topicObserved("oskeystore-testonly-reauth",
-    (subject, data) => data == value);
-}
-
-const testText = "test string";
-let cipherText;
-
-add_task(async function test_encrypt_decrypt() {
-  Assert.equal(await OSKeyStore.ensureLoggedIn(), true, "Started logged in.");
-
-  cipherText = await OSKeyStore.encrypt(testText);
-  Assert.notEqual(testText, cipherText);
-
-  let plainText = await OSKeyStore.decrypt(cipherText);
-  Assert.equal(testText, plainText);
-});
-
-// TODO: skipped because re-auth is not implemented (bug 1429265).
-add_task(async function test_reauth() {
-  let canTest = OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
-  if (!canTest) {
-    todo_check_false(canTest,
-      "test_reauth: Cannot test OS key store login on official builds.");
-    return;
-  }
-
-  if (OSKeyStore.isNSSKeyStore) {
-    let windowWatcherCID;
-    windowWatcherCID =
-      MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
-                             gWindowWatcher);
-    registerCleanupFunction(() => {
-      MockRegistrar.unregister(windowWatcherCID);
-    });
-
-    // If we use the NSS key store implementation test that everything works
-    // when a master password is set.
-    // Set an initial password.
-    let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]
-                    .getService(Ci.nsIPK11TokenDB);
-    nssToken = tokenDB.getInternalKeyToken();
-    nssToken.initPassword("hunter2");
-  }
-
-  let reauthObserved = waitForReauth(false);
-  await new Promise(resolve => TestUtils.executeSoon(resolve));
-  try {
-    await OSKeyStore.decrypt(cipherText, true);
-    throw new Error("Not receiving canceled OS unlock error");
-  } catch (ex) {
-    Assert.equal(ex.message, "User canceled OS unlock entry");
-    Assert.equal(ex.result, Cr.NS_ERROR_ABORT);
-  }
-  await reauthObserved;
-
-  reauthObserved = waitForReauth(false);
-  await new Promise(resolve => TestUtils.executeSoon(resolve));
-  Assert.equal(await OSKeyStore.ensureLoggedIn(true), false, "Reauth cancelled.");
-  await reauthObserved;
-
-  reauthObserved = waitForReauth(true);
-  await new Promise(resolve => TestUtils.executeSoon(resolve));
-  let plainText2 = await OSKeyStore.decrypt(cipherText, true);
-  await reauthObserved;
-  Assert.equal(testText, plainText2);
-
-  reauthObserved = waitForReauth(true);
-  await new Promise(resolve => TestUtils.executeSoon(resolve));
-  Assert.equal(await OSKeyStore.ensureLoggedIn(true), true, "Reauth logged in.");
-  await reauthObserved;
-}).skip();
-
-add_task(async function test_decryption_failure() {
-  try {
-    await OSKeyStore.decrypt("Malformed cipher text");
-    throw new Error("Not receiving decryption error");
-  } catch (ex) {
-    Assert.notEqual(ex.result, Cr.NS_ERROR_ABORT);
-  }
-});
--- a/browser/extensions/formautofill/test/unit/test_reconcile.js
+++ b/browser/extensions/formautofill/test/unit/test_reconcile.js
@@ -465,220 +465,220 @@ const ADDRESS_RECONCILE_TESTCASES = [
 ];
 
 const CREDIT_CARD_RECONCILE_TESTCASES = [
   {
     description: "Local change",
     parent: {
       // So when we last wrote the record to the server, it had these values.
       "guid": "2bbd2d8fbc6b",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       // The current local record - by comparing against parent we can see that
       // only the cc-number has changed locally.
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       // This is the incoming record. It has the same values as "parent", so
       // we can deduce the record hasn't actually been changed remotely so we
       // can safely ignore the incoming record and write our local changes.
       "guid": "2bbd2d8fbc6b",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "2bbd2d8fbc6b",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Remote change",
     parent: {
       "guid": "e3680e9f890d",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "e3680e9f890d",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     reconciled: {
       "guid": "e3680e9f890d",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
 
   {
     description: "New local field",
     parent: {
       "guid": "0cba738b1be0",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "0cba738b1be0",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "0cba738b1be0",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
   },
   {
     description: "New remote field",
     parent: {
       "guid": "be3ef97f8285",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "be3ef97f8285",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     reconciled: {
       "guid": "be3ef97f8285",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
   },
   {
     description: "Deleted field locally",
     parent: {
       "guid": "9627322248ec",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "9627322248ec",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     reconciled: {
       "guid": "9627322248ec",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
   },
   {
     description: "Deleted field remotely",
     parent: {
       "guid": "7d7509f3eeb2",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "7d7509f3eeb2",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "7d7509f3eeb2",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
   },
   {
     description: "Local and remote changes to unrelated fields",
     parent: {
       // The last time we wrote this to the server, "cc-exp-month" was 12.
       "guid": "e087a06dfc57",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       // The current local record - so locally we've changed "cc-number".
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 12,
     }],
     remote: {
       // Remotely, we've changed "cc-exp-month" to 1.
       "guid": "e087a06dfc57",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 1,
     },
     reconciled: {
       "guid": "e087a06dfc57",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 1,
     },
   },
   {
     description: "Multiple local changes",
     parent: {
       "guid": "340a078c596f",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
     }, {
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "340a078c596f",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-year": 2000,
     },
     reconciled: {
       "guid": "340a078c596f",
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
@@ -687,54 +687,54 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
     },
   },
   {
     // Local and remote diverged from the shared parent, but the values are the
     // same, so we shouldn't fork.
     description: "Same change to local and remote",
     parent: {
       "guid": "0b3a72a1bea2",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       "guid": "0b3a72a1bea2",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     reconciled: {
       "guid": "0b3a72a1bea2",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Conflicting changes to single field",
     parent: {
       // This is what we last wrote to the sync server.
       "guid": "62068784d089",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       // The current version of the local record - the cc-number has changed locally.
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
     }],
     remote: {
       // An incoming record has a different cc-number than any of the above!
       "guid": "62068784d089",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     forked: {
       // So we've forked the local record to a new GUID (and the next sync is
       // going to write this as a new record)
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
@@ -745,29 +745,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Conflicting changes to multiple fields",
     parent: {
       "guid": "244dbb692e94",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
       "cc-exp-month": 1,
     }],
     remote: {
       "guid": "244dbb692e94",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 3,
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
       "cc-exp-month": 1,
@@ -778,28 +778,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-number": "4929001587121045",
       "cc-exp-month": 3,
     },
   },
   {
     description: "Field deleted locally, changed remotely",
     parent: {
       "guid": "6fc45e03d19a",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "6fc45e03d19a",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
@@ -809,29 +809,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
   },
   {
     description: "Field changed locally, deleted remotely",
     parent: {
       "guid": "fff9fa27fa18",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     }],
     remote: {
       "guid": "fff9fa27fa18",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
@@ -843,28 +843,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
   },
   {
     // Created, last modified should be synced; last used and times used should
     // be local. Remote created time older than local, remote modified time
     // newer than local.
     description: "Created, last modified time reconciliation without local changes",
     parent: {
       "guid": "5113f329c42f",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1234,
       "timeLastModified": 5678,
       "timeLastUsed": 5678,
       "timesUsed": 6,
     },
     local: [],
     remote: {
       "guid": "5113f329c42f",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1200,
       "timeLastModified": 5700,
       "timeLastUsed": 5700,
       "timesUsed": 3,
     },
     reconciled: {
@@ -878,31 +878,31 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
     },
   },
   {
     // Local changes, remote created time newer than local, remote modified time
     // older than local.
     description: "Created, last modified time reconciliation with local changes",
     parent: {
       "guid": "791e5608b80a",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1234,
       "timeLastModified": 5678,
       "timeLastUsed": 5678,
       "timesUsed": 6,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       "guid": "791e5608b80a",
-      "version": 2,
+      "version": 1,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1300,
       "timeLastModified": 5000,
       "timeLastUsed": 5000,
       "timesUsed": 3,
     },
     reconciled: {
@@ -917,17 +917,17 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
 ];
 
 add_task(async function test_reconcile_unknown_version() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
 
   // Cross-version reconciliation isn't supported yet. See bug 1377204.
   await Assert.rejects(profileStorage.addresses.reconcile({
     "guid": "31d83d2725ec",
-    "version": 3,
+    "version": 2,
     "given-name": "Mark",
     "family-name": "Hammond",
   }), /Got unknown record version/);
 });
 
 add_task(async function test_reconcile_idempotent() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
 
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -33,20 +33,20 @@ support-files =
 [test_getCategoriesFromFieldNames.js]
 [test_getFormInputDetails.js]
 [test_getInfo.js]
 [test_getRecords.js]
 [test_isAvailable.js]
 [test_isCJKName.js]
 [test_isFieldEligibleForAutofill.js]
 [test_markAsAutofillField.js]
+[test_masterPassword.js]
 [test_migrateRecords.js]
 [test_nameUtils.js]
 [test_onFormSubmitted.js]
-[test_osKeyStore.js]
 [test_parseAddressFormat.js]
 [test_profileAutocompleteResult.js]
 [test_phoneNumber.js]
 [test_reconcile.js]
 [test_savedFieldNames.js]
 [test_toOneLineAddress.js]
 [test_storage_tombstones.js]
 [test_storage_remove.js]
--- a/browser/modules/Sanitizer.jsm
+++ b/browser/modules/Sanitizer.jsm
@@ -706,16 +706,24 @@ async function sanitizeOnShutdown(progre
 }
 
 async function sanitizeSessionPrincipals() {
   if (Services.prefs.getIntPref(PREF_COOKIE_LIFETIME,
                                 Ci.nsICookieService.ACCEPT_NORMALLY) != Ci.nsICookieService.ACCEPT_SESSION) {
     return;
   }
 
+  // When PREF_COOKIE_LIFETIME is set to ACCEPT_SESSION, any new cookie will be
+  // marked as session only. But we don't touch the existing ones. For this
+  // reason, here we delete any existing cookie, at shutdown.
+  await new Promise(resolve => {
+    Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_COOKIES,
+                                  resolve);
+  });
+
   let principals = await new Promise(resolve => {
     quotaManagerService.getUsage(request => {
       if (request.resultCode != Cr.NS_OK) {
         // We are probably shutting down. We don't want to propagate the
         // error, rejecting the promise.
         resolve([]);
         return;
       }
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -1121,21 +1121,23 @@ include('compile-checks.configure')
          try_compile(body='static_assert(sizeof(void *) == 8, "")',
                      check_msg='for 64-bit OS'))
 def check_have_64_bit(have_64_bit, compiler_have_64_bit):
     if have_64_bit != compiler_have_64_bit:
         configure_error('The target compiler does not agree with configure '
                         'about the target bitness.')
 
 
-@depends(c_compiler)
-def default_debug_flags(compiler_info):
+@depends(c_compiler, target)
+def default_debug_flags(compiler_info, target):
     # Debug info is ON by default.
     if compiler_info.type in ('msvc', 'clang-cl'):
         return '-Zi'
+    elif target.kernel == 'WINNT' and compiler_info.type == 'clang':
+        return '-g -gcodeview'
     return '-g'
 
 
 option(env='MOZ_DEBUG_FLAGS',
        nargs=1,
        help='Debug compiler flags')
 
 imply_option('--enable-debug-symbols',
@@ -1987,21 +1989,26 @@ add_old_configure_assignment('LIBFUZZER_
 
 # Shared library building
 # ==============================================================
 
 # XXX: The use of makefile constructs in these variables is awful.
 @depends(target, c_compiler)
 def make_shared_library(target, compiler):
     if target.os == 'WINNT':
-        if compiler.type in ('gcc', 'clang'):
+        if compiler.type == 'gcc':
             return namespace(
                 mkshlib=['$(CXX)', '$(DSO_LDOPTS)', '-o', '$@'],
                 mkcshlib=['$(CC)', '$(DSO_LDOPTS)', '-o', '$@'],
             )
+        elif compiler.type == 'clang':
+            return namespace(
+                mkshlib=['$(CXX)', '$(DSO_LDOPTS)', '-Wl,-pdb,$(LINK_PDBFILE)', '-o', '$@'],
+                mkcshlib=['$(CC)', '$(DSO_LDOPTS)', '-Wl,-pdb,$(LINK_PDBFILE)', '-o', '$@'],
+            )
         else:
             linker = [
                 '$(LINKER)',
                 '-NOLOGO', '-DLL',
                 '-OUT:$@',
                 '-PDB:$(LINK_PDBFILE)',
                 '$(DSO_LDOPTS)'
             ]
--- a/build/win64/mozconfig.asan
+++ b/build/win64/mozconfig.asan
@@ -2,16 +2,17 @@
 
 if [ -d "$topsrcdir/clang" ]; then
     CLANG_LIB_DIR="$(cd $topsrcdir/clang/lib/clang/* && cd lib/windows && pwd)"
 
     export LIB=$LIB:$CLANG_LIB_DIR
     mk_export_correct_style LIB
     export LDFLAGS="clang_rt.asan_dynamic-x86_64.lib clang_rt.asan_dynamic_runtime_thunk-x86_64.lib"
 
+    export MOZ_COPY_PDBS=1
     export LLVM_SYMBOLIZER="$topsrcdir/clang/bin/llvm-symbolizer.exe"
     export MOZ_CLANG_RT_ASAN_LIB_PATH="${CLANG_LIB_DIR}/clang_rt.asan_dynamic-x86_64.dll"
 fi
 
 # Enable ASan specific code and build workarounds
 ac_add_options --enable-address-sanitizer
 
 # Mandatory options required for ASan builds
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -121,16 +121,19 @@ MKSHLIB			= $(MKCSHLIB)
 endif
 
 EMBED_MANIFEST_AT=2
 
 endif # MKSHLIB
 endif # FORCE_SHARED_LIB
 
 ifeq ($(OS_ARCH),WINNT)
+
+LINK_PDBFILE ?= $(basename $(@F)).pdb
+
 ifndef GNU_CC
 
 #
 # Unless we're building SIMPLE_PROGRAMS, all C++ files share a PDB file per
 # directory. For parallel builds, this PDB file is shared and locked by
 # MSPDBSRV.EXE, starting with MSVC8 SP1. If you're using MSVC 7.1 or MSVC8
 # without SP1, don't do parallel builds.
 #
@@ -142,30 +145,35 @@ ifndef GNU_CC
 ifdef SIMPLE_PROGRAMS
 COMPILE_PDB_FLAG ?= -Fd$(basename $(@F)).pdb
 else
 COMPILE_PDB_FLAG ?= -Fdgenerated.pdb -FS
 endif
 COMPILE_CFLAGS += $(COMPILE_PDB_FLAG)
 COMPILE_CXXFLAGS += $(COMPILE_PDB_FLAG)
 
-LINK_PDBFILE ?= $(basename $(@F)).pdb
 ifdef MOZ_DEBUG
 CODFILE=$(basename $(@F)).cod
 endif
 
 endif # !GNU_CC
 endif # WINNT
 
 ifeq (arm-Darwin,$(CPU_ARCH)-$(OS_TARGET))
 ifdef PROGRAM
 MOZ_PROGRAM_LDFLAGS += -Wl,-rpath -Wl,@executable_path/Frameworks
 endif
 endif
 
+ifeq ($(OS_ARCH),WINNT)
+ifeq ($(CC_TYPE),clang)
+MOZ_PROGRAM_LDFLAGS += -Wl,-pdb,$(dir $@)/$(LINK_PDBFILE)
+endif
+endif
+
 ifeq ($(HOST_OS_ARCH),WINNT)
 HOST_PDBFILE=$(basename $(@F)).pdb
 HOST_PDB_FLAG ?= -Fd$(HOST_PDBFILE)
 HOST_CFLAGS += $(HOST_PDB_FLAG)
 HOST_CXXFLAGS += $(HOST_PDB_FLAG)
 HOST_C_LDFLAGS += $(HOST_PDB_FLAG)
 HOST_CXX_LDFLAGS += $(HOST_PDB_FLAG)
 endif
@@ -814,23 +822,23 @@ endif
 endif
 
 ifdef MOZ_AUTOMATION
 ifeq (,$(filter 1,$(MOZ_AUTOMATION_BUILD_SYMBOLS)))
 DUMP_SYMS_TARGETS :=
 endif
 endif
 
-ifdef MOZ_CRASHREPORTER
-$(foreach file,$(DUMP_SYMS_TARGETS),$(eval $(call syms_template,$(file),$(notdir $(file))_syms.track)))
-else ifneq (,$(and $(LLVM_SYMBOLIZER),$(filter WINNT,$(OS_ARCH)),$(MOZ_AUTOMATION)))
+ifdef MOZ_COPY_PDBS
 PDB_FILES = $(addsuffix .pdb,$(basename $(DUMP_SYMS_TARGETS)))
 PDB_DEST ?= $(FINAL_TARGET)
 PDB_TARGET = syms
 INSTALL_TARGETS += PDB
+else ifdef MOZ_CRASHREPORTER
+$(foreach file,$(DUMP_SYMS_TARGETS),$(eval $(call syms_template,$(file),$(notdir $(file))_syms.track)))
 endif
 
 cargo_host_flag := --target=$(RUST_HOST_TARGET)
 cargo_target_flag := --target=$(RUST_TARGET)
 
 # Permit users to pass flags to cargo from their mozconfigs (e.g. --color=always).
 cargo_build_flags = $(CARGOFLAGS)
 ifndef MOZ_DEBUG_RUST
--- a/config/version_win.pl
+++ b/config/version_win.pl
@@ -269,22 +269,24 @@ print RCFILE qq{
 // Use module.ver to explicitly set these values
 
 // Do not edit this file. Changes won't affect the build.
 
 };
 
 my $versionlevel=0;
 my $insideversion=0;
+my $has_manifest=0;
 if (open(RCINCLUDE, "<$rcinclude")) 
 {
 	print RCFILE "// From included resource $rcinclude\n";
 #	my $mstring="";
 	while (<RCINCLUDE>) 
 	{
+		$has_manifest = 1 if /^1 (24|RT_MANIFEST) "$binary.manifest"/;
 		$_ =~ s/\@MOZ_APP_DISPLAYNAME\@/$displayname/g;
 		print RCFILE $_;
 #		my $instr=$_;
 #		chomp($instr);
 #		$mstring .= "$instr\;";
 	}
 	close(RCINCLUDE);
 #	$mstring =~ s/\/\*.*\*\///g;
@@ -326,16 +328,20 @@ if (open(RCINCLUDE, "<$rcinclude"))
 #		}
 #	}
 	
 }
 
 my $fileflags = join(' | ', @fileflags);
 
 print RCFILE qq{
+1 RT_MANIFEST "$binary.manifest"
+} if !$has_manifest && $binary =~ /\.exe$/ && -e "$objdir/$binary.manifest";
+
+print RCFILE qq{
 
 
 /////////////////////////////////////////////////////////////////////////////
 //
 // Version
 //
 
 1 VERSIONINFO
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -871,17 +871,17 @@ Inspector.prototype = {
       hideTabstripe: true
     });
 
     // defaultTab may also be an empty string or a tab id that doesn't exist anymore
     // (e.g. it was a tab registered by an addon that has been uninstalled).
     let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
 
     if (this.is3PaneModeEnabled && defaultTab === "ruleview") {
-      defaultTab = "computedview";
+      defaultTab = "layoutview";
     }
 
     // Append all side panels
 
     await this.addRuleView({ defaultTab });
 
     // Inject a lazy loaded react tab by exposing a fake React object
     // with a lazy defined Tab thanks to `panel` being a function
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -176,16 +176,17 @@ skip-if = (os == "win" && debug) # bug 9
 [browser_rules_grid-highlighter-on-navigate.js]
 [browser_rules_grid-highlighter-on-reload.js]
 [browser_rules_grid-highlighter-restored-after-reload.js]
 [browser_rules_grid-toggle_01.js]
 [browser_rules_grid-toggle_01b.js]
 [browser_rules_grid-toggle_02.js]
 [browser_rules_grid-toggle_03.js]
 [browser_rules_grid-toggle_04.js]
+[browser_rules_grid-toggle_05.js]
 [browser_rules_gridline-names-autocomplete.js]
 [browser_rules_guessIndentation.js]
 [browser_rules_highlight-used-fonts.js]
 [browser_rules_inherited-properties_01.js]
 [browser_rules_inherited-properties_02.js]
 [browser_rules_inherited-properties_03.js]
 [browser_rules_inherited-properties_04.js]
 [browser_rules_inline-source-map.js]
--- a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js
@@ -24,35 +24,37 @@ add_task(async function() {
   const {inspector, view} = await openRuleView();
   const highlighters = view.highlighters;
 
   await selectNode("#grid", inspector);
   const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
   const gridToggle = container.querySelector(".ruleview-grid");
 
   info("Checking the initial state of the CSS grid toggle in the rule-view.");
-  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
   ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
 
   info("Toggling ON the CSS grid highlighter from the rule-view.");
   const onHighlighterShown = highlighters.once("grid-highlighter-shown");
   gridToggle.click();
   await onHighlighterShown;
 
   info("Checking the CSS grid highlighter is created and toggle button is active in " +
     "the rule-view.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(gridToggle.classList.contains("active"),
     "Grid highlighter toggle is active.");
   is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
 
   info("Toggling OFF the CSS grid highlighter from the rule-view.");
   const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
   gridToggle.click();
   await onHighlighterHidden;
 
   info("Checking the CSS grid highlighter is not shown and toggle button is not active " +
     "in the rule-view.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
   ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
 });
--- a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01b.js
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01b.js
@@ -24,35 +24,37 @@ add_task(async function() {
   const {inspector, view} = await openRuleView();
   const highlighters = view.highlighters;
 
   await selectNode("#grid", inspector);
   const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
   const gridToggle = container.querySelector(".ruleview-grid");
 
   info("Checking the initial state of the CSS grid toggle in the rule-view.");
-  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
   ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
 
   info("Toggling ON the CSS grid highlighter from the rule-view.");
   const onHighlighterShown = highlighters.once("grid-highlighter-shown");
   gridToggle.click();
   await onHighlighterShown;
 
   info("Checking the CSS grid highlighter is created and toggle button is active in " +
     "the rule-view.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(gridToggle.classList.contains("active"),
     "Grid highlighter toggle is active.");
   is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
 
   info("Toggling OFF the CSS grid highlighter from the rule-view.");
   const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
   gridToggle.click();
   await onHighlighterHidden;
 
   info("Checking the CSS grid highlighter is not shown and toggle button is not active " +
     "in the rule-view.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
   ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
 });
--- a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js
@@ -29,17 +29,19 @@ add_task(async function() {
 
   await selectNode("#grid", inspector);
   const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
   const gridToggle = container.querySelector(".ruleview-grid");
   const overriddenContainer = getRuleViewProperty(view, "div, ul", "display").valueSpan;
   const overriddenGridToggle = overriddenContainer.querySelector(".ruleview-grid");
 
   info("Checking the initial state of the CSS grid toggle in the rule-view.");
-  ok(gridToggle && overriddenGridToggle, "Grid highlighter toggles are visible.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
+  ok(!overriddenGridToggle.hasAttribute("disabled"),
+    "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active") &&
     !overriddenGridToggle.classList.contains("active"),
     "Grid highlighter toggle buttons are not active.");
   ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
 
   info("Toggling ON the CSS grid highlighter from the overridden rule in the rule-view.");
   const onHighlighterShown = highlighters.once("grid-highlighter-shown");
   overriddenGridToggle.click();
--- a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
@@ -30,42 +30,43 @@ add_task(async function() {
 
   info("Selecting the first grid container.");
   await selectNode("#grid1", inspector);
   let container = getRuleViewProperty(view, ".grid", "display").valueSpan;
   let gridToggle = container.querySelector(".ruleview-grid");
 
   info("Checking the state of the CSS grid toggle for the first grid container in the " +
     "rule-view.");
-  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
   ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
 
   info("Toggling ON the CSS grid highlighter for the first grid container from the " +
     "rule-view.");
   let onHighlighterShown = highlighters.once("grid-highlighter-shown");
   gridToggle.click();
   await onHighlighterShown;
 
   info("Checking the CSS grid highlighter is created and toggle button is active in " +
     "the rule-view.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(gridToggle.classList.contains("active"),
     "Grid highlighter toggle is active.");
   is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
 
   info("Selecting the second grid container.");
   await selectNode("#grid2", inspector);
   const firstGridHighterShown = highlighters.gridHighlighters.keys().next().value;
   container = getRuleViewProperty(view, ".grid", "display").valueSpan;
   gridToggle = container.querySelector(".ruleview-grid");
 
   info("Checking the state of the CSS grid toggle for the second grid container in the " +
     "rule-view.");
-  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
   is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is still shown.");
 
   info("Toggling ON the CSS grid highlighter for the second grid container from the " +
     "rule-view.");
   onHighlighterShown = highlighters.once("grid-highlighter-shown");
   gridToggle.click();
@@ -80,12 +81,12 @@ add_task(async function() {
 
   info("Selecting the first grid container.");
   await selectNode("#grid1", inspector);
   container = getRuleViewProperty(view, ".grid", "display").valueSpan;
   gridToggle = container.querySelector(".ruleview-grid");
 
   info("Checking the state of the CSS grid toggle for the first grid container in the " +
     "rule-view.");
-  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
 });
--- a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_04.js
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_04.js
@@ -24,35 +24,37 @@ add_task(async function() {
   const {inspector, view} = await openRuleView();
   const highlighters = view.highlighters;
 
   await selectNode("#grid", inspector);
   const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
   const gridToggle = container.querySelector(".ruleview-grid");
 
   info("Checking the initial state of the CSS grid toggle in the rule-view.");
-  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
   ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
 
   info("Toggling ON the CSS grid highlighter from the rule-view.");
   const onHighlighterShown = highlighters.once("grid-highlighter-shown");
   gridToggle.click();
   await onHighlighterShown;
 
   info("Checking the CSS grid highlighter is created and toggle button is active in " +
     "the rule-view.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(gridToggle.classList.contains("active"),
     "Grid highlighter toggle is active.");
   is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
 
   info("Toggling OFF the CSS grid highlighter from the rule-view.");
   const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
   gridToggle.click();
   await onHighlighterHidden;
 
   info("Checking the CSS grid highlighter is not shown and toggle button is not active " +
     "in the rule-view.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
   ok(!gridToggle.classList.contains("active"),
     "Grid highlighter toggle button is not active.");
   ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_05.js
@@ -0,0 +1,102 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the grid toggle is hidden when the maximum number of grid highlighters
+// have been reached.
+
+const TEST_URI = `
+  <style type='text/css'>
+    .grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid1" class="grid">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+  <div id="grid2" class="grid">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+  <div id="grid3" class="grid">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+`;
+
+add_task(async function() {
+  await pushPref("devtools.gridinspector.maxHighlighters", 2);
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  const { inspector, gridInspector } = await openLayoutView();
+  const ruleView = selectRuleView(inspector);
+  const { document: doc } = gridInspector;
+  const { highlighters } = inspector;
+
+  await selectNode("#grid1", inspector);
+  const gridList = doc.getElementById("grid-list");
+  const checkbox2 = gridList.children[1].querySelector("input");
+  const checkbox3 = gridList.children[2].querySelector("input");
+  const container = getRuleViewProperty(ruleView, ".grid", "display").valueSpan;
+  const gridToggle = container.querySelector(".ruleview-grid");
+
+  info("Checking the initial state of the CSS grid toggle in the rule-view.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+  ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter for #grid2.");
+  let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  checkbox2.click();
+  await onHighlighterShown;
+
+  info("Checking the CSS grid toggle for #grid1 is not disabled and not active.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+  is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter for #grid3.");
+  onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  checkbox3.click();
+  await onHighlighterShown;
+
+  info("Checking the CSS grid toggle for #grid1 is disabled.");
+  ok(gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is disabled.");
+  is(highlighters.gridHighlighters.size, 2, "CSS grid highlighters are shown.");
+
+  info("Toggling OFF the CSS grid highlighter for #grid3.");
+  let onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+  checkbox3.click();
+  await onHighlighterHidden;
+
+  info("Checking the CSS grid toggle for #grid1 is not disabled and not active.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+  is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter for #grid1 from the rule-view.");
+  onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  gridToggle.click();
+  await onHighlighterShown;
+
+  info("Checking the CSS grid toggle for #grid1 is not disabled.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
+  ok(gridToggle.classList.contains("active"), "Grid highlighter toggle is active.");
+  is(highlighters.gridHighlighters.size, 2, "CSS grid highlighters are shown.");
+
+  info("Toggling OFF the CSS grid highlighter for #grid1 from the rule-view.");
+  onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+  gridToggle.click();
+  await onHighlighterHidden;
+
+  info("Checking the CSS grid toggle for #grid1 is not disabled and not active.");
+  ok(!gridToggle.hasAttribute("disabled"), "Grid highlighter toggle is not disabled.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+  is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+});
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -71,16 +71,17 @@ function TextPropertyEditor(ruleEditor, 
   this.ruleEditor = ruleEditor;
   this.ruleView = this.ruleEditor.ruleView;
   this.cssProperties = this.ruleView.cssProperties;
   this.doc = this.ruleEditor.doc;
   this.popup = this.ruleView.popup;
   this.prop = property;
   this.prop.editor = this;
   this.browserWindow = this.doc.defaultView.top;
+
   this._populatedComputed = false;
   this._hasPendingClick = false;
   this._clickedElementOptions = null;
 
   this.toolbox = this.ruleView.inspector.toolbox;
   this.telemetry = this.toolbox.telemetry;
 
   this.getGridlineNames = this.getGridlineNames.bind(this);
@@ -516,32 +517,32 @@ TextPropertyEditor.prototype = {
     if (this.ruleEditor.isEditable) {
       for (const angleSpan of this.angleSwatchSpans) {
         angleSpan.on("unit-change", this._onSwatchCommit);
         const title = l10n("rule.angleSwatch.tooltip");
         angleSpan.setAttribute("title", title);
       }
     }
 
+    const nodeFront = this.ruleView.inspector.selection.nodeFront;
+
     const flexToggle = this.valueSpan.querySelector(".ruleview-flex");
     if (flexToggle) {
       flexToggle.setAttribute("title", l10n("rule.flexToggle.tooltip"));
-      if (this.ruleView.highlighters.flexboxHighlighterShown ===
-          this.ruleView.inspector.selection.nodeFront) {
-        flexToggle.classList.add("active");
-      }
+      flexToggle.classList.toggle("active",
+        this.ruleView.highlighters.flexboxHighlighterShown === nodeFront);
     }
 
     const gridToggle = this.valueSpan.querySelector(".ruleview-grid");
     if (gridToggle) {
       gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
-      if (this.ruleView.highlighters.gridHighlighters.has(
-            this.ruleView.inspector.selection.nodeFront)) {
-        gridToggle.classList.add("active");
-      }
+      gridToggle.classList.toggle("active",
+        this.ruleView.highlighters.gridHighlighters.has(nodeFront));
+      gridToggle.toggleAttribute("disabled",
+        !this.ruleView.highlighters.canGridHighlighterToggle(nodeFront));
     }
 
     const shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch");
     if (shapeToggle) {
       const mode = "css" + name.split("-").map(s => {
         return s[0].toUpperCase() + s.slice(1);
       }).join("");
       shapeToggle.setAttribute("data-mode", mode);
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -23,16 +23,18 @@ class HighlightersOverlay {
    * @param  {Inspector} inspector
    *         Inspector toolbox panel.
    */
   constructor(inspector) {
     this.inspector = inspector;
     this.highlighterUtils = this.inspector.toolbox.highlighterUtils;
     this.store = this.inspector.store;
     this.telemetry = inspector.telemetry;
+    this.maxGridHighlighters =
+      Services.prefs.getIntPref("devtools.gridinspector.maxHighlighters");
 
     // Collection of instantiated highlighter actors like FlexboxHighlighter,
     // ShapesHighlighter and GeometryEditorHighlighter.
     this.highlighters = {};
     // Map of grid container NodeFront to their instantiated grid highlighter actors.
     this.gridHighlighters = new Map();
     // Array of reusable grid highlighters that have been instantiated and are not
     // associated with any NodeFront.
@@ -427,29 +429,26 @@ class HighlightersOverlay {
    * @param  {Object} options
    *         Object used for passing options to the grid highlighter.
    * @param. {String|null} trigger
    *         String name matching "grid" or "rule" to indicate where the
    *         grid highlighter was toggled on from. "grid" represents the grid view
    *         "rule" represents the rule view.
    */
   async showGridHighlighter(node, options, trigger) {
-    const maxHighlighters =
-      Services.prefs.getIntPref("devtools.gridinspector.maxHighlighters");
-
     // When the grid highlighter has the given node, it is probably called with new
     // highlighting options, so skip any extra grid highlighter handling.
     if (!this.gridHighlighters.has(node)) {
-      if (maxHighlighters === 1) {
+      if (this.maxGridHighlighters === 1) {
         // Only one grid highlighter can be shown at a time. Hides any instantiated
         // grid highlighters.
         for (const nodeFront of this.gridHighlighters.keys()) {
           await this.hideGridHighlighter(nodeFront);
         }
-      } else if (this.gridHighlighters.size === maxHighlighters) {
+      } else if (this.gridHighlighters.size === this.maxGridHighlighters) {
         // The maximum number of grid highlighters shown have been reached. Don't show
         // any additional grid highlighters.
         return;
       }
     }
 
     const highlighter = await this._getGridHighlighter(node);
     if (!highlighter) {
@@ -491,27 +490,27 @@ class HighlightersOverlay {
    * @param  {NodeFront} node
    *         The NodeFront of the grid container element to unhighlight.
    */
   async hideGridHighlighter(node) {
     if (!this.gridHighlighters.has(node)) {
       return;
     }
 
-    this._toggleRuleViewIcon(node, false, ".ruleview-grid");
-
     // Hide the highlighter and put it in the pool of extra grid highlighters
     // so that it can be reused.
     const highlighter = this.gridHighlighters.get(node);
     await highlighter.hide();
     this.extraGridHighlighterPool.push(highlighter);
 
     this.state.grids.delete(node);
     this.gridHighlighters.delete(node);
 
+    this._toggleRuleViewIcon(node, false, ".ruleview-grid");
+
     // Emit the NodeFront of the grid container element that the grid highlighter was
     // hidden for.
     this.emit("grid-highlighter-hidden", node);
   }
 
   /**
    * Show the box model highlighter for the given node.
    *
@@ -785,22 +784,32 @@ class HighlightersOverlay {
    * @param  {NodeFront} node
    *         The NodeFront of the element with a shape to highlight.
    * @param  {Boolean} active
    *         Whether or not the shape icon should be active.
    * @param  {String} selector
    *         The selector of the rule view icon to toggle.
    */
   _toggleRuleViewIcon(node, active, selector) {
-    if (this.inspector.selection.nodeFront != node) {
+    const ruleViewEl = this.inspector.getPanel("ruleview").view.element;
+
+    if (this.inspector.selection.nodeFront !== node) {
+      if (selector === ".ruleview-grid") {
+        for (const icon of ruleViewEl.querySelectorAll(selector)) {
+          if (this.canGridHighlighterToggle(this.inspector.selection.nodeFront)) {
+            icon.removeAttribute("disabled");
+          } else {
+            icon.setAttribute("disabled", true);
+          }
+        }
+      }
+
       return;
     }
 
-    const ruleViewEl = this.inspector.getPanel("ruleview").view.element;
-
     for (const icon of ruleViewEl.querySelectorAll(selector)) {
       icon.classList.toggle("active", active);
     }
   }
 
   /**
    * Toggle the class "active" on the given shape point in the rule view if the current
    * inspector selection is highlighted by the shapes highlighter.
--- a/devtools/client/inspector/test/browser_inspector_sidebarstate.js
+++ b/devtools/client/inspector/test/browser_inspector_sidebarstate.js
@@ -7,37 +7,37 @@ const TEST_URI = "data:text/html;charset
   "<h1>browser_inspector_sidebarstate.js</h1>";
 const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
 
 const TELEMETRY_DATA = [
   {
     timestamp: null,
     category: "devtools.main",
     method: "tool_timer",
-    object: "computedview",
-    value: null,
-    extra: {
-      time_open: ""
-    }
-  },
-  {
-    timestamp: null,
-    category: "devtools.main",
-    method: "tool_timer",
     object: "layoutview",
     value: null,
     extra: {
       time_open: ""
     }
   },
   {
     timestamp: null,
     category: "devtools.main",
     method: "tool_timer",
-    object: "ruleview",
+    object: "fontinspector",
+    value: null,
+    extra: {
+      time_open: ""
+    }
+  },
+  {
+    timestamp: null,
+    category: "devtools.main",
+    method: "tool_timer",
+    object: "computedview",
     value: null,
     extra: {
       time_open: ""
     }
   }
 ];
 
 add_task(async function() {
@@ -45,41 +45,41 @@ add_task(async function() {
   Services.telemetry.clearEvents();
 
   // Ensure no events have been logged
   const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
   ok(!snapshot.parent, "No events have been logged for the main process");
 
   let { inspector, toolbox } = await openInspectorForURL(TEST_URI);
 
+  info("Selecting font inspector.");
+  inspector.sidebar.select("fontinspector");
+
+  is(inspector.sidebar.getCurrentTabID(), "fontinspector",
+    "Font Inspector is selected");
+
   info("Selecting computed view.");
   inspector.sidebar.select("computedview");
 
   is(inspector.sidebar.getCurrentTabID(), "computedview",
     "Computed View is selected");
 
-  info("Selecting layout view.");
-  inspector.sidebar.select("layoutview");
-
-  is(inspector.sidebar.getCurrentTabID(), "layoutview",
-    "Layout View is selected");
-
   info("Closing inspector.");
   await toolbox.destroy();
 
   info("Re-opening inspector.");
   inspector = (await openInspector()).inspector;
 
   if (!inspector.sidebar.getCurrentTabID()) {
     info("Default sidebar still to be selected, adding select listener.");
     await inspector.sidebar.once("select");
   }
 
-  is(inspector.sidebar.getCurrentTabID(), "layoutview",
-     "Layout view is selected by default.");
+  is(inspector.sidebar.getCurrentTabID(), "computedview",
+     "Computed view is selected by default.");
 
   checkTelemetryResults();
 });
 
 function checkTelemetryResults() {
   const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
   const events = snapshot.parent.filter(event => event[1] === "devtools.main" &&
                                                  event[2] === "tool_timer"
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -28,17 +28,17 @@ pref("devtools.command-button-screenshot
 pref("devtools.command-button-rulers.enabled", false);
 pref("devtools.command-button-measure.enabled", false);
 pref("devtools.command-button-noautohide.enabled", false);
 
 // Inspector preferences
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
 // What was the last active sidebar in the inspector
-pref("devtools.inspector.activeSidebar", "ruleview");
+pref("devtools.inspector.activeSidebar", "layoutview");
 pref("devtools.inspector.remote", false);
 
 // Enable the 3 pane mode in the inspector
 pref("devtools.inspector.three-pane-enabled", true);
 // Enable the 3 pane mode in the chrome inspector
 pref("devtools.inspector.chrome.three-pane-enabled", false);
 // Collapse pseudo-elements by default in the rule-view
 pref("devtools.inspector.show_pseudo_elements", false);
--- a/devtools/client/shared/test/browser_telemetry_sidebar.js
+++ b/devtools/client/shared/test/browser_telemetry_sidebar.js
@@ -14,17 +14,17 @@ const TOOL_DELAY = 200;
 const DATA = [
   {
     timestamp: null,
     category: "devtools.main",
     method: "sidepanel_changed",
     object: "inspector",
     value: null,
     extra: {
-      oldpanel: "computedview",
+      oldpanel: "layoutview",
       newpanel: "animationinspector"
     }
   },
   {
     timestamp: null,
     category: "devtools.main",
     method: "sidepanel_changed",
     object: "inspector",
@@ -151,18 +151,18 @@ function testSidebar(toolbox) {
   });
 }
 
 function checkResults() {
   // For help generating these tests use generateTelemetryTests("DEVTOOLS_")
   // here.
   checkTelemetry("DEVTOOLS_INSPECTOR_OPENED_COUNT", "", [1, 0, 0], "array");
   checkTelemetry("DEVTOOLS_RULEVIEW_OPENED_COUNT", "", [1, 0, 0], "array");
-  checkTelemetry("DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT", "", [3, 0, 0], "array");
-  checkTelemetry("DEVTOOLS_LAYOUTVIEW_OPENED_COUNT", "", [2, 0, 0], "array");
+  checkTelemetry("DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT", "", [2, 0, 0], "array");
+  checkTelemetry("DEVTOOLS_LAYOUTVIEW_OPENED_COUNT", "", [3, 0, 0], "array");
   checkTelemetry("DEVTOOLS_FONTINSPECTOR_OPENED_COUNT", "", [2, 0, 0], "array");
   checkTelemetry("DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS", "", null, "hasentries");
   checkTelemetry("DEVTOOLS_LAYOUTVIEW_TIME_ACTIVE_SECONDS", "", null, "hasentries");
   checkTelemetry("DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS", "", null, "hasentries");
 }
 
 function checkEventTelemetry() {
   const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -435,16 +435,21 @@
   border-radius: 0;
 }
 
 .ruleview-grid {
   background: url("chrome://devtools/skin/images/grid.svg");
   border-radius: 0;
 }
 
+.ruleview-grid[disabled] {
+  cursor: default;
+  opacity: 0.5;
+}
+
 .ruleview-shape-point.active,
 .ruleview-shapeswatch.active + .ruleview-shape > .ruleview-shape-point:hover {
   background-color: var(--rule-highlight-background-color);
 }
 
 .ruleview-colorswatch::before {
   content: '';
   background-color: #eee;
--- a/dom/base/TimeoutExecutor.cpp
+++ b/dom/base/TimeoutExecutor.cpp
@@ -51,32 +51,28 @@ TimeoutExecutor::ScheduleDelayed(const T
   MOZ_DIAGNOSTIC_ASSERT(mDeadline.IsNull());
   MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::None);
   MOZ_DIAGNOSTIC_ASSERT(!aMinDelay.IsZero() ||
                         aDeadline > (aNow + mAllowedEarlyFiringTime));
 
   nsresult rv = NS_OK;
 
   if (!mTimer) {
-    mTimer = NS_NewTimer();
+    mTimer = NS_NewTimer(mOwner->EventTarget());
     NS_ENSURE_TRUE(mTimer, NS_ERROR_OUT_OF_MEMORY);
 
     uint32_t earlyMicros = 0;
     MOZ_ALWAYS_SUCCEEDS(mTimer->GetAllowedEarlyFiringMicroseconds(&earlyMicros));
     mAllowedEarlyFiringTime = TimeDuration::FromMicroseconds(earlyMicros);
+  } else {
+    // Always call Cancel() in case we are re-using a timer.
+    rv = mTimer->Cancel();
+    NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  // Always call Cancel() in case we are re-using a timer.  Otherwise
-  // the subsequent SetTarget() may fail.
-  rv = mTimer->Cancel();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  rv = mTimer->SetTarget(mOwner->EventTarget());
-  NS_ENSURE_SUCCESS(rv, rv);
-
   // Calculate the delay based on the deadline and current time.  If we have
   // a minimum delay set then clamp to that value.
   //
   // Note, we don't actually adjust our mDeadline for the minimum delay, just
   // the nsITimer value.  This is necessary to avoid lots of needless
   // rescheduling if more deadlines come in between now and the minimum delay
   // firing time.
   TimeDuration delay = TimeDuration::Max(aMinDelay, aDeadline - aNow);
--- a/dom/file/FileReader.cpp
+++ b/dom/file/FileReader.cpp
@@ -526,24 +526,23 @@ FileReader::WrapObject(JSContext* aCx, J
 {
   return FileReader_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 void
 FileReader::StartProgressEventTimer()
 {
   if (!mProgressNotifier) {
-    mProgressNotifier = NS_NewTimer();
+    mProgressNotifier = NS_NewTimer(mTarget);
   }
 
   if (mProgressNotifier) {
     mProgressEventWasDelayed = false;
     mTimerIsActive = true;
     mProgressNotifier->Cancel();
-    mProgressNotifier->SetTarget(mTarget);
     mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL,
                                         nsITimer::TYPE_ONE_SHOT);
   }
 }
 
 void
 FileReader::ClearProgressEventTimer()
 {
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -1587,17 +1587,17 @@ ContentParent::MarkAsDead()
 {
   MarkAsTroubled();
   mIsAlive = false;
 }
 
 void
 ContentParent::OnChannelError()
 {
-  RefPtr<ContentParent> content(this);
+  RefPtr<ContentParent> kungFuDeathGrip(this);
   PContentParent::OnChannelError();
 }
 
 void
 ContentParent::OnChannelConnected(int32_t pid)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -1733,24 +1733,24 @@ DelayedDeleteSubprocess(GeckoChildProces
 
 // This runnable only exists to delegate ownership of the
 // ContentParent to this runnable, until it's deleted by the event
 // system.
 struct DelayedDeleteContentParentTask : public Runnable
 {
   explicit DelayedDeleteContentParentTask(ContentParent* aObj)
     : Runnable("dom::DelayedDeleteContentParentTask")
-    , mObj(aObj)
+    , mKungFuDeathGrip(aObj)
   {
   }
 
   // No-op
   NS_IMETHOD Run() override { return NS_OK; }
 
-  RefPtr<ContentParent> mObj;
+  RefPtr<ContentParent> mKungFuDeathGrip;
 };
 
 } // namespace
 
 void
 ContentParent::ActorDestroy(ActorDestroyReason why)
 {
   if (mForceKillTimer) {
--- a/dom/security/featurepolicy/FeaturePolicyUtils.cpp
+++ b/dom/security/featurepolicy/FeaturePolicyUtils.cpp
@@ -18,26 +18,26 @@ struct FeatureMap {
 };
 
 /*
  * IMPORTANT: Do not change this list without review from a DOM peer _AND_ a
  * DOM Security peer!
  */
 static FeatureMap sSupportedFeatures[] = {
   { "autoplay", FeaturePolicyUtils::FeaturePolicyValue::eAll },
-  { "camera", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
-  { "encrypted-media", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
-  { "fullscreen", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
-  { "geolocation", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
-  { "microphone", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
-  { "midi", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
-  { "payment", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
+  { "camera", FeaturePolicyUtils::FeaturePolicyValue::eAll },
+  { "encrypted-media", FeaturePolicyUtils::FeaturePolicyValue::eAll },
+  { "fullscreen", FeaturePolicyUtils::FeaturePolicyValue::eAll },
+  { "geolocation", FeaturePolicyUtils::FeaturePolicyValue::eAll },
+  { "microphone", FeaturePolicyUtils::FeaturePolicyValue::eAll },
+  { "midi", FeaturePolicyUtils::FeaturePolicyValue::eAll },
+  { "payment", FeaturePolicyUtils::FeaturePolicyValue::eAll },
   // TODO: not supported yet!!!
-  { "speaker", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
-  { "vr", FeaturePolicyUtils::FeaturePolicyValue::eAll  },
+  { "speaker", FeaturePolicyUtils::FeaturePolicyValue::eAll },
+  { "vr", FeaturePolicyUtils::FeaturePolicyValue::eAll },
 };
 
 /* static */ bool
 FeaturePolicyUtils::IsSupportedFeature(const nsAString& aFeatureName)
 {
   uint32_t numFeatures = (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
   for (uint32_t i = 0; i < numFeatures; ++i) {
     if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i].mFeatureName)) {
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -3137,23 +3137,23 @@ XMLHttpRequestMainThread::SetTimeout(uin
   }
 
   mTimeoutMilliseconds = aTimeout;
   if (mRequestSentTime) {
     StartTimeoutTimer();
   }
 }
 
-void
-XMLHttpRequestMainThread::SetTimerEventTarget(nsITimer* aTimer)
+nsIEventTarget*
+XMLHttpRequestMainThread::GetTimerEventTarget()
 {
   if (nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal()) {
-    nsCOMPtr<nsIEventTarget> target = global->EventTargetFor(TaskCategory::Other);
-    aTimer->SetTarget(target);
-  }
+    return global->EventTargetFor(TaskCategory::Other);
+  }
+  return nullptr;
 }
 
 nsresult
 XMLHttpRequestMainThread::DispatchToMainThread(already_AddRefed<nsIRunnable> aRunnable)
 {
   if (nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal()) {
     nsCOMPtr<nsIEventTarget> target = global->EventTargetFor(TaskCategory::Other);
     MOZ_ASSERT(target);
@@ -3178,18 +3178,17 @@ XMLHttpRequestMainThread::StartTimeoutTi
     mTimeoutTimer->Cancel();
   }
 
   if (!mTimeoutMilliseconds) {
     return;
   }
 
   if (!mTimeoutTimer) {
-    mTimeoutTimer = NS_NewTimer();
-    SetTimerEventTarget(mTimeoutTimer);
+    mTimeoutTimer = NS_NewTimer(GetTimerEventTarget());
   }
   uint32_t elapsed =
     (uint32_t)((PR_Now() - mRequestSentTime) / PR_USEC_PER_MSEC);
   mTimeoutTimer->InitWithCallback(
     this,
     mTimeoutMilliseconds > elapsed ? mTimeoutMilliseconds - elapsed : 0,
     nsITimer::TYPE_ONE_SHOT
   );
@@ -3596,18 +3595,17 @@ XMLHttpRequestMainThread::StopProgressEv
     mProgressNotifier->Cancel();
   }
 }
 
 void
 XMLHttpRequestMainThread::StartProgressEventTimer()
 {
   if (!mProgressNotifier) {
-    mProgressNotifier = NS_NewTimer();
-    SetTimerEventTarget(mProgressNotifier);
+    mProgressNotifier = NS_NewTimer(GetTimerEventTarget());
   }
   if (mProgressNotifier) {
     mProgressTimerIsActive = true;
     mProgressNotifier->Cancel();
     mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL,
                                         nsITimer::TYPE_ONE_SHOT);
   }
 }
@@ -3623,18 +3621,17 @@ XMLHttpRequestMainThread::MaybeStartSync
   }
 
   // If we are in a beforeunload or a unload event, we must force a timeout.
   TimeDuration diff = (TimeStamp::NowLoRes() - doc->GetPageUnloadingEventTimeStamp());
   if (diff.ToMilliseconds() > MAX_SYNC_TIMEOUT_WHEN_UNLOADING) {
     return eErrorOrExpired;
   }
 
-  mSyncTimeoutTimer = NS_NewTimer();
-  SetTimerEventTarget(mSyncTimeoutTimer);
+  mSyncTimeoutTimer = NS_NewTimer(GetTimerEventTarget());
   if (!mSyncTimeoutTimer) {
     return eErrorOrExpired;
   }
 
   uint32_t timeout = MAX_SYNC_TIMEOUT_WHEN_UNLOADING - diff.ToMilliseconds();
   nsresult rv = mSyncTimeoutTimer->InitWithCallback(this, timeout,
                                                     nsITimer::TYPE_ONE_SHOT);
   return NS_FAILED(rv) ? eErrorOrExpired : eTimerStarted;
--- a/dom/xhr/XMLHttpRequestMainThread.h
+++ b/dom/xhr/XMLHttpRequestMainThread.h
@@ -509,17 +509,17 @@ protected:
 
   void StartProgressEventTimer();
   void StopProgressEventTimer();
 
   void MaybeCreateBlobStorage();
 
   nsresult OnRedirectVerifyCallback(nsresult result);
 
-  void SetTimerEventTarget(nsITimer* aTimer);
+  nsIEventTarget* GetTimerEventTarget();
 
   nsresult DispatchToMainThread(already_AddRefed<nsIRunnable> aRunnable);
 
   void DispatchOrStoreEvent(DOMEventTargetHelper* aTarget, Event* aEvent);
 
   already_AddRefed<nsXMLHttpRequestXPCOMifier> EnsureXPCOMifier();
 
   void SuspendEventDispatching();
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -732,16 +732,17 @@ WebRenderMemoryReporter::CollectReports(
       helper.Report(aReport.images, "resource-cache/images");
       helper.Report(aReport.rasterized_blobs, "resource-cache/rasterized-blobs");
 
       // GPU Memory.
       helper.ReportTexture(aReport.gpu_cache_textures, "gpu-cache");
       helper.ReportTexture(aReport.vertex_data_textures, "vertex-data");
       helper.ReportTexture(aReport.render_target_textures, "render-targets");
       helper.ReportTexture(aReport.texture_cache_textures, "texture-cache");
+      helper.ReportTexture(aReport.depth_target_textures, "depth-targets");
 
       FinishAsyncMemoryReport();
     },
     [](mozilla::ipc::ResponseRejectReason aReason) {
       FinishAsyncMemoryReport();
     }
   );
 
--- a/gfx/webrender/res/brush.glsl
+++ b/gfx/webrender/res/brush.glsl
@@ -17,16 +17,17 @@ void brush_vs(
 );
 
 #define VECS_PER_SEGMENT                    2
 
 #define BRUSH_FLAG_PERSPECTIVE_INTERPOLATION    1
 #define BRUSH_FLAG_SEGMENT_RELATIVE             2
 #define BRUSH_FLAG_SEGMENT_REPEAT_X             4
 #define BRUSH_FLAG_SEGMENT_REPEAT_Y             8
+#define BRUSH_FLAG_TEXEL_RECT                  16
 
 void main(void) {
     // Load the brush instance from vertex attributes.
     int prim_header_address = aData.x;
     int clip_address = aData.y;
     int segment_index = aData.z & 0xffff;
     int edge_flags = (aData.z >> 16) & 0xff;
     int brush_flags = (aData.z >> 24) & 0xff;
--- a/gfx/webrender/res/brush_image.glsl
+++ b/gfx/webrender/res/brush_image.glsl
@@ -46,17 +46,17 @@ void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize prim_rect,
     RectWithSize segment_rect,
     ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
-    vec4 texel_rect
+    vec4 segment_data
 ) {
     ImageBrushData image_data = fetch_image_data(prim_address);
 
     // If this is in WR_FEATURE_TEXTURE_RECT mode, the rect and size use
     // non-normalized texture coordinates.
 #ifdef WR_FEATURE_TEXTURE_RECT
     vec2 texture_size = vec2(1, 1);
 #else
@@ -71,29 +71,28 @@ void brush_vs(
     vec2 stretch_size = image_data.stretch_size;
 
     // If this segment should interpolate relative to the
     // segment, modify the parameters for that.
     if ((brush_flags & BRUSH_FLAG_SEGMENT_RELATIVE) != 0) {
         local_rect = segment_rect;
         stretch_size = local_rect.size;
 
-        // Note: Here we can assume that texels in device
-        //       space map to local space, due to how border-image
-        //       works. That assumption may not hold if this
-        //       is used for other purposes in the future.
         if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_X) != 0) {
-            stretch_size.x = (texel_rect.z - texel_rect.x) / pic_task.common_data.device_pixel_scale;
+            stretch_size.x = (segment_data.z - segment_data.x);
         }
         if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_Y) != 0) {
-            stretch_size.y = (texel_rect.w - texel_rect.y) / pic_task.common_data.device_pixel_scale;
+            stretch_size.y = (segment_data.w - segment_data.y);
         }
 
-        uv0 = res.uv_rect.p0 + texel_rect.xy;
-        uv1 = res.uv_rect.p0 + texel_rect.zw;
+        // If the extra data is a texel rect, modify the UVs.
+        if ((brush_flags & BRUSH_FLAG_TEXEL_RECT) != 0) {
+            uv0 = res.uv_rect.p0 + segment_data.xy;
+            uv1 = res.uv_rect.p0 + segment_data.zw;
+        }
     }
 
     vUv.z = res.layer;
 
     // Handle case where the UV coords are inverted (e.g. from an
     // external image).
     vec2 min_uv = min(uv0, uv1);
     vec2 max_uv = max(uv0, uv1);
--- a/gfx/webrender/res/ps_split_composite.glsl
+++ b/gfx/webrender/res/ps_split_composite.glsl
@@ -5,34 +5,31 @@
 #include shared,prim_shared
 
 varying vec3 vUv;
 flat varying vec4 vUvSampleBounds;
 
 #ifdef WR_VERTEX_SHADER
 struct SplitGeometry {
     vec2 local[4];
-    RectWithSize local_rect;
 };
 
 SplitGeometry fetch_split_geometry(int address) {
     ivec2 uv = get_gpu_cache_uv(address);
 
     vec4 data0 = TEXEL_FETCH(sGpuCache, uv, 0, ivec2(0, 0));
     vec4 data1 = TEXEL_FETCH(sGpuCache, uv, 0, ivec2(1, 0));
-    vec4 data2 = TEXEL_FETCH(sGpuCache, uv, 0, ivec2(2, 0));
 
     SplitGeometry geo;
     geo.local = vec2[4](
         data0.xy,
         data0.zw,
         data1.xy,
         data1.zw
     );
-    geo.local_rect = RectWithSize(data2.xy, data2.zw);
 
     return geo;
 }
 
 vec2 bilerp(vec2 a, vec2 b, vec2 c, vec2 d, float s, float t) {
     vec2 x = mix(a, b, t);
     vec2 y = mix(c, d, t);
     return mix(x, y, s);
@@ -93,17 +90,17 @@ void main(void) {
     vec2 min_uv = min(uv0, uv1);
     vec2 max_uv = max(uv0, uv1);
 
     vUvSampleBounds = vec4(
         min_uv + vec2(0.5),
         max_uv - vec2(0.5)
     ) / texture_size.xyxy;
 
-    vec2 f = (local_pos - geometry.local_rect.p0) / geometry.local_rect.size;
+    vec2 f = (local_pos - ph.local_rect.p0) / ph.local_rect.size;
 
     f = bilerp(
         extra_data.st_tl, extra_data.st_tr,
         extra_data.st_bl, extra_data.st_br,
         f.y, f.x
     );
     vec2 uv = mix(uv0, uv1, f);
 
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -15,22 +15,23 @@ use gpu_types::{ClipMaskInstance, SplitC
 use gpu_types::{PrimitiveInstanceData, RasterizationSpace, GlyphInstance};
 use gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
 use internal_types::{FastHashMap, SavedTargetIndex, TextureSource};
 use picture::{PictureCompositeMode, PicturePrimitive, PictureSurface};
 use plane_split::{BspSplitter, Clipper, Polygon, Splitter};
 use prim_store::{BrushKind, BrushPrimitive, BrushSegmentTaskId, DeferredResolve};
 use prim_store::{EdgeAaSegmentMask, ImageSource, PrimitiveIndex};
 use prim_store::{VisibleGradientTile, PrimitiveInstance};
-use prim_store::{BorderSource, Primitive, PrimitiveDetails};
+use prim_store::{BrushSegment, BorderSource, Primitive, PrimitiveDetails};
 use render_task::{RenderTaskAddress, RenderTaskId, RenderTaskTree};
 use renderer::{BlendMode, ImageBufferKind, ShaderColorMode};
 use renderer::BLOCKS_PER_UV_RECT;
 use resource_cache::{CacheItem, GlyphFetchResult, ImageRequest, ResourceCache, ImageProperties};
 use scene::FilterOpHelpers;
+use smallvec::SmallVec;
 use std::{f32, i32, usize};
 use tiling::{RenderTargetContext};
 use util::{MatrixHelpers, TransformedRectKind};
 
 // Special sentinel value recognized by the shader. It is considered to be
 // a dummy task that doesn't mask out anything.
 const OPAQUE_TASK_ADDRESS: RenderTaskAddress = RenderTaskAddress(0x7fff);
 
@@ -414,16 +415,25 @@ impl AlphaBatchContainer {
                     self.alpha_batches.push(other_batch);
                     min_batch_index = self.alpha_batches.len();
                 }
             }
         }
     }
 }
 
+/// Each segment can optionally specify a per-segment
+/// texture set and one user data field.
+#[derive(Debug, Copy, Clone)]
+struct SegmentInstanceData {
+    textures: BatchTextures,
+    user_data: i32,
+    is_opaque_override: Option<bool>,
+}
+
 /// Encapsulates the logic of building batches for items that are blended.
 pub struct AlphaBatchBuilder {
     pub batch_list: BatchList,
     glyph_fetch_buffer: Vec<GlyphFetchResult>,
     target_rect: DeviceIntRect,
     can_merge: bool,
 }
 
@@ -546,17 +556,16 @@ impl AlphaBatchBuilder {
                 transform.transform_point3d(&poly.points[0].cast()).unwrap(),
                 transform.transform_point3d(&poly.points[1].cast()).unwrap(),
                 transform.transform_point3d(&poly.points[2].cast()).unwrap(),
                 transform.transform_point3d(&poly.points[3].cast()).unwrap(),
             ];
             let gpu_blocks = [
                 [local_points[0].x, local_points[0].y, local_points[1].x, local_points[1].y].into(),
                 [local_points[2].x, local_points[2].y, local_points[3].x, local_points[3].y].into(),
-                pic_metadata.local_rect.into(),
             ];
 
             let gpu_handle = gpu_cache.push_per_frame_blocks(&gpu_blocks);
             let key = BatchKey::new(
                 BatchKind::SplitComposite,
                 BlendMode::PremultipliedAlpha,
                 BatchTextures::no_texture(),
             );
@@ -1098,47 +1107,39 @@ impl AlphaBatchBuilder {
                             clip_task_address,
                             gpu_cache,
                             &mut self.batch_list,
                             &prim_header,
                             prim_headers,
                         );
                     }
                     _ => {
-                        // TODO(gw): As an interim step, just return one value for the
-                        //           per-segment user data. In the future, this method
-                        //           will be expanded to optionally return a list of
-                        //           (BatchTextures, user_data) per segment, which will
-                        //           allow a different texture / render task to be used
-                        //           per segment.
-                        if let Some((batch_kind, textures, user_data, segment_user_data)) = brush.get_batch_params(
-                                ctx.resource_cache,
-                                gpu_cache,
-                                deferred_resolves,
-                                ctx.prim_store.chase_id == Some(prim_instance.prim_index),
+                        if let Some(params) = brush.get_batch_params(
+                            ctx.resource_cache,
+                            gpu_cache,
+                            deferred_resolves,
+                            ctx.prim_store.chase_id == Some(prim_instance.prim_index),
                         ) {
-                            let prim_header_index = prim_headers.push(&prim_header, user_data);
+                            let prim_header_index = prim_headers.push(&prim_header, params.prim_user_data);
                             if cfg!(debug_assertions) && ctx.prim_store.chase_id == Some(prim_instance.prim_index) {
                                 println!("\t{:?} {:?}, task relative bounds {:?}",
-                                    batch_kind, prim_header_index, bounding_rect);
+                                    params.batch_kind, prim_header_index, bounding_rect);
                             }
 
                             self.add_brush_to_batch(
                                 brush,
+                                &params,
                                 prim_instance,
-                                batch_kind,
                                 specified_blend_mode,
                                 non_segmented_blend_mode,
-                                textures,
                                 prim_header_index,
                                 clip_task_address,
                                 bounding_rect,
                                 transform_kind,
                                 render_tasks,
-                                segment_user_data,
                             );
                         }
                     }
                 }
             }
             PrimitiveDetails::TextRun(ref text_cpu) => {
                 let subpx_dir = text_cpu.used_font.get_subpx_dir();
 
@@ -1265,90 +1266,157 @@ impl AlphaBatchBuilder {
         self.batch_list.push_single_instance(
             batch_key,
             bounding_rect,
             prim_instance.prim_index,
             PrimitiveInstanceData::from(base_instance),
         );
     }
 
+    /// Add a single segment instance to a batch.
+    fn add_segment_to_batch(
+        &mut self,
+        segment: &BrushSegment,
+        segment_data: &SegmentInstanceData,
+        segment_index: i32,
+        batch_kind: BrushBatchKind,
+        prim_instance: &PrimitiveInstance,
+        prim_header_index: PrimitiveHeaderIndex,
+        alpha_blend_mode: BlendMode,
+        bounding_rect: &WorldRect,
+        transform_kind: TransformedRectKind,
+        render_tasks: &RenderTaskTree,
+    ) {
+        let clip_task_address = match segment.clip_task_id {
+            BrushSegmentTaskId::RenderTaskId(id) =>
+                render_tasks.get_task_address(id),
+            BrushSegmentTaskId::Opaque => OPAQUE_TASK_ADDRESS,
+            BrushSegmentTaskId::Empty => return,
+        };
+
+        // If the segment instance data specifies opacity for that
+        // segment, use it. Otherwise, assume opacity for the segment
+        // from the overall primitive opacity.
+        let is_segment_opaque = match segment_data.is_opaque_override {
+            Some(is_opaque) => is_opaque,
+            None => prim_instance.opacity.is_opaque,
+        };
+
+        let is_inner = segment.edge_flags.is_empty();
+        let needs_blending = !is_segment_opaque ||
+                             segment.clip_task_id.needs_blending() ||
+                             (!is_inner && transform_kind == TransformedRectKind::Complex);
+
+        let instance = PrimitiveInstanceData::from(BrushInstance {
+            segment_index,
+            edge_flags: segment.edge_flags,
+            clip_task_address,
+            brush_flags: BrushFlags::PERSPECTIVE_INTERPOLATION | segment.brush_flags,
+            prim_header_index,
+            user_data: segment_data.user_data,
+        });
+
+        let batch_key = BatchKey {
+            blend_mode: if needs_blending { alpha_blend_mode } else { BlendMode::None },
+            kind: BatchKind::Brush(batch_kind),
+            textures: segment_data.textures,
+        };
+
+        self.batch_list.push_single_instance(
+            batch_key,
+            bounding_rect,
+            prim_instance.prim_index,
+            instance,
+        );
+    }
+
+    /// Add any segment(s) from a brush to batches.
     fn add_brush_to_batch(
         &mut self,
         brush: &BrushPrimitive,
+        params: &BrushBatchParameters,
         prim_instance: &PrimitiveInstance,
-        batch_kind: BrushBatchKind,
         alpha_blend_mode: BlendMode,
         non_segmented_blend_mode: BlendMode,
-        textures: BatchTextures,
         prim_header_index: PrimitiveHeaderIndex,
         clip_task_address: RenderTaskAddress,
         bounding_rect: &WorldRect,
         transform_kind: TransformedRectKind,
         render_tasks: &RenderTaskTree,
-        user_data: i32,
     ) {
-        let base_instance = BrushInstance {
-            prim_header_index,
-            clip_task_address,
-            segment_index: 0,
-            edge_flags: EdgeAaSegmentMask::all(),
-            brush_flags: BrushFlags::PERSPECTIVE_INTERPOLATION,
-            user_data,
-        };
-
-        match brush.segment_desc {
-            Some(ref segment_desc) => {
-                for (i, segment) in segment_desc.segments.iter().enumerate() {
-                    let is_inner = segment.edge_flags.is_empty();
-                    let needs_blending = !prim_instance.opacity.is_opaque ||
-                                         segment.clip_task_id.needs_blending() ||
-                                         (!is_inner && transform_kind == TransformedRectKind::Complex);
-
-                    let clip_task_address = match segment.clip_task_id {
-                        BrushSegmentTaskId::RenderTaskId(id) =>
-                            render_tasks.get_task_address(id),
-                        BrushSegmentTaskId::Opaque => OPAQUE_TASK_ADDRESS,
-                        BrushSegmentTaskId::Empty => continue,
-                    };
-
-                    let instance = PrimitiveInstanceData::from(BrushInstance {
-                        segment_index: i as i32,
-                        edge_flags: segment.edge_flags,
-                        clip_task_address,
-                        brush_flags: base_instance.brush_flags | segment.brush_flags,
-                        ..base_instance
-                    });
-
-                    let batch_key = BatchKey {
-                        blend_mode: if needs_blending { alpha_blend_mode } else { BlendMode::None },
-                        kind: BatchKind::Brush(batch_kind),
-                        textures,
-                    };
-
-                    self.batch_list.push_single_instance(
-                        batch_key,
+        match (&brush.segment_desc, &params.segment_data) {
+            (Some(ref segment_desc), SegmentDataKind::Instanced(ref segment_data)) => {
+                // In this case, we have both a list of segments, and a list of
+                // per-segment instance data. Zip them together to build batches.
+                debug_assert_eq!(segment_desc.segments.len(), segment_data.len());
+                for (segment_index, (segment, segment_data)) in segment_desc.segments
+                    .iter()
+                    .zip(segment_data.iter())
+                    .enumerate() {
+                    self.add_segment_to_batch(
+                        segment,
+                        segment_data,
+                        segment_index as i32,
+                        params.batch_kind,
+                        prim_instance,
+                        prim_header_index,
+                        alpha_blend_mode,
                         bounding_rect,
-                        prim_instance.prim_index,
-                        instance,
+                        transform_kind,
+                        render_tasks,
                     );
                 }
             }
-            None => {
+            (Some(ref segment_desc), SegmentDataKind::Shared(ref segment_data)) => {
+                // A list of segments, but the per-segment data is common
+                // between all segments.
+                for (segment_index, segment) in segment_desc.segments
+                    .iter()
+                    .enumerate() {
+                    self.add_segment_to_batch(
+                        segment,
+                        segment_data,
+                        segment_index as i32,
+                        params.batch_kind,
+                        prim_instance,
+                        prim_header_index,
+                        alpha_blend_mode,
+                        bounding_rect,
+                        transform_kind,
+                        render_tasks,
+                    );
+                }
+            }
+            (None, SegmentDataKind::Shared(ref segment_data)) => {
+                // No segments, and thus no per-segment instance data.
                 let batch_key = BatchKey {
                     blend_mode: non_segmented_blend_mode,
-                    kind: BatchKind::Brush(batch_kind),
-                    textures,
+                    kind: BatchKind::Brush(params.batch_kind),
+                    textures: segment_data.textures,
                 };
+                let instance = PrimitiveInstanceData::from(BrushInstance {
+                    segment_index: 0,
+                    edge_flags: EdgeAaSegmentMask::all(),
+                    clip_task_address,
+                    brush_flags: BrushFlags::PERSPECTIVE_INTERPOLATION,
+                    prim_header_index,
+                    user_data: segment_data.user_data,
+                });
                 self.batch_list.push_single_instance(
                     batch_key,
                     bounding_rect,
                     prim_instance.prim_index,
-                    PrimitiveInstanceData::from(base_instance),
+                    PrimitiveInstanceData::from(instance),
                 );
             }
+            (None, SegmentDataKind::Instanced(..)) => {
+                // We should never hit the case where there are no segments,
+                // but a list of segment instance data.
+                unreachable!();
+            }
         }
     }
 }
 
 fn add_gradient_tiles(
     prim_instance: &PrimitiveInstance,
     visible_tiles: &[VisibleGradientTile],
     stops_handle: &GpuCacheHandle,
@@ -1421,24 +1489,76 @@ fn get_image_tile_params(
                 RasterizationSpace::Local as i32,
                 0,
             ],
             gpu_cache.get_address(&cache_item.uv_rect_handle),
         ))
     }
 }
 
+/// Either a single texture / user data for all segments,
+/// or a list of one per segment.
+enum SegmentDataKind {
+    Shared(SegmentInstanceData),
+    Instanced(SmallVec<[SegmentInstanceData; 8]>),
+}
+
+/// The parameters that are specific to a kind of brush,
+/// used by the common method to add a brush to batches.
+struct BrushBatchParameters {
+    batch_kind: BrushBatchKind,
+    prim_user_data: [i32; 3],
+    segment_data: SegmentDataKind,
+}
+
+impl BrushBatchParameters {
+    /// This brush instance has a list of per-segment
+    /// instance data.
+    fn instanced(
+        batch_kind: BrushBatchKind,
+        prim_user_data: [i32; 3],
+        segment_data: SmallVec<[SegmentInstanceData; 8]>,
+    ) -> Self {
+        BrushBatchParameters {
+            batch_kind,
+            prim_user_data,
+            segment_data: SegmentDataKind::Instanced(segment_data),
+        }
+    }
+
+    /// This brush instance shares the per-segment data
+    /// across all segments.
+    fn shared(
+        batch_kind: BrushBatchKind,
+        textures: BatchTextures,
+        prim_user_data: [i32; 3],
+        segment_user_data: i32,
+    ) -> Self {
+        BrushBatchParameters {
+            batch_kind,
+            prim_user_data,
+            segment_data: SegmentDataKind::Shared(
+                SegmentInstanceData {
+                    textures,
+                    user_data: segment_user_data,
+                    is_opaque_override: None,
+                }
+            ),
+        }
+    }
+}
+
 impl BrushPrimitive {
     fn get_batch_params(
         &self,
         resource_cache: &ResourceCache,
         gpu_cache: &mut GpuCache,
         deferred_resolves: &mut Vec<DeferredResolve>,
         is_chased: bool,
-    ) -> Option<(BrushBatchKind, BatchTextures, [i32; 3], i32)> {
+    ) -> Option<BrushBatchParameters> {
         match self.kind {
             BrushKind::Image { request, ref source, .. } => {
                 let cache_item = match *source {
                     ImageSource::Default => {
                         resolve_image(
                             request,
                             resource_cache,
                             gpu_cache,
@@ -1458,129 +1578,150 @@ impl BrushPrimitive {
                     println!("\tsource {:?}", cache_item);
                 }
 
                 if cache_item.texture_id == TextureSource::Invalid {
                     None
                 } else {
                     let textures = BatchTextures::color(cache_item.texture_id);
 
-                    Some((
+                    Some(BrushBatchParameters::shared(
                         BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
                         textures,
                         [
                             ShaderColorMode::Image as i32,
                             RasterizationSpace::Local as i32,
                             0,
                         ],
                         cache_item.uv_rect_handle.as_int(gpu_cache),
                     ))
                 }
             }
             BrushKind::LineDecoration { ref handle, style, .. } => {
                 match style {
                     LineStyle::Solid => {
-                        Some((
+                        Some(BrushBatchParameters::shared(
                             BrushBatchKind::Solid,
                             BatchTextures::no_texture(),
                             [0; 3],
                             0,
                         ))
                     }
                     LineStyle::Dotted |
                     LineStyle::Dashed |
                     LineStyle::Wavy => {
                         let rt_cache_entry = resource_cache
                             .get_cached_render_task(handle.as_ref().unwrap());
                         let cache_item = resource_cache.get_texture_cache_item(&rt_cache_entry.handle);
                         let textures = BatchTextures::color(cache_item.texture_id);
-                        Some((
+                        Some(BrushBatchParameters::shared(
                             BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
                             textures,
                             [
                                 ShaderColorMode::Image as i32,
                                 RasterizationSpace::Local as i32,
                                 0,
                             ],
                             cache_item.uv_rect_handle.as_int(gpu_cache),
                         ))
                     }
                 }
             }
             BrushKind::Border { ref source, .. } => {
-                let cache_item = match *source {
+                match *source {
                     BorderSource::Image(request) => {
-                        resolve_image(
+                        let cache_item = resolve_image(
                             request,
                             resource_cache,
                             gpu_cache,
                             deferred_resolves,
-                        )
+                        );
+
+                        if cache_item.texture_id == TextureSource::Invalid {
+                            return None;
+                        }
+
+                        let textures = BatchTextures::color(cache_item.texture_id);
+
+                        Some(BrushBatchParameters::shared(
+                            BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
+                            textures,
+                            [
+                                ShaderColorMode::Image as i32,
+                                RasterizationSpace::Local as i32,
+                                0,
+                            ],
+                            cache_item.uv_rect_handle.as_int(gpu_cache),
+                        ))
                     }
-                    BorderSource::Border { ref handle, .. } => {
-                        let rt_handle = match *handle {
-                            Some(ref handle) => handle,
-                            None => return None,
-                        };
-                        let rt_cache_entry = resource_cache
-                            .get_cached_render_task(rt_handle);
-                        resource_cache.get_texture_cache_item(&rt_cache_entry.handle)
-                    }
-                };
+                    BorderSource::Border { ref segments, .. } => {
+                        let mut segment_data = SmallVec::new();
+
+                        // Collect the segment instance data from each render
+                        // task for each valid edge / corner of the border.
 
-                if cache_item.texture_id == TextureSource::Invalid {
-                    None
-                } else {
-                    let textures = BatchTextures::color(cache_item.texture_id);
+                        for segment in segments {
+                            let rt_cache_entry = resource_cache
+                                .get_cached_render_task(segment.handle.as_ref().unwrap());
+                            let cache_item = resource_cache
+                                .get_texture_cache_item(&rt_cache_entry.handle);
+                            segment_data.push(
+                                SegmentInstanceData {
+                                    textures: BatchTextures::color(cache_item.texture_id),
+                                    user_data: cache_item.uv_rect_handle.as_int(gpu_cache),
+                                    is_opaque_override: Some(segment.is_opaque),
+                                }
+                            );
+                        }
 
-                    Some((
-                        BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
-                        textures,
-                        [
-                            ShaderColorMode::Image as i32,
-                            RasterizationSpace::Local as i32,
-                            0,
-                        ],
-                        cache_item.uv_rect_handle.as_int(gpu_cache),
-                    ))
+                        Some(BrushBatchParameters::instanced(
+                            BrushBatchKind::Image(ImageBufferKind::Texture2DArray),
+                            [
+                                ShaderColorMode::Image as i32,
+                                RasterizationSpace::Local as i32,
+                                0,
+                            ],
+                            segment_data,
+                        ))
+                    }
                 }
             }
             BrushKind::Picture { .. } => {
                 panic!("bug: get_batch_key is handled at higher level for pictures");
             }
             BrushKind::Solid { .. } => {
-                Some((
+                Some(BrushBatchParameters::shared(
                     BrushBatchKind::Solid,
                     BatchTextures::no_texture(),
                     [0; 3],
                     0,
                 ))
             }
             BrushKind::Clear => {
-                Some((
+                Some(BrushBatchParameters::shared(
                     BrushBatchKind::Solid,
                     BatchTextures::no_texture(),
                     [0; 3],
                     0,
                 ))
             }
             BrushKind::RadialGradient { ref stops_handle, .. } => {
-                Some((
+                Some(BrushBatchParameters::shared(
                     BrushBatchKind::RadialGradient,
                     BatchTextures::no_texture(),
                     [
                         stops_handle.as_int(gpu_cache),
                         0,
                         0,
                     ],
                     0,
                 ))
             }
             BrushKind::LinearGradient { ref stops_handle, .. } => {
-                Some((
+                Some(BrushBatchParameters::shared(
                     BrushBatchKind::LinearGradient,
                     BatchTextures::no_texture(),
                     [
                         stops_handle.as_int(gpu_cache),
                         0,
                         0,
                     ],
                     0,
@@ -1626,17 +1767,17 @@ impl BrushPrimitive {
 
                 let kind = BrushBatchKind::YuvImage(
                     buffer_kind,
                     format,
                     color_depth,
                     color_space,
                 );
 
-                Some((
+                Some(BrushBatchParameters::shared(
                     kind,
                     textures,
                     [
                         uv_rect_addresses[0],
                         uv_rect_addresses[1],
                         uv_rect_addresses[2],
                     ],
                     0,
--- a/gfx/webrender/src/border.rs
+++ b/gfx/webrender/src/border.rs
@@ -1,23 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{BorderRadius, BorderSide, BorderStyle, ColorF, ColorU, DeviceRect, DeviceSize};
 use api::{LayoutSideOffsets, LayoutSizeAu, LayoutPrimitiveInfo, LayoutToDeviceScale};
-use api::{DeviceVector2D, DevicePoint, DeviceIntSize, LayoutRect, LayoutSize, NormalBorder};
+use api::{DeviceVector2D, DevicePoint, LayoutRect, LayoutSize, NormalBorder, DeviceIntSize};
 use api::{AuHelpers};
-use app_units::Au;
 use ellipse::Ellipse;
-use euclid::SideOffsets2D;
 use display_list_flattener::DisplayListFlattener;
 use gpu_types::{BorderInstance, BorderSegment, BrushFlags};
-use prim_store::{BrushKind, BrushPrimitive, BrushSegment, BrushSegmentVec};
-use prim_store::{EdgeAaSegmentMask, PrimitiveContainer, ScrollNodeAndClipChain};
+use prim_store::{BorderSegmentInfo, BrushKind, BrushPrimitive, BrushSegment, BrushSegmentVec};
+use prim_store::{EdgeAaSegmentMask, PrimitiveContainer, ScrollNodeAndClipChain, BrushSegmentDescriptor};
+use render_task::{RenderTaskCacheKey, RenderTaskCacheKeyKind};
+use smallvec::SmallVec;
 use util::{lerp, RectHelpers};
 
 // Using 2048 as the maximum radius in device space before which we
 // start stretching is up for debate.
 // the value must be chosen so that the corners will not use an
 // unreasonable amount of memory but should allow crisp corners in the
 // common cases.
 
@@ -88,48 +88,40 @@ impl From<BorderSide> for BorderSideAu {
     fn from(side: BorderSide) -> Self {
         BorderSideAu {
             color: side.color.into(),
             style: side.style,
         }
     }
 }
 
+/// Cache key that uniquely identifies a border
+/// edge in the render task cache.
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct BorderCacheKey {
-    pub left: BorderSideAu,
-    pub right: BorderSideAu,
-    pub top: BorderSideAu,
-    pub bottom: BorderSideAu,
-    pub radius: BorderRadiusAu,
-    pub widths: SideOffsets2D<Au>,
+pub struct BorderEdgeCacheKey {
+    pub side: BorderSideAu,
+    pub size: LayoutSizeAu,
     pub do_aa: bool,
-    pub scale: Au,
+    pub segment: BorderSegment,
 }
 
-impl BorderCacheKey {
-    pub fn new(border: &NormalBorder, widths: &LayoutSideOffsets) -> Self {
-        BorderCacheKey {
-            left: border.left.into(),
-            top: border.top.into(),
-            right: border.right.into(),
-            bottom: border.bottom.into(),
-            widths: SideOffsets2D::new(
-                Au::from_f32_px(widths.top),
-                Au::from_f32_px(widths.right),
-                Au::from_f32_px(widths.bottom),
-                Au::from_f32_px(widths.left),
-            ),
-            radius: border.radius.into(),
-            do_aa: border.do_aa,
-            scale: Au(0),
-        }
-    }
+/// Cache key that uniquely identifies a border
+/// corner in the render task cache.
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderCornerCacheKey {
+    pub widths: LayoutSizeAu,
+    pub radius: LayoutSizeAu,
+    pub side0: BorderSideAu,
+    pub side1: BorderSideAu,
+    pub segment: BorderSegment,
+    pub do_aa: bool,
 }
 
 pub fn ensure_no_corner_overlap(
     radius: &mut BorderRadius,
     rect: &LayoutRect,
 ) {
     let mut ratio = 1.0;
     let top_left_radius = &mut radius.top_left;
@@ -172,38 +164,40 @@ pub fn ensure_no_corner_overlap(
     }
 }
 
 impl<'a> DisplayListFlattener<'a> {
     pub fn add_normal_border(
         &mut self,
         info: &LayoutPrimitiveInfo,
         border: &NormalBorder,
-        widths: &LayoutSideOffsets,
+        widths: LayoutSideOffsets,
         clip_and_scroll: ScrollNodeAndClipChain,
     ) {
         let mut border = *border;
         ensure_no_corner_overlap(&mut border.radius, &info.rect);
 
-        let prim = BrushPrimitive::new(
-            BrushKind::new_border(border, *widths),
-            None,
+        let prim = create_normal_border_prim(
+            &info.rect,
+            border,
+            widths,
         );
 
         self.add_primitive(
             clip_and_scroll,
             info,
             Vec::new(),
             PrimitiveContainer::Brush(prim),
         );
     }
 }
 
 pub trait BorderSideHelpers {
     fn border_color(&self, is_inner_border: bool) -> ColorF;
+    fn is_opaque(&self) -> bool;
 }
 
 impl BorderSideHelpers for BorderSide {
     fn border_color(&self, is_inner_border: bool) -> ColorF {
         let lighter = match self.style {
             BorderStyle::Inset => is_inner_border,
             BorderStyle::Outset => !is_inner_border,
             _ => return self.color,
@@ -222,16 +216,21 @@ impl BorderSideHelpers for BorderSide {
         if self.color.r != 0.0 || self.color.g != 0.0 || self.color.b != 0.0 {
             let scale = if lighter { 1.0 } else { 2.0 / 3.0 };
             return self.color.scale_rgb(scale)
         }
 
         let black = if lighter { 0.7 } else { 0.3 };
         ColorF::new(black, black, black, self.color.a)
     }
+
+    /// Returns true if all pixels in this border style are opaque.
+    fn is_opaque(&self) -> bool {
+        self.color.a >= 1.0 && self.style.is_opaque()
+    }
 }
 
 /// The kind of border corner clip.
 #[repr(C)]
 #[derive(Copy, Debug, Clone, PartialEq)]
 pub enum BorderClipKind {
     DashCorner = 1,
     DashEdge = 2,
@@ -512,109 +511,37 @@ struct DotInfo {
 }
 
 impl DotInfo {
     fn new(arc_pos: f32, diameter: f32) -> DotInfo {
         DotInfo { arc_pos, diameter }
     }
 }
 
-#[derive(Debug)]
-pub struct BorderSegmentInfo {
-    task_rect: DeviceRect,
-    segment: BorderSegment,
-    radius: DeviceSize,
-    widths: DeviceSize,
-}
-
-bitflags! {
-    /// Whether we depend on the available size for the border (effectively in
-    /// the local rect of the primitive), and in which direction.
-    ///
-    /// Note that this relies on the corners being only dependent on the border
-    /// widths and radius.
-    ///
-    /// This is not just a single boolean to allow instance caching for border
-    /// boxes where one of the directions differ but not the one on the affected
-    /// border is.
-    ///
-    /// This allows sharing instances for stuff like paragraphs of different
-    /// heights separated by horizontal borders.
-    pub struct AvailableSizeDependence : u8 {
-        /// There's a dependence on the vertical direction, that is, at least
-        /// one of the right or left edges is dashed or dotted.
-        const VERTICAL = 1 << 0;
-        /// Same but for the horizontal direction.
-        const HORIZONTAL = 1 << 1;
-    }
-}
-
-/// This is the data that describes a single border with (up to) four sides and
-/// four corners.
-///
-/// This object gets created for each border primitive at least once. Note,
-/// however, that the instances this produces via `build_instances()` can and
-/// will be shared by multiple borders, as long as they share the same cache
-/// key.
-///
-/// Segments, however, also get build once per primitive.
-///
-/// So the important invariant to preserve when going through this code is that
-/// the result of `build_instances()` would remain invariant for a given cache
-/// key.
-///
-/// That means, then, that `border_segments` can't depend at all on something
-/// that isn't on the key like the available_size, while the brush segments can
-/// (and will, since we skip painting empty segments caused by things like edges
-/// getting constrained by huge border-radius).
-///
-/// Note that the cache key is not only `BorderCacheKey`, but also a
-/// `size` from `RenderTaskCacheKey`, which will always be zero unless
-/// `available_size_dependence` is non-empty, which is effectively just dashed
-/// and dotted borders for now, since the spacing between the dash and dots
-/// changes depending on that size.
-#[derive(Debug)]
-pub struct BorderRenderTaskInfo {
-    pub border_segments: Vec<BorderSegmentInfo>,
-    pub size: DeviceIntSize,
-    pub available_size_dependence: AvailableSizeDependence,
-    do_aa: bool,
-}
-
-#[derive(PartialEq, Eq)]
-enum DependsOnAvailableSize {
-    No,
-    Yes,
-}
-
 /// Information needed to place and draw a border edge.
 #[derive(Debug)]
 struct EdgeInfo {
     /// Offset in local space to place the edge from origin.
     local_offset: f32,
     /// Size of the edge in local space.
     local_size: f32,
-    /// Size in device pixels needed in the render task.
-    device_size: f32,
-    /// Whether this edge depends on the available size.
-    depends_on_available_size: bool,
+    /// Local stretch size for this edge (repeat past this).
+    stretch_size: f32,
 }
 
 impl EdgeInfo {
     fn new(
         local_offset: f32,
         local_size: f32,
-        device_size: f32,
-        depends_on_avail_size: DependsOnAvailableSize,
+        stretch_size: f32,
     ) -> Self {
         Self {
             local_offset,
             local_size,
-            device_size,
-            depends_on_available_size: depends_on_avail_size == DependsOnAvailableSize::Yes,
+            stretch_size,
         }
     }
 }
 
 // Given a side width and the available space, compute the half-dash (half of
 // the 'on' segment) and the count of them for a given segment.
 fn compute_half_dash(side_width: f32, total_size: f32) -> (f32, u32) {
     let half_dash = side_width * 1.5;
@@ -640,458 +567,254 @@ fn compute_half_dash(side_width: f32, to
 
 // Get the needed size in device pixels for an edge,
 // based on the border style of that edge. This is used
 // to determine how big the render task should be.
 fn get_edge_info(
     style: BorderStyle,
     side_width: f32,
     avail_size: f32,
-    scale: f32,
 ) -> EdgeInfo {
     // To avoid division by zero below.
-    if side_width <= 0.0 {
-        return EdgeInfo::new(0.0, 0.0, 0.0, DependsOnAvailableSize::No);
+    if side_width <= 0.0 || avail_size <= 0.0 {
+        return EdgeInfo::new(0.0, 0.0, 0.0);
     }
 
     match style {
         BorderStyle::Dashed => {
             // Basically, two times the dash size.
             let (half_dash, _num_half_dashes) =
                 compute_half_dash(side_width, avail_size);
-            let device_size = (2.0 * 2.0 * half_dash * scale).round();
-            EdgeInfo::new(0., avail_size, device_size, DependsOnAvailableSize::Yes)
+            let stretch_size = 2.0 * 2.0 * half_dash;
+            EdgeInfo::new(0., avail_size, stretch_size)
         }
         BorderStyle::Dotted => {
             let dot_and_space_size = 2.0 * side_width;
             if avail_size < dot_and_space_size * 0.75 {
-                return EdgeInfo::new(0.0, 0.0, 0.0, DependsOnAvailableSize::Yes);
+                return EdgeInfo::new(0.0, 0.0, 0.0);
             }
             let approx_dot_count = avail_size / dot_and_space_size;
             let dot_count = approx_dot_count.floor().max(1.0);
             let used_size = dot_count * dot_and_space_size;
             let extra_space = avail_size - used_size;
-            let device_size = dot_and_space_size * scale;
+            let stretch_size = dot_and_space_size;
             let offset = (extra_space * 0.5).round();
-            EdgeInfo::new(offset, used_size, device_size, DependsOnAvailableSize::Yes)
+            EdgeInfo::new(offset, used_size, stretch_size)
         }
         _ => {
-            EdgeInfo::new(0.0, avail_size, 8.0, DependsOnAvailableSize::No)
+            EdgeInfo::new(0.0, avail_size, 8.0)
         }
     }
 }
 
-impl BorderRenderTaskInfo {
-    pub fn new(
-        rect: &LayoutRect,
-        border: &NormalBorder,
-        widths: &LayoutSideOffsets,
-        scale: LayoutToDeviceScale,
-        brush_segments: &mut BrushSegmentVec,
-    ) -> Option<Self> {
-        let mut border_segments = Vec::new();
-
-        let dp_width_top = (widths.top * scale.0).ceil();
-        let dp_width_bottom = (widths.bottom * scale.0).ceil();
-        let dp_width_left = (widths.left * scale.0).ceil();
-        let dp_width_right = (widths.right * scale.0).ceil();
-
-        let dp_corner_tl = (border.radius.top_left * scale).ceil();
-        let dp_corner_tr = (border.radius.top_right * scale).ceil();
-        let dp_corner_bl = (border.radius.bottom_left * scale).ceil();
-        let dp_corner_br = (border.radius.bottom_right * scale).ceil();
-
-        let dp_size_tl = DeviceSize::new(
-            dp_corner_tl.width.max(dp_width_left),
-            dp_corner_tl.height.max(dp_width_top),
-        );
-        let dp_size_tr = DeviceSize::new(
-            dp_corner_tr.width.max(dp_width_right),
-            dp_corner_tr.height.max(dp_width_top),
-        );
-        let dp_size_br = DeviceSize::new(
-            dp_corner_br.width.max(dp_width_right),
-            dp_corner_br.height.max(dp_width_bottom),
-        );
-        let dp_size_bl = DeviceSize::new(
-            dp_corner_bl.width.max(dp_width_left),
-            dp_corner_bl.height.max(dp_width_bottom),
-        );
-
-        let local_size_tl = LayoutSize::new(
-            border.radius.top_left.width.max(widths.left),
-            border.radius.top_left.height.max(widths.top),
-        );
-        let local_size_tr = LayoutSize::new(
-            border.radius.top_right.width.max(widths.right),
-            border.radius.top_right.height.max(widths.top),
-        );
-        let local_size_br = LayoutSize::new(
-            border.radius.bottom_right.width.max(widths.right),
-            border.radius.bottom_right.height.max(widths.bottom),
-        );
-        let local_size_bl = LayoutSize::new(
-            border.radius.bottom_left.width.max(widths.left),
-            border.radius.bottom_left.height.max(widths.bottom),
-        );
-
-        let top_edge_info = get_edge_info(
-            border.top.style,
-            widths.top,
-            rect.size.width - local_size_tl.width - local_size_tr.width,
-            scale.0,
-        );
-        let bottom_edge_info = get_edge_info(
-            border.bottom.style,
-            widths.bottom,
-            rect.size.width - local_size_bl.width - local_size_br.width,
-            scale.0,
-        );
-        let inner_width = top_edge_info.device_size.max(bottom_edge_info.device_size).ceil();
-
-        let left_edge_info = get_edge_info(
-            border.left.style,
-            widths.left,
-            rect.size.height - local_size_tl.height - local_size_bl.height,
-            scale.0,
-        );
-        let right_edge_info = get_edge_info(
-            border.right.style,
-            widths.right,
-            rect.size.height - local_size_tr.height - local_size_br.height,
-            scale.0,
-        );
-
-        let inner_height = left_edge_info.device_size.max(right_edge_info.device_size).ceil();
-
-        let size = DeviceSize::new(
-            dp_size_tl.width.max(dp_size_bl.width) + inner_width + dp_size_tr.width.max(dp_size_br.width),
-            dp_size_tl.height.max(dp_size_tr.height) + inner_height + dp_size_bl.height.max(dp_size_br.height),
-        );
-
-        if size.width == 0.0 || size.height == 0.0 {
-            return None;
-        }
+/// Create the set of border segments and render task
+/// cache keys for a given CSS border.
+fn create_border_segments(
+    rect: &LayoutRect,
+    border: &NormalBorder,
+    widths: &LayoutSideOffsets,
+    border_segments: &mut SmallVec<[BorderSegmentInfo; 8]>,
+    brush_segments: &mut BrushSegmentVec,
+) {
+    let local_size_tl = LayoutSize::new(
+        border.radius.top_left.width.max(widths.left),
+        border.radius.top_left.height.max(widths.top),
+    );
+    let local_size_tr = LayoutSize::new(
+        border.radius.top_right.width.max(widths.right),
+        border.radius.top_right.height.max(widths.top),
+    );
+    let local_size_br = LayoutSize::new(
+        border.radius.bottom_right.width.max(widths.right),
+        border.radius.bottom_right.height.max(widths.bottom),
+    );
+    let local_size_bl = LayoutSize::new(
+        border.radius.bottom_left.width.max(widths.left),
+        border.radius.bottom_left.height.max(widths.bottom),
+    );
 
-        let mut size_dependence = AvailableSizeDependence::empty();
-        if top_edge_info.depends_on_available_size ||
-            bottom_edge_info.depends_on_available_size
-        {
-            size_dependence.insert(AvailableSizeDependence::HORIZONTAL);
-        }
-
-        if left_edge_info.depends_on_available_size ||
-            right_edge_info.depends_on_available_size
-        {
-            size_dependence.insert(AvailableSizeDependence::VERTICAL);
-        }
-
-        add_edge_segment(
-            LayoutRect::from_floats(
-                rect.origin.x,
-                rect.origin.y + local_size_tl.height + left_edge_info.local_offset,
-                rect.origin.x + widths.left,
-                rect.origin.y + local_size_tl.height + left_edge_info.local_offset + left_edge_info.local_size,
-            ),
-            DeviceRect::from_floats(
-                0.0,
-                dp_size_tl.height,
-                dp_width_left,
-                dp_size_tl.height + left_edge_info.device_size,
-            ),
-            &border.left,
-            BorderSegment::Left,
-            EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT,
-            &mut border_segments,
-            BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_REPEAT_Y,
-            brush_segments,
-        );
+    let top_edge_info = get_edge_info(
+        border.top.style,
+        widths.top,
+        rect.size.width - local_size_tl.width - local_size_tr.width,
+    );
+    let bottom_edge_info = get_edge_info(
+        border.bottom.style,
+        widths.bottom,
+        rect.size.width - local_size_bl.width - local_size_br.width,
+    );
 
-        add_edge_segment(
-            LayoutRect::from_floats(
-                rect.origin.x + local_size_tl.width + top_edge_info.local_offset,
-                rect.origin.y,
-                rect.origin.x + local_size_tl.width + top_edge_info.local_offset + top_edge_info.local_size,
-                rect.origin.y + widths.top,
-            ),
-            DeviceRect::from_floats(
-                dp_size_tl.width,
-                0.0,
-                dp_size_tl.width + top_edge_info.device_size,
-                dp_width_top,
-            ),
-            &border.top,
-            BorderSegment::Top,
-            EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM,
-            &mut border_segments,
-            BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_REPEAT_X,
-            brush_segments,
-        );
-
-        add_edge_segment(
-            LayoutRect::from_floats(
-                rect.origin.x + rect.size.width - widths.right,
-                rect.origin.y + local_size_tr.height + right_edge_info.local_offset,
-                rect.origin.x + rect.size.width,
-                rect.origin.y + local_size_tr.height + right_edge_info.local_offset + right_edge_info.local_size,
-            ),
-            DeviceRect::from_floats(
-                size.width - dp_width_right,
-                dp_size_tr.height,
-                size.width,
-                dp_size_tr.height + right_edge_info.device_size,
-            ),
-            &border.right,
-            BorderSegment::Right,
-            EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT,
-            &mut border_segments,
-            BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_REPEAT_Y,
-            brush_segments,
-        );
-
-        add_edge_segment(
-            LayoutRect::from_floats(
-                rect.origin.x + local_size_bl.width + bottom_edge_info.local_offset,
-                rect.origin.y + rect.size.height - widths.bottom,
-                rect.origin.x + local_size_bl.width + bottom_edge_info.local_offset + bottom_edge_info.local_size,
-                rect.origin.y + rect.size.height,
-            ),
-            DeviceRect::from_floats(
-                dp_size_bl.width,
-                size.height - dp_width_bottom,
-                dp_size_bl.width + bottom_edge_info.device_size,
-                size.height,
-            ),
-            &border.bottom,
-            BorderSegment::Bottom,
-            EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP,
-            &mut border_segments,
-            BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_REPEAT_X,
-            brush_segments,
-        );
+    let left_edge_info = get_edge_info(
+        border.left.style,
+        widths.left,
+        rect.size.height - local_size_tl.height - local_size_bl.height,
+    );
+    let right_edge_info = get_edge_info(
+        border.right.style,
+        widths.right,
+        rect.size.height - local_size_tr.height - local_size_br.height,
+    );
 
-        add_corner_segment(
-            LayoutRect::from_floats(
-                rect.origin.x,
-                rect.origin.y,
-                rect.origin.x + local_size_tl.width,
-                rect.origin.y + local_size_tl.height,
-            ),
-            DeviceRect::from_floats(
-                0.0,
-                0.0,
-                dp_size_tl.width,
-                dp_size_tl.height,
-            ),
-            &border.left,
-            &border.top,
-            DeviceSize::new(dp_width_left, dp_width_top),
-            dp_corner_tl,
-            BorderSegment::TopLeft,
-            EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT,
-            &mut border_segments,
-            brush_segments,
-        );
-
-        add_corner_segment(
-            LayoutRect::from_floats(
-                rect.origin.x + rect.size.width - local_size_tr.width,
-                rect.origin.y,
-                rect.origin.x + rect.size.width,
-                rect.origin.y + local_size_tr.height,
-            ),
-            DeviceRect::from_floats(
-                size.width - dp_size_tr.width,
-                0.0,
-                size.width,
-                dp_size_tr.height,
-            ),
-            &border.top,
-            &border.right,
-            DeviceSize::new(dp_width_right, dp_width_top),
-            dp_corner_tr,
-            BorderSegment::TopRight,
-            EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT,
-            &mut border_segments,
-            brush_segments,
-        );
-
-        add_corner_segment(
-            LayoutRect::from_floats(
-                rect.origin.x + rect.size.width - local_size_br.width,
-                rect.origin.y + rect.size.height - local_size_br.height,
-                rect.origin.x + rect.size.width,
-                rect.origin.y + rect.size.height,
-            ),
-            DeviceRect::from_floats(
-                size.width - dp_size_br.width,
-                size.height - dp_size_br.height,
-                size.width,
-                size.height,
-            ),
-            &border.right,
-            &border.bottom,
-            DeviceSize::new(dp_width_right, dp_width_bottom),
-            dp_corner_br,
-            BorderSegment::BottomRight,
-            EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT,
-            &mut border_segments,
-            brush_segments,
-        );
-
-        add_corner_segment(
-            LayoutRect::from_floats(
-                rect.origin.x,
-                rect.origin.y + rect.size.height - local_size_bl.height,
-                rect.origin.x + local_size_bl.width,
-                rect.origin.y + rect.size.height,
-            ),
-            DeviceRect::from_floats(
-                0.0,
-                size.height - dp_size_bl.height,
-                dp_size_bl.width,
-                size.height,
-            ),
-            &border.bottom,
-            &border.left,
-            DeviceSize::new(dp_width_left, dp_width_bottom),
-            dp_corner_bl,
-            BorderSegment::BottomLeft,
-            EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::LEFT,
-            &mut border_segments,
-            brush_segments,
-        );
+    add_edge_segment(
+        LayoutRect::from_floats(
+            rect.origin.x,
+            rect.origin.y + local_size_tl.height + left_edge_info.local_offset,
+            rect.origin.x + widths.left,
+            rect.origin.y + local_size_tl.height + left_edge_info.local_offset + left_edge_info.local_size,
+        ),
+        &left_edge_info,
+        border.left,
+        widths.left,
+        BorderSegment::Left,
+        EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT,
+        brush_segments,
+        border_segments,
+        border.do_aa,
+    );
+    add_edge_segment(
+        LayoutRect::from_floats(
+            rect.origin.x + local_size_tl.width + top_edge_info.local_offset,
+            rect.origin.y,
+            rect.origin.x + local_size_tl.width + top_edge_info.local_offset + top_edge_info.local_size,
+            rect.origin.y + widths.top,
+        ),
+        &top_edge_info,
+        border.top,
+        widths.top,
+        BorderSegment::Top,
+        EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM,
+        brush_segments,
+        border_segments,
+        border.do_aa,
+    );
+    add_edge_segment(
+        LayoutRect::from_floats(
+            rect.origin.x + rect.size.width - widths.right,
+            rect.origin.y + local_size_tr.height + right_edge_info.local_offset,
+            rect.origin.x + rect.size.width,
+            rect.origin.y + local_size_tr.height + right_edge_info.local_offset + right_edge_info.local_size,
+        ),
+        &right_edge_info,
+        border.right,
+        widths.right,
+        BorderSegment::Right,
+        EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT,
+        brush_segments,
+        border_segments,
+        border.do_aa,
+    );
+    add_edge_segment(
+        LayoutRect::from_floats(
+            rect.origin.x + local_size_bl.width + bottom_edge_info.local_offset,
+            rect.origin.y + rect.size.height - widths.bottom,
+            rect.origin.x + local_size_bl.width + bottom_edge_info.local_offset + bottom_edge_info.local_size,
+            rect.origin.y + rect.size.height,
+        ),
+        &bottom_edge_info,
+        border.bottom,
+        widths.bottom,
+        BorderSegment::Bottom,
+        EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP,
+        brush_segments,
+        border_segments,
+        border.do_aa,
+    );
 
-        Some(BorderRenderTaskInfo {
-            border_segments,
-            size: size.to_i32(),
-            available_size_dependence: size_dependence,
-            do_aa: border.do_aa,
-        })
-    }
-
-    /// Returns the cache key size for this task, based on our available size
-    /// dependence computed earlier.
-    #[inline]
-    pub fn cache_key_size(
-        &self,
-        local_size: &LayoutSize,
-        scale: LayoutToDeviceScale,
-    ) -> DeviceIntSize {
-        let mut size = DeviceIntSize::zero();
-        if self.available_size_dependence.is_empty() {
-            return size;
-        }
-
-        let device_size = (*local_size * scale).to_i32();
-        if self.available_size_dependence.contains(AvailableSizeDependence::VERTICAL) {
-            size.height = device_size.height;
-        }
-        if self.available_size_dependence.contains(AvailableSizeDependence::HORIZONTAL) {
-            size.width = device_size.width;
-        }
-        size
-    }
-
-    pub fn build_instances(&self, border: &NormalBorder) -> Vec<BorderInstance> {
-        let mut instances = Vec::new();
-
-        for info in &self.border_segments {
-            let (side0, side1, flip0, flip1) = match info.segment {
-                BorderSegment::Left => (&border.left, &border.left, false, false),
-                BorderSegment::Top => (&border.top, &border.top, false, false),
-                BorderSegment::Right => (&border.right, &border.right, true, true),
-                BorderSegment::Bottom => (&border.bottom, &border.bottom, true, true),
-                BorderSegment::TopLeft => (&border.left, &border.top, false, false),
-                BorderSegment::TopRight => (&border.top, &border.right, false, true),
-                BorderSegment::BottomRight => (&border.right, &border.bottom, true, true),
-                BorderSegment::BottomLeft => (&border.bottom, &border.left, true, false),
-            };
-
-            let style0 = if side0.style.is_hidden() {
-                side1.style
-            } else {
-                side0.style
-            };
-            let style1 = if side1.style.is_hidden() {
-                side0.style
-            } else {
-                side1.style
-            };
-
-            let color0 = side0.border_color(flip0);
-            let color1 = side1.border_color(flip1);
-
-            add_segment(
-                info.task_rect,
-                style0,
-                style1,
-                color0,
-                color1,
-                info.segment,
-                &mut instances,
-                info.widths,
-                info.radius,
-                self.do_aa,
-            );
-        }
-
-        instances
-    }
-
-    /// Computes the maximum scale that we allow for this set of border parameters.
-    /// capping the scale will result in rendering very large corners at a lower
-    /// resolution and stretching them, so they will have the right shape, but
-    /// blurrier.
-    pub fn get_max_scale(
-        radii: &BorderRadius,
-        widths: &LayoutSideOffsets
-    ) -> LayoutToDeviceScale {
-        let r = radii.top_left.width
-            .max(radii.top_left.height)
-            .max(radii.top_right.width)
-            .max(radii.top_right.height)
-            .max(radii.bottom_left.width)
-            .max(radii.bottom_left.height)
-            .max(radii.bottom_right.width)
-            .max(radii.bottom_right.height)
-            .max(widths.top)
-            .max(widths.bottom)
-            .max(widths.left)
-            .max(widths.right);
-
-        LayoutToDeviceScale::new(MAX_BORDER_RESOLUTION as f32 / r)
-    }
+    add_corner_segment(
+        LayoutRect::from_floats(
+            rect.origin.x,
+            rect.origin.y,
+            rect.origin.x + local_size_tl.width,
+            rect.origin.y + local_size_tl.height,
+        ),
+        border.left,
+        border.top,
+        LayoutSize::new(widths.left, widths.top),
+        border.radius.top_left,
+        BorderSegment::TopLeft,
+        EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT,
+        brush_segments,
+        border_segments,
+        border.do_aa,
+    );
+    add_corner_segment(
+        LayoutRect::from_floats(
+            rect.origin.x + rect.size.width - local_size_tr.width,
+            rect.origin.y,
+            rect.origin.x + rect.size.width,
+            rect.origin.y + local_size_tr.height,
+        ),
+        border.top,
+        border.right,
+        LayoutSize::new(widths.right, widths.top),
+        border.radius.top_right,
+        BorderSegment::TopRight,
+        EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT,
+        brush_segments,
+        border_segments,
+        border.do_aa,
+    );
+    add_corner_segment(
+        LayoutRect::from_floats(
+            rect.origin.x + rect.size.width - local_size_br.width,
+            rect.origin.y + rect.size.height - local_size_br.height,
+            rect.origin.x + rect.size.width,
+            rect.origin.y + rect.size.height,
+        ),
+        border.right,
+        border.bottom,
+        LayoutSize::new(widths.right, widths.bottom),
+        border.radius.bottom_right,
+        BorderSegment::BottomRight,
+        EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT,
+        brush_segments,
+        border_segments,
+        border.do_aa,
+    );
+    add_corner_segment(
+        LayoutRect::from_floats(
+            rect.origin.x,
+            rect.origin.y + rect.size.height - local_size_bl.height,
+            rect.origin.x + local_size_bl.width,
+            rect.origin.y + rect.size.height,
+        ),
+        border.bottom,
+        border.left,
+        LayoutSize::new(widths.left, widths.bottom),
+        border.radius.bottom_left,
+        BorderSegment::BottomLeft,
+        EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::LEFT,
+        brush_segments,
+        border_segments,
+        border.do_aa,
+    );
 }
 
-fn add_brush_segment(
-    image_rect: LayoutRect,
-    task_rect: DeviceRect,
-    brush_flags: BrushFlags,
-    edge_flags: EdgeAaSegmentMask,
-    brush_segments: &mut BrushSegmentVec,
-) {
-    if image_rect.size.width <= 0. || image_rect.size.width <= 0. {
-        return;
-    }
+/// Computes the maximum scale that we allow for this set of border parameters.
+/// capping the scale will result in rendering very large corners at a lower
+/// resolution and stretching them, so they will have the right shape, but
+/// blurrier.
+pub fn get_max_scale_for_border(
+    radii: &BorderRadius,
+    widths: &LayoutSideOffsets
+) -> LayoutToDeviceScale {
+    let r = radii.top_left.width
+        .max(radii.top_left.height)
+        .max(radii.top_right.width)
+        .max(radii.top_right.height)
+        .max(radii.bottom_left.width)
+        .max(radii.bottom_left.height)
+        .max(radii.bottom_right.width)
+        .max(radii.bottom_right.height)
+        .max(widths.top)
+        .max(widths.bottom)
+        .max(widths.left)
+        .max(widths.right);
 
-    brush_segments.push(
-        BrushSegment::new(
-            image_rect,
-            /* may_need_clip_mask = */ true,
-            edge_flags,
-            [
-                task_rect.origin.x,
-                task_rect.origin.y,
-                task_rect.origin.x + task_rect.size.width,
-                task_rect.origin.y + task_rect.size.height,
-            ],
-            brush_flags,
-        )
-    );
+    LayoutToDeviceScale::new(MAX_BORDER_RESOLUTION as f32 / r)
 }
 
 fn add_segment(
     task_rect: DeviceRect,
     style0: BorderStyle,
     style1: BorderStyle,
     color0: ColorF,
     color1: ColorF,
@@ -1204,81 +927,233 @@ fn add_segment(
                 _ => {
                     instances.push(base_instance);
                 }
             }
         }
     }
 }
 
+/// Add a corner segment (if valid) to the list of
+/// border segments for this primitive.
 fn add_corner_segment(
     image_rect: LayoutRect,
-    task_rect: DeviceRect,
-    side0: &BorderSide,
-    side1: &BorderSide,
-    widths: DeviceSize,
-    radius: DeviceSize,
+    side0: BorderSide,
+    side1: BorderSide,
+    widths: LayoutSize,
+    radius: LayoutSize,
     segment: BorderSegment,
     edge_flags: EdgeAaSegmentMask,
-    border_segments: &mut Vec<BorderSegmentInfo>,
     brush_segments: &mut BrushSegmentVec,
+    border_segments: &mut SmallVec<[BorderSegmentInfo; 8]>,
+    do_aa: bool,
 ) {
     if side0.color.a <= 0.0 && side1.color.a <= 0.0 {
         return;
     }
 
     if widths.width <= 0.0 && widths.height <= 0.0 {
         return;
     }
 
     if side0.style.is_hidden() && side1.style.is_hidden() {
         return;
     }
 
-    border_segments.push(BorderSegmentInfo {
-        task_rect,
-        segment,
-        radius,
-        widths,
-    });
+    if image_rect.size.width <= 0. || image_rect.size.height <= 0. {
+        return;
+    }
+
+    let is_opaque =
+        side0.is_opaque() &&
+        side1.is_opaque() &&
+        radius.width <= 0.0 &&
+        radius.height <= 0.0;
 
-    add_brush_segment(
-        image_rect,
-        task_rect,
-        BrushFlags::SEGMENT_RELATIVE,
-        edge_flags,
-        brush_segments,
+    brush_segments.push(
+        BrushSegment::new(
+            image_rect,
+            /* may_need_clip_mask = */ true,
+            edge_flags,
+            [0.0; 4],
+            BrushFlags::SEGMENT_RELATIVE,
+        )
     );
+
+    border_segments.push(BorderSegmentInfo {
+        handle: None,
+        local_task_size: image_rect.size,
+        is_opaque,
+        cache_key: RenderTaskCacheKey {
+            size: DeviceIntSize::zero(),
+            kind: RenderTaskCacheKeyKind::BorderCorner(
+                BorderCornerCacheKey {
+                    do_aa,
+                    side0: side0.into(),
+                    side1: side1.into(),
+                    segment,
+                    radius: radius.to_au(),
+                    widths: widths.to_au(),
+                }
+            ),
+        },
+    });
 }
 
+/// Add an edge segment (if valid) to the list of
+/// border segments for this primitive.
 fn add_edge_segment(
     image_rect: LayoutRect,
-    task_rect: DeviceRect,
-    side: &BorderSide,
+    edge_info: &EdgeInfo,
+    side: BorderSide,
+    width: f32,
     segment: BorderSegment,
     edge_flags: EdgeAaSegmentMask,
-    border_segments: &mut Vec<BorderSegmentInfo>,
-    brush_flags: BrushFlags,
     brush_segments: &mut BrushSegmentVec,
+    border_segments: &mut SmallVec<[BorderSegmentInfo; 8]>,
+    do_aa: bool,
 ) {
     if side.color.a <= 0.0 {
         return;
     }
 
     if side.style.is_hidden() {
         return;
     }
 
+    let (size, brush_flags) = match segment {
+        BorderSegment::Left | BorderSegment::Right => {
+            (LayoutSize::new(width, edge_info.stretch_size), BrushFlags::SEGMENT_REPEAT_Y)
+        }
+        BorderSegment::Top | BorderSegment::Bottom => {
+            (LayoutSize::new(edge_info.stretch_size, width), BrushFlags::SEGMENT_REPEAT_X)
+        }
+        _ => {
+            unreachable!();
+        }
+    };
+
+    if image_rect.size.width <= 0. || image_rect.size.height <= 0. {
+        return;
+    }
+
+    let is_opaque = side.is_opaque();
+
+    brush_segments.push(
+        BrushSegment::new(
+            image_rect,
+            /* may_need_clip_mask = */ true,
+            edge_flags,
+            [0.0, 0.0, size.width, size.height],
+            BrushFlags::SEGMENT_RELATIVE | brush_flags,
+        )
+    );
+
     border_segments.push(BorderSegmentInfo {
-        task_rect,
-        segment,
-        radius: DeviceSize::zero(),
-        widths: task_rect.size,
+        handle: None,
+        local_task_size: size,
+        is_opaque,
+        cache_key: RenderTaskCacheKey {
+            size: DeviceIntSize::zero(),
+            kind: RenderTaskCacheKeyKind::BorderEdge(
+                BorderEdgeCacheKey {
+                    do_aa,
+                    side: side.into(),
+                    size: size.to_au(),
+                    segment,
+                },
+            ),
+        },
     });
+}
+
+/// Build the set of border instances needed to draw a border
+/// segment into the render task cache.
+pub fn build_border_instances(
+    cache_key: &RenderTaskCacheKey,
+    border: &NormalBorder,
+    scale: LayoutToDeviceScale,
+) -> Vec<BorderInstance> {
+    let mut instances = Vec::new();
+
+    let (segment, widths, radius) = match cache_key.kind {
+        RenderTaskCacheKeyKind::BorderEdge(ref key) => {
+            (key.segment, LayoutSize::from_au(key.size), LayoutSize::zero())
+        }
+        RenderTaskCacheKeyKind::BorderCorner(ref key) => {
+            (key.segment, LayoutSize::from_au(key.widths), LayoutSize::from_au(key.radius))
+        }
+        _ => {
+            unreachable!();
+        }
+    };
 
-    add_brush_segment(
-        image_rect,
-        task_rect,
-        brush_flags,
-        edge_flags,
-        brush_segments,
+    let (side0, side1, flip0, flip1) = match segment {
+        BorderSegment::Left => (&border.left, &border.left, false, false),
+        BorderSegment::Top => (&border.top, &border.top, false, false),
+        BorderSegment::Right => (&border.right, &border.right, true, true),
+        BorderSegment::Bottom => (&border.bottom, &border.bottom, true, true),
+        BorderSegment::TopLeft => (&border.left, &border.top, false, false),
+        BorderSegment::TopRight => (&border.top, &border.right, false, true),
+        BorderSegment::BottomRight => (&border.right, &border.bottom, true, true),
+        BorderSegment::BottomLeft => (&border.bottom, &border.left, true, false),
+    };
+
+    let style0 = if side0.style.is_hidden() {
+        side1.style
+    } else {
+        side0.style
+    };
+    let style1 = if side1.style.is_hidden() {
+        side0.style
+    } else {
+        side1.style
+    };
+
+    let color0 = side0.border_color(flip0);
+    let color1 = side1.border_color(flip1);
+
+    let widths = (widths * scale).ceil();
+    let radius = (radius * scale).ceil();
+
+    add_segment(
+        DeviceRect::new(DevicePoint::zero(), cache_key.size.to_f32()),
+        style0,
+        style1,
+        color0,
+        color1,
+        segment,
+        &mut instances,
+        widths,
+        radius,
+        border.do_aa,
     );
+
+    instances
 }
+
+pub fn create_normal_border_prim(
+    local_rect: &LayoutRect,
+    border: NormalBorder,
+    widths: LayoutSideOffsets,
+) -> BrushPrimitive {
+    let mut brush_segments = BrushSegmentVec::new();
+    let mut border_segments = SmallVec::new();
+
+    create_border_segments(
+        local_rect,
+        &border,
+        &widths,
+        &mut border_segments,
+        &mut brush_segments,
+    );
+
+    BrushPrimitive::new(
+        BrushKind::new_border(
+            border,
+            widths,
+            border_segments,
+        ),
+        Some(BrushSegmentDescriptor {
+            segments: brush_segments,
+        }),
+    )
+}
--- a/gfx/webrender/src/clip_scroll_tree.rs
+++ b/gfx/webrender/src/clip_scroll_tree.rs
@@ -400,18 +400,18 @@ impl ClipScrollTree {
             }
             SpatialNodeType::ScrollFrame(scrolling_info) => {
                 pt.new_level(format!("ScrollFrame"));
                 pt.add_item(format!("index: {:?}", index));
                 pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
                 pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
                 pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset));
             }
-            SpatialNodeType::ReferenceFrame(ref info) => {
-                pt.new_level(format!("ReferenceFrame {:?}", info.resolved_transform));
+            SpatialNodeType::ReferenceFrame(ref _info) => {
+                pt.new_level(format!("ReferenceFrame"));
                 pt.add_item(format!("index: {:?}", index));
             }
         }
 
         pt.add_item(format!("world_viewport_transform: {:?}", node.world_viewport_transform));
         pt.add_item(format!("world_content_transform: {:?}", node.world_content_transform));
         pt.add_item(format!("coordinate_system_id: {:?}", node.coordinate_system_id));
 
--- a/gfx/webrender/src/device/gl.rs
+++ b/gfx/webrender/src/device/gl.rs
@@ -1,25 +1,26 @@
 /* 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 super::super::shader_source;
-use api::{ColorF, ImageFormat};
+use api::{ColorF, ImageFormat, MemoryReport};
 use api::{DeviceIntPoint, DeviceIntRect, DeviceUintRect, DeviceUintSize};
 use api::TextureTarget;
 #[cfg(any(feature = "debug_renderer", feature="capture"))]
 use api::ImageDescriptor;
 use euclid::Transform3D;
 use gleam::gl;
-use internal_types::{FastHashMap, RenderTargetInfo};
+use internal_types::{FastHashMap, LayerIndex, RenderTargetInfo};
 use log::Level;
 use smallvec::SmallVec;
 use std::cell::RefCell;
 use std::cmp;
+use std::collections::hash_map::Entry;
 use std::fs::File;
 use std::io::Read;
 use std::marker::PhantomData;
 use std::mem;
 use std::ops::Add;
 use std::path::PathBuf;
 use std::ptr;
 use std::rc::Rc;
@@ -437,55 +438,61 @@ impl ExternalTexture {
 pub struct Texture {
     id: gl::GLuint,
     target: gl::GLuint,
     layer_count: i32,
     format: ImageFormat,
     width: u32,
     height: u32,
     filter: TextureFilter,
-    render_target: Option<RenderTargetInfo>,
-    fbo_ids: Vec<FBOId>,
-    depth_rb: Option<RBOId>,
+    /// Framebuffer Objects, one for each layer of the texture, allowing this
+    /// texture to be rendered to. Empty if this texture is not used as a render
+    /// target.
+    fbos: Vec<FBOId>,
+    /// Same as the above, but with a depth buffer attached.
+    ///
+    /// FBOs are cheap to create but expensive to reconfigure (since doing so
+    /// invalidates framebuffer completeness caching). Moreover, rendering with
+    /// a depth buffer attached but the depth write+test disabled relies on the
+    /// driver to optimize it out of the rendering pass, which most drivers
+    /// probably do but, according to jgilbert, is best not to rely on.
+    ///
+    /// So we lazily generate a second list of FBOs with depth. This list is
+    /// empty if this texture is not used as a render target _or_ if it is, but
+    /// the depth buffer has never been requested.
+    ///
+    /// Note that we always fill fbos, and then lazily create fbos_with_depth
+    /// when needed. We could make both lazy (i.e. render targets would have one
+    /// or the other, but not both, unless they were actually used in both
+    /// configurations). But that would complicate a lot of logic in this module,
+    /// and FBOs are cheap enough to create.
+    fbos_with_depth: Vec<FBOId>,
     last_frame_used: FrameId,
 }
 
 impl Texture {
     pub fn get_dimensions(&self) -> DeviceUintSize {
         DeviceUintSize::new(self.width, self.height)
     }
 
-    pub fn get_render_target_layer_count(&self) -> usize {
-        self.fbo_ids.len()
-    }
-
     pub fn get_layer_count(&self) -> i32 {
         self.layer_count
     }
 
     pub fn get_format(&self) -> ImageFormat {
         self.format
     }
 
     #[cfg(any(feature = "debug_renderer", feature = "capture"))]
     pub fn get_filter(&self) -> TextureFilter {
         self.filter
     }
 
-    #[cfg(any(feature = "debug_renderer", feature = "capture"))]
-    pub fn get_render_target(&self) -> Option<RenderTargetInfo> {
-        self.render_target.clone()
-    }
-
-    pub fn has_depth(&self) -> bool {
-        self.depth_rb.is_some()
-    }
-
-    pub fn get_rt_info(&self) -> Option<&RenderTargetInfo> {
-        self.render_target.as_ref()
+    pub fn supports_depth(&self) -> bool {
+        !self.fbos_with_depth.is_empty()
     }
 
     pub fn used_in_frame(&self, frame_id: FrameId) -> bool {
         self.last_frame_used == frame_id
     }
 
     /// Returns true if this texture was used within `threshold` frames of
     /// the current frame.
@@ -708,16 +715,32 @@ pub struct Capabilities {
 }
 
 #[derive(Clone, Debug)]
 pub enum ShaderError {
     Compilation(String, String), // name, error message
     Link(String, String),        // name, error message
 }
 
+/// A refcounted depth target, which may be shared by multiple textures across
+/// the device.
+struct SharedDepthTarget {
+    /// The Render Buffer Object representing the depth target.
+    rbo_id: RBOId,
+    /// Reference count. When this drops to zero, the RBO is deleted.
+    refcount: usize,
+}
+
+#[cfg(feature = "debug")]
+impl Drop for SharedDepthTarget {
+    fn drop(&mut self) {
+        debug_assert!(thread::panicking() || self.refcount == 0);
+    }
+}
+
 pub struct Device {
     gl: Rc<gl::Gl>,
     // device state
     bound_textures: [gl::GLuint; 16],
     bound_program: gl::GLuint,
     bound_vao: gl::GLuint,
     bound_read_fbo: FBOId,
     bound_draw_fbo: FBOId,
@@ -730,16 +753,22 @@ pub struct Device {
 
     // HW or API capabilities
     #[cfg(feature = "debug_renderer")]
     capabilities: Capabilities,
 
     bgra_format_internal: gl::GLuint,
     bgra_format_external: gl::GLuint,
 
+    /// Map from texture dimensions to shared depth buffers for render targets.
+    ///
+    /// Render targets often have the same width/height, so we can save memory
+    /// by sharing these across targets.
+    depth_targets: FastHashMap<DeviceUintSize, SharedDepthTarget>,
+
     // debug
     inside_frame: bool,
 
     // resources
     resource_override_path: Option<PathBuf>,
 
     max_texture_size: u32,
     renderer_name: String,
@@ -754,16 +783,45 @@ pub struct Device {
     /// otherwise are on some drivers, particularly ANGLE), If it's not
     /// supported, we fall back to glTexImage*.
     supports_texture_storage: bool,
 
     // GL extensions
     extensions: Vec<String>,
 }
 
+/// Contains the parameters necessary to bind a texture-backed draw target.
+#[derive(Clone, Copy)]
+pub struct TextureDrawTarget<'a> {
+    /// The target texture.
+    pub texture: &'a Texture,
+    /// The slice within the texture array to draw to.
+    pub layer: LayerIndex,
+    /// Whether to draw with the texture's associated depth target.
+    pub with_depth: bool,
+}
+
+/// Contains the parameters necessary to bind a texture-backed read target.
+#[derive(Clone, Copy)]
+pub struct TextureReadTarget<'a> {
+    /// The source texture.
+    pub texture: &'a Texture,
+    /// The slice within the texture array to read from.
+    pub layer: LayerIndex,
+}
+
+impl<'a> From<TextureDrawTarget<'a>> for TextureReadTarget<'a> {
+    fn from(t: TextureDrawTarget<'a>) -> Self {
+        TextureReadTarget {
+            texture: t.texture,
+            layer: t.layer,
+        }
+    }
+}
+
 impl Device {
     pub fn new(
         gl: Rc<gl::Gl>,
         resource_override_path: Option<PathBuf>,
         upload_method: UploadMethod,
         cached_programs: Option<Rc<ProgramCache>>,
     ) -> Device {
         let mut max_texture_size = [0];
@@ -824,16 +882,18 @@ impl Device {
             #[cfg(feature = "debug_renderer")]
             capabilities: Capabilities {
                 supports_multisampling: false, //TODO
             },
 
             bgra_format_internal,
             bgra_format_external,
 
+            depth_targets: FastHashMap::default(),
+
             bound_textures: [0; 16],
             bound_program: 0,
             bound_vao: 0,
             bound_read_fbo: FBOId(0),
             bound_draw_fbo: FBOId(0),
             program_mode_id: UniformLocation::INVALID,
             default_read_fbo: 0,
             default_draw_fbo: 0,
@@ -1001,40 +1061,44 @@ impl Device {
         debug_assert!(self.inside_frame);
 
         if self.bound_read_fbo != fbo_id {
             self.bound_read_fbo = fbo_id;
             fbo_id.bind(self.gl(), FBOTarget::Read);
         }
     }
 
-    pub fn bind_read_target(&mut self, texture_and_layer: Option<(&Texture, i32)>) {
-        let fbo_id = texture_and_layer.map_or(FBOId(self.default_read_fbo), |texture_and_layer| {
-            texture_and_layer.0.fbo_ids[texture_and_layer.1 as usize]
+    pub fn bind_read_target(&mut self, texture_target: Option<TextureReadTarget>) {
+        let fbo_id = texture_target.map_or(FBOId(self.default_read_fbo), |target| {
+            target.texture.fbos[target.layer]
         });
 
         self.bind_read_target_impl(fbo_id)
     }
 
     fn bind_draw_target_impl(&mut self, fbo_id: FBOId) {
         debug_assert!(self.inside_frame);
 
         if self.bound_draw_fbo != fbo_id {
             self.bound_draw_fbo = fbo_id;
             fbo_id.bind(self.gl(), FBOTarget::Draw);
         }
     }
 
     pub fn bind_draw_target(
         &mut self,
-        texture_and_layer: Option<(&Texture, i32)>,
+        texture_target: Option<TextureDrawTarget>,
         dimensions: Option<DeviceUintSize>,
     ) {
-        let fbo_id = texture_and_layer.map_or(FBOId(self.default_draw_fbo), |texture_and_layer| {
-            texture_and_layer.0.fbo_ids[texture_and_layer.1 as usize]
+        let fbo_id = texture_target.map_or(FBOId(self.default_draw_fbo), |target| {
+            if target.with_depth {
+                target.texture.fbos_with_depth[target.layer]
+            } else {
+                target.texture.fbos[target.layer]
+            }
         });
 
         self.bind_draw_target_impl(fbo_id);
 
         if let Some(dimensions) = dimensions {
             self.gl.viewport(
                 0,
                 0,
@@ -1227,19 +1291,18 @@ impl Device {
         let mut texture = Texture {
             id: self.gl.gen_textures(1)[0],
             target: get_gl_target(target),
             width,
             height,
             layer_count,
             format,
             filter,
-            render_target,
-            fbo_ids: vec![],
-            depth_rb: None,
+            fbos: vec![],
+            fbos_with_depth: vec![],
             last_frame_used: self.frame_id,
         };
         self.bind_texture(DEFAULT_TEXTURE, &texture);
         self.set_texture_parameters(texture.target, filter);
 
         // Allocate storage.
         let desc = self.gl_describe_format(texture.format);
         let is_array = match texture.target {
@@ -1304,17 +1367,20 @@ impl Device {
                     desc.external,
                     desc.pixel_type,
                     None,
                 ),
         }
 
         // Set up FBOs, if required.
         if let Some(rt_info) = render_target {
-            self.init_fbos(&mut texture, rt_info);
+            self.init_fbos(&mut texture, false);
+            if rt_info.has_depth {
+                self.init_fbos(&mut texture, true);
+            }
         }
 
         texture
     }
 
     fn set_texture_parameters(&mut self, target: gl::GLuint, filter: TextureFilter) {
         let mag_filter = match filter {
             TextureFilter::Nearest => gl::NEAREST,
@@ -1344,35 +1410,39 @@ impl Device {
         dst: &mut Texture,
         src: &Texture,
     ) {
         debug_assert!(self.inside_frame);
         debug_assert!(dst.width >= src.width);
         debug_assert!(dst.height >= src.height);
 
         let rect = DeviceIntRect::new(DeviceIntPoint::zero(), src.get_dimensions().to_i32());
-        for (read_fbo, draw_fbo) in src.fbo_ids.iter().zip(&dst.fbo_ids) {
+        for (read_fbo, draw_fbo) in src.fbos.iter().zip(&dst.fbos) {
             self.bind_read_target_impl(*read_fbo);
             self.bind_draw_target_impl(*draw_fbo);
             self.blit_render_target(rect, rect);
         }
         self.bind_read_target(None);
     }
 
     /// Notifies the device that the contents of a render target are no longer
     /// needed.
+    ///
+    /// FIXME(bholley): We could/should invalidate the depth targets earlier
+    /// than the color targets, i.e. immediately after each pass.
     pub fn invalidate_render_target(&mut self, texture: &Texture) {
-        let attachments: &[gl::GLenum] = if texture.has_depth() {
-            &[gl::COLOR_ATTACHMENT0, gl::DEPTH_ATTACHMENT]
+        let (fbos, attachments) = if texture.supports_depth() {
+            (&texture.fbos_with_depth,
+             &[gl::COLOR_ATTACHMENT0, gl::DEPTH_ATTACHMENT] as &[gl::GLenum])
         } else {
-            &[gl::COLOR_ATTACHMENT0]
+            (&texture.fbos, &[gl::COLOR_ATTACHMENT0] as &[gl::GLenum])
         };
 
         let original_bound_fbo = self.bound_draw_fbo;
-        for fbo_id in texture.fbo_ids.iter() {
+        for fbo_id in fbos.iter() {
             // Note: The invalidate extension may not be supported, in which
             // case this is a no-op. That's ok though, because it's just a
             // hint.
             self.bind_external_draw_target(*fbo_id);
             self.gl.invalidate_framebuffer(gl::FRAMEBUFFER, attachments);
         }
         self.bind_external_draw_target(original_bound_fbo);
     }
@@ -1381,52 +1451,38 @@ impl Device {
     ///
     /// This method adds or removes a depth target as necessary.
     pub fn reuse_render_target<T: Texel>(
         &mut self,
         texture: &mut Texture,
         rt_info: RenderTargetInfo,
     ) {
         texture.last_frame_used = self.frame_id;
-        texture.render_target = Some(rt_info);
 
-        // If the depth target requirements changed, just drop the FBOs and
-        // reinitialize.
-        //
-        // FIXME(bholley): I have a patch to do this better.
-        if rt_info.has_depth != texture.has_depth() {
-            self.deinit_fbos(texture);
-            self.init_fbos(texture, rt_info);
+        // Add depth support if needed.
+        if rt_info.has_depth && !texture.supports_depth() {
+            self.init_fbos(texture, true);
         }
     }
 
-    fn init_fbos(&mut self, texture: &mut Texture, rt_info: RenderTargetInfo) {
-        // Generate the FBOs.
-        assert!(texture.fbo_ids.is_empty());
-        texture.fbo_ids.extend(
-            self.gl.gen_framebuffers(texture.layer_count).into_iter().map(FBOId)
-        );
+    fn init_fbos(&mut self, texture: &mut Texture, with_depth: bool) {
+        let (fbos, depth_rb) = if with_depth {
+            let depth_target = self.acquire_depth_target(texture.get_dimensions());
+            (&mut texture.fbos_with_depth, Some(depth_target))
+        } else {
+            (&mut texture.fbos, None)
+        };
 
-        // Optionally generate a depth target.
-        if rt_info.has_depth {
-            let renderbuffer_ids = self.gl.gen_renderbuffers(1);
-            let depth_rb = renderbuffer_ids[0];
-            texture.depth_rb = Some(RBOId(depth_rb));
-            self.gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
-            self.gl.renderbuffer_storage(
-                gl::RENDERBUFFER,
-                gl::DEPTH_COMPONENT24,
-                texture.width as _,
-                texture.height as _,
-            );
-        }
+        // Generate the FBOs.
+        assert!(fbos.is_empty());
+        fbos.extend(self.gl.gen_framebuffers(texture.layer_count).into_iter().map(FBOId));
 
         // Bind the FBOs.
         let original_bound_fbo = self.bound_draw_fbo;
-        for (fbo_index, &fbo_id) in texture.fbo_ids.iter().enumerate() {
+        for (fbo_index, &fbo_id) in fbos.iter().enumerate() {
             self.bind_external_draw_target(fbo_id);
             match texture.target {
                 gl::TEXTURE_2D_ARRAY => {
                     self.gl.framebuffer_texture_layer(
                         gl::DRAW_FRAMEBUFFER,
                         gl::COLOR_ATTACHMENT0,
                         texture.id,
                         0,
@@ -1440,44 +1496,72 @@ impl Device {
                         gl::COLOR_ATTACHMENT0,
                         texture.target,
                         texture.id,
                         0,
                     )
                 }
             }
 
-            if let Some(depth_rb) = texture.depth_rb {
+            if let Some(depth_rb) = depth_rb {
                 self.gl.framebuffer_renderbuffer(
                     gl::DRAW_FRAMEBUFFER,
                     gl::DEPTH_ATTACHMENT,
                     gl::RENDERBUFFER,
                     depth_rb.0,
                 );
             }
         }
         self.bind_external_draw_target(original_bound_fbo);
     }
 
-    fn deinit_fbos(&mut self, texture: &mut Texture) {
-        if let Some(RBOId(depth_rb)) = texture.depth_rb.take() {
-            self.gl.delete_renderbuffers(&[depth_rb]);
-            texture.depth_rb = None;
-        }
-
-        if !texture.fbo_ids.is_empty() {
-            let fbo_ids: Vec<_> = texture
-                .fbo_ids
+    fn deinit_fbos(&mut self, fbos: &mut Vec<FBOId>) {
+        if !fbos.is_empty() {
+            let fbo_ids: SmallVec<[gl::GLuint; 8]> = fbos
                 .drain(..)
                 .map(|FBOId(fbo_id)| fbo_id)
                 .collect();
             self.gl.delete_framebuffers(&fbo_ids[..]);
         }
     }
 
+    fn acquire_depth_target(&mut self, dimensions: DeviceUintSize) -> RBOId {
+        let gl = &self.gl;
+        let target = self.depth_targets.entry(dimensions).or_insert_with(|| {
+            let renderbuffer_ids = gl.gen_renderbuffers(1);
+            let depth_rb = renderbuffer_ids[0];
+            gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
+            gl.renderbuffer_storage(
+                gl::RENDERBUFFER,
+                gl::DEPTH_COMPONENT24,
+                dimensions.width as _,
+                dimensions.height as _,
+            );
+            SharedDepthTarget {
+                rbo_id: RBOId(depth_rb),
+                refcount: 0,
+            }
+        });
+        target.refcount += 1;
+        target.rbo_id
+    }
+
+    fn release_depth_target(&mut self, dimensions: DeviceUintSize) {
+        let mut entry = match self.depth_targets.entry(dimensions) {
+            Entry::Occupied(x) => x,
+            Entry::Vacant(..) => panic!("Releasing unknown depth target"),
+        };
+        debug_assert!(entry.get().refcount != 0);
+        entry.get_mut().refcount -= 1;
+        if entry.get().refcount == 0 {
+            let t = entry.remove();
+            self.gl.delete_renderbuffers(&[t.rbo_id.0]);
+        }
+    }
+
     pub fn blit_render_target(&mut self, src_rect: DeviceIntRect, dest_rect: DeviceIntRect) {
         debug_assert!(self.inside_frame);
 
         self.gl.blit_framebuffer(
             src_rect.origin.x,
             src_rect.origin.y,
             src_rect.origin.x + src_rect.size.width,
             src_rect.origin.y + src_rect.size.height,
@@ -1487,17 +1571,23 @@ impl Device {
             dest_rect.origin.y + dest_rect.size.height,
             gl::COLOR_BUFFER_BIT,
             gl::LINEAR,
         );
     }
 
     pub fn delete_texture(&mut self, mut texture: Texture) {
         debug_assert!(self.inside_frame);
-        self.deinit_fbos(&mut texture);
+        let had_depth = texture.supports_depth();
+        self.deinit_fbos(&mut texture.fbos);
+        self.deinit_fbos(&mut texture.fbos_with_depth);
+        if had_depth {
+            self.release_depth_target(texture.get_dimensions());
+        }
+
         self.gl.delete_textures(&[texture.id]);
 
         for bound_texture in &mut self.bound_textures {
             if *bound_texture == texture.id {
                 *bound_texture = 0
             }
         }
 
@@ -2312,16 +2402,28 @@ impl Device {
             },
             ImageFormat::RG8 => FormatDesc {
                 internal: gl::RG8,
                 external: gl::RG,
                 pixel_type: gl::UNSIGNED_BYTE,
             },
         }
     }
+
+    /// Generates a memory report for the resources managed by the device layer.
+    pub fn report_memory(&self) -> MemoryReport {
+        let mut report = MemoryReport::default();
+        for dim in self.depth_targets.keys() {
+            // DEPTH24 textures generally reserve 3 bytes for depth and 1 byte
+            // for stencil, so we measure them as 32 bytes.
+            let pixels: u32 = dim.width * dim.height;
+            report.depth_target_textures += (pixels as usize) * 4;
+        }
+        report
+    }
 }
 
 struct FormatDesc {
     internal: gl::GLenum,
     external: gl::GLuint,
     pixel_type: gl::GLuint,
 }
 
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -1,16 +1,16 @@
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{AlphaType, BorderDetails, BorderDisplayItem, BuiltDisplayListIter, ClipAndScrollInfo};
 use api::{ClipId, ColorF, ComplexClipRegion, DeviceIntPoint, DeviceIntRect, DeviceIntSize};
-use api::{DevicePixelScale, DeviceUintRect, DisplayItemRef, ExtendMode, ExternalScrollId};
+use api::{DisplayItemRef, ExtendMode, ExternalScrollId};
 use api::{FilterOp, FontInstanceKey, GlyphInstance, GlyphOptions, RasterSpace, GradientStop};
 use api::{IframeDisplayItem, ImageKey, ImageRendering, ItemRange, LayoutPoint, ColorDepth};
 use api::{LayoutPrimitiveInfo, LayoutRect, LayoutSize, LayoutTransform, LayoutVector2D};
 use api::{LineOrientation, LineStyle, NinePatchBorderSource, PipelineId};
 use api::{PropertyBinding, ReferenceFrame, RepeatMode, ScrollFrameDisplayItem, ScrollSensitivity};
 use api::{Shadow, SpecificDisplayItem, StackingContext, StickyFrameDisplayItem, TexelRect};
 use api::{ClipMode, TransformStyle, YuvColorSpace, YuvData};
 use clip::{ClipChainId, ClipRegion, ClipItemKey, ClipStore};
@@ -27,17 +27,17 @@ use picture::{PictureCompositeMode, Pict
 use prim_store::{BrushKind, BrushPrimitive, BrushSegmentDescriptor, PrimitiveInstance};
 use prim_store::{EdgeAaSegmentMask, ImageSource, PrimitiveOpacity, PrimitiveKey};
 use prim_store::{BorderSource, BrushSegment, BrushSegmentVec, PrimitiveContainer, PrimitiveIndex, PrimitiveStore};
 use prim_store::{OpacityBinding, ScrollNodeAndClipChain, TextRunPrimitive};
 use render_backend::{DocumentView};
 use resource_cache::{FontInstanceMap, ImageRequest};
 use scene::{Scene, ScenePipeline, StackingContextHelpers};
 use scene_builder::DocumentResources;
-use spatial_node::{SpatialNodeType, StickyFrameInfo};
+use spatial_node::{StickyFrameInfo};
 use std::{f32, mem};
 use std::collections::vec_deque::VecDeque;
 use tiling::{CompositeOps};
 use util::{MaxRect, RectHelpers};
 
 #[derive(Debug, Copy, Clone)]
 struct ClipNode {
     id: ClipChainId,
@@ -204,17 +204,16 @@ impl<'a> DisplayListFlattener<'a> {
             root_prim_index: PrimitiveIndex(0),
         };
 
         flattener.push_root(
             root_pipeline_id,
             &root_pipeline.viewport_size,
             &root_pipeline.content_size,
         );
-        flattener.setup_viewport_offset(view.inner_rect, view.accumulated_scale_factor());
         flattener.flatten_root(root_pipeline, &root_pipeline.viewport_size);
 
         debug_assert!(flattener.sc_stack.is_empty());
 
         new_scene.root_pipeline_id = Some(root_pipeline_id);
         new_scene.pipeline_epochs = scene.pipeline_epochs.clone();
         new_scene.pipelines = scene.pipelines.clone();
 
@@ -1252,30 +1251,16 @@ impl<'a> DisplayListFlattener<'a> {
         match parent_id {
             Some(ref parent_id) =>
                 self.id_to_index_mapper.map_to_parent_clip_chain(reference_frame_id, parent_id),
             _ => self.id_to_index_mapper.add_clip_chain(reference_frame_id, ClipChainId::NONE, 0),
         }
         index
     }
 
-    pub fn setup_viewport_offset(
-        &mut self,
-        inner_rect: DeviceUintRect,
-        device_pixel_scale: DevicePixelScale,
-    ) {
-        let viewport_offset = (inner_rect.origin.to_vector().to_f32() / device_pixel_scale).round();
-        let root_id = self.clip_scroll_tree.root_reference_frame_index();
-        let root_node = &mut self.clip_scroll_tree.spatial_nodes[root_id.0];
-        if let SpatialNodeType::ReferenceFrame(ref mut info) = root_node.node_type {
-            info.resolved_transform =
-                LayoutVector2D::new(viewport_offset.x, viewport_offset.y).into();
-        }
-    }
-
     pub fn push_root(
         &mut self,
         pipeline_id: PipelineId,
         viewport_size: &LayoutSize,
         content_size: &LayoutSize,
     ) {
         if let ChasePrimitive::Index(prim_index) = self.config.chase_primitive {
             println!("Chasing {:?} by index", prim_index);
@@ -1479,17 +1464,20 @@ impl<'a> DisplayListFlattener<'a> {
                             info.rect = info.rect.translate(&pending_shadow.shadow.offset);
                             info.clip_rect = info.clip_rect.translate(&pending_shadow.shadow.offset);
 
                             // Construct and add a primitive for the given shadow.
                             let shadow_prim_instance = self.create_primitive(
                                 &info,
                                 pending_primitive.clip_and_scroll.clip_chain_id,
                                 pending_primitive.clip_and_scroll.spatial_node_index,
-                                pending_primitive.container.create_shadow(&pending_shadow.shadow),
+                                pending_primitive.container.create_shadow(
+                                    &pending_shadow.shadow,
+                                    &info.rect,
+                                ),
                             );
 
                             // Add the new primitive to the shadow picture.
                             prims.push(shadow_prim_instance);
                         }
                     }
 
                     // No point in adding a shadow here if there were no primitives
@@ -1682,17 +1670,19 @@ impl<'a> DisplayListFlattener<'a> {
                     repeat_horizontal: RepeatMode,
                     repeat_vertical: RepeatMode
                 ) {
                     if uv_rect.uv1.x > uv_rect.uv0.x &&
                        uv_rect.uv1.y > uv_rect.uv0.y {
 
                         // Use segment relative interpolation for all
                         // instances in this primitive.
-                        let mut brush_flags = BrushFlags::SEGMENT_RELATIVE;
+                        let mut brush_flags =
+                            BrushFlags::SEGMENT_RELATIVE |
+                            BrushFlags::SEGMENT_TEXEL_RECT;
 
                         // Enable repeat modes on the segment.
                         if repeat_horizontal == RepeatMode::Repeat {
                             brush_flags |= BrushFlags::SEGMENT_REPEAT_X;
                         }
                         if repeat_vertical == RepeatMode::Repeat {
                             brush_flags |= BrushFlags::SEGMENT_REPEAT_Y;
                         }
@@ -1840,17 +1830,22 @@ impl<'a> DisplayListFlattener<'a> {
                 };
 
                 let prim = PrimitiveContainer::Brush(
                     BrushPrimitive::new(brush_kind, Some(descriptor))
                 );
                 self.add_primitive(clip_and_scroll, info, Vec::new(), prim);
             }
             BorderDetails::Normal(ref border) => {
-                self.add_normal_border(info, border, &border_item.widths, clip_and_scroll);
+                self.add_normal_border(
+                    info,
+                    border,
+                    border_item.widths,
+                    clip_and_scroll,
+                );
             }
         }
     }
 
     pub fn create_brush_kind_for_gradient(
         &mut self,
         info: &LayoutPrimitiveInfo,
         start_point: LayoutPoint,
--- a/gfx/webrender/src/gpu_glyph_renderer.rs
+++ b/gfx/webrender/src/gpu_glyph_renderer.rs
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! GPU glyph rasterization using Pathfinder.
 
 use api::{DeviceIntPoint, DeviceIntRect, DeviceUintSize, FontRenderMode};
 use api::{ImageFormat, TextureTarget};
 use debug_colors;
-use device::{Device, Texture, TextureFilter, VAO};
+use device::{Device, Texture, TextureDrawTarget, TextureFilter, VAO};
 use euclid::{Point2D, Size2D, Transform3D, TypedVector2D, Vector2D};
 use internal_types::RenderTargetInfo;
 use pathfinder_gfx_utils::ShelfBinPacker;
 use profiler::GpuProfileTag;
 use renderer::{self, ImageBufferKind, Renderer, RendererError, RendererStats};
 use renderer::{TextureSampler, VertexArrayKind, ShaderPrecacheFlags};
 use shade::{LazilyCompiledShader, ShaderKind};
 use tiling::GlyphJob;
@@ -189,17 +189,21 @@ impl Renderer {
             1,
         );
         self.device.upload_texture_immediate(&path_info_texture, &path_info_texels);
 
         self.gpu_glyph_renderer.vector_stencil.bind(&mut self.device,
                                                     projection,
                                                     &mut self.renderer_errors);
 
-        self.device.bind_draw_target(Some((&current_page.texture, 0)), Some(*target_size));
+        self.device.bind_draw_target(Some(TextureDrawTarget {
+            texture: &current_page.texture,
+            layer: 0,
+            with_depth: false,
+        }), Some(*target_size));
         self.device.clear_target(Some([0.0, 0.0, 0.0, 0.0]), None, None);
 
         self.device.set_blend(true);
         self.device.set_blend_mode_subpixel_pass1();
 
         let mut instance_data = vec![];
         for (path_id, &glyph_id) in glyph_indices.iter().enumerate() {
             let glyph = &glyphs[glyph_id];
--- a/gfx/webrender/src/gpu_types.rs
+++ b/gfx/webrender/src/gpu_types.rs
@@ -84,17 +84,17 @@ pub struct BlurInstance {
 #[repr(C)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ScalingInstance {
     pub task_address: RenderTaskAddress,
     pub src_task_address: RenderTaskAddress,
 }
 
-#[derive(Debug, Copy, Clone, PartialEq)]
+#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
 #[repr(C)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum BorderSegment {
     TopLeft,
     TopRight,
     BottomRight,
     BottomLeft,
@@ -313,16 +313,18 @@ bitflags! {
         const PERSPECTIVE_INTERPOLATION = 0x1;
         /// Do interpolation relative to segment rect,
         /// rather than primitive rect.
         const SEGMENT_RELATIVE = 0x2;
         /// Repeat UVs horizontally.
         const SEGMENT_REPEAT_X = 0x4;
         /// Repeat UVs vertically.
         const SEGMENT_REPEAT_Y = 0x8;
+        /// The extra segment data is a texel rect.
+        const SEGMENT_TEXEL_RECT = 0x10;
     }
 }
 
 // TODO(gw): Some of these fields can be moved to the primitive
 //           header since they are constant, and some can be
 //           compressed to a smaller size.
 #[repr(C)]
 pub struct BrushInstance {
--- a/gfx/webrender/src/internal_types.rs
+++ b/gfx/webrender/src/internal_types.rs
@@ -33,16 +33,29 @@ pub type FastHashSet<K> = HashSet<K, Bui
 /// thread maintains a map from cache texture ID to native texture.
 ///
 /// We never reuse IDs, so we use a u64 here to be safe.
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct CacheTextureId(pub u64);
 
+/// Canonical type for texture layer indices.
+///
+/// WebRender is currently not very consistent about layer index types. Some
+/// places use i32 (since that's the type used in various OpenGL APIs), some
+/// places use u32 (since having it be signed is non-sensical, but the
+/// underlying graphics APIs generally operate on 32-bit integers) and some
+/// places use usize (since that's most natural in Rust).
+///
+/// Going forward, we aim to us usize throughout the codebase, since that allows
+/// operations like indexing without a cast, and convert to the required type in
+/// the device module when making calls into the platform layer.
+pub type LayerIndex = usize;
+
 /// Identifies a render pass target that is persisted until the end of the frame.
 ///
 /// By default, only the targets of the immediately-preceding pass are bound as
 /// inputs to the next pass. However, tasks can opt into having their target
 /// preserved in a list until the end of the frame, and this type specifies the
 /// index in that list.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
--- a/gfx/webrender/src/platform/windows/font.rs
+++ b/gfx/webrender/src/platform/windows/font.rs
@@ -126,19 +126,30 @@ impl FontContext {
     }
 
     pub fn add_native_font(&mut self, font_key: &FontKey, font_handle: dwrote::FontDescriptor) {
         if self.fonts.contains_key(font_key) {
             return;
         }
 
         let system_fc = dwrote::FontCollection::system();
-        let font = match system_fc.get_font_from_descriptor(&font_handle) {
-            Some(font) => font,
-            None => { panic!("missing descriptor {:?}", font_handle) }
+        // A version of get_font_from_descriptor() that panics early to help with bug 1455848
+        let font = if let Some(family) = system_fc.get_font_family_by_name(&font_handle.family_name) {
+            let font = family.get_first_matching_font(font_handle.weight, font_handle.stretch, font_handle.style);
+            // Exact matches only here
+            if font.weight() == font_handle.weight &&
+                font.stretch() == font_handle.stretch &&
+                font.style() == font_handle.style
+            {
+                font
+            } else {
+                panic!("font mismatch for descriptor {:?} {:?}", font_handle, font.to_descriptor())
+            }
+        } else {
+            panic!("missing font family for descriptor {:?}", font_handle)
         };
         let face = font.create_font_face();
         self.fonts.insert(*font_key, face);
     }
 
     pub fn delete_font(&mut self, font_key: &FontKey) {
         if let Some(_) = self.fonts.remove(font_key) {
             self.variations.retain(|k, _| k.0 != *font_key);
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -2,35 +2,35 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{AlphaType, BorderRadius, BuiltDisplayList, ClipMode, ColorF, PictureRect};
 use api::{DeviceIntRect, DeviceIntSize, DevicePixelScale, ExtendMode, DeviceRect, PictureToRasterTransform};
 use api::{FilterOp, GlyphInstance, GradientStop, ImageKey, ImageRendering, ItemRange, TileOffset};
 use api::{RasterSpace, LayoutPoint, LayoutRect, LayoutSideOffsets, LayoutSize, LayoutToWorldTransform};
 use api::{LayoutVector2D, PremultipliedColorF, PropertyBinding, Shadow, YuvColorSpace, YuvFormat};
-use api::{DeviceIntSideOffsets, WorldPixel, BoxShadowClipMode, LayoutToWorldScale, NormalBorder, WorldRect};
+use api::{DeviceIntSideOffsets, WorldPixel, BoxShadowClipMode, NormalBorder, WorldRect, LayoutToWorldScale};
 use api::{PicturePixel, RasterPixel, ColorDepth, LineStyle, LineOrientation, LayoutSizeAu, AuHelpers};
 use app_units::Au;
-use border::{BorderCacheKey, BorderRenderTaskInfo};
+use border::{get_max_scale_for_border, build_border_instances, create_normal_border_prim};
 use clip_scroll_tree::{ClipScrollTree, CoordinateSystemId, SpatialNodeIndex};
 use clip::{ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem, ClipNodeCollector};
 use euclid::{TypedTransform3D, TypedRect, TypedScale};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use frame_builder::PrimitiveContext;
 use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
 use gpu_cache::{GpuBlockData, GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest,
                 ToGpuBlocks};
 use gpu_types::BrushFlags;
 use image::{for_each_tile, for_each_repetition};
 use intern;
 use picture::{PictureCompositeMode, PicturePrimitive};
 #[cfg(debug_assertions)]
 use render_backend::FrameId;
-use render_task::{BlitSource, RenderTask, RenderTaskCacheKey};
+use render_task::{BlitSource, RenderTask, RenderTaskCacheKey, to_cache_size};
 use render_task::{RenderTaskCacheKeyKind, RenderTaskId, RenderTaskCacheEntryHandle};
 use renderer::{MAX_VERTEX_TEXTURE_WIDTH};
 use resource_cache::{ImageProperties, ImageRequest, ResourceCache};
 use scene::SceneProperties;
 use std::{cmp, fmt, mem, usize};
 use util::{ScaleOffset, MatrixHelpers, pack_as_float, project_rect, raster_rect_to_device_pixels};
 use smallvec::SmallVec;
 
@@ -340,23 +340,31 @@ pub struct VisibleImageTile {
 
 #[derive(Debug)]
 pub struct VisibleGradientTile {
     pub handle: GpuCacheHandle,
     pub local_rect: LayoutRect,
     pub local_clip_rect: LayoutRect,
 }
 
+/// Information about how to cache a border segment,
+/// along with the current render task cache entry.
+#[derive(Debug)]
+pub struct BorderSegmentInfo {
+    pub handle: Option<RenderTaskCacheEntryHandle>,
+    pub local_task_size: LayoutSize,
+    pub cache_key: RenderTaskCacheKey,
+    pub is_opaque: bool,
+}
+
 #[derive(Debug)]
 pub enum BorderSource {
     Image(ImageRequest),
     Border {
-        handle: Option<RenderTaskCacheEntryHandle>,
-        cache_key: BorderCacheKey,
-        task_info: Option<BorderRenderTaskInfo>,
+        segments: SmallVec<[BorderSegmentInfo; 8]>,
         border: NormalBorder,
         widths: LayoutSideOffsets,
     },
 }
 
 #[derive(Debug)]
 pub enum BrushKind {
     Solid {
@@ -451,28 +459,29 @@ impl BrushKind {
         BrushKind::Solid {
             color,
             opacity_binding: OpacityBinding::new(),
         }
     }
 
     // Construct a brush that is a border with `border` style and `widths`
     // dimensions.
-    pub fn new_border(mut border: NormalBorder, widths: LayoutSideOffsets) -> BrushKind {
+    pub fn new_border(
+        mut border: NormalBorder,
+        widths: LayoutSideOffsets,
+        segments: SmallVec<[BorderSegmentInfo; 8]>,
+    ) -> BrushKind {
         // FIXME(emilio): Is this the best place to do this?
         border.normalize(&widths);
 
-        let cache_key = BorderCacheKey::new(&border, &widths);
         BrushKind::Border {
             source: BorderSource::Border {
                 border,
                 widths,
-                cache_key,
-                task_info: None,
-                handle: None,
+                segments,
             }
         }
     }
 
     // Construct a brush that is an image wisth `stretch_size` dimensions and
     // `color`.
     pub fn new_image(
         request: ImageRequest,
@@ -1407,17 +1416,21 @@ impl PrimitiveContainer {
                 }
             }
         }
     }
 
     // Create a clone of this PrimitiveContainer, applying whatever
     // changes are necessary to the primitive to support rendering
     // it as part of the supplied shadow.
-    pub fn create_shadow(&self, shadow: &Shadow) -> PrimitiveContainer {
+    pub fn create_shadow(
+        &self,
+        shadow: &Shadow,
+        prim_rect: &LayoutRect,
+    ) -> PrimitiveContainer {
         match *self {
             PrimitiveContainer::TextRun(ref info) => {
                 let mut font = FontInstance {
                     color: shadow.color.into(),
                     ..info.specified_font.clone()
                 };
                 if shadow.blur_radius > 0.0 {
                     font.disable_subpixel_aa();
@@ -1435,32 +1448,35 @@ impl PrimitiveContainer {
                 match brush.kind {
                     BrushKind::Solid { .. } => {
                         PrimitiveContainer::Brush(BrushPrimitive::new(
                             BrushKind::new_solid(shadow.color),
                             None,
                         ))
                     }
                     BrushKind::Border { ref source } => {
-                        let source = match *source {
+                        let prim = match *source {
                             BorderSource::Image(request) => {
-                                BrushKind::Border {
-                                    source: BorderSource::Image(request)
-                                }
-                            },
+                                BrushPrimitive::new(
+                                    BrushKind::Border {
+                                        source: BorderSource::Image(request)
+                                    },
+                                    None,
+                                )
+                            }
                             BorderSource::Border { border, widths, .. } => {
                                 let border = border.with_color(shadow.color);
-                                BrushKind::new_border(border, widths)
-
+                                create_normal_border_prim(
+                                    prim_rect,
+                                    border,
+                                    widths,
+                                )
                             }
                         };
-                        PrimitiveContainer::Brush(BrushPrimitive::new(
-                            source,
-                            None,
-                        ))
+                        PrimitiveContainer::Brush(prim)
                     }
                     BrushKind::LineDecoration { style, orientation, wavy_line_thickness, .. } => {
                         PrimitiveContainer::Brush(BrushPrimitive::new_line_decoration(
                             shadow.color,
                             style,
                             orientation,
                             wavy_line_thickness,
                         ))
@@ -1955,23 +1971,16 @@ impl PrimitiveStore {
                 Some(rect) => rect,
                 None => {
                     return false;
                 }
             };
 
             prim_instance.clipped_world_rect = Some(clipped_world_rect);
 
-            prim.build_prim_segments_if_needed(
-                prim_instance,
-                pic_state,
-                frame_state,
-                frame_context,
-            );
-
             prim.update_clip_task(
                 prim_instance,
                 prim_context,
                 clipped_world_rect,
                 pic_state.raster_spatial_node_index,
                 &clip_chain,
                 pic_state,
                 frame_context,
@@ -2834,19 +2843,56 @@ impl Primitive {
                                         image_properties.descriptor.is_opaque;
 
                                     frame_state.resource_cache.request_image(
                                         request,
                                         frame_state.gpu_cache,
                                     );
                                 }
                             }
-                            BorderSource::Border { .. } => {
-                                // Handled earlier since we need to update the segment
-                                // descriptor *before* update_clip_task() is called.
+                            BorderSource::Border { ref border, ref widths, ref mut segments, .. } => {
+                                // TODO(gw): When drawing in screen raster mode, we should also incorporate a
+                                //           scale factor from the world transform to get an appropriately
+                                //           sized border task.
+                                let world_scale = LayoutToWorldScale::new(1.0);
+                                let mut scale = world_scale * frame_context.device_pixel_scale;
+                                let max_scale = get_max_scale_for_border(&border.radius, widths);
+                                scale.0 = scale.0.min(max_scale.0);
+
+                                // For each edge and corner, request the render task by content key
+                                // from the render task cache. This ensures that the render task for
+                                // this segment will be available for batching later in the frame.
+                                for segment in segments {
+                                    // Update the cache key device size based on requested scale.
+                                    segment.cache_key.size = to_cache_size(segment.local_task_size * scale);
+
+                                    segment.handle = Some(frame_state.resource_cache.request_render_task(
+                                        segment.cache_key.clone(),
+                                        frame_state.gpu_cache,
+                                        frame_state.render_tasks,
+                                        None,
+                                        segment.is_opaque,
+                                        |render_tasks| {
+                                            let task = RenderTask::new_border_segment(
+                                                segment.cache_key.size,
+                                                build_border_instances(
+                                                    &segment.cache_key,
+                                                    border,
+                                                    scale,
+                                                ),
+                                            );
+
+                                            let task_id = render_tasks.add(task);
+
+                                            pic_state.tasks.push(task_id);
+
+                                            task_id
+                                        }
+                                    ));
+                                }
                             }
                         }
                     }
                     BrushKind::RadialGradient {
                         stops_range,
                         center,
                         start_radius,
                         end_radius,
@@ -3087,105 +3133,16 @@ impl Primitive {
                     println!("\tcreated task {:?} with device rect {:?}",
                         clip_task_id, device_rect);
                 }
                 prim_instance.clip_task_id = Some(clip_task_id);
                 pic_state.tasks.push(clip_task_id);
             }
         }
     }
-
-    fn build_prim_segments_if_needed(
-        &mut self,
-        prim_instance: &mut PrimitiveInstance,
-        pic_state: &mut PictureState,
-        frame_state: &mut FrameBuildingState,
-        frame_context: &FrameBuildingContext,
-    ) {
-        let brush = match self.details {
-            PrimitiveDetails::Brush(ref mut brush) => brush,
-            PrimitiveDetails::TextRun(..) => return,
-        };
-
-        if let BrushKind::Border {
-            source: BorderSource::Border {
-                ref border,
-                ref mut cache_key,
-                ref widths,
-                ref mut handle,
-                ref mut task_info,
-                ..
-            }
-        } = brush.kind {
-            // TODO(gw): When drawing in screen raster mode, we should also incorporate a
-            //           scale factor from the world transform to get an appropriately
-            //           sized border task.
-            let world_scale = LayoutToWorldScale::new(1.0);
-            let mut scale = world_scale * frame_context.device_pixel_scale;
-            let max_scale = BorderRenderTaskInfo::get_max_scale(&border.radius, &widths);
-            scale.0 = scale.0.min(max_scale.0);
-            let scale_au = Au::from_f32_px(scale.0);
-
-            // NOTE(emilio): This `needs_update` relies on the local rect for a
-            // given primitive being immutable. If that changes, this code
-            // should probably handle changes to it as well, retaining the old
-            // size in cache_key.
-            let needs_update = scale_au != cache_key.scale;
-
-            let mut new_segments = BrushSegmentVec::new();
-
-            let local_rect = &self.metadata.local_rect;
-            if needs_update {
-                cache_key.scale = scale_au;
-
-                *task_info = BorderRenderTaskInfo::new(
-                    local_rect,
-                    border,
-                    widths,
-                    scale,
-                    &mut new_segments,
-                );
-            }
-
-            *handle = task_info.as_ref().map(|task_info| {
-                frame_state.resource_cache.request_render_task(
-                    RenderTaskCacheKey {
-                        size: task_info.cache_key_size(&local_rect.size, scale),
-                        kind: RenderTaskCacheKeyKind::Border(cache_key.clone()),
-                    },
-                    frame_state.gpu_cache,
-                    frame_state.render_tasks,
-                    None,
-                    false,          // todo
-                    |render_tasks| {
-                        let task = RenderTask::new_border(
-                            task_info.size,
-                            task_info.build_instances(border),
-                        );
-
-                        let task_id = render_tasks.add(task);
-
-                        pic_state.tasks.push(task_id);
-
-                        task_id
-                    }
-                )
-            });
-
-            if needs_update {
-                brush.segment_desc = Some(BrushSegmentDescriptor {
-                    segments: new_segments,
-                });
-
-                // The segments have changed, so force the GPU cache to
-                // re-upload the primitive information.
-                frame_state.gpu_cache.invalidate(&mut prim_instance.gpu_location);
-            }
-        }
-    }
 }
 
 pub fn get_raster_rects(
     pic_rect: PictureRect,
     map_to_raster: &SpaceMapper<PicturePixel, RasterPixel>,
     map_to_world: &SpaceMapper<RasterPixel, WorldPixel>,
     prim_bounding_rect: WorldRect,
     device_pixel_scale: DevicePixelScale,
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -272,16 +272,22 @@ impl Document {
                 tx.send(self.get_scroll_node_state()).unwrap();
             }
             FrameMsg::UpdateDynamicProperties(property_bindings) => {
                 self.dynamic_properties.set_properties(property_bindings);
             }
             FrameMsg::AppendDynamicProperties(property_bindings) => {
                 self.dynamic_properties.add_properties(property_bindings);
             }
+            FrameMsg::SetPinchZoom(factor) => {
+                if self.view.pinch_zoom_factor != factor.get() {
+                    self.view.pinch_zoom_factor = factor.get();
+                    self.frame_is_valid = false;
+                }
+            }
         }
 
         DocumentOps::nop()
     }
 
     fn build_frame(
         &mut self,
         resource_cache: &mut ResourceCache,
@@ -503,19 +509,16 @@ impl RenderBackend {
 
         match message {
             SceneMsg::UpdateEpoch(pipeline_id, epoch) => {
                 txn.epoch_updates.push((pipeline_id, epoch));
             }
             SceneMsg::SetPageZoom(factor) => {
                 doc.view.page_zoom_factor = factor.get();
             }
-            SceneMsg::SetPinchZoom(factor) => {
-                doc.view.pinch_zoom_factor = factor.get();
-            }
             SceneMsg::SetWindowParameters {
                 window_size,
                 inner_rect,
                 device_pixel_ratio,
             } => {
                 doc.view.window_size = window_size;
                 doc.view.inner_rect = inner_rect;
                 doc.view.device_pixel_ratio = device_pixel_ratio;
--- a/gfx/webrender/src/render_task.rs
+++ b/gfx/webrender/src/render_task.rs
@@ -2,28 +2,28 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DeviceSize, DeviceIntSideOffsets};
 use api::{DevicePixelScale, ImageDescriptor, ImageFormat};
 use api::{LineStyle, LineOrientation, LayoutSize};
 #[cfg(feature = "pathfinder")]
 use api::FontRenderMode;
-use border::BorderCacheKey;
+use border::{BorderCornerCacheKey, BorderEdgeCacheKey};
 use box_shadow::{BoxShadowCacheKey};
 use clip::{ClipDataStore, ClipItem, ClipStore, ClipNodeRange};
 use clip_scroll_tree::SpatialNodeIndex;
 use device::TextureFilter;
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
 use glyph_rasterizer::GpuGlyphCacheKey;
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::{BorderInstance, ImageSource, UvRectKind};
-use internal_types::{CacheTextureId, FastHashMap, SavedTargetIndex};
+use internal_types::{CacheTextureId, FastHashMap, LayerIndex, SavedTargetIndex};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
 use picture::PictureCacheKey;
 use prim_store::{PrimitiveIndex, ImageCacheKey, LineDecorationCacheKey};
 #[cfg(feature = "debugger")]
 use print_tree::{PrintTreePrinter};
 use render_backend::FrameId;
 use resource_cache::{CacheItem, ResourceCache};
@@ -108,17 +108,17 @@ impl RenderTaskTree {
         }
 
         // Sanity check - can be relaxed if needed
         match task.location {
             RenderTaskLocation::Fixed(..) => {
                 debug_assert!(pass_index == passes.len() - 1);
             }
             RenderTaskLocation::Dynamic(..) |
-            RenderTaskLocation::TextureCache(..) => {
+            RenderTaskLocation::TextureCache { .. } => {
                 debug_assert!(pass_index < passes.len() - 1);
             }
         }
 
         let pass_index = if task.is_global_cached_task() {
             0
         } else {
             pass_index
@@ -187,17 +187,24 @@ pub enum RenderTaskLocation {
     /// The second member specifies the width and height of the task
     /// output, and the first member is initially left as `None`. During the
     /// build phase, we invoke `RenderTargetList::alloc()` and store the
     /// resulting location in the first member. That location identifies the
     /// render target and the offset of the allocated region within that target.
     Dynamic(Option<(DeviceIntPoint, RenderTargetIndex)>, DeviceIntSize),
     /// The output of the `RenderTask` will be persisted beyond this frame, and
     /// thus should be drawn into the `TextureCache`.
-    TextureCache(CacheTextureId, i32, DeviceIntRect),
+    TextureCache {
+        /// Which texture in the texture cache should be drawn into.
+        texture: CacheTextureId,
+        /// The target layer in the above texture.
+        layer: LayerIndex,
+        /// The target region within the above layer.
+        rect: DeviceIntRect,
+    },
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct CacheMaskTask {
     actual_rect: DeviceIntRect,
     pub root_spatial_node_index: SpatialNodeIndex,
@@ -381,17 +388,17 @@ impl RenderTask {
         content_origin: DeviceIntPoint,
         children: Vec<RenderTaskId>,
         uv_rect_kind: UvRectKind,
         root_spatial_node_index: SpatialNodeIndex,
     ) -> Self {
         let size = match location {
             RenderTaskLocation::Dynamic(_, size) => size,
             RenderTaskLocation::Fixed(rect) => rect.size,
-            RenderTaskLocation::TextureCache(_, _, rect) => rect.size,
+            RenderTaskLocation::TextureCache { rect, .. } => rect.size,
         };
 
         render_task_sanity_check(&size);
 
         let can_merge = size.width as f32 >= unclipped_size.width &&
                         size.height as f32 >= unclipped_size.height;
 
         RenderTask {
@@ -650,17 +657,17 @@ impl RenderTask {
                 target_kind,
                 uv_rect_handle: GpuCacheHandle::new(),
                 uv_rect_kind,
             }),
             clear_mode,
         )
     }
 
-    pub fn new_border(
+    pub fn new_border_segment(
         size: DeviceIntSize,
         instances: Vec<BorderInstance>,
     ) -> Self {
         RenderTask::with_dynamic_location(
             size,
             Vec::new(),
             RenderTaskKind::Border(BorderTask {
                 instances,
@@ -834,17 +841,17 @@ impl RenderTask {
             }
         }
     }
 
     pub fn get_dynamic_size(&self) -> DeviceIntSize {
         match self.location {
             RenderTaskLocation::Fixed(..) => DeviceIntSize::zero(),
             RenderTaskLocation::Dynamic(_, size) => size,
-            RenderTaskLocation::TextureCache(_, _, rect) => rect.size,
+            RenderTaskLocation::TextureCache { rect, .. } => rect.size,
         }
     }
 
     pub fn get_target_rect(&self) -> (DeviceIntRect, RenderTargetIndex) {
         match self.location {
             RenderTaskLocation::Fixed(rect) => {
                 (rect, RenderTargetIndex(0))
             }
@@ -863,17 +870,17 @@ impl RenderTask {
             //           to mark a task as unused explicitly. This
             //           would allow us to restore this debug check.
             RenderTaskLocation::Dynamic(Some((origin, target_index)), size) => {
                 (DeviceIntRect::new(origin, size), target_index)
             }
             RenderTaskLocation::Dynamic(None, _) => {
                 (DeviceIntRect::zero(), RenderTargetIndex(0))
             }
-            RenderTaskLocation::TextureCache(_, layer, rect) => {
+            RenderTaskLocation::TextureCache {layer, rect, .. } => {
                 (rect, RenderTargetIndex(layer as usize))
             }
         }
     }
 
     pub fn target_kind(&self) -> RenderTargetKind {
         match self.kind {
             RenderTaskKind::Readback(..) => RenderTargetKind::Color,
@@ -1039,33 +1046,34 @@ impl RenderTask {
 
     /// Mark this render task for keeping the results alive up until the end of the frame.
     pub fn mark_for_saving(&mut self) {
         match self.location {
             RenderTaskLocation::Fixed(..) |
             RenderTaskLocation::Dynamic(..) => {
                 self.saved_index = Some(SavedTargetIndex::PENDING);
             }
-            RenderTaskLocation::TextureCache(..) => {
+            RenderTaskLocation::TextureCache { .. } => {
                 panic!("Unable to mark a permanently cached task for saving!");
             }
         }
     }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum RenderTaskCacheKeyKind {
     BoxShadow(BoxShadowCacheKey),
     Image(ImageCacheKey),
     #[allow(dead_code)]
     Glyph(GpuGlyphCacheKey),
     Picture(PictureCacheKey),
-    Border(BorderCacheKey),
+    BorderEdge(BorderEdgeCacheKey),
+    BorderCorner(BorderCornerCacheKey),
     LineDecoration(LineDecorationCacheKey),
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderTaskCacheKey {
     pub size: DeviceIntSize,
@@ -1156,17 +1164,17 @@ impl RenderTaskCache {
 
             if let Some(pending_render_task_id) = entry.pending_render_task_id.take() {
                 let render_task = &mut render_tasks[pending_render_task_id];
                 let target_kind = render_task.target_kind();
 
                 // Find out what size to alloc in the texture cache.
                 let size = match render_task.location {
                     RenderTaskLocation::Fixed(..) |
-                    RenderTaskLocation::TextureCache(..) => {
+                    RenderTaskLocation::TextureCache { .. } => {
                         panic!("BUG: dynamic task was expected");
                     }
                     RenderTaskLocation::Dynamic(_, size) => size,
                 };
 
                 // Select the right texture page to allocate from.
                 let image_format = match target_kind {
                     RenderTargetKind::Color => ImageFormat::BGRA8,
@@ -1198,21 +1206,21 @@ impl RenderTaskCache {
 
                 // Get the allocation details in the texture cache, and store
                 // this in the render task. The renderer will draw this
                 // task into the appropriate layer and rect of the texture
                 // cache on this frame.
                 let (texture_id, texture_layer, uv_rect) =
                     texture_cache.get_cache_location(&entry.handle);
 
-                render_task.location = RenderTaskLocation::TextureCache(
-                    texture_id,
-                    texture_layer,
-                    uv_rect.to_i32()
-                );
+                render_task.location = RenderTaskLocation::TextureCache {
+                    texture: texture_id,
+                    layer: texture_layer,
+                    rect: uv_rect.to_i32(),
+                };
             }
         }
     }
 
     pub fn request_render_task<F>(
         &mut self,
         key: RenderTaskCacheKey,
         texture_cache: &mut TextureCache,
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -31,17 +31,17 @@ use api::{RenderApiSender, RenderNotifie
 use api::{channel};
 use api::DebugCommand;
 use api::channel::PayloadReceiverHelperMethods;
 use batch::{BatchKind, BatchTextures, BrushBatchKind};
 #[cfg(any(feature = "capture", feature = "replay"))]
 use capture::{CaptureConfig, ExternalCaptureImage, PlainExternalImage};
 use debug_colors;
 use device::{DepthFunction, Device, FrameId, Program, UploadMethod, Texture, PBO};
-use device::{ExternalTexture, FBOId, TextureSlot};
+use device::{ExternalTexture, FBOId, TextureDrawTarget, TextureReadTarget, TextureSlot};
 use device::{ShaderError, TextureFilter,
              VertexUsageHint, VAO, VBO, CustomVAO};
 use device::{ProgramCache, ReadPixelsFormat};
 #[cfg(feature = "debug_renderer")]
 use euclid::rect;
 use euclid::Transform3D;
 use frame_builder::{ChasePrimitive, FrameBuilderConfig};
 use gleam::gl;
@@ -49,17 +49,17 @@ use glyph_rasterizer::{GlyphFormat, Glyp
 use gpu_cache::{GpuBlockData, GpuCacheUpdate, GpuCacheUpdateList};
 #[cfg(feature = "debug_renderer")]
 use gpu_cache::GpuDebugChunk;
 #[cfg(feature = "pathfinder")]
 use gpu_glyph_renderer::GpuGlyphRenderer;
 use gpu_types::ScalingInstance;
 use internal_types::{TextureSource, ORTHO_FAR_PLANE, ORTHO_NEAR_PLANE, ResourceCacheError};
 use internal_types::{CacheTextureId, DebugOutput, FastHashMap, RenderedDocument, ResultMsg};
-use internal_types::{TextureUpdateList, TextureUpdateOp, TextureUpdateSource};
+use internal_types::{LayerIndex, TextureUpdateList, TextureUpdateOp, TextureUpdateSource};
 use internal_types::{RenderTargetInfo, SavedTargetIndex};
 use prim_store::DeferredResolve;
 use profiler::{BackendProfileCounters, FrameProfileCounters,
                GpuProfileTag, RendererProfileCounters, RendererProfileTimers};
 use device::query::GpuProfiler;
 use rayon::{ThreadPool, ThreadPoolBuilder};
 use record::ApiRecordingReceiver;
 use render_backend::RenderBackend;
@@ -1332,17 +1332,21 @@ impl GpuCacheTexture {
                 rows_dirty
             }
             GpuCacheBus::Scatter { ref program, ref vao, count, .. } => {
                 device.disable_depth();
                 device.set_blend(false);
                 device.bind_program(program);
                 device.bind_custom_vao(vao);
                 device.bind_draw_target(
-                    Some((texture, 0)),
+                    Some(TextureDrawTarget {
+                        texture,
+                        layer: 0,
+                        with_depth: false,
+                    }),
                     Some(texture.get_dimensions()),
                 );
                 device.draw_nonindexed_points(0, count as _);
                 0
             }
         }
     }
 }
@@ -2913,17 +2917,17 @@ impl Renderer {
             }
         }
 
         self.profile_counters.vertices.add(6 * data.len());
     }
 
     fn handle_readback_composite(
         &mut self,
-        render_target: Option<(&Texture, i32)>,
+        render_target: Option<TextureDrawTarget>,
         framebuffer_size: DeviceUintSize,
         scissor_rect: Option<DeviceIntRect>,
         source: &RenderTask,
         backdrop: &RenderTask,
         readback: &RenderTask,
     ) {
         if scissor_rect.is_some() {
             self.device.disable_scissor();
@@ -2946,34 +2950,38 @@ impl Renderer {
             RenderTaskKind::Picture(ref task_info) => task_info.content_origin,
             _ => panic!("bug: composite on non-picture?"),
         };
 
         // Bind the FBO to blit the backdrop to.
         // Called per-instance in case the layer (and therefore FBO)
         // changes. The device will skip the GL call if the requested
         // target is already bound.
-        let cache_draw_target = (cache_texture, readback_layer.0 as i32);
+        let cache_draw_target = TextureDrawTarget {
+            texture: cache_texture,
+            layer: readback_layer.0 as usize,
+            with_depth: false,
+        };
         self.device.bind_draw_target(Some(cache_draw_target), None);
 
         let mut src = DeviceIntRect::new(
             source_screen_origin + (backdrop_rect.origin - backdrop_screen_origin),
             readback_rect.size,
         );
         let mut dest = readback_rect.to_i32();
 
         // Need to invert the y coordinates and flip the image vertically when
         // reading back from the framebuffer.
         if render_target.is_none() {
             src.origin.y = framebuffer_size.height as i32 - src.size.height - src.origin.y;
             dest.origin.y += dest.size.height;
             dest.size.height = -dest.size.height;
         }
 
-        self.device.bind_read_target(render_target);
+        self.device.bind_read_target(render_target.map(|r| r.into()));
         self.device.blit_render_target(src, dest);
 
         // Restore draw target to current pass render target + layer.
         // Note: leaving the viewport unchanged, it's not a part of FBO state
         self.device.bind_draw_target(render_target, None);
 
         if scissor_rect.is_some() {
             self.device.enable_scissor();
@@ -2992,32 +3000,32 @@ impl Renderer {
         let _timer = self.gpu_profile.start_timer(GPU_TAG_BLIT);
 
         // TODO(gw): For now, we don't bother batching these by source texture.
         //           If if ever shows up as an issue, we can easily batch them.
         for blit in blits {
             let source_rect = match blit.source {
                 BlitJobSource::Texture(texture_id, layer, source_rect) => {
                     // A blit from a texture into this target.
-                    let src_texture = self.texture_resolver
+                    let texture = self.texture_resolver
                         .resolve(&texture_id)
                         .expect("BUG: invalid source texture");
-                    self.device.bind_read_target(Some((src_texture, layer)));
+                    self.device.bind_read_target(Some(TextureReadTarget { texture, layer: layer as usize }));
                     source_rect
                 }
                 BlitJobSource::RenderTask(task_id) => {
                     // A blit from the child render task into this target.
                     // TODO(gw): Support R8 format here once we start
                     //           creating mips for alpha masks.
-                    let src_texture = self.texture_resolver
+                    let texture = self.texture_resolver
                         .resolve(&TextureSource::PrevPassColor)
                         .expect("BUG: invalid source texture");
                     let source = &render_tasks[task_id];
                     let (source_rect, layer) = source.get_target_rect();
-                    self.device.bind_read_target(Some((src_texture, layer.0 as i32)));
+                    self.device.bind_read_target(Some(TextureReadTarget { texture, layer: layer.0 }));
                     source_rect
                 }
             };
             debug_assert_eq!(source_rect.size, blit.target_rect.size);
             self.device.blit_render_target(
                 source_rect,
                 blit.target_rect,
             );
@@ -3054,33 +3062,33 @@ impl Renderer {
             VertexArrayKind::Scale,
             &BatchTextures::no_texture(),
             stats,
         );
     }
 
     fn draw_color_target(
         &mut self,
-        render_target: Option<(&Texture, i32)>,
+        render_target: Option<TextureDrawTarget>,
         target: &ColorRenderTarget,
         framebuffer_target_rect: DeviceUintRect,
         target_size: DeviceUintSize,
         depth_is_ready: bool,
         clear_color: Option<[f32; 4]>,
         render_tasks: &RenderTaskTree,
         projection: &Transform3D<f32>,
         frame_id: FrameId,
         stats: &mut RendererStats,
     ) {
         self.profile_counters.color_targets.inc();
         let _gm = self.gpu_profile.start_marker("color target");
 
         // sanity check for the depth buffer
-        if let Some((texture, _)) = render_target {
-            assert!(texture.has_depth() >= target.needs_depth());
+        if let Some(t) = render_target {
+            assert!(t.texture.supports_depth() >= target.needs_depth());
         }
 
         let framebuffer_kind = if render_target.is_none() {
             FramebufferKind::Main
         } else {
             FramebufferKind::Other
         };
 
@@ -3367,27 +3375,27 @@ impl Renderer {
                 };
                 let (src_rect, _) = render_tasks[output.task_id].get_target_rect();
                 let mut dest_rect = DeviceIntRect::new(DeviceIntPoint::zero(), output_size);
 
                 // Invert Y coordinates, to correctly convert between coordinate systems.
                 dest_rect.origin.y += dest_rect.size.height;
                 dest_rect.size.height *= -1;
 
-                self.device.bind_read_target(render_target);
+                self.device.bind_read_target(render_target.map(|r| r.into()));
                 self.device.bind_external_draw_target(fbo_id);
                 self.device.blit_render_target(src_rect, dest_rect);
                 handler.unlock(output.pipeline_id);
             }
         }
     }
 
     fn draw_alpha_target(
         &mut self,
-        render_target: (&Texture, i32),
+        render_target: TextureDrawTarget,
         target: &AlphaRenderTarget,
         target_size: DeviceUintSize,
         projection: &Transform3D<f32>,
         render_tasks: &RenderTaskTree,
         stats: &mut RendererStats,
     ) {
         self.profile_counters.alpha_targets.inc();
         let _gm = self.gpu_profile.start_marker("alpha target");
@@ -3522,17 +3530,17 @@ impl Renderer {
         }
 
         self.gpu_profile.finish_sampler(alpha_sampler);
     }
 
     fn draw_texture_cache_target(
         &mut self,
         texture: &CacheTextureId,
-        layer: i32,
+        layer: LayerIndex,
         target: &TextureCacheRenderTarget,
         render_tasks: &RenderTaskTree,
         stats: &mut RendererStats,
     ) {
         let texture_source = TextureSource::TextureCache(*texture);
         let (target_size, projection) = {
             let texture = self.texture_resolver
                 .resolve(&texture_source)
@@ -3556,18 +3564,21 @@ impl Renderer {
 
         // Handle any Pathfinder glyphs.
         let stencil_page = self.stencil_glyphs(&target.glyphs, &projection, &target_size, stats);
 
         {
             let texture = self.texture_resolver
                 .resolve(&texture_source)
                 .expect("BUG: invalid target texture");
-            self.device
-                .bind_draw_target(Some((texture, layer)), Some(target_size));
+            self.device.bind_draw_target(Some(TextureDrawTarget {
+                texture,
+                layer,
+                with_depth: false,
+            }), Some(target_size));
         }
 
         self.device.disable_depth();
         self.device.disable_depth_write();
         self.set_blend(false, FramebufferKind::Other);
 
         for rect in &target.clears {
             self.device.clear_target(Some([0.0, 0.0, 0.0, 0.0]), None, Some(*rect));
@@ -3796,25 +3807,25 @@ impl Renderer {
         }
 
         counters.targets_used.inc();
 
         // Try finding a match in the existing pool. If there's no match, we'll
         // create a new texture.
         let selector = TargetSelector {
             size: list.max_size,
-            num_layers: list.targets.len() as _,
+            num_layers: list.targets.len(),
             format: list.format,
         };
         let index = self.texture_resolver.render_target_pool
             .iter()
             .position(|texture| {
                 selector == TargetSelector {
                     size: texture.get_dimensions(),
-                    num_layers: texture.get_render_target_layer_count(),
+                    num_layers: texture.get_layer_count() as usize,
                     format: texture.get_format(),
                 }
             });
 
         let rt_info = RenderTargetInfo { has_depth: list.needs_depth() };
         let texture = if let Some(idx) = index {
             let mut t = self.texture_resolver.render_target_pool.swap_remove(idx);
             self.device.reuse_render_target::<u8>(&mut t, rt_info);
@@ -3975,17 +3986,21 @@ impl Renderer {
                             alpha.max_size.width as f32,
                             0.0,
                             alpha.max_size.height as f32,
                             ORTHO_NEAR_PLANE,
                             ORTHO_FAR_PLANE,
                         );
 
                         self.draw_alpha_target(
-                            (&alpha_tex.as_ref().unwrap().texture, target_index as i32),
+                            TextureDrawTarget {
+                                texture: &alpha_tex.as_ref().unwrap().texture,
+                                layer: target_index,
+                                with_depth: false,
+                            },
                             target,
                             alpha.max_size,
                             &projection,
                             &frame.render_tasks,
                             stats,
                         );
                     }
 
@@ -3997,17 +4012,21 @@ impl Renderer {
                             color.max_size.width as f32,
                             0.0,
                             color.max_size.height as f32,
                             ORTHO_NEAR_PLANE,
                             ORTHO_FAR_PLANE,
                         );
 
                         self.draw_color_target(
-                            Some((&color_tex.as_ref().unwrap().texture, target_index as i32)),
+                            Some(TextureDrawTarget {
+                                texture: &color_tex.as_ref().unwrap().texture,
+                                layer: target_index,
+                                with_depth: target.needs_depth(),
+                            }),
                             target,
                             frame.inner_rect,
                             color.max_size,
                             false,
                             Some([0.0, 0.0, 0.0, 0.0]),
                             &frame.render_tasks,
                             &projection,
                             frame_id,
@@ -4101,34 +4120,33 @@ impl Renderer {
             return;
         }
 
         let mut spacing = 16;
         let mut size = 512;
         let fb_width = framebuffer_size.width as i32;
         let num_layers: i32 = self.texture_resolver.render_target_pool
             .iter()
-            .map(|texture| texture.get_render_target_layer_count() as i32)
+            .map(|texture| texture.get_layer_count() as i32)
             .sum();
 
         if num_layers * (size + spacing) > fb_width {
             let factor = fb_width as f32 / (num_layers * (size + spacing)) as f32;
             size = (size as f32 * factor) as i32;
             spacing = (spacing as f32 * factor) as i32;
         }
 
         let mut target_index = 0;
         for texture in &self.texture_resolver.render_target_pool {
             let dimensions = texture.get_dimensions();
             let src_rect = DeviceIntRect::new(DeviceIntPoint::zero(), dimensions.to_i32());
 
-            let layer_count = texture.get_render_target_layer_count();
-            for layer_index in 0 .. layer_count {
-                self.device
-                    .bind_read_target(Some((texture, layer_index as i32)));
+            let layer_count = texture.get_layer_count() as usize;
+            for layer in 0 .. layer_count {
+                self.device.bind_read_target(Some(TextureReadTarget { texture, layer }));
                 let x = fb_width - (spacing + size) * (target_index + 1);
                 let y = spacing;
 
                 let dest_rect = rect(x, y, size, size);
                 self.device.blit_render_target(src_rect, dest_rect);
                 target_index += 1;
             }
         }
@@ -4163,19 +4181,19 @@ impl Renderer {
                 0
             };
             let dimensions = texture.get_dimensions();
             let src_rect = DeviceIntRect::new(
                 DeviceIntPoint::zero(),
                 DeviceIntSize::new(dimensions.width as i32, dimensions.height as i32),
             );
 
-            let layer_count = texture.get_layer_count();
-            for layer_index in 0 .. layer_count {
-                self.device.bind_read_target(Some((texture, layer_index)));
+            let layer_count = texture.get_layer_count() as usize;
+            for layer in 0 .. layer_count {
+                self.device.bind_read_target(Some(TextureReadTarget { texture, layer}));
 
                 let x = fb_width - (spacing + size) * (i as i32 + 1);
 
                 // If we have more targets than fit on one row in screen, just early exit.
                 if x > fb_width {
                     return;
                 }
 
@@ -4272,17 +4290,17 @@ impl Renderer {
         pixels
     }
 
     pub fn read_gpu_cache(&mut self) -> (DeviceUintSize, Vec<u8>) {
         let texture = self.gpu_cache_texture.texture.as_ref().unwrap();
         let size = texture.get_dimensions();
         let mut texels = vec![0; (size.width * size.height * 16) as usize];
         self.device.begin_frame();
-        self.device.bind_read_target(Some((texture, 0)));
+        self.device.bind_read_target(Some(TextureReadTarget { texture, layer: 0 }));
         self.device.read_pixels_into(
             DeviceUintRect::new(DeviceUintPoint::zero(), size),
             ReadPixelsFormat::Standard(ImageFormat::RGBAF32),
             &mut texels,
         );
         self.device.bind_read_target(None);
         self.device.end_frame();
         (size, texels)
@@ -4357,16 +4375,19 @@ impl Renderer {
         report.vertex_data_textures += self.prim_header_f_texture.size_in_bytes();
         report.vertex_data_textures += self.prim_header_i_texture.size_in_bytes();
         report.vertex_data_textures += self.transforms_texture.size_in_bytes();
         report.vertex_data_textures += self.render_task_texture.size_in_bytes();
 
         // Texture cache and render target GPU memory.
         report += self.texture_resolver.report_memory();
 
+        // Textures held internally within the device layer.
+        report += self.device.report_memory();
+
         report
     }
 
     // Sets the blend mode. Blend is unconditionally set if the "show overdraw" debugging mode is
     // enabled.
     fn set_blend(&self, mut blend: bool, framebuffer_kind: FramebufferKind) {
         if framebuffer_kind == FramebufferKind::Main &&
                 self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
@@ -4626,17 +4647,16 @@ impl RendererStats {
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct PlainTexture {
     data: String,
     size: (u32, u32, i32),
     format: ImageFormat,
     filter: TextureFilter,
-    render_target: Option<RenderTargetInfo>,
 }
 
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct PlainRenderer {
     gpu_cache: PlainTexture,
@@ -4736,24 +4756,24 @@ impl Renderer {
                 .unwrap();
         }
 
         PlainTexture {
             data: short_path,
             size: (rect.size.width, rect.size.height, texture.get_layer_count()),
             format: texture.get_format(),
             filter: texture.get_filter(),
-            render_target: texture.get_render_target(),
         }
     }
 
     #[cfg(feature = "replay")]
     fn load_texture(
         target: TextureTarget,
         plain: &PlainTexture,
+        rt_info: Option<RenderTargetInfo>,
         root: &PathBuf,
         device: &mut Device
     ) -> (Texture, Vec<u8>)
     {
         use std::fs::File;
         use std::io::Read;
 
         let mut texels = Vec::new();
@@ -4763,17 +4783,17 @@ impl Renderer {
             .unwrap();
 
         let texture = device.create_texture(
             target,
             plain.format,
             plain.size.0,
             plain.size.1,
             plain.filter,
-            plain.render_target,
+            rt_info,
             plain.size.2,
         );
         device.upload_texture_immediate(&texture, &texels);
 
         (texture, texels)
     }
 
     #[cfg(feature = "capture")]
@@ -4934,29 +4954,31 @@ impl Renderer {
             for (_id, texture) in self.texture_resolver.texture_cache_map.drain() {
                 self.device.delete_texture(texture);
             }
             for (id, texture) in renderer.textures {
                 info!("\t{}", texture.data);
                 let t = Self::load_texture(
                     TextureTarget::Array,
                     &texture,
+                    Some(RenderTargetInfo { has_depth: false }),
                     &root,
                     &mut self.device
                 );
                 self.texture_resolver.texture_cache_map.insert(id, t.0);
             }
 
             info!("loading gpu cache");
             if let Some(t) = self.gpu_cache_texture.texture.take() {
                 self.device.delete_texture(t);
             }
             let (t, gpu_cache_data) = Self::load_texture(
                 TextureTarget::Default,
                 &renderer.gpu_cache,
+                Some(RenderTargetInfo { has_depth: false }),
                 &root,
                 &mut self.device,
             );
             self.gpu_cache_texture.texture = Some(t);
             match self.gpu_cache_texture.bus {
                 GpuCacheBus::PixelBuffer { ref mut rows, ref mut cpu_blocks, .. } => {
                     let dim = self.gpu_cache_texture.texture.as_ref().unwrap().get_dimensions();
                     let blocks = unsafe {
@@ -4991,21 +5013,21 @@ impl Renderer {
                     Entry::Vacant(e) => {
                         //TODO: provide a way to query both the layer count and the filter from external images
                         let (layer_count, filter) = (1, TextureFilter::Linear);
                         let plain_tex = PlainTexture {
                             data: e.key().clone(),
                             size: (descriptor.size.width, descriptor.size.height, layer_count),
                             format: descriptor.format,
                             filter,
-                            render_target: None,
                         };
                         let t = Self::load_texture(
                             target,
                             &plain_tex,
+                            None,
                             &root,
                             &mut self.device
                         );
                         let extex = t.0.into_external();
                         self.owned_external_images.insert(key, extex.clone());
                         e.insert(extex.internal_id()).clone()
                     }
                 };
--- a/gfx/webrender/src/spatial_node.rs
+++ b/gfx/webrender/src/spatial_node.rs
@@ -117,17 +117,16 @@ impl SpatialNode {
         source_perspective: Option<LayoutTransform>,
         origin_in_parent_reference_frame: LayoutVector2D,
         pipeline_id: PipelineId,
     ) -> Self {
         let identity = LayoutTransform::identity();
         let source_perspective = source_perspective.map_or_else(
             LayoutFastTransform::identity, |perspective| perspective.into());
         let info = ReferenceFrameInfo {
-            resolved_transform: LayoutFastTransform::identity(),
             source_transform: source_transform.unwrap_or(PropertyBinding::Value(identity)),
             source_perspective,
             origin_in_parent_reference_frame,
             invertible: true,
         };
         Self::new(pipeline_id, parent_index, SpatialNodeType:: ReferenceFrame(info))
     }
 
@@ -251,26 +250,26 @@ impl SpatialNode {
             SpatialNodeType::ReferenceFrame(ref mut info) => {
                 // Resolve the transform against any property bindings.
                 let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
                 // Do a change-basis operation on the perspective matrix using
                 // the scroll offset.
                 let scrolled_perspective = info.source_perspective
                     .pre_translate(&state.parent_accumulated_scroll_offset)
                     .post_translate(-state.parent_accumulated_scroll_offset);
-                info.resolved_transform =
+                let resolved_transform =
                     LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
                     .pre_mul(&source_transform.into())
                     .pre_mul(&scrolled_perspective);
 
                 // The transformation for this viewport in world coordinates is the transformation for
                 // our parent reference frame, plus any accumulated scrolling offsets from nodes
                 // between our reference frame and this node. Finally, we also include
                 // whatever local transformation this reference frame provides.
-                let relative_transform = info.resolved_transform
+                let relative_transform = resolved_transform
                     .post_translate(state.parent_accumulated_scroll_offset)
                     .to_transform()
                     .with_destination::<LayoutPixel>();
                 self.world_viewport_transform =
                     state.parent_reference_frame_transform.pre_mul(&relative_transform.into());
                 self.world_content_transform = self.world_viewport_transform;
 
                 info.invertible = self.world_viewport_transform.is_invertible();
@@ -617,20 +616,16 @@ impl ScrollFrameInfo {
             external_id: self.external_id,
         }
     }
 }
 
 /// Contains information about reference frames.
 #[derive(Copy, Clone, Debug)]
 pub struct ReferenceFrameInfo {
-    /// The transformation that establishes this reference frame, relative to the parent
-    /// reference frame. The origin of the reference frame is included in the transformation.
-    pub resolved_transform: LayoutFastTransform,
-
     /// The source transform and perspective matrices provided by the stacking context
     /// that forms this reference frame. We maintain the property binding information
     /// here so that we can resolve the animated transform and update the tree each
     /// frame.
     pub source_transform: PropertyBinding<LayoutTransform>,
     pub source_perspective: LayoutFastTransform,
 
     /// The original, not including the transform and relative to the parent reference frame,
--- a/gfx/webrender/src/texture_cache.rs
+++ b/gfx/webrender/src/texture_cache.rs
@@ -4,17 +4,17 @@
 
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize};
 use api::{ExternalImageType, ImageData, ImageFormat};
 use api::ImageDescriptor;
 use device::TextureFilter;
 use freelist::{FreeList, FreeListHandle, UpsertResult, WeakFreeListHandle};
 use gpu_cache::{GpuCache, GpuCacheHandle};
 use gpu_types::{ImageSource, UvRectKind};
-use internal_types::{CacheTextureId, TextureUpdateList, TextureUpdateSource};
+use internal_types::{CacheTextureId, LayerIndex, TextureUpdateList, TextureUpdateSource};
 use internal_types::{RenderTargetInfo, TextureSource, TextureUpdate, TextureUpdateOp};
 use profiler::{ResourceProfileCounter, TextureCacheProfileCounters};
 use render_backend::FrameId;
 use resource_cache::CacheItem;
 use std::cell::Cell;
 use std::cmp;
 use std::mem;
 use std::rc::Rc;
@@ -536,22 +536,24 @@ impl TextureCache {
                     uv_rect: DeviceUintRect::new(origin, entry.size),
                     texture_layer: layer_index as i32,
                 }
             }
             None => panic!("BUG: handle not requested earlier in frame"),
         }
     }
 
-    // A more detailed version of get(). This allows access to the actual
-    // device rect of the cache allocation.
+    /// A more detailed version of get(). This allows access to the actual
+    /// device rect of the cache allocation.
+    ///
+    /// Returns a tuple identifying the texture, the layer, and the region.
     pub fn get_cache_location(
         &self,
         handle: &TextureCacheHandle,
-    ) -> (CacheTextureId, i32, DeviceUintRect) {
+    ) -> (CacheTextureId, LayerIndex, DeviceUintRect) {
         let handle = handle
             .entry
             .as_ref()
             .expect("BUG: handle not requested earlier in frame");
 
         let entry = self.entries
             .get_opt(handle)
             .expect("BUG: was dropped from cache or not updated!");
@@ -562,17 +564,17 @@ impl TextureCache {
             }
             EntryKind::Cache {
                 layer_index,
                 origin,
                 ..
             } => (layer_index, origin),
         };
         (entry.texture_id,
-         layer_index as i32,
+         layer_index as usize,
          DeviceUintRect::new(origin, entry.size))
     }
 
     pub fn mark_unused(&mut self, handle: &TextureCacheHandle) {
         if let Some(ref handle) = handle.entry {
             if let Some(entry) = self.entries.get_opt_mut(handle) {
                 // Set a very low last accessed frame to make it very likely that this entry
                 // will get cleaned up next time we try to expire entries.
--- a/gfx/webrender/src/tiling.rs
+++ b/gfx/webrender/src/tiling.rs
@@ -280,20 +280,18 @@ impl<T: RenderTarget> RenderTargetList<T
 
     pub fn needs_depth(&self) -> bool {
         self.targets.iter().any(|target| target.needs_depth())
     }
 
     pub fn check_ready(&self, t: &Texture) {
         assert_eq!(t.get_dimensions(), self.max_size);
         assert_eq!(t.get_format(), self.format);
-        assert_eq!(t.get_render_target_layer_count(), self.targets.len());
         assert_eq!(t.get_layer_count() as usize, self.targets.len());
-        assert_eq!(t.has_depth(), t.get_rt_info().unwrap().has_depth);
-        assert_eq!(t.has_depth(), self.needs_depth());
+        assert!(t.supports_depth() >= self.needs_depth());
     }
 }
 
 /// Frame output information for a given pipeline ID.
 /// Storing the task ID allows the renderer to find
 /// the target rect within the render target that this
 /// pipeline exists at.
 #[cfg_attr(feature = "capture", derive(Serialize))]
@@ -816,17 +814,17 @@ impl TextureCacheRenderTarget {
 pub enum RenderPassKind {
     /// The final pass to the main frame buffer, where we have a single color
     /// target for display to the user.
     MainFramebuffer(ColorRenderTarget),
     /// An intermediate pass, where we may have multiple targets.
     OffScreen {
         alpha: RenderTargetList<AlphaRenderTarget>,
         color: RenderTargetList<ColorRenderTarget>,
-        texture_cache: FastHashMap<(CacheTextureId, i32), TextureCacheRenderTarget>,
+        texture_cache: FastHashMap<(CacheTextureId, usize), TextureCacheRenderTarget>,
     },
 }
 
 /// A render pass represents a set of rendering operations that don't depend on one
 /// another.
 ///
 /// A render pass can have several render targets if there wasn't enough space in one
 /// target to do all of the rendering for that pass. See `RenderTargetList`.
@@ -945,18 +943,18 @@ impl RenderPass {
                 for &task_id in &self.tasks {
                     let (target_kind, texture_target) = {
                         let task = &mut render_tasks[task_id];
                         let target_kind = task.target_kind();
 
                         // Find a target to assign this task to, or create a new
                         // one if required.
                         let texture_target = match task.location {
-                            RenderTaskLocation::TextureCache(texture_id, layer, _) => {
-                                Some((texture_id, layer))
+                            RenderTaskLocation::TextureCache { texture, layer, .. } => {
+                                Some((texture, layer))
                             }
                             RenderTaskLocation::Fixed(..) => {
                                 None
                             }
                             RenderTaskLocation::Dynamic(ref mut origin, size) => {
                                 let alloc_size = DeviceUintSize::new(size.width as u32, size.height as u32);
                                 let (alloc_origin, target_index) =  match target_kind {
                                     RenderTargetKind::Color => color.allocate(alloc_size),
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -227,17 +227,17 @@ impl Transaction {
         self.frame_ops.push(FrameMsg::ScrollNodeWithId(origin, id, clamp));
     }
 
     pub fn set_page_zoom(&mut self, page_zoom: ZoomFactor) {
         self.scene_ops.push(SceneMsg::SetPageZoom(page_zoom));
     }
 
     pub fn set_pinch_zoom(&mut self, pinch_zoom: ZoomFactor) {
-        self.scene_ops.push(SceneMsg::SetPinchZoom(pinch_zoom));
+        self.frame_ops.push(FrameMsg::SetPinchZoom(pinch_zoom));
     }
 
     pub fn set_pan(&mut self, pan: DeviceIntPoint) {
         self.frame_ops.push(FrameMsg::SetPan(pan));
     }
 
     /// Generate a new frame. When it's done and a RenderNotifier has been set
     /// in `webrender::Renderer`, [new_frame_ready()][notifier] gets called.
@@ -518,17 +518,16 @@ pub struct AddFontInstance {
     pub variations: Vec<FontVariation>,
 }
 
 // Frame messages affect building the scene.
 #[derive(Clone, Deserialize, Serialize)]
 pub enum SceneMsg {
     UpdateEpoch(PipelineId, Epoch),
     SetPageZoom(ZoomFactor),
-    SetPinchZoom(ZoomFactor),
     SetRootPipeline(PipelineId),
     RemovePipeline(PipelineId),
     SetDisplayList {
         list_descriptor: BuiltDisplayListDescriptor,
         epoch: Epoch,
         pipeline_id: PipelineId,
         background: Option<ColorF>,
         viewport_size: LayoutSize,
@@ -549,25 +548,25 @@ pub enum FrameMsg {
     HitTest(Option<PipelineId>, WorldPoint, HitTestFlags, MsgSender<HitTestResult>),
     SetPan(DeviceIntPoint),
     EnableFrameOutput(PipelineId, bool),
     Scroll(ScrollLocation, WorldPoint),
     ScrollNodeWithId(LayoutPoint, ExternalScrollId, ScrollClamping),
     GetScrollNodeState(MsgSender<Vec<ScrollNodeState>>),
     UpdateDynamicProperties(DynamicProperties),
     AppendDynamicProperties(DynamicProperties),
+    SetPinchZoom(ZoomFactor),
 }
 
 impl fmt::Debug for SceneMsg {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         f.write_str(match *self {
             SceneMsg::UpdateEpoch(..) => "SceneMsg::UpdateEpoch",
             SceneMsg::SetDisplayList { .. } => "SceneMsg::SetDisplayList",
             SceneMsg::SetPageZoom(..) => "SceneMsg::SetPageZoom",
-            SceneMsg::SetPinchZoom(..) => "SceneMsg::SetPinchZoom",
             SceneMsg::RemovePipeline(..) => "SceneMsg::RemovePipeline",
             SceneMsg::SetWindowParameters { .. } => "SceneMsg::SetWindowParameters",
             SceneMsg::SetRootPipeline(..) => "SceneMsg::SetRootPipeline",
         })
     }
 }
 
 impl fmt::Debug for FrameMsg {
@@ -577,16 +576,17 @@ impl fmt::Debug for FrameMsg {
             FrameMsg::HitTest(..) => "FrameMsg::HitTest",
             FrameMsg::SetPan(..) => "FrameMsg::SetPan",
             FrameMsg::Scroll(..) => "FrameMsg::Scroll",
             FrameMsg::ScrollNodeWithId(..) => "FrameMsg::ScrollNodeWithId",
             FrameMsg::GetScrollNodeState(..) => "FrameMsg::GetScrollNodeState",
             FrameMsg::EnableFrameOutput(..) => "FrameMsg::EnableFrameOutput",
             FrameMsg::UpdateDynamicProperties(..) => "FrameMsg::UpdateDynamicProperties",
             FrameMsg::AppendDynamicProperties(..) => "FrameMsg::AppendDynamicProperties",
+            FrameMsg::SetPinchZoom(..) => "FrameMsg::SetPinchZoom",
         })
     }
 }
 
 bitflags!{
     /// Bit flags for WR stages to store in a capture.
     // Note: capturing `FRAME` without `SCENE` is not currently supported.
     #[derive(Deserialize, Serialize)]
@@ -786,16 +786,17 @@ pub struct MemoryReport {
     pub rasterized_blobs: usize,
     //
     // GPU memory.
     //
     pub gpu_cache_textures: usize,
     pub vertex_data_textures: usize,
     pub render_target_textures: usize,
     pub texture_cache_textures: usize,
+    pub depth_target_textures: usize,
 }
 
 impl ::std::ops::AddAssign for MemoryReport {
     fn add_assign(&mut self, other: MemoryReport) {
         self.primitive_stores += other.primitive_stores;
         self.clip_stores += other.clip_stores;
         self.gpu_cache_metadata += other.gpu_cache_metadata;
         self.gpu_cache_cpu_mirror += other.gpu_cache_cpu_mirror;
@@ -803,16 +804,17 @@ impl ::std::ops::AddAssign for MemoryRep
         self.hit_testers += other.hit_testers;
         self.fonts += other.fonts;
         self.images += other.images;
         self.rasterized_blobs += other.rasterized_blobs;
         self.gpu_cache_textures += other.gpu_cache_textures;
         self.vertex_data_textures += other.vertex_data_textures;
         self.render_target_textures += other.render_target_textures;
         self.texture_cache_textures += other.texture_cache_textures;
+        self.depth_target_textures += other.depth_target_textures;
     }
 }
 
 /// A C function that takes a pointer to a heap allocation and returns its size.
 ///
 /// This is borrowed from the malloc_size_of crate, upon which we want to avoid
 /// a dependency from WebRender.
 pub type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize;
--- a/gfx/webrender_api/src/display_item.rs
+++ b/gfx/webrender_api/src/display_item.rs
@@ -430,16 +430,39 @@ pub enum BorderStyle {
     Inset = 8,
     Outset = 9,
 }
 
 impl BorderStyle {
     pub fn is_hidden(&self) -> bool {
         *self == BorderStyle::Hidden || *self == BorderStyle::None
     }
+
+    /// Returns true if the border style itself is opaque. Other
+    /// factors (such as color, or border radii) may mean that
+    /// the border segment isn't opaque regardless of this.
+    pub fn is_opaque(&self) -> bool {
+        match *self {
+            BorderStyle::None |
+            BorderStyle::Double |
+            BorderStyle::Dotted |
+            BorderStyle::Dashed |
+            BorderStyle::Hidden => {
+                false
+            }
+
+            BorderStyle::Solid |
+            BorderStyle::Groove |
+            BorderStyle::Ridge |
+            BorderStyle::Inset |
+            BorderStyle::Outset => {
+                true
+            }
+        }
+    }
 }
 
 #[repr(u32)]
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
 pub enum BoxShadowClipMode {
     Outset = 0,
     Inset = 1,
 }
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-98d507003c07c003ef0e0297dc4d29ee896a5868
+74f265e447d2927c27d4320c676779956d39eaf0
--- a/gfx/webrender_bindings/webrender_ffi_generated.h
+++ b/gfx/webrender_bindings/webrender_ffi_generated.h
@@ -537,31 +537,33 @@ struct MemoryReport {
   uintptr_t hit_testers;
   uintptr_t fonts;
   uintptr_t images;
   uintptr_t rasterized_blobs;
   uintptr_t gpu_cache_textures;
   uintptr_t vertex_data_textures;
   uintptr_t render_target_textures;
   uintptr_t texture_cache_textures;
+  uintptr_t depth_target_textures;
 
   bool operator==(const MemoryReport& aOther) const {
     return primitive_stores == aOther.primitive_stores &&
            clip_stores == aOther.clip_stores &&
            gpu_cache_metadata == aOther.gpu_cache_metadata &&
            gpu_cache_cpu_mirror == aOther.gpu_cache_cpu_mirror &&
            render_tasks == aOther.render_tasks &&
            hit_testers == aOther.hit_testers &&
            fonts == aOther.fonts &&
            images == aOther.images &&
            rasterized_blobs == aOther.rasterized_blobs &&
            gpu_cache_textures == aOther.gpu_cache_textures &&
            vertex_data_textures == aOther.vertex_data_textures &&
            render_target_textures == aOther.render_target_textures &&
-           texture_cache_textures == aOther.texture_cache_textures;
+           texture_cache_textures == aOther.texture_cache_textures &&
+           depth_target_textures == aOther.depth_target_textures;
   }
 };
 
 template<typename T, typename U>
 struct TypedSize2D {
   T width;
   T height;
 
--- a/ipc/app/plugin-container.exe.manifest
+++ b/ipc/app/plugin-container.exe.manifest
@@ -14,16 +14,26 @@
                         name="Microsoft.Windows.Common-Controls"
                         version="6.0.0.0"
                         processorArchitecture="*"
                         publicKeyToken="6595b64144ccf1df"
                         language="*"
                 />
         </dependentAssembly>
 </dependency>
+<dependency>
+        <dependentAssembly>
+                <assemblyIdentity
+                        type="win32"
+                        name="mozglue"
+                        version="1.0.0.0"
+                        language="*"
+                />
+        </dependentAssembly>
+</dependency>
   <ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
     <ms_asmv3:security>
       <ms_asmv3:requestedPrivileges>
         <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
       </ms_asmv3:requestedPrivileges>
     </ms_asmv3:security>
   </ms_asmv3:trustInfo>
   <ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
--- a/ipc/glue/BackgroundChild.h
+++ b/ipc/glue/BackgroundChild.h
@@ -2,17 +2,16 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_ipc_backgroundchild_h__
 #define mozilla_ipc_backgroundchild_h__
 
-#include "base/process.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/ipc/Transport.h"
 
 namespace mozilla {
 namespace dom {
 
 class BlobImpl;
 class ContentChild;
@@ -42,17 +41,16 @@ class PBackgroundChild;
 //
 // The PBackgroundChild actor and all its sub-protocol actors will be
 // automatically destroyed when its designated thread completes.
 class BackgroundChild final
 {
   friend class mozilla::dom::ContentChild;
   friend class mozilla::dom::ContentParent;
 
-  typedef base::ProcessId ProcessId;
   typedef mozilla::ipc::Transport Transport;
 
 public:
   // See above.
   static PBackgroundChild*
   GetForCurrentThread();
 
   // See above.
--- a/js/src/builtin/Array.cpp
+++ b/js/src/builtin/Array.cpp
@@ -407,19 +407,16 @@ js::GetElementsWithAdder(JSContext* cx, 
         if (!adder->append(cx, val)) {
             return false;
         }
     }
 
     return true;
 }
 
-static bool
-ObjectMayHaveExtraIndexedProperties(JSObject* obj);
-
 static inline bool
 IsPackedArrayOrNoExtraIndexedProperties(JSObject* obj, uint64_t length)
 {
     return (IsPackedArray(obj) && obj->as<ArrayObject>().length() == length) ||
            !ObjectMayHaveExtraIndexedProperties(obj);
 }
 
 static bool
@@ -1045,18 +1042,18 @@ ObjectMayHaveExtraIndexedOwnProperties(J
                              obj->getClass(), INT_TO_JSID(0), obj);
 }
 
 /*
  * Whether obj may have indexed properties anywhere besides its dense
  * elements. This includes other indexed properties in its shape hierarchy, and
  * indexed properties or elements along its prototype chain.
  */
-static bool
-ObjectMayHaveExtraIndexedProperties(JSObject* obj)
+bool
+js::ObjectMayHaveExtraIndexedProperties(JSObject* obj)
 {
     MOZ_ASSERT_IF(obj->hasDynamicPrototype(), !obj->isNative());
 
     if (ObjectMayHaveExtraIndexedOwnProperties(obj)) {
         return true;
     }
 
     do {
--- a/js/src/builtin/Array.h
+++ b/js/src/builtin/Array.h
@@ -191,16 +191,19 @@ ArrayConstructor(JSContext* cx, unsigned
 
 // Like Array constructor, but doesn't perform GetPrototypeFromConstructor.
 extern bool
 array_construct(JSContext* cx, unsigned argc, Value* vp);
 
 extern bool
 IsCrossRealmArrayConstructor(JSContext* cx, const Value& v, bool* result);
 
+extern bool
+ObjectMayHaveExtraIndexedProperties(JSObject* obj);
+
 class MOZ_NON_TEMPORARY_CLASS ArraySpeciesLookup final
 {
     /*
      * An ArraySpeciesLookup holds the following:
      *
      *  Array.prototype (arrayProto_)
      *      To ensure that the incoming array has the standard proto.
      *
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/cacheir/bug1494537.js
@@ -0,0 +1,125 @@
+setJitCompilerOption("ion.forceinlineCaches", 1);
+
+let offsets = [213, 559, 255, 515, 30, 507, 252, 329, 487, 7];
+
+function update_index(i, j) {
+    var offset = offsets[j % offsets.length];
+    return i + offset;
+}
+
+function compute_index(initial, count) {
+    for (var i = 0; i < count; i++) {
+        initial = update_index(initial, i);
+    }
+    return initial;
+}
+
+// This is written so that the IC added in the bug activates.
+function mutate_array(array, count, epsilon = 0) {
+    var index = 0;
+    for (var i = 0; i < count; i++) {
+        index = update_index(index, i);
+        array[index] = i + epsilon;
+    }
+    return array[offsets[0]+offsets[1]] === (1 + epsilon) &&
+           array[10] === undefined;
+}
+
+// Monomorphizing mutate_array to ensure we get the IC chains we want
+function create_variant(variant) {
+    var source = mutate_array.toString().replace("mutate_array", "mutate_array_"+variant);
+    return source;
+}
+
+function test_basic() {
+    eval(create_variant("basic"));
+    var x = [];
+
+    var count = 100;
+    assertEq(mutate_array_basic(x, count), true);
+    var end = compute_index(0, count);
+    assertEq(x[end], count - 1);
+    assertEq(x[end - 1], undefined);
+}
+
+// Ensure the IC respects frozen.
+function test_frozen() {
+    eval(create_variant("frozen"));
+    var x = [];
+    Object.freeze(x);
+
+    var count = 100;
+    assertEq(mutate_array_frozen(x, count), false);
+    assertEq(x.length, 0);
+
+    var end = compute_index(0, count);
+
+    var y = [];
+    assertEq(mutate_array_frozen(y, count), true);
+    assertEq(y[end], count - 1);
+    Object.freeze(y);
+
+    // After a mutated array is frozen, can't subsequently modify elements
+    assertEq(mutate_array_frozen(x, count, 10), false);
+    assertEq(y[end], count - 1);
+}
+
+// Let's make sure updates to the array happen as expected.
+function test_update() {
+    eval(create_variant("update"));
+
+    var x = [];
+    var count = 100;
+    assertEq(mutate_array_update(x, count), true);
+    var end = compute_index(0, count);
+    assertEq(x[end], count - 1);
+    assertEq(x[end - 1], undefined);
+
+    var epsilon = 2;
+    mutate_array_update(x, 200, epsilon);
+    assertEq(x[end], count -1 + epsilon)
+}
+
+// Elements may be non-writable, let us not write them.
+function test_nonwritable() {
+    eval(create_variant("nonwritable"));
+    var x = [];
+    var count = 100;
+    var index = compute_index(0, 10);
+    Object.defineProperty(x, index, {value: -10, writable: false});
+    mutate_array_nonwritable(x, count);
+    assertEq(x[index], -10);
+}
+
+// Random indices can get setters, let's make sure we honour those.
+function test_setter() {
+    eval(create_variant("setter"));
+    var x = [];
+    var count = 100;
+    var index = compute_index(0, 80);
+    var sigil = 0;
+    Object.defineProperty(x, index, {set(newVal) {sigil++; }});
+    mutate_array_setter(x, count);
+    assertEq(sigil, 1);
+    assertEq(x[index], undefined);
+}
+
+// Ensure indexes on the prototype don't break things;
+//
+function test_proto_indices() {
+    eval(create_variant("proto_indices"));
+    var x = [];
+    var count = 100;
+    var index = compute_index(0, 80);
+    x.__proto__[index] = "hello";
+    mutate_array_proto_indices(x, count);
+    assertEq(x.__proto__[index], "hello");
+    assertEq(x[index], 79);
+}
+
+test_basic();
+test_frozen();
+test_update();
+test_nonwritable();
+test_setter();
+test_proto_indices();
--- a/js/src/jit/BaselineCacheIRCompiler.cpp
+++ b/js/src/jit/BaselineCacheIRCompiler.cpp
@@ -1875,16 +1875,43 @@ BaselineCacheIRCompiler::emitCallProxySe
         return false;
     }
 
     stubFrame.leave(masm);
     return true;
 }
 
 bool
+BaselineCacheIRCompiler::emitCallAddOrUpdateSparseElementHelper()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register id = allocator.useRegister(masm, reader.int32OperandId());
+    ValueOperand val = allocator.useValueRegister(masm, reader.valOperandId());
+    bool strict = reader.readBool();
+    AutoScratchRegister scratch(allocator, masm);
+
+    allocator.discardStack(masm);
+
+    AutoStubFrame stubFrame(*this);
+    stubFrame.enter(masm, scratch);
+
+    masm.Push(Imm32(strict));
+    masm.Push(val);
+    masm.Push(id);
+    masm.Push(obj);
+
+    if (!callVM(masm, AddOrUpdateSparseElementHelperInfo)) {
+        return false;
+    }
+    stubFrame.leave(masm);
+    return true;
+}
+
+
+bool
 BaselineCacheIRCompiler::emitMegamorphicSetElement()
 {
     Register obj = allocator.useRegister(masm, reader.objOperandId());
     ValueOperand idVal = allocator.useValueRegister(masm, reader.valOperandId());
     ValueOperand val = allocator.useValueRegister(masm, reader.valOperandId());
     bool strict = reader.readBool();
 
     allocator.discardStack(masm);
--- a/js/src/jit/CacheIR.cpp
+++ b/js/src/jit/CacheIR.cpp
@@ -3445,16 +3445,19 @@ SetPropIRGenerator::tryAttachStub()
                 return true;
             }
             if (tryAttachSetDenseElementHole(obj, objId, index, indexId, rhsValId)) {
                 return true;
             }
             if (tryAttachSetTypedElement(obj, objId, index, indexId, rhsValId)) {
                 return true;
             }
+            if (tryAttachAddOrUpdateSparseElement(obj, objId, index, indexId, rhsValId)) {
+                return true;
+            }
             return false;
         }
         return false;
     }
     return false;
 }
 
 static void
@@ -4061,16 +4064,99 @@ SetPropIRGenerator::tryAttachSetDenseEle
 
     // Type inference uses JSID_VOID for the element types.
     typeCheckInfo_.set(nobj->group(), JSID_VOID);
 
     trackAttached(isAdd ? "AddDenseElement" : "StoreDenseElementHole");
     return true;
 }
 
+// Add an IC for adding or updating a sparse array element.
+bool
+SetPropIRGenerator::tryAttachAddOrUpdateSparseElement(HandleObject obj, ObjOperandId objId,
+                                                      uint32_t index, Int32OperandId indexId,
+                                                      ValOperandId rhsId)
+{
+    JSOp op = JSOp(*pc_);
+    MOZ_ASSERT(IsPropertySetOp(op) || IsPropertyInitOp(op));
+
+    if (op != JSOP_SETELEM && op != JSOP_STRICTSETELEM) {
+        return false;
+    }
+
+    if (!obj->isNative()) {
+        return false;
+    }
+    RootedNativeObject nobj(cx_, &obj->as<NativeObject>());
+
+    // We cannot attach a stub to a non-extensible object
+    if (!nobj->isExtensible()) {
+        return false;
+    }
+
+    // Stub doesn't handle negative indices.
+    if (index > INT_MAX) {
+        return false;
+    }
+
+    // We also need to be past the end of the dense capacity, to ensure sparse.
+    if (index < nobj->getDenseInitializedLength()) {
+        return false;
+    }
+
+    // Only handle Array objects in this stub.
+    if (!nobj->is<ArrayObject>()) {
+        return false;
+    }
+    RootedArrayObject aobj(cx_, &obj->as<ArrayObject>());
+
+    // Don't attach if we're adding to an array with non-writable length.
+    bool isAdd = (index >= aobj->length());
+    if (isAdd && !aobj->lengthIsWritable()) {
+        return false;
+    }
+
+    // Indexed properties on the prototype chain aren't handled by the helper.
+    if (ObjectMayHaveExtraIndexedProperties(aobj->staticPrototype())) {
+        return false;
+    }
+
+    // Ensure we are still talking about an array class.
+    writer.guardClass(objId, GuardClassKind::Array);
+
+    // The helper we are going to call only applies to non-dense elements.
+    writer.guardIndexGreaterThanDenseInitLength(objId, indexId);
+
+    // Guard extensible: We may be trying to add a new element, and so we'd best
+    // be able to do so safely.
+    writer.guardIsExtensible(objId);
+
+    // Ensures we are able to efficiently able to map to an integral jsid.
+    writer.guardIndexIsNonNegative(indexId);
+
+    // Shape guard the prototype chain to avoid shadowing indexes from appearing.
+    // Dense elements may appear on the prototype chain (and prototypes may
+    // have a different notion of which elements are dense), but they can
+    // only be data properties, so our specialized Set handler is ok to bind
+    // to them.
+    ShapeGuardProtoChain(writer, obj, objId);
+
+    // Ensure that if we're adding an element to the object, the object's
+    // length is writable.
+    writer.guardIndexIsValidUpdateOrAdd(objId, indexId);
+
+    writer.callAddOrUpdateSparseElementHelper(objId, indexId, rhsId,
+                                              /* strict = */op == JSOP_STRICTSETELEM);
+    writer.returnFromIC();
+
+    trackAttached("AddOrUpdateSparseElement");
+    return true;
+}
+
+
 bool
 SetPropIRGenerator::tryAttachSetTypedElement(HandleObject obj, ObjOperandId objId,
                                              uint32_t index, Int32OperandId indexId,
                                              ValOperandId rhsId)
 {
     if (!obj->is<TypedArrayObject>() && !IsPrimitiveArrayTypedObject(obj)) {
         return false;
     }
--- a/js/src/jit/CacheIR.h
+++ b/js/src/jit/CacheIR.h
@@ -196,16 +196,17 @@ extern const char* const CacheKindNames[
     _(GuardIsInt32Index)                  \
     _(GuardType)                          \
     _(GuardShape)                         \
     _(GuardGroup)                         \
     _(GuardProto)                         \
     _(GuardClass)                         /* Guard an object class, per GuardClassKind */ \
     _(GuardAnyClass)                      /* Guard an arbitrary class for an object */ \
     _(GuardCompartment)                   \
+    _(GuardIsExtensible)                  \
     _(GuardIsNativeFunction)              \
     _(GuardIsNativeObject)                \
     _(GuardIsProxy)                       \
     _(GuardHasProxyHandler)               \
     _(GuardNotDOMProxy)                   \
     _(GuardSpecificObject)                \
     _(GuardSpecificAtom)                  \
     _(GuardSpecificSymbol)                \
@@ -217,16 +218,19 @@ extern const char* const CacheKindNames[
     _(GuardNoUnboxedExpando)              \
     _(GuardAndLoadUnboxedExpando)         \
     _(GuardAndGetIndexFromString)         \
     _(GuardAndGetNumberFromString)        \
     _(GuardAndGetIterator)                \
     _(GuardHasGetterSetter)               \
     _(GuardGroupHasUnanalyzedNewScript)   \
     _(GuardIndexIsNonNegative)            \
+    _(GuardIndexGreaterThanDenseCapacity) \
+    _(GuardIndexGreaterThanArrayLength)   \
+    _(GuardIndexIsValidUpdateOrAdd)       \
     _(GuardIndexGreaterThanDenseInitLength) \
     _(GuardTagNotEqual)                   \
     _(GuardXrayExpandoShapeAndDefaultProto) \
     _(GuardFunctionPrototype)             \
     _(GuardNoAllocationMetadataBuilder)   \
     _(GuardObjectGroupNotPretenured)      \
     _(LoadStackValue)                     \
     _(LoadObject)                         \
@@ -262,16 +266,17 @@ extern const char* const CacheKindNames[
     _(ArrayPush)                          \
     _(ArrayJoinResult)                    \
     _(StoreTypedElement)                  \
     _(CallNativeSetter)                   \
     _(CallScriptedSetter)                 \
     _(CallSetArrayLength)                 \
     _(CallProxySet)                       \
     _(CallProxySetByValue)                \
+    _(CallAddOrUpdateSparseElementHelper) \
     _(CallInt32ToString)                  \
     _(CallNumberToString)                 \
                                           \
     /* The *Result ops load a value into the cache's result register. */ \
     _(LoadFixedSlotResult)                \
     _(LoadDynamicSlotResult)              \
     _(LoadUnboxedPropertyResult)          \
     _(LoadTypedObjectResult)              \
@@ -760,16 +765,19 @@ class MOZ_RAII CacheIRWriter : public JS
     void guardCompartment(ObjOperandId obj, JSObject* global, JS::Compartment* compartment) {
         assertSameCompartment(global);
         writeOpWithOperandId(CacheOp::GuardCompartment, obj);
         // Add a reference to a global in the compartment to keep it alive.
         addStubField(uintptr_t(global), StubField::Type::JSObject);
         // Use RawWord, because compartments never move and it can't be GCed.
         addStubField(uintptr_t(compartment), StubField::Type::RawWord);
     }
+    void guardIsExtensible(ObjOperandId obj) {
+        writeOpWithOperandId(CacheOp::GuardIsExtensible, obj);
+    }
     void guardNoDetachedTypedObjects() {
         writeOp(CacheOp::GuardNoDetachedTypedObjects);
     }
     void guardFrameHasNoArgumentsObject() {
         writeOp(CacheOp::GuardFrameHasNoArgumentsObject);
     }
 
     Int32OperandId guardAndGetIndexFromString(StringOperandId str) {
@@ -806,16 +814,28 @@ class MOZ_RAII CacheIRWriter : public JS
 
     void guardIndexIsNonNegative(Int32OperandId index) {
         writeOpWithOperandId(CacheOp::GuardIndexIsNonNegative, index);
     }
     void guardIndexGreaterThanDenseInitLength(ObjOperandId obj, Int32OperandId index) {
         writeOpWithOperandId(CacheOp::GuardIndexGreaterThanDenseInitLength, obj);
         writeOperandId(index);
     }
+    void guardIndexGreaterThanDenseCapacity(ObjOperandId obj, Int32OperandId index) {
+        writeOpWithOperandId(CacheOp::GuardIndexGreaterThanDenseCapacity, obj);
+        writeOperandId(index);
+    }
+    void guardIndexGreaterThanArrayLength(ObjOperandId obj, Int32OperandId index) {
+        writeOpWithOperandId(CacheOp::GuardIndexGreaterThanArrayLength, obj);
+        writeOperandId(index);
+    }
+    void guardIndexIsValidUpdateOrAdd(ObjOperandId obj, Int32OperandId index) {
+        writeOpWithOperandId(CacheOp::GuardIndexIsValidUpdateOrAdd, obj);
+        writeOperandId(index);
+    }
     void guardTagNotEqual(ValueTagOperandId lhs, ValueTagOperandId rhs) {
         writeOpWithOperandId(CacheOp::GuardTagNotEqual, lhs);
         writeOperandId(rhs);
     }
 
     void loadFrameCalleeResult() {
         writeOp(CacheOp::LoadFrameCalleeResult);
     }
@@ -1036,16 +1056,22 @@ class MOZ_RAII CacheIRWriter : public JS
         buffer_.writeByte(uint32_t(strict));
     }
     void callProxySetByValue(ObjOperandId obj, ValOperandId id, ValOperandId rhs, bool strict) {
         writeOpWithOperandId(CacheOp::CallProxySetByValue, obj);
         writeOperandId(id);
         writeOperandId(rhs);
         buffer_.writeByte(uint32_t(strict));
     }
+    void callAddOrUpdateSparseElementHelper(ObjOperandId obj, Int32OperandId id, ValOperandId rhs, bool strict) {
+        writeOpWithOperandId(CacheOp::CallAddOrUpdateSparseElementHelper, obj);
+        writeOperandId(id);
+        writeOperandId(rhs);
+        buffer_.writeByte(uint32_t(strict));
+    }
     StringOperandId callInt32ToString(Int32OperandId id) {
         StringOperandId res(nextOperandId_++);
         writeOpWithOperandId(CacheOp::CallInt32ToString, id);
         writeOperandId(res);
         return res;
     }
     StringOperandId callNumberToString(ValOperandId id) {
         StringOperandId res(nextOperandId_++);
@@ -1748,16 +1774,20 @@ class MOZ_RAII SetPropIRGenerator : publ
     bool tryAttachSetDenseElement(HandleObject obj, ObjOperandId objId, uint32_t index,
                                   Int32OperandId indexId, ValOperandId rhsId);
     bool tryAttachSetTypedElement(HandleObject obj, ObjOperandId objId, uint32_t index,
                                   Int32OperandId indexId, ValOperandId rhsId);
 
     bool tryAttachSetDenseElementHole(HandleObject obj, ObjOperandId objId, uint32_t index,
                                       Int32OperandId indexId, ValOperandId rhsId);
 
+    bool tryAttachAddOrUpdateSparseElement(HandleObject obj, ObjOperandId objId, uint32_t index,
+                                           Int32OperandId indexId, ValOperandId rhsId);
+
+
     bool tryAttachGenericProxy(HandleObject obj, ObjOperandId objId, HandleId id,
                                ValOperandId rhsId, bool handleDOMProxies);
     bool tryAttachDOMProxyShadowed(HandleObject obj, ObjOperandId objId, HandleId id,
                                    ValOperandId rhsId);
     bool tryAttachDOMProxyUnshadowed(HandleObject obj, ObjOperandId objId, HandleId id,
                                      ValOperandId rhsId);
     bool tryAttachDOMProxyExpando(HandleObject obj, ObjOperandId objId, HandleId id,
                                   ValOperandId rhsId);
--- a/js/src/jit/CacheIRCompiler.cpp
+++ b/js/src/jit/CacheIRCompiler.cpp
@@ -1730,16 +1730,45 @@ CacheIRCompiler::emitGuardClass()
         masm.branchTestObjClassNoSpectreMitigations(Assembler::NotEqual, obj, clasp, scratch,
                                                     failure->label());
     }
 
     return true;
 }
 
 bool
+CacheIRCompiler::emitGuardIsExtensible()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    AutoScratchRegister scratch(allocator, masm);
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure)) {
+        return false;
+    }
+
+    Address shape(obj, ShapedObject::offsetOfShape());
+    masm.loadPtr(shape, scratch);
+
+    Address baseShape(scratch, Shape::offsetOfBaseShape());
+    masm.loadPtr(baseShape, scratch);
+
+    Address baseShapeFlags(scratch, BaseShape::offsetOfFlags());
+    masm.loadPtr(baseShapeFlags, scratch);
+
+    masm.and32(Imm32(js::BaseShape::NOT_EXTENSIBLE), scratch);
+
+    // Spectre-style checks are not needed here because we do not
+    // interpret data based on this check.
+    masm.branch32(Assembler::Equal, scratch, Imm32(js::BaseShape::NOT_EXTENSIBLE),
+                  failure->label());
+    return true;
+}
+
+bool
 CacheIRCompiler::emitGuardIsNativeFunction()
 {
     Register obj = allocator.useRegister(masm, reader.objOperandId());
     JSNative nativeFunc = reinterpret_cast<JSNative>(reader.pointer());
     AutoScratchRegister scratch(allocator, masm);
 
     FailurePath* failure;
     if (!addFailurePath(&failure)) {
@@ -2831,27 +2860,110 @@ CacheIRCompiler::emitGuardIndexGreaterTh
     FailurePath* failure;
     if (!addFailurePath(&failure)) {
         return false;
     }
 
     // Load obj->elements.
     masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
 
-    // Ensure index >= capacity.
+    // Ensure index >= initLength.
     Label outOfBounds;
     Address capacity(scratch, ObjectElements::offsetOfInitializedLength());
     masm.spectreBoundsCheck32(index, capacity, scratch2, &outOfBounds);
     masm.jump(failure->label());
     masm.bind(&outOfBounds);
 
     return true;
 }
 
 bool
+CacheIRCompiler::emitGuardIndexGreaterThanDenseCapacity()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register index = allocator.useRegister(masm, reader.int32OperandId());
+    AutoScratchRegister scratch(allocator, masm);
+    AutoScratchRegister scratch2(allocator, masm);
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure)) {
+        return false;
+    }
+
+    // Load obj->elements.
+    masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
+
+    // Ensure index >= capacity.
+    Label outOfBounds;
+    Address capacity(scratch, ObjectElements::offsetOfCapacity());
+    masm.spectreBoundsCheck32(index, capacity, scratch2, &outOfBounds);
+    masm.jump(failure->label());
+    masm.bind(&outOfBounds);
+
+    return true;
+}
+
+bool
+CacheIRCompiler::emitGuardIndexGreaterThanArrayLength()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register index = allocator.useRegister(masm, reader.int32OperandId());
+    AutoScratchRegister scratch(allocator, masm);
+    AutoScratchRegister scratch2(allocator, masm);
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure)) {
+        return false;
+    }
+
+    // Load obj->elements.
+    masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
+
+    // Ensure index >= length;
+    Label outOfBounds;
+    Address length(scratch, ObjectElements::offsetOfLength());
+    masm.spectreBoundsCheck32(index, length, scratch2, &outOfBounds);
+    masm.jump(failure->label());
+    masm.bind(&outOfBounds);
+    return true;
+}
+
+bool
+CacheIRCompiler::emitGuardIndexIsValidUpdateOrAdd()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register index = allocator.useRegister(masm, reader.int32OperandId());
+    AutoScratchRegister scratch(allocator, masm);
+    AutoScratchRegister scratch2(allocator, masm);
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure)) {
+        return false;
+    }
+
+    // Load obj->elements.
+    masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
+
+    Label success;
+
+    // If length is writable, branch to &success.  All indices are writable.
+    Address flags(scratch, ObjectElements::offsetOfFlags());
+    masm.branchTest32(Assembler::Zero, flags,
+                      Imm32(ObjectElements::Flags::NONWRITABLE_ARRAY_LENGTH),
+                      &success);
+
+    // Otherwise, ensure index is in bounds.
+    Address length(scratch, ObjectElements::offsetOfLength());
+    masm.spectreBoundsCheck32(index, length, scratch2,
+                              /* failure = */ failure->label());
+    masm.bind(&success);
+    return true;
+}
+
+bool
 CacheIRCompiler::emitGuardTagNotEqual()
 {
     Register lhs = allocator.useRegister(masm, reader.valueTagOperandId());
     Register rhs = allocator.useRegister(masm, reader.valueTagOperandId());
 
     FailurePath* failure;
     if (!addFailurePath(&failure)) {
         return false;
--- a/js/src/jit/CacheIRCompiler.h
+++ b/js/src/jit/CacheIRCompiler.h
@@ -27,30 +27,34 @@ namespace jit {
     _(GuardIsString)                      \
     _(GuardIsSymbol)                      \
     _(GuardIsNumber)                      \
     _(GuardIsInt32)                       \
     _(GuardIsInt32Index)                  \
     _(GuardType)                          \
     _(GuardClass)                         \
     _(GuardGroupHasUnanalyzedNewScript)   \
+    _(GuardIsExtensible)                  \
     _(GuardIsNativeFunction)              \
     _(GuardFunctionPrototype)             \
     _(GuardIsNativeObject)                \
     _(GuardIsProxy)                       \
     _(GuardNotDOMProxy)                   \
     _(GuardSpecificInt32Immediate)        \
     _(GuardMagicValue)                    \
     _(GuardNoUnboxedExpando)              \
     _(GuardAndLoadUnboxedExpando)         \
     _(GuardNoDetachedTypedObjects)        \
     _(GuardNoDenseElements)               \
     _(GuardAndGetNumberFromString)        \
     _(GuardAndGetIndexFromString)         \
     _(GuardIndexIsNonNegative)            \
+    _(GuardIndexGreaterThanDenseCapacity) \
+    _(GuardIndexGreaterThanArrayLength)   \
+    _(GuardIndexIsValidUpdateOrAdd)       \
     _(GuardIndexGreaterThanDenseInitLength) \
     _(GuardTagNotEqual)                   \
     _(GuardXrayExpandoShapeAndDefaultProto)\
     _(GuardNoAllocationMetadataBuilder)   \
     _(GuardObjectGroupNotPretenured)      \
     _(LoadObject)                         \
     _(LoadProto)                          \
     _(LoadEnclosingEnvironment)           \
--- a/js/src/jit/IonCacheIRCompiler.cpp
+++ b/js/src/jit/IonCacheIRCompiler.cpp
@@ -2261,16 +2261,38 @@ IonCacheIRCompiler::emitCallProxySetByVa
     masm.Push(val);
     masm.Push(idVal);
     masm.Push(obj);
 
     return callVM(masm, ProxySetPropertyByValueInfo);
 }
 
 bool
+IonCacheIRCompiler::emitCallAddOrUpdateSparseElementHelper()
+{
+    AutoSaveLiveRegisters save(*this);
+
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register id = allocator.useRegister(masm, reader.int32OperandId());
+    ValueOperand val = allocator.useValueRegister(masm, reader.valOperandId());
+    bool strict = reader.readBool();
+
+    Label done;
+    prepareVMCall(masm, save);
+
+    masm.Push(Imm32(strict));
+    masm.Push(val);
+    masm.Push(id);
+    masm.Push(obj);
+
+    return callVM(masm, AddOrUpdateSparseElementHelperInfo);
+}
+
+
+bool
 IonCacheIRCompiler::emitMegamorphicSetElement()
 {
     AutoSaveLiveRegisters save(*this);
 
     Register obj = allocator.useRegister(masm, reader.objOperandId());
     ConstantOrRegister idVal = allocator.useConstantOrRegister(masm, reader.valOperandId());
     ConstantOrRegister val = allocator.useConstantOrRegister(masm, reader.valOperandId());
     bool strict = reader.readBool();
--- a/js/src/jit/VMFunctions.cpp
+++ b/js/src/jit/VMFunctions.cpp
@@ -2067,10 +2067,15 @@ const VMFunction ProxyHasInfo = Function
 typedef bool (*ProxyHasOwnFn)(JSContext*, HandleObject, HandleValue, MutableHandleValue);
 const VMFunction ProxyHasOwnInfo = FunctionInfo<ProxyHasOwnFn>(ProxyHasOwn, "ProxyHasOwn");
 
 typedef bool (*NativeGetElementFn)(JSContext*, HandleNativeObject, HandleValue, int32_t,
                                    MutableHandleValue);
 const VMFunction NativeGetElementInfo =
     FunctionInfo<NativeGetElementFn>(NativeGetElement, "NativeGetProperty");
 
+typedef bool (*AddOrUpdateSparseElementHelperFn)(JSContext* cx, HandleArrayObject obj,
+                                                 int32_t int_id, HandleValue v, bool strict);
+const VMFunction AddOrUpdateSparseElementHelperInfo =
+    FunctionInfo<AddOrUpdateSparseElementHelperFn>(AddOrUpdateSparseElementHelper, "AddOrUpdateSparseElementHelper");
+
 } // namespace jit
 } // namespace js
--- a/js/src/jit/VMFunctions.h
+++ b/js/src/jit/VMFunctions.h
@@ -977,15 +977,17 @@ extern const VMFunction ProxyGetProperty
 extern const VMFunction ProxyGetPropertyByValueInfo;
 extern const VMFunction ProxySetPropertyInfo;
 extern const VMFunction ProxySetPropertyByValueInfo;
 extern const VMFunction ProxyHasInfo;
 extern const VMFunction ProxyHasOwnInfo;
 
 extern const VMFunction NativeGetElementInfo;
 
+extern const VMFunction AddOrUpdateSparseElementHelperInfo;
+
 // TailCall VMFunctions
 extern const VMFunction DoConcatStringObjectInfo;
 
 } // namespace jit
 } // namespace js
 
 #endif /* jit_VMFunctions_h */
--- a/js/src/jit/x86/Assembler-x86.h
+++ b/js/src/jit/x86/Assembler-x86.h
@@ -454,30 +454,21 @@ class Assembler : public AssemblerX86Sha
     }
 
     void vhaddpd(FloatRegister src, FloatRegister dest) {
         MOZ_ASSERT(HasSSE3());
         MOZ_ASSERT(src.size() == 16);
         MOZ_ASSERT(dest.size() == 16);
         masm.vhaddpd_rr(src.encoding(), dest.encoding());
     }
-    void vsubpd(const Operand& src1, FloatRegister src0, FloatRegister dest) {
+    void vsubpd(FloatRegister src1, FloatRegister src0, FloatRegister dest) {
         MOZ_ASSERT(HasSSE2());
         MOZ_ASSERT(src0.size() == 16);
         MOZ_ASSERT(dest.size() == 16);
-        switch (src1.kind()) {
-          case Operand::MEM_REG_DISP:
-            masm.vsubpd_mr(src1.disp(), src1.base(), src0.encoding(), dest.encoding());
-            break;
-          case Operand::MEM_ADDRESS32:
-            masm.vsubpd_mr(src1.address(), src0.encoding(), dest.encoding());
-            break;
-          default:
-            MOZ_CRASH("unexpected operand kind");
-        }
+        masm.vsubpd_rr(src1.encoding(), src0.encoding(), dest.encoding());
     }
 
     void vpunpckldq(FloatRegister src1, FloatRegister src0, FloatRegister dest) {
         MOZ_ASSERT(HasSSE2());
         MOZ_ASSERT(src0.size() == 16);
         MOZ_ASSERT(src1.size() == 16);
         MOZ_ASSERT(dest.size() == 16);
         masm.vpunpckldq_rr(src1.encoding(), src0.encoding(), dest.encoding());
--- a/js/src/jit/x86/BaseAssembler-x86.h
+++ b/js/src/jit/x86/BaseAssembler-x86.h
@@ -171,24 +171,16 @@ class BaseAssemblerX86 : public BaseAsse
     {
         twoByteOpSimdFlags("vhaddpd", VEX_PD, OP2_HADDPD, src, dst);
     }
 
     void vsubpd_rr(XMMRegisterID src1, XMMRegisterID src0, XMMRegisterID dst)
     {
         twoByteOpSimd("vsubpd", VEX_PD, OP2_SUBPS_VpsWps, src1, src0, dst);
     }
-    void vsubpd_mr(int32_t offset, RegisterID base, XMMRegisterID src0, XMMRegisterID dst)
-    {
-        twoByteOpSimd("vsubpd", VEX_PD, OP2_SUBPS_VpsWps, offset, base, src0, dst);
-    }
-    void vsubpd_mr(const void* address, XMMRegisterID src0, XMMRegisterID dst)
-    {
-        twoByteOpSimd("vsubpd", VEX_PD, OP2_SUBPS_VpsWps, address, src0, dst);
-    }
 
     void vpunpckldq_rr(XMMRegisterID src1, XMMRegisterID src0, XMMRegisterID dst) {
         twoByteOpSimd("vpunpckldq", VEX_PD, OP2_PUNPCKLDQ, src1, src0, dst);
     }
     void vpunpckldq_mr(int32_t offset, RegisterID base, XMMRegisterID src0, XMMRegisterID dst)
     {
         twoByteOpSimd("vpunpckldq", VEX_PD, OP2_PUNPCKLDQ, offset, base, src0, dst);
     }
--- a/js/src/jit/x86/MacroAssembler-x86.cpp
+++ b/js/src/jit/x86/MacroAssembler-x86.cpp
@@ -1115,25 +1115,16 @@ MacroAssembler::wasmTruncateFloat32ToUIn
     if (isSaturating) {
         bind(oolRejoin);
     }
 }
 
 // ========================================================================
 // Convert floating point.
 
-// vpunpckldq requires 16-byte boundary for memory operand.
-// See convertUInt64ToDouble for the details.
-MOZ_ALIGNED_DECL(static const uint64_t, 16) TO_DOUBLE[4] = {
-    0x4530000043300000LL,
-    0x0LL,
-    0x4330000000000000LL,
-    0x4530000000000000LL
-};
-
 bool
 MacroAssembler::convertUInt64ToDoubleNeedsTemp()
 {
     return HasSSE3();
 }
 
 void
 MacroAssembler::convertUInt64ToDouble(Register64 src, FloatRegister dest, Register temp)
@@ -1182,28 +1173,44 @@ MacroAssembler::convertUInt64ToDouble(Re
     vpunpckldq(ScratchSimd128Reg, dest128, dest128);
 
     // Unpack and interleave dest and a constant C1 to dest:
     //   C1       = 0x 00000000 00000000  45300000 43300000
     //   dest     = 0x 45300000 HHHHHHHH  43300000 LLLLLLLL
     // here, each 64-bit part of dest represents following double:
     //   HI(dest) = 0x 1.00000HHHHHHHH * 2**84 == 2**84 + 0x HHHHHHHH 00000000
     //   LO(dest) = 0x 1.00000LLLLLLLL * 2**52 == 2**52 + 0x 00000000 LLLLLLLL
-    movePtr(ImmWord((uintptr_t)TO_DOUBLE), temp);
-    vpunpckldq(Operand(temp, 0), dest128, dest128);
+    // See convertUInt64ToDouble for the details.
+    static const int32_t CST1[4] = {
+        0x43300000,
+        0x45300000,
+        0x0,
+        0x0,
+    };
+
+    loadConstantSimd128Int(SimdConstant::CreateX4(CST1), ScratchSimd128Reg);
+    vpunpckldq(ScratchSimd128Reg, dest128, dest128);
 
     // Subtract a constant C2 from dest, for each 64-bit part:
     //   C2       = 0x 45300000 00000000  43300000 00000000
     // here, each 64-bit part of C2 represents following double:
     //   HI(C2)   = 0x 1.0000000000000 * 2**84 == 2**84
     //   LO(C2)   = 0x 1.0000000000000 * 2**52 == 2**52
     // after the operation each 64-bit part of dest represents following:
     //   HI(dest) = double(0x HHHHHHHH 00000000)
     //   LO(dest) = double(0x 00000000 LLLLLLLL)
-    vsubpd(Operand(temp, sizeof(uint64_t) * 2), dest128, dest128);
+    static const int32_t CST2[4] = {
+        0x0,
+        0x43300000,
+        0x0,
+        0x45300000,
+    };
+
+    loadConstantSimd128Int(SimdConstant::CreateX4(CST2), ScratchSimd128Reg);
+    vsubpd(ScratchSimd128Reg, dest128, dest128);
 
     // Add HI(dest) and LO(dest) in double and store it into LO(dest),
     //   LO(dest) = double(0x HHHHHHHH 00000000) + double(0x 00000000 LLLLLLLL)
     //            = double(0x HHHHHHHH LLLLLLLL)
     //            = double(src)
     vhaddpd(dest128, dest128);
 }
 
--- a/js/src/vm/NativeObject.cpp
+++ b/js/src/vm/NativeObject.cpp
@@ -2102,16 +2102,58 @@ DefineNonexistentProperty(JSContext* cx,
         if (!AddDataProperty(cx, obj, id, v)) {
             return false;
         }
     }
 
     return result.succeed();
 }
 
+bool
+js::AddOrUpdateSparseElementHelper(JSContext* cx, HandleArrayObject obj, int32_t int_id,
+                                   HandleValue v, bool strict)
+{
+    MOZ_ASSERT(INT_FITS_IN_JSID(int_id));
+    RootedId id(cx, INT_TO_JSID(int_id));
+
+    // This helper doesn't handle the case where the index may be in the dense elements
+    MOZ_ASSERT(int_id >= 0);
+    MOZ_ASSERT(uint32_t(int_id) >= obj->getDenseInitializedLength());
+
+    // First decide if this is an add or an update. Because the IC guards have
+    // already ensured this exists exterior to the dense array range, and the
+    // prototype checks have ensured there are no indexes on the prototype, we
+    // can use the shape lineage to find the element if it exists:
+    RootedShape shape(cx, obj->lastProperty()->search(cx, id));
+
+    // If we didn't find the shape, we're on the add path: delegate to
+    // AddSparseElement:
+    if (shape == nullptr) {
+        Rooted<PropertyDescriptor> desc(cx);
+        desc.setDataDescriptor(v, JSPROP_ENUMERATE);
+        desc.assertComplete();
+
+        return AddOrChangeProperty<IsAddOrChange::Add>(cx, obj, id, desc);
+    }
+
+    // At this point we're updating a property: See SetExistingProperty
+    if (shape->writable() && shape->isDataProperty()) {
+        // While all JSID_INT properties use a single TI entry,
+        // nothing yet has inspected the updated value so we *must* use setSlotWithType().
+        obj->setSlotWithType(cx, shape, v, /* overwriting = */ true);
+        return true;
+    }
+
+    // We don't know exactly what this object looks like, hit the slowpath.
+    RootedValue receiver(cx, ObjectValue(*obj));
+    JS::ObjectOpResult result;
+    return SetProperty(cx, obj, id, v, receiver, result) &&
+           result.checkStrictErrorOrWarning(cx, obj, id, strict);
+}