Bug 1286798 - Part 31: Support for lazy loading of items; r=asuth,mrbkap,mccr8
authorJan Varga <jan.varga@gmail.com>
Thu, 29 Nov 2018 21:48:54 +0100
changeset 508029 bc288ab2655c2cf6d2404fd204b7afa1d67262d2
parent 508028 e2b5bee9812c4b8f0f2344eb848d85696886151d
child 508030 1309fe77cfaa7237aeb193db5b3544f7c2a0f6f0
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth, mrbkap, mccr8
bugs1286798
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1286798 - Part 31: Support for lazy loading of items; r=asuth,mrbkap,mccr8 There's now an upper limit for snapshot prefilling. The value is configurable and is currently set to 4096 bytes. Snapshots can operate in multiple modes depending on if all items have been loaded or all keys have been received. This should provide the best performance for each specific state. This patch also adds support for creating explicit snapshots which can be used for testing.
dom/localstorage/ActorsParent.cpp
dom/localstorage/LSDatabase.cpp
dom/localstorage/LSDatabase.h
dom/localstorage/LSObject.cpp
dom/localstorage/LSObject.h
dom/localstorage/LSSnapshot.cpp
dom/localstorage/LSSnapshot.h
dom/localstorage/PBackgroundLSDatabase.ipdl
dom/localstorage/PBackgroundLSSnapshot.ipdl
dom/localstorage/test/unit/test_snapshotting.js
dom/storage/Storage.h
dom/webidl/Storage.webidl
ipc/ipdl/sync-messages.ini
modules/libpref/init/all.js
--- a/dom/localstorage/ActorsParent.cpp
+++ b/dom/localstorage/ActorsParent.cpp
@@ -113,17 +113,19 @@ static_assert(kSQLiteGrowthIncrement >= 
 #define DATA_FILE_NAME "data.sqlite"
 #define JOURNAL_FILE_NAME "data.sqlite-journal"
 
 const uint32_t kAutoCommitTimeoutMs = 5000;
 
 const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited";
 
 const uint32_t kDefaultOriginLimitKB = 5 * 1024;
+const uint32_t kDefaultSnapshotPrefill = 4096;
 const char kDefaultQuotaPref[] = "dom.storage.default_quota";
+const char kSnapshotPrefillPref[] = "dom.storage.snapshot_prefill";
 
 const uint32_t kPreparedDatastoreTimeoutMs = 20000;
 
 #define LS_ARCHIVE_FILE_NAME "ls-archive.sqlite"
 
 bool
 IsOnConnectionThread();
 
@@ -1122,23 +1124,30 @@ public:
 #endif
 
   void
   NoteActiveDatabase(Database* aDatabase);
 
   void
   NoteInactiveDatabase(Database* aDatabase);
 
+  uint32_t
+  GetLength() const;
+
   void
-  GetItemInfos(nsTArray<LSItemInfo>* aItemInfos);
+  GetItemInfos(nsTHashtable<nsStringHashKey>& aLoadedItems,
+               nsTArray<LSItemInfo>& aItemInfos);
 
   void
   GetItem(const nsString& aKey, nsString& aValue) const;
 
   void
+  GetKeys(nsTArray<nsString>& aKeys) const;
+
+  void
   SetItem(Database* aDatabase,
           const nsString& aDocumentURI,
           const nsString& aKey,
           const nsString& aOldValue,
           const nsString& aValue);
 
   void
   RemoveItem(Database* aDatabase,
@@ -1173,16 +1182,22 @@ private:
 
   void
   ConnectionClosedCallback();
 
   void
   CleanupMetadata();
 
   void
+  NotifySnapshots(Database* aDatabase,
+                  const nsAString& aKey,
+                  const nsAString& aOldValue,
+                  bool aAffectsOrder);
+
+  void
   NotifyObservers(Database* aDatabase,
                   const nsString& aDocumentURI,
                   const nsString& aKey,
                   const nsString& aOldValue,
                   const nsString& aNewValue);
 
   void
   EnsureTransaction();
@@ -1344,16 +1359,23 @@ public:
   SetActorAlive(Datastore* aDatastore);
 
   void
   RegisterSnapshot(Snapshot* aSnapshot);
 
   void
   UnregisterSnapshot(Snapshot* aSnapshot);
 
+  Snapshot*
+  GetSnapshot() const
+  {
+    AssertIsOnBackgroundThread();
+    return mSnapshot;
+  }
+
   void
   RequestAllowToClose();
 
   NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Database)
 
 private:
   // Reference counted.
   ~Database();
@@ -1387,40 +1409,66 @@ private:
                                      override;
 };
 
 class Snapshot final
   : public PBackgroundLSSnapshotParent
 {
   RefPtr<Database> mDatabase;
   RefPtr<Datastore> mDatastore;
+  nsTHashtable<nsStringHashKey> mLoadedItems;
+  nsTHashtable<nsStringHashKey> mUnknownItems;
+  nsDataHashtable<nsStringHashKey, nsString> mValues;
+  nsTArray<nsString> mKeys;
   nsString mDocumentURI;
+  uint32_t mTotalLength;
   int64_t mInitialUsage;
   int64_t mPeakUsage;
+  bool mSavedKeys;
   bool mActorDestroyed;
   bool mFinishReceived;
+  bool mLoadedReceived;
+  bool mLoadedAllItems;
+  bool mLoadKeysReceived;
 
 public:
   // Created in AllocPBackgroundLSSnapshotParent.
   Snapshot(Database* aDatabase,
            const nsAString& aDocumentURI);
 
   void
-  SetUsage(int64_t aInitialUsage,
-           int64_t aPeakUsage)
+  Init(nsTHashtable<nsStringHashKey>& aLoadedItems,
+       uint32_t aTotalLength,
+       int64_t aInitialUsage,
+       int64_t aPeakUsage,
+       bool aFullPrefill)
   {
     AssertIsOnBackgroundThread();
     MOZ_ASSERT(aInitialUsage >= 0);
     MOZ_ASSERT(aPeakUsage >= aInitialUsage);
+    MOZ_ASSERT_IF(aFullPrefill, aLoadedItems.Count() == 0);
+    MOZ_ASSERT(mTotalLength == 0);
     MOZ_ASSERT(mInitialUsage == -1);
     MOZ_ASSERT(mPeakUsage == -1);
 
+    mLoadedItems.SwapElements(aLoadedItems);
+    mTotalLength = aTotalLength;
     mInitialUsage = aInitialUsage;
     mPeakUsage = aPeakUsage;
-  }
+    if (aFullPrefill) {
+      mLoadedReceived = true;
+      mLoadedAllItems = true;
+      mLoadKeysReceived = true;
+    }
+  }
+
+  void
+  SaveItem(const nsAString& aKey,
+           const nsAString& aOldValue,
+           bool aAffectsOrder);
 
   NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot)
 
 private:
   // Reference counted.
   ~Snapshot();
 
   // IPDL methods are only called by IPDL.
