Bug 1286798 - Part 42: Implement snapshot reusing; r=asuth
authorJan Varga <jan.varga@gmail.com>
Thu, 29 Nov 2018 21:49:31 +0100
changeset 505260 398f80b485a969faead8677260975d1b6e2f4606
parent 505259 270fc081f01f49ebc32dfc9cad99e937e2fba4a2
child 505261 5dbbe4015d16797f33ae37207b526dc81b89d50b
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth
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 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
@@ -1545,17 +1545,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)
 
@@ -1577,16 +1577,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
@@ -1819,24 +1822,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,
@@ -1846,52 +1850,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;
 
@@ -4267,25 +4280,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)
 {
@@ -4301,52 +4317,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);
@@ -4476,16 +4496,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);
@@ -4794,24 +4829,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);
@@ -4820,16 +4856,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;
@@ -4838,29 +4876,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);
@@ -4868,36 +4930,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(),
@@ -4923,17 +4980,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
@@ -1288,16 +1288,17 @@ 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", 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);