Bug 1286798 - Part 29: Implement implicit snapshotting of databases; r=asuth,mccr8
authorJan Varga <jan.varga@gmail.com>
Thu, 29 Nov 2018 21:48:47 +0100
changeset 505247 70ba8b2410f833ec27655fb3cb6f69f7bb01fbcb
parent 505246 17a4f3ac425a1c687b5d238452a4a2740f301907
child 505248 e2b5bee9812c4b8f0f2344eb848d85696886151d
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, 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 29: Implement implicit snapshotting of databases; r=asuth,mccr8 This improves performance a lot in cases when multiple operations are invoked by a single JS function (number of sync IPC calls is reduced to a minimum). It also improves correctness since changes are not visible to other content processes until a JS function finishes. The patch implements core infrastructure, all items are sent to content when a snapshot is initialized and everything is fully working. However, sending of all items at once is not optimal for bigger databases. Support for lazy loading of items is implemented in a following patch.
dom/localstorage/ActorsChild.cpp
dom/localstorage/ActorsChild.h
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/LocalStorageCommon.h
dom/localstorage/PBackgroundLSDatabase.ipdl
dom/localstorage/PBackgroundLSSnapshot.ipdl
dom/localstorage/moz.build
dom/localstorage/test/unit/head.js
dom/localstorage/test/unit/test_groupLimit.js
ipc/ipdl/sync-messages.ini
--- a/dom/localstorage/ActorsChild.cpp
+++ b/dom/localstorage/ActorsChild.cpp
@@ -5,16 +5,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ActorsChild.h"
 
 #include "LocalStorageCommon.h"
 #include "LSDatabase.h"
 #include "LSObject.h"
 #include "LSObserver.h"
