Bug 1408710 - Don't pass around GUIDs of individual record store success, just an aggregate counter r=rnewman draft
authorGrigory Kruglov <gkruglov@mozilla.com>
Mon, 26 Feb 2018 15:12:21 -0500
changeset 760195 e7443ffabde02059008e6c833ee52c45e206ec81
parent 760194 62f5f7940bb8db9a18704edfd0b9cb38eb410b71
push id100565
push userbmo:gkruglov@mozilla.com
push dateMon, 26 Feb 2018 23:36:11 +0000
reviewersrnewman
bugs1408710
milestone60.0a1
Bug 1408710 - Don't pass around GUIDs of individual record store success, just an aggregate counter r=rnewman We don't use these GUIDs anywhere, and this change lets us kill off some expensive data structures necessary to maintain lists of successfully uploaded GUIDs, particularly during an upload. MozReview-Commit-ID: F0kcY8o8DUw
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestBookmarks.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestPasswordsRepository.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/ThreadedRepositoryTestCase.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/TestStoreTracking.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java
mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/testhelpers/WBORepository.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/VersioningDelegateHelper.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksSessionHelper.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/HistorySessionHelper.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadDispatcher.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/StoreBatchTracker.java
mobile/android/services/src/test/java/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
mobile/android/services/src/test/java/org/mozilla/gecko/background/testhelpers/WBORepository.java
mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java
mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java
mobile/android/services/src/test/java/org/mozilla/gecko/sync/synchronizer/StoreBatchTrackerTest.java
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestBookmarks.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestBookmarks.java
@@ -604,17 +604,17 @@ public class TestBookmarks extends Andro
           while (iter.hasNext()) {
             tracked.add(iter.next());
           }
         }
         finishAndNotify(session);
       }
 
       @Override
-      public void onRecordStoreSucceeded(String guid) {
+      public void onRecordStoreSucceeded(int count) {
       }
 
       @Override
       public void onRecordStoreReconciled(String guid, String oldGuid, Integer newVersion) {
       }
 
       @Override
       public void onStoreFailed(Exception e) {
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java
@@ -317,17 +317,17 @@ public class TestFormHistoryRepositorySe
     insertTwoRecords(session);
 
     FormHistoryRecord rec;
 
     // remote regular, local missing => should store.
     rec = new FormHistoryRecord("new1", "forms", System.currentTimeMillis(), false);
     rec.fieldName  = "fieldName1";
     rec.fieldValue = "fieldValue1";
-    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(1)));
     performWait(fetchRunnable(session, new String[] { rec.guid }, new Record[] { rec }));
 
     // remote deleted, local missing => should delete, but at the moment we ignore.
     rec = new FormHistoryRecord("new2", "forms", System.currentTimeMillis(), true);
     performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
     performWait(fetchRunnable(session, new String[] { rec.guid }, new Record[] { }));
 
     session.abort();
@@ -340,29 +340,29 @@ public class TestFormHistoryRepositorySe
     long newTimestamp = System.currentTimeMillis();
 
     FormHistoryRecord rec;
 
     // remote regular, local regular, remote newer => should update.
     rec = new FormHistoryRecord(regular1.guid, regular1.collection, newTimestamp, false);
     rec.fieldName  = regular1.fieldName;
     rec.fieldValue = regular1.fieldValue + "NEW";
-    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(1)));
     performWait(fetchRunnable(session, new String[] { regular1.guid }, new Record[] { rec }));
 
     // remote deleted, local regular, remote newer => should delete everything.
     rec = new FormHistoryRecord(regular2.guid, regular2.collection, newTimestamp, true);
-    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(1)));
     performWait(fetchRunnable(session, new String[] { regular2.guid }, new Record[] { }));
 
     // remote regular, local deleted, remote newer => should update.
     rec = new FormHistoryRecord(deleted1.guid, deleted1.collection, newTimestamp, false);
     rec.fieldName  = regular1.fieldName;
     rec.fieldValue = regular1.fieldValue + "NEW";
-    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(1)));
     performWait(fetchRunnable(session, new String[] { deleted1.guid }, new Record[] { rec }));
 
     // remote deleted, local deleted, remote newer => should delete everything.
     rec = new FormHistoryRecord(deleted2.guid, deleted2.collection, newTimestamp, true);
     performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
     performWait(fetchRunnable(session, new String[] { deleted2.guid }, new Record[] { }));
 
     session.abort();
