diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index b11800c1e00..375c5d3d447 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -6,6 +6,8 @@ - [changed] Improve query performance in large result sets by replacing the deprecated AsyncTask thread pool with a self-managed thread pool. [#7376](//github.com/firebase/firebase-android-sdk/issues/7376) +- [changed] Improve query performance via internal memoization of calculated document data. + [#7370](//github.com/firebase/firebase-android-sdk/issues/7370) # 26.0.0 diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java index a7ea2997618..1620cf13da5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java @@ -16,6 +16,7 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.firestore.model.mutation.FieldMask; @@ -28,10 +29,31 @@ /** A structured object value stored in Firestore. */ public final class ObjectValue implements Cloneable { + + private final Object lock = new Object(); + + /** + * The immutable Value proto for this object with all overlays applied. + *
+ * The value for this member is calculated _lazily_ and is null if the overlays have not yet been + * applied. + *
+ * This member MAY be READ concurrently from multiple threads without acquiring any particular + * locks; however, UPDATING it MUST have the `lock` lock held. + *
+ * Internal Invariant: Exactly one of `mergedValue` and `partialValue` must be null, with the + * other being non-null. + */ + @Nullable private volatile Value mergedValue; + /** * The immutable Value proto for this object. Local mutations are stored in `overlayMap` and only * applied when {@link #buildProto()} is invoked. + *
+ * Internal Invariant: Exactly one of `mergedValue` and `partialValue` must be null, with the
+ * other being non-null.
*/
+ @GuardedBy("lock")
private Value partialValue;
/**
@@ -39,6 +61,7 @@ public final class ObjectValue implements Cloneable {
* #partialValue}. Values can either be {@link Value} protos, {@code Map This method applies any outstanding modifications and memoizes the result. Further
- * invocations are based on this memoized result.
+ * invocations are based on this memoized result until overlays are applied, at which point the
+ * memoized result is marked as "stale" and a new result is calculated and memoized upon the next
+ * invocation of this method.
*/
private Value buildProto() {
- synchronized (overlayMap) {
- MapValue mergedResult = applyOverlay(FieldPath.EMPTY_PATH, overlayMap);
- if (mergedResult != null) {
- partialValue = Value.newBuilder().setMapValue(mergedResult).build();
- overlayMap.clear();
+ // Use double-checked locking to avoid acquiring a lock in the cases where the memoized result
+ // has already been calculated (https://en.wikipedia.org/wiki/Double-checked_locking).
+ Value value = this.mergedValue;
+
+ if (value == null) {
+ synchronized (lock) {
+ value = mergedValue;
+ if (value == null) {
+ assert (partialValue != null);
+ if (overlayMap.isEmpty()) {
+ value = partialValue;
+ } else {
+ MapValue mergedResult = applyOverlay(partialValue, FieldPath.EMPTY_PATH, overlayMap);
+ if (mergedResult == null) {
+ value = partialValue;
+ } else {
+ value = Value.newBuilder().setMapValue(mergedResult).build();
+ }
+ }
+
+ assert (value != null);
+ mergedValue = value;
+ partialValue = null;
+ overlayMap.clear();
+ }
}
}
- return partialValue;
+
+ return value;
}
/**
@@ -171,6 +217,17 @@ public void setAll(Map