Skip to content

Commit c7e5012

Browse files
Don't deserialize full Document proto for Query execution (#2115)
1 parent c851936 commit c7e5012

File tree

15 files changed

+183
-71
lines changed

15 files changed

+183
-71
lines changed

packages/firestore/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Unreleased (1.5.0)
32
- [feature] Added a `Firestore.waitForPendingWrites()` method that
43
allows users to wait until all pending writes are acknowledged by the
@@ -7,6 +6,8 @@
76
the instance, releasing any held resources. Once it completes, you can
87
optionally call `Firestore.clearPersistence()` to wipe persisted Firestore
98
data from disk.
9+
- [changed] Improved performance for queries with filters that only return a
10+
small subset of the documents in a collection.
1011

1112
# 1.4.10
1213
- [changed] Transactions now perform exponential backoff before retrying.
@@ -25,6 +26,7 @@
2526
match the query (https://github.com/firebase/firebase-android-sdk/issues/155).
2627

2728
# 1.4.4
29+
>>>>>>> master
2830
- [fixed] Fixed an internal assertion that was triggered when an update
2931
with a `FieldValue.serverTimestamp()` and an update with a
3032
`FieldValue.increment()` were pending for the same document.

packages/firestore/src/api/database.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,7 +1276,7 @@ export class DocumentSnapshot implements firestore.DocumentSnapshot {
12761276
return !this._document
12771277
? undefined
12781278
: this.convertObject(
1279-
this._document.data,
1279+
this._document.data(),
12801280
FieldValueOptions.fromSnapshotOptions(
12811281
options,
12821282
this._firestore._areTimestampsInSnapshotsEnabled()
@@ -1291,9 +1291,9 @@ export class DocumentSnapshot implements firestore.DocumentSnapshot {
12911291
validateBetweenNumberOfArgs('DocumentSnapshot.get', arguments, 1, 2);
12921292
options = validateSnapshotOptions('DocumentSnapshot.get', options);
12931293
if (this._document) {
1294-
const value = this._document.data.field(
1295-
fieldPathFromArgument('DocumentSnapshot.get', fieldPath)
1296-
);
1294+
const value = this._document
1295+
.data()
1296+
.field(fieldPathFromArgument('DocumentSnapshot.get', fieldPath));
12971297
if (value !== null) {
12981298
return this.convertValue(
12991299
value,

packages/firestore/src/core/query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ export class Bound {
759759
} else {
760760
const docValue = doc.field(orderByComponent.field);
761761
assert(
762-
docValue !== undefined,
762+
docValue !== null,
763763
'Field should exist since document matched the orderBy already.'
764764
);
765765
comparison = component.compareTo(docValue!);

packages/firestore/src/core/view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export class View {
169169

170170
// Calculate change
171171
if (oldDoc && newDoc) {
172-
const docsEqual = oldDoc.data.isEqual(newDoc.data);
172+
const docsEqual = oldDoc.data().isEqual(newDoc.data());
173173
if (!docsEqual) {
174174
if (!this.shouldWaitForSyncedDocument(oldDoc, newDoc)) {
175175
changeSet.track({

packages/firestore/src/model/document.ts

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
*/
1717

1818
import { SnapshotVersion } from '../core/snapshot_version';
19-
import { fail } from '../util/assert';
19+
import { assert, fail } from '../util/assert';
2020

2121
import { DocumentKey } from './document_key';
2222
import { FieldValue, JsonObject, ObjectValue } from './field_value';
2323
import { FieldPath } from './path';
2424

2525
import * as api from '../protos/firestore_proto_api';
26+
import * as obj from '../util/obj';
2627

2728
export interface DocumentOptions {
2829
hasLocalMutations?: boolean;
@@ -59,43 +60,91 @@ export class Document extends MaybeDocument {
5960
readonly hasLocalMutations: boolean;
6061
readonly hasCommittedMutations: boolean;
6162

63+
/**
64+
* A cache of canonicalized FieldPaths to FieldValues that have already been
65+
* deserialized in `getField()`.
66+
*/
67+
private fieldValueCache?: Map<string, FieldValue | null>;
68+
6269
constructor(
6370
key: DocumentKey,
6471
version: SnapshotVersion,
65-
readonly data: ObjectValue,
6672
options: DocumentOptions,
67-
/**
68-
* Memoized serialized form of the document for optimization purposes (avoids repeated
69-
* serialization). Might be undefined.
70-
*/
71-
readonly proto?: api.Document
73+
private objectValue?: ObjectValue,
74+
readonly proto?: api.Document,
75+
private readonly converter?: (value: api.Value) => FieldValue
7276
) {
7377
super(key, version);
78+
assert(
79+
this.objectValue !== undefined ||
80+
(this.proto !== undefined && this.converter !== undefined),
81+
'If objectValue is not defined, proto and converter need to be set.'
82+
);
83+
7484
this.hasLocalMutations = !!options.hasLocalMutations;
7585
this.hasCommittedMutations = !!options.hasCommittedMutations;
7686
}
7787

7888
field(path: FieldPath): FieldValue | null {
79-
return this.data.field(path);
89+
if (this.objectValue) {
90+
return this.objectValue.field(path);
91+
} else {
92+
if (!this.fieldValueCache) {
93+
// TODO(b/136090445): Remove the cache when `getField` is no longer
94+
// called during Query ordering.
95+
this.fieldValueCache = new Map<string, FieldValue>();
96+
}
97+
98+
const canonicalPath = path.canonicalString();
99+
100+
let fieldValue = this.fieldValueCache.get(canonicalPath);
101+
102+
if (fieldValue === undefined) {
103+
// Instead of deserializing the full Document proto, we only
104+
// deserialize the value at the requested field path. This speeds up
105+
// Query execution as query filters can discard documents based on a
106+
// single field.
107+
const protoValue = this.getProtoField(path);
108+
if (protoValue === undefined) {
109+
fieldValue = null;
110+
} else {
111+
fieldValue = this.converter!(protoValue);
112+
}
113+
this.fieldValueCache.set(canonicalPath, fieldValue);
114+
}
115+
116+
return fieldValue!;
117+
}
80118
}
81119

82-
fieldValue(path: FieldPath): unknown {
83-
const field = this.field(path);
84-
return field ? field.value() : undefined;
120+
data(): ObjectValue {
121+
if (!this.objectValue) {
122+
let result = ObjectValue.EMPTY;
123+
obj.forEach(this.proto!.fields, (key: string, value: api.Value) => {
124+
result = result.set(new FieldPath([key]), this.converter!(value));
125+
});
126+
this.objectValue = result;
127+
128+
// Once objectValue is computed, values inside the fieldValueCache are no
129+
// longer accessed.
130+
this.fieldValueCache = undefined;
131+
}
132+
133+
return this.objectValue;
85134
}
86135

87136
value(): JsonObject<unknown> {
88-
return this.data.value();
137+
return this.data().value();
89138
}
90139

91140
isEqual(other: MaybeDocument | null | undefined): boolean {
92141
return (
93142
other instanceof Document &&
94143
this.key.isEqual(other.key) &&
95144
this.version.isEqual(other.version) &&
96-
this.data.isEqual(other.data) &&
97145
this.hasLocalMutations === other.hasLocalMutations &&
98-
this.hasCommittedMutations === other.hasCommittedMutations
146+
this.hasCommittedMutations === other.hasCommittedMutations &&
147+
this.data().isEqual(other.data())
99148
);
100149
}
101150

@@ -111,6 +160,29 @@ export class Document extends MaybeDocument {
111160
return this.hasLocalMutations || this.hasCommittedMutations;
112161
}
113162

163+
/**
164+
* Returns the nested Protobuf value for 'path`. Can only be called if
165+
* `proto` was provided at construction time.
166+
*/
167+
private getProtoField(path: FieldPath): api.Value | undefined {
168+
assert(
169+
this.proto !== undefined,
170+
'Can only call getProtoField() when proto is defined'
171+
);
172+
173+
let protoValue: api.Value | undefined = this.proto!.fields[
174+
path.firstSegment()
175+
];
176+
for (let i = 1; i < path.length; ++i) {
177+
if (!protoValue || !protoValue.mapValue) {
178+
return undefined;
179+
}
180+
protoValue = protoValue.mapValue.fields[path.get(i)];
181+
}
182+
183+
return protoValue;
184+
}
185+
114186
static compareByField(field: FieldPath, d1: Document, d2: Document): number {
115187
const v1 = d1.field(field);
116188
const v2 = d2.field(field);

packages/firestore/src/model/mutation.ts

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,14 @@ export class SetMutation extends Mutation {
353353
// have held.
354354

355355
const version = mutationResult.version;
356-
return new Document(this.key, version, this.value, {
357-
hasCommittedMutations: true
358-
});
356+
return new Document(
357+
this.key,
358+
version,
359+
{
360+
hasCommittedMutations: true
361+
},
362+
this.value
363+
);
359364
}
360365

361366
applyToLocalView(
@@ -370,9 +375,14 @@ export class SetMutation extends Mutation {
370375
}
371376

372377
const version = Mutation.getPostMutationVersion(maybeDoc);
373-
return new Document(this.key, version, this.value, {
374-
hasLocalMutations: true
375-
});
378+
return new Document(
379+
this.key,
380+
version,
381+
{
382+
hasLocalMutations: true
383+
},
384+
this.value
385+
);
376386
}
377387

378388
extractBaseValue(maybeDoc: MaybeDocument | null): null {
@@ -434,9 +444,14 @@ export class PatchMutation extends Mutation {
434444
}
435445

436446
const newData = this.patchDocument(maybeDoc);
437-
return new Document(this.key, mutationResult.version, newData, {
438-
hasCommittedMutations: true
439-
});
447+
return new Document(
448+
this.key,
449+
mutationResult.version,
450+
{
451+
hasCommittedMutations: true
452+
},
453+
newData
454+
);
440455
}
441456

442457
applyToLocalView(
@@ -452,9 +467,14 @@ export class PatchMutation extends Mutation {
452467

453468
const version = Mutation.getPostMutationVersion(maybeDoc);
454469
const newData = this.patchDocument(maybeDoc);
455-
return new Document(this.key, version, newData, {
456-
hasLocalMutations: true
457-
});
470+
return new Document(
471+
this.key,
472+
version,
473+
{
474+
hasLocalMutations: true
475+
},
476+
newData
477+
);
458478
}
459479

460480
extractBaseValue(maybeDoc: MaybeDocument | null): null {
@@ -478,7 +498,7 @@ export class PatchMutation extends Mutation {
478498
private patchDocument(maybeDoc: MaybeDocument | null): ObjectValue {
479499
let data: ObjectValue;
480500
if (maybeDoc instanceof Document) {
481-
data = maybeDoc.data;
501+
data = maybeDoc.data();
482502
} else {
483503
data = ObjectValue.EMPTY;
484504
}
@@ -550,10 +570,15 @@ export class TransformMutation extends Mutation {
550570
);
551571

552572
const version = mutationResult.version;
553-
const newData = this.transformObject(doc.data, transformResults);
554-
return new Document(this.key, version, newData, {
555-
hasCommittedMutations: true
556-
});
573+
const newData = this.transformObject(doc.data(), transformResults);
574+
return new Document(
575+
this.key,
576+
version,
577+
{
578+
hasCommittedMutations: true
579+
},
580+
newData
581+
);
557582
}
558583

559584
applyToLocalView(
@@ -573,10 +598,15 @@ export class TransformMutation extends Mutation {
573598
maybeDoc,
574599
baseDoc
575600
);
576-
const newData = this.transformObject(doc.data, transformResults);
577-
return new Document(this.key, doc.version, newData, {
578-
hasLocalMutations: true
579-
});
601+
const newData = this.transformObject(doc.data(), transformResults);
602+
return new Document(
603+
this.key,
604+
doc.version,
605+
{
606+
hasLocalMutations: true
607+
},
608+
newData
609+
);
580610
}
581611

582612
extractBaseValue(maybeDoc: MaybeDocument | null): ObjectValue | null {

0 commit comments

Comments
 (0)