Bug 1286798 - Part 42: Implement snapshot reusing; r=asuth draft
authorJan Varga <jan.varga@gmail.com>
Wed, 24 Oct 2018 06:59:08 +0200
changeset 481720 564e6a8da37f657c000ee3ab771874d1d7766b8e
parent 481719 fb2a73c881f8990d814f5bba57f3c70423050dd4
child 481721 413cee18d1f90f3d668495e7bae4c36a72540cf3
push id10
push userbugmail@asutherland.org
push dateSun, 18 Nov 2018 18:57:42 +0000
reviewersasuth
bugs1286798
milestone65.0a1
Bug 1286798 - Part 42: Implement snapshot reusing; r=asuth This improves performance by keeping snapshots around for some time if there are no changes done by other processes. If a snapshot is not destroyed immediately after getting into the stable state then there's a chance that it won't have to be synchronously created again when a new opeartion is requested.
browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
dom/localstorage/ActorsChild.cpp
dom/localstorage/ActorsChild.h
dom/localstorage/ActorsParent.cpp
dom/localstorage/LSDatabase.cpp
dom/localstorage/LSSnapshot.cpp
dom/localstorage/LSSnapshot.h
dom/localstorage/PBackgroundLSSnapshot.ipdl
dom/localstorage/test/unit/test_eviction.js
dom/localstorage/test/unit/test_groupLimit.js
dom/localstorage/test/unit/test_snapshotting.js
modules/libpref/init/all.js
--- a/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
@@ -55,16 +55,19 @@ add_task(async function testLocalStorage
     await browser.browsingData.removeLocalStorage({});
     await sendMessageToTabs(tabs, "checkLocalStorageCleared");
 
     await sendMessageToTabs(tabs, "resetLocalStorage");
     await sendMessageToTabs(tabs, "checkLocalStorageSet");
     await browser.browsingData.remove({}, {localStorage: true});
     await sendMessageToTabs(tabs, "checkLocalStorageCleared");
 
+    // Cleanup (checkLocalStorageCleared creates empty LS databases).
+    await browser.browsingData.removeLocalStorage({});
+
     browser.tabs.remove(tabs.map(tab => tab.id));
 
     browser.test.notifyPass("done");
   }
 
   function contentScript() {
     browser.runtime.onMessage.addListener(msg => {
       if (msg === "resetLocalStorage") {
--- a/dom/localstorage/ActorsChild.cpp
+++ b/dom/localstorage/ActorsChild.cpp
@@ -313,10 +313,24 @@ LSSnapshotChild::ActorDestroy(ActorDestr
   if (mSnapshot) {
     mSnapshot->ClearActor();
 #ifdef DEBUG
     mSnapshot = nullptr;
 #endif
   }
 }
 
+mozilla::ipc::IPCResult
+LSSnapshotChild::RecvMarkDirty()
+{
+  AssertIsOnOwningThread();
+
+  if (!mSnapshot) {
+    return IPC_OK();
+  }
+
+  mSnapshot->MarkDirty();
+
+  return IPC_OK();
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/ActorsChild.h
+++ b/dom/localstorage/ActorsChild.h
@@ -241,14 +241,17 @@ private:
   ~LSSnapshotChild();
 
   void
   SendDeleteMeInternal();
 
   // IPDL methods are only called by IPDL.
   void
   ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  RecvMarkDirty() override;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_localstorage_ActorsChild_h
--- a/dom/localstorage/ActorsParent.cpp
+++ b/dom/localstorage/ActorsParent.cpp
@@ -1526,17 +1526,17 @@ public:
         const nsString& aDocumentURI);
 
   void
   PrivateBrowsingClear();
 
   void
   BeginUpdateBatch(int64_t aSnapshotInitialUsage);
 
-  void
+  int64_t
   EndUpdateBatch(int64_t aSnapshotPeakUsage);
 
   int64_t
   RequestUpdateUsage(int64_t aRequestedSize,
                      int64_t aMinSize);
 
   NS_INLINE_DECL_REFCOUNTING(Datastore)
 
@@ -1558,16 +1558,19 @@ private:
 
   void
   NotifySnapshots(Database* aDatabase,
                   const nsAString& aKey,
                   const nsAString& aOldValue,
                   bool aAffectsOrder);
 
   void
+  MarkSnapshotsDirty();
+
+  void
   NotifyObservers(Database* aDatabase,
                   const nsString& aDocumentURI,
                   const nsString& aKey,
                   const nsString& aOldValue,
                   const nsString& aNewValue);
 };
 
 class PreparedDatastore
@@ -1800,24 +1803,25 @@ class Snapshot final
   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 mUsage;
   int64_t mPeakUsage;
   bool mSavedKeys;
   bool mActorDestroyed;
   bool mFinishReceived;
   bool mLoadedReceived;
   bool mLoadedAllItems;
   bool mLoadKeysReceived;
+  bool mSentMarkDirty;
 
 public:
   // Created in AllocPBackgroundLSSnapshotParent.
   Snapshot(Database* aDatabase,
            const nsAString& aDocumentURI);
 
   void
   Init(nsTHashtable<nsStringHashKey>& aLoadedItems,
@@ -1827,52 +1831,61 @@ public:
        LSSnapshot::LoadState aLoadState)
   {
     AssertIsOnBackgroundThread();
     MOZ_ASSERT(aInitialUsage >= 0);
     MOZ_ASSERT(aPeakUsage >= aInitialUsage);
     MOZ_ASSERT_IF(aLoadState == LSSnapshot::LoadState::AllOrderedItems,
                   aLoadedItems.Count() == 0);
     MOZ_ASSERT(mTotalLength == 0);
-    MOZ_ASSERT(mInitialUsage == -1);
+    MOZ_ASSERT(mUsage == -1);
     MOZ_ASSERT(mPeakUsage == -1);
 
     mLoadedItems.SwapElements(aLoadedItems);
     mTotalLength = aTotalLength;
-    mInitialUsage = aInitialUsage;
+    mUsage = aInitialUsage;
     mPeakUsage = aPeakUsage;
     if (aLoadState == LSSnapshot::LoadState::AllOrderedKeys) {
       mLoadKeysReceived = true;
     } else if (aLoadState == LSSnapshot::LoadState::AllOrderedItems) {
       mLoadedReceived = true;
       mLoadedAllItems = true;
       mLoadKeysReceived = true;
     }
   }
 
   void
   SaveItem(const nsAString& aKey,
            const nsAString& aOldValue,
            bool aAffectsOrder);
 
+  void
+  MarkDirty();
+
   NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot)
 
 private:
   // Reference counted.
   ~Snapshot();
 
+  void
+  Finish();
+
   // IPDL methods are only called by IPDL.
   void
   ActorDestroy(ActorDestroyReason aWhy) override;
 
   mozilla::ipc::IPCResult
   RecvDeleteMe() override;
 
   mozilla::ipc::IPCResult
-  RecvFinish(const LSSnapshotFinishInfo& aFinishInfo) override;
+  RecvCheckpoint(nsTArray<LSWriteInfo>&& aWriteInfos) override;
+
+  mozilla::ipc::IPCResult
+  RecvFinish() override;
 
   mozilla::ipc::IPCResult
   RecvLoaded() override;
 
   mozilla::ipc::IPCResult
   RecvLoadItem(const nsString& aKey,
                nsString* aValue) override;
 
@@ -4252,25 +4265,28 @@ Datastore::Clear(Database* aDatabase,
 }
 
 void
 Datastore::PrivateBrowsingClear()
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(mPrivateBrowsingId);
   MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(!mInUpdateBatch);
 
   if (mValues.Count()) {
-    DebugOnly<bool> ok = UpdateUsage(-mUsage);
-    MOZ_ASSERT(ok);
+    MarkSnapshotsDirty();
 
     mValues.Clear();
 
     mOrderedItems.Clear();
 
+    DebugOnly<bool> ok = UpdateUsage(-mSizeOfItems);
+    MOZ_ASSERT(ok);
+
     mSizeOfKeys = 0;
     mSizeOfItems = 0;
   }
 }
 
 void
 Datastore::BeginUpdateBatch(int64_t aSnapshotInitialUsage)
 {
@@ -4286,52 +4302,56 @@ Datastore::BeginUpdateBatch(int64_t aSna
     mConnection->BeginUpdateBatch();
   }
 
 #ifdef DEBUG
   mInUpdateBatch = true;
 #endif
 }
 
-void
+int64_t
 Datastore::EndUpdateBatch(int64_t aSnapshotPeakUsage)
 {
   AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aSnapshotPeakUsage >= 0);
   MOZ_ASSERT(!mClosed);
   MOZ_ASSERT(mInUpdateBatch);
 
   mWriteOptimizer.ApplyWrites(mOrderedItems);
 
-  int64_t delta = mUpdateBatchUsage - aSnapshotPeakUsage;
-
-  if (mActiveDatabases.Count()) {
-    // We can't apply deltas while other databases are still active.
-    // The final delta must be zero or negative, but individual deltas can be
-    // positive. A positive delta can't be applied asynchronously since there's
-    // no way to fire the quota exceeded error event.
-
-    mPendingUsageDeltas.AppendElement(delta);
-  } else {
-    MOZ_ASSERT(delta <= 0);
-    if (delta != 0) {
-      DebugOnly<bool> ok = UpdateUsage(delta);
-      MOZ_ASSERT(ok);
+  if (aSnapshotPeakUsage >= 0) {
+    int64_t delta = mUpdateBatchUsage - aSnapshotPeakUsage;
+
+    if (mActiveDatabases.Count()) {
+      // We can't apply deltas while other databases are still active.
+      // The final delta must be zero or negative, but individual deltas can
+      // be positive. A positive delta can't be applied asynchronously since
+      // there's no way to fire the quota exceeded error event.
+
+      mPendingUsageDeltas.AppendElement(delta);
+    } else {
+      MOZ_ASSERT(delta <= 0);
+      if (delta != 0) {
+        DebugOnly<bool> ok = UpdateUsage(delta);
+        MOZ_ASSERT(ok);
+      }
     }
   }
 
+  int64_t result = mUpdateBatchUsage;
   mUpdateBatchUsage = -1;
 
   if (IsPersistent()) {
     mConnection->EndUpdateBatch();
   }
 
 #ifdef DEBUG
   mInUpdateBatch = false;
 #endif
+
+  return result;
 }
 
 int64_t
 Datastore::RequestUpdateUsage(int64_t aRequestedSize,
                               int64_t aMinSize)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aRequestedSize > 0);
@@ -4461,16 +4481,31 @@ Datastore::NotifySnapshots(Database* aDa
     Snapshot* snapshot = database->GetSnapshot();
     if (snapshot) {
       snapshot->SaveItem(aKey, aOldValue, aAffectsOrder);
     }
   }
 }
 
 void