@@ -1429,19 +1477,32 @@ private:
 
   mozilla::ipc::IPCResult
   RecvDeleteMe() override;
 
   mozilla::ipc::IPCResult
   RecvFinish(const LSSnapshotFinishInfo& aFinishInfo) override;
 
   mozilla::ipc::IPCResult
+  RecvLoaded() override;
+
+  mozilla::ipc::IPCResult
+  RecvLoadItem(const nsString& aKey,
+               nsString* aValue) override;
+
+  mozilla::ipc::IPCResult
+  RecvLoadKeys(nsTArray<nsString>* aKeys) override;
+
+  mozilla::ipc::IPCResult
   RecvIncreasePeakUsage(const int64_t& aRequestedSize,
                         const int64_t& aMinSize,
                         int64_t* aSize) override;
+
+  mozilla::ipc::IPCResult
+  RecvPing() override;
 };
 
 class Observer final
   : public PBackgroundLSObserverParent
 {
   nsCString mOrigin;
   bool mActorDestroyed;
 
@@ -2080,16 +2141,17 @@ typedef nsRefPtrHashtable<nsUint64HashKe
 StaticAutoPtr<PreparedObserverHashtable> gPreparedObsevers;
 
 typedef nsClassHashtable<nsCStringHashKey, nsTArray<Observer*>>
   ObserverHashtable;
 
 StaticAutoPtr<ObserverHashtable> gObservers;
 
 Atomic<uint32_t, Relaxed> gOriginLimitKB(kDefaultOriginLimitKB);
+Atomic<int32_t, Relaxed> gSnapshotPrefill(kDefaultSnapshotPrefill);
 
 typedef nsDataHashtable<nsCStringHashKey, int64_t> UsageHashtable;
 
 // Can only be touched on the Quota Manager I/O thread.
 StaticAutoPtr<UsageHashtable> gUsages;
 
 typedef nsTHashtable<nsCStringHashKey> ArchivedOriginHashtable;
 
@@ -2228,16 +2290,34 @@ GetUsage(mozIStorageConnection* aConnect
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   *aUsage = usage;
   return NS_OK;
 }
 
+void
+SnapshotPrefillPrefChangedCallback(const char* aPrefName, void* aClosure)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!strcmp(aPrefName, kSnapshotPrefillPref));
+  MOZ_ASSERT(!aClosure);
+
+  int32_t snapshotPrefill =
+    Preferences::GetInt(aPrefName, kDefaultSnapshotPrefill);
+
+  // The magic -1 is for use only by tests.
+  if (snapshotPrefill == -1) {
+    snapshotPrefill = INT32_MAX;
+  }
+
+  gSnapshotPrefill = snapshotPrefill;
+}
+
 } // namespace
 
 /*******************************************************************************
  * Exported functions
  ******************************************************************************/
 
 PBackgroundLSDatabaseParent*
 AllocPBackgroundLSDatabaseParent(const PrincipalInfo& aPrincipalInfo,
@@ -3273,68 +3353,110 @@ Datastore::NoteInactiveDatabase(Database
       DebugOnly<bool> ok = UpdateUsage(finalDelta);
       MOZ_ASSERT(ok);
     }
 
     mPendingUsageDeltas.Clear();
   }
 }
 
+uint32_t
+Datastore::GetLength() const
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mClosed);
+
+  return mValues.Count();
+}
+
 void
-Datastore::GetItemInfos(nsTArray<LSItemInfo>* aItemInfos)
-{
+Datastore::GetItemInfos(nsTHashtable<nsStringHashKey>& aLoadedItems,
+                        nsTArray<LSItemInfo>& aItemInfos)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mClosed);
+
+  int64_t size = 0;
   for (auto key : mKeys) {
     nsString value;
     DebugOnly<bool> hasValue = mValues.Get(key, &value);
     MOZ_ASSERT(hasValue);
 
-    LSItemInfo* itemInfo = aItemInfos->AppendElement();
+    size += static_cast<int64_t>(key.Length()) +
+            static_cast<int64_t>(value.Length());
+
+    if (size > gSnapshotPrefill) {
+      return;
+    }
+
+    aLoadedItems.PutEntry(key);
+
+    LSItemInfo* itemInfo = aItemInfos.AppendElement();
     itemInfo->key() = key;
     itemInfo->value() = value;
   }
+
+  aLoadedItems.Clear();
 }
 
 void
 Datastore::GetItem(const nsString& aKey, nsString& aValue) const
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(!mClosed);
 
   if (!mValues.Get(aKey, &aValue)) {
     aValue.SetIsVoid(true);
   }
 }
 
 void
+Datastore::GetKeys(nsTArray<nsString>& aKeys) const
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mClosed);
+
+  aKeys.AppendElements(mKeys);
+}
+
+void
 Datastore::SetItem(Database* aDatabase,
                    const nsString& aDocumentURI,
                    const nsString& aKey,
                    const nsString& aOldValue,
                    const nsString& aValue)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabase);
   MOZ_ASSERT(!mClosed);
   MOZ_ASSERT(mInUpdateBatch);
 
   nsString oldValue;
   GetItem(aKey, oldValue);
 
   if (oldValue != aValue || oldValue.IsVoid() != aValue.IsVoid()) {
+    bool affectsOrder;
+
     int64_t delta = static_cast<int64_t>(aValue.Length()) -
                     static_cast<int64_t>(oldValue.Length());
 
     if (oldValue.IsVoid()) {
+      affectsOrder = true;
+
       delta += static_cast<int64_t>(aKey.Length());
 
       mUpdateBatchAppends.AppendElement(aKey);
+    } else {
+      affectsOrder = false;
     }
 
     mUpdateBatchUsage += delta;
 
+    NotifySnapshots(aDatabase, aKey, oldValue, affectsOrder);
+
     mValues.Put(aKey, aValue);
 
     if (IsPersistent()) {
       EnsureTransaction();
 
       RefPtr<SetItemOp> op = new SetItemOp(mConnection, aKey, aValue);
       mConnection->Dispatch(op);
     }
@@ -3365,16 +3487,18 @@ Datastore::RemoveItem(Database* aDatabas
       entry.OrInsert([]() { return 1; });
     }
 
     int64_t delta = -(static_cast<int64_t>(aKey.Length()) +
                       static_cast<int64_t>(oldValue.Length()));
 
     mUpdateBatchUsage += delta;
 
+    NotifySnapshots(aDatabase, aKey, oldValue, /* aAffectsOrder */ true);
+
     mValues.Remove(aKey);
 
     if (IsPersistent()) {
       EnsureTransaction();
 
       RefPtr<RemoveItemOp> op = new RemoveItemOp(mConnection, aKey);
       mConnection->Dispatch(op);
     }
@@ -3399,16 +3523,18 @@ Datastore::Clear(Database* aDatabase,
     for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
       const nsAString& key = iter.Key();
       const nsAString& value = iter.Data();
 
       int64_t delta = -(static_cast<int64_t>(key.Length()) +
                         static_cast<int64_t>(value.Length()));
 
       mUpdateBatchUsage += delta;
+
+      NotifySnapshots(aDatabase, key, value, /* aAffectsOrder */ true);
     }
 
     mValues.Clear();
     mKeys.Clear();
 
     if (IsPersistent()) {
       EnsureTransaction();
 
@@ -3600,16 +3726,38 @@ Datastore::CleanupMetadata()
   gDatastores->Remove(mOrigin);
 
   if (!gDatastores->Count()) {
     gDatastores = nullptr;
   }
 }
 
 void
+Datastore::NotifySnapshots(Database* aDatabase,
+                           const nsAString& aKey,
+                           const nsAString& aOldValue,
+                           bool aAffectsOrder)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+
+  for (auto iter = mDatabases.ConstIter(); !iter.Done(); iter.Next()) {
+    Database* database = iter.Get()->GetKey();
+    if (database == aDatabase) {
+      continue;
+    }
+
+    Snapshot* snapshot = database->GetSnapshot();
+    if (snapshot) {
+      snapshot->SaveItem(aKey, aOldValue, aAffectsOrder);
+    }
+  }
+}
+
+void
 Datastore::NotifyObservers(Database* aDatabase,
                            const nsString& aDocumentURI,
                            const nsString& aKey,
                            const nsString& aOldValue,
                            const nsString& aNewValue)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabase);
