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 {
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 ;
32
48
33
49
/**
34
50
* The immutable Value proto for this object. Local mutations are stored in `overlayMap` and only
35
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.
36
55
*/
56
+ @ GuardedBy ("lock" )
37
57
private Value partialValue ;
38
58
39
59
/**
40
60
* A nested map that contains the accumulated changes that haven't yet been applied to {@link
41
61
* #partialValue}. Values can either be {@link Value} protos, {@code Map<String, Object>} values
42
62
* (to represent additional nesting) or {@code null} (to represent field deletes).
43
63
*/
64
+ @ GuardedBy ("lock" )
44
65
private final Map <String , Object > overlayMap = new HashMap <>();
45
66
46
67
public static ObjectValue fromMap (Map <String , Value > value ) {
@@ -55,7 +76,7 @@ public ObjectValue(Value value) {
55
76
hardAssert (
56
77
!ServerTimestamps .isServerTimestamp (value ),
57
78
"ServerTimestamps should not be used as an ObjectValue" );
58
- this .partialValue = value ;
79
+ this .mergedValue = value ;
59
80
}
60
81
61
82
public ObjectValue () {
@@ -105,7 +126,7 @@ private FieldMask extractFieldMask(MapValue value) {
105
126
}
106
127
107
128
@ Nullable
108
- private Value extractNestedValue (Value value , FieldPath fieldPath ) {
129
+ private static Value extractNestedValue (Value value , FieldPath fieldPath ) {
109
130
if (fieldPath .isEmpty ()) {
110
131
return value ;
111
132
} else {
@@ -123,19 +144,40 @@ private Value extractNestedValue(Value value, FieldPath fieldPath) {
123
144
* Returns the Protobuf that backs this ObjectValue.
124
145
*
125
146
* <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.
127
150
*/
128
151
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 ();
134
176
}
135
- overlayMap .clear ();
136
177
}
137
178
}
138
- return partialValue ;
179
+
180
+ return value ;
139
181
}
140
182
141
183
/**
@@ -176,32 +218,41 @@ public void setAll(Map<FieldPath, Value> data) {
176
218
*/
177
219
private void setOverlay (FieldPath path , @ Nullable Value value ) {
178
220
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 ;
201
224
}
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 ;
202
232
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
+ }
204
253
}
254
+
255
+ currentLevel .put (path .getLastSegment (), value );
205
256
}
206
257
207
258
/**
@@ -214,8 +265,8 @@ private void setOverlay(FieldPath path, @Nullable Value value) {
214
265
* overlayMap}.
215
266
* @return The merged data at `currentPath` or null if no modifications were applied.
216
267
*/
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 ) {
219
270
boolean modified = false ;
220
271
221
272
@ Nullable Value existingValue = extractNestedValue (partialValue , currentPath );
@@ -233,7 +284,8 @@ private void setOverlay(FieldPath path, @Nullable Value value) {
233
284
if (value instanceof Map ) {
234
285
@ Nullable
235
286
MapValue nested =
236
- applyOverlayLocked (currentPath .append (pathSegment ), (Map <String , Object >) value );
287
+ applyOverlay (
288
+ partialValue , currentPath .append (pathSegment ), (Map <String , Object >) value );
237
289
if (nested != null ) {
238
290
resultAtPath .putFields (pathSegment , Value .newBuilder ().setMapValue (nested ).build ());
239
291
modified = true ;
0 commit comments