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 {
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