@@ -3890,31 +4038,39 @@ Database::RecvPBackgroundLSSnapshotConst
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aRequestedSize >= 0);
   MOZ_ASSERT(!mAllowedToClose);
 
   auto* snapshot = static_cast<Snapshot*>(aActor);
 
   // TODO: This can be optimized depending on which operation triggers snapshot
   //       creation. For example clear() doesn't need to receive items at all.
+  nsTHashtable<nsStringHashKey> loadedItems;
   nsTArray<LSItemInfo> itemInfos;
-  mDatastore->GetItemInfos(&itemInfos);
+  mDatastore->GetItemInfos(loadedItems, itemInfos);
+
+  uint32_t totalLength = mDatastore->GetLength();
 
   int64_t initialUsage = mDatastore->Usage();
 
   int64_t peakUsage = initialUsage;
   if (aRequestedSize && mDatastore->UpdateUsage(aRequestedSize)) {
     peakUsage += aRequestedSize;
   }
 
-  snapshot->SetUsage(initialUsage, peakUsage);
+  snapshot->Init(loadedItems,
+                 totalLength,
+                 initialUsage,
+                 peakUsage,
+                 /* aFullPrefill */ itemInfos.Length() == totalLength);
 
   RegisterSnapshot(snapshot);
 
   aInitInfo->itemInfos() = std::move(itemInfos);
+  aInitInfo->totalLength() = totalLength;
   aInitInfo->initialUsage() = initialUsage;
   aInitInfo->peakUsage() = peakUsage;
 
   return IPC_OK();
 }
 
 bool
 Database::DeallocPBackgroundLSSnapshotParent(
@@ -3933,32 +4089,61 @@ Database::DeallocPBackgroundLSSnapshotPa
  * Snapshot
  ******************************************************************************/
 
 Snapshot::Snapshot(Database* aDatabase,
                    const nsAString& aDocumentURI)
   : mDatabase(aDatabase)
   , mDatastore(aDatabase->GetDatastore())
   , mDocumentURI(aDocumentURI)
+  , mTotalLength(0)
   , mInitialUsage(-1)
   , mPeakUsage(-1)
+  , mSavedKeys(false)
   , mActorDestroyed(false)
   , mFinishReceived(false)
+  , mLoadedReceived(false)
+  , mLoadedAllItems(false)
+  , mLoadKeysReceived(false)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabase);
 }
 
 Snapshot::~Snapshot()
 {
   MOZ_ASSERT(mActorDestroyed);
   MOZ_ASSERT(mFinishReceived);
 }
 
 void