+#include "LSSnapshot.h"
 
 namespace mozilla {
 namespace dom {
 
 /*******************************************************************************
  * LSDatabaseChild
  ******************************************************************************/
 
@@ -61,27 +62,45 @@ LSDatabaseChild::ActorDestroy(ActorDestr
 }
 
 mozilla::ipc::IPCResult
 LSDatabaseChild::RecvRequestAllowToClose()
 {
   AssertIsOnOwningThread();
 
   if (mDatabase) {
-    mDatabase->AllowToClose();
+    mDatabase->RequestAllowToClose();
 
     // TODO: A new datastore will be prepared at first LocalStorage API
     //       synchronous call. It would be better to start preparing a new
     //       datastore right here, but asynchronously.
     //       However, we probably shouldn't do that if we are shutting down.
   }
 
   return IPC_OK();
 }
 
+PBackgroundLSSnapshotChild*
+LSDatabaseChild::AllocPBackgroundLSSnapshotChild(const nsString& aDocumentURI,
+                                                 const int64_t& aRequestedSize,
+                                                 LSSnapshotInitInfo* aInitInfo)
+{
+  MOZ_CRASH("PBackgroundLSSnapshotChild actor should be manually constructed!");
+}
+
+bool
+LSDatabaseChild::DeallocPBackgroundLSSnapshotChild(
+                                             PBackgroundLSSnapshotChild* aActor)
+{
+  MOZ_ASSERT(aActor);
+
+  delete aActor;
+  return true;
+}
+
 /*******************************************************************************
  * LSObserverChild
  ******************************************************************************/
 
 LSObserverChild::LSObserverChild(LSObserver* aObserver)
   : mObserver(aObserver)
 {
   AssertIsOnOwningThread();
@@ -246,10 +265,56 @@ LSSimpleRequestChild::Recv__delete__(con
 {
   AssertIsOnOwningThread();
 
   mCallback->OnResponse(aResponse);
 
   return IPC_OK();
 }
 
+/*******************************************************************************
+ * LSSnapshotChild
+ ******************************************************************************/
+
+LSSnapshotChild::LSSnapshotChild(LSSnapshot* aSnapshot)
+  : mSnapshot(aSnapshot)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aSnapshot);
+
+  MOZ_COUNT_CTOR(LSSnapshotChild);
+}
+
+LSSnapshotChild::~LSSnapshotChild()
+{
+  AssertIsOnOwningThread();
+
+  MOZ_COUNT_DTOR(LSSnapshotChild);
+}
+
+void
+LSSnapshotChild::SendDeleteMeInternal()
+{
+  AssertIsOnOwningThread();
+
+  if (mSnapshot) {
+    mSnapshot->ClearActor();
+    mSnapshot = nullptr;
+
+    MOZ_ALWAYS_TRUE(PBackgroundLSSnapshotChild::SendDeleteMe());
+  }
+}
+
+void
+LSSnapshotChild::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnOwningThread();
+
+  if (mSnapshot) {
+    mSnapshot->ClearActor();
+#ifdef DEBUG
+    mSnapshot = nullptr;
+#endif
+  }
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/ActorsChild.h
+++ b/dom/localstorage/ActorsChild.h
@@ -6,16 +6,17 @@
 
 #ifndef mozilla_dom_localstorage_ActorsChild_h
 #define mozilla_dom_localstorage_ActorsChild_h
 
 #include "mozilla/dom/PBackgroundLSDatabaseChild.h"
 #include "mozilla/dom/PBackgroundLSObserverChild.h"
 #include "mozilla/dom/PBackgroundLSRequestChild.h"
 #include "mozilla/dom/PBackgroundLSSimpleRequestChild.h"
+#include "mozilla/dom/PBackgroundLSSnapshotChild.h"
 
 namespace mozilla {
 
 namespace ipc {
 
 class BackgroundChildImpl;
 
 } // namespace ipc
@@ -23,16 +24,17 @@ class BackgroundChildImpl;
 namespace dom {
 
 class LocalStorageManager2;
 class LSDatabase;
 class LSObject;
 class LSObserver;
 class LSRequestChildCallback;
 class LSSimpleRequestChildCallback;
+class LSSnapshot;
 
 class LSDatabaseChild final
   : public PBackgroundLSDatabaseChild
 {
   friend class mozilla::ipc::BackgroundChildImpl;
   friend class LSDatabase;
   friend class LSObject;
 
@@ -58,16 +60,25 @@ private:
   SendDeleteMeInternal();
 
   // IPDL methods are only called by IPDL.
   void
   ActorDestroy(ActorDestroyReason aWhy) override;
 
   mozilla::ipc::IPCResult
   RecvRequestAllowToClose() override;
+
+  PBackgroundLSSnapshotChild*
+  AllocPBackgroundLSSnapshotChild(const nsString& aDocumentURI,
+                                  const int64_t& aRequestedSize,
+                                  LSSnapshotInitInfo* aInitInfo) override;
+
+  bool
+  DeallocPBackgroundLSSnapshotChild(PBackgroundLSSnapshotChild* aActor)
+                                    override;
 };
 
 class LSObserverChild final
   : public PBackgroundLSObserverChild
 {
   friend class mozilla::ipc::BackgroundChildImpl;
   friend class LSObserver;
   friend class LSObject;
@@ -198,12 +209,44 @@ public:
   virtual void
   OnResponse(const LSSimpleRequestResponse& aResponse) = 0;
 
 protected:
   virtual ~LSSimpleRequestChildCallback()
   { }
 };
 
+class LSSnapshotChild final
+  : public PBackgroundLSSnapshotChild
+{
+  friend class LSDatabase;
+  friend class LSSnapshot;
+
+  LSSnapshot* mSnapshot;
+
+  NS_DECL_OWNINGTHREAD
+
+public:
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(LSSnapshotChild);
+  }
+
+private:
+  // Only created by LSDatabase.
+  explicit LSSnapshotChild(LSSnapshot* aSnapshot);
+
+  // Only destroyed by LSDatabaseChild.
+  ~LSSnapshotChild();
+
+  void
+  SendDeleteMeInternal();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+};
+
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_localstorage_ActorsChild_h
--- a/dom/localstorage/ActorsParent.cpp
+++ b/dom/localstorage/ActorsParent.cpp
@@ -14,16 +14,17 @@
 #include "mozStorageHelper.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/PBackgroundLSDatabaseParent.h"
 #include "mozilla/dom/PBackgroundLSObserverParent.h"
 #include "mozilla/dom/PBackgroundLSRequestParent.h"
 #include "mozilla/dom/PBackgroundLSSharedTypes.h"
 #include "mozilla/dom/PBackgroundLSSimpleRequestParent.h"
+#include "mozilla/dom/PBackgroundLSSnapshotParent.h"
 #include "mozilla/dom/StorageDBUpdater.h"
 #include "mozilla/dom/StorageUtils.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "mozilla/dom/quota/QuotaObject.h"
 #include "mozilla/dom/quota/UsageInfo.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/PBackgroundParent.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
@@ -56,16 +57,17 @@ using namespace mozilla::ipc;
 namespace {
 
 class ArchivedOriginInfo;
 class Connection;
 class ConnectionThread;
 class Database;
 class PrepareDatastoreOp;
 class PreparedDatastore;
+class Snapshot;
 
 /*******************************************************************************
  * Constants
  ******************************************************************************/
 
 // Major schema version. Bump for almost everything.
 const uint32_t kMajorSchemaVersion = 1;
 
@@ -1021,21 +1023,27 @@ class Datastore final
   RefPtr<DirectoryLock> mDirectoryLock;
   RefPtr<Connection> mConnection;
   RefPtr<QuotaObject> mQuotaObject;
   nsCOMPtr<nsITimer> mAutoCommitTimer;
   nsCOMPtr<nsIRunnable> mCompleteCallback;
   nsTHashtable<nsPtrHashKey<PrepareDatastoreOp>> mPrepareDatastoreOps;
   nsTHashtable<nsPtrHashKey<PreparedDatastore>> mPreparedDatastores;
   nsTHashtable<nsPtrHashKey<Database>> mDatabases;
+  nsTHashtable<nsPtrHashKey<Database>> mActiveDatabases;
   nsDataHashtable<nsStringHashKey, nsString> mValues;
+  nsTArray<int64_t> mPendingUsageDeltas;
   const nsCString mOrigin;
   const uint32_t mPrivateBrowsingId;
   int64_t mUsage;
+  int64_t mUpdateBatchUsage;
   bool mClosed;
+#ifdef DEBUG
+  bool mInUpdateBatch;
+#endif
 
 public:
   // Created by PrepareDatastoreOp.
   Datastore(const nsACString& aOrigin,
             uint32_t aPrivateBrowsingId,
             int64_t aUsage,
             already_AddRefed<DirectoryLock>&& aDirectoryLock,
             already_AddRefed<Connection>&& aConnection,
@@ -1055,16 +1063,23 @@ public:
   }
 
   bool
   IsPersistent() const
   {
     return mPrivateBrowsingId == 0;
   }
 
+  int64_t
+  Usage() const
+  {
+    AssertIsOnBackgroundThread();
+    return mUsage;
+  }
+
   void
   Close();
 
   bool
   IsClosed() const
   {
     AssertIsOnBackgroundThread();
 
@@ -1097,64 +1112,72 @@ public:
   void
   NoteFinishedDatabase(Database* aDatabase);
 
 #ifdef DEBUG
   bool
   HasLiveDatabases() const;
 #endif
 
-  uint32_t
-  GetLength() const;
+  void
+  NoteActiveDatabase(Database* aDatabase);
 
   void
-  GetKey(uint32_t aIndex, nsString& aKey) const;
+  NoteInactiveDatabase(Database* aDatabase);
+
+  void
+  GetItemInfos(nsTArray<LSItemInfo>* aItemInfos);
 
   void
   GetItem(const nsString& aKey, nsString& aValue) const;
 
   void
   SetItem(Database* aDatabase,
           const nsString& aDocumentURI,
           const nsString& aKey,
-          const nsString& aValue,
-          LSWriteOpResponse& aResponse);
+          const nsString& aOldValue,
+          const nsString& aValue);
 
   void
   RemoveItem(Database* aDatabase,
              const nsString& aDocumentURI,
              const nsString& aKey,
-             LSWriteOpResponse& aResponse);
+             const nsString& aOldValue);
 
   void
   Clear(Database* aDatabase,
-        const nsString& aDocumentURI,
-        LSWriteOpResponse& aResponse);
+        const nsString& aDocumentURI);
+
+  void
+  PrivateBrowsingClear();
 
   void
-  GetKeys(nsTArray<nsString>& aKeys) const;
+  BeginUpdateBatch(int64_t aSnapshotInitialUsage);
+
+  void
+  EndUpdateBatch(int64_t aSnapshotPeakUsage);
+
+  bool
+  UpdateUsage(int64_t aDelta);
 
   NS_INLINE_DECL_REFCOUNTING(Datastore)
 
 private:
   // Reference counted.
   ~Datastore();
 
   void
   MaybeClose();
 
   void
   ConnectionClosedCallback();
 
   void
   CleanupMetadata();
 
-  bool
-  UpdateUsage(int64_t aDelta);
-
   void
   NotifyObservers(Database* aDatabase,
                   const nsString& aDocumentURI,
                   const nsString& aKey,
                   const nsString& aOldValue,
                   const nsString& aNewValue);
 
   void
@@ -1264,16 +1287,17 @@ private:
 /*******************************************************************************
  * Actor class declarations
  ******************************************************************************/
 
 class Database final
   : public PBackgroundLSDatabaseParent
 {
   RefPtr<Datastore> mDatastore;
+  Snapshot* mSnapshot;
   const PrincipalInfo mPrincipalInfo;
   // Strings share buffers if possible, so it's not a problem to duplicate the
   // origin here.
   nsCString mOrigin;
   uint32_t mPrivateBrowsingId;
   bool mAllowedToClose;
   bool mActorDestroyed;
   bool mRequestedAllowToClose;
@@ -1282,16 +1306,23 @@ class Database final
 #endif
 
 public:
   // Created in AllocPBackgroundLSDatabaseParent.
   Database(const PrincipalInfo& aPrincipalInfo,
            const nsACString& aOrigin,
            uint32_t aPrivateBrowsingId);
 
+  Datastore*
+  GetDatastore() const
+  {
+    AssertIsOnBackgroundThread();
+    return mDatastore;
+  }
+
   const PrincipalInfo&
   GetPrincipalInfo() const
   {
     return mPrincipalInfo;
   }
 
   uint32_t
   PrivateBrowsingId() const
@@ -1304,16 +1335,22 @@ public:
   {
     return mOrigin;
   }
 
   void
   SetActorAlive(Datastore* aDatastore);
 
   void
+  RegisterSnapshot(Snapshot* aSnapshot);
+
+  void
+  UnregisterSnapshot(Snapshot* aSnapshot);
+
+  void
   RequestAllowToClose();
 
   NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Database)
 
 private:
   // Reference counted.
   ~Database();
 
@@ -1325,42 +1362,82 @@ private:
   ActorDestroy(ActorDestroyReason aWhy) override;
 
   mozilla::ipc::IPCResult
   RecvDeleteMe() override;
 
   mozilla::ipc::IPCResult
   RecvAllowToClose() override;
 
-  mozilla::ipc::IPCResult
-  RecvGetLength(uint32_t* aLength) override;
-
-  mozilla::ipc::IPCResult
-  RecvGetKey(const uint32_t& aIndex, nsString* aKey) override;
-
-  mozilla::ipc::IPCResult
-  RecvGetItem(const nsString& aKey, nsString* aValue) override;
-
-  mozilla::ipc::IPCResult
-  RecvGetKeys(nsTArray<nsString>* aKeys) override;
+  PBackgroundLSSnapshotParent*
+  AllocPBackgroundLSSnapshotParent(const nsString& aDocumentURI,
+                                   const int64_t& aRequestedSize,
+                                   LSSnapshotInitInfo* aInitInfo) override;
 
   mozilla::ipc::IPCResult
-  RecvSetItem(const nsString& aDocumentURI,
-              const nsString& aKey,
-              const nsString& aValue,
-              LSWriteOpResponse* aResponse) override;
+  RecvPBackgroundLSSnapshotConstructor(PBackgroundLSSnapshotParent* aActor,
+                                       const nsString& aDocumentURI,
+                                       const int64_t& aRequestedSize,
+                                       LSSnapshotInitInfo* aInitInfo) override;
+
+  bool
+  DeallocPBackgroundLSSnapshotParent(PBackgroundLSSnapshotParent* aActor)
+                                     override;
+};
+
+class Snapshot final
+  : public PBackgroundLSSnapshotParent
+{
+  RefPtr<Database> mDatabase;
+  RefPtr<Datastore> mDatastore;
+  nsString mDocumentURI;
+  int64_t mInitialUsage;
+  int64_t mPeakUsage;
+  bool mActorDestroyed;
+  bool mFinishReceived;
+
+public:
+  // Created in AllocPBackgroundLSSnapshotParent.
+  Snapshot(Database* aDatabase,
+           const nsAString& aDocumentURI);
+
+  void
+  SetUsage(int64_t aInitialUsage,
+           int64_t aPeakUsage)
+  {
+    AssertIsOnBackgroundThread();
+    MOZ_ASSERT(aInitialUsage >= 0);
+    MOZ_ASSERT(aPeakUsage >= aInitialUsage);
+    MOZ_ASSERT(mInitialUsage == -1);
+    MOZ_ASSERT(mPeakUsage == -1);
+
+    mInitialUsage = aInitialUsage;
+    mPeakUsage = aPeakUsage;
+  }
+
+  NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot)
+
+private:
+  // Reference counted.
+  ~Snapshot();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
 
   mozilla::ipc::IPCResult
-  RecvRemoveItem(const nsString& aDocumentURI,
-                 const nsString& aKey,
-                 LSWriteOpResponse* aResponse) override;
+  RecvDeleteMe() override;
 
   mozilla::ipc::IPCResult
-  RecvClear(const nsString& aDocumentURI,
-            LSWriteOpResponse* aResponse) override;
+  RecvFinish(const LSSnapshotFinishInfo& aFinishInfo) override;
+
+  mozilla::ipc::IPCResult
+  RecvIncreasePeakUsage(const int64_t& aRequestedSize,
+                        const int64_t& aMinSize,
+                        int64_t* aSize) override;
 };
 
 class Observer final
   : public PBackgroundLSObserverParent
 {
   nsCString mOrigin;
   bool mActorDestroyed;
 
@@ -2976,17 +3053,21 @@ Datastore::Datastore(const nsACString& a
                      already_AddRefed<QuotaObject>&& aQuotaObject,
                      nsDataHashtable<nsStringHashKey, nsString>& aValues)
   : mDirectoryLock(std::move(aDirectoryLock))
   , mConnection(std::move(aConnection))
   , mQuotaObject(std::move(aQuotaObject))
   , mOrigin(aOrigin)
   , mPrivateBrowsingId(aPrivateBrowsingId)
   , mUsage(aUsage)
+  , mUpdateBatchUsage(-1)
   , mClosed(false)
+#ifdef DEBUG
+  , mInUpdateBatch(false)
+#endif
 {
   AssertIsOnBackgroundThread();
 
   mValues.SwapElements(aValues);
 }
 
 Datastore::~Datastore()
 {
@@ -3124,16 +3205,17 @@ Datastore::NoteLiveDatabase(Database* aD
 }
 
 void
 Datastore::NoteFinishedDatabase(Database* aDatabase)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabase);
   MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase));
   MOZ_ASSERT(mDirectoryLock);
   MOZ_ASSERT(!mClosed);
 
   mDatabases.RemoveEntry(aDatabase);
 
   MaybeClose();
 }
 