+Datastore::MarkSnapshotsDirty()
+{
+  AssertIsOnBackgroundThread();
+
+  for (auto iter = mDatabases.ConstIter(); !iter.Done(); iter.Next()) {
+    Database* database = iter.Get()->GetKey();
+
+    Snapshot* snapshot = database->GetSnapshot();
+    if (snapshot) {
+      snapshot->MarkDirty();
+    }
+  }
+}
+
+void
 Datastore::NotifyObservers(Database* aDatabase,
                            const nsString& aDocumentURI,
                            const nsString& aKey,
                            const nsString& aOldValue,
                            const nsString& aNewValue)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabase);
@@ -4779,24 +4814,25 @@ Database::DeallocPBackgroundLSSnapshotPa
  ******************************************************************************/
 
 Snapshot::Snapshot(Database* aDatabase,
                    const nsAString& aDocumentURI)
   : mDatabase(aDatabase)
   , mDatastore(aDatabase->GetDatastore())
   , mDocumentURI(aDocumentURI)
   , mTotalLength(0)
-  , mInitialUsage(-1)
+  , mUsage(-1)
   , mPeakUsage(-1)
   , mSavedKeys(false)
   , mActorDestroyed(false)
   , mFinishReceived(false)
   , mLoadedReceived(false)
   , mLoadedAllItems(false)
   , mLoadKeysReceived(false)
