Skip to content

Commit 48dcf7e

Browse files
Don't deserialize full document Proto for Query execution
1 parent 2e162e3 commit 48dcf7e

File tree

11 files changed

+123
-80
lines changed

11 files changed

+123
-80
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Unreleased
2+
- [changed] Improved performance for queries that only return a subset of the
3+
data in a collection.
24
- [changed] Instead of failing silently, Firestore now crashes the client app
35
if it fails to load SSL Ciphers. To avoid these crashes, you must bundle
46
Conscrypt to support non-GMSCore devices on Android KitKat or JellyBean (see

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,9 @@ private com.google.firestore.v1.Document encodeDocument(Document document) {
111111
/** Decodes a Document proto to the equivalent model. */
112112
private Document decodeDocument(
113113
com.google.firestore.v1.Document document, boolean hasCommittedMutations) {
114-
DocumentKey key = rpcSerializer.decodeKey(document.getName());
115-
ObjectValue value = rpcSerializer.decodeFields(document.getFieldsMap());
116-
SnapshotVersion version = rpcSerializer.decodeVersion(document.getUpdateTime());
117-
return new Document(
118-
key,
119-
version,
120-
value,
114+
return Document.fromProto(
115+
this.rpcSerializer,
116+
document,
121117
hasCommittedMutations
122118
? Document.DocumentState.COMMITTED_MUTATIONS
123119
: Document.DocumentState.SYNCED);

firebase-firestore/src/main/java/com/google/firebase/firestore/model/Document.java

Lines changed: 92 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@
1414

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

17+
import static com.google.firebase.firestore.util.Assert.hardAssert;
18+
1719
import com.google.firebase.firestore.model.value.FieldValue;
1820
import com.google.firebase.firestore.model.value.ObjectValue;
21+
import com.google.firebase.firestore.remote.RemoteSerializer;
22+
import com.google.firestore.v1.Value;
1923
import java.util.Comparator;
2024
import javax.annotation.Nullable;
2125

2226
/**
2327
* Represents a document in Firestore with a key, version, data and whether the data has local
2428
* mutations applied to it.
2529
*/
26-
public final class Document extends MaybeDocument {
30+
public abstract class Document extends MaybeDocument {
2731

2832
/** Describes the `hasPendingWrites` state of a document. */
2933
public enum DocumentState {
@@ -36,59 +40,107 @@ public enum DocumentState {
3640
}
3741

3842
private static final Comparator<Document> KEY_COMPARATOR =
39-
new Comparator<Document>() {
40-
@Override
41-
public int compare(Document left, Document right) {
42-
return left.getKey().compareTo(right.getKey());
43-
}
44-
};
43+
(left, right) -> left.getKey().compareTo(right.getKey());
4544

4645
/** A document comparator that returns document by key and key only. */
4746
public static Comparator<Document> keyComparator() {
4847
return KEY_COMPARATOR;
4948
}
5049

51-
private final ObjectValue data;
52-
53-
private final DocumentState documentState;
50+
/** Creates a new Document from an existing ObjectValue. */
51+
public static Document fromObjectValue(
52+
DocumentKey key, SnapshotVersion version, ObjectValue data, DocumentState documentState) {
53+
return new Document(key, version, documentState) {
54+
@Nullable
55+
@Override
56+
public com.google.firestore.v1.Document getProto() {
57+
return null;
58+
}
59+
60+
@Override
61+
public ObjectValue getData() {
62+
return data;
63+
}
64+
65+
@Nullable
66+
@Override
67+
public FieldValue getField(FieldPath path) {
68+
return data.get(path);
69+
}
70+
};
71+
}
5472

5573
/**
56-
* Memoized serialized form of the document for optimization purposes (avoids repeated
57-
* serialization). Might be null.
74+
* Creates a new Document from a Proto representation.
75+
*
76+
* <p>The Proto is only converted to an ObjectValue if the consumer calls `getData()`.
5877
*/
59-
private final com.google.firestore.v1.Document proto;
60-
61-
public @Nullable com.google.firestore.v1.Document getProto() {
62-
return proto;
78+
public static Document fromProto(
79+
RemoteSerializer serializer,
80+
com.google.firestore.v1.Document proto,
81+
DocumentState documentState) {
82+
DocumentKey key = serializer.decodeKey(proto.getName());
83+
SnapshotVersion version = serializer.decodeVersion(proto.getUpdateTime());
84+
85+
hardAssert(
86+
!version.equals(SnapshotVersion.NONE), "Found a document Proto with no snapshot version");
87+
88+
return new Document(key, version, documentState) {
89+
private ObjectValue memoizedData = null;
90+
91+
@Override
92+
public com.google.firestore.v1.Document getProto() {
93+
return proto;
94+
}
95+
96+
@Override
97+
public ObjectValue getData() {
98+
if (memoizedData != null) {
99+
return memoizedData;
100+
} else {
101+
memoizedData = serializer.decodeFields(proto.getFieldsMap());
102+
return memoizedData;
103+
}
104+
}
105+
106+
@Nullable
107+
@Override
108+
public FieldValue getField(FieldPath path) {
109+
if (memoizedData != null) {
110+
return memoizedData.get(path);
111+
} else {
112+
// Instead of deserializing the full Document proto, we only deserialize the value at
113+
// the requested field path. This speeds up Query execution as query filters can discard
114+
// documents based on a single field.
115+
Value value = proto.getFieldsMap().get(path.getFirstSegment());
116+
for (int i = 1; value != null && i < path.length(); ++i) {
117+
if (value.getValueTypeCase() != Value.ValueTypeCase.MAP_VALUE) {
118+
return null;
119+
}
120+
value = value.getMapValue().getFieldsMap().get(path.getSegment(i));
121+
}
122+
return value != null ? serializer.decodeValue(value) : null;
123+
}
124+
}
125+
};
63126
}
64127

65-
public Document(
66-
DocumentKey key, SnapshotVersion version, ObjectValue data, DocumentState documentState) {
67-
super(key, version);
68-
this.data = data;
69-
this.documentState = documentState;
70-
this.proto = null;
71-
}
128+
private final DocumentState documentState;
72129

73-
public Document(
74-
DocumentKey key,
75-
SnapshotVersion version,
76-
ObjectValue data,
77-
DocumentState documentState,
78-
com.google.firestore.v1.Document proto) {
130+
private Document(DocumentKey key, SnapshotVersion version, DocumentState documentState) {
79131
super(key, version);
80-
this.data = data;
81132
this.documentState = documentState;
82-
this.proto = proto;
83133
}
84134

85-
public ObjectValue getData() {
86-
return data;
87-
}
135+
/**
136+
* Memoized serialized form of the document for optimization purposes (avoids repeated
137+
* serialization). Might be null.
138+
*/
139+
public abstract @Nullable com.google.firestore.v1.Document getProto();
88140

89-
public @Nullable FieldValue getField(FieldPath path) {
90-
return data.get(path);
91-
}
141+
public abstract ObjectValue getData();
142+
143+
public abstract @Nullable FieldValue getField(FieldPath path);
92144

93145
public @Nullable Object getFieldValue(FieldPath path) {
94146
FieldValue value = getField(path);
@@ -113,7 +165,7 @@ public boolean equals(Object o) {
113165
if (this == o) {
114166
return true;
115167
}
116-
if (o == null || getClass() != o.getClass()) {
168+
if (!(o instanceof Document)) {
117169
return false;
118170
}
119171

@@ -122,13 +174,12 @@ public boolean equals(Object o) {
122174
return getVersion().equals(document.getVersion())
123175
&& getKey().equals(document.getKey())
124176
&& documentState.equals(document.documentState)
125-
&& data.equals(document.data);
177+
&& getData().equals(document.getData());
126178
}
127179

128180
@Override
129181
public int hashCode() {
130182
int result = getKey().hashCode();
131-
result = 31 * result + data.hashCode();
132183
result = 31 * result + getVersion().hashCode();
133184
result = 31 * result + documentState.hashCode();
134185
return result;
@@ -140,7 +191,7 @@ public String toString() {
140191
+ "key="
141192
+ getKey()
142193
+ ", data="
143-
+ data
194+
+ getData()
144195
+ ", version="
145196
+ getVersion()
146197
+ ", documentState="

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/PatchMutation.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ public MaybeDocument applyToRemoteDocument(
112112

113113
SnapshotVersion version = mutationResult.getVersion();
114114
ObjectValue newData = patchDocument(maybeDoc);
115-
return new Document(getKey(), version, newData, Document.DocumentState.COMMITTED_MUTATIONS);
115+
return Document.fromObjectValue(
116+
getKey(), version, newData, Document.DocumentState.COMMITTED_MUTATIONS);
116117
}
117118

118119
@Nullable
@@ -127,7 +128,8 @@ public MaybeDocument applyToLocalView(
127128

128129
SnapshotVersion version = getPostMutationVersion(maybeDoc);
129130
ObjectValue newData = patchDocument(maybeDoc);
130-
return new Document(getKey(), version, newData, Document.DocumentState.LOCAL_MUTATIONS);
131+
return Document.fromObjectValue(
132+
getKey(), version, newData, Document.DocumentState.LOCAL_MUTATIONS);
131133
}
132134

133135
@Nullable

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/SetMutation.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ public MaybeDocument applyToRemoteDocument(
7272
// accepted the mutation so the precondition must have held.
7373

7474
SnapshotVersion version = mutationResult.getVersion();
75-
return new Document(getKey(), version, value, Document.DocumentState.COMMITTED_MUTATIONS);
75+
return Document.fromObjectValue(
76+
getKey(), version, value, Document.DocumentState.COMMITTED_MUTATIONS);
7677
}
7778

7879
@Nullable
@@ -86,7 +87,8 @@ public MaybeDocument applyToLocalView(
8687
}
8788

8889
SnapshotVersion version = getPostMutationVersion(maybeDoc);
89-
return new Document(getKey(), version, value, Document.DocumentState.LOCAL_MUTATIONS);
90+
return Document.fromObjectValue(
91+
getKey(), version, value, Document.DocumentState.LOCAL_MUTATIONS);
9092
}
9193

9294
@Nullable

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/TransformMutation.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public MaybeDocument applyToRemoteDocument(
102102
List<FieldValue> transformResults =
103103
serverTransformResults(doc, mutationResult.getTransformResults());
104104
ObjectValue newData = transformObject(doc.getData(), transformResults);
105-
return new Document(
105+
return Document.fromObjectValue(
106106
getKey(), mutationResult.getVersion(), newData, Document.DocumentState.COMMITTED_MUTATIONS);
107107
}
108108

@@ -119,7 +119,7 @@ public MaybeDocument applyToLocalView(
119119
Document doc = requireDocument(maybeDoc);
120120
List<FieldValue> transformResults = localTransformResults(localWriteTime, baseDoc);
121121
ObjectValue newData = transformObject(doc.getData(), transformResults);
122-
return new Document(
122+
return Document.fromObjectValue(
123123
getKey(), doc.getVersion(), newData, Document.DocumentState.LOCAL_MUTATIONS);
124124
}
125125

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -405,12 +405,7 @@ private Document decodeFoundDocument(BatchGetDocumentsResponse response) {
405405
Assert.hardAssert(
406406
response.getResultCase().equals(ResultCase.FOUND),
407407
"Tried to deserialize a found document from a missing document.");
408-
DocumentKey key = decodeKey(response.getFound().getName());
409-
ObjectValue value = decodeFields(response.getFound().getFieldsMap());
410-
SnapshotVersion version = decodeVersion(response.getFound().getUpdateTime());
411-
hardAssert(
412-
!version.equals(SnapshotVersion.NONE), "Got a document response with no snapshot version");
413-
return new Document(key, version, value, Document.DocumentState.SYNCED, response.getFound());
408+
return Document.fromProto(this, response.getFound(), Document.DocumentState.SYNCED);
414409
}
415410

416411
private NoDocument decodeMissingDocument(BatchGetDocumentsResponse response) {
@@ -1041,24 +1036,16 @@ public WatchChange decodeWatchChange(ListenResponse protoChange) {
10411036
DocumentChange docChange = protoChange.getDocumentChange();
10421037
List<Integer> added = docChange.getTargetIdsList();
10431038
List<Integer> removed = docChange.getRemovedTargetIdsList();
1044-
DocumentKey key = decodeKey(docChange.getDocument().getName());
1045-
SnapshotVersion version = decodeVersion(docChange.getDocument().getUpdateTime());
1046-
hardAssert(
1047-
!version.equals(SnapshotVersion.NONE), "Got a document change without an update time");
1048-
ObjectValue data = decodeFields(docChange.getDocument().getFieldsMap());
1049-
// The document may soon be re-serialized back to protos in order to store it in local
1050-
// persistence. Memoize the encoded form to avoid encoding it again.
10511039
Document document =
1052-
new Document(
1053-
key, version, data, Document.DocumentState.SYNCED, docChange.getDocument());
1040+
Document.fromProto(this, docChange.getDocument(), Document.DocumentState.SYNCED);
10541041
watchChange = new WatchChange.DocumentChange(added, removed, document.getKey(), document);
10551042
break;
10561043
case DOCUMENT_DELETE:
10571044
DocumentDelete docDelete = protoChange.getDocumentDelete();
10581045
removed = docDelete.getRemovedTargetIdsList();
1059-
key = decodeKey(docDelete.getDocument());
1046+
DocumentKey key = decodeKey(docDelete.getDocument());
10601047
// Note that version might be unset in which case we use SnapshotVersion.NONE
1061-
version = decodeVersion(docDelete.getReadTime());
1048+
SnapshotVersion version = decodeVersion(docDelete.getReadTime());
10621049
NoDocument doc = new NoDocument(key, version, /*hasCommittedMutations=*/ false);
10631050
watchChange =
10641051
new WatchChange.DocumentChange(Collections.emptyList(), removed, doc.getKey(), doc);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,8 @@ public void testRemoveTargetsThenGC() {
553553
() -> {
554554
SnapshotVersion newVersion = version(3);
555555
Document doc =
556-
new Document(middleDocToUpdate, newVersion, testValue, Document.DocumentState.SYNCED);
556+
Document.fromObjectValue(
557+
middleDocToUpdate, newVersion, testValue, Document.DocumentState.SYNCED);
557558
documentCache.add(doc);
558559
updateTargetInTransaction(middleTarget);
559560
});

firebase-firestore/src/test/java/com/google/firebase/firestore/model/DocumentTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@
3636
public class DocumentTest {
3737

3838
@Test
39-
public void testConstructor() {
39+
public void testInstantiation() {
4040
Document document =
41-
new Document(
41+
Document.fromObjectValue(
4242
key("messages/first"), version(1), wrapObject("a", 1), Document.DocumentState.SYNCED);
4343

4444
assertEquals(key("messages/first"), document.getKey());
@@ -56,7 +56,8 @@ public void testExtractFields() {
5656
"owner",
5757
map("name", "Jonny", "title", "scallywag"));
5858
Document document =
59-
new Document(key("rooms/eros"), version(1), data, Document.DocumentState.SYNCED);
59+
Document.fromObjectValue(
60+
key("rooms/eros"), version(1), data, Document.DocumentState.SYNCED);
6061

6162
assertEquals("Discuss all the project related stuff", document.getFieldValue(field("desc")));
6263
assertEquals("scallywag", document.getFieldValue(field("owner.title")));

firebase-firestore/src/test/java/com/google/firebase/firestore/model/MutationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public void testAppliesLocalServerTimestampTransformsToDocuments() {
161161
new ServerTimestampValue(timestamp, StringValue.valueOf("bar-value")));
162162

163163
Document expectedDoc =
164-
new Document(
164+
Document.fromObjectValue(
165165
key("collection/key"),
166166
version(0),
167167
expectedData,

firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,22 +174,23 @@ public static SnapshotVersion version(long versionMicros) {
174174
}
175175

176176
public static Document doc(String key, long version, Map<String, Object> data) {
177-
return new Document(
177+
return Document.fromObjectValue(
178178
key(key), version(version), wrapObject(data), Document.DocumentState.SYNCED);
179179
}
180180

181181
public static Document doc(DocumentKey key, long version, Map<String, Object> data) {
182-
return new Document(key, version(version), wrapObject(data), Document.DocumentState.SYNCED);
182+
return Document.fromObjectValue(
183+
key, version(version), wrapObject(data), Document.DocumentState.SYNCED);
183184
}
184185

185186
public static Document doc(
186187
String key, long version, ObjectValue data, Document.DocumentState documentState) {
187-
return new Document(key(key), version(version), data, documentState);
188+
return Document.fromObjectValue(key(key), version(version), data, documentState);
188189
}
189190

190191
public static Document doc(
191192
String key, long version, Map<String, Object> data, Document.DocumentState documentState) {
192-
return new Document(key(key), version(version), wrapObject(data), documentState);
193+
return Document.fromObjectValue(key(key), version(version), wrapObject(data), documentState);
193194
}
194195

195196
public static NoDocument deletedDoc(String key, long version) {

0 commit comments

Comments
 (0)