@@ -3142,38 +3224,65 @@ bool
 Datastore::HasLiveDatabases() const
 {
   AssertIsOnBackgroundThread();
 
   return mDatabases.Count();
 }
 #endif
 
-uint32_t
-Datastore::GetLength() const
+void
+Datastore::NoteActiveDatabase(Database* aDatabase)
 {
   AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase));
   MOZ_ASSERT(!mClosed);
 
-  return mValues.Count();
+  mActiveDatabases.PutEntry(aDatabase);
 }
 
 void
-Datastore::GetKey(uint32_t aIndex, nsString& aKey) const
+Datastore::NoteInactiveDatabase(Database* aDatabase)
 {
   AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(mActiveDatabases.GetEntry(aDatabase));
   MOZ_ASSERT(!mClosed);
 
-  aKey.SetIsVoid(true);
+  mActiveDatabases.RemoveEntry(aDatabase);
+
+  if (!mActiveDatabases.Count() &&
+      mPendingUsageDeltas.Length()) {
+    int64_t finalDelta = 0;
+
+    for (auto delta : mPendingUsageDeltas) {
+      finalDelta += delta;
+    }
+
+    MOZ_ASSERT(finalDelta <= 0);
+
+    if (finalDelta != 0) {
+      DebugOnly<bool> ok = UpdateUsage(finalDelta);
+      MOZ_ASSERT(ok);
+    }
+
+    mPendingUsageDeltas.Clear();
+  }
+}
+
+void
+Datastore::GetItemInfos(nsTArray<LSItemInfo>* aItemInfos)
+{
   for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
-    if (aIndex == 0) {
-      aKey = iter.Key();
-      return;
-    }
-    aIndex--;
+    LSItemInfo* itemInfo = aItemInfos->AppendElement();
+    itemInfo->key() = iter.Key();
+    itemInfo->value() = iter.Data();
   }
 }
 
 void
 Datastore::GetItem(const nsString& aKey, nsString& aValue) const
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(!mClosed);
@@ -3182,157 +3291,225 @@ Datastore::GetItem(const nsString& aKey,
     aValue.SetIsVoid(true);
   }
 }
 
 void
 Datastore::SetItem(Database* aDatabase,
                    const nsString& aDocumentURI,
                    const nsString& aKey,
-                   const nsString& aValue,
-                   LSWriteOpResponse& aResponse)
+                   const nsString& aOldValue,
+                   const nsString& aValue)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabase);
   MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(mInUpdateBatch);
 
   nsString oldValue;
-  if (!mValues.Get(aKey, &oldValue)) {
-    oldValue.SetIsVoid(true);
-  }
-
-  bool changed;
-  if (oldValue == aValue && oldValue.IsVoid() == aValue.IsVoid()) {
-    changed = false;
-  } else {
-    changed = true;
-
-    int64_t delta = 0;
+  GetItem(aKey, oldValue);
+
+  if (oldValue != aValue || oldValue.IsVoid() != aValue.IsVoid()) {
+    int64_t delta = static_cast<int64_t>(aValue.Length()) -
+                    static_cast<int64_t>(oldValue.Length());
 
     if (oldValue.IsVoid()) {
       delta += static_cast<int64_t>(aKey.Length());
     }
 
-    delta += static_cast<int64_t>(aValue.Length()) -
-             static_cast<int64_t>(oldValue.Length());
-
-    if (!UpdateUsage(delta)) {
-      aResponse = NS_ERROR_FILE_NO_DEVICE_SPACE;
-      return;
-    }
+    mUpdateBatchUsage += delta;
 
     mValues.Put(aKey, aValue);
 
-    NotifyObservers(aDatabase, aDocumentURI, aKey, oldValue, aValue);
-
     if (IsPersistent()) {
       EnsureTransaction();
 
       RefPtr<SetItemOp> op = new SetItemOp(mConnection, aKey, aValue);
       mConnection->Dispatch(op);
     }
   }
 
-  LSNotifyInfo info;
-  info.changed() = changed;
-  info.oldValue() = oldValue;
-  aResponse = info;
+  NotifyObservers(aDatabase, aDocumentURI, aKey, aOldValue, aValue);
 }
 
 void
 Datastore::RemoveItem(Database* aDatabase,
                       const nsString& aDocumentURI,
                       const nsString& aKey,
-                      LSWriteOpResponse& aResponse)
+                      const nsString& aOldValue)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabase);
   MOZ_ASSERT(!mClosed);
-
-  bool changed;
+  MOZ_ASSERT(mInUpdateBatch);
+
   nsString oldValue;
-  if (!mValues.Get(aKey, &oldValue)) {
-    oldValue.SetIsVoid(true);
-    changed = false;
-  } else {
-    changed = true;
-
+  GetItem(aKey, oldValue);
+
+  if (!oldValue.IsVoid()) {
     int64_t delta = -(static_cast<int64_t>(aKey.Length()) +
                       static_cast<int64_t>(oldValue.Length()));
 
-    DebugOnly<bool> ok = UpdateUsage(delta);
-    MOZ_ASSERT(ok);
+    mUpdateBatchUsage += delta;
 
     mValues.Remove(aKey);
 
-    NotifyObservers(aDatabase, aDocumentURI, aKey, oldValue, VoidString());
-
     if (IsPersistent()) {
       EnsureTransaction();
 
       RefPtr<RemoveItemOp> op = new RemoveItemOp(mConnection, aKey);
       mConnection->Dispatch(op);
     }
   }
 
-  LSNotifyInfo info;
-  info.changed() = changed;
-  info.oldValue() = oldValue;
-  aResponse = info;
+  NotifyObservers(aDatabase, aDocumentURI, aKey, aOldValue, VoidString());
 }
 
 void
 Datastore::Clear(Database* aDatabase,
-                 const nsString& aDocumentURI,
-                 LSWriteOpResponse& aResponse)
+                 const nsString& aDocumentURI)
 {
   AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
   MOZ_ASSERT(!mClosed);
-
-  bool changed;
-  if (!mValues.Count()) {
-    changed = false;
-  } else {
-    changed = true;
-
-    DebugOnly<bool> ok = UpdateUsage(-mUsage);
-    MOZ_ASSERT(ok);
+  MOZ_ASSERT(mInUpdateBatch);
+
+  if (mValues.Count()) {
+    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;
+    }
 
     mValues.Clear();
 
-    if (aDatabase) {
-      NotifyObservers(aDatabase,
-                      aDocumentURI,
-                      VoidString(),
-                      VoidString(),
-                      VoidString());
-    }
-
     if (IsPersistent()) {
       EnsureTransaction();
 
       RefPtr<ClearOp> op = new ClearOp(mConnection);
       mConnection->Dispatch(op);
     }
   }
 
-  LSNotifyInfo info;
-  info.changed() = changed;
-  aResponse = info;
+  NotifyObservers(aDatabase,
+                  aDocumentURI,
+                  VoidString(),
+                  VoidString(),
+                  VoidString());
+}
+
+void
+Datastore::PrivateBrowsingClear()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mPrivateBrowsingId);
+  MOZ_ASSERT(!mClosed);
+
+  if (mValues.Count()) {
+    DebugOnly<bool> ok = UpdateUsage(-mUsage);
+    MOZ_ASSERT(ok);
+
+    mValues.Clear();
+  }
+}
+
+void
+Datastore::BeginUpdateBatch(int64_t aSnapshotInitialUsage)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aSnapshotInitialUsage >= 0);
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(mUpdateBatchUsage == -1);
+  MOZ_ASSERT(!mInUpdateBatch);
+
+  mUpdateBatchUsage = aSnapshotInitialUsage;
+
+#ifdef DEBUG
+  mInUpdateBatch = true;
+#endif
 }
 
 void