+Snapshot::SaveItem(const nsAString& aKey,
+                   const nsAString& aOldValue,
+                   bool aAffectsOrder)
+{
+  AssertIsOnBackgroundThread();
+
+  if (mLoadedAllItems) {
+    return;
+  }
+
+  if (!mLoadedItems.GetEntry(aKey) && !mUnknownItems.GetEntry(aKey)) {
+    nsString oldValue(aOldValue);
+    mValues.LookupForAdd(aKey).OrInsert([oldValue]() {
+      return oldValue;
+    });
+  }
+
+  if (aAffectsOrder && !mSavedKeys && !mLoadKeysReceived) {
+    mDatastore->GetKeys(mKeys);
+    mSavedKeys = true;
+  }
+}
+
+void
 Snapshot::ActorDestroy(ActorDestroyReason aWhy)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(!mActorDestroyed);
 
   mActorDestroyed = true;
 
   if (!mFinishReceived) {
@@ -4040,16 +4225,142 @@ Snapshot::RecvFinish(const LSSnapshotFin
   }
 
   mDatastore->EndUpdateBatch(mPeakUsage);
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+Snapshot::RecvLoaded()
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedAllItems)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadKeysReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  mLoadedReceived = true;
+
+  mLoadedItems.Clear();
+  mUnknownItems.Clear();
+  mValues.Clear();
+  mKeys.Clear();
+  mLoadedAllItems = true;
+  mLoadKeysReceived = true;
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvLoadItem(const nsString& aKey,
+                       nsString* aValue)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aValue);
+  MOZ_ASSERT(mDatastore);
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedAllItems)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (mLoadedItems.GetEntry(aKey) || mUnknownItems.GetEntry(aKey)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (auto entry = mValues.Lookup(aKey)) {
+    *aValue = entry.Data();
+    entry.Remove();
+  } else {
+    mDatastore->GetItem(aKey, *aValue);
+  }
+
+  if (aValue->IsVoid()) {
+    mUnknownItems.PutEntry(aKey);
+  } else {
+    mLoadedItems.PutEntry(aKey);
+
+    if (mLoadedItems.Count() == mTotalLength) {
+      mLoadedItems.Clear();
+      mUnknownItems.Clear();
+#ifdef DEBUG
+      for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+        MOZ_ASSERT(iter.Data().IsVoid());
+      }
+#endif
+      mValues.Clear();
+      mLoadedAllItems = true;
+    }
+  }
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvLoadKeys(nsTArray<nsString>* aKeys)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aKeys);
+  MOZ_ASSERT(mDatastore);
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadKeysReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  mLoadKeysReceived = true;
+
+  if (mSavedKeys) {
+    aKeys->AppendElements(std::move(mKeys));
+  } else {
+    mDatastore->GetKeys(*aKeys);
+  }
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 Snapshot::RecvIncreasePeakUsage(const int64_t& aRequestedSize,
                                 const int64_t& aMinSize,
                                 int64_t* aSize)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aSize);
 
   if (NS_WARN_IF(aRequestedSize <= 0)) {
@@ -4075,16 +4386,27 @@ Snapshot::RecvIncreasePeakUsage(const in
     *aSize = aMinSize;
   } else {
     *aSize = 0;
   }
 
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+Snapshot::RecvPing()
+{
+  AssertIsOnBackgroundThread();
+
+  // Do nothing here. This is purely a sync message allowing the child to
+  // confirm that the actor has received previous async message.
+
+  return IPC_OK();
+}
+
 /*******************************************************************************
  * Observer
  ******************************************************************************/
 
 Observer::Observer(const nsACString& aOrigin)
   : mOrigin(aOrigin)
   , mActorDestroyed(false)
 {
@@ -5738,16 +6060,19 @@ QuotaClient::RegisterObservers(nsIEventT
     }
 
     if (NS_FAILED(Preferences::AddAtomicUintVarCache(&gOriginLimitKB,
                                                      kDefaultQuotaPref,
                                                      kDefaultOriginLimitKB))) {
       NS_WARNING("Unable to respond to default quota pref changes!");
     }
 
+    Preferences::RegisterCallbackAndCall(SnapshotPrefillPrefChangedCallback,
+                                         kSnapshotPrefillPref);
+
     sObserversRegistered = true;
   }
 
   return NS_OK;
 }
 
 nsresult
 QuotaClient::InitOrigin(PersistenceType aPersistenceType,
--- a/dom/localstorage/LSDatabase.cpp
+++ b/dom/localstorage/LSDatabase.cpp
@@ -247,21 +247,67 @@ LSDatabase::Clear(LSObject* aObject,
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 nsresult
+LSDatabase::BeginExplicitSnapshot(LSObject* aObject)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  if (mSnapshot) {
+    return NS_ERROR_ALREADY_INITIALIZED;
+  }
+
+  nsresult rv = EnsureSnapshot(aObject,
+                               /* aRequestedBySetItem */ false,
+                               /* aExplicit */ true);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSDatabase::EndExplicitSnapshot(LSObject* aObject)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  if (!mSnapshot) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  MOZ_ASSERT(mSnapshot->Explicit());
+
+  nsresult rv = mSnapshot->Finish();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
 LSDatabase::EnsureSnapshot(LSObject* aObject,
-                           bool aRequestedBySetItem)
+                           bool aRequestedBySetItem,
+                           bool aExplicit)
 {
   MOZ_ASSERT(aObject);
   MOZ_ASSERT(mActor);
+  MOZ_ASSERT_IF(mSnapshot, !aExplicit);
   MOZ_ASSERT(!mAllowedToClose);
 
   if (mSnapshot) {
     return NS_OK;
   }
 
   RefPtr<LSSnapshot> snapshot = new LSSnapshot(this);
 
@@ -277,17 +323,17 @@ LSDatabase::EnsureSnapshot(LSObject* aOb
                                                  &initInfo);
   if (NS_WARN_IF(!ok)) {
     return NS_ERROR_FAILURE;
   }
 
   snapshot->SetActor(actor);
 
   // This add refs snapshot.
-  nsresult rv = snapshot->Init(initInfo);
+  nsresult rv = snapshot->Init(initInfo, aExplicit);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   // This is cleared in LSSnapshot::Run() before the snapshot is destroyed.
   mSnapshot = snapshot;
 
   return NS_OK;
--- a/dom/localstorage/LSDatabase.h
+++ b/dom/localstorage/LSDatabase.h
@@ -92,22 +92,29 @@ public:
   RemoveItem(LSObject* aObject,
              const nsAString& aKey,
              LSNotifyInfo& aNotifyInfo);
 
   nsresult
   Clear(LSObject* aObject,
         LSNotifyInfo& aNotifyInfo);
 
+  nsresult
+  BeginExplicitSnapshot(LSObject* aObject);
+
+  nsresult
+  EndExplicitSnapshot(LSObject* aObject);
+
 private:
   ~LSDatabase();
 
   nsresult
   EnsureSnapshot(LSObject* aObject,
-                 bool aRequestedBySetItem);
+                 bool aRequestedBySetItem,
+                 bool aExplicit = false);
 
   void
   AllowToClose();
 };
 
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/localstorage/LSObject.cpp
+++ b/dom/localstorage/LSObject.cpp
@@ -123,16 +123,17 @@ private:
 };
 
 } // namespace
 
 LSObject::LSObject(nsPIDOMWindowInner* aWindow,
                    nsIPrincipal* aPrincipal)
   : Storage(aWindow, aPrincipal)
   , mPrivateBrowsingId(0)
+  , mInExplicitSnapshot(false)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(NextGenLocalStorageEnabled());
 }
 
 LSObject::~LSObject()
 {
   AssertIsOnOwningThread();
@@ -558,16 +559,70 @@ LSObject::Close(nsIPrincipal& aSubjectPr
   if (!CanUseStorage(aSubjectPrincipal)) {
     aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return;
   }
 
   DropDatabase();
 }
 
+void
+LSObject::BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+                                ErrorResult& aError)
+{
+  AssertIsOnOwningThread();
+
+  if (!CanUseStorage(aSubjectPrincipal)) {
+    aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return;
+  }
+
+  if (mInExplicitSnapshot) {
+    aError.Throw(NS_ERROR_ALREADY_INITIALIZED);
+    return;
+  }
+
+  nsresult rv = EnsureDatabase();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aError.Throw(rv);
+    return;
+  }
+
+  rv = mDatabase->BeginExplicitSnapshot(this);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aError.Throw(rv);
+    return;
+  }
+
+  mInExplicitSnapshot = true;
+}
+
+void
+LSObject::EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+                              ErrorResult& aError)
+{
+  AssertIsOnOwningThread();
+
+  if (!CanUseStorage(aSubjectPrincipal)) {
+    aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return;
+  }
+
+  if (!mInExplicitSnapshot) {
+    aError.Throw(NS_ERROR_NOT_INITIALIZED);
+    return;
+  }
+
+  nsresult rv = EndExplicitSnapshotInternal();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aError.Throw(rv);
+    return;
+  }
+}
+
 NS_IMPL_ADDREF_INHERITED(LSObject, Storage)
 NS_IMPL_RELEASE_INHERITED(LSObject, Storage)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LSObject)
 NS_INTERFACE_MAP_END_INHERITING(Storage)
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(LSObject)
 
@@ -687,16 +742,21 @@ LSObject::EnsureDatabase()
   return NS_OK;
 }
 
 void
 LSObject::DropDatabase()
 {
   AssertIsOnOwningThread();
 
+  if (mInExplicitSnapshot) {
+    nsresult rv = EndExplicitSnapshotInternal();
+    Unused << NS_WARN_IF(NS_FAILED(rv));
+  }
+
   mDatabase = nullptr;
 }
 
 nsresult
 LSObject::EnsureObserver()
 {
   AssertIsOnOwningThread();
 
@@ -775,16 +835,48 @@ LSObject::OnChange(const nsAString& aKey
                aOldValue,
                aNewValue,
                /* aStorageType */ kLocalStorageType,
                mDocumentURI,
                /* aIsPrivate */ !!mPrivateBrowsingId,
                /* aImmediateDispatch */ false);
 }
 
