Bug 1546723 - Part 1: Convert WriterOptimizer to a generic reusable class; r=asuth a=jcristau
authorJan Varga <jan.varga@gmail.com>
Wed, 15 May 2019 06:11:10 +0200
changeset 536837 3660181b7bca4a5b08bb4b8608ecff2a741a562c
parent 536836 00fdf7317dfdd5690b902f1f8e2a9ba78146f988
child 536838 4b55e5f016e61ba390b8fc32fdffb9eb08312728
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth, jcristau
bugs1546723
milestone68.0
Bug 1546723 - Part 1: Convert WriterOptimizer to a generic reusable class; r=asuth a=jcristau This patch creates a new generic class LSWriteOptimizer which can be used with any value type for specific write optimizations either on the parent side or the child side. Differential Revision: https://phabricator.services.mozilla.com/D31196
dom/localstorage/ActorsParent.cpp
dom/localstorage/LSWriteOptimizer.cpp
dom/localstorage/LSWriteOptimizer.h
dom/localstorage/moz.build
--- a/dom/localstorage/ActorsParent.cpp
+++ b/dom/localstorage/ActorsParent.cpp
@@ -13,16 +13,17 @@
 #include "mozIStorageService.h"
 #include "mozStorageCID.h"
 #include "mozStorageHelper.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/ClientManagerService.h"
+#include "mozilla/dom/LSWriteOptimizer.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"
@@ -1232,145 +1233,52 @@ nsresult LoadUsageFile(nsIFile* aUsageFi
   return NS_OK;
 }
 
 /*******************************************************************************
  * Non-actor class declarations
  ******************************************************************************/
 
 /**
- * Coalescing manipulation queue used by `Connection` and `DataStore`.  Used by
- * `Connection` to buffer and coalesce manipulations applied to the Datastore
- * in batches by Snapshot Checkpointing until flushed to disk.  Used by
- * `Datastore` to update `DataStore::mOrderedItems` efficiently/for code
- * simplification.  (DataStore does not actually depend on the coalescing, as
- * mutations are applied atomically when a Snapshot Checkpoints, and with
- * `Datastore::mValues` being updated at the same time the mutations are applied
- * to Datastore's mWriteOptimizer.)
+ * Coalescing manipulation queue used by `Datastore`.  Used by `Datastore` to
+ * update `Datastore::mOrderedItems` efficiently/for code simplification.
+ * (Datastore does not actually depend on the coalescing, as mutations are
+ * applied atomically when a Snapshot Checkpoints, and with `Datastore::mValues`
+ * being updated at the same time the mutations are applied to Datastore's
+ * mWriteOptimizer.)
  */