-Datastore::GetKeys(nsTArray<nsString>& aKeys) const
+Datastore::EndUpdateBatch(int64_t aSnapshotPeakUsage)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aSnapshotPeakUsage >= 0);
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(mInUpdateBatch);
+
+  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);
+    }
+  }
+
+  mUpdateBatchUsage = -1;
+
+#ifdef DEBUG
+  mInUpdateBatch = false;
+#endif
+}
+
+bool
+Datastore::UpdateUsage(int64_t aDelta)
 {
   AssertIsOnBackgroundThread();
-  MOZ_ASSERT(!mClosed);
-
-  for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
-    aKeys.AppendElement(iter.Key());
-  }
+
+  // Check internal LocalStorage origin limit.
+  int64_t newUsage = mUsage + aDelta;
+  if (newUsage > gOriginLimitKB * 1024) {
+    return false;
+  }
+
+  // Check QuotaManager limits (group and global limit).
+  if (IsPersistent()) {
+    MOZ_ASSERT(mQuotaObject);
+
+    if (!mQuotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) {
+      return false;
+    }
+
+  }
+
+  // Quota checks passed, set new usage.
+
+  mUsage = newUsage;
+
+  if (IsPersistent()) {
+    RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+      "Datastore::UpdateUsage",
+      [origin = mOrigin, newUsage] () {
+        MOZ_ASSERT(gUsages);
+        MOZ_ASSERT(gUsages->Contains(origin));
+        gUsages->Put(origin, newUsage);
+      });
+
+    QuotaManager* quotaManager = QuotaManager::Get();
+    MOZ_ASSERT(quotaManager);
+
+    MOZ_ALWAYS_SUCCEEDS(
+      quotaManager->IOThread()->Dispatch(runnable, NS_DISPATCH_NORMAL));
+  }
+
+  return true;
 }
 
 void
 Datastore::MaybeClose()
 {
   AssertIsOnBackgroundThread();
 
   if (!mPrepareDatastoreOps.Count() &&
@@ -3376,60 +3553,16 @@ Datastore::CleanupMetadata()
   MOZ_ASSERT(gDatastores->Get(mOrigin));
   gDatastores->Remove(mOrigin);
 
   if (!gDatastores->Count()) {
     gDatastores = nullptr;
   }
 }
 
-bool
-Datastore::UpdateUsage(int64_t aDelta)
-{
-  AssertIsOnBackgroundThread();
-
-  // Check internal LocalStorage origin limit.
-  int64_t newUsage = mUsage + aDelta;
-  if (newUsage > gOriginLimitKB * 1024) {
-    return false;
-  }
-
-  // Check QuotaManager limits (group and global limit).
-  if (IsPersistent()) {
-    MOZ_ASSERT(mQuotaObject);
-
-    if (!mQuotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) {
-      return false;
-    }
-
-  }
-
-  // Quota checks passed, set new usage.
-
-  mUsage = newUsage;
-
-  if (IsPersistent()) {
-    RefPtr<Runnable> runnable = NS_NewRunnableFunction(
-      "Datastore::UpdateUsage",
-      [origin = mOrigin, newUsage] () {
-        MOZ_ASSERT(gUsages);
-        MOZ_ASSERT(gUsages->Contains(origin));
-        gUsages->Put(origin, newUsage);
-      });
-
-    QuotaManager* quotaManager = QuotaManager::Get();
-    MOZ_ASSERT(quotaManager);
-
-    MOZ_ALWAYS_SUCCEEDS(
-      quotaManager->IOThread()->Dispatch(runnable, NS_DISPATCH_NORMAL));
-  }
-
-  return true;
-}
-
 void
 Datastore::NotifyObservers(Database* aDatabase,
                            const nsString& aDocumentURI,
                            const nsString& aKey,
                            const nsString& aOldValue,
                            const nsString& aNewValue)
 {
   AssertIsOnBackgroundThread();
@@ -3524,17 +3657,18 @@ PreparedDatastore::TimerCallback(nsITime
 
 /*******************************************************************************
  * Database
  ******************************************************************************/
 
 Database::Database(const PrincipalInfo& aPrincipalInfo,
                    const nsACString& aOrigin,
                    uint32_t aPrivateBrowsingId)
-  : mPrincipalInfo(aPrincipalInfo)
+  : mSnapshot(nullptr)
+  , mPrincipalInfo(aPrincipalInfo)
   , mOrigin(aOrigin)
   , mPrivateBrowsingId(aPrivateBrowsingId)
   , mAllowedToClose(false)
   , mActorDestroyed(false)
   , mRequestedAllowToClose(false)
 #ifdef DEBUG
   , mActorWasAlive(false)
 #endif
@@ -3566,16 +3700,41 @@ Database::SetActorAlive(Datastore* aData
   if (!gLiveDatabases) {
     gLiveDatabases = new LiveDatabaseArray();
   }
 
   gLiveDatabases->AppendElement(this);
 }
 
 void
+Database::RegisterSnapshot(Snapshot* aSnapshot)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aSnapshot);
+  MOZ_ASSERT(!mSnapshot);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  // Only one snapshot at a time is currently supported.
+  mSnapshot = aSnapshot;
+
+  mDatastore->NoteActiveDatabase(this);
+}
+
+void
+Database::UnregisterSnapshot(Snapshot* aSnapshot)
+{
+  MOZ_ASSERT(aSnapshot);
+  MOZ_ASSERT(mSnapshot == aSnapshot);
+
+  mSnapshot = nullptr;
+
+  mDatastore->NoteInactiveDatabase(this);
+}
+
+void
 Database::RequestAllowToClose()
 {
   AssertIsOnBackgroundThread();
 
   if (mRequestedAllowToClose) {
     return;
   }
 
@@ -3647,137 +3806,235 @@ Database::RecvAllowToClose()
     return IPC_FAIL_NO_REASON(this);
   }
 
   AllowToClose();
 
   return IPC_OK();
 }
 
-mozilla::ipc::IPCResult
-Database::RecvGetLength(uint32_t* aLength)
+PBackgroundLSSnapshotParent*
+Database::AllocPBackgroundLSSnapshotParent(const nsString& aDocumentURI,
+                                           const int64_t& aRequestedSize,
+                                           LSSnapshotInitInfo* aInitInfo)
 {
   AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aLength);
-  MOZ_ASSERT(mDatastore);
+
+  if (NS_WARN_IF(aRequestedSize < 0)) {
+    ASSERT_UNLESS_FUZZING();
+    return nullptr;
+  }
 
   if (NS_WARN_IF(mAllowedToClose)) {
     ASSERT_UNLESS_FUZZING();
-    return IPC_FAIL_NO_REASON(this);
-  }
-
-  *aLength = mDatastore->GetLength();
-
+    return nullptr;
+  }
+
+  RefPtr<Snapshot> snapshot = new Snapshot(this, aDocumentURI);
+
+  // Transfer ownership to IPDL.
+  return snapshot.forget().take();
+}
+
+mozilla::ipc::IPCResult
+Database::RecvPBackgroundLSSnapshotConstructor(
+                                            PBackgroundLSSnapshotParent* aActor,
+                                            const nsString& aDocumentURI,
+                                            const int64_t& aRequestedSize,
+                                            LSSnapshotInitInfo* aInitInfo)
+{
+  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.
+  nsTArray<LSItemInfo> itemInfos;
+  mDatastore->GetItemInfos(&itemInfos);
+
+  int64_t initialUsage = mDatastore->Usage();
+
+  int64_t peakUsage = initialUsage;
+  if (aRequestedSize && mDatastore->UpdateUsage(aRequestedSize)) {
+    peakUsage += aRequestedSize;
+  }
+
+  snapshot->SetUsage(initialUsage, peakUsage);
+
+  RegisterSnapshot(snapshot);
+
+  aInitInfo->itemInfos() = std::move(itemInfos);
+  aInitInfo->initialUsage() = initialUsage;
+  aInitInfo->peakUsage() = peakUsage;
+
+  return IPC_OK();
+}
+
+bool
+Database::DeallocPBackgroundLSSnapshotParent(
+                                            PBackgroundLSSnapshotParent* aActor)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+
+  // Transfer ownership back from IPDL.
+  RefPtr<Snapshot> actor = dont_AddRef(static_cast<Snapshot*>(aActor));
+
+  return true;
+}
+
+/*******************************************************************************
+ * Snapshot
+ ******************************************************************************/
+
+Snapshot::Snapshot(Database* aDatabase,
+                   const nsAString& aDocumentURI)
+  : mDatabase(aDatabase)
+  , mDatastore(aDatabase->GetDatastore())
+  , mDocumentURI(aDocumentURI)
+  , mInitialUsage(-1)
+  , mPeakUsage(-1)
+  , mActorDestroyed(false)
+  , mFinishReceived(false)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+}
+
+Snapshot::~Snapshot()
+{
+  MOZ_ASSERT(mActorDestroyed);
+  MOZ_ASSERT(mFinishReceived);
+}
+
+void
+Snapshot::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorDestroyed);
+
+  mActorDestroyed = true;
+
+  if (!mFinishReceived) {
+    mDatabase->UnregisterSnapshot(this);
+
+    mDatastore->BeginUpdateBatch(mInitialUsage);
+
+    mDatastore->EndUpdateBatch(mPeakUsage);
+  }
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvDeleteMe()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorDestroyed);
+
+  IProtocol* mgr = Manager();
+  if (!PBackgroundLSSnapshotParent::Send__delete__(this)) {
+    return IPC_FAIL_NO_REASON(mgr);
+  }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
-Database::RecvGetKey(const uint32_t& aIndex, nsString* aKey)
+Snapshot::RecvFinish(const LSSnapshotFinishInfo& aFinishInfo)
 {
   AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aKey);
-  MOZ_ASSERT(mDatastore);
-
-  if (NS_WARN_IF(mAllowedToClose)) {
+  MOZ_ASSERT(mInitialUsage >= 0);
+  MOZ_ASSERT(mPeakUsage >= mInitialUsage);
+
+  if (NS_WARN_IF(mFinishReceived)) {
     ASSERT_UNLESS_FUZZING();
     return IPC_FAIL_NO_REASON(this);
   }
 
-  mDatastore->GetKey(aIndex, *aKey);
-
-  return IPC_OK();
-}
-
-mozilla::ipc::IPCResult
-Database::RecvGetItem(const nsString& aKey, nsString* aValue)
-{
-  AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aValue);
-  MOZ_ASSERT(mDatastore);
-
-  if (NS_WARN_IF(mAllowedToClose)) {
-    ASSERT_UNLESS_FUZZING();
-    return IPC_FAIL_NO_REASON(this);
-  }
-
-  mDatastore->GetItem(aKey, *aValue);
+  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];
+    switch (writeInfo.type()) {
+      case LSWriteInfo::TLSSetItemInfo: {
+        const LSSetItemInfo& info = writeInfo.get_LSSetItemInfo();
+
+        mDatastore->SetItem(mDatabase,
+                            mDocumentURI,
+                            info.key(),
+                            info.oldValue(),
+                            info.value());
+
+        break;
+      }
+
+      case LSWriteInfo::TLSRemoveItemInfo: {
+        const LSRemoveItemInfo& info = writeInfo.get_LSRemoveItemInfo();
+
+        mDatastore->RemoveItem(mDatabase,
+                               mDocumentURI,
+                               info.key(),
+                               info.oldValue());
+
+        break;
+      }
+
+      case LSWriteInfo::TLSClearInfo: {
+        mDatastore->Clear(mDatabase, mDocumentURI);
+
+        break;
+      }
+
+      default:
+        MOZ_CRASH("Should never get here!");
+    }
+  }
+
+  mDatastore->EndUpdateBatch(mPeakUsage);
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
-Database::RecvSetItem(const nsString& aDocumentURI,
-                      const nsString& aKey,
-                      const nsString& aValue,
-                      LSWriteOpResponse* aResponse)
+Snapshot::RecvIncreasePeakUsage(const int64_t& aRequestedSize,
+                                const int64_t& aMinSize,
+                                int64_t* aSize)
 {
   AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aResponse);
