Skip to content

Commit 1888bd7

Browse files
authored
Port performance optimizations to speed up reading large collections from Android (#1433)
Straightforward port of firebase/firebase-android-sdk#123.
1 parent 54df997 commit 1888bd7

17 files changed

+433
-46
lines changed

packages/firestore/src/local/indexeddb_mutation_queue.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { BATCHID_UNKNOWN, MutationBatch } from '../model/mutation_batch';
2525
import { ResourcePath } from '../model/path';
2626
import { assert, fail } from '../util/assert';
2727
import { primitiveComparator } from '../util/misc';
28+
import { SortedMap } from '../util/sorted_map';
2829
import { SortedSet } from '../util/sorted_set';
2930

3031
import * as EncodedResourcePath from './encoded_resource_path';
@@ -46,6 +47,8 @@ import { PersistenceTransaction, ReferenceDelegate } from './persistence';
4647
import { PersistencePromise } from './persistence_promise';
4748
import { SimpleDbStore, SimpleDbTransaction } from './simple_db';
4849

50+
import { AnyJs } from '../../src/util/misc';
51+
4952
/** A mutation queue for a specific user, backed by IndexedDB. */
5053
export class IndexedDbMutationQueue implements MutationQueue {
5154
/**
@@ -325,7 +328,7 @@ export class IndexedDbMutationQueue implements MutationQueue {
325328

326329
getAllMutationBatchesAffectingDocumentKeys(
327330
transaction: PersistenceTransaction,
328-
documentKeys: DocumentKeySet
331+
documentKeys: SortedMap<DocumentKey, AnyJs>
329332
): PersistencePromise<MutationBatch[]> {
330333
let uniqueBatchIDs = new SortedSet<BatchId>(primitiveComparator);
331334

packages/firestore/src/local/indexeddb_remote_document_cache.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616

1717
import { Query } from '../core/query';
1818
import {
19+
DocumentKeySet,
1920
documentKeySet,
2021
DocumentMap,
2122
documentMap,
23+
DocumentSizeEntries,
2224
DocumentSizeEntry,
2325
MaybeDocumentMap,
24-
maybeDocumentMap
26+
maybeDocumentMap,
27+
nullableMaybeDocumentMap,
28+
NullableMaybeDocumentMap
2529
} from '../model/collections';
2630
import { Document, MaybeDocument, NoDocument } from '../model/document';
2731
import { DocumentKey } from '../model/document_key';
32+
import { SortedMap } from '../util/sorted_map';
2833

2934
import { SnapshotVersion } from '../core/snapshot_version';
3035
import { assert, fail } from '../util/assert';
@@ -178,6 +183,110 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache {
178183
});
179184
}
180185

186+
getEntries(
187+
transaction: PersistenceTransaction,
188+
documentKeys: DocumentKeySet
189+
): PersistencePromise<NullableMaybeDocumentMap> {
190+
let results = nullableMaybeDocumentMap();
191+
return this.forEachDbEntry(
192+
transaction,
193+
documentKeys,
194+
(key, dbRemoteDoc) => {
195+
if (dbRemoteDoc) {
196+
results = results.insert(
197+
key,
198+
this.serializer.fromDbRemoteDocument(dbRemoteDoc)
199+
);
200+
} else {
201+
results = results.insert(key, null);
202+
}
203+
}
204+
).next(() => results);
205+
}
206+
207+
/**
208+
* Looks up several entries in the cache.
209+
*
210+
* @param documentKeys The set of keys entries to look up.
211+
* @return A map of MaybeDocuments indexed by key (if a document cannot be
212+
* found, the key will be mapped to null) and a map of sizes indexed by
213+
* key (zero if the key cannot be found).
214+
*/
215+
getSizedEntries(
216+
transaction: PersistenceTransaction,
217+
documentKeys: DocumentKeySet
218+
): PersistencePromise<DocumentSizeEntries> {
219+
let results = nullableMaybeDocumentMap();
220+
let sizeMap = new SortedMap<DocumentKey, number>(DocumentKey.comparator);
221+
return this.forEachDbEntry(
222+
transaction,
223+
documentKeys,
224+
(key, dbRemoteDoc) => {
225+
if (dbRemoteDoc) {
226+
results = results.insert(
227+
key,
228+
this.serializer.fromDbRemoteDocument(dbRemoteDoc)
229+
);
230+
sizeMap = sizeMap.insert(key, dbDocumentSize(dbRemoteDoc));
231+
} else {
232+
results = results.insert(key, null);
233+
sizeMap = sizeMap.insert(key, 0);
234+
}
235+
}
236+
).next(() => {
237+
return { maybeDocuments: results, sizeMap };
238+
});
239+
}
240+
241+
private forEachDbEntry(
242+
transaction: PersistenceTransaction,
243+
documentKeys: DocumentKeySet,
244+
callback: (key: DocumentKey, doc: DbRemoteDocument | null) => void
245+
): PersistencePromise<void> {
246+
if (documentKeys.isEmpty()) {
247+
return PersistencePromise.resolve();
248+
}
249+
250+
const range = IDBKeyRange.bound(
251+
documentKeys.first()!.path.toArray(),
252+
documentKeys.last()!.path.toArray()
253+
);
254+
const keyIter = documentKeys.getIterator();
255+
let nextKey: DocumentKey | null = keyIter.getNext();
256+
257+
return remoteDocumentsStore(transaction)
258+
.iterate({ range }, (potentialKeyRaw, dbRemoteDoc, control) => {
259+
const potentialKey = DocumentKey.fromSegments(potentialKeyRaw);
260+
261+
// Go through keys not found in cache.
262+
while (nextKey && DocumentKey.comparator(nextKey!, potentialKey) < 0) {
263+
callback(nextKey!, null);
264+
nextKey = keyIter.getNext();
265+
}
266+
267+
if (nextKey && nextKey!.isEqual(potentialKey)) {
268+
// Key found in cache.
269+
callback(nextKey!, dbRemoteDoc);
270+
nextKey = keyIter.hasNext() ? keyIter.getNext() : null;
271+
}
272+
273+
// Skip to the next key (if there is one).
274+
if (nextKey) {
275+
control.skip(nextKey!.path.toArray());
276+
} else {
277+
control.done();
278+
}
279+
})
280+
.next(() => {
281+
// The rest of the keys are not in the cache. One case where `iterate`
282+
// above won't go through them is when the cache is empty.
283+
while (nextKey) {
284+
callback(nextKey!, null);
285+
nextKey = keyIter.hasNext() ? keyIter.getNext() : null;
286+
}
287+
});
288+
}
289+
181290
getDocumentsMatchingQuery(
182291
transaction: PersistenceTransaction,
183292
query: Query
@@ -381,6 +490,13 @@ class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer {
381490
): PersistencePromise<DocumentSizeEntry | null> {
382491
return this.documentCache.getSizedEntry(transaction, documentKey);
383492
}
493+
494+
protected getAllFromCache(
495+
transaction: PersistenceTransaction,
496+
documentKeys: DocumentKeySet
497+
): PersistencePromise<DocumentSizeEntries> {
498+
return this.documentCache.getSizedEntries(transaction, documentKeys);
499+
}
384500
}
385501

386502
export function isDocumentChangeMissingError(err: FirestoreError): boolean {

packages/firestore/src/local/local_documents_view.ts

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import {
2121
DocumentMap,
2222
documentMap,
2323
MaybeDocumentMap,
24-
maybeDocumentMap
24+
maybeDocumentMap,
25+
NullableMaybeDocumentMap,
26+
nullableMaybeDocumentMap
2527
} from '../model/collections';
2628
import { Document, MaybeDocument, NoDocument } from '../model/document';
2729
import { DocumentKey } from '../model/document_key';
@@ -74,6 +76,23 @@ export class LocalDocumentsView {
7476
});
7577
}
7678

79+
// Returns the view of the given `docs` as they would appear after applying
80+
// all mutations in the given `batches`.
81+
private applyLocalMutationsToDocuments(
82+
transaction: PersistenceTransaction,
83+
docs: NullableMaybeDocumentMap,
84+
batches: MutationBatch[]
85+
): NullableMaybeDocumentMap {
86+
let results = nullableMaybeDocumentMap();
87+
docs.forEach((key, localView) => {
88+
for (const batch of batches) {
89+
localView = batch.applyToLocalView(key, localView);
90+
}
91+
results = results.insert(key, localView);
92+
});
93+
return results;
94+
}
95+
7796
/**
7897
* Gets the local view of the documents identified by `keys`.
7998
*
@@ -83,29 +102,38 @@ export class LocalDocumentsView {
83102
getDocuments(
84103
transaction: PersistenceTransaction,
85104
keys: DocumentKeySet
105+
): PersistencePromise<MaybeDocumentMap> {
106+
return this.remoteDocumentCache
107+
.getEntries(transaction, keys)
108+
.next(docs => this.getLocalViewOfDocuments(transaction, docs));
109+
}
110+
111+
/**
112+
* Similar to `getDocuments`, but creates the local view from the given
113+
* `baseDocs` without retrieving documents from the local store.
114+
*/
115+
getLocalViewOfDocuments(
116+
transaction: PersistenceTransaction,
117+
baseDocs: NullableMaybeDocumentMap
86118
): PersistencePromise<MaybeDocumentMap> {
87119
return this.mutationQueue
88-
.getAllMutationBatchesAffectingDocumentKeys(transaction, keys)
120+
.getAllMutationBatchesAffectingDocumentKeys(transaction, baseDocs)
89121
.next(batches => {
90-
const promises = [] as Array<PersistencePromise<void>>;
122+
const docs = this.applyLocalMutationsToDocuments(
123+
transaction,
124+
baseDocs,
125+
batches
126+
);
91127
let results = maybeDocumentMap();
92-
keys.forEach(key => {
93-
promises.push(
94-
this.getDocumentInternal(transaction, key, batches).next(
95-
maybeDoc => {
96-
// TODO(http://b/32275378): Don't conflate missing / deleted.
97-
if (!maybeDoc) {
98-
maybeDoc = new NoDocument(
99-
key,
100-
SnapshotVersion.forDeletedDoc()
101-
);
102-
}
103-
results = results.insert(key, maybeDoc);
104-
}
105-
)
106-
);
128+
docs.forEach((key, maybeDoc) => {
129+
// TODO(http://b/32275378): Don't conflate missing / deleted.
130+
if (!maybeDoc) {
131+
maybeDoc = new NoDocument(key, SnapshotVersion.forDeletedDoc());
132+
}
133+
results = results.insert(key, maybeDoc);
107134
});
108-
return PersistencePromise.waitFor(promises).next(() => results);
135+
136+
return results;
109137
});
110138
}
111139

packages/firestore/src/local/local_serializer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ export class LocalSerializer {
7171
/** Encodes a document for storage locally. */
7272
toDbRemoteDocument(maybeDoc: MaybeDocument): DbRemoteDocument {
7373
if (maybeDoc instanceof Document) {
74-
const doc = this.remoteSerializer.toDocument(maybeDoc);
74+
const doc = maybeDoc.proto
75+
? maybeDoc.proto
76+
: this.remoteSerializer.toDocument(maybeDoc);
7577
const hasCommittedMutations = maybeDoc.hasCommittedMutations;
7678
return new DbRemoteDocument(
7779
/* unknownDocument= */ null,

packages/firestore/src/local/local_store.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
DocumentKeySet,
2424
documentKeySet,
2525
DocumentMap,
26+
maybeDocumentMap,
2627
MaybeDocumentMap
2728
} from '../model/collections';
2829
import { MaybeDocument } from '../model/document';
@@ -466,11 +467,18 @@ export class LocalStore {
466467
}
467468
);
468469

469-
let changedDocKeys = documentKeySet();
470+
let changedDocs = maybeDocumentMap();
471+
let updatedKeys = documentKeySet();
470472
remoteEvent.documentUpdates.forEach((key, doc) => {
471-
changedDocKeys = changedDocKeys.add(key);
472-
promises.push(
473-
documentBuffer.getEntry(txn, key).next(existingDoc => {
473+
updatedKeys = updatedKeys.add(key);
474+
});
475+
476+
// Each loop iteration only affects its "own" doc, so it's safe to get all the remote
477+
// documents in advance in a single call.
478+
promises.push(
479+
documentBuffer.getEntries(txn, updatedKeys).next(existingDocs => {
480+
remoteEvent.documentUpdates.forEach((key, doc) => {
481+
const existingDoc = existingDocs.get(key);
474482
// If a document update isn't authoritative, make sure we don't
475483
// apply an old document version to the remote cache. We make an
476484
// exception for SnapshotVersion.MIN which can happen for
@@ -484,6 +492,7 @@ export class LocalStore {
484492
doc.version.compareTo(existingDoc.version) >= 0
485493
) {
486494
documentBuffer.addEntry(doc);
495+
changedDocs = changedDocs.insert(key, doc);
487496
} else {
488497
log.debug(
489498
LOG_TAG,
@@ -495,14 +504,18 @@ export class LocalStore {
495504
doc.version
496505
);
497506
}
498-
})
499-
);
500-
if (remoteEvent.resolvedLimboDocuments.has(key)) {
501-
promises.push(
502-
this.persistence.referenceDelegate.updateLimboDocument(txn, key)
503-
);
504-
}
505-
});
507+
508+
if (remoteEvent.resolvedLimboDocuments.has(key)) {
509+
promises.push(
510+
this.persistence.referenceDelegate.updateLimboDocument(
511+
txn,
512+
key
513+
)
514+
);
515+
}
516+
});
517+
})
518+
);
506519

507520
// HACK: The only reason we allow a null snapshot version is so that we
508521
// can synthesize remote events when we get permission denied errors while
@@ -532,7 +545,10 @@ export class LocalStore {
532545
return PersistencePromise.waitFor(promises)
533546
.next(() => documentBuffer.apply(txn))
534547
.next(() => {
535-
return this.localDocuments.getDocuments(txn, changedDocKeys);
548+
return this.localDocuments.getLocalViewOfDocuments(
549+
txn,
550+
changedDocs
551+
);
536552
});
537553
}
538554
);

packages/firestore/src/local/memory_mutation_queue.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ import { BATCHID_UNKNOWN, MutationBatch } from '../model/mutation_batch';
2424
import { emptyByteString } from '../platform/platform';
2525
import { assert } from '../util/assert';
2626
import { primitiveComparator } from '../util/misc';
27+
import { SortedMap } from '../util/sorted_map';
2728
import { SortedSet } from '../util/sorted_set';
2829

2930
import { MutationQueue } from './mutation_queue';
3031
import { PersistenceTransaction, ReferenceDelegate } from './persistence';
3132
import { PersistencePromise } from './persistence_promise';
3233
import { DocReference } from './reference_set';
3334

35+
import { AnyJs } from '../../src/util/misc';
36+
3437
export class MemoryMutationQueue implements MutationQueue {
3538
/**
3639
* The set of all mutations that have been sent but not yet been applied to
@@ -203,7 +206,7 @@ export class MemoryMutationQueue implements MutationQueue {
203206

204207
getAllMutationBatchesAffectingDocumentKeys(
205208
transaction: PersistenceTransaction,
206-
documentKeys: DocumentKeySet
209+
documentKeys: SortedMap<DocumentKey, AnyJs>
207210
): PersistencePromise<MutationBatch[]> {
208211
let uniqueBatchIDs = new SortedSet<number>(primitiveComparator);
209212

0 commit comments

Comments
 (0)