-class WriteOptimizer final {
-  class WriteInfo;
-  class AddItemInfo;
-  class UpdateItemInfo;
-  class RemoveItemInfo;
-  class ClearInfo;
-
-  nsAutoPtr<WriteInfo> mClearInfo;
-  nsClassHashtable<nsStringHashKey, WriteInfo> mWriteInfos;
-  int64_t mTotalDelta;
-
+class DatastoreWriteOptimizer final : public LSWriteOptimizer<LSValue> {
  public:
-  WriteOptimizer() : mTotalDelta(0) {}
-
-  WriteOptimizer(WriteOptimizer&& aWriteOptimizer)
-      : mClearInfo(std::move(aWriteOptimizer.mClearInfo)) {
-    AssertIsOnBackgroundThread();
-    MOZ_ASSERT(&aWriteOptimizer != this);
-
-    mWriteInfos.SwapElements(aWriteOptimizer.mWriteInfos);
-    mTotalDelta = aWriteOptimizer.mTotalDelta;
-    aWriteOptimizer.mTotalDelta = 0;
-  }
-
-  void AddItem(const nsString& aKey, const LSValue& aValue, int64_t aDelta = 0);
-
-  void UpdateItem(const nsString& aKey, const LSValue& aValue,
-                  int64_t aDelta = 0);
-
-  void RemoveItem(const nsString& aKey, int64_t aDelta = 0);
-
-  void Clear(int64_t aDelta = 0);
-
-  bool HasWrites() const {
-    AssertIsOnBackgroundThread();
-
-    return mClearInfo || !mWriteInfos.IsEmpty();
-  }
-
-  void ApplyWrites(nsTArray<LSItemInfo>& aOrderedItems);
-
-  nsresult PerformWrites(Connection* aConnection, bool aShadowWrites,
-                         int64_t& aOutUsage);
+  void ApplyAndReset(nsTArray<LSItemInfo>& aOrderedItems);
 };
 
 /**
- * Base class for specific mutations.  Each subclass knows how to `Perform` the
- * manipulation against a `Connection` and the "shadow" database (legacy
- * webappsstore.sqlite database that exists so LSNG can be disabled/safely
- * downgraded from.)
+ * Coalescing manipulation queue used by `Connection`.  Used by `Connection` to
+ * buffer and coalesce manipulations applied to the Datastore in batches by
+ * Snapshot Checkpointing until flushed to disk.
  */
-class WriteOptimizer::WriteInfo {
+class ConnectionWriteOptimizer final : public LSWriteOptimizer<LSValue> {
  public:
-  enum Type { AddItem = 0, UpdateItem, RemoveItem, Clear };
-
-  virtual Type GetType() = 0;
-
-  virtual nsresult Perform(Connection* aConnection, bool aShadowWrites) = 0;
-
-  virtual ~WriteInfo() = default;
-};
-
-/**
- * SetItem mutation where the key did not previously exist.
- */
-class WriteOptimizer::AddItemInfo : public WriteInfo {
-  nsString mKey;
-  LSValue mValue;
-
- public:
-  AddItemInfo(const nsAString& aKey, const LSValue& aValue)
-      : mKey(aKey), mValue(aValue) {}
-
-  const nsAString& GetKey() const { return mKey; }
-
-  const LSValue& GetValue() const { return mValue; }
+  nsresult Perform(Connection* aConnection, bool aShadowWrites,
+                   int64_t& aOutUsage);
 
  private:
-  Type GetType() override { return AddItem; }
-
-  nsresult Perform(Connection* aConnection, bool aShadowWrites) override;
-};
-
-/**
- * SetItem mutation where the key already existed.
- */
-class WriteOptimizer::UpdateItemInfo final : public AddItemInfo {
- public:
-  UpdateItemInfo(const nsAString& aKey, const LSValue& aValue)
-      : AddItemInfo(aKey, aValue) {}
-
- private:
-  Type GetType() override { return UpdateItem; }
-};
-
-class WriteOptimizer::RemoveItemInfo final : public WriteInfo {
-  nsString mKey;
-
- public:
-  explicit RemoveItemInfo(const nsAString& aKey) : mKey(aKey) {}
-
-  const nsAString& GetKey() const { return mKey; }
-
- private:
-  Type GetType() override { return RemoveItem; }
-
-  nsresult Perform(Connection* aConnection, bool aShadowWrites) override;
-};
-
-/**
- * Clear mutation.
- */
-class WriteOptimizer::ClearInfo final : public WriteInfo {
- public:
-  ClearInfo() {}
-
- private:
-  Type GetType() override { return Clear; }
-
-  nsresult Perform(Connection* aConnection, bool aShadowWrites) override;
+  /**
+   * Handlers for specific mutations.  Each method knows how to `Perform` the
+   * manipulation against a `Connection` and the "shadow" database (legacy
+   * webappsstore.sqlite database that exists so LSNG can be disabled/safely
+   * downgraded from.)
+   */
+  nsresult PerformInsertOrUpdate(Connection* aConnection, bool aShadowWrites,
+                                 const nsAString& aKey, const LSValue& aValue);
+
+  nsresult PerformDelete(Connection* aConnection, bool aShadowWrites,
+                         const nsAString& aKey);
+
+  nsresult PerformTruncate(Connection* aConnection, bool aShadowWrites);
 };
 
 class DatastoreOperationBase : public Runnable {
   nsCOMPtr<nsIEventTarget> mOwningEventTarget;
   nsresult mResultCode;
   Atomic<bool> mMayProceedOnNonOwningThread;
   bool mMayProceed;
 
@@ -1495,17 +1403,17 @@ class Connection final {
 
   RefPtr<ConnectionThread> mConnectionThread;
   RefPtr<QuotaClient> mQuotaClient;
   nsCOMPtr<nsITimer> mFlushTimer;
   nsCOMPtr<mozIStorageConnection> mStorageConnection;
   nsAutoPtr<ArchivedOriginScope> mArchivedOriginScope;
   nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement>
       mCachedStatements;
-  WriteOptimizer mWriteOptimizer;
+  ConnectionWriteOptimizer mWriteOptimizer;
   const nsCString mSuffix;
   const nsCString mGroup;
   const nsCString mOrigin;
   nsString mDirectoryPath;
   /**
    * Propagated from PrepareDatastoreOp. PrepareDatastoreOp may defer the
    * creation of the localstorage client directory and database on the
    * QuotaManager IO thread in its DatabaseWork method to
@@ -1538,19 +1446,18 @@ class Connection final {
   // This method is used to asynchronously execute a connection datastore
   // operation on the connection thread.
   void Dispatch(ConnectionDatastoreOperationBase* aOp);
 
   // This method is used to asynchronously close the storage connection on the
   // connection thread.
   void Close(nsIRunnable* aCallback);
 
-  void AddItem(const nsString& aKey, const LSValue& aValue, int64_t aDelta);
-
-  void UpdateItem(const nsString& aKey, const LSValue& aValue, int64_t aDelta);
+  void SetItem(const nsString& aKey, const LSValue& aValue, int64_t aDelta,
+               bool aIsNewItem);
 
   void RemoveItem(const nsString& aKey, int64_t aDelta);
 
   void Clear(int64_t aDelta);
 
   void BeginUpdateBatch();
 
   void EndUpdateBatch();
@@ -1649,24 +1556,26 @@ class Connection::InitOriginHelper final
   ~InitOriginHelper() {}
 
   nsresult RunOnIOThread();
 
   NS_DECL_NSIRUNNABLE
 };
 
 class Connection::FlushOp final : public ConnectionDatastoreOperationBase {
-  WriteOptimizer mWriteOptimizer;
+  ConnectionWriteOptimizer mWriteOptimizer;
   bool mShadowWrites;
 
  public:
-  FlushOp(Connection* aConnection, WriteOptimizer&& aWriteOptimizer);
+  FlushOp(Connection* aConnection, ConnectionWriteOptimizer&& aWriteOptimizer);
 
  private:
   nsresult DoDatastoreWork() override;
+
+  void Cleanup() override;
 };
 
 class Connection::CloseOp final : public ConnectionDatastoreOperationBase {
   nsCOMPtr<nsIRunnable> mCallback;
 
  public:
   CloseOp(Connection* aConnection, nsIRunnable* aCallback)
       : ConnectionDatastoreOperationBase(aConnection,
@@ -1755,17 +1664,17 @@ class Datastore final
    */
   nsDataHashtable<nsStringHashKey, LSValue> mValues;
   /**
    * The authoritative ordered state of the Datastore; mValue also exists as an
    * unordered hashtable for efficient lookup.
    */
   nsTArray<LSItemInfo> mOrderedItems;
   nsTArray<int64_t> mPendingUsageDeltas;
-  WriteOptimizer mWriteOptimizer;
+  DatastoreWriteOptimizer mWriteOptimizer;
   const nsCString mOrigin;
   const uint32_t mPrivateBrowsingId;
   int64_t mUsage;
   int64_t mUpdateBatchUsage;
   int64_t mSizeOfKeys;
   int64_t mSizeOfItems;
   bool mClosed;
 #ifdef DEBUG
@@ -3719,149 +3628,122 @@ already_AddRefed<mozilla::dom::quota::Cl
 
   RefPtr<QuotaClient> client = new QuotaClient();
   return client.forget();
 }
 
 }  // namespace localstorage
 
 /*******************************************************************************
- * WriteOptimizer
+ * DatastoreWriteOptimizer
  ******************************************************************************/
 
-void WriteOptimizer::AddItem(const nsString& aKey, const LSValue& aValue,
-                             int64_t aDelta) {
-  AssertIsOnBackgroundThread();
-
-  WriteInfo* existingWriteInfo;
-  nsAutoPtr<WriteInfo> newWriteInfo;
-  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
-      existingWriteInfo->GetType() == WriteInfo::RemoveItem) {
-    newWriteInfo = new UpdateItemInfo(aKey, aValue);
-  } else {
-    newWriteInfo = new AddItemInfo(aKey, aValue);
-  }
-  mWriteInfos.Put(aKey, newWriteInfo.forget());
-
-  mTotalDelta += aDelta;
-}
-
-void WriteOptimizer::UpdateItem(const nsString& aKey, const LSValue& aValue,
-                                int64_t aDelta) {
-  AssertIsOnBackgroundThread();
-
-  WriteInfo* existingWriteInfo;
-  nsAutoPtr<WriteInfo> newWriteInfo;
-  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
-      existingWriteInfo->GetType() == WriteInfo::AddItem) {
-    newWriteInfo = new AddItemInfo(aKey, aValue);
-  } else {
-    newWriteInfo = new UpdateItemInfo(aKey, aValue);
-  }
-  mWriteInfos.Put(aKey, newWriteInfo.forget());
-
-  mTotalDelta += aDelta;
-}
-
-void WriteOptimizer::RemoveItem(const nsString& aKey, int64_t aDelta) {
-  AssertIsOnBackgroundThread();
-
-  WriteInfo* existingWriteInfo;
-  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
-      existingWriteInfo->GetType() == WriteInfo::AddItem) {
-    mWriteInfos.Remove(aKey);
-  } else {
-    nsAutoPtr<WriteInfo> newWriteInfo(new RemoveItemInfo(aKey));
-    mWriteInfos.Put(aKey, newWriteInfo.forget());
-  }
-
-  mTotalDelta += aDelta;
-}
-
-void WriteOptimizer::Clear(int64_t aDelta) {
-  AssertIsOnBackgroundThread();
-
-  mWriteInfos.Clear();
-
-  if (!mClearInfo) {
-    mClearInfo = new ClearInfo();
-  }
-
-  mTotalDelta += aDelta;
-}
-
-void WriteOptimizer::ApplyWrites(nsTArray<LSItemInfo>& aOrderedItems) {
-  AssertIsOnBackgroundThread();
-
-  if (mClearInfo) {
+void DatastoreWriteOptimizer::ApplyAndReset(
+    nsTArray<LSItemInfo>& aOrderedItems) {
+  AssertIsOnOwningThread();
+
+  if (mTruncateInfo) {
     aOrderedItems.Clear();
-    mClearInfo = nullptr;
+    mTruncateInfo = nullptr;
   }
 
   for (int32_t index = aOrderedItems.Length() - 1; index >= 0; index--) {
     LSItemInfo& item = aOrderedItems[index];
 
     if (auto entry = mWriteInfos.Lookup(item.key())) {
       WriteInfo* writeInfo = entry.Data();
 
       switch (writeInfo->GetType()) {
-        case WriteInfo::RemoveItem:
+        case WriteInfo::DeleteItem:
           aOrderedItems.RemoveElementAt(index);
           entry.Remove();
           break;
 
         case WriteInfo::UpdateItem: {
           auto updateItemInfo = static_cast<UpdateItemInfo*>(writeInfo);
           item.value() = updateItemInfo->GetValue();
           entry.Remove();
           break;
         }
 
-        case WriteInfo::AddItem:
+        case WriteInfo::InsertItem:
           break;
 
         default:
           MOZ_CRASH("Bad type!");
       }
     }
   }
 
   for (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) {
     WriteInfo* writeInfo = iter.Data();
 
-    MOZ_ASSERT(writeInfo->GetType() == WriteInfo::AddItem);
-
-    auto addItemInfo = static_cast<AddItemInfo*>(writeInfo);
+    MOZ_ASSERT(writeInfo->GetType() == WriteInfo::InsertItem);
+
+    auto insertItemInfo = static_cast<InsertItemInfo*>(writeInfo);
 
     LSItemInfo* itemInfo = aOrderedItems.AppendElement();
-    itemInfo->key() = addItemInfo->GetKey();
-    itemInfo->value() = addItemInfo->GetValue();
+    itemInfo->key() = insertItemInfo->GetKey();
+    itemInfo->value() = insertItemInfo->GetValue();
   }
 
   mWriteInfos.Clear();
 }
 
-nsresult WriteOptimizer::PerformWrites(Connection* aConnection,
-                                       bool aShadowWrites, int64_t& aOutUsage) {
+/*******************************************************************************
+ * ConnectionWriteOptimizer
+ ******************************************************************************/
+
+nsresult ConnectionWriteOptimizer::Perform(Connection* aConnection,
+                                           bool aShadowWrites,
+                                           int64_t& aOutUsage) {
   AssertIsOnConnectionThread();
   MOZ_ASSERT(aConnection);
 
   nsresult rv;
 
-  if (mClearInfo) {
-    rv = mClearInfo->Perform(aConnection, aShadowWrites);
+  if (mTruncateInfo) {
+    rv = PerformTruncate(aConnection, aShadowWrites);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
   for (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) {
-    rv = iter.Data()->Perform(aConnection, aShadowWrites);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
+    WriteInfo* writeInfo = iter.Data();
+
+    switch (writeInfo->GetType()) {
+      case WriteInfo::InsertItem:
+      case WriteInfo::UpdateItem: {
+        auto insertItemInfo = static_cast<InsertItemInfo*>(writeInfo);
+
+        rv = PerformInsertOrUpdate(aConnection, aShadowWrites,
+                                   insertItemInfo->GetKey(),
+                                   insertItemInfo->GetValue());
+        if (NS_WARN_IF(NS_FAILED(rv))) {
+          return rv;
+        }
+
+        break;
+      }
+
+      case WriteInfo::DeleteItem: {
+        auto deleteItemInfo = static_cast<DeleteItemInfo*>(writeInfo);
+
+        rv =
+            PerformDelete(aConnection, aShadowWrites, deleteItemInfo->GetKey());
+        if (NS_WARN_IF(NS_FAILED(rv))) {
+          return rv;
+        }
+
+        break;
+      }
+
+      default:
+        MOZ_CRASH("Bad type!");
     }
   }
 
   Connection::CachedStatement stmt;
   rv = aConnection->GetCachedStatement(
       NS_LITERAL_CSTRING("UPDATE database "
                          "SET usage = usage + :delta"),
       &stmt);
@@ -3901,49 +3783,50 @@ nsresult WriteOptimizer::PerformWrites(C
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   aOutUsage = usage;
   return NS_OK;
 }
 
-nsresult WriteOptimizer::AddItemInfo::Perform(Connection* aConnection,
-                                              bool aShadowWrites) {
+nsresult ConnectionWriteOptimizer::PerformInsertOrUpdate(
+    Connection* aConnection, bool aShadowWrites, const nsAString& aKey,
+    const LSValue& aValue) {
   AssertIsOnConnectionThread();
   MOZ_ASSERT(aConnection);
 
   Connection::CachedStatement stmt;
   nsresult rv = aConnection->GetCachedStatement(
       NS_LITERAL_CSTRING(
           "INSERT OR REPLACE INTO data (key, value, utf16Length, compressed) "
           "VALUES(:key, :value, :utf16Length, :compressed)"),
       &stmt);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
-  }
-
-  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), mValue);
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), aKey);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), aValue);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("utf16Length"),
-                             mValue.UTF16Length());
+                             aValue.UTF16Length());
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("compressed"),
-                             mValue.IsCompressed());
+                             aValue.IsCompressed());
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = stmt->Execute();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
@@ -3973,57 +3856,58 @@ nsresult WriteOptimizer::AddItemInfo::Pe
   nsCString scope = Scheme0Scope(archivedOriginScope->OriginSuffix(),
                                  archivedOriginScope->OriginNoSuffix());
 
   rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"), scope);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