+nsresult
+LSObject::EndExplicitSnapshotInternal()
+{
+  AssertIsOnOwningThread();
+
+  // Can be only called if the mInExplicitSnapshot flag is true.
+  // An explicit snapshot must have been created.
+  MOZ_ASSERT(mInExplicitSnapshot);
+
+  // If an explicit snapshot have been created then mDatabase must be not null.
+  // DropDatabase could be called in the meatime, but that would set
+  // mInExplicitSnapshot to false. EnsureDatabase could be called in the
+  // meantime too, but that can't set mDatabase to null or to a new value. See
+  // the comment below.
+  MOZ_ASSERT(mDatabase);
+
+  // Existence of a snapshot prevents the database from allowing to close. See
+  // LSDatabase::RequestAllowToClose and LSDatabase::NoteFinishedSnapshot.
+  // If the database is not allowed to close then mDatabase could not have been
+  // nulled out or set to a new value. See EnsureDatabase.
+  MOZ_ASSERT(!mDatabase->IsAllowedToClose());
+
+  nsresult rv = mDatabase->EndExplicitSnapshot(this);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  mInExplicitSnapshot = false;
+
+  return NS_OK;
+}
+
 void
 LSObject::LastRelease()
 {
   AssertIsOnOwningThread();
 
   DropDatabase();
 }
 
--- a/dom/localstorage/LSObject.h
+++ b/dom/localstorage/LSObject.h
@@ -44,16 +44,18 @@ class LSObject final
 
   RefPtr<LSDatabase> mDatabase;
   RefPtr<LSObserver> mObserver;
 
   uint32_t mPrivateBrowsingId;
   nsCString mOrigin;
   nsString mDocumentURI;
 
+  bool mInExplicitSnapshot;
+
 public:
   static nsresult
   CreateForWindow(nsPIDOMWindowInner* aWindow,
                   Storage** aStorage);
 
   static nsresult
   CreateForPrincipal(nsPIDOMWindowInner* aWindow,
                      nsIPrincipal* aPrincipal,
@@ -136,16 +138,24 @@ public:
   void
   Open(nsIPrincipal& aSubjectPrincipal,
        ErrorResult& aError) override;
 
   void
   Close(nsIPrincipal& aSubjectPrincipal,
         ErrorResult& aError) override;
 
+  void
+  BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+                        ErrorResult& aError) override;
+
+  void
+  EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+                      ErrorResult& aError) override;
+
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(LSObject, Storage)
 
 private:
   LSObject(nsPIDOMWindowInner* aWindow,
            nsIPrincipal* aPrincipal);
 
   ~LSObject();
@@ -166,16 +176,19 @@ private:
   void
   DropObserver();
 
   void
   OnChange(const nsAString& aKey,
            const nsAString& aOldValue,
            const nsAString& aNewValue);
 
+  nsresult
+  EndExplicitSnapshotInternal();
+
   // Storage overrides.
   void
   LastRelease() override;
 };
 
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/localstorage/LSSnapshot.cpp
+++ b/dom/localstorage/LSSnapshot.cpp
@@ -9,18 +9,22 @@
 #include "nsContentUtils.h"
 
 namespace mozilla {
 namespace dom {
 
 LSSnapshot::LSSnapshot(LSDatabase* aDatabase)
   : mDatabase(aDatabase)
   , mActor(nullptr)
+  , mInitLength(0)
+  , mLength(0)
   , mExactUsage(0)
   , mPeakUsage(0)
+  , mLoadState(LoadState::Initial)
+  , mExplicit(false)
 #ifdef DEBUG
   , mInitialized(false)
   , mSentFinish(false)
 #endif
 {
   AssertIsOnOwningThread();
 }
 
@@ -42,64 +46,90 @@ LSSnapshot::SetActor(LSSnapshotChild* aA
   AssertIsOnOwningThread();
   MOZ_ASSERT(aActor);
   MOZ_ASSERT(!mActor);
 
   mActor = aActor;
 }
 
 nsresult
-LSSnapshot::Init(const LSSnapshotInitInfo& aInitInfo)
+LSSnapshot::Init(const LSSnapshotInitInfo& aInitInfo,
+                 bool aExplicit)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mLoadState == LoadState::Initial);
   MOZ_ASSERT(!mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
   const nsTArray<LSItemInfo>& itemInfos = aInitInfo.itemInfos();
   for (uint32_t i = 0; i < itemInfos.Length(); i++) {
     const LSItemInfo& itemInfo = itemInfos[i];
+    mLoadedItems.PutEntry(itemInfo.key());
     mValues.Put(itemInfo.key(), itemInfo.value());
   }
 
+  if (itemInfos.Length() == aInitInfo.totalLength()) {
+    mLoadState = LoadState::AllOrderedItems;
+  } else {
+    mLoadState = LoadState::Partial;
+    mInitLength = aInitInfo.totalLength();
+    mLength = mInitLength;
+  }
+
   mExactUsage = aInitInfo.initialUsage();
   mPeakUsage = aInitInfo.peakUsage();
 
-  nsCOMPtr<nsIRunnable> runnable = this;
-  nsContentUtils::RunInStableState(runnable.forget());
+  mExplicit = aExplicit;
+
+  if (mExplicit) {
+    mSelfRef = this;
+  } else {
+    nsCOMPtr<nsIRunnable> runnable = this;
+    nsContentUtils::RunInStableState(runnable.forget());
+  }
 
 #ifdef DEBUG
   mInitialized = true;
 #endif
 
   return NS_OK;
 }
 
 nsresult
 LSSnapshot::GetLength(uint32_t* aResult)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
-  *aResult = mValues.Count();
+  if (mLoadState == LoadState::Partial) {
+    *aResult = mLength;
+  } else {
+    *aResult = mValues.Count();
+  }
 
   return NS_OK;
 }
 
 nsresult
 LSSnapshot::GetKey(uint32_t aIndex,
                    nsAString& aResult)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  nsresult rv = EnsureAllKeys();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
   aResult.SetIsVoid(true);
   for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
     if (aIndex == 0) {
       aResult = iter.Key();
       return NS_OK;
     }
     aIndex--;
   }
@@ -112,32 +142,103 @@ LSSnapshot::GetItem(const nsAString& aKe
                     nsAString& aResult)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
   nsString result;
