Skip to content

Commit 9222c8e

Browse files
committed
use volatile instead of synchronized for better performance of buildProto()
1 parent e7c83bc commit 9222c8e

File tree

1 file changed

+89
-37
lines changed
  • firebase-firestore/src/main/java/com/google/firebase/firestore/model

1 file changed

+89
-37
lines changed

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

Lines changed: 89 additions & 37 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,19 +29,39 @@
2829

2930
/** A structured object value stored in Firestore. */
3031
public final class ObjectValue implements Cloneable {
31-
private final Object lock = new Object(); // Monitor object
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;
3248

3349
/**
3450
* The immutable Value proto for this object. Local mutations are stored in `overlayMap` and only
3551
* 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.
3655
*/
56+
@GuardedBy("lock")
3757
private Value partialValue;
3858

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

4667
public static ObjectValue fromMap(Map<String, Value> value) {
@@ -55,7 +76,7 @@ public ObjectValue(Value value) {
5576
hardAssert(
5677
!ServerTimestamps.isServerTimestamp(value),
5778
"ServerTimestamps should not be used as an ObjectValue");
58-
this.partialValue = value;
79+
this.mergedValue = value;
5980
}
6081

6182
public ObjectValue() {
@@ -105,7 +126,7 @@ private FieldMask extractFieldMask(MapValue value) {
105126
}
106127

107128
@Nullable
108-
private Value extractNestedValue(Value value, FieldPath fieldPath) {
129+
private static Value extractNestedValue(Value value, FieldPath fieldPath) {
109130
if (fieldPath.isEmpty()) {
110131
return value;
111132
} else {
@@ -123,19 +144,40 @@ private Value extractNestedValue(Value value, FieldPath fieldPath) {
123144
* Returns the Protobuf that backs this ObjectValue.
124145
*
125146
* <p>This method applies any outstanding modifications and memoizes the result. Further
126-
* 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.
127150
*/
128151
private Value buildProto() {
129-
synchronized (lock) {
130-
if (!overlayMap.isEmpty()) {
131-
MapValue mergedResult = applyOverlayLocked(FieldPath.EMPTY_PATH, overlayMap);
132-
if (mergedResult != null) {
133-
partialValue = Value.newBuilder().setMapValue(mergedResult).build();
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();
134176
}
135-
overlayMap.clear();
136177
}
137178
}
138-
return partialValue;
179+
180+
return value;
139181
}
140182

141183
/**
@@ -176,32 +218,41 @@ public void setAll(Map<FieldPath, Value> data) {
176218
*/
177219
private void setOverlay(FieldPath path, @Nullable Value value) {
178220
synchronized (lock) {
179-
Map<String, Object> currentLevel = overlayMap;
180-
181-
for (int i = 0; i < path.length() - 1; ++i) {
182-
String currentSegment = path.getSegment(i);
183-
Object currentValue = currentLevel.get(currentSegment);
184-
185-
if (currentValue instanceof Map) {
186-
// Re-use a previously created map
187-
currentLevel = (Map<String, Object>) currentValue;
188-
} else if (currentValue instanceof Value
189-
&& ((Value) currentValue).getValueTypeCase() == Value.ValueTypeCase.MAP_VALUE) {
190-
// Convert the existing Protobuf MapValue into a Java map
191-
Map<String, Object> nextLevel =
192-
new HashMap<>(((Value) currentValue).getMapValue().getFieldsMap());
193-
currentLevel.put(currentSegment, nextLevel);
194-
currentLevel = nextLevel;
195-
} else {
196-
// Create an empty hash map to represent the current nesting level
197-
Map<String, Object> nextLevel = new HashMap<>();
198-
currentLevel.put(currentSegment, nextLevel);
199-
currentLevel = nextLevel;
200-
}
221+
if (mergedValue != null) {
222+
partialValue = mergedValue;
223+
mergedValue = null;
201224
}
225+
setOverlay(overlayMap, path, value);
226+
}
227+
}
228+
229+
private static void setOverlay(
230+
Map<String, Object> overlayMap, FieldPath path, @Nullable Value value) {
231+
Map<String, Object> currentLevel = overlayMap;
202232

203-
currentLevel.put(path.getLastSegment(), value);
233+
for (int i = 0; i < path.length() - 1; ++i) {
234+
String currentSegment = path.getSegment(i);
235+
Object currentValue = currentLevel.get(currentSegment);
236+
237+
if (currentValue instanceof Map) {
238+
// Re-use a previously created map
239+
currentLevel = (Map<String, Object>) currentValue;
240+
} else if (currentValue instanceof Value
241+
&& ((Value) currentValue).getValueTypeCase() == Value.ValueTypeCase.MAP_VALUE) {
242+
// Convert the existing Protobuf MapValue into a Java map
243+
Map<String, Object> nextLevel =
244+
new HashMap<>(((Value) currentValue).getMapValue().getFieldsMap());
245+
currentLevel.put(currentSegment, nextLevel);
246+
currentLevel = nextLevel;
247+
} else {
248+
// Create an empty hash map to represent the current nesting level
249+
Map<String, Object> nextLevel = new HashMap<>();
250+
currentLevel.put(currentSegment, nextLevel);
251+
currentLevel = nextLevel;
252+
}
204253
}
254+
255+
currentLevel.put(path.getLastSegment(), value);
205256
}
206257

207258
/**
@@ -214,8 +265,8 @@ private void setOverlay(FieldPath path, @Nullable Value value) {
214265
* overlayMap}.
215266
* @return The merged data at `currentPath` or null if no modifications were applied.
216267
*/
217-
private @Nullable MapValue applyOverlayLocked(
218-
FieldPath currentPath, Map<String, Object> currentOverlays) {
268+
private static @Nullable MapValue applyOverlay(
269+
Value partialValue, FieldPath currentPath, Map<String, Object> currentOverlays) {
219270
boolean modified = false;
220271

221272
@Nullable Value existingValue = extractNestedValue(partialValue, currentPath);
@@ -233,7 +284,8 @@ private void setOverlay(FieldPath path, @Nullable Value value) {
233284
if (value instanceof Map) {
234285
@Nullable
235286
MapValue nested =
236-
applyOverlayLocked(currentPath.append(pathSegment), (Map<String, Object>) value);
287+
applyOverlay(
288+
partialValue, currentPath.append(pathSegment), (Map<String, Object>) value);
237289
if (nested != null) {
238290
resultAtPath.putFields(pathSegment, Value.newBuilder().setMapValue(nested).build());
239291
modified = true;

0 commit comments

Comments
 (0)