Skip to content

Commit ee6e656

Browse files
Parse Overlays in background (#3420)
1 parent 3f29bba commit ee6e656

File tree

8 files changed

+227
-89
lines changed

8 files changed

+227
-89
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.firebase.firestore.model.mutation.Mutation;
2121
import com.google.firebase.firestore.model.mutation.Overlay;
2222
import java.util.Map;
23+
import java.util.SortedSet;
2324

2425
/**
2526
* Provides methods to read and write document overlays.
@@ -38,6 +39,12 @@ public interface DocumentOverlayCache {
3839
@Nullable
3940
Overlay getOverlay(DocumentKey key);
4041

42+
/**
43+
* Gets the saved overlay mutation for the given document keys. Skips keys for which there are no
44+
* overlays.
45+
*/
46+
Map<DocumentKey, Overlay> getOverlays(SortedSet<DocumentKey> keys);
47+
4148
/**
4249
* Saves the given document key to mutation map to persistence as overlays. All overlays will have
4350
* their largest batch id set to {@code largestBatchId}.

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
import java.util.List;
4040
import java.util.Map;
4141
import java.util.Set;
42+
import java.util.SortedSet;
4243
import java.util.TreeMap;
44+
import java.util.TreeSet;
4345

4446
/**
4547
* A readonly view of the local state of all documents we're tracking (i.e. we have a cached version
@@ -115,24 +117,20 @@ ImmutableSortedMap<DocumentKey, Document> getDocuments(Iterable<DocumentKey> key
115117
*/
116118
ImmutableSortedMap<DocumentKey, Document> getLocalViewOfDocuments(
117119
Map<DocumentKey, MutableDocument> docs, Set<DocumentKey> existenceStateChanged) {
118-
return computeViews(docs, Collections.emptyMap(), existenceStateChanged);
120+
Map<DocumentKey, Overlay> overlays = new HashMap<>();
121+
populateOverlays(overlays, docs.keySet());
122+
return computeViews(docs, overlays, existenceStateChanged);
119123
}
120124

121-
/**
122-
* Computes the local view for doc, applying overlays from both {@code memoizedOverlays} and the
123-
* overlay cache.
124-
*/
125+
/*Computes the local view for doc */
125126
private ImmutableSortedMap<DocumentKey, Document> computeViews(
126127
Map<DocumentKey, MutableDocument> docs,
127-
Map<DocumentKey, Overlay> memoizedOverlays,
128+
Map<DocumentKey, Overlay> overlays,
128129
Set<DocumentKey> existenceStateChanged) {
129130
ImmutableSortedMap<DocumentKey, Document> results = emptyDocumentMap();
130131
Map<DocumentKey, MutableDocument> recalculateDocuments = new HashMap<>();
131132
for (MutableDocument doc : docs.values()) {
132-
Overlay overlay =
133-
memoizedOverlays.containsKey(doc.getKey())
134-
? memoizedOverlays.get(doc.getKey())
135-
: documentOverlayCache.getOverlay(doc.getKey());
133+
Overlay overlay = overlays.get(doc.getKey());
136134
// Recalculate an overlay if the document's existence state is changed due to a remote
137135
// event *and* the overlay is a PatchMutation. This is because document existence state
138136
// can change if some patch mutation's preconditions are met.
@@ -290,11 +288,26 @@ LocalDocumentsResult getNextDocuments(String collectionGroup, IndexOffset offset
290288
largestBatchId = Math.max(largestBatchId, overlay.getLargestBatchId());
291289
}
292290

291+
populateOverlays(overlays, docs.keySet());
293292
ImmutableSortedMap<DocumentKey, Document> localDocs =
294293
computeViews(docs, overlays, Collections.emptySet());
295294
return new LocalDocumentsResult(largestBatchId, localDocs);
296295
}
297296

297+
/**
298+
* Fetches the overlays for {@code keys} and adds them to provided overlay map if the map does not
299+
* already contain an entry for the given key.
300+
*/
301+
private void populateOverlays(Map<DocumentKey, Overlay> overlays, Set<DocumentKey> keys) {
302+
SortedSet<DocumentKey> missingOverlays = new TreeSet<>();
303+
for (DocumentKey key : keys) {
304+
if (!overlays.containsKey(key)) {
305+
missingOverlays.add(key);
306+
}
307+
}
308+
overlays.putAll(documentOverlayCache.getOverlays(missingOverlays));
309+
}
310+
298311
private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollectionQuery(
299312
Query query, IndexOffset offset) {
300313
Map<DocumentKey, MutableDocument> remoteDocuments =

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Set;
2626
import java.util.SortedMap;
27+
import java.util.SortedSet;
2728
import java.util.TreeMap;
2829

2930
public class MemoryDocumentOverlayCache implements DocumentOverlayCache {
@@ -38,6 +39,17 @@ public Overlay getOverlay(DocumentKey key) {
3839
return overlays.get(key);
3940
}
4041

42+
public Map<DocumentKey, Overlay> getOverlays(SortedSet<DocumentKey> keys) {
43+
Map<DocumentKey, Overlay> result = new HashMap<>();
44+
for (DocumentKey key : keys) {
45+
Overlay overlay = overlays.get(key);
46+
if (overlay != null) {
47+
result.put(key, overlay);
48+
}
49+
}
50+
return result;
51+
}
52+
4153
private void saveOverlay(int largestBatchId, @Nullable Mutation mutation) {
4254
if (mutation == null) {
4355
return;

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

Lines changed: 97 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,26 @@
1515
package com.google.firebase.firestore.local;
1616

1717
import static com.google.firebase.firestore.util.Assert.fail;
18+
import static com.google.firebase.firestore.util.Assert.hardAssert;
1819

20+
import android.database.Cursor;
1921
import androidx.annotation.Nullable;
2022
import com.google.firebase.firestore.auth.User;
2123
import com.google.firebase.firestore.model.DocumentKey;
2224
import com.google.firebase.firestore.model.ResourcePath;
2325
import com.google.firebase.firestore.model.mutation.Mutation;
2426
import com.google.firebase.firestore.model.mutation.Overlay;
27+
import com.google.firebase.firestore.util.BackgroundQueue;
28+
import com.google.firebase.firestore.util.Executors;
2529
import com.google.firestore.v1.Write;
2630
import com.google.protobuf.InvalidProtocolBufferException;
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
2733
import java.util.HashMap;
34+
import java.util.List;
2835
import java.util.Map;
36+
import java.util.SortedSet;
37+
import java.util.concurrent.Executor;
2938

3039
public class SQLiteDocumentOverlayCache implements DocumentOverlayCache {
3140
private final SQLitePersistence db;
@@ -47,7 +56,54 @@ public Overlay getOverlay(DocumentKey key) {
4756
"SELECT overlay_mutation, largest_batch_id FROM document_overlays "
4857
+ "WHERE uid = ? AND collection_path = ? AND document_id = ?")
4958
.binding(uid, collectionPath, documentId)
50-
.firstValue(this::decodeOverlay);
59+
.firstValue(row -> this.decodeOverlay(row.getBlob(0), row.getInt(1)));
60+
}
61+
62+
@Override
63+
public Map<DocumentKey, Overlay> getOverlays(SortedSet<DocumentKey> keys) {
64+
hardAssert(keys.comparator() == null, "getOverlays() requires natural order");
65+
Map<DocumentKey, Overlay> result = new HashMap<>();
66+
67+
BackgroundQueue backgroundQueue = new BackgroundQueue();
68+
ResourcePath currentCollection = ResourcePath.EMPTY;
69+
List<Object> accumulatedDocumentIds = new ArrayList<>();
70+
for (DocumentKey key : keys) {
71+
if (!currentCollection.equals(key.getCollectionPath())) {
72+
processSingleCollection(result, backgroundQueue, currentCollection, accumulatedDocumentIds);
73+
currentCollection = key.getCollectionPath();
74+
accumulatedDocumentIds.clear();
75+
}
76+
accumulatedDocumentIds.add(key.getDocumentId());
77+
}
78+
79+
processSingleCollection(result, backgroundQueue, currentCollection, accumulatedDocumentIds);
80+
backgroundQueue.drain();
81+
return result;
82+
}
83+
84+
/** Reads the overlays for the documents in a single collection. */
85+
private void processSingleCollection(
86+
Map<DocumentKey, Overlay> result,
87+
BackgroundQueue backgroundQueue,
88+
ResourcePath collectionPath,
89+
List<Object> documentIds) {
90+
if (documentIds.isEmpty()) {
91+
return;
92+
}
93+
94+
SQLitePersistence.LongQuery longQuery =
95+
new SQLitePersistence.LongQuery(
96+
db,
97+
"SELECT overlay_mutation, largest_batch_id FROM document_overlays "
98+
+ "WHERE uid = ? AND collection_path = ? AND document_id IN (",
99+
Arrays.asList(uid, EncodedPath.encode(collectionPath)),
100+
documentIds,
101+
")");
102+
while (longQuery.hasMoreSubqueries()) {
103+
longQuery
104+
.performNextSubquery()
105+
.forEach(row -> processOverlaysInBackground(backgroundQueue, result, row));
106+
}
51107
}
52108

53109
private void saveOverlay(int largestBatchId, DocumentKey key, @Nullable Mutation mutation) {
@@ -83,49 +139,48 @@ public void removeOverlaysForBatchId(int batchId) {
83139

84140
@Override
85141
public Map<DocumentKey, Overlay> getOverlays(ResourcePath collection, int sinceBatchId) {
86-
String collectionPath = EncodedPath.encode(collection);
87-
88142
Map<DocumentKey, Overlay> result = new HashMap<>();
143+
BackgroundQueue backgroundQueue = new BackgroundQueue();
89144
db.query(
90145
"SELECT overlay_mutation, largest_batch_id FROM document_overlays "
91146
+ "WHERE uid = ? AND collection_path = ? AND largest_batch_id > ?")
92-
.binding(uid, collectionPath, sinceBatchId)
93-
.forEach(
94-
row -> {
95-
Overlay overlay = decodeOverlay(row);
96-
result.put(overlay.getKey(), overlay);
97-
});
98-
147+
.binding(uid, EncodedPath.encode(collection), sinceBatchId)
148+
.forEach(row -> processOverlaysInBackground(backgroundQueue, result, row));
149+
backgroundQueue.drain();
99150
return result;
100151
}
101152

102153
@Override
103154
public Map<DocumentKey, Overlay> getOverlays(
104155
String collectionGroup, int sinceBatchId, int count) {
105156
Map<DocumentKey, Overlay> result = new HashMap<>();
106-
Overlay[] lastOverlay = new Overlay[] {null};
157+
String[] lastCollectionPath = new String[1];
158+
String[] lastDocumentPath = new String[1];
159+
int[] lastLargestBatchId = new int[1];
107160

161+
BackgroundQueue backgroundQueue = new BackgroundQueue();
108162
db.query(
109-
"SELECT overlay_mutation, largest_batch_id FROM document_overlays "
163+
"SELECT overlay_mutation, largest_batch_id, collection_path, document_id "
164+
+ " FROM document_overlays "
110165
+ "WHERE uid = ? AND collection_group = ? AND largest_batch_id > ? "
111166
+ "ORDER BY largest_batch_id, collection_path, document_id LIMIT ?")
112167
.binding(uid, collectionGroup, sinceBatchId, count)
113168
.forEach(
114169
row -> {
115-
lastOverlay[0] = decodeOverlay(row);
116-
result.put(lastOverlay[0].getKey(), lastOverlay[0]);
170+
lastLargestBatchId[0] = row.getInt(1);
171+
lastCollectionPath[0] = row.getString(2);
172+
lastDocumentPath[0] = row.getString(3);
173+
processOverlaysInBackground(backgroundQueue, result, row);
117174
});
118175

119-
if (lastOverlay[0] == null) {
176+
if (lastCollectionPath[0] == null) {
120177
return result;
121178
}
122179

123180
// This function should not return partial batch overlays, even if the number of overlays in the
124181
// result set exceeds the given `count` argument. Since the `LIMIT` in the above query might
125182
// result in a partial batch, the following query appends any remaining overlays for the last
126183
// batch.
127-
DocumentKey key = lastOverlay[0].getKey();
128-
String encodedCollectionPath = EncodedPath.encode(key.getCollectionPath());
129184
db.query(
130185
"SELECT overlay_mutation, largest_batch_id FROM document_overlays "
131186
+ "WHERE uid = ? AND collection_group = ? "
@@ -134,23 +189,35 @@ public Map<DocumentKey, Overlay> getOverlays(
134189
.binding(
135190
uid,
136191
collectionGroup,
137-
encodedCollectionPath,
138-
encodedCollectionPath,
139-
key.getDocumentId(),
140-
lastOverlay[0].getLargestBatchId())
141-
.forEach(
142-
row -> {
143-
Overlay overlay = decodeOverlay(row);
144-
result.put(overlay.getKey(), overlay);
145-
});
146-
192+
lastCollectionPath[0],
193+
lastCollectionPath[0],
194+
lastDocumentPath[0],
195+
lastLargestBatchId[0])
196+
.forEach(row -> processOverlaysInBackground(backgroundQueue, result, row));
197+
backgroundQueue.drain();
147198
return result;
148199
}
149200

150-
private Overlay decodeOverlay(android.database.Cursor row) {
201+
private void processOverlaysInBackground(
202+
BackgroundQueue backgroundQueue, Map<DocumentKey, Overlay> results, Cursor row) {
203+
byte[] rawMutation = row.getBlob(0);
204+
int largestBatchId = row.getInt(1);
205+
206+
// Since scheduling background tasks incurs overhead, we only dispatch to a
207+
// background thread if there are still some documents remaining.
208+
Executor executor = row.isLast() ? Executors.DIRECT_EXECUTOR : backgroundQueue;
209+
executor.execute(
210+
() -> {
211+
Overlay overlay = decodeOverlay(rawMutation, largestBatchId);
212+
synchronized (results) {
213+
results.put(overlay.getKey(), overlay);
214+
}
215+
});
216+
}
217+
218+
private Overlay decodeOverlay(byte[] rawMutation, int largestBatchId) {
151219
try {
152-
Write write = Write.parseFrom(row.getBlob(0));
153-
int largestBatchId = row.getInt(1);
220+
Write write = Write.parseFrom(rawMutation);
154221
Mutation mutation = serializer.decodeMutation(write);
155222
return Overlay.create(largestBatchId, mutation);
156223
} catch (InvalidProtocolBufferException e) {

0 commit comments

Comments
 (0)