-
Notifications
You must be signed in to change notification settings - Fork 617
Performance: Don't deserialize full document Proto for Query execution #561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
48dcf7e
8d31c26
9f3e2e7
c76f65a
d60bb85
4f793f0
f559f72
a7de38f
22b2c25
f50e19a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,16 +14,20 @@ | |
|
||
package com.google.firebase.firestore.model; | ||
|
||
import static com.google.firebase.firestore.util.Assert.hardAssert; | ||
|
||
import com.google.firebase.firestore.model.value.FieldValue; | ||
import com.google.firebase.firestore.model.value.ObjectValue; | ||
import com.google.firebase.firestore.remote.RemoteSerializer; | ||
import com.google.firestore.v1.Value; | ||
import java.util.Comparator; | ||
import javax.annotation.Nullable; | ||
|
||
/** | ||
* Represents a document in Firestore with a key, version, data and whether the data has local | ||
* mutations applied to it. | ||
*/ | ||
public final class Document extends MaybeDocument { | ||
public abstract class Document extends MaybeDocument { | ||
|
||
/** Describes the `hasPendingWrites` state of a document. */ | ||
public enum DocumentState { | ||
|
@@ -36,59 +40,107 @@ public enum DocumentState { | |
} | ||
|
||
private static final Comparator<Document> KEY_COMPARATOR = | ||
new Comparator<Document>() { | ||
@Override | ||
public int compare(Document left, Document right) { | ||
return left.getKey().compareTo(right.getKey()); | ||
} | ||
}; | ||
(left, right) -> left.getKey().compareTo(right.getKey()); | ||
|
||
/** A document comparator that returns document by key and key only. */ | ||
public static Comparator<Document> keyComparator() { | ||
return KEY_COMPARATOR; | ||
} | ||
|
||
private final ObjectValue data; | ||
|
||
private final DocumentState documentState; | ||
/** Creates a new Document from an existing ObjectValue. */ | ||
public static Document fromObjectValue( | ||
DocumentKey key, SnapshotVersion version, ObjectValue data, DocumentState documentState) { | ||
return new Document(key, version, documentState) { | ||
@Nullable | ||
@Override | ||
public com.google.firestore.v1.Document getProto() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public ObjectValue getData() { | ||
return data; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public FieldValue getField(FieldPath path) { | ||
return data.get(path); | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* Memoized serialized form of the document for optimization purposes (avoids repeated | ||
* serialization). Might be null. | ||
* Creates a new Document from a Proto representation. | ||
* | ||
* <p>The Proto is only converted to an ObjectValue if the consumer calls `getData()`. | ||
*/ | ||
private final com.google.firestore.v1.Document proto; | ||
|
||
public @Nullable com.google.firestore.v1.Document getProto() { | ||
return proto; | ||
public static Document fromProto( | ||
wilhuff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
RemoteSerializer serializer, | ||
com.google.firestore.v1.Document proto, | ||
DocumentState documentState) { | ||
DocumentKey key = serializer.decodeKey(proto.getName()); | ||
SnapshotVersion version = serializer.decodeVersion(proto.getUpdateTime()); | ||
|
||
hardAssert( | ||
!version.equals(SnapshotVersion.NONE), "Found a document Proto with no snapshot version"); | ||
|
||
return new Document(key, version, documentState) { | ||
wilhuff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private ObjectValue memoizedData = null; | ||
|
||
@Override | ||
public com.google.firestore.v1.Document getProto() { | ||
return proto; | ||
} | ||
|
||
@Override | ||
public ObjectValue getData() { | ||
if (memoizedData != null) { | ||
return memoizedData; | ||
} else { | ||
memoizedData = serializer.decodeFields(proto.getFieldsMap()); | ||
return memoizedData; | ||
} | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public FieldValue getField(FieldPath path) { | ||
if (memoizedData != null) { | ||
return memoizedData.get(path); | ||
} else { | ||
// Instead of deserializing the full Document proto, we only deserialize the value at | ||
// the requested field path. This speeds up Query execution as query filters can discard | ||
// documents based on a single field. | ||
Value value = proto.getFieldsMap().get(path.getFirstSegment()); | ||
for (int i = 1; value != null && i < path.length(); ++i) { | ||
if (value.getValueTypeCase() != Value.ValueTypeCase.MAP_VALUE) { | ||
return null; | ||
} | ||
value = value.getMapValue().getFieldsMap().get(path.getSegment(i)); | ||
} | ||
return value != null ? serializer.decodeValue(value) : null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a cache - but this has to be a ConcurrentHashMap since this function can be called from user land. Please do shout if that is not actually an issue :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Users can call this, albeit indirectly. I don't really care if we cached these values for that purpose. I wish there were a way to avoid instantiating a new ConcurrentHashMap per Document. Were you able to see the effect of the cache in a query that had an orderby? If not, this probably isn't justified? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the impact of this caching will be hard to quantify. I thought about adding this initially, but was only won over by your comments earlier. We do re-run getField() excessively during the computation of our Views. I changed the code to lazy-init the field value cache. I do want to change the comparator to compare on the Protos directly, at which point we can remove the cache again. |
||
} | ||
} | ||
}; | ||
} | ||
|
||
public Document( | ||
DocumentKey key, SnapshotVersion version, ObjectValue data, DocumentState documentState) { | ||
super(key, version); | ||
this.data = data; | ||
this.documentState = documentState; | ||
this.proto = null; | ||
} | ||
private final DocumentState documentState; | ||
|
||
public Document( | ||
DocumentKey key, | ||
SnapshotVersion version, | ||
ObjectValue data, | ||
DocumentState documentState, | ||
com.google.firestore.v1.Document proto) { | ||
private Document(DocumentKey key, SnapshotVersion version, DocumentState documentState) { | ||
super(key, version); | ||
this.data = data; | ||
this.documentState = documentState; | ||
this.proto = proto; | ||
} | ||
|
||
public ObjectValue getData() { | ||
return data; | ||
} | ||
/** | ||
* Memoized serialized form of the document for optimization purposes (avoids repeated | ||
* serialization). Might be null. | ||
*/ | ||
public abstract @Nullable com.google.firestore.v1.Document getProto(); | ||
|
||
public @Nullable FieldValue getField(FieldPath path) { | ||
return data.get(path); | ||
} | ||
public abstract ObjectValue getData(); | ||
|
||
public abstract @Nullable FieldValue getField(FieldPath path); | ||
|
||
public @Nullable Object getFieldValue(FieldPath path) { | ||
FieldValue value = getField(path); | ||
|
@@ -113,7 +165,7 @@ public boolean equals(Object o) { | |
if (this == o) { | ||
return true; | ||
} | ||
if (o == null || getClass() != o.getClass()) { | ||
if (!(o instanceof Document)) { | ||
return false; | ||
} | ||
|
||
|
@@ -122,13 +174,12 @@ public boolean equals(Object o) { | |
return getVersion().equals(document.getVersion()) | ||
&& getKey().equals(document.getKey()) | ||
&& documentState.equals(document.documentState) | ||
&& data.equals(document.data); | ||
&& getData().equals(document.getData()); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
int result = getKey().hashCode(); | ||
result = 31 * result + data.hashCode(); | ||
result = 31 * result + getVersion().hashCode(); | ||
result = 31 * result + documentState.hashCode(); | ||
return result; | ||
|
@@ -140,7 +191,7 @@ public String toString() { | |
+ "key=" | ||
+ getKey() | ||
+ ", data=" | ||
+ data | ||
+ getData() | ||
+ ", version=" | ||
+ getVersion() | ||
+ ", documentState=" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,12 +45,13 @@ public void testEquals() { | |
assertNotEquals(base, differentData); | ||
assertNotEquals(base, fromCache); | ||
|
||
// Note: `base` and `differentData` have the same hash code since we no longer take document | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can't be right. We can't just ignore
However if we have a non- What do you think of implementing both equality and hashCode according to the principle above? I think that would give most (if not all) the benefit without compromising the contract of equals/hashCode. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hrm. This is a good comment, and I 100% agree with you, but I don't think I broke the contract here in the way that you stated. I removed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha! Sorry about misreading that. I didn't realize that this was about an expected collision. An alternative phrasing that might be less confusing is:
You might also want to call attention to this in an implementation comment in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea about the additional comment. I added it to |
||
// contents into account. | ||
assertEquals(base.hashCode(), baseDup.hashCode()); | ||
assertEquals(noData.hashCode(), noDataDup.hashCode()); | ||
assertNotEquals(base.hashCode(), noData.hashCode()); | ||
assertNotEquals(noData.hashCode(), base.hashCode()); | ||
assertNotEquals(base.hashCode(), differentPath.hashCode()); | ||
assertNotEquals(base.hashCode(), differentData.hashCode()); | ||
assertNotEquals(base.hashCode(), fromCache.hashCode()); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.