-  MOZ_ASSERT(mDatastore);
-
-  if (NS_WARN_IF(mAllowedToClose)) {
+  MOZ_ASSERT(aSize);
+
+  if (NS_WARN_IF(aRequestedSize <= 0)) {
     ASSERT_UNLESS_FUZZING();
     return IPC_FAIL_NO_REASON(this);
   }
 
-  mDatastore->SetItem(this, aDocumentURI, aKey, aValue, *aResponse);
-
-  return IPC_OK();
-}
-
-mozilla::ipc::IPCResult
-Database::RecvRemoveItem(const nsString& aDocumentURI,
-                         const nsString& aKey,
-                         LSWriteOpResponse* aResponse)
-{
-  AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aResponse);
-  MOZ_ASSERT(mDatastore);
-
-  if (NS_WARN_IF(mAllowedToClose)) {
+  if (NS_WARN_IF(aMinSize <= 0)) {
     ASSERT_UNLESS_FUZZING();
     return IPC_FAIL_NO_REASON(this);
   }
 
-  mDatastore->RemoveItem(this, aDocumentURI, aKey, *aResponse);
-
-  return IPC_OK();
-}
-
-mozilla::ipc::IPCResult
-Database::RecvClear(const nsString& aDocumentURI,
-                    LSWriteOpResponse* aResponse)
-{
-  AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aResponse);
-  MOZ_ASSERT(mDatastore);
-
-  if (NS_WARN_IF(mAllowedToClose)) {
+  if (NS_WARN_IF(mFinishReceived)) {
     ASSERT_UNLESS_FUZZING();
     return IPC_FAIL_NO_REASON(this);
   }
 
-  mDatastore->Clear(this, aDocumentURI, *aResponse);
-
-  return IPC_OK();
-}
-
-mozilla::ipc::IPCResult
-Database::RecvGetKeys(nsTArray<nsString>* aKeys)
-{
-  AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aKeys);
-  MOZ_ASSERT(mDatastore);
-
-  if (NS_WARN_IF(mAllowedToClose)) {
-    ASSERT_UNLESS_FUZZING();
-    return IPC_FAIL_NO_REASON(this);
-  }
-
-  mDatastore->GetKeys(*aKeys);
+  if (mDatastore->UpdateUsage(aRequestedSize)) {
+    mPeakUsage += aRequestedSize;
+    *aSize = aRequestedSize;
+  } else if (mDatastore->UpdateUsage(aMinSize)) {
+    mPeakUsage += aMinSize;
+    *aSize = aMinSize;
+  } else {
+    *aSize = 0;
+  }
 
   return IPC_OK();
 }
 
 /*******************************************************************************
  * Observer
  ******************************************************************************/
 
@@ -5774,18 +6031,17 @@ ClearPrivateBrowsingRunnable::Run()
   AssertIsOnBackgroundThread();
 
   if (gDatastores) {
     for (auto iter = gDatastores->ConstIter(); !iter.Done(); iter.Next()) {
       Datastore* datastore = iter.Data();
       MOZ_ASSERT(datastore);
 
       if (datastore->PrivateBrowsingId()) {
-        LSWriteOpResponse dummy;
-        datastore->Clear(nullptr, EmptyString(), dummy);
+        datastore->PrivateBrowsingClear();
       }
     }
   }
 
   return NS_OK;
 }
 
 NS_IMPL_ISUPPORTS(QuotaClient::Observer, nsIObserver)
--- a/dom/localstorage/LSDatabase.cpp
+++ b/dom/localstorage/LSDatabase.cpp
@@ -14,32 +14,35 @@ namespace {
 typedef nsDataHashtable<nsCStringHashKey, LSDatabase*> LSDatabaseHashtable;
 
 StaticAutoPtr<LSDatabaseHashtable> gLSDatabases;
 
 } // namespace
 
 LSDatabase::LSDatabase(const nsACString& aOrigin)
   : mActor(nullptr)
+  , mSnapshot(nullptr)
   , mOrigin(aOrigin)
   , mAllowedToClose(false)
+  , mRequestedAllowToClose(false)
 {
   AssertIsOnOwningThread();
 
   if (!gLSDatabases) {
     gLSDatabases = new LSDatabaseHashtable();
   }
 
   MOZ_ASSERT(!gLSDatabases->Get(mOrigin));
   gLSDatabases->Put(mOrigin, this);
 }
 
 LSDatabase::~LSDatabase()
 {
   AssertIsOnOwningThread();
+  MOZ_ASSERT(!mSnapshot);
 
   if (!mAllowedToClose) {
     AllowToClose();
   }
 
   if (mActor) {
     mActor->SendDeleteMeInternal();
     MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
@@ -59,155 +62,258 @@ LSDatabase::SetActor(LSDatabaseChild* aA
   AssertIsOnOwningThread();
   MOZ_ASSERT(aActor);
   MOZ_ASSERT(!mActor);
 
   mActor = aActor;
 }
 
 void
+LSDatabase::RequestAllowToClose()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(!mRequestedAllowToClose);
+
+  mRequestedAllowToClose = true;
+
+  if (!mSnapshot) {
+    AllowToClose();
+  }
+}
+
+void
+LSDatabase::NoteFinishedSnapshot(LSSnapshot* aSnapshot)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aSnapshot == mSnapshot);
+
+  mSnapshot = nullptr;
+
+  if (mRequestedAllowToClose) {
+    AllowToClose();
+  }
+}
+
+nsresult
+LSDatabase::GetLength(LSObject* aObject,
+                      uint32_t* aResult)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  nsresult rv = EnsureSnapshot(aObject, /* aRequestedBySetItem */ false);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mSnapshot->GetLength(aResult);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSDatabase::GetKey(LSObject* aObject,
+                   uint32_t aIndex,
+                   nsAString& aResult)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  nsresult rv = EnsureSnapshot(aObject, /* aRequestedBySetItem */ false);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mSnapshot->GetKey(aIndex, aResult);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSDatabase::GetItem(LSObject* aObject,
+                    const nsAString& aKey,
+                    nsAString& aResult)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  nsresult rv = EnsureSnapshot(aObject, /* aRequestedBySetItem */ false);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mSnapshot->GetItem(aKey, aResult);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSDatabase::GetKeys(LSObject* aObject,
+                    nsTArray<nsString>& aKeys)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  nsresult rv = EnsureSnapshot(aObject, /* aRequestedBySetItem */ false);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mSnapshot->GetKeys(aKeys);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSDatabase::SetItem(LSObject* aObject,
+                    const nsAString& aKey,
+                    const nsAString& aValue,
+                    LSNotifyInfo& aNotifyInfo)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  nsresult rv = EnsureSnapshot(aObject, /* aRequestedBySetItem */ true);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mSnapshot->SetItem(aKey, aValue, aNotifyInfo);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSDatabase::RemoveItem(LSObject* aObject,
+                       const nsAString& aKey,
+                       LSNotifyInfo& aNotifyInfo)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  nsresult rv = EnsureSnapshot(aObject, /* aRequestedBySetItem */ false);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mSnapshot->RemoveItem(aKey, aNotifyInfo);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSDatabase::Clear(LSObject* aObject,
+                  LSNotifyInfo& aNotifyInfo)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  nsresult rv = EnsureSnapshot(aObject, /* aRequestedBySetItem */ false);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mSnapshot->Clear(aNotifyInfo);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSDatabase::EnsureSnapshot(LSObject* aObject,
+                           bool aRequestedBySetItem)
+{
+  MOZ_ASSERT(aObject);
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  if (mSnapshot) {
+    return NS_OK;
+  }
+
+  RefPtr<LSSnapshot> snapshot = new LSSnapshot(this);
+
+  LSSnapshotChild* actor = new LSSnapshotChild(snapshot);
+
+  int64_t requestedSize = aRequestedBySetItem ? 4096 : 0;
+
+  LSSnapshotInitInfo initInfo;
+  bool ok =
+    mActor->SendPBackgroundLSSnapshotConstructor(actor,
+                                                 aObject->DocumentURI(),
+                                                 requestedSize,
+                                                 &initInfo);
+  if (NS_WARN_IF(!ok)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  snapshot->SetActor(actor);
+
+  // This add refs snapshot.
+  nsresult rv = snapshot->Init(initInfo);
+  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;
+}
+
+void
 LSDatabase::AllowToClose()
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(!mAllowedToClose);
+  MOZ_ASSERT(!mSnapshot);
 
   mAllowedToClose = true;
 
   if (mActor) {
     mActor->SendAllowToClose();
   }
 
   MOZ_ASSERT(gLSDatabases);
   MOZ_ASSERT(gLSDatabases->Get(mOrigin));
   gLSDatabases->Remove(mOrigin);
 
   if (!gLSDatabases->Count()) {
     gLSDatabases = nullptr;
   }
 }
 