-  }
-
-  if (mValue.IsCompressed()) {
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), aKey);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (aValue.IsCompressed()) {
     nsCString value;
-    if (NS_WARN_IF(!SnappyUncompress(mValue, value))) {
+    if (NS_WARN_IF(!SnappyUncompress(aValue, value))) {
       return NS_ERROR_FAILURE;
     }
     rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), value);
   } else {
-    rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), mValue);
+    rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), aValue);
   }
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = stmt->Execute();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
-nsresult WriteOptimizer::RemoveItemInfo::Perform(Connection* aConnection,
-                                                 bool aShadowWrites) {
+nsresult ConnectionWriteOptimizer::PerformDelete(Connection* aConnection,
+                                                 bool aShadowWrites,
+                                                 const nsAString& aKey) {
   AssertIsOnConnectionThread();
   MOZ_ASSERT(aConnection);
 
   Connection::CachedStatement stmt;
   nsresult rv =
       aConnection->GetCachedStatement(NS_LITERAL_CSTRING("DELETE FROM data "
                                                          "WHERE key = :key;"),
                                       &stmt);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey);
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), aKey);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = stmt->Execute();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
@@ -4042,31 +3926,31 @@ nsresult WriteOptimizer::RemoveItemInfo:
     return rv;
   }
 
   rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey);
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), aKey);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = stmt->Execute();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
-nsresult WriteOptimizer::ClearInfo::Perform(Connection* aConnection,
-                                            bool aShadowWrites) {
+nsresult ConnectionWriteOptimizer::PerformTruncate(Connection* aConnection,
+                                                   bool aShadowWrites) {
   AssertIsOnConnectionThread();
   MOZ_ASSERT(aConnection);
 
   Connection::CachedStatement stmt;
   nsresult rv = aConnection->GetCachedStatement(
       NS_LITERAL_CSTRING("DELETE FROM data;"), &stmt);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
@@ -4256,44 +4140,40 @@ void Connection::Close(nsIRunnable* aCal
     mFlushTimer = nullptr;
   }
 
   RefPtr<CloseOp> op = new CloseOp(this, aCallback);
 
   Dispatch(op);
 }
 
-void Connection::AddItem(const nsString& aKey, const LSValue& aValue,
-                         int64_t aDelta) {
+void Connection::SetItem(const nsString& aKey, const LSValue& aValue,
+                         int64_t aDelta, bool aIsNewItem) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mInUpdateBatch);
 
-  mWriteOptimizer.AddItem(aKey, aValue, aDelta);
-}
-
-void Connection::UpdateItem(const nsString& aKey, const LSValue& aValue,
-                            int64_t aDelta) {
-  AssertIsOnOwningThread();
-  MOZ_ASSERT(mInUpdateBatch);
-
-  mWriteOptimizer.UpdateItem(aKey, aValue, aDelta);
+  if (aIsNewItem) {
+    mWriteOptimizer.InsertItem(aKey, aValue, aDelta);
+  } else {
+    mWriteOptimizer.UpdateItem(aKey, aValue, aDelta);
+  }
 }
 
 void Connection::RemoveItem(const nsString& aKey, int64_t aDelta) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mInUpdateBatch);
 