-  if (!mValues.Get(aKey, &result)) {
-    result.SetIsVoid(true);
+
+  switch (mLoadState) {
+    case LoadState::Partial: {
+      if (mValues.Get(aKey, &result)) {
+        MOZ_ASSERT(!result.IsVoid());
+      } else if (mLoadedItems.GetEntry(aKey) || mUnknownItems.GetEntry(aKey)) {
+        result.SetIsVoid(true);
+      } else {
+        if (NS_WARN_IF(!mActor->SendLoadItem(nsString(aKey), &result))) {
+          return NS_ERROR_FAILURE;
+        }
+
+        if (result.IsVoid()) {
+          mUnknownItems.PutEntry(aKey);
+        } else {
+          mLoadedItems.PutEntry(aKey);
+          mValues.Put(aKey, result);
+
+          if (mLoadedItems.Count() == mInitLength) {
+            mLoadedItems.Clear();
+            mUnknownItems.Clear();
+            mLength = 0;
+            mLoadState = LoadState::AllUnorderedItems;
+          }
+        }
+      }
+
+      break;
+    }
+
+    case LoadState::AllOrderedKeys: {
+      if (mValues.Get(aKey, &result)) {
+        if (result.IsVoid()) {
+          if (NS_WARN_IF(!mActor->SendLoadItem(nsString(aKey), &result))) {
+            return NS_ERROR_FAILURE;
+          }
+
+          MOZ_ASSERT(!result.IsVoid());
+
+          mLoadedItems.PutEntry(aKey);
+          mValues.Put(aKey, result);
+
+          if (mLoadedItems.Count() == mInitLength) {
+            mLoadedItems.Clear();
+            MOZ_ASSERT(mLength == 0);
+            mLoadState = LoadState::AllOrderedItems;
+          }
+        }
+      } else {
+        result.SetIsVoid(true);
+      }
+
+      break;
+    }
+
+    case LoadState::AllUnorderedItems:
+    case LoadState::AllOrderedItems: {
+      if (mValues.Get(aKey, &result)) {
+        MOZ_ASSERT(!result.IsVoid());
+      } else {
+        result.SetIsVoid(true);
+      }
+
+      break;
+    }
+
+    default:
+      MOZ_CRASH("Bad state!");
   }
 
   aResult = result;
   return NS_OK;
 }
 
 nsresult
 LSSnapshot::GetKeys(nsTArray<nsString>& aKeys)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  nsresult rv = EnsureAllKeys();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
   for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
     aKeys.AppendElement(iter.Key());
   }
 
   return NS_OK;
 }
 
 nsresult
@@ -171,16 +272,20 @@ LSSnapshot::SetItem(const nsAString& aKe
 
     rv = UpdateUsage(delta);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     mValues.Put(aKey, nsString(aValue));
 
+    if (oldValue.IsVoid() && mLoadState == LoadState::Partial) {
+      mLength++;
+    }
+
     LSSetItemInfo setItemInfo;
     setItemInfo.key() = aKey;
     setItemInfo.oldValue() = oldValue;
     setItemInfo.value() = aValue;
 
     mWriteInfos.AppendElement(std::move(setItemInfo));
   }
 
@@ -214,16 +319,20 @@ LSSnapshot::RemoveItem(const nsAString& 
     int64_t delta = -(static_cast<int64_t>(aKey.Length()) +
                       static_cast<int64_t>(oldValue.Length()));
 
     DebugOnly<nsresult> rv = UpdateUsage(delta);
     MOZ_ASSERT(NS_SUCCEEDED(rv));
 
     mValues.Remove(aKey);
 
+    if (mLoadState == LoadState::Partial) {
+      mLength--;
+    }
+
     LSRemoveItemInfo removeItemInfo;
     removeItemInfo.key() = aKey;
     removeItemInfo.oldValue() = oldValue;
 
     mWriteInfos.AppendElement(std::move(removeItemInfo));
   }
 
   aNotifyInfo.changed() = changed;