-nsresult
-LSDatabase::GetLength(uint32_t* aResult)
-{
-  AssertIsOnOwningThread();
-  MOZ_ASSERT(mActor);
-  MOZ_ASSERT(!mAllowedToClose);
-
-  uint32_t result;
-  if (NS_WARN_IF(!mActor->SendGetLength(&result))) {
-    return NS_ERROR_FAILURE;
-  }
-
-  *aResult = result;
-  return NS_OK;
-}
-
-nsresult
-LSDatabase::GetKey(uint32_t aIndex,
-                   nsAString& aResult)
-{
-  AssertIsOnOwningThread();
-  MOZ_ASSERT(mActor);
-  MOZ_ASSERT(!mAllowedToClose);
-
-  nsString result;
-  if (NS_WARN_IF(!mActor->SendGetKey(aIndex, &result))) {
-    return NS_ERROR_FAILURE;
-  }
-
-  aResult = result;
-  return NS_OK;
-}
-
-nsresult
-LSDatabase::GetItem(const nsAString& aKey,
-                    nsAString& aResult)
-{
-  AssertIsOnOwningThread();
-  MOZ_ASSERT(mActor);
-  MOZ_ASSERT(!mAllowedToClose);
-
-  nsString result;
-  if (NS_WARN_IF(!mActor->SendGetItem(nsString(aKey), &result))) {
-    return NS_ERROR_FAILURE;
-  }
-
-  aResult = result;
-  return NS_OK;
-}
-
-nsresult
-LSDatabase::GetKeys(nsTArray<nsString>& aKeys)
-{
-  AssertIsOnOwningThread();
-  MOZ_ASSERT(mActor);
-  MOZ_ASSERT(!mAllowedToClose);
-
-  nsTArray<nsString> result;
-  if (NS_WARN_IF(!mActor->SendGetKeys(&result))) {
-    return NS_ERROR_FAILURE;
-  }
-
-  aKeys.SwapElements(result);
-  return NS_OK;
-}
-
-nsresult
-LSDatabase::SetItem(const nsAString& aDocumentURI,
-                    const nsAString& aKey,
-                    const nsAString& aValue,
-                    LSWriteOpResponse& aResponse)
-{
-  AssertIsOnOwningThread();
-  MOZ_ASSERT(mActor);
-  MOZ_ASSERT(!mAllowedToClose);
-
-  LSWriteOpResponse response;
-  if (NS_WARN_IF(!mActor->SendSetItem(nsString(aDocumentURI),
-                                      nsString(aKey),
-                                      nsString(aValue),
-                                      &response))) {
-    return NS_ERROR_FAILURE;
-  }
-
-  aResponse = response;
-  return NS_OK;
-}
-
-nsresult
-LSDatabase::RemoveItem(const nsAString& aDocumentURI,
-                       const nsAString& aKey,
-                       LSWriteOpResponse& aResponse)
-{
-  AssertIsOnOwningThread();
-  MOZ_ASSERT(mActor);
-  MOZ_ASSERT(!mAllowedToClose);
-
-  LSWriteOpResponse response;
-  if (NS_WARN_IF(!mActor->SendRemoveItem(nsString(aDocumentURI),
-                                         nsString(aKey),
-                                         &response))) {
-    return NS_ERROR_FAILURE;
-  }
-
-  aResponse = response;
-  return NS_OK;
-}
-
-nsresult
-LSDatabase::Clear(const nsAString& aDocumentURI,
-                  LSWriteOpResponse& aResponse)
-{
-  AssertIsOnOwningThread();
-  MOZ_ASSERT(mActor);
-  MOZ_ASSERT(!mAllowedToClose);
-
-  LSWriteOpResponse response;
-  if (NS_WARN_IF(!mActor->SendClear(nsString(aDocumentURI), &response))) {
-    return NS_ERROR_FAILURE;
-  }
-
-  aResponse = response;
-  return NS_OK;
-}
-
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/LSDatabase.h
+++ b/dom/localstorage/LSDatabase.h
@@ -6,25 +6,28 @@
 
 #ifndef mozilla_dom_localstorage_LSDatabase_h
 #define mozilla_dom_localstorage_LSDatabase_h
 
 namespace mozilla {
 namespace dom {
 
 class LSDatabaseChild;
-class LSWriteOpResponse;
+class LSSnapshot;
 
 class LSDatabase final
 {
   LSDatabaseChild* mActor;
 
+  LSSnapshot* mSnapshot;
+
   const nsCString mOrigin;
 
   bool mAllowedToClose;
+  bool mRequestedAllowToClose;
 
 public:
   explicit LSDatabase(const nsACString& aOrigin);
 
   static LSDatabase*
   Get(const nsACString& aOrigin);
 
   NS_INLINE_DECL_REFCOUNTING(LSDatabase)
@@ -42,56 +45,70 @@ public:
   ClearActor()
   {
     AssertIsOnOwningThread();
     MOZ_ASSERT(mActor);
 
     mActor = nullptr;
   }
 
-  void
-  AllowToClose();
-
   bool
   IsAllowedToClose() const
   {
     AssertIsOnOwningThread();
 
     return mAllowedToClose;
   }
 
-  nsresult
-  GetLength(uint32_t* aResult);
+  void
+  RequestAllowToClose();
+
+  void
+  NoteFinishedSnapshot(LSSnapshot* aSnapshot);
 
   nsresult
-  GetKey(uint32_t aIndex,
+  GetLength(LSObject* aObject,
+            uint32_t* aResult);
+
+  nsresult
+  GetKey(LSObject* aObject,
+         uint32_t aIndex,
          nsAString& aResult);
 
   nsresult
-  GetItem(const nsAString& aKey,
+  GetItem(LSObject* aObject,
+          const nsAString& aKey,
           nsAString& aResult);
 
   nsresult
-  GetKeys(nsTArray<nsString>& aKeys);
+  GetKeys(LSObject* aObject,
+          nsTArray<nsString>& aKeys);
 
   nsresult
-  SetItem(const nsAString& aDocumentURI,
+  SetItem(LSObject* aObject,
           const nsAString& aKey,
           const nsAString& aValue,
-          LSWriteOpResponse& aResponse);
+          LSNotifyInfo& aNotifyInfo);
 
   nsresult
-  RemoveItem(const nsAString& aDocumentURI,
+  RemoveItem(LSObject* aObject,
              const nsAString& aKey,
-             LSWriteOpResponse& aResponse);
+             LSNotifyInfo& aNotifyInfo);
 
   nsresult
-  Clear(const nsAString& aDocumentURI,
-        LSWriteOpResponse& aResponse);
+  Clear(LSObject* aObject,
+        LSNotifyInfo& aNotifyInfo);
 
 private:
   ~LSDatabase();
+
+  nsresult
+  EnsureSnapshot(LSObject* aObject,
+                 bool aRequestedBySetItem);
+
+  void
+  AllowToClose();
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_localstorage_LSDatabase_h
--- a/dom/localstorage/LSObject.cpp
+++ b/dom/localstorage/LSObject.cpp
@@ -344,17 +344,17 @@ LSObject::GetLength(nsIPrincipal& aSubje
 
   nsresult rv = EnsureDatabase();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return 0;
   }
 
   uint32_t result;
-  rv = mDatabase->GetLength(&result);
+  rv = mDatabase->GetLength(this, &result);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return 0;
   }
 
   return result;
 }
 
@@ -373,17 +373,17 @@ LSObject::Key(uint32_t aIndex,
 
   nsresult rv = EnsureDatabase();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
   nsString result;
-  rv = mDatabase->GetKey(aIndex, result);
+  rv = mDatabase->GetKey(this, aIndex, result);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
   aResult = result;
 }
 
@@ -402,17 +402,17 @@ LSObject::GetItem(const nsAString& aKey,
 
   nsresult rv = EnsureDatabase();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
   nsString result;
-  rv = mDatabase->GetItem(aKey, result);
+  rv = mDatabase->GetItem(this, aKey, result);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
   aResult = result;
 }
 
@@ -427,17 +427,17 @@ LSObject::GetSupportedNames(nsTArray<nsS
     return;
   }
 
   nsresult rv = EnsureDatabase();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
 
-  rv = mDatabase->GetKeys(aNames);
+  rv = mDatabase->GetKeys(this, aNames);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
 }
 
 void
 LSObject::SetItem(const nsAString& aKey,
                   const nsAString& aValue,
@@ -452,30 +452,26 @@ LSObject::SetItem(const nsAString& aKey,
   }
 
   nsresult rv = EnsureDatabase();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
-  LSWriteOpResponse response;
-  rv = mDatabase->SetItem(mDocumentURI, aKey, aValue, response);
+  LSNotifyInfo info;
+  rv = mDatabase->SetItem(this, aKey, aValue, info);
+  if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) {
+    rv = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR;
+  }
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
-  LSNotifyInfo info;
-  rv = GetInfoFromResponse(response, info);
-  if (NS_FAILED(rv)) {
-    aError.Throw(rv);
-    return;
-  }
-
   if (info.changed()) {
     OnChange(aKey, info.oldValue(), aValue);
   }
 }
 
 void
 LSObject::RemoveItem(const nsAString& aKey,
                      nsIPrincipal& aSubjectPrincipal,
@@ -489,30 +485,23 @@ LSObject::RemoveItem(const nsAString& aK
   }
 
   nsresult rv = EnsureDatabase();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
-  LSWriteOpResponse response;
-  rv = mDatabase->RemoveItem(mDocumentURI, aKey, response);
+  LSNotifyInfo info;
+  rv = mDatabase->RemoveItem(this, aKey, info);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
-  LSNotifyInfo info;
-  rv = GetInfoFromResponse(response, info);
-  if (NS_FAILED(rv)) {
-    aError.Throw(rv);
-    return;
-  }
-
   if (info.changed()) {
     OnChange(aKey, info.oldValue(), VoidString());
   }
 }
 
 void
 LSObject::Clear(nsIPrincipal& aSubjectPrincipal,
                 ErrorResult& aError)
@@ -525,30 +514,23 @@ LSObject::Clear(nsIPrincipal& aSubjectPr
   }
 
   nsresult rv = EnsureDatabase();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
-  LSWriteOpResponse response;
-  rv = mDatabase->Clear(mDocumentURI, response);
+  LSNotifyInfo info;
+  rv = mDatabase->Clear(this, info);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aError.Throw(rv);
     return;
   }
 
