Skip to content

Commit ca008a4

Browse files
authored
Port optimizations to LocalDocumentsView from iOS (#1055)
* add a method to find batches affecting a set of keys (port of [1479](firebase/firebase-ios-sdk#1479)); * use the newly-added method to avoid rereading batches when getting documents in `LocalDocumentsView` (port of [1505](firebase/firebase-ios-sdk#1505)); * avoid rereading batches when searching for documents in a collection (port of [1533](firebase/firebase-ios-sdk#1533)). Speedup was measured by running tests in browser and checking time spent writing 10 batches of 500 mutations each, and then querying the resulting 5K docs collection from cache in offline mode. For this case, the writing speedup is about 3x, and querying speedup is about 6x (see PR for more details).
1 parent 063f729 commit ca008a4

File tree

7 files changed

+238
-127
lines changed

7 files changed

+238
-127
lines changed

packages/firestore/src/local/indexeddb_mutation_queue.ts

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Timestamp } from '../api/timestamp';
1818
import { User } from '../auth/user';
1919
import { Query } from '../core/query';
2020
import { BatchId, ProtoByteString } from '../core/types';
21+
import { DocumentKeySet } from '../model/collections';
2122
import { DocumentKey } from '../model/document_key';
2223
import { Mutation } from '../model/mutation';
2324
import { BATCHID_UNKNOWN, MutationBatch } from '../model/mutation_batch';
@@ -363,6 +364,50 @@ export class IndexedDbMutationQueue implements MutationQueue {
363364
.next(() => results);
364365
}
365366

367+
getAllMutationBatchesAffectingDocumentKeys(
368+
transaction: PersistenceTransaction,
369+
documentKeys: DocumentKeySet
370+
): PersistencePromise<MutationBatch[]> {
371+
let uniqueBatchIDs = new SortedSet<BatchId>(primitiveComparator);
372+
373+
const promises: Array<PersistencePromise<void>> = [];
374+
documentKeys.forEach(documentKey => {
375+
const indexStart = DbDocumentMutation.prefixForPath(
376+
this.userId,
377+
documentKey.path
378+
);
379+
const range = IDBKeyRange.lowerBound(indexStart);
380+
381+
const promise = documentMutationsStore(transaction).iterate(
382+
{ range },
383+
(indexKey, _, control) => {
384+
const [userID, encodedPath, batchID] = indexKey;
385+
386+
// Only consider rows matching exactly the specific key of
387+
// interest. Note that because we order by path first, and we
388+
// order terminators before path separators, we'll encounter all
389+
// the index rows for documentKey contiguously. In particular, all
390+
// the rows for documentKey will occur before any rows for
391+
// documents nested in a subcollection beneath documentKey so we
392+
// can stop as soon as we hit any such row.
393+
const path = EncodedResourcePath.decode(encodedPath);
394+
if (userID !== this.userId || !documentKey.path.isEqual(path)) {
395+
control.done();
396+
return;
397+
}
398+
399+
uniqueBatchIDs = uniqueBatchIDs.add(batchID);
400+
}
401+
);
402+
403+
promises.push(promise);
404+
});
405+
406+
return PersistencePromise.waitFor(promises).next(() =>
407+
this.lookupMutationBatches(transaction, uniqueBatchIDs)
408+
);
409+
}
410+
366411
getAllMutationBatchesAffectingQuery(
367412
transaction: PersistenceTransaction,
368413
query: Query
@@ -414,29 +459,34 @@ export class IndexedDbMutationQueue implements MutationQueue {
414459
}
415460
uniqueBatchIDs = uniqueBatchIDs.add(batchID);
416461
})
417-
.next(() => {
418-
const results: MutationBatch[] = [];
419-
const promises: Array<PersistencePromise<void>> = [];
420-
// TODO(rockwood): Implement this using iterate.
421-
uniqueBatchIDs.forEach(batchID => {
422-
const mutationKey = this.keyForBatchId(batchID);
423-
promises.push(
424-
mutationsStore(transaction)
425-
.get(mutationKey)
426-
.next(mutation => {
427-
if (mutation === null) {
428-
fail(
429-
'Dangling document-mutation reference found, ' +
430-
'which points to ' +
431-
mutationKey
432-
);
433-
}
434-
results.push(this.serializer.fromDbMutationBatch(mutation!));
435-
})
436-
);
437-
});
438-
return PersistencePromise.waitFor(promises).next(() => results);
439-
});
462+
.next(() => this.lookupMutationBatches(transaction, uniqueBatchIDs));
463+
}
464+
465+
private lookupMutationBatches(
466+
transaction: PersistenceTransaction,
467+
batchIDs: SortedSet<BatchId>
468+
): PersistencePromise<MutationBatch[]> {
469+
const results: MutationBatch[] = [];
470+
const promises: Array<PersistencePromise<void>> = [];
471+
// TODO(rockwood): Implement this using iterate.
472+
batchIDs.forEach(batchID => {
473+
const mutationKey = this.keyForBatchId(batchID);
474+
promises.push(
475+
mutationsStore(transaction)
476+
.get(mutationKey)
477+
.next(mutation => {
478+
if (mutation === null) {
479+
fail(
480+
'Dangling document-mutation reference found, ' +
481+
'which points to ' +
482+
mutationKey
483+
);
484+
}
485+
results.push(this.serializer.fromDbMutationBatch(mutation!));
486+
})
487+
);
488+
});
489+
return PersistencePromise.waitFor(promises).next(() => results);
440490
}
441491