-  mWriteOptimizer.RemoveItem(aKey, aDelta);
+  mWriteOptimizer.DeleteItem(aKey, aDelta);
 }
 
 void Connection::Clear(int64_t aDelta) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mInUpdateBatch);
 
-  mWriteOptimizer.Clear(aDelta);
+  mWriteOptimizer.Truncate(aDelta);
 }
 
 void Connection::BeginUpdateBatch() {
   AssertIsOnOwningThread();
   MOZ_ASSERT(!mInUpdateBatch);
 
 #ifdef DEBUG
   mInUpdateBatch = true;
@@ -4634,17 +4514,17 @@ Connection::InitOriginHelper::Run() {
 
   mWaiting = false;
   lock.Notify();
 
   return NS_OK;
 }
 
 Connection::FlushOp::FlushOp(Connection* aConnection,
-                             WriteOptimizer&& aWriteOptimizer)
+                             ConnectionWriteOptimizer&& aWriteOptimizer)
     : ConnectionDatastoreOperationBase(aConnection),
       mWriteOptimizer(std::move(aWriteOptimizer)),
       mShadowWrites(gShadowWrites) {}
 
 nsresult Connection::FlushOp::DoDatastoreWork() {
   AssertIsOnConnectionThread();
   MOZ_ASSERT(mConnection);
 
@@ -4711,17 +4591,17 @@ nsresult Connection::FlushOp::DoDatastor
   }
 
   rv = stmt->Execute();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   int64_t usage;