-  LSNotifyInfo info;
-  rv = GetInfoFromResponse(response, info);
-  if (NS_FAILED(rv)) {
-    aError.Throw(rv);
-    return;
-  }
-
   if (info.changed()) {
     OnChange(VoidString(), VoidString(), VoidString());
   }
 }
 
 void
 LSObject::Open(nsIPrincipal& aSubjectPrincipal,
                ErrorResult& aError)
@@ -775,38 +757,16 @@ LSObject::DropObserver()
 {
   AssertIsOnOwningThread();
 
   if (mObserver) {
     mObserver = nullptr;
   }
 }
 
-nsresult
-LSObject::GetInfoFromResponse(const LSWriteOpResponse& aResponse,
-                              LSNotifyInfo& aInfo)
-{
-  AssertIsOnOwningThread();
-
-  if (aResponse.type() == LSWriteOpResponse::Tnsresult) {
-    nsresult errorCode = aResponse.get_nsresult();
-
-    if (errorCode == NS_ERROR_FILE_NO_DEVICE_SPACE) {
-      errorCode = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR;
-    }
-
-    return errorCode;
-  }
-
-  MOZ_ASSERT(aResponse.type() == LSWriteOpResponse::TLSNotifyInfo);
-
-  aInfo = aResponse.get_LSNotifyInfo();
-  return NS_OK;
-}
-
 void
 LSObject::OnChange(const nsAString& aKey,
                    const nsAString& aOldValue,
                    const nsAString& aNewValue)
 {
   AssertIsOnOwningThread();
 
   NotifyChange(/* aStorage */ this,
--- a/dom/localstorage/LSObject.h
+++ b/dom/localstorage/LSObject.h
@@ -21,24 +21,22 @@ namespace ipc {
 
 class PrincipalInfo;
 
 } // namespace ipc
 
 namespace dom {
 
 class LSDatabase;
-class LSNotifyInfo;
 class LSObjectChild;
 class LSObserver;
 class LSRequestChild;
 class LSRequestChildCallback;
 class LSRequestParams;
 class LSRequestResponse;
-class LSWriteOpResponse;
 
 class LSObject final
   : public Storage
 {
   typedef mozilla::ipc::PrincipalInfo PrincipalInfo;
 
   friend nsGlobalWindowInner;
 
@@ -75,16 +73,22 @@ public:
   CancelSyncLoop();
 
   void
   AssertIsOnOwningThread() const
   {
     NS_ASSERT_OWNINGTHREAD(LSObject);
   }
 
+  const nsString&
+  DocumentURI() const
+  {
+    return mDocumentURI;
+  }
+
   LSRequestChild*
   StartRequest(nsIEventTarget* aMainEventTarget,
                const LSRequestParams& aParams,
                LSRequestChildCallback* aCallback);
 
   // Storage overrides.
   StorageType
   Type() const override;
@@ -157,20 +161,16 @@ private:
   DropDatabase();
 
   nsresult
   EnsureObserver();
 
   void
   DropObserver();
 
-  nsresult
-  GetInfoFromResponse(const LSWriteOpResponse& aResponse,
-                      LSNotifyInfo& aInfo);
-
   void
   OnChange(const nsAString& aKey,
            const nsAString& aOldValue,
            const nsAString& aNewValue);
 
   // Storage overrides.
   void
   LastRelease() override;
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/LSSnapshot.cpp
@@ -0,0 +1,320 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "LSSnapshot.h"
+
+#include "nsContentUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+LSSnapshot::LSSnapshot(LSDatabase* aDatabase)
+  : mDatabase(aDatabase)
+  , mActor(nullptr)
+  , mExactUsage(0)
+  , mPeakUsage(0)
+#ifdef DEBUG
+  , mInitialized(false)
+  , mSentFinish(false)
+#endif
+{
+  AssertIsOnOwningThread();
+}
+
+LSSnapshot::~LSSnapshot()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mDatabase);
+  MOZ_ASSERT_IF(mInitialized, mSentFinish);
+
+  if (mActor) {
+    mActor->SendDeleteMeInternal();
+    MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
+  }
+}
+
+void
+LSSnapshot::SetActor(LSSnapshotChild* aActor)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aActor);
+  MOZ_ASSERT(!mActor);
+
+  mActor = aActor;
+}
+
+nsresult
+LSSnapshot::Init(const LSSnapshotInitInfo& aInitInfo)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  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];
+    mValues.Put(itemInfo.key(), itemInfo.value());
+  }
+
+  mExactUsage = aInitInfo.initialUsage();
+  mPeakUsage = aInitInfo.peakUsage();
+
+  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();
+
+  return NS_OK;
+}
+
+nsresult
+LSSnapshot::GetKey(uint32_t aIndex,
+                   nsAString& aResult)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  aResult.SetIsVoid(true);
+  for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+    if (aIndex == 0) {
+      aResult = iter.Key();
+      return NS_OK;
+    }
+    aIndex--;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSSnapshot::GetItem(const nsAString& aKey,
+                    nsAString& aResult)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  nsString result;
+  if (!mValues.Get(aKey, &result)) {
+    result.SetIsVoid(true);
+  }
+
+  aResult = result;
+  return NS_OK;
+}
+
+nsresult
+LSSnapshot::GetKeys(nsTArray<nsString>& aKeys)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+    aKeys.AppendElement(iter.Key());
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LSSnapshot::SetItem(const nsAString& aKey,
+                    const nsAString& aValue,
+                    LSNotifyInfo& aNotifyInfo)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  nsString oldValue;
+  nsresult rv = GetItem(aKey, oldValue);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool changed;
+  if (oldValue == aValue && oldValue.IsVoid() == aValue.IsVoid()) {
+    changed = false;
+  } else {
+    changed = true;
+
+    int64_t delta = static_cast<int64_t>(aValue.Length()) -
+                    static_cast<int64_t>(oldValue.Length());
+
+    if (oldValue.IsVoid()) {
+      delta += static_cast<int64_t>(aKey.Length());
+    }
+
+    rv = UpdateUsage(delta);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    mValues.Put(aKey, nsString(aValue));
+
+    LSSetItemInfo setItemInfo;
+    setItemInfo.key() = aKey;
+    setItemInfo.oldValue() = oldValue;
+    setItemInfo.value() = aValue;
+
+    mWriteInfos.AppendElement(std::move(setItemInfo));
+  }
+
+  aNotifyInfo.changed() = changed;
+  aNotifyInfo.oldValue() = oldValue;
+
+  return NS_OK;
+}
+
+nsresult
+LSSnapshot::RemoveItem(const nsAString& aKey,
+                       LSNotifyInfo& aNotifyInfo)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  nsString oldValue;
+  nsresult rv = GetItem(aKey, oldValue);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool changed;
+  if (oldValue.IsVoid()) {
+    changed = false;
+  } else {
+    changed = true;
+
+    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);
+
+    LSRemoveItemInfo removeItemInfo;
+    removeItemInfo.key() = aKey;
+    removeItemInfo.oldValue() = oldValue;
+
+    mWriteInfos.AppendElement(std::move(removeItemInfo));
+  }
+
+  aNotifyInfo.changed() = changed;
+  aNotifyInfo.oldValue() = oldValue;
+
+  return NS_OK;
+}
+
+nsresult
+LSSnapshot::Clear(LSNotifyInfo& aNotifyInfo)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mActor);
+  MOZ_ASSERT(mInitialized);
+  MOZ_ASSERT(!mSentFinish);
+
+  bool changed;
+  if (!mValues.Count()) {
+    changed = false;
+  } else {
+    changed = true;
+
+    DebugOnly<nsresult> rv = UpdateUsage(-mExactUsage);
+    MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+    mValues.Clear();
+
+    LSClearInfo clearInfo;
+
+    mWriteInfos.AppendElement(std::move(clearInfo));
+  }
+
+  aNotifyInfo.changed() = changed;
+
+  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);
+
+  int64_t newExactUsage = mExactUsage + aDelta;
+  if (newExactUsage > mPeakUsage) {
+    int64_t minSize = newExactUsage - mPeakUsage;
+    int64_t requestedSize = minSize + 4096;
+    int64_t size;
+    if (NS_WARN_IF(!mActor->SendIncreasePeakUsage(requestedSize,
+                                                  minSize,
+                                                  &size))) {
+      return NS_ERROR_FAILURE;
+    }
+
+    MOZ_ASSERT(size >= 0);
+
+    if (size == 0) {
+      return NS_ERROR_FILE_NO_DEVICE_SPACE;
+    }
+
+    mPeakUsage += size;
+  }
+
+  mExactUsage = newExactUsage;
+  return NS_OK;
+}
+
+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));
+
+#ifdef DEBUG
+  mSentFinish = true;
+#endif
+
+  mDatabase->NoteFinishedSnapshot(this);
+
+  return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/LSSnapshot.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LSSnapshot_h
+#define mozilla_dom_localstorage_LSSnapshot_h
+
+namespace mozilla {
+namespace dom {
+
+class LSSnapshotChild;
+
+class LSSnapshot final
+  : public nsIRunnable
+{
+  RefPtr<LSDatabase> mDatabase;
+
+  LSSnapshotChild* mActor;
+
+  nsDataHashtable<nsStringHashKey, nsString> mValues;
+  nsTArray<LSWriteInfo> mWriteInfos;
+
+  int64_t mExactUsage;
+  int64_t mPeakUsage;
+
+#ifdef DEBUG
+  bool mInitialized;
+  bool mSentFinish;
+#endif
+
+public:
+  explicit LSSnapshot(LSDatabase* aDatabase);
+
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(LSSnapshot);
+  }
+
+  void
+  SetActor(LSSnapshotChild* aActor);
+
+  void
+  ClearActor()
+  {
+    AssertIsOnOwningThread();
+    MOZ_ASSERT(mActor);
+
+    mActor = nullptr;
+  }
+
+  nsresult
+  Init(const LSSnapshotInitInfo& aInitInfo);
+
+  nsresult
+  GetLength(uint32_t* aResult);
+
+  nsresult
+  GetKey(uint32_t aIndex,
+         nsAString& aResult);
+
+  nsresult
+  GetItem(const nsAString& aKey,
+          nsAString& aResult);
+
+  nsresult
+  GetKeys(nsTArray<nsString>& aKeys);
+
+  nsresult
+  SetItem(const nsAString& aKey,
+          const nsAString& aValue,
+          LSNotifyInfo& aNotifyInfo);
+
+  nsresult
+  RemoveItem(const nsAString& aKey,
+             LSNotifyInfo& aNotifyInfo);
+
+  nsresult
+  Clear(LSNotifyInfo& aNotifyInfo);
+
+private:
+  ~LSSnapshot();
+
+  nsresult
+  UpdateUsage(int64_t aDelta);
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIRUNNABLE
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LSSnapshot_h
--- a/dom/localstorage/LocalStorageCommon.h
+++ b/dom/localstorage/LocalStorageCommon.h
@@ -181,16 +181,51 @@
  * interface.
  */
 
 namespace mozilla {
 namespace dom {
 
 extern const char16_t* kLocalStorageType;
 
+class MOZ_STACK_CLASS LSNotifyInfo
+{
+  bool mChanged;
+  nsString mOldValue;
+
+public:
+  LSNotifyInfo()
+    : mChanged(false)
+  { }
+
+  bool
+  changed() const
+  {
+    return mChanged;
+  }
+
+  bool&
+  changed()
+  {
+    return mChanged;
+  }
+
+  const nsString&
+  oldValue() const
+  {
+    return mOldValue;
+  }
+
+  nsString&
+  oldValue()
+  {
+    return mOldValue;
+  }
+};
+
 bool
 NextGenLocalStorageEnabled();
 
 bool
 CachedNextGenLocalStorageEnabled();
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/localstorage/PBackgroundLSDatabase.ipdl
+++ b/dom/localstorage/PBackgroundLSDatabase.ipdl
@@ -1,67 +1,53 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 include protocol PBackground;
+include protocol PBackgroundLSSnapshot;
 
 namespace mozilla {
 namespace dom {
 
-struct LSNotifyInfo
+struct LSItemInfo
 {
-  bool changed;
-  nsString oldValue;
+  nsString key;
+  nsString value;
 };
 
-union LSWriteOpResponse
+struct LSSnapshotInitInfo
 {
-  nsresult;
-  LSNotifyInfo;
+  LSItemInfo[] itemInfos;
+  int64_t initialUsage;
+  int64_t peakUsage;
 };
 
 sync protocol PBackgroundLSDatabase
 {
   manager PBackground;
+  manages PBackgroundLSSnapshot;
 
 parent:
   // The DeleteMe message is used to avoid a race condition between the parent
   // actor and the child actor. The PBackgroundLSDatabase protocol could be
   // simply destroyed by sending the __delete__ message from the child side.
   // However, that would destroy the child actor immediatelly and the parent
   // could be sending a message to the child at the same time resulting in a
   // routing error since the child actor wouldn't exist anymore. A routing
   // error typically causes a crash. The race can be prevented by doing the
   // teardown in two steps. First, we send the DeleteMe message to the parent
   // and the parent then sends the __delete__ message to the child.
   async DeleteMe();
 
   async AllowToClose();
 
-  sync GetLength()
-    returns (uint32_t length);
-
-  sync GetKey(uint32_t index)
-    returns (nsString key);
-
-  sync GetItem(nsString key)
-    returns (nsString value);
-
-  sync GetKeys()
-    returns (nsString[] keys);
-
-  sync SetItem(nsString documentURI, nsString key, nsString value)
-    returns (LSWriteOpResponse response);
-
-  sync RemoveItem(nsString documentURI, nsString key)
-    returns (LSWriteOpResponse response);
-
-  sync Clear(nsString documentURI)
-    returns (LSWriteOpResponse response);
+  sync PBackgroundLSSnapshot(nsString documentURI,
+                             int64_t requestedSize)
+    returns (LSSnapshotInitInfo initInfo);
 
 child:
   async __delete__();
 
   async RequestAllowToClose();
 };
 
 } // namespace dom
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSSnapshot.ipdl
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+include protocol PBackgroundLSDatabase;
+
+namespace mozilla {
+namespace dom {
+
+struct LSSetItemInfo
+{
+  nsString key;
+  nsString oldValue;
+  nsString value;
+};
+
+struct LSRemoveItemInfo
+{
+  nsString key;
+  nsString oldValue;
+};
+
+struct LSClearInfo
+{
+};
+
+union LSWriteInfo
+{
+  LSSetItemInfo;
+  LSRemoveItemInfo;
+  LSClearInfo;
+};
+
+struct LSSnapshotFinishInfo
+{
+  LSWriteInfo[] writeInfos;
+};
+
+sync protocol PBackgroundLSSnapshot
+{
+  manager PBackgroundLSDatabase;
+
+parent:
+  async DeleteMe();
+
+  async Finish(LSSnapshotFinishInfo finishInfo);
+
+  sync IncreasePeakUsage(int64_t requestedSize, int64_t minSize)
+    returns (int64_t size);
+
+child:
+  async __delete__();
+};
+
+} // namespace dom
+} // namespace mozilla
--- a/dom/localstorage/moz.build
+++ b/dom/localstorage/moz.build
@@ -28,25 +28,27 @@ EXPORTS.mozilla.dom += [
 UNIFIED_SOURCES += [
     'ActorsChild.cpp',
     'ActorsParent.cpp',
     'LocalStorageCommon.cpp',
     'LocalStorageManager2.cpp',
     'LSDatabase.cpp',
     'LSObject.cpp',
     'LSObserver.cpp',
+    'LSSnapshot.cpp',
     'ReportInternalError.cpp',
 ]
 
 IPDL_SOURCES += [
     'PBackgroundLSDatabase.ipdl',
     'PBackgroundLSObserver.ipdl',
     'PBackgroundLSRequest.ipdl',
     'PBackgroundLSSharedTypes.ipdlh',
     'PBackgroundLSSimpleRequest.ipdl',
+    'PBackgroundLSSnapshot.ipdl',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 
 if CONFIG['GNU_CXX']:
     CXXFLAGS += ['-Wno-error=shadow']
--- a/dom/localstorage/test/unit/head.js
+++ b/dom/localstorage/test/unit/head.js
@@ -38,16 +38,23 @@ function finishTest()
 {
   resetTesting();
 
   executeSoon(function() {
     do_test_finished();
   })
 }
 
+function continueToNextStep()
+{
+  executeSoon(function() {
+    testGenerator.next();
+  });
+}
+
 function continueToNextStepSync()
 {
   testGenerator.next();
 }
 
 function enableTesting()
 {
   Services.prefs.setBoolPref("dom.storage.testing", true);
--- a/dom/localstorage/test/unit/test_groupLimit.js
+++ b/dom/localstorage/test/unit/test_groupLimit.js
@@ -62,16 +62,21 @@ function* testSteps()
       is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code");
     }
   }
 
   info("Clearing first origin");
 
   storages[0].clear();
 
+  // Let the internal snapshot finish (usage is not descreased until all
+  // snapshots finish)..
+  continueToNextStep();
+  yield undefined;
+
   info("Verifying more data can be written");
 
   for (let i = 0; i < urls.length; i++) {
     storages[i].setItem("B", "");
   }
 
   finishTest();
 }
--- a/ipc/ipdl/sync-messages.ini
+++ b/ipc/ipdl/sync-messages.ini
@@ -917,29 +917,19 @@ description =
 [PRemoteDecoderManager::PRemoteVideoDecoder]
 description = See Bug 1505976 - investigate changing to async instead of matching GPU pattern
 [PVideoDecoderManager::PVideoDecoder]
 description =
 [PVideoDecoderManager::Readback]
 description =
 [PBackgroundStorage::Preload]
 description =
-[PBackgroundLSDatabase::GetLength]
-description =
-[PBackgroundLSDatabase::GetKey]
-description =
-[PBackgroundLSDatabase::GetItem]
+[PBackgroundLSDatabase::PBackgroundLSSnapshot]
 description =
-[PBackgroundLSDatabase::GetKeys]
-description =
-[PBackgroundLSDatabase::SetItem]
-description =
-[PBackgroundLSDatabase::RemoveItem]
-description =
-[PBackgroundLSDatabase::Clear]
+[PBackgroundLSSnapshot::IncreasePeakUsage]
 description =
 [PRemoteSpellcheckEngine::Check]
 description =
 [PRemoteSpellcheckEngine::CheckAndSuggest]
 description =
 [PRemoteSpellcheckEngine::SetDictionary]
 description =
 [PGPU::AddLayerTreeIdMapping]