@@ -400,15 +400,15 @@ public class TestFormHistoryRepositorySe
   }
 
   public void testStoreDifferentGuid() throws Exception {
     final FormHistoryRepositorySession session = createAndBeginSession();
 
     insertTwoRecords(session);
 
     FormHistoryRecord rec = (FormHistoryRecord) regular1.copyWithIDs("distinct", 999);
-    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+    performWait(storeRunnable(session, rec, new ExpectStoredDelegate(1)));
     // Existing record should take remote record's GUID.
     performWait(fetchAllRunnable(session, new Record[] { rec, deleted1 }));
 
     session.abort();
   }
 }
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestPasswordsRepository.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/TestPasswordsRepository.java
@@ -382,17 +382,17 @@ public class TestPasswordsRepository ext
   }
 
   private static long updatePassword(String password, PasswordRecord record) {
     return updatePassword(password, record, System.currentTimeMillis());
   }
 
   // Runnable Helpers.
   private static Runnable storeRunnable(final RepositorySession session, final Record record) {
-    return storeRunnable(session, record, new ExpectStoredDelegate(record.guid));
+    return storeRunnable(session, record, new ExpectStoredDelegate(1));
   }
 
   private static Runnable storeRunnable(final RepositorySession session, final Record record, final RepositorySessionStoreDelegate delegate) {
     return new Runnable() {
       @Override
       public void run() {
         session.setStoreDelegate(delegate);
         try {
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/ThreadedRepositoryTestCase.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/db/ThreadedRepositoryTestCase.java
@@ -109,17 +109,17 @@ public abstract class ThreadedRepository
         } catch (NoStoreDelegateException e) {
           fail("NoStoreDelegateException should not occur.");
         }
       }
     };
   }
 
   public static Runnable storeRunnable(final RepositorySession session, final Record record) {
-    return storeRunnable(session, record, new ExpectStoredDelegate(record.guid));
+    return storeRunnable(session, record, new ExpectStoredDelegate(1));
   }
 
   public static Runnable storeManyRunnable(final RepositorySession session, final Record[] records, final DefaultStoreDelegate delegate) {
     return new Runnable() {
       @Override
       public void run() {
         session.setStoreDelegate(delegate);
         try {
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/TestStoreTracking.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/TestStoreTracking.java
@@ -46,25 +46,23 @@ public class TestStoreTracking extends A
   public void doTestStoreRetrieveByGUID(final WBORepository repository,
                                         final RepositorySession session,
                                         final String expectedGUID,
                                         final Record record) {
 
     final SimpleSuccessStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() {
 
       @Override
-      public void onRecordStoreSucceeded(String guid) {
-        Logger.debug(getName(), "Stored " + guid);
-        assertEq(expectedGUID, guid);
+      public void onRecordStoreSucceeded(int count) {
+        Logger.debug(getName(), "Stored " + count);
       }
 
       @Override
       public void onRecordStoreReconciled(String guid, String oldGuid, Integer newVersion) {
         Logger.debug(getName(), "Reconciled " + guid);
-        assertEq(expectedGUID, guid);
       }
 
       @Override
       public void onStoreCompleted() {
         Logger.debug(getName(), "Store completed.");
         try {
           session.fetch(new String[] { expectedGUID }, new SimpleSuccessFetchDelegate() {
             @Override
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java
@@ -10,17 +10,17 @@ import org.mozilla.gecko.sync.repositori
 public class DefaultStoreDelegate extends DefaultDelegate implements RepositorySessionStoreDelegate {
 
   @Override
   public void onRecordStoreFailed(Exception ex, String guid) {
     performNotify("Record store failed", ex);
   }
 
   @Override
-  public void onRecordStoreSucceeded(String guid) {
+  public void onRecordStoreSucceeded(int count) {
     performNotify("DefaultStoreDelegate used", null);
   }
 
   @Override
   public void onStoreCompleted() {
     performNotify("DefaultStoreDelegate used", null);
   }
 
@@ -38,21 +38,21 @@ public class DefaultStoreDelegate extend
   public void onRecordStoreReconciled(String guid, String oldGuid, Integer newVersion) {}
 
   @Override
   public RepositorySessionStoreDelegate deferredStoreDelegate(final ExecutorService executor) {
     final RepositorySessionStoreDelegate self = this;
     return new RepositorySessionStoreDelegate() {
 
       @Override
-      public void onRecordStoreSucceeded(final String guid) {
+      public void onRecordStoreSucceeded(final int count) {
         executor.execute(new Runnable() {
           @Override
           public void run() {
-            self.onRecordStoreSucceeded(guid);
+            self.onRecordStoreSucceeded(count);
           }
         });
       }
 
       @Override
       public void onRecordStoreFailed(final Exception ex, final String guid) {
         executor.execute(new Runnable() {
           @Override
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java
@@ -9,40 +9,31 @@ import static junit.framework.Assert.ass
 import java.util.HashSet;
 import java.util.concurrent.atomic.AtomicLong;
 
 import junit.framework.AssertionFailedError;
 
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
 public class ExpectManyStoredDelegate extends DefaultStoreDelegate {
-  HashSet<String> expectedGUIDs;
+  int expectedStored;
   AtomicLong stored;
 
   public ExpectManyStoredDelegate(Record[] records) {
-    HashSet<String> s = new HashSet<String>();
-    for (Record record : records) {
-      s.add(record.guid);
-    }
-    expectedGUIDs = s;
+    expectedStored = records.length;
     stored = new AtomicLong(0);
   }
 
   @Override
   public void onStoreCompleted() {
     try {
-      assertEquals(expectedGUIDs.size(), stored.get());
+      assertEquals(expectedStored, stored.get());
       performNotify();
     } catch (AssertionFailedError e) {
       performNotify(e);
     }
   }
 
   @Override
-  public void onRecordStoreSucceeded(String guid) {
-    try {
-      assertTrue(expectedGUIDs.contains(guid));
-    } catch (AssertionFailedError e) {
-      performNotify(e);
-    }
-    stored.incrementAndGet();
+  public void onRecordStoreSucceeded(int count) {
+    stored.addAndGet(count);
   }
 }
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java
@@ -1,11 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.sync.helpers;
 
 public class ExpectNoStoreDelegate extends ExpectStoreCompletedDelegate {
     @Override
-    public void onRecordStoreSucceeded(String guid) {
-        performNotify("Should not have stored record " + guid, null);
+    public void onRecordStoreSucceeded(int count) {
+        performNotify("Should not have stored records: " + count, null);
     }
 }
\ No newline at end of file
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.sync.helpers;
 
 public class ExpectStoreCompletedDelegate extends DefaultStoreDelegate {
 
   @Override
-  public void onRecordStoreSucceeded(String guid) {
+  public void onRecordStoreSucceeded(int count) {
     // That's fine.
   }
 
   @Override
   public void onStoreCompleted() {
     performNotify();
   }
 }
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java
@@ -1,39 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.sync.helpers;
 
 import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
 import junit.framework.AssertionFailedError;
 
 public class ExpectStoredDelegate extends DefaultStoreDelegate {
-  String expectedGUID;
-  String storedGuid;
-
-  public ExpectStoredDelegate(String guid) {
-    this.expectedGUID = guid;
+  final int expectedCount;
+  int count;
+  public ExpectStoredDelegate(int expectedCount) {
+    this.expectedCount = expectedCount;
   }
 
   @Override
   public synchronized void onStoreCompleted() {
     try {
-      assertNotNull(storedGuid);
+      assertEquals(expectedCount, count);
       performNotify();
     } catch (AssertionFailedError e) {
-      performNotify("GUID " + this.expectedGUID + " was not stored", e);
+      performNotify("Wrong # of GUIDS stored: " + count, e);
     }
   }
 
   @Override
-  public synchronized void onRecordStoreSucceeded(String guid) {
-    this.storedGuid = guid;
-    try {
-      if (this.expectedGUID != null) {
-        assertEquals(this.expectedGUID, guid);
-      }
-    } catch (AssertionFailedError e) {
-      performNotify(e);
-    }
+  public synchronized void onRecordStoreSucceeded(int count) {
+    this.count += count;
   }
 }
--- a/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/testhelpers/WBORepository.java
+++ b/mobile/android/services/src/androidTest/java/org/mozilla/gecko/background/testhelpers/WBORepository.java
@@ -128,32 +128,32 @@ public class WBORepository extends Repos
       if (stats.storeBegan < 0) {
         stats.storeBegan = now;
       }
       Record existing = wbos.get(record.guid);
       Logger.debug(LOG_TAG, "Existing record is " + (existing == null ? "<null>" : (existing.guid + ", " + existing)));
       if (existing != null &&
           existing.lastModified > record.lastModified) {
         Logger.debug(LOG_TAG, "Local record is newer. Not storing.");
-        storeDelegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid);
+        storeDelegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(1);
         return;
       }
       if (existing != null) {
         Logger.debug(LOG_TAG, "Replacing local record.");
       }
 
       // Store a copy of the record with an updated modified time.
       Record toStore = record.copyWithIDs(record.guid, record.androidID);
       if (bumpTimestamps) {
         toStore.lastModified = now;
       }
       wbos.put(record.guid, toStore);
 
       trackRecord(toStore);
-      storeDelegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid);
+      storeDelegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(1);
     }
 
     @Override
     public void wipe(final RepositorySessionWipeDelegate delegate) {
       if (!isActive()) {
         delegate.onWipeFailed(new InactiveSessionException());
         return;
       }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java
@@ -302,17 +302,17 @@ public class BaseResource implements Res
     connectionManager.shutdown();
   }
 
   private void execute() {
     HttpResponse response;
     try {
       response = client.execute(request, context);
       Logger.debug(LOG_TAG, "Response: " + response.getStatusLine().toString());
-    } catch (ClientProtocolException e) {
+     } catch (ClientProtocolException e) {
       delegate.handleHttpProtocolException(e);
       return;
     } catch (IOException e) {
       Logger.debug(LOG_TAG, "I/O exception returned from execute.");
       if (!retryOnFailedRequest) {
         delegate.handleHttpIOException(e);
       } else {
         retryRequest();
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/VersioningDelegateHelper.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/VersioningDelegateHelper.java
@@ -182,18 +182,18 @@ public class VersioningDelegateHelper {
             // That is why we remove old GUIDs from the map whenever we perform a replacement.
             // See Bug 1392716.
             localVersionsOfGuids.remove(oldGuid);
             localVersionsOfGuids.put(guid, newVersion);
             inner.onRecordStoreReconciled(guid, oldGuid, newVersion);
         }
 
         @Override
-        public void onRecordStoreSucceeded(String guid) {
-            inner.onRecordStoreSucceeded(guid);
+        public void onRecordStoreSucceeded(int count) {
+            inner.onRecordStoreSucceeded(count);
         }
 
         @Override
         public void onStoreCompleted() {
             inner.onStoreCompleted();
         }
 
         @Override
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java
@@ -205,19 +205,17 @@ public class BookmarksDeletionManager {
   }
 
   private void invokeCallbacks(RepositorySessionStoreDelegate delegate,
                                String[] nonFolderGUIDs) {
     if (delegate == null) {
       return;
     }
     Logger.trace(LOG_TAG, "Invoking store callback for " + nonFolderGUIDs.length + " GUIDs.");
-    for (String guid : nonFolderGUIDs) {
-      delegate.onRecordStoreSucceeded(guid);
-    }
+    delegate.onRecordStoreSucceeded(nonFolderGUIDs.length);
   }
 
   /**
    * Clear state in case of redundancy (e.g., wipe).
    */
   public void clear() {
     nonFolders.clear();
     nonFolderCount = 0;
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksSessionHelper.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksSessionHelper.java
@@ -326,17 +326,17 @@ import java.util.concurrent.ConcurrentHa
             Logger.debug(LOG_TAG, "Inserted folder with guid " + toStore.guid + " as androidID " + toStore.androidID);
 
             updateBookkeeping(toStore);
         } catch (Exception e) {
             delegate.onRecordStoreFailed(e, record.guid);
             return false;
         }
         session.trackRecord(toStore);
-        delegate.onRecordStoreSucceeded(record.guid);
+        delegate.onRecordStoreSucceeded(1);
         return true;
     }
 
     /**
      * Implement method of BookmarksInsertionManager.BookmarkInserter.
      */
     @Override
     public void bulkInsertNonFolders(RepositorySessionStoreDelegate delegate, Collection<BookmarkRecord> records) {
@@ -368,18 +368,18 @@ import java.util.concurrent.ConcurrentHa
         // Success For All!
         for (Record succeeded : toStores) {
             try {
                 updateBookkeeping(succeeded);
             } catch (Exception e) {
                 Logger.warn(LOG_TAG, "Got exception updating bookkeeping of non-folder with guid " + succeeded.guid + ".", e);
             }
             session.trackRecord(succeeded);
-            delegate.onRecordStoreSucceeded(succeeded.guid);
         }
+        delegate.onRecordStoreSucceeded(toStores.size());
     }
 
     @Override
     /* package-private */ void doBegin() throws NullCursorException {
         // To deal with parent mapping of bookmarks we have to do some
         // hairy stuff. Here's the setup for it.
         Cursor cur = dbAccessor.getGuidsIDsForFolders();
 
@@ -451,17 +451,17 @@ import java.util.concurrent.ConcurrentHa
         // Note that we don't track records here; deciding that is the job
         // of reconcileRecords.
         Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
                 "(" + replaced.androidID + ")");
 
         // There's book-keeping which needs to happen with versions, and so we need to pass
         // along the new localVersion.
         delegate.onRecordStoreReconciled(replaced.guid, existingRecord.guid, replaced.localVersion);
-        delegate.onRecordStoreSucceeded(replaced.guid);
+        delegate.onRecordStoreSucceeded(1);
         return true;
     }
 
     @Override
     /* package-private */ boolean isLocallyModified(Record record) {
         if (record.localVersion == null || record.syncVersion == null) {
             throw new IllegalArgumentException("Bookmark session helper received non-versioned record");
         }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java
@@ -245,17 +245,17 @@ public class FennecTabsRepository extend
             // This is nice and easy: we *always* store.
             final String[] selectionArgs = new String[] { tabsRecord.guid };
             if (tabsRecord.deleted) {
               try {
                 Logger.debug(LOG_TAG, "Clearing entry for client " + tabsRecord.guid);
                 clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI,
                                        CLIENT_GUID_IS,
                                        selectionArgs);
-                storeDelegate.onRecordStoreSucceeded(record.guid);
+                storeDelegate.onRecordStoreSucceeded(1);
               } catch (Exception e) {
                 storeDelegate.onRecordStoreFailed(e, record.guid);
               }
               return;
             }
 
             // If it exists, update the client record; otherwise insert.
             final ContentValues clientsCV = tabsRecord.getClientsContentValues();
@@ -278,17 +278,17 @@ public class FennecTabsRepository extend
             // Now insert tabs.
             final ContentValues[] tabsArray = tabsRecord.getTabsContentValues();
             Logger.debug(LOG_TAG, "Inserting " + tabsArray.length + " tabs for client " + tabsRecord.guid);
 
             tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, selectionArgs);
             final int inserted = tabsProvider.bulkInsert(BrowserContractHelpers.TABS_CONTENT_URI, tabsArray);
             Logger.trace(LOG_TAG, "Inserted: " + inserted);
 
-            storeDelegate.onRecordStoreSucceeded(record.guid);
+            storeDelegate.onRecordStoreSucceeded(1);
           } catch (Exception e) {
             Logger.warn(LOG_TAG, "Error storing tabs.", e);
             storeDelegate.onRecordStoreFailed(e, record.guid);
           }
         }
       };
 
       storeWorkQueue.execute(command);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java
@@ -549,30 +549,30 @@ public class FormHistoryRepositorySessio
               return;
             }
 
             boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
             if (!locallyModified) {
               Logger.trace(LOG_TAG, "Remote modified, local not. Deleting.");
               deleteExistingRecord(existingRecord);
               trackRecord(record);
-              storeDelegate.onRecordStoreSucceeded(record.guid);
+              storeDelegate.onRecordStoreSucceeded(1);
               return;
             }
 
             Logger.trace(LOG_TAG, "Both local and remote records have been modified.");
             if (record.lastModified > existingRecord.lastModified) {
               Logger.trace(LOG_TAG, "Remote is newer, and deleted. Purging local.");
               deleteExistingRecord(existingRecord);
               trackRecord(record);
               // Note that while this counts as "reconciliation", we're probably over-counting.
               // Currently, locallyModified above is _always_ true if a record exists locally,
               // and so we'll consider any deletions of already present records as reconciliations.
               storeDelegate.onRecordStoreReconciled(record.guid, null, null);
-              storeDelegate.onRecordStoreSucceeded(record.guid);
+              storeDelegate.onRecordStoreSucceeded(1);
               return;
             }
 
             Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring.");
             return;
           }
           // End deletion logic.
 
@@ -582,49 +582,49 @@ public class FormHistoryRepositorySessio
             existingRecord = findExistingRecordByPayload(record);
           }
 
           if (existingRecord == null) {
             // The record is new.
             Logger.trace(LOG_TAG, "No match. Inserting.");
             insertNewRegularRecord(record);
             trackRecord(record);
-            storeDelegate.onRecordStoreSucceeded(record.guid);
+            storeDelegate.onRecordStoreSucceeded(1);
             return;
           }
 
           // We found a local duplicate.
           Logger.trace(LOG_TAG, "Incoming record " + record.guid + " dupes to local record " + existingRecord.guid);
 
           if (!RepoUtils.stringsEqual(record.guid, existingRecord.guid)) {
             // We found a local record that does NOT have the same GUID -- keep the server's version.
             Logger.trace(LOG_TAG, "Remote guid different from local guid. Storing to keep remote guid.");
             replaceExistingRecordWithRegularRecord(record, existingRecord);
             trackRecord(record);
-            storeDelegate.onRecordStoreSucceeded(record.guid);
+            storeDelegate.onRecordStoreSucceeded(1);
             return;
           }
 
           // We found a local record that does have the same GUID -- check modification times.
           boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
           if (!locallyModified) {
             Logger.trace(LOG_TAG, "Remote modified, local not. Storing.");
             replaceExistingRecordWithRegularRecord(record, existingRecord);
             trackRecord(record);
-            storeDelegate.onRecordStoreSucceeded(record.guid);
+            storeDelegate.onRecordStoreSucceeded(1);
             return;
           }
 
           Logger.trace(LOG_TAG, "Both local and remote records have been modified.");
           if (record.lastModified > existingRecord.lastModified) {
             Logger.trace(LOG_TAG, "Remote is newer, and not deleted. Storing.");
             replaceExistingRecordWithRegularRecord(record, existingRecord);
             trackRecord(record);
             storeDelegate.onRecordStoreReconciled(record.guid, existingRecord.guid, null);
-            storeDelegate.onRecordStoreSucceeded(record.guid);
+            storeDelegate.onRecordStoreSucceeded(1);
             return;
           }
 
           Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring.");
         } catch (Exception e) {
           Logger.error(LOG_TAG, "Store failed for " + record.guid, e);
           storeDelegate.onRecordStoreFailed(e, record.guid);
           return;
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/HistorySessionHelper.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/HistorySessionHelper.java
@@ -52,17 +52,17 @@ import java.util.ArrayList;
      * Neither argument will ever be null.
      *
      * @param record the incoming record. This will be mostly blank, given that it's a deletion.
      * @param existingRecord the existing record. Use this to decide how to process the deletion.
      */
     @Override
     /* package-private */ void storeRecordDeletion(RepositorySessionStoreDelegate storeDelegate, Record record, Record existingRecord) {
         dbHelper.purgeGuid(record.guid);
-        storeDelegate.onRecordStoreSucceeded(record.guid);
+        storeDelegate.onRecordStoreSucceeded(1);
     }
 
     @Override
     /* package-private */ boolean shouldIgnore(Record record) {
         return shouldIgnoreStatic(record);
     }
 
     @VisibleForTesting
@@ -145,17 +145,17 @@ import java.util.ArrayList;
         // newRecord should already have suitable androidID and guid.
         dbHelper.update(existingRecord.guid, preparedToStore);
         updateBookkeeping(preparedToStore);
         Logger.debug(LOG_TAG, "replace() returning record " + preparedToStore.guid);
 
         Logger.debug(LOG_TAG, "Calling delegate callback with guid " + preparedToStore.guid +
                 "(" + preparedToStore.androidID + ")");
         delegate.onRecordStoreReconciled(preparedToStore.guid, existingRecord.guid, null);
-        delegate.onRecordStoreSucceeded(preparedToStore.guid);
+        delegate.onRecordStoreSucceeded(1);
 
         return true;
     }
 
     @Override
     boolean isLocallyModified(Record record) {
         return record.lastModified > session.getLastSyncTimestamp();
     }
@@ -253,12 +253,12 @@ import java.util.ArrayList;
                 updateBookkeeping(succeeded);
             } catch (NoGuidForIdException | ParentNotFoundException e) {
                 // Should not happen.
                 throw new NullCursorException(e);
             } catch (NullCursorException e) {
                 throw e;
             }
             session.trackRecord(succeeded);
-            delegate.onRecordStoreSucceeded(succeeded.guid); // At this point, we are really inserted.
         }
+        delegate.onRecordStoreSucceeded(outgoing.size()); // At this point, we are really inserted.
     }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java
@@ -299,17 +299,17 @@ public class PasswordsRepositorySession 
           try {
             inserted = insert(remoteRecord);
           } catch (RemoteException e) {
             Logger.debug(LOG_TAG, "Record insert caused a RemoteException.");
             storeDelegate.onRecordStoreFailed(e, record.guid);
             return;
           }
           trackRecord(inserted);
-          storeDelegate.onRecordStoreSucceeded(inserted.guid);
+          storeDelegate.onRecordStoreSucceeded(1);
           return;
         }
 
         // We found a local dupe.
         trace("Incoming record " + remoteRecord.guid + " dupes to local record " + existingRecord.guid);
         Logger.debug(LOG_TAG, "remote " + remoteRecord.guid + " dupes to " + existingRecord.guid);
 
         if (existingRecord.deleted && existingRecord.lastModified > remoteRecord.lastModified) {
@@ -335,17 +335,17 @@ public class PasswordsRepositorySession 
           return;
         }
 
         // Note that we don't track records here; deciding that is the job
         // of reconcileRecords.
         Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
                               "(" + replaced.androidID + ")");
         storeDelegate.onRecordStoreReconciled(record.guid, existingRecord.guid, null);
-        storeDelegate.onRecordStoreSucceeded(record.guid);
+        storeDelegate.onRecordStoreSucceeded(1);
         return;
       }
     };
     storeWorkQueue.execute(storeRunnable);
   }
 
   @Override
   public void wipe(final RepositorySessionWipeDelegate delegate) {
@@ -593,17 +593,17 @@ public class PasswordsRepositorySession 
   private void storeRecordDeletion(Record record) {
     try {
       deleteGUID(record.guid);
     } catch (RemoteException e) {
       Logger.error(LOG_TAG, "RemoteException in password delete.");
       storeDelegate.onRecordStoreFailed(e, record.guid);
       return;
     }
-    storeDelegate.onRecordStoreSucceeded(record.guid);
+    storeDelegate.onRecordStoreSucceeded(1);
   }
 
   /**
    * Make a PasswordRecord from a Cursor.
    * @param cur
    *        Cursor from query.
    * @return
    *        PasswordRecord populated from Cursor.
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java
@@ -13,21 +13,21 @@ public class DeferredRepositorySessionSt
 
   public DeferredRepositorySessionStoreDelegate(
       RepositorySessionStoreDelegate inner, ExecutorService executor) {
     this.inner = inner;
     this.executor = executor;
   }
 
   @Override
-  public void onRecordStoreSucceeded(final String guid) {
+  public void onRecordStoreSucceeded(final int count) {
     executor.execute(new Runnable() {
       @Override
       public void run() {
-        inner.onRecordStoreSucceeded(guid);
+        inner.onRecordStoreSucceeded(count);
       }
     });
   }
 
   @Override
   public void onRecordStoreFailed(final Exception ex, final String guid) {
     executor.execute(new Runnable() {
       @Override
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java
@@ -17,15 +17,15 @@ public interface RepositorySessionStoreD
   void onRecordStoreFailed(Exception ex, String recordGuid);
 
   // Meant for signaling that a record has been reconciled.
   // Only makes sense in context of local repositories.
   // Further call to onRecordStoreSucceeded is necessary.
   void onRecordStoreReconciled(String guid, String oldGuid, Integer newVersion);
 
   // Called with a GUID when store has succeeded.
-  void onRecordStoreSucceeded(String guid);
+  void onRecordStoreSucceeded(int count);
   void onStoreCompleted();
   void onStoreFailed(Exception e);
   // Only relevant for store batches, and exists to help us record correct telemetry.
   void onBatchCommitted();
   RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor);
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java
@@ -7,60 +7,50 @@ package org.mozilla.gecko.sync.repositor
 import android.support.annotation.Nullable;
 
 import org.mozilla.gecko.background.common.log.Logger;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
 
 
 /**
  * Keeps track of various meta information about a batch series.
  *
  * NB regarding concurrent access:
  * - this class expects access by possibly different, sequentially running threads.
  * - concurrent access is not supported.
  */
 public class BatchMeta {
     private static final String LOG_TAG = "BatchMeta";
 
     private volatile Boolean inBatchingMode;
     @Nullable private volatile Long lastModified;
     private volatile String token;
 
-    // NB: many of the operations on ConcurrentLinkedQueue are not atomic (toArray, for example),
-    // and so use of this queue type is only possible because this class does not support concurrent
-    // access.
-    private final ConcurrentLinkedQueue<String> successRecordGuids = new ConcurrentLinkedQueue<>();
+    private final AtomicInteger recordSuccessCounter = new AtomicInteger(0);
 
     BatchMeta(@Nullable Long initialLastModified, Boolean initialInBatchingMode) {
         lastModified = initialLastModified;
         inBatchingMode = initialInBatchingMode;
     }
 
-    String[] getSuccessRecordGuids() {
-        // NB: This really doesn't play well with concurrent access.
-        final String[] guids = new String[this.successRecordGuids.size()];
-        this.successRecordGuids.toArray(guids);
-        return guids;
+    int getSuccessRecordCount() {
+        return recordSuccessCounter.get();
     }
 
-    void recordSucceeded(final String recordGuid) {
-        // Sanity check.
-        if (recordGuid == null) {
-            throw new IllegalStateException("Record guid is unexpectedly null");
-        }
-
-        successRecordGuids.add(recordGuid);
+    void recordsSucceeded(int count) {
+        recordSuccessCounter.addAndGet(count);
     }
 
-    void clearSuccessRecordGuids() {
-        successRecordGuids.clear();
+    void clearSuccessRecordCounter() {
+        recordSuccessCounter.set(0);
     }
 
     /* package-local */ void setInBatchingMode(boolean inBatchingMode) {
         this.inBatchingMode = inBatchingMode;
     }
 
     /* package-local */ Boolean getInBatchingMode() {
         return inBatchingMode;
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadDispatcher.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadDispatcher.java
@@ -75,30 +75,28 @@ class PayloadDispatcher {
      * @param isLastPayload was this a very last payload we'll upload?
      */
     void payloadSucceeded(final SyncStorageResponse response, final boolean isCommit, final boolean isLastPayload) {
         // Sanity check.
         if (batchWhiteboard.getInBatchingMode() == null) {
             throw new IllegalStateException("Can't process payload success until we know if we're in a batching mode");
         }
 
-        final String[] guids = batchWhiteboard.getSuccessRecordGuids();
+        final int recordsSucceeded = batchWhiteboard.getSuccessRecordCount();
         // We consider records to have been committed if we're not in a batching mode or this was a commit.
         // If records have been committed, notify our store delegate.
         if (!batchWhiteboard.getInBatchingMode() || isCommit) {
-            for (String guid : guids) {
-                uploader.sessionStoreDelegate.onRecordStoreSucceeded(guid);
-            }
+            uploader.sessionStoreDelegate.onRecordStoreSucceeded(recordsSucceeded);
 
             // If we're not in a batching mode, or just committed a batch, uploaded records have
             // been applied to the server storage and are now visible to other clients.
             // Therefore, we bump our local "last store" timestamp.
             bumpTimestampTo(uploadTimestamp, response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED));
             uploader.setLastStoreTimestamp(uploadTimestamp);
-            batchWhiteboard.clearSuccessRecordGuids();
+            batchWhiteboard.clearSuccessRecordCounter();
         }
 
         if (isCommit || !batchWhiteboard.getInBatchingMode()) {
             uploader.sessionStoreDelegate.onBatchCommitted();
         }
 
         // If this was our very last commit, we're done storing records.
         // Get Last-Modified timestamp from the response, and pass it upstream.
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java
@@ -136,24 +136,17 @@ class PayloadUploadDelegate implements S
             success = body.getArray("success");
         } catch (NonArrayJSONException e) {
             handleRequestError(e);
             return;
         }
 
         if (success != null && !success.isEmpty()) {
             Logger.trace(LOG_TAG, "Successful records: " + success.toString());
-            for (Object o : success) {
-                try {
-                    dispatcher.batchWhiteboard.recordSucceeded((String) o);
-                } catch (ClassCastException e) {
-                    Logger.error(LOG_TAG, "Got exception parsing POST success guid.", e);
-                    // Not much to be done.
-                }
-            }
+            dispatcher.batchWhiteboard.recordsSucceeded(success.size());
         }
         // GC
         success = null;
 
         ExtendedJSONObject failed;
         try {
             failed = body.getObject("failed");
         } catch (NonObjectJSONException e) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java
@@ -246,19 +246,19 @@ public class RecordsChannel implements
   public void onRecordStoreFailed(Exception ex, String recordGuid) {
     Logger.trace(LOG_TAG, "Failed to store record with guid " + recordGuid);
     storeFailedCount.incrementAndGet();
     storeTracker.onRecordStoreFailed();
     delegate.onFlowStoreFailed(this, ex, recordGuid);
   }
 
   @Override
-  public void onRecordStoreSucceeded(String guid) {
-    storeAcceptedCount.incrementAndGet();
-    storeTracker.onRecordStoreSucceeded();
+  public void onRecordStoreSucceeded(int count) {
+    storeAcceptedCount.addAndGet(count);
+    storeTracker.onRecordStoreSucceeded(count);
   }
 
   @Override
   public void onRecordStoreReconciled(String guid, String oldGuid, Integer newVersion) {
     storeReconciledCount.incrementAndGet();
   }
 
   @Override
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/StoreBatchTracker.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/StoreBatchTracker.java
@@ -62,18 +62,18 @@ public class StoreBatchTracker {
         }
         onBatchFinished();
     }
 
     /* package-local */ void onRecordStoreFailed() {
         currentStoreBatchFailed.incrementAndGet();
     }
 
-    /* package-local */ void onRecordStoreSucceeded() {
-        currentStoreBatchAccepted.incrementAndGet();
+    /* package-local */ void onRecordStoreSucceeded(int count) {
+        currentStoreBatchAccepted.addAndGet(count);
     }
 
     /* package-local */ void onRecordStoreAttempted() {
         currentStoreBatchAttempted.incrementAndGet();
     }
 
     // Note that this finishes the current batch (if any exists).
     /* package-local */ ArrayList<Batch> getStoreBatches() {
--- a/mobile/android/services/src/test/java/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
+++ b/mobile/android/services/src/test/java/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
@@ -18,17 +18,17 @@ public class ExpectSuccessRepositorySess
 
   @Override
   public void onRecordStoreFailed(Exception ex, String guid) {
     log("Record store failed.", ex);
     performNotify(new AssertionFailedError("onRecordStoreFailed: record store should not have failed."));
   }
 
   @Override
-  public void onRecordStoreSucceeded(String guid) {
+  public void onRecordStoreSucceeded(int count) {
     log("Record store succeeded.");
   }
 
   @Override
   public void onStoreCompleted() {
     log("Record store completed");
   }
 
--- a/mobile/android/services/src/test/java/org/mozilla/gecko/background/testhelpers/WBORepository.java
+++ b/mobile/android/services/src/test/java/org/mozilla/gecko/background/testhelpers/WBORepository.java
@@ -127,32 +127,32 @@ public class WBORepository extends Repos
       if (stats.storeBegan < 0) {
         stats.storeBegan = now;
       }
       Record existing = wbos.get(record.guid);
       Logger.debug(LOG_TAG, "Existing record is " + (existing == null ? "<null>" : (existing.guid + ", " + existing)));
       if (existing != null &&
           existing.lastModified > record.lastModified) {
         Logger.debug(LOG_TAG, "Local record is newer. Not storing.");
-        storeDelegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid);
+        storeDelegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(1);
         return;
       }
       if (existing != null) {
         Logger.debug(LOG_TAG, "Replacing local record.");
       }
 
       // Store a copy of the record with an updated modified time.
       Record toStore = record.copyWithIDs(record.guid, record.androidID);
       if (bumpTimestamps) {
         toStore.lastModified = now;
       }
       wbos.put(record.guid, toStore);
 
       trackRecord(toStore);
-      storeDelegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid);
+      storeDelegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(1);
     }
 
     @Override
     public void wipe(final RepositorySessionWipeDelegate delegate) {
       if (!isActive()) {
         delegate.onWipeFailed(new InactiveSessionException());
         return;
       }
--- a/mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java
+++ b/mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java
@@ -116,23 +116,16 @@ public class BatchMetaTest {
         } catch (BatchingUploader.TokenModifiedException e) {
             fail("Should be able to set token to null during onCommit set");
         }
         assertNull(batchMeta.getToken());
     }
 
     @Test
     public void testRecordSucceeded() {
-        assertEquals(0, batchMeta.getSuccessRecordGuids().length);
-
-        batchMeta.recordSucceeded("guid1");
-
-        assertEquals(1, batchMeta.getSuccessRecordGuids().length);
-        assertEquals("guid1", batchMeta.getSuccessRecordGuids()[0]);
+        assertEquals(0, batchMeta.getSuccessRecordCount());
+        batchMeta.recordsSucceeded(1);
+        assertEquals(1, batchMeta.getSuccessRecordCount());
 
-        try {
-            batchMeta.recordSucceeded(null);
-            fail();
-        } catch (IllegalStateException e) {
-            assertTrue("Should not be able to 'succeed' a null guid", true);
-        }
+        batchMeta.recordsSucceeded(12);
+        assertEquals(13, batchMeta.getSuccessRecordCount());
     }
 }
\ No newline at end of file
--- a/mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
+++ b/mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
@@ -136,18 +136,18 @@ public class BatchingUploaderTest {
 
         @Override
         public void onRecordStoreFailed(Exception ex, String recordGuid) {
             lastRecordStoreFailedException = ex;
             ++storeFailed;
         }
 
         @Override
-        public void onRecordStoreSucceeded(String guid) {
-            ++recordStoreSucceeded;
+        public void onRecordStoreSucceeded(int count) {
+            recordStoreSucceeded += count;
         }
 
         @Override
         public void onStoreCompleted() {
             ++storeCompleted;
         }
 
         @Override
--- a/mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java
+++ b/mobile/android/services/src/test/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java
@@ -58,20 +58,20 @@ public class PayloadUploadDelegateTest {
         public int committedGuids = 0;
 
         public MockPayloadDispatcher(final Executor workQueue, final BatchingUploader uploader) {
             super(workQueue, uploader, null);
         }
 
         @Override
         public void payloadSucceeded(final SyncStorageResponse response, final boolean isCommit, final boolean isLastPayload) {
-            final String[] guids = batchWhiteboard.getSuccessRecordGuids();
+            final int successCount = batchWhiteboard.getSuccessRecordCount();
             successResponses.add(response);
             if (!batchWhiteboard.getInBatchingMode() || isCommit) {
-                committedGuids += guids.length;
+                committedGuids += successCount;
             }
             if (isCommit) {
                 ++commitPayloadsSucceeded;
             }
             if (isLastPayload) {
                 ++lastPayloadsSucceeded;
             }
             super.payloadSucceeded(response, isCommit, isLastPayload);
@@ -91,27 +91,27 @@ public class PayloadUploadDelegateTest {
         public void payloadFailed(Exception e) {
             didPayloadFail = true;
             super.payloadFailed(e);
         }
     }
 
     class MockRepositorySessionStoreDelegate implements RepositorySessionStoreDelegate {
         Exception storeFailedException;
-        ArrayList<String> succeededGuids = new ArrayList<>();
+        int successCount = 0;
         HashMap<String, Exception> failedGuids = new HashMap<>();
 
         @Override
         public void onRecordStoreFailed(Exception ex, String recordGuid) {
             failedGuids.put(recordGuid, ex);
         }
 
         @Override
-        public void onRecordStoreSucceeded(String guid) {
-            succeededGuids.add(guid);
+        public void onRecordStoreSucceeded(int count) {
+            successCount = count;
         }
 
         @Override
         public void onStoreCompleted() {}
 
         @Override
         public void onStoreFailed(Exception e) {
             storeFailedException = e;
--- a/mobile/android/services/src/test/java/org/mozilla/gecko/sync/synchronizer/StoreBatchTrackerTest.java
+++ b/mobile/android/services/src/test/java/org/mozilla/gecko/sync/synchronizer/StoreBatchTrackerTest.java
@@ -21,19 +21,17 @@ public class StoreBatchTrackerTest {
     public void setUp() throws Exception {
         tracker = new StoreBatchTracker();
     }
 
     private void recordCounts(int attempted, int succeeded, int failed) {
         for (int i = 0; i < attempted; ++i) {
             tracker.onRecordStoreAttempted();
         }
-        for (int i = 0; i < succeeded; ++i) {
-            tracker.onRecordStoreSucceeded();
-        }
+        tracker.onRecordStoreSucceeded(succeeded);
         for (int i = 0; i < failed; ++i) {
             tracker.onRecordStoreFailed();
         }
     }
 
     @Test
     public void testSingleBatch() {
         {