-  rv = mWriteOptimizer.PerformWrites(mConnection, mShadowWrites, usage);
+  rv = mWriteOptimizer.Perform(mConnection, mShadowWrites, usage);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   nsCOMPtr<nsIFile> usageFile;
   rv = GetUsageFile(mConnection->DirectoryPath(), getter_AddRefs(usageFile));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
@@ -4774,16 +4654,26 @@ nsresult Connection::FlushOp::DoDatastor
                              });
 
   MOZ_ALWAYS_SUCCEEDS(
       quotaManager->IOThread()->Dispatch(runnable, NS_DISPATCH_NORMAL));
 
   return NS_OK;
 }
 
+void Connection::FlushOp::Cleanup() {
+  AssertIsOnOwningThread();
+
+  mWriteOptimizer.Reset();
+
+  MOZ_ASSERT(!mWriteOptimizer.HasWrites());
+
+  ConnectionDatastoreOperationBase::Cleanup();
+}
+
 nsresult Connection::CloseOp::DoDatastoreWork() {
   AssertIsOnConnectionThread();
   MOZ_ASSERT(mConnection);
 
   if (mConnection->StorageConnection()) {
     mConnection->CloseStorageConnection();
   }
 
@@ -5283,45 +5173,40 @@ void Datastore::SetItem(Database* aDatab
 
     NotifySnapshots(aDatabase, aKey, oldValue, /* affectsOrder */ isNewItem);
 
     mValues.Put(aKey, aValue);
 
     int64_t delta;
 
     if (isNewItem) {
-      mWriteOptimizer.AddItem(aKey, aValue);
+      mWriteOptimizer.InsertItem(aKey, aValue);
 
       int64_t sizeOfKey = static_cast<int64_t>(aKey.Length());
 
       delta = sizeOfKey + static_cast<int64_t>(aValue.UTF16Length());
 
       mUpdateBatchUsage += delta;
 
       mSizeOfKeys += sizeOfKey;
       mSizeOfItems += sizeOfKey + static_cast<int64_t>(aValue.Length());
-      ;
     } else {
       mWriteOptimizer.UpdateItem(aKey, aValue);
 
       delta = static_cast<int64_t>(aValue.UTF16Length()) -
               static_cast<int64_t>(oldValue.UTF16Length());
 
       mUpdateBatchUsage += delta;
 
       mSizeOfItems += static_cast<int64_t>(aValue.Length()) -
                       static_cast<int64_t>(oldValue.Length());
     }
 
     if (IsPersistent()) {
-      if (oldValue.IsVoid()) {
-        mConnection->AddItem(aKey, aValue, delta);
-      } else {
-        mConnection->UpdateItem(aKey, aValue, delta);
-      }
+      mConnection->SetItem(aKey, aValue, delta, isNewItem);
     }
   }
 
   NotifyObservers(aDatabase, aDocumentURI, aKey, aOldValue, aValue);
 }
 
 void Datastore::RemoveItem(Database* aDatabase, const nsString& aDocumentURI,
                            const nsString& aKey, const LSValue& aOldValue) {
@@ -5333,17 +5218,17 @@ void Datastore::RemoveItem(Database* aDa
   LSValue oldValue;
   GetItem(aKey, oldValue);
 
   if (!oldValue.IsVoid()) {
     NotifySnapshots(aDatabase, aKey, oldValue, /* aAffectsOrder */ true);
 
     mValues.Remove(aKey);
 
-    mWriteOptimizer.RemoveItem(aKey);
+    mWriteOptimizer.DeleteItem(aKey);
 
     int64_t sizeOfKey = static_cast<int64_t>(aKey.Length());
 
     int64_t delta = -sizeOfKey - static_cast<int64_t>(oldValue.UTF16Length());
 
     mUpdateBatchUsage += delta;
 
     mSizeOfKeys -= sizeOfKey;
@@ -5372,17 +5257,17 @@ void Datastore::Clear(Database* aDatabas
       delta += -static_cast<int64_t>(key.Length()) -
                static_cast<int64_t>(value.UTF16Length());
 
       NotifySnapshots(aDatabase, key, value, /* aAffectsOrder */ true);
     }
 
     mValues.Clear();
 
-    mWriteOptimizer.Clear();
+    mWriteOptimizer.Truncate();
 
     mUpdateBatchUsage += delta;
 
     mSizeOfKeys = 0;
     mSizeOfItems = 0;
 
     if (IsPersistent()) {
       mConnection->Clear(delta);
@@ -5432,17 +5317,19 @@ void Datastore::BeginUpdateBatch(int64_t
 #endif
 }
 
 int64_t Datastore::EndUpdateBatch(int64_t aSnapshotPeakUsage) {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(!mClosed);
   MOZ_ASSERT(mInUpdateBatch);
 
-  mWriteOptimizer.ApplyWrites(mOrderedItems);
+  mWriteOptimizer.ApplyAndReset(mOrderedItems);
+
+  MOZ_ASSERT(!mWriteOptimizer.HasWrites());
 
   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
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/LSWriteOptimizer.cpp
@@ -0,0 +1,76 @@
+/* -*- 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 "LSWriteOptimizer.h"
+
+namespace mozilla {
+namespace dom {
+
+void LSWriteOptimizerBase::DeleteItem(const nsAString& aKey, int64_t aDelta) {
+  AssertIsOnOwningThread();
+
+  WriteInfo* existingWriteInfo;
+  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+      existingWriteInfo->GetType() == WriteInfo::InsertItem) {
+    mWriteInfos.Remove(aKey);
+  } else {
+    nsAutoPtr<WriteInfo> newWriteInfo(new DeleteItemInfo(aKey));
+    mWriteInfos.Put(aKey, newWriteInfo.forget());
+  }
+
+  mTotalDelta += aDelta;
+}
+
+void LSWriteOptimizerBase::Truncate(int64_t aDelta) {
+  AssertIsOnOwningThread();
+
+  mWriteInfos.Clear();
+
+  if (!mTruncateInfo) {
+    mTruncateInfo = new TruncateInfo();
+  }
+
+  mTotalDelta += aDelta;
+}
+
+template <typename T, typename U>
+void LSWriteOptimizer<T, U>::InsertItem(const nsAString& aKey, const T& aValue,
+                                        int64_t aDelta) {
+  AssertIsOnOwningThread();
+
+  WriteInfo* existingWriteInfo;
+  nsAutoPtr<WriteInfo> newWriteInfo;
+  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+      existingWriteInfo->GetType() == WriteInfo::DeleteItem) {
+    newWriteInfo = new UpdateItemInfo(aKey, aValue);
+  } else {
+    newWriteInfo = new InsertItemInfo(aKey, aValue);
+  }
+  mWriteInfos.Put(aKey, newWriteInfo.forget());
+
+  mTotalDelta += aDelta;
+}
+
+template <typename T, typename U>
+void LSWriteOptimizer<T, U>::UpdateItem(const nsAString& aKey, const T& aValue,
+                                        int64_t aDelta) {
+  AssertIsOnOwningThread();
+
+  WriteInfo* existingWriteInfo;
+  nsAutoPtr<WriteInfo> newWriteInfo;
+  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+      existingWriteInfo->GetType() == WriteInfo::InsertItem) {
+    newWriteInfo = new InsertItemInfo(aKey, aValue);
+  } else {
+    newWriteInfo = new UpdateItemInfo(aKey, aValue);
+  }
+  mWriteInfos.Put(aKey, newWriteInfo.forget());
+
+  mTotalDelta += aDelta;
+}
+
+}  // namespace dom
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/LSWriteOptimizer.h
@@ -0,0 +1,152 @@
+/* -*- 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_LSWriteOptimizer_h
+#define mozilla_dom_localstorage_LSWriteOptimizer_h
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * Base class for coalescing manipulation queue.
+ */
+class LSWriteOptimizerBase {
+ protected:
+  class WriteInfo;
+  class DeleteItemInfo;
+  class TruncateInfo;
+
+  nsAutoPtr<WriteInfo> mTruncateInfo;
+  nsClassHashtable<nsStringHashKey, WriteInfo> mWriteInfos;
+  int64_t mTotalDelta;
+
+  NS_DECL_OWNINGTHREAD
+
+ public:
+  LSWriteOptimizerBase() : mTotalDelta(0) {}
+
+  LSWriteOptimizerBase(LSWriteOptimizerBase&& aWriteOptimizer)
+      : mTruncateInfo(std::move(aWriteOptimizer.mTruncateInfo)) {
+    AssertIsOnOwningThread();
+    MOZ_ASSERT(&aWriteOptimizer != this);
+
+    mWriteInfos.SwapElements(aWriteOptimizer.mWriteInfos);
+    mTotalDelta = aWriteOptimizer.mTotalDelta;
+    aWriteOptimizer.mTotalDelta = 0;
+  }
+
+  void AssertIsOnOwningThread() const {
+    NS_ASSERT_OWNINGTHREAD(LSWriteOptimizerBase);
+  }
+
+  void DeleteItem(const nsAString& aKey, int64_t aDelta = 0);
+
+  void Truncate(int64_t aDelta = 0);
+
+  bool HasWrites() const {
+    AssertIsOnOwningThread();
+
+    return mTruncateInfo || !mWriteInfos.IsEmpty();
+  }
+
+  void Reset() {
+    AssertIsOnOwningThread();
+
+    mTruncateInfo = nullptr;
+    mWriteInfos.Clear();
+  }
+};
+
+/**
+ * Base class for specific mutations.
+ */
+class LSWriteOptimizerBase::WriteInfo {
+ public:
+  virtual ~WriteInfo() = default;
+
+  enum Type { InsertItem = 0, UpdateItem, DeleteItem, Truncate };
+
+  virtual Type GetType() = 0;
+};
+
+class LSWriteOptimizerBase::DeleteItemInfo final : public WriteInfo {
+  nsString mKey;
+
+ public:
+  explicit DeleteItemInfo(const nsAString& aKey) : mKey(aKey) {}
+
+  const nsAString& GetKey() const { return mKey; }
+
+ private:
+  Type GetType() override { return DeleteItem; }
+};
+
+/**
+ * Truncate mutation.
+ */
+class LSWriteOptimizerBase::TruncateInfo final : public WriteInfo {
+ public:
+  TruncateInfo() {}
+
+ private:
+  Type GetType() override { return Truncate; }
+};
+
+/**
+ * Coalescing manipulation queue.
+ */
+template <typename T, typename U = T>
+class LSWriteOptimizer;
+
+template <typename T, typename U>
+class LSWriteOptimizer : public LSWriteOptimizerBase {
+ protected:
+  class InsertItemInfo;
+  class UpdateItemInfo;
+
+ public:
+  void InsertItem(const nsAString& aKey, const T& aValue, int64_t aDelta = 0);
+
+  void UpdateItem(const nsAString& aKey, const T& aValue, int64_t aDelta = 0);
+};
+
+/**
+ * Insert mutation (the key did not previously exist).
+ */
+template <typename T, typename U>
+class LSWriteOptimizer<T, U>::InsertItemInfo : public WriteInfo {
+  nsString mKey;
+  U mValue;
+
+ public:
+  InsertItemInfo(const nsAString& aKey, const T& aValue)
+      : mKey(aKey), mValue(aValue) {}
+
+  const nsAString& GetKey() const { return mKey; }
+
+  const T& GetValue() const { return mValue; }
+
+ private:
+  WriteInfo::Type GetType() override { return InsertItem; }
+};
+
+/**
+ * Update mutation (the key already existed).
+ */
+template <typename T, typename U>
+class LSWriteOptimizer<T, U>::UpdateItemInfo final : public InsertItemInfo {
+ public:
+  UpdateItemInfo(const nsAString& aKey, const T& aValue)
+      : InsertItemInfo(aKey, aValue) {}
+
+ private:
+  WriteInfo::Type GetType() override { return WriteInfo::UpdateItem; }
+};
+
+}  // namespace dom
+}  // namespace mozilla
+
+#endif  // mozilla_dom_localstorage_LSWriteOptimizer_h
--- a/dom/localstorage/moz.build
+++ b/dom/localstorage/moz.build
@@ -30,29 +30,31 @@ EXPORTS.mozilla.dom.localstorage += [
 
 EXPORTS.mozilla.dom += [
     'LocalStorageCommon.h',
     'LocalStorageManager2.h',
     'LSObject.h',
     'LSObserver.h',
     'LSSnapshot.h',
     'LSValue.h',
+    'LSWriteOptimizer.h',
     'SnappyUtils.h',
 ]
 
 UNIFIED_SOURCES += [
     'ActorsChild.cpp',
     'ActorsParent.cpp',
     'LocalStorageCommon.cpp',
     'LocalStorageManager2.cpp',
     'LSDatabase.cpp',
     'LSObject.cpp',
     'LSObserver.cpp',
     'LSSnapshot.cpp',
     'LSValue.cpp',
+    'LSWriteOptimizer.cpp',
     'ReportInternalError.cpp',
     'SnappyUtils.cpp',
 ]
 
 IPDL_SOURCES += [
     'PBackgroundLSDatabase.ipdl',
     'PBackgroundLSObserver.ipdl',
     'PBackgroundLSRequest.ipdl',