@@ -235,18 +344,33 @@ LSSnapshot::RemoveItem(const nsAString& 
 nsresult
 LSSnapshot::Clear(LSNotifyInfo& aNotifyInfo)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  uint32_t length;
+  if (mLoadState == LoadState::Partial) {
+    length = mLength;
+    MOZ_ASSERT(length);
+
+    MOZ_ALWAYS_TRUE(mActor->SendLoaded());
+
+    mLoadedItems.Clear();
+    mUnknownItems.Clear();
+    mLength = 0;
+    mLoadState = LoadState::AllOrderedItems;
+  } else {
+    length = mValues.Count();
+  }
+
   bool changed;
-  if (!mValues.Count()) {
+  if (!length) {
     changed = false;
   } else {
     changed = true;
 
     DebugOnly<nsresult> rv = UpdateUsage(-mExactUsage);
     MOZ_ASSERT(NS_SUCCEEDED(rv));
 
     mValues.Clear();
@@ -257,16 +381,122 @@ LSSnapshot::Clear(LSNotifyInfo& aNotifyI
   }
 
   aNotifyInfo.changed() = changed;
 
   return NS_OK;
 }
 
 nsresult
+LSSnapshot::Finish()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mDatabase);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  MOZ_ALWAYS_TRUE(mActor->SendFinish(mWriteInfos));
+
+#ifdef DEBUG
+  mSentFinish = true;
+#endif
+
+  if (mExplicit) {
+    if (NS_WARN_IF(!mActor->SendPing())) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  mDatabase->NoteFinishedSnapshot(this);
+
+  if (mExplicit) {
+    mSelfRef = nullptr;
+  } else {
+    MOZ_ASSERT(!mSelfRef);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSSnapshot::EnsureAllKeys()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+  MOZ_ASSERT(mLoadState != LoadState::Initial);
+
+  if (mLoadState == LoadState::AllOrderedKeys ||
+      mLoadState == LoadState::AllOrderedItems) {
+    return NS_OK;
+  }
+
+  nsTArray<nsString> keys;
+  if (NS_WARN_IF(!mActor->SendLoadKeys(&keys))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsDataHashtable<nsStringHashKey, nsString> newValues;
+
+  for (auto key : keys) {
+    newValues.Put(key, VoidString());
+  }
+
+  for (uint32_t index = 0; index < mWriteInfos.Length(); index++) {
+    const LSWriteInfo& writeInfo = mWriteInfos[index];
+
+    switch (writeInfo.type()) {
+      case LSWriteInfo::TLSSetItemInfo: {
+        newValues.Put(writeInfo.get_LSSetItemInfo().key(), VoidString());
+        break;
+      }
+      case LSWriteInfo::TLSRemoveItemInfo: {
+        newValues.Remove(writeInfo.get_LSRemoveItemInfo().key());
+        break;
+      }
+      case LSWriteInfo::TLSClearInfo: {
+        newValues.Clear();
+        break;
+      }
+
+      default:
+        MOZ_CRASH("Should never get here!");
+    }
+  }
+
+  MOZ_ASSERT_IF(mLoadState == LoadState::AllUnorderedItems,
+                newValues.Count() == mValues.Count());
+
+  for (auto iter = newValues.Iter(); !iter.Done(); iter.Next()) {
+    nsString value;
+    if (mValues.Get(iter.Key(), &value)) {
+      iter.Data() = value;
+    }
+  }
+
+  mValues.SwapElements(newValues);
+
+  if (mLoadState == LoadState::Partial) {
+    mUnknownItems.Clear();
+    mLength = 0;
+    mLoadState = LoadState::AllOrderedKeys;
+  } else {
+    MOZ_ASSERT(mLoadState == LoadState::AllUnorderedItems);
+
+    MOZ_ASSERT(mUnknownItems.Count() == 0);
+    MOZ_ASSERT(mLength == 0);
+    mLoadState = LoadState::AllOrderedItems;
+  }
+
+  return NS_OK;
+}
+
+nsresult
 LSSnapshot::UpdateUsage(int64_t aDelta)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mDatabase);
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mPeakUsage >= mExactUsage);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
@@ -296,25 +526,17 @@ LSSnapshot::UpdateUsage(int64_t aDelta)
 }
 
 NS_IMPL_ISUPPORTS(LSSnapshot, nsIRunnable)
 
 NS_IMETHODIMP
 LSSnapshot::Run()
 {
   AssertIsOnOwningThread();
-  MOZ_ASSERT(mDatabase);
-  MOZ_ASSERT(mActor);
-  MOZ_ASSERT(!mSentFinish);
-
-  MOZ_ALWAYS_TRUE(mActor->SendFinish(mWriteInfos));
+  MOZ_ASSERT(!mExplicit);
 
-#ifdef DEBUG
-  mSentFinish = true;
-#endif
-
-  mDatabase->NoteFinishedSnapshot(this);
+  MOZ_ALWAYS_SUCCEEDS(Finish());
 
   return NS_OK;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/LSSnapshot.h
+++ b/dom/localstorage/LSSnapshot.h
@@ -10,26 +10,44 @@
 namespace mozilla {
 namespace dom {
 
 class LSSnapshotChild;
 
 class LSSnapshot final
   : public nsIRunnable
 {
+  enum class LoadState
+  {
+    Initial,
+    Partial,
+    AllOrderedKeys,
+    AllUnorderedItems,
+    AllOrderedItems
+  };
+
+  RefPtr<LSSnapshot> mSelfRef;
+
   RefPtr<LSDatabase> mDatabase;
 
   LSSnapshotChild* mActor;
 
+  nsTHashtable<nsStringHashKey> mLoadedItems;
+  nsTHashtable<nsStringHashKey> mUnknownItems;
   nsDataHashtable<nsStringHashKey, nsString> mValues;
   nsTArray<LSWriteInfo> mWriteInfos;
 
+  uint32_t mInitLength;
+  uint32_t mLength;
   int64_t mExactUsage;
   int64_t mPeakUsage;
 
+  LoadState mLoadState;
+  bool mExplicit;
+
 #ifdef DEBUG
   bool mInitialized;
   bool mSentFinish;
 #endif
 
 public:
   explicit LSSnapshot(LSDatabase* aDatabase);
 
@@ -46,18 +64,25 @@ public:
   ClearActor()
   {
     AssertIsOnOwningThread();
     MOZ_ASSERT(mActor);
 
     mActor = nullptr;
   }
 
+  bool
+  Explicit() const
+  {
+    return mExplicit;
+  }
+
   nsresult
-  Init(const LSSnapshotInitInfo& aInitInfo);
+  Init(const LSSnapshotInitInfo& aInitInfo,
+       bool aExplicit);
 
   nsresult
   GetLength(uint32_t* aResult);
 
   nsresult
   GetKey(uint32_t aIndex,
          nsAString& aResult);
 
@@ -75,20 +100,26 @@ public:
 
   nsresult
   RemoveItem(const nsAString& aKey,
              LSNotifyInfo& aNotifyInfo);
 
   nsresult
   Clear(LSNotifyInfo& aNotifyInfo);
 
+  nsresult
+  Finish();
+
 private:
   ~LSSnapshot();
 
   nsresult
+  EnsureAllKeys();
+
+  nsresult
   UpdateUsage(int64_t aDelta);
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIRUNNABLE
 };
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/PBackgroundLSDatabase.ipdl
+++ b/dom/localstorage/PBackgroundLSDatabase.ipdl
@@ -12,16 +12,17 @@ struct LSItemInfo
 {
   nsString key;
   nsString value;
 };
 
 struct LSSnapshotInitInfo
 {
   LSItemInfo[] itemInfos;
+  uint32_t totalLength;
   int64_t initialUsage;
   int64_t peakUsage;
 };
 
 sync protocol PBackgroundLSDatabase
 {
   manager PBackground;
   manages PBackgroundLSSnapshot;
--- a/dom/localstorage/PBackgroundLSSnapshot.ipdl
+++ b/dom/localstorage/PBackgroundLSSnapshot.ipdl
@@ -41,17 +41,30 @@ sync protocol PBackgroundLSSnapshot
 {
   manager PBackgroundLSDatabase;
 
 parent:
   async DeleteMe();
 
   async Finish(LSSnapshotFinishInfo finishInfo);
 
+  async Loaded();
+
+  sync LoadItem(nsString key)
+    returns (nsString value);
+
+  sync LoadKeys()
+    returns (nsString[] keys);
+
   sync IncreasePeakUsage(int64_t requestedSize, int64_t minSize)
     returns (int64_t size);
 
+  // A synchronous ping to the parent actor to confirm that the parent actor
+  // has received previous async message. This should only be used by the
+  // snapshotting code to end an explicit snapshot.
+  sync Ping();
+
 child:
   async __delete__();
 };
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/test/unit/test_snapshotting.js
+++ b/dom/localstorage/test/unit/test_snapshotting.js
@@ -9,42 +9,235 @@ function* testSteps()
 {
   const url = "http://example.com";
 
   const items = [
     { key: "key1", value: "value1" },
     { key: "key2", value: "value2" },
     { key: "key3", value: "value3" },
     { key: "key4", value: "value4" },
-    { key: "key5", value: "value5" }
+    { key: "key5", value: "value5" },
+    { key: "key6", value: "value6" },
+    { key: "key7", value: "value7" },
+    { key: "key8", value: "value8" },
+    { key: "key9", value: "value9" },
+    { key: "key10", value: "value10" }
   ];
 
-  info("Getting storage");
-
-  let storage = getLocalStorage(getPrincipal(url));
-
-  info("Adding data");
-
-  for (let item of items) {
-    storage.setItem(item.key, item.value);
+  function getPartialPrefill()
+  {
+    let size = 0;
+    for (let i = 0; i < items.length / 2; i++) {
+      let item = items[i];
+      size += item.key.length + item.value.length;
+    }
+    return size;
   }
 
-  info("Saving key order");
+  const prefillValues = [
+    0,                   // no prefill
+    getPartialPrefill(), // partial prefill
+    -1,                  // full prefill
+  ];
+
+  for (let prefillValue of prefillValues) {
+    info("Setting prefill value");
+
+    Services.prefs.setIntPref("dom.storage.snapshot_prefill", prefillValue);
+
+    info("Getting storage");
+
+    let storage = getLocalStorage(getPrincipal(url));
+
+    // 1st snapshot
+
+    info("Adding data");
+
+    for (let item of items) {
+      storage.setItem(item.key, item.value);
+    }
+
+    info("Saving key order");
+
+    // This forces GetKeys to be called internally.
+    let savedKeys = Object.keys(storage);
+
+    // GetKey should match GetKeys
+    for (let i = 0; i < savedKeys.length; i++) {
+      is(storage.key(i), savedKeys[i], "Correct key");
+    }
+
+    info("Returning to event loop");
+
+    // Returning to event loop forces the internal snapshot to finish.
+    continueToNextStep();
+    yield undefined;
+
+    // 2nd snapshot
+
+    info("Verifying length");
+
+    is(storage.length, items.length, "Correct length");
+
+    info("Verifying key order");
+
+    let keys = Object.keys(storage);
+
+    is(keys.length, savedKeys.length);
 
-  let keys = [];
-  for (let i = 0; i < items.length; i++) {
-    keys.push(storage.key(i));
-  }
+    for (let i = 0; i < keys.length; i++) {
+      is(keys[i], savedKeys[i], "Correct key");
+    }
+
+    info("Verifying values");
+
+    for (let item of items) {
+      is(storage.getItem(item.key), item.value, "Correct value");
+    }
+
+    info("Returning to event loop");
+
+    continueToNextStep();
+    yield undefined;
+
+    // 3rd snapshot
+
+    // Force key2 to load.
+    storage.getItem("key2");
+
+    // Fill out write infos a bit.
+    storage.removeItem("key5");
+    storage.setItem("key5", "value5");
+    storage.removeItem("key5");
+    storage.setItem("key11", "value11");
+    storage.setItem("key5", "value5");
+
+    items.push({ key: "key11", value: "value11" });
+
+    info("Verifying length");
+
+    is(storage.length, items.length, "Correct length");
+
+    // This forces to get all keys from the parent and then apply write infos
+    // on already cached values.
+    savedKeys = Object.keys(storage);
+
+    info("Verifying values");
+
+    for (let item of items) {
+      is(storage.getItem(item.key), item.value, "Correct value");
+    }
+
+    storage.removeItem("key11");
+
+    items.pop();
+
+    info("Returning to event loop");
+
+    continueToNextStep();
+    yield undefined;
 
-  // Let the internal snapshot finish by returning to the event loop.
-  continueToNextStep();
-  yield undefined;
+    // 4th snapshot
+
+    // Force loading of all items.
+    info("Verifying length");
+
+    is(storage.length, items.length, "Correct length");
+
+    info("Verifying values");
+
+    for (let item of items) {
+      is(storage.getItem(item.key), item.value, "Correct value");
+    }
+
+    is(storage.getItem("key11"), null, "Correct value");
+
+    info("Returning to event loop");
+
+    continueToNextStep();
+    yield undefined;
+
+    // 5th snapshot
+
+    // Force loading of all keys.
+    info("Saving key order");
+
+    savedKeys = Object.keys(storage);
+
+    // Force loading of all items.
+    info("Verifying length");
+
+    is(storage.length, items.length, "Correct length");
+
+    info("Verifying values");
+
+    for (let item of items) {
+      is(storage.getItem(item.key), item.value, "Correct value");
+    }
+
+    is(storage.getItem("key11"), null, "Correct value");
+
+    info("Returning to event loop");
+
+    continueToNextStep();
+    yield undefined;
+
+    // 6th snapshot
+    info("Verifying unknown item");
+
+    is(storage.getItem("key11"), null, "Correct value");
 
-  is(storage.length, items.length, "Correct length");
+    info("Verifying unknown item again");
+
+    is(storage.getItem("key11"), null, "Correct value");
+
+    info("Returning to event loop");
+
+    continueToNextStep();
+    yield undefined;
+
+    // 7th snapshot
+
+    // Save actual key order.
+    info("Saving key order");
+
+    savedKeys = Object.keys(storage);
+
+    continueToNextStep();
+    yield undefined;
+
+    // 8th snapshot
+
+    // Force loading of all items, but in reverse order.
+    info("Getting values");
 
-  info("Verifying key order");
+    for (let i = items.length - 1; i >= 0; i--) {
+      let item = items[i];
+      storage.getItem(item.key);
+    }
+
+    info("Verifying key order");
+
+    keys = Object.keys(storage);
+
+    is(keys.length, savedKeys.length);
 
-  for (let i = 0; i < items.length; i++) {
-    is(storage.key(i), keys[i], "Correct key");
+    for (let i = 0; i < keys.length; i++) {
+      is(keys[i], savedKeys[i], "Correct key");
+    }
+
+    continueToNextStep();
+    yield undefined;
+
+    // 9th snapshot
+
+    info("Clearing");
+
+    storage.clear();
+
+    info("Returning to event loop");
+
+    continueToNextStep();
+    yield undefined;
   }
 
   finishTest();
 }
--- a/dom/storage/Storage.h
+++ b/dom/storage/Storage.h
@@ -113,16 +113,24 @@ public:
   virtual void
   Open(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv)
   { }
 
   virtual void
   Close(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv)
   { }
 
+  virtual void
+  BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv)
+  { }
+
+  virtual void
+  EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv)
+  { }
+
   // aStorage can be null if this method is called by LocalStorageCacheChild.
   //
   // aImmediateDispatch is for use by child IPC code (LocalStorageCacheChild)
   // so that PBackground ordering can be maintained.  Without this, the event
   // would be/ enqueued and run in a future turn of the event loop, potentially
   // allowing other PBackground Recv* methods to trigger script that wants to
   // assume our localstorage changes have already been applied.  This is the
   // case for message manager messages which are used by ContentTask testing
--- a/dom/webidl/Storage.webidl
+++ b/dom/webidl/Storage.webidl
@@ -36,9 +36,15 @@ interface Storage {
 
 // Testing only.
 partial interface Storage {
   [Throws, NeedsSubjectPrincipal, Pref="dom.storage.testing"]
   void open();
 
   [Throws, NeedsSubjectPrincipal, Pref="dom.storage.testing"]
   void close();
+
+  [Throws, NeedsSubjectPrincipal, Pref="dom.storage.testing"]
+  void beginExplicitSnapshot();
+
+  [Throws, NeedsSubjectPrincipal, Pref="dom.storage.testing"]
+  void endExplicitSnapshot();
 };
--- a/ipc/ipdl/sync-messages.ini
+++ b/ipc/ipdl/sync-messages.ini
@@ -919,18 +919,24 @@ description = See Bug 1505976 - investig
 [PVideoDecoderManager::PVideoDecoder]
 description =
 [PVideoDecoderManager::Readback]
 description =
 [PBackgroundStorage::Preload]
 description =
 [PBackgroundLSDatabase::PBackgroundLSSnapshot]
 description =
+[PBackgroundLSSnapshot::LoadItem]
+description =
+[PBackgroundLSSnapshot::LoadKeys]
+description =
 [PBackgroundLSSnapshot::IncreasePeakUsage]
 description =
+[PBackgroundLSSnapshot::Ping]
+description = See corresponding comment in PBackgroundLSSnapshot.ipdl
 [PRemoteSpellcheckEngine::Check]
 description =
 [PRemoteSpellcheckEngine::CheckAndSuggest]
 description =
 [PRemoteSpellcheckEngine::SetDictionary]
 description =
 [PGPU::AddLayerTreeIdMapping]
 description =
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1287,16 +1287,17 @@ pref("dom.serviceWorkers.disable_open_cl
 pref("dom.storage.enabled", true);
 // Whether or not LSNG (Next Generation Local Storage) is enabled.
 #ifdef NIGHTLY_BUILD
 pref("dom.storage.next_gen", true);
 #else
 pref("dom.storage.next_gen", false);
 #endif
 pref("dom.storage.default_quota",      5120);
+pref("dom.storage.snapshot_prefill", 4096);
 pref("dom.storage.testing", false);
 
 pref("dom.send_after_paint_to_content", false);
 
 // Timeout clamp in ms for timeouts we clamp
 pref("dom.min_timeout_value", 4);
 // And for background windows
 pref("dom.min_background_timeout_value", 1000);