Skip to content

Commit ae770aa

Browse files
firestore: update ObjectValue.buildProto to return the memoized proto (#7370)
1 parent 6f769ab commit ae770aa

File tree

2 files changed

+72
-12
lines changed

2 files changed

+72
-12
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- [changed] Improve query performance in large result sets by replacing the deprecated AsyncTask
77
thread pool with a self-managed thread pool.
88
[#7376](//github.com/firebase/firebase-android-sdk/issues/7376)
9+
- [changed] Improve query performance via internal memoization of calculated document data.
10+
[#7370](//github.com/firebase/firebase-android-sdk/issues/7370)
911

1012
# 26.0.0
1113

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

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.firebase.firestore.util.Assert.hardAssert;
1818

19+
import androidx.annotation.GuardedBy;
1920
import androidx.annotation.NonNull;
2021
import androidx.annotation.Nullable;
2122
import com.google.firebase.firestore.model.mutation.FieldMask;
@@ -28,17 +29,39 @@
2829

2930
/** A structured object value stored in Firestore. */
3031
public final class ObjectValue implements Cloneable {
32+
33+
private final Object lock = new Object();
34+
35+
/**
36+
* The immutable Value proto for this object with all overlays applied.
37+
* <p>
38+
* The value for this member is calculated _lazily_ and is null if the overlays have not yet been
39+
* applied.
40+
* <p>
41+
* This member MAY be READ concurrently from multiple threads without acquiring any particular
42+
* locks; however, UPDATING it MUST have the `lock` lock held.
43+
* <p>
44+
* Internal Invariant: Exactly one of `mergedValue` and `partialValue` must be null, with the
45+
* other being non-null.
46+
*/
47+
@Nullable private volatile Value mergedValue;
48+
3149
/**
3250
* The immutable Value proto for this object. Local mutations are stored in `overlayMap` and only
3351
* applied when {@link #buildProto()} is invoked.
52+
* <p>
53+
* Internal Invariant: Exactly one of `mergedValue` and `partialValue` must be null, with the
54+
* other being non-null.
3455
*/
56+
@GuardedBy("lock")
3557
private Value partialValue;
3658

3759
/**
3860
* A nested map that contains the accumulated changes that haven't yet been applied to {@link
3961
* #partialValue}. Values can either be {@link Value} protos, {@code Map<String, Object>} values
4062
* (to represent additional nesting) or {@code null} (to represent field deletes).
4163
*/
64+
@GuardedBy("lock")
4265
private final Map<String, Object> overlayMap = new HashMap<>();
4366

4467
public static ObjectValue fromMap(Map<String, Value> value) {
@@ -53,7 +76,7 @@ public ObjectValue(Value value) {
5376
hardAssert(
5477
!ServerTimestamps.isServerTimestamp(value),
5578
"ServerTimestamps should not be used as an ObjectValue");
56-
this.partialValue = value;
79+
this.mergedValue = value;
5780
}
5881

5982
public ObjectValue() {
@@ -103,7 +126,7 @@ private FieldMask extractFieldMask(MapValue value) {
103126
}
104127

105128
@Nullable
106-
private Value extractNestedValue(Value value, FieldPath fieldPath) {
129+
private static Value extractNestedValue(Value value, FieldPath fieldPath) {
107130
if (fieldPath.isEmpty()) {
108131
return value;
109132
} else {
@@ -121,17 +144,40 @@ private Value extractNestedValue(Value value, FieldPath fieldPath) {
121144
* Returns the Protobuf that backs this ObjectValue.
122145
*
123146
* <p>This method applies any outstanding modifications and memoizes the result. Further
124-
* invocations are based on this memoized result.
147+
* invocations are based on this memoized result until overlays are applied, at which point the
148+
* memoized result is marked as "stale" and a new result is calculated and memoized upon the next
149+
* invocation of this method.
125150
*/
126151
private Value buildProto() {
127-
synchronized (overlayMap) {
128-
MapValue mergedResult = applyOverlay(FieldPath.EMPTY_PATH, overlayMap);
129-
if (mergedResult != null) {
130-
partialValue = Value.newBuilder().setMapValue(mergedResult).build();
131-
overlayMap.clear();
152+
// Use double-checked locking to avoid acquiring a lock in the cases where the memoized result
153+
// has already been calculated (https://en.wikipedia.org/wiki/Double-checked_locking).
154+
Value value = this.mergedValue;
155+
156+
if (value == null) {
157+
synchronized (lock) {
158+
value = mergedValue;
159+
if (value == null) {
160+
assert (partialValue != null);
161+
if (overlayMap.isEmpty()) {
162+
value = partialValue;
163+
} else {
164+
MapValue mergedResult = applyOverlay(partialValue, FieldPath.EMPTY_PATH, overlayMap);
165+
if (mergedResult == null) {
166+
value = partialValue;
167+
} else {
168+
value = Value.newBuilder().setMapValue(mergedResult).build();
169+
}
170+
}
171+
172+
assert (value != null);
173+
mergedValue = value;
174+
partialValue = null;
175+
overlayMap.clear();
176+
}
132177
}
133178
}
134-
return partialValue;
179+
180+
return value;
135181
}
136182

137183
/**
@@ -171,6 +217,17 @@ public void setAll(Map<FieldPath, Value> data) {
171217
* Adds {@code value} to the overlay map at {@code path}. Creates nested map entries if needed.
172218
*/
173219
private void setOverlay(FieldPath path, @Nullable Value value) {
220+
synchronized (lock) {
221+
if (mergedValue != null) {
222+
partialValue = mergedValue;
223+
mergedValue = null;
224+
}
225+
setOverlay(overlayMap, path, value);
226+
}
227+
}
228+
229+
private static void setOverlay(
230+
Map<String, Object> overlayMap, FieldPath path, @Nullable Value value) {
174231
Map<String, Object> currentLevel = overlayMap;
175232

176233
for (int i = 0; i < path.length() - 1; ++i) {
@@ -208,8 +265,8 @@ private void setOverlay(FieldPath path, @Nullable Value value) {
208265
* overlayMap}.
209266
* @return The merged data at `currentPath` or null if no modifications were applied.
210267
*/
211-
private @Nullable MapValue applyOverlay(
212-
FieldPath currentPath, Map<String, Object> currentOverlays) {
268+
private static @Nullable MapValue applyOverlay(
269+
Value partialValue, FieldPath currentPath, Map<String, Object> currentOverlays) {
213270
boolean modified = false;
214271

215272
@Nullable Value existingValue = extractNestedValue(partialValue, currentPath);
@@ -227,7 +284,8 @@ private void setOverlay(FieldPath path, @Nullable Value value) {
227284
if (value instanceof Map) {
228285
@Nullable
229286
MapValue nested =
230-
applyOverlay(currentPath.append(pathSegment), (Map<String, Object>) value);
287+
applyOverlay(
288+
partialValue, currentPath.append(pathSegment), (Map<String, Object>) value);
231289
if (nested != null) {
232290
resultAtPath.putFields(pathSegment, Value.newBuilder().setMapValue(nested).build());
233291
modified = true;

0 commit comments

Comments
 (0)