Skip to content

Commit 96ef850

Browse files
authored
Reduce memory usage by applying query check sooner (#4591)
1 parent 9d99981 commit 96ef850

File tree

10 files changed

+118
-40
lines changed

10 files changed

+118
-40
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Unreleased
2+
* [fixed] Fix a potential high-memory usage issue.
23
* [fixed] Fix an issue that stops some performance optimization being applied.
34

45
# 24.4.1

firebase-firestore/firebase-firestore.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ dependencies {
146146
androidTestAnnotationProcessor 'com.google.auto.value:auto-value:1.6.5'
147147
annotationProcessor 'com.google.auto.value:auto-value:1.6.5'
148148

149+
testImplementation project(':firebase-firestore')
149150
testImplementation 'junit:junit:4.13.2'
150151
testImplementation 'androidx.test:core:1.2.0'
151152
testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0"
@@ -155,6 +156,7 @@ dependencies {
155156
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
156157
testImplementation 'com.google.guava:guava-testlib:12.0-rc2'
157158

159+
androidTestImplementation project(':firebase-firestore')
158160
androidTestImplementation 'junit:junit:4.13.2'
159161
androidTestImplementation("com.google.truth:truth:$googleTruthVersion"){
160162
exclude group: "org.codehaus.mojo", module: "animal-sniffer-annotations"

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,10 +363,10 @@ private void populateOverlays(Map<DocumentKey, Overlay> overlays, Set<DocumentKe
363363

364364
private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollectionQuery(
365365
Query query, IndexOffset offset) {
366-
Map<DocumentKey, MutableDocument> remoteDocuments =
367-
remoteDocumentCache.getAll(query.getPath(), offset);
368366
Map<DocumentKey, Overlay> overlays =
369367
documentOverlayCache.getOverlays(query.getPath(), offset.getLargestBatchId());
368+
Map<DocumentKey, MutableDocument> remoteDocuments =
369+
remoteDocumentCache.getDocumentsMatchingQuery(query, offset, overlays.keySet());
370370

371371
// As documents might match the query because of their overlay we need to include documents
372372
// for all overlays in the initial document set.

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@
1919

2020
import androidx.annotation.NonNull;
2121
import com.google.firebase.database.collection.ImmutableSortedMap;
22+
import com.google.firebase.firestore.core.Query;
2223
import com.google.firebase.firestore.model.Document;
2324
import com.google.firebase.firestore.model.DocumentKey;
2425
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2526
import com.google.firebase.firestore.model.MutableDocument;
26-
import com.google.firebase.firestore.model.ResourcePath;
2727
import com.google.firebase.firestore.model.SnapshotVersion;
2828
import java.util.Collection;
2929
import java.util.HashMap;
3030
import java.util.Iterator;
3131
import java.util.Map;
32+
import java.util.Set;
33+
import javax.annotation.Nonnull;
3234

3335
/** In-memory cache of remote documents. */
3436
final class MemoryRemoteDocumentCache implements RemoteDocumentCache {
@@ -94,25 +96,26 @@ public Map<DocumentKey, MutableDocument> getAll(
9496
}
9597

9698
@Override
97-
public Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset) {
99+
public Map<DocumentKey, MutableDocument> getDocumentsMatchingQuery(
100+
Query query, IndexOffset offset, @Nonnull Set<DocumentKey> mutatedKeys) {
98101
Map<DocumentKey, MutableDocument> result = new HashMap<>();
99102

100103
// Documents are ordered by key, so we can use a prefix scan to narrow down the documents
101104
// we need to match the query against.
102-
DocumentKey prefix = DocumentKey.fromPath(collection.append(""));
105+
DocumentKey prefix = DocumentKey.fromPath(query.getPath().append(""));
103106
Iterator<Map.Entry<DocumentKey, Document>> iterator = docs.iteratorFrom(prefix);
104107

105108
while (iterator.hasNext()) {
106109
Map.Entry<DocumentKey, Document> entry = iterator.next();
107110
Document doc = entry.getValue();
108111

109112
DocumentKey key = entry.getKey();
110-
if (!collection.isPrefixOf(key.getPath())) {
113+
if (!query.getPath().isPrefixOf(key.getPath())) {
111114
// We are now scanning the next collection. Abort.
112115
break;
113116
}
114117

115-
if (key.getPath().length() > collection.length() + 1) {
118+
if (key.getPath().length() > query.getPath().length() + 1) {
116119
// Exclude entries from subcollections.
117120
continue;
118121
}
@@ -122,6 +125,10 @@ public Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOf
122125
continue;
123126
}
124127

128+
if (!mutatedKeys.contains(doc.getKey()) && !query.matches(doc)) {
129+
continue;
130+
}
131+
125132
result.put(doc.getKey(), doc.mutableCopy());
126133
}
127134

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
package com.google.firebase.firestore.local;
1616

17+
import com.google.firebase.firestore.core.Query;
1718
import com.google.firebase.firestore.model.DocumentKey;
1819
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
1920
import com.google.firebase.firestore.model.MutableDocument;
20-
import com.google.firebase.firestore.model.ResourcePath;
2121
import com.google.firebase.firestore.model.SnapshotVersion;
2222
import java.util.Collection;
2323
import java.util.Map;
24+
import java.util.Set;
25+
import javax.annotation.Nonnull;
2426

2527
/**
2628
* Represents cached documents received from the remote backend.
@@ -77,11 +79,14 @@ interface RemoteDocumentCache {
7779
Map<DocumentKey, MutableDocument> getAll(String collectionGroup, IndexOffset offset, int limit);
7880

7981
/**
80-
* Returns the documents from the provided collection.
82+
* Returns the documents that match the given query.
8183
*
82-
* @param collection The collection to read.
84+
* @param query The query to match against remote documents.
8385
* @param offset The read time and document key to start scanning at (exclusive).
86+
* @param mutatedKeys The keys of documents who have mutations attached, they should be read
87+
* regardless whether they match the given query.
8488
* @return A newly created map with the set of documents in the collection.
8589
*/
86-
Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset);
90+
Map<DocumentKey, MutableDocument> getDocumentsMatchingQuery(
91+
Query query, IndexOffset offset, @Nonnull Set<DocumentKey> mutatedKeys);
8792
}

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

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import androidx.annotation.VisibleForTesting;
2525
import com.google.firebase.Timestamp;
2626
import com.google.firebase.database.collection.ImmutableSortedMap;
27+
import com.google.firebase.firestore.core.Query;
2728
import com.google.firebase.firestore.model.Document;
2829
import com.google.firebase.firestore.model.DocumentKey;
2930
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
@@ -32,6 +33,7 @@
3233
import com.google.firebase.firestore.model.SnapshotVersion;
3334
import com.google.firebase.firestore.util.BackgroundQueue;
3435
import com.google.firebase.firestore.util.Executors;
36+
import com.google.firebase.firestore.util.Function;
3537
import com.google.protobuf.InvalidProtocolBufferException;
3638
import com.google.protobuf.MessageLite;
3739
import java.util.ArrayList;
@@ -40,7 +42,10 @@
4042
import java.util.HashMap;
4143
import java.util.List;
4244
import java.util.Map;
45+
import java.util.Set;
4346
import java.util.concurrent.Executor;
47+
import javax.annotation.Nonnull;
48+
import javax.annotation.Nullable;
4449

4550
final class SQLiteRemoteDocumentCache implements RemoteDocumentCache {
4651
/** The number of bind args per collection group in {@link #getAll(String, IndexOffset, int)} */
@@ -135,7 +140,7 @@ public Map<DocumentKey, MutableDocument> getAll(Iterable<DocumentKey> documentKe
135140
while (longQuery.hasMoreSubqueries()) {
136141
longQuery
137142
.performNextSubquery()
138-
.forEach(row -> processRowInBackground(backgroundQueue, results, row));
143+
.forEach(row -> processRowInBackground(backgroundQueue, results, row, /*filter*/ null));
139144
}
140145
backgroundQueue.drain();
141146
return results;
@@ -153,15 +158,18 @@ public Map<DocumentKey, MutableDocument> getAll(
153158
if (collections.isEmpty()) {
154159
return Collections.emptyMap();
155160
} else if (BINDS_PER_STATEMENT * collections.size() < SQLitePersistence.MAX_ARGS) {
156-
return getAll(collections, offset, limit);
161+
return getAll(collections, offset, limit, /*filter*/ null);
157162
} else {
158163
// We need to fan out our collection scan since SQLite only supports 999 binds per statement.
159164
Map<DocumentKey, MutableDocument> results = new HashMap<>();
160165
int pageSize = SQLitePersistence.MAX_ARGS / BINDS_PER_STATEMENT;
161166
for (int i = 0; i < collections.size(); i += pageSize) {
162167
results.putAll(
163168
getAll(
164-
collections.subList(i, Math.min(collections.size(), i + pageSize)), offset, limit));
169+
collections.subList(i, Math.min(collections.size(), i + pageSize)),
170+
offset,
171+
limit,
172+
/*filter*/ null));
165173
}
166174
return firstNEntries(results, limit, IndexOffset.DOCUMENT_COMPARATOR);
167175
}
@@ -171,7 +179,10 @@ public Map<DocumentKey, MutableDocument> getAll(
171179
* Returns the next {@code count} documents from the provided collections, ordered by read time.
172180
*/
173181
private Map<DocumentKey, MutableDocument> getAll(
174-
List<ResourcePath> collections, IndexOffset offset, int count) {
182+
List<ResourcePath> collections,
183+
IndexOffset offset,
184+
int count,
185+
@Nullable Function<MutableDocument, Boolean> filter) {
175186
Timestamp readTime = offset.getReadTime().getTimestamp();
176187
DocumentKey documentKey = offset.getDocumentKey();
177188

@@ -207,13 +218,16 @@ private Map<DocumentKey, MutableDocument> getAll(
207218
Map<DocumentKey, MutableDocument> results = new HashMap<>();
208219
db.query(sql.toString())
209220
.binding(bindVars)
210-
.forEach(row -> processRowInBackground(backgroundQueue, results, row));
221+
.forEach(row -> processRowInBackground(backgroundQueue, results, row, filter));
211222
backgroundQueue.drain();
212223
return results;
213224
}
214225

215226
private void processRowInBackground(
216-
BackgroundQueue backgroundQueue, Map<DocumentKey, MutableDocument> results, Cursor row) {
227+
BackgroundQueue backgroundQueue,
228+
Map<DocumentKey, MutableDocument> results,
229+
Cursor row,
230+
@Nullable Function<MutableDocument, Boolean> filter) {
217231
byte[] rawDocument = row.getBlob(0);
218232
int readTimeSeconds = row.getInt(1);
219233
int readTimeNanos = row.getInt(2);
@@ -225,15 +239,22 @@ private void processRowInBackground(
225239
() -> {
226240
MutableDocument document =
227241
decodeMaybeDocument(rawDocument, readTimeSeconds, readTimeNanos);
228-
synchronized (results) {
229-
results.put(document.getKey(), document);
242+
if (filter == null || filter.apply(document)) {
243+
synchronized (results) {
244+
results.put(document.getKey(), document);
245+
}
230246
}
231247
});
232248
}
233249

234250
@Override
235-
public Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset) {
236-
return getAll(Collections.singletonList(collection), offset, Integer.MAX_VALUE);
251+
public Map<DocumentKey, MutableDocument> getDocumentsMatchingQuery(
252+
Query query, IndexOffset offset, @Nonnull Set<DocumentKey> mutatedKeys) {
253+
return getAll(
254+
Collections.singletonList(query.getPath()),
255+
offset,
256+
Integer.MAX_VALUE,
257+
(MutableDocument doc) -> query.matches(doc) || mutatedKeys.contains(doc.getKey()));
237258
}
238259

239260
private MutableDocument decodeMaybeDocument(

firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Collections;
3434
import java.util.HashMap;
3535
import java.util.Map;
36+
import java.util.Set;
3637
import java.util.SortedSet;
3738

3839
/**
@@ -167,8 +168,10 @@ public Map<DocumentKey, MutableDocument> getAll(
167168
}
168169

169170
@Override
170-
public Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset) {
171-
Map<DocumentKey, MutableDocument> result = subject.getAll(collection, offset);
171+
public Map<DocumentKey, MutableDocument> getDocumentsMatchingQuery(
172+
Query query, IndexOffset offset, Set<DocumentKey> mutatedKeys) {
173+
Map<DocumentKey, MutableDocument> result =
174+
subject.getDocumentsMatchingQuery(query, offset, mutatedKeys);
172175
documentsReadByCollection[0] += result.size();
173176
return result;
174177
}

firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1105,7 +1105,7 @@ public void testUsesTargetMappingToExecuteQueries() {
11051105
// Execute the query, but note that we read all existing documents from the RemoteDocumentCache
11061106
// since we do not yet have target mapping.
11071107
executeQuery(query);
1108-
assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 3);
1108+
assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2);
11091109

11101110
// Issue a RemoteEvent to persist the target mapping.
11111111
applyRemoteEvent(

0 commit comments

Comments
 (0)