442492
removeMutationBatches(

packages/firestore/src/local/local_documents_view.ts

Lines changed: 60 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import { Query } from '../core/query';
1818
import { SnapshotVersion } from '../core/snapshot_version';
1919
import {
20-
documentKeySet,
2120
DocumentKeySet,
2221
DocumentMap,
2322
documentMap,
@@ -26,6 +25,7 @@ import {
2625
} from '../model/collections';
2726
import { Document, MaybeDocument, NoDocument } from '../model/document';
2827
import { DocumentKey } from '../model/document_key';
28+
import { MutationBatch } from '../model/mutation_batch';
2929
import { ResourcePath } from '../model/path';
3030
import { fail } from '../util/assert';
3131

@@ -56,11 +56,23 @@ export class LocalDocumentsView {
5656
transaction: PersistenceTransaction,
5757
key: DocumentKey
5858
): PersistencePromise<MaybeDocument | null> {
59-
return this.remoteDocumentCache
60-
.getEntry(transaction, key)
61-
.next(remoteDoc => {
62-
return this.computeLocalDocument(transaction, key, remoteDoc);
63-
});
59+
return this.mutationQueue
60+
.getAllMutationBatchesAffectingDocumentKey(transaction, key)
61+
.next(batches => this.getDocumentInternal(transaction, key, batches));
62+
}
63+
64+
/** Internal version of `getDocument` that allows reusing batches. */
65+
private getDocumentInternal(
66+
transaction: PersistenceTransaction,
67+
key: DocumentKey,
68+
inBatches: MutationBatch[]
69+
): PersistencePromise<MaybeDocument | null> {
70+
return this.remoteDocumentCache.getEntry(transaction, key).next(doc => {
71+
for (const batch of inBatches) {
72+
doc = batch.applyToLocalView(key, doc);
73+
}
74+
return doc;
75+
});
6476
}
6577

6678
/**
@@ -73,20 +85,29 @@ export class LocalDocumentsView {
7385
transaction: PersistenceTransaction,
7486
keys: DocumentKeySet
7587
): PersistencePromise<MaybeDocumentMap> {
76-
const promises = [] as Array<PersistencePromise<void>>;
77-
let results = maybeDocumentMap();
78-
keys.forEach(key => {
79-
promises.push(
80-
this.getDocument(transaction, key).next(maybeDoc => {
81-
// TODO(http://b/32275378): Don't conflate missing / deleted.
82-
if (!maybeDoc) {
83-
maybeDoc = new NoDocument(key, SnapshotVersion.forDeletedDoc());
84-
}
85-
results = results.insert(key, maybeDoc);
86-
})
87-
);
88-
});
89-
return PersistencePromise.waitFor(promises).next(() => results);
88+
return this.mutationQueue
89+
.getAllMutationBatchesAffectingDocumentKeys(transaction, keys)
90+
.next(batches => {
91+
const promises = [] as Array<PersistencePromise<void>>;
92+
let results = maybeDocumentMap();
93+
keys.forEach(key => {
94+
promises.push(
95+
this.getDocumentInternal(transaction, key, batches).next(
96+
maybeDoc => {
97+
// TODO(http://b/32275378): Don't conflate missing / deleted.
98+
if (!maybeDoc) {
99+
maybeDoc = new NoDocument(
100+
key,
101+
SnapshotVersion.forDeletedDoc()
102+
);
103+
}
104+
results = results.insert(key, maybeDoc);
105+
}
106+
)
107+
);
108+
});
109+
return PersistencePromise.waitFor(promises).next(() => results);
110+
});
90111
}
91112

92113
/** Performs a query against the local view of all documents. */
@@ -122,48 +143,40 @@ export class LocalDocumentsView {
122143
query: Query
123144
): PersistencePromise<DocumentMap> {
124145
// Query the remote documents and overlay mutations.
125-
// TODO(mikelehen): There may be significant overlap between the mutations
126-
// affecting these remote documents and the
127-
// getAllMutationBatchesAffectingQuery() mutations. Consider optimizing.
128146
let results: DocumentMap;
129147
return this.remoteDocumentCache
130148
.getDocumentsMatchingQuery(transaction, query)
131149
.next(queryResults => {
132-
return this.computeLocalDocuments(transaction, queryResults);
133-
})
134-
.next(promisedResults => {
135-
results = promisedResults;
136-
// Now use the mutation queue to discover any other documents that may
137-
// match the query after applying mutations.
150+
results = queryResults;
138151
return this.mutationQueue.getAllMutationBatchesAffectingQuery(
139152
transaction,
140153
query
141154
);
142155
})
143156
.next(matchingMutationBatches => {
144-
let matchingKeys = documentKeySet();
145157
for (const batch of matchingMutationBatches) {
146158
for (const mutation of batch.mutations) {
147-
// TODO(mikelehen): PERF: Check if this mutation actually
148-
// affects the query to reduce work.
149-
if (!results.get(mutation.key)) {
150-
matchingKeys = matchingKeys.add(mutation.key);
159+
const key = mutation.key;
160+
// Only process documents belonging to the collection.
161+
if (!query.path.isImmediateParentOf(key.path)) {
162+
continue;
163+
}
164+
165+
const baseDoc = results.get(key);
166+
const mutatedDoc = mutation.applyToLocalView(
167+
baseDoc,
168+
baseDoc,
169+
batch.localWriteTime
170+
);
171+
if (!mutatedDoc || mutatedDoc instanceof NoDocument) {
172+
results = results.remove(key);
173+
} else if (mutatedDoc instanceof Document) {
174+
results = results.insert(key, mutatedDoc);
175+
} else {
176+
fail('Unknown MaybeDocument: ' + mutatedDoc);
151177
}
152178
}
153179
}
154-
155-
// Now add in the results for the matchingKeys.
156-
const promises = [] as Array<PersistencePromise<void>>;
157-
matchingKeys.forEach(key => {
158-
promises.push(
159-
this.getDocument(transaction, key).next(doc => {
160-
if (doc instanceof Document) {
161-
results = results.insert(doc.key, doc);
162-
}
163-
})
164-
);
165-
});
166-
return PersistencePromise.waitFor(promises);
167180
})
168181
.next(() => {
169182
// Finally, filter out any documents that don't actually match
@@ -177,57 +190,4 @@ export class LocalDocumentsView {
177190
return results;
178191
});
179192
}
180-
181-
/**
182-
* Takes a remote document and applies local mutations to generate the local
183-
* view of the document.
184-
* @param transaction The transaction in which to perform any persistence
185-
* operations.
186-
* @param documentKey The key of the document (necessary when remoteDocument
187-
* is null).
188-
* @param document The base remote document to apply mutations to or null.
189-
*/
190-
private computeLocalDocument(
191-
transaction: PersistenceTransaction,
192-
documentKey: DocumentKey,
193-
document: MaybeDocument | null
194-
): PersistencePromise<MaybeDocument | null> {
195-
return this.mutationQueue
196-
.getAllMutationBatchesAffectingDocumentKey(transaction, documentKey)
197-
.next(batches => {
198-
for (const batch of batches) {
199-
document = batch.applyToLocalView(documentKey, document);
200-
}
201-
return document;
202-
});
203-
}
204-
205-
/**
206-
* Takes a set of remote documents and applies local mutations to generate the
207-
* local view of the documents.
208-
* @param transaction The transaction in which to perform any persistence
209-
* operations.
210-
* @param documents The base remote documents to apply mutations to.
211-
* @return The local view of the documents.
212-
*/
213-
private computeLocalDocuments(
214-
transaction: PersistenceTransaction,
215-
documents: DocumentMap
216-
): PersistencePromise<DocumentMap> {
217-
const promises = [] as Array<PersistencePromise<void>>;
218-
documents.forEach((key, doc) => {
219-
promises.push(
220-
this.computeLocalDocument(transaction, key, doc).next(mutatedDoc => {
221-
if (mutatedDoc instanceof Document) {
222-
documents = documents.insert(mutatedDoc.key, mutatedDoc);
223-
} else if (mutatedDoc instanceof NoDocument) {
224-
documents = documents.remove(mutatedDoc.key);
225-
} else {
226-
fail('Unknown MaybeDocument: ' + mutatedDoc);
227-
}
228-
})
229-
);
230-
});
231-
return PersistencePromise.waitFor(promises).next(() => documents);
232-
}
233193
}

packages/firestore/src/local/memory_mutation_queue.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { Timestamp } from '../api/timestamp';
1818
import { Query } from '../core/query';
1919
import { BatchId, ProtoByteString } from '../core/types';
20+
import { DocumentKeySet } from '../model/collections';
2021
import { DocumentKey } from '../model/document_key';
2122
import { Mutation } from '../model/mutation';
2223
import { BATCHID_UNKNOWN, MutationBatch } from '../model/mutation_batch';
@@ -252,6 +253,28 @@ export class MemoryMutationQueue implements MutationQueue {
252253
return PersistencePromise.resolve(result);
253254
}
254255

256+
getAllMutationBatchesAffectingDocumentKeys(
257+
transaction: PersistenceTransaction,
258+
documentKeys: DocumentKeySet
259+
): PersistencePromise<MutationBatch[]> {
260+
let uniqueBatchIDs = new SortedSet<number>(primitiveComparator);
261+
262+
documentKeys.forEach(documentKey => {
263+
const start = new DocReference(documentKey, 0);
264+
const end = new DocReference(documentKey, Number.POSITIVE_INFINITY);
265+
this.batchesByDocumentKey.forEachInRange([start, end], ref => {
266+
assert(
267+
documentKey.isEqual(ref.key),
268+
"For each key, should only iterate over a single key's batches"
269+
);
270+
271+
uniqueBatchIDs = uniqueBatchIDs.add(ref.targetOrBatchId);
272+
});
273+
});
274+
275+
return PersistencePromise.resolve(this.findMutationBatches(uniqueBatchIDs));
276+
}
277+
255278
getAllMutationBatchesAffectingQuery(
256279
transaction: PersistenceTransaction,
257280
query: Query
@@ -293,16 +316,20 @@ export class MemoryMutationQueue implements MutationQueue {
293316
}
294317
}, start);
295318

319+
return PersistencePromise.resolve(this.findMutationBatches(uniqueBatchIDs));
320+
}
321+
322+
private findMutationBatches(batchIDs: SortedSet<number>): MutationBatch[] {
296323
// Construct an array of matching batches, sorted by batchID to ensure that
297324
// multiple mutations affecting the same document key are applied in order.
298325
const result: MutationBatch[] = [];
299-
uniqueBatchIDs.forEach(batchId => {
326+
batchIDs.forEach(batchId => {
300327
const batch = this.findMutationBatch(batchId);
301328
if (batch !== null) {
302329
result.push(batch);
303330
}
304331
});
305-
return PersistencePromise.resolve(result);
332+
return result;
306333
}
307334

308335
removeMutationBatches(

0 commit comments

Comments
 (0)