1616
1717import static com .google .firebase .firestore .util .Assert .hardAssert ;
1818
19+ import androidx .annotation .GuardedBy ;
1920import androidx .annotation .NonNull ;
2021import androidx .annotation .Nullable ;
2122import com .google .firebase .firestore .model .mutation .FieldMask ;
2829
2930/** A structured object value stored in Firestore. */
3031public 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