+  , mSentMarkDirty(false)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabase);
 }
 
 Snapshot::~Snapshot()
 {
   MOZ_ASSERT(mActorDestroyed);
@@ -4805,16 +4841,18 @@ Snapshot::~Snapshot()
 
 void
 Snapshot::SaveItem(const nsAString& aKey,
                    const nsAString& aOldValue,
                    bool aAffectsOrder)
 {
   AssertIsOnBackgroundThread();
 
+  MarkDirty();
+
   if (mLoadedAllItems) {
     return;
   }
 
   if (!mLoadedItems.GetEntry(aKey) && !mUnknownItems.GetEntry(aKey)) {
     nsString oldValue(aOldValue);
     mValues.LookupForAdd(aKey).OrInsert([oldValue]() {
       return oldValue;
@@ -4823,29 +4861,53 @@ Snapshot::SaveItem(const nsAString& aKey
 
   if (aAffectsOrder && !mSavedKeys && !mLoadKeysReceived) {
     mDatastore->GetKeys(mKeys);
     mSavedKeys = true;
   }
 }
 
 void
+Snapshot::MarkDirty()
+{
+  AssertIsOnBackgroundThread();
+
+  if (!mSentMarkDirty) {
+    Unused << SendMarkDirty();
+    mSentMarkDirty = true;
+  }
+}
+
+void
+Snapshot::Finish()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mDatabase);
+  MOZ_ASSERT(mDatastore);
+  MOZ_ASSERT(!mFinishReceived);
+
+  mDatastore->BeginUpdateBatch(mUsage);
+
+  mDatastore->EndUpdateBatch(mPeakUsage);
+
+  mDatabase->UnregisterSnapshot(this);
+
+  mFinishReceived = true;
+}
+
+void
 Snapshot::ActorDestroy(ActorDestroyReason aWhy)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(!mActorDestroyed);
 
   mActorDestroyed = true;
 
   if (!mFinishReceived) {
-    mDatabase->UnregisterSnapshot(this);
-
-    mDatastore->BeginUpdateBatch(mInitialUsage);
-
-    mDatastore->EndUpdateBatch(mPeakUsage);
+    Finish();
   }
 }
 
 mozilla::ipc::IPCResult
 Snapshot::RecvDeleteMe()
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(!mActorDestroyed);
@@ -4853,36 +4915,31 @@ Snapshot::RecvDeleteMe()
   IProtocol* mgr = Manager();
   if (!PBackgroundLSSnapshotParent::Send__delete__(this)) {
     return IPC_FAIL_NO_REASON(mgr);
   }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
-Snapshot::RecvFinish(const LSSnapshotFinishInfo& aFinishInfo)
+Snapshot::RecvCheckpoint(nsTArray<LSWriteInfo>&& aWriteInfos)
 {
   AssertIsOnBackgroundThread();
-  MOZ_ASSERT(mInitialUsage >= 0);
-  MOZ_ASSERT(mPeakUsage >= mInitialUsage);
-
-  if (NS_WARN_IF(mFinishReceived)) {
+  MOZ_ASSERT(mUsage >= 0);
+  MOZ_ASSERT(mPeakUsage >= mUsage);
+
+  if (NS_WARN_IF(aWriteInfos.IsEmpty())) {
     ASSERT_UNLESS_FUZZING();
     return IPC_FAIL_NO_REASON(this);
   }
 
-  mFinishReceived = true;
-
-  mDatabase->UnregisterSnapshot(this);
-
-  mDatastore->BeginUpdateBatch(mInitialUsage);
-
-  const nsTArray<LSWriteInfo>& writeInfos = aFinishInfo.writeInfos();
-  for (uint32_t index = 0; index < writeInfos.Length(); index++) {
-    const LSWriteInfo& writeInfo = writeInfos[index];
+  mDatastore->BeginUpdateBatch(mUsage);
+
+  for (uint32_t index = 0; index < aWriteInfos.Length(); index++) {
+    const LSWriteInfo& writeInfo = aWriteInfos[index];
     switch (writeInfo.type()) {
       case LSWriteInfo::TLSSetItemInfo: {
         const LSSetItemInfo& info = writeInfo.get_LSSetItemInfo();
 
         mDatastore->SetItem(mDatabase,
                             mDocumentURI,
                             info.key(),
                             info.oldValue(),
@@ -4908,17 +4965,32 @@ Snapshot::RecvFinish(const LSSnapshotFin
         break;
       }
 
       default:
         MOZ_CRASH("Should never get here!");
     }
   }
 
-  mDatastore->EndUpdateBatch(mPeakUsage);
+  mUsage = mDatastore->EndUpdateBatch(-1);
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvFinish()
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  Finish();
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 Snapshot::RecvLoaded()
 {
   AssertIsOnBackgroundThread();
--- a/dom/localstorage/LSDatabase.cpp
+++ b/dom/localstorage/LSDatabase.cpp
@@ -69,17 +69,19 @@ LSDatabase::SetActor(LSDatabaseChild* aA
 void
 LSDatabase::RequestAllowToClose()
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(!mRequestedAllowToClose);
 
   mRequestedAllowToClose = true;
 
-  if (!mSnapshot) {
+  if (mSnapshot) {
+    mSnapshot->MarkDirty();
+  } else {
     AllowToClose();
   }
 }
 
 void
 LSDatabase::NoteFinishedSnapshot(LSSnapshot* aSnapshot)
 {
   AssertIsOnOwningThread();
@@ -280,17 +282,17 @@ LSDatabase::EndExplicitSnapshot(LSObject
   MOZ_ASSERT(!mAllowedToClose);
 
   if (!mSnapshot) {
     return NS_ERROR_NOT_INITIALIZED;
   }
 
   MOZ_ASSERT(mSnapshot->Explicit());
 
-  nsresult rv = mSnapshot->Finish();
+  nsresult rv = mSnapshot->End();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 nsresult
--- a/dom/localstorage/LSSnapshot.cpp
+++ b/dom/localstorage/LSSnapshot.cpp
@@ -6,37 +6,48 @@
 
 #include "LSSnapshot.h"
 
 #include "nsContentUtils.h"
 
 namespace mozilla {
 namespace dom {
 
+namespace {
+
+const uint32_t kSnapshotTimeoutMs = 20000;
+
+} // namespace
+
 LSSnapshot::LSSnapshot(LSDatabase* aDatabase)
   : mDatabase(aDatabase)
   , mActor(nullptr)
   , mInitLength(0)
   , mLength(0)
   , mExactUsage(0)
   , mPeakUsage(0)
   , mLoadState(LoadState::Initial)
   , mExplicit(false)
+  , mHasPendingStableStateCallback(false)
+  , mHasPendingTimerCallback(false)
+  , mDirty(false)
 #ifdef DEBUG
   , mInitialized(false)
   , mSentFinish(false)
 #endif
 {
   AssertIsOnOwningThread();
 }
 
 LSSnapshot::~LSSnapshot()
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mDatabase);
+  MOZ_ASSERT(!mHasPendingStableStateCallback);
+  MOZ_ASSERT(!mHasPendingTimerCallback);
   MOZ_ASSERT_IF(mInitialized, mSentFinish);
 
   if (mActor) {
     mActor->SendDeleteMeInternal();
     MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
   }
 }
 
@@ -50,27 +61,23 @@ LSSnapshot::SetActor(LSSnapshotChild* aA
   mActor = aActor;
 }
 
 nsresult
 LSSnapshot::Init(const LSSnapshotInitInfo& aInitInfo,
                  bool aExplicit)
 {
   AssertIsOnOwningThread();
+  MOZ_ASSERT(!mSelfRef);
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mLoadState == LoadState::Initial);
   MOZ_ASSERT(!mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
-  if (aExplicit) {
-    mSelfRef = this;
-  } else {
-    nsCOMPtr<nsIRunnable> runnable = this;
-    nsContentUtils::RunInStableState(runnable.forget());
-  }
+  mSelfRef = this;
 
   LoadState loadState = aInitInfo.loadState();
 
   const nsTArray<LSItemInfo>& itemInfos = aInitInfo.itemInfos();
   for (uint32_t i = 0; i < itemInfos.Length(); i++) {
     const LSItemInfo& itemInfo = itemInfos[i];
 
     const nsString& value = itemInfo.value();
@@ -97,27 +104,36 @@ LSSnapshot::Init(const LSSnapshotInitInf
   mLoadState = aInitInfo.loadState();
 
   mExplicit = aExplicit;
 
 #ifdef DEBUG
   mInitialized = true;
 #endif
 
+  if (!mExplicit) {
+    mTimer = NS_NewTimer();
+    MOZ_ASSERT(mTimer);
+
+    ScheduleStableStateCallback();
+  }
+
   return NS_OK;
 }
 
 nsresult
 LSSnapshot::GetLength(uint32_t* aResult)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  MaybeScheduleStableStateCallback();
+
   if (mLoadState == LoadState::Partial) {
     *aResult = mLength;
   } else {
     *aResult = mValues.Count();
   }
 
   return NS_OK;
 }
@@ -126,16 +142,18 @@ nsresult
 LSSnapshot::GetKey(uint32_t aIndex,
                    nsAString& aResult)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  MaybeScheduleStableStateCallback();
+
   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) {
@@ -152,16 +170,18 @@ nsresult
 LSSnapshot::GetItem(const nsAString& aKey,
                     nsAString& aResult)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  MaybeScheduleStableStateCallback();
+
   nsString result;
   nsresult rv = GetItemInternal(aKey, Optional<nsString>(), result);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   aResult = result;
   return NS_OK;
@@ -170,16 +190,18 @@ LSSnapshot::GetItem(const nsAString& aKe
 nsresult
 LSSnapshot::GetKeys(nsTArray<nsString>& aKeys)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  MaybeScheduleStableStateCallback();
+
   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());
   }
@@ -192,16 +214,18 @@ LSSnapshot::SetItem(const nsAString& aKe
                     const nsAString& aValue,
                     LSNotifyInfo& aNotifyInfo)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  MaybeScheduleStableStateCallback();
+
   nsString oldValue;
   nsresult rv =
     GetItemInternal(aKey, Optional<nsString>(nsString(aValue)), oldValue);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   bool changed;
@@ -249,16 +273,18 @@ nsresult
 LSSnapshot::RemoveItem(const nsAString& aKey,
                        LSNotifyInfo& aNotifyInfo)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  MaybeScheduleStableStateCallback();
+
   nsString oldValue;
   nsresult rv =
     GetItemInternal(aKey, Optional<nsString>(VoidString()), oldValue);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   bool changed;
@@ -293,16 +319,18 @@ LSSnapshot::RemoveItem(const nsAString& 
 nsresult
 LSSnapshot::Clear(LSNotifyInfo& aNotifyInfo)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
   MOZ_ASSERT(!mSentFinish);
 
+  MaybeScheduleStableStateCallback();
+
   uint32_t length;
   if (mLoadState == LoadState::Partial) {
     length = mLength;
     MOZ_ASSERT(length);
 
     MOZ_ALWAYS_TRUE(mActor->SendLoaded());
 
     mLoadedItems.Clear();
@@ -329,48 +357,99 @@ LSSnapshot::Clear(LSNotifyInfo& aNotifyI
     mWriteInfos.AppendElement(std::move(clearInfo));
   }
 
   aNotifyInfo.changed() = changed;
 
   return NS_OK;
 }
 
-nsresult
-LSSnapshot::Finish()
+void
+LSSnapshot::MarkDirty()
 {
   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;
-    }
+  if (mDirty) {
+    return;
   }
 
-  mDatabase->NoteFinishedSnapshot(this);
+  mDirty = true;
+
+  if (!mExplicit && !mHasPendingStableStateCallback) {
+    CancelTimer();
+
+    MOZ_ALWAYS_SUCCEEDS(Checkpoint());
+
+    MOZ_ALWAYS_SUCCEEDS(Finish());
+  } else {
+    MOZ_ASSERT(!mHasPendingTimerCallback);
+  }
+}
 
-  if (mExplicit) {
-    mSelfRef = nullptr;
-  } else {
-    MOZ_ASSERT(!mSelfRef);
+nsresult
+LSSnapshot::End()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mExplicit);
+  MOZ_ASSERT(!mHasPendingStableStateCallback);
+  MOZ_ASSERT(!mHasPendingTimerCallback);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  nsresult rv = Checkpoint();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  RefPtr<LSSnapshot> kungFuDeathGrip = this;
+
+  rv = Finish();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (NS_WARN_IF(!mActor->SendPing())) {
+    return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
+void
+LSSnapshot::ScheduleStableStateCallback()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mTimer);
+  MOZ_ASSERT(!mExplicit);
+  MOZ_ASSERT(!mHasPendingStableStateCallback);
+
+  CancelTimer();
+
+  nsCOMPtr<nsIRunnable> runnable = this;
+  nsContentUtils::RunInStableState(runnable.forget());
+
+  mHasPendingStableStateCallback = true;
+}
+
+void
+LSSnapshot::MaybeScheduleStableStateCallback()
+{
+  AssertIsOnOwningThread();
+
+  if (!mExplicit && !mHasPendingStableStateCallback) {
+    ScheduleStableStateCallback();
+  } else {
+    MOZ_ASSERT(!mHasPendingTimerCallback);
+  }
+}
+
 nsresult
 LSSnapshot::GetItemInternal(const nsAString& aKey,
                             const Optional<nsString>& aValue,
                             nsAString& aResult)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mActor);
   MOZ_ASSERT(mInitialized);
@@ -594,23 +673,113 @@ LSSnapshot::UpdateUsage(int64_t aDelta)
 
     mPeakUsage += size;
   }
 
   mExactUsage = newExactUsage;
   return NS_OK;
 }
 
+nsresult
+LSSnapshot::Checkpoint()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  if (!mWriteInfos.IsEmpty()) {
+    MOZ_ALWAYS_TRUE(mActor->SendCheckpoint(mWriteInfos));
+
+    mWriteInfos.Clear();
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSSnapshot::Finish()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mDatabase);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  MOZ_ALWAYS_TRUE(mActor->SendFinish());
+
+  mDatabase->NoteFinishedSnapshot(this);
+
+#ifdef DEBUG
+  mSentFinish = true;
+#endif
+
+  // Clear the self reference added in Init method.
+  MOZ_ASSERT(mSelfRef);
+  mSelfRef = nullptr;
+
+  return NS_OK;
+}
+
+void
+LSSnapshot::CancelTimer()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mTimer);
+
+  if (mHasPendingTimerCallback) {
+    MOZ_ALWAYS_SUCCEEDS(mTimer->Cancel());
+    mHasPendingTimerCallback = false;
+  }
+}
+
+// static
+void
+LSSnapshot::TimerCallback(nsITimer* aTimer, void* aClosure)
+{
+  MOZ_ASSERT(aTimer);
+
+  auto* self = static_cast<LSSnapshot*>(aClosure);
+  MOZ_ASSERT(self);
+  MOZ_ASSERT(self->mTimer);
+  MOZ_ASSERT(SameCOMIdentity(self->mTimer, aTimer));
+  MOZ_ASSERT(!self->mHasPendingStableStateCallback);
+  MOZ_ASSERT(self->mHasPendingTimerCallback);
+
+  self->mHasPendingTimerCallback = false;
+
+  MOZ_ALWAYS_SUCCEEDS(self->Finish());
+}
+
 NS_IMPL_ISUPPORTS(LSSnapshot, nsIRunnable)
 
 NS_IMETHODIMP
 LSSnapshot::Run()
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(!mExplicit);
+  MOZ_ASSERT(mHasPendingStableStateCallback);
+  MOZ_ASSERT(!mHasPendingTimerCallback);
 
-  MOZ_ALWAYS_SUCCEEDS(Finish());
+  mHasPendingStableStateCallback = false;
+
+  MOZ_ALWAYS_SUCCEEDS(Checkpoint());
+
+  if (mDirty || !Preferences::GetBool("dom.storage.snapshot_reusing")) {
+    MOZ_ALWAYS_SUCCEEDS(Finish());
+  } else if (!mExplicit) {
+    MOZ_ASSERT(mTimer);
+
+    MOZ_ALWAYS_SUCCEEDS(
+      mTimer->InitWithNamedFuncCallback(TimerCallback,
+                                        this,
+                                        kSnapshotTimeoutMs,
+                                        nsITimer::TYPE_ONE_SHOT,
+                                        "LSSnapshot::TimerCallback"));
+
+    mHasPendingTimerCallback = true;
+  }
 
   return NS_OK;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/LSSnapshot.h
+++ b/dom/localstorage/LSSnapshot.h
@@ -30,31 +30,36 @@ public:
     EndGuard
   };
 
 private:
   RefPtr<LSSnapshot> mSelfRef;
 
   RefPtr<LSDatabase> mDatabase;
 
+  nsCOMPtr<nsITimer> mTimer;
+
   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;
+  bool mHasPendingStableStateCallback;
+  bool mHasPendingTimerCallback;
+  bool mDirty;
 
 #ifdef DEBUG
   bool mInitialized;
   bool mSentFinish;
 #endif
 
 public:
   explicit LSSnapshot(LSDatabase* aDatabase);
@@ -108,33 +113,54 @@ public:
 
   nsresult
   RemoveItem(const nsAString& aKey,
              LSNotifyInfo& aNotifyInfo);
 
   nsresult
   Clear(LSNotifyInfo& aNotifyInfo);
 
+  void
+  MarkDirty();
+
   nsresult
-  Finish();
+  End();
 
 private:
   ~LSSnapshot();
 
+  void
+  ScheduleStableStateCallback();
+
+  void
+  MaybeScheduleStableStateCallback();
+
   nsresult
   GetItemInternal(const nsAString& aKey,
                   const Optional<nsString>& aValue,
                   nsAString& aResult);
 
   nsresult
   EnsureAllKeys();
 
   nsresult
   UpdateUsage(int64_t aDelta);
 
+  nsresult
+  Checkpoint();
+
+  nsresult
+  Finish();
+
+  void
+  CancelTimer();
+
+  static void
+  TimerCallback(nsITimer* aTimer, void* aClosure);
+
   NS_DECL_ISUPPORTS
   NS_DECL_NSIRUNNABLE
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_localstorage_LSSnapshot_h
--- a/dom/localstorage/PBackgroundLSSnapshot.ipdl
+++ b/dom/localstorage/PBackgroundLSSnapshot.ipdl
@@ -27,29 +27,26 @@ struct LSClearInfo
 
 union LSWriteInfo
 {
   LSSetItemInfo;
   LSRemoveItemInfo;
   LSClearInfo;
 };
 
-struct LSSnapshotFinishInfo
-{
-  LSWriteInfo[] writeInfos;
-};
-
 sync protocol PBackgroundLSSnapshot
 {
   manager PBackgroundLSDatabase;
 
 parent:
   async DeleteMe();
 
-  async Finish(LSSnapshotFinishInfo finishInfo);
+  async Checkpoint(LSWriteInfo[] writeInfos);
+
+  async Finish();
 
   async Loaded();
 
   sync LoadItem(nsString key)
     returns (nsString value);
 
   sync LoadKeys()
     returns (nsString[] keys);
@@ -58,13 +55,15 @@ parent:
     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 MarkDirty();
+
   async __delete__();
 };
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/test/unit/test_eviction.js
+++ b/dom/localstorage/test/unit/test_eviction.js
@@ -14,19 +14,20 @@ function* testSteps()
   data.key = "A";
   data.value = repeatChar(data.sizeKB * 1024 - data.key.length, ".");
   data.urlCount = globalLimitKB / data.sizeKB;
 
   function getSpec(index) {
     return "http://example" + index + ".com";
   }
 
-  info("Setting pref");
+  info("Setting prefs");
 
   Services.prefs.setBoolPref("dom.storage.next_gen", true);
+  Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
 
   info("Setting limits");
 
   setGlobalLimit(globalLimitKB);
 
   clear(continueToNextStepSync);
   yield undefined;
 
--- a/dom/localstorage/test/unit/test_groupLimit.js
+++ b/dom/localstorage/test/unit/test_groupLimit.js
@@ -21,16 +21,20 @@ function* testSteps()
   ];
 
   const data = {};
   data.sizeKB = 5 * 1024;
   data.key = "A";
   data.value = repeatChar(data.sizeKB * 1024 - data.key.length, ".");
   data.urlCount = groupLimitKB / data.sizeKB;
 
+  info("Setting pref");
+
+  Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+
   info("Setting limits");
 
   setGlobalLimit(globalLimitKB);
 
   clear(continueToNextStepSync);
   yield undefined;
 
   setOriginLimit(originLimit);
--- a/dom/localstorage/test/unit/test_snapshotting.js
+++ b/dom/localstorage/test/unit/test_snapshotting.js
@@ -33,16 +33,20 @@ function* testSteps()
   }
 
   const prefillValues = [
     0,                   // no prefill
     getPartialPrefill(), // partial prefill
     -1,                  // full prefill
   ];
 
+  info("Setting pref");
+
+  Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+
   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));
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1267,16 +1267,17 @@ pref("dom.serviceWorkers.disable_open_cl
 pref("dom.storage.enabled", true);
 #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", 16384);
+pref("dom.storage.snapshot_reusing", true);
 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);