Skip to content

Commit fc71eff

Browse files
authored
Performance optimizations to speed up reading large collections (#123)
* memoize the encoded form of `Document`s received from the backend and use it when writing to local storage instead of reencoding; * when applying remote event, get all base documents in a single query, don't issue a separate query for each; * when applying remote event, don't reread documents from local storage after processing them.
1 parent 6c2e9a5 commit fc71eff

12 files changed

+364
-56
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,46 @@ private MaybeDocument getDocument(DocumentKey key, List<MutationBatch> inBatches
6969
return document;
7070
}
7171

72+
// Returns the view of the given {@code docs} as they would appear after applying all mutations in
73+
// the given {@code batches}.
74+
private Map<DocumentKey, MaybeDocument> applyLocalMutationsToDocuments(
75+
Map<DocumentKey, MaybeDocument> docs, List<MutationBatch> batches) {
76+
for (Map.Entry<DocumentKey, MaybeDocument> base : docs.entrySet()) {
77+
MaybeDocument localView = base.getValue();
78+
for (MutationBatch batch : batches) {
79+
localView = batch.applyToLocalView(base.getKey(), localView);
80+
}
81+
base.setValue(localView);
82+
}
83+
84+
return docs;
85+
}
86+
7287
/**
7388
* Gets the local view of the documents identified by {@code keys}.
7489
*
7590
* <p>If we don't have cached state for a document in {@code keys}, a NoDocument will be stored
7691
* for that key in the resulting set.
7792
*/
7893
ImmutableSortedMap<DocumentKey, MaybeDocument> getDocuments(Iterable<DocumentKey> keys) {
94+
Map<DocumentKey, MaybeDocument> docs = remoteDocumentCache.getAll(keys);
95+
return getLocalViewOfDocuments(docs);
96+
}
97+
98+
/**
99+
* Similar to {@code #getDocuments}, but creates the local view from the given {@code baseDocs}
100+
* without retrieving documents from the local store.
101+
*/
102+
ImmutableSortedMap<DocumentKey, MaybeDocument> getLocalViewOfDocuments(
103+
Map<DocumentKey, MaybeDocument> baseDocs) {
79104
ImmutableSortedMap<DocumentKey, MaybeDocument> results = emptyMaybeDocumentMap();
80105

81-
List<MutationBatch> batches = mutationQueue.getAllMutationBatchesAffectingDocumentKeys(keys);
82-
for (DocumentKey key : keys) {
83-
// TODO: PERF: Consider fetching all remote documents at once rather than
84-
// one-by-one.
85-
MaybeDocument maybeDoc = getDocument(key, batches);
106+
List<MutationBatch> batches =
107+
mutationQueue.getAllMutationBatchesAffectingDocumentKeys(baseDocs.keySet());
108+
Map<DocumentKey, MaybeDocument> docs = applyLocalMutationsToDocuments(baseDocs, batches);
109+
for (Map.Entry<DocumentKey, MaybeDocument> entry : docs.entrySet()) {
110+
DocumentKey key = entry.getKey();
111+
MaybeDocument maybeDoc = entry.getValue();
86112
// TODO: Don't conflate missing / deleted.
87113
if (maybeDoc == null) {
88114
maybeDoc = new NoDocument(key, SnapshotVersion.NONE, /*hasCommittedMutations=*/ false);

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalSerializer.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,19 @@ public LocalSerializer(RemoteSerializer rpcSerializer) {
4848
com.google.firebase.firestore.proto.MaybeDocument encodeMaybeDocument(MaybeDocument document) {
4949
com.google.firebase.firestore.proto.MaybeDocument.Builder builder =
5050
com.google.firebase.firestore.proto.MaybeDocument.newBuilder();
51+
5152
if (document instanceof NoDocument) {
5253
NoDocument noDocument = (NoDocument) document;
5354
builder.setNoDocument(encodeNoDocument(noDocument));
5455
builder.setHasCommittedMutations(noDocument.hasCommittedMutations());
5556
} else if (document instanceof Document) {
5657
Document existingDocument = (Document) document;
57-
builder.setDocument(encodeDocument(existingDocument));
58+
// Use the memoized encoded form if it exists.
59+
if (existingDocument.getProto() != null) {
60+
builder.setDocument(existingDocument.getProto());
61+
} else {
62+
builder.setDocument(encodeDocument(existingDocument));
63+
}
5864
builder.setHasCommittedMutations(existingDocument.hasCommittedMutations());
5965
} else if (document instanceof UnknownDocument) {
6066
builder.setUnknownDocument(encodeUnknownDocument((UnknownDocument) document));

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.google.firebase.firestore.remote.TargetChange;
3636
import com.google.firebase.firestore.util.Logger;
3737
import com.google.protobuf.ByteString;
38+
import java.util.HashMap;
3839
import java.util.HashSet;
3940
import java.util.List;
4041
import java.util.Map;
@@ -329,14 +330,19 @@ public ImmutableSortedMap<DocumentKey, MaybeDocument> applyRemoteEvent(RemoteEve
329330
}
330331
}
331332

332-
Set<DocumentKey> changedDocKeys = new HashSet<>();
333+
Map<DocumentKey, MaybeDocument> changedDocs = new HashMap<>();
333334
Map<DocumentKey, MaybeDocument> documentUpdates = remoteEvent.getDocumentUpdates();
334335
Set<DocumentKey> limboDocuments = remoteEvent.getResolvedLimboDocuments();
336+
// Each loop iteration only affects its "own" doc, so it's safe to get all the remote
337+
// documents in advance in a single call.
338+
Map<DocumentKey, MaybeDocument> existingDocs =
339+
remoteDocuments.getAll(documentUpdates.keySet());
340+
335341
for (Entry<DocumentKey, MaybeDocument> entry : documentUpdates.entrySet()) {
336342
DocumentKey key = entry.getKey();
337343
MaybeDocument doc = entry.getValue();
338-
changedDocKeys.add(key);
339-
MaybeDocument existingDoc = remoteDocuments.get(key);
344+
MaybeDocument existingDoc = existingDocs.get(key);
345+
340346
// If a document update isn't authoritative, make sure we don't
341347
// apply an old document version to the remote cache. We make an
342348
// exception for SnapshotVersion.MIN which can happen for
@@ -347,6 +353,7 @@ public ImmutableSortedMap<DocumentKey, MaybeDocument> applyRemoteEvent(RemoteEve
347353
|| (authoritativeUpdates.contains(doc.getKey()) && !existingDoc.hasPendingWrites())
348354
|| doc.getVersion().compareTo(existingDoc.getVersion()) >= 0) {
349355
remoteDocuments.add(doc);
356+
changedDocs.put(key, doc);
350357
} else {
351358
Logger.debug(
352359
"LocalStore",
@@ -376,7 +383,7 @@ public ImmutableSortedMap<DocumentKey, MaybeDocument> applyRemoteEvent(RemoteEve
376383
queryCache.setLastRemoteSnapshotVersion(remoteVersion);
377384
}
378385

379-
return localDocuments.getDocuments(changedDocKeys);
386+
return localDocuments.getLocalViewOfDocuments(changedDocs);
380387
});
381388
}
382389

firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.firebase.firestore.model.DocumentKey;
2424
import com.google.firebase.firestore.model.MaybeDocument;
2525
import com.google.firebase.firestore.model.ResourcePath;
26+
import java.util.HashMap;
2627
import java.util.Iterator;
2728
import java.util.Map;
2829
import javax.annotation.Nullable;
@@ -53,6 +54,19 @@ public MaybeDocument get(DocumentKey key) {
5354
return docs.get(key);
5455
}
5556

57+
@Override
58+
public Map<DocumentKey, MaybeDocument> getAll(Iterable<DocumentKey> keys) {
59+
Map<DocumentKey, MaybeDocument> result = new HashMap<>();
60+
61+
for (DocumentKey key : keys) {
62+
// Make sure each key has a corresponding entry, which is null in case the document is not
63+
// found.
64+
result.put(key, get(key));
65+
}
66+
67+
return result;
68+
}
69+
5670
@Override
5771
public ImmutableSortedMap<DocumentKey, Document> getAllDocumentsMatchingQuery(Query query) {
5872
ImmutableSortedMap<DocumentKey, Document> result = emptyDocumentMap();

firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.firebase.firestore.model.Document;
2020
import com.google.firebase.firestore.model.DocumentKey;
2121
import com.google.firebase.firestore.model.MaybeDocument;
22+
import java.util.Map;
2223
import javax.annotation.Nullable;
2324

2425
/**
@@ -51,6 +52,15 @@ interface RemoteDocumentCache {
5152
@Nullable
5253
MaybeDocument get(DocumentKey documentKey);
5354

55+
/**
56+
* Looks up a set of entries in the cache.
57+
*
58+
* @param documentKeys The keys of the entries to look up.
59+
* @return The cached Document or NoDocument entries indexed by key. If an entry is not cached,
60+
* the corresponding key will be mapped to a null value.
61+
*/
62+
Map<DocumentKey, MaybeDocument> getAll(Iterable<DocumentKey> documentKeys);
63+
5464
/**
5565
* Executes a query against the cached Document entries
5666
*

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
import com.google.protobuf.InvalidProtocolBufferException;
3333
import com.google.protobuf.MessageLite;
3434
import java.util.ArrayList;
35+
import java.util.Arrays;
3536
import java.util.Collections;
3637
import java.util.HashSet;
37-
import java.util.Iterator;
3838
import java.util.List;
3939
import java.util.Set;
4040
import javax.annotation.Nullable;
@@ -277,46 +277,29 @@ public List<MutationBatch> getAllMutationBatchesAffectingDocumentKey(DocumentKey
277277
@Override
278278
public List<MutationBatch> getAllMutationBatchesAffectingDocumentKeys(
279279
Iterable<DocumentKey> documentKeys) {
280-
List<MutationBatch> result = new ArrayList<>();
281-
if (!documentKeys.iterator().hasNext()) {
282-
return result;
280+
List<Object> args = new ArrayList<>();
281+
for (DocumentKey key : documentKeys) {
282+
args.add(EncodedPath.encode(key.getPath()));
283283
}
284284

285-
// SQLite limits maximum number of host parameters to 999 (see
286-
// https://www.sqlite.org/limits.html). To work around this, split the given keys into several
287-
// smaller sets and issue a separate query for each.
288-
int limit = 900;
289-
Iterator<DocumentKey> keyIter = documentKeys.iterator();
285+
SQLitePersistence.LongQuery longQuery =
286+
new SQLitePersistence.LongQuery(
287+
db,
288+
"SELECT DISTINCT dm.batch_id, m.mutations FROM document_mutations dm, mutations m "
289+
+ "WHERE dm.uid = ? "
290+
+ "AND dm.path IN (",
291+
Arrays.asList(uid),
292+
args,
293+
") "
294+
+ "AND dm.uid = m.uid "
295+
+ "AND dm.batch_id = m.batch_id "
296+
+ "ORDER BY dm.batch_id");
297+
298+
List<MutationBatch> result = new ArrayList<>();
290299
Set<Integer> uniqueBatchIds = new HashSet<>();
291-
int queriesPerformed = 0;
292-
while (keyIter.hasNext()) {
293-
++queriesPerformed;
294-
StringBuilder placeholdersBuilder = new StringBuilder();
295-
List<String> args = new ArrayList<>();
296-
args.add(uid);
297-
298-
for (int i = 0; keyIter.hasNext() && i < limit; i++) {
299-
DocumentKey key = keyIter.next();
300-
301-
if (i > 0) {
302-
placeholdersBuilder.append(", ");
303-
}
304-
placeholdersBuilder.append("?");
305-
306-
args.add(EncodedPath.encode(key.getPath()));
307-
}
308-
String placeholders = placeholdersBuilder.toString();
309-
310-
db.query(
311-
"SELECT DISTINCT dm.batch_id, m.mutations FROM document_mutations dm, mutations m "
312-
+ "WHERE dm.uid = ? "
313-
+ "AND dm.path IN ("
314-
+ placeholders
315-
+ ") "
316-
+ "AND dm.uid = m.uid "
317-
+ "AND dm.batch_id = m.batch_id "
318-
+ "ORDER BY dm.batch_id")
319-
.binding(args.toArray())
300+
while (longQuery.hasMoreSubqueries()) {
301+
longQuery
302+
.performNextSubquery()
320303
.forEach(
321304
row -> {
322305
int batchId = row.getInt(0);
@@ -330,7 +313,7 @@ public List<MutationBatch> getAllMutationBatchesAffectingDocumentKeys(
330313
// If more than one query was issued, batches might be in an unsorted order (batches are ordered
331314
// within one query's results, but not across queries). It's likely to be rare, so don't impose
332315
// performance penalty on the normal case.
333-
if (queriesPerformed > 1) {
316+
if (longQuery.getSubqueriesPerformed() > 1) {
334317
Collections.sort(
335318
result,
336319
(MutationBatch lhs, MutationBatch rhs) ->

0 commit comments

Comments
 (0)