16
16
17
17
import static com .google .firebase .firestore .util .Assert .hardAssert ;
18
18
19
+ import androidx .annotation .GuardedBy ;
19
20
import androidx .annotation .NonNull ;
20
21
import androidx .annotation .Nullable ;
21
22
import com .google .firebase .firestore .model .mutation .FieldMask ;
28
29
29
30
/** A structured object value stored in Firestore. */
30
31
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
+
31
49
/**
32
50
* The immutable Value proto for this object. Local mutations are stored in `overlayMap` and only
33
51
* 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.
34
55
*/
56
+ @ GuardedBy ("lock" )
35
57
private Value partialValue ;
36
58
37
59
/**
38
60
* A nested map that contains the accumulated changes that haven't yet been applied to {@link
39
61
* #partialValue}. Values can either be {@link Value} protos, {@code Map<String, Object>} values
40
62
* (to represent additional nesting) or {@code null} (to represent field deletes).
41
63
*/
64
+ @ GuardedBy ("lock" )
42
65
private final Map <String , Object > overlayMap = new HashMap <>();
43
66
44
67
public static ObjectValue fromMap (Map <String , Value > value ) {
@@ -53,7 +76,7 @@ public ObjectValue(Value value) {
53
76
hardAssert (
54
77
!ServerTimestamps .isServerTimestamp (value ),
55
78
"ServerTimestamps should not be used as an ObjectValue" );
56
- this .partialValue = value ;
79
+ this .mergedValue = value ;
57
80
}
58
81
59
82
public ObjectValue () {
@@ -103,7 +126,7 @@ private FieldMask extractFieldMask(MapValue value) {
103
126
}
104
127
105
128
@ Nullable
106
- private Value extractNestedValue (Value value , FieldPath fieldPath ) {
129
+ private static Value extractNestedValue (Value value , FieldPath fieldPath ) {
107
130
if (fieldPath .isEmpty ()) {
108
131
return value ;
109
132
} else {
@@ -121,17 +144,40 @@ private Value extractNestedValue(Value value, FieldPath fieldPath) {
121
144
* Returns the Protobuf that backs this ObjectValue.
122
145
*
123
146
* <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.
125
150
*/
126
151
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
+ }
132
177
}
133
178
}
134
- return partialValue ;
179
+
180
+ return value ;
135
181
}
136
182
137
183
/**
@@ -171,6 +217,17 @@ public void setAll(Map<FieldPath, Value> data) {
171
217
* Adds {@code value} to the overlay map at {@code path}. Creates nested map entries if needed.
172
218
*/
173
219
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 ) {
174
231
Map <String , Object > currentLevel = overlayMap ;
175
232
176
233
for (int i = 0 ; i < path .length () - 1 ; ++i ) {
@@ -208,8 +265,8 @@ private void setOverlay(FieldPath path, @Nullable Value value) {
208
265
* overlayMap}.
209
266
* @return The merged data at `currentPath` or null if no modifications were applied.
210
267
*/
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 ) {
213
270
boolean modified = false ;
214
271
215
272
@ Nullable Value existingValue = extractNestedValue (partialValue , currentPath );
@@ -227,7 +284,8 @@ private void setOverlay(FieldPath path, @Nullable Value value) {
227
284
if (value instanceof Map ) {
228
285
@ Nullable
229
286
MapValue nested =
230
- applyOverlay (currentPath .append (pathSegment ), (Map <String , Object >) value );
287
+ applyOverlay (
288
+ partialValue , currentPath .append (pathSegment ), (Map <String , Object >) value );
231
289
if (nested != null ) {
232
290
resultAtPath .putFields (pathSegment , Value .newBuilder ().setMapValue (nested ).build ());
233
291
modified = true ;
0 commit comments