| /* |
| * Copyright 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| @file:OptIn(InternalComposeApi::class, ExperimentalComposeApi::class) |
| |
| package androidx.compose.runtime.snapshots |
| |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.ExperimentalComposeApi |
| import androidx.compose.runtime.InternalComposeApi |
| import androidx.compose.runtime.ThreadLocal |
| |
| /** |
| * Take a snapshot of the current value of all state objects. The values are preserved until |
| * [Snapshot.dispose] is called on the result. |
| * |
| * The [readObserver] parameter can be used to track when all state objects are read when in |
| * [Snapshot.enter]. A [SnapshotApplyObserver] can be registered using |
| * [Snapshot.registerApplyObserver] to observe modification of state objects. |
| * |
| * An active snapshot (after it is created but before [Snapshot.dispose] is called) requires |
| * resources to track the values in the snapshot. Once a snapshot is no longer needed it should |
| * disposed by calling [Snapshot.dispose]. |
| * |
| * Leaving a snapshot active could cause hard to diagnose memory leaks values as are maintained |
| * by state objects for these unneeded snapshots. Take care to always call [Snapshot.dispose] on all |
| * snapshots when they are no longer needed. |
| * |
| * Composition uses both of these to implicitly subscribe to changes to state object and |
| * automatically update the composition when state objects read during composition change. |
| * |
| * A nested snapshot can be taken of a snapshot which is an independent read-only copy of the |
| * snapshot and can be disposed independently. This is used by [takeSnapshot] when in a read-only |
| * snapshot for API consistency allowing the result of [takeSnapshot] to be disposed leaving the |
| * parent snapshot active. |
| * |
| * @param readObserver called when any state object is read in the lambda passed to |
| * [Snapshot.enter] or in the [Snapshot.enter] of any nested snapshot. |
| * |
| * @see Snapshot |
| * @see Snapshot.registerApplyObserver |
| * @see SnapshotApplyObserver |
| */ |
| fun takeSnapshot( |
| readObserver: SnapshotReadObserver? = null |
| ): Snapshot = currentSnapshot().takeNestedSnapshot(readObserver) |
| |
| /** |
| * Take a snapshot of the current value of all state objects that also allows the state to be |
| * changed and later atomically applied when [MutableSnapshot.apply] is called. The values are |
| * preserved until [Snapshot.dispose] is called on the result. The global state will either see |
| * all the changes made as one atomic change, when [MutableSnapshot.apply] is called, or none of |
| * the changes if the mutable state object is disposed before being applied. |
| * |
| * The values in a snapshot can be modified by calling [Snapshot.enter] and then, in its lambda, |
| * modify any state object. The new values of the state objects will only become visible to the |
| * global state when [MutableSnapshot.apply] is called. |
| * |
| * An active snapshot (after it is created but before [Snapshot.dispose] is called) requires |
| * resources to track the values in the snapshot. Once a snapshot is no longer needed it should |
| * disposed by calling [Snapshot.dispose]. |
| * |
| * Leaving a snapshot active could cause hard to diagnose memory leaks as values are maintained |
| * by state objects for these unneeded snapshots. Take care to always call [Snapshot.dispose] on all |
| * snapshots when they are no longer needed. |
| * |
| * A nested snapshot can be taken by calling [Snapshot.takeNestedSnapshot], for a read-only |
| * snapshot, or [MutableSnapshot.takeNestedMutableSnapshot] for a snapshot that can be changed. |
| * Nested mutable snapshots are applied to the this, the parent snapshot, when their |
| * [MutableSnapshot.apply] is called. Their applied changes will be visible to in this snapshot |
| * but will not be visible other snapshots (including other nested snapshots) or the global state |
| * until this snapshot is applied by calling [MutableSnapshot.apply]. |
| * |
| * Once [MutableSnapshot.apply] is called on this, the parent snapshot, all calls to |
| * [MutableSnapshot.apply] on an active nested snapshot will fail. |
| * |
| * Changes to a mutable snapshot are isolated, using snapshot isolation, from all other snapshots. |
| * Their changes are only visible as global state or to new snapshots once [MutableSnapshot.apply] |
| * is called. |
| * |
| * Applying a snapshot can fail if currently visible changes to the state object conflicts with a |
| * change made in the snapshot. |
| * |
| * When in a mutable snapshot, [takeMutableSnapshot] creates a nested snapshot of the current mutable |
| * snapshot. If the current snapshot is read-only, an exception is thrown. The current snapshot |
| * is the result of calling [currentSnapshot] which is updated by calling [Snapshot.enter] which |
| * makes the [Snapshot] the current snapshot while in its lambda. |
| * |
| * Composition uses mutable snapshots to allow changes made in a [Composable] functions to be |
| * temporarily isolated from the global state and is later applied to the global state when the |
| * composition is applied. If [MutableSnapshot.apply] fails applying this snapshot, the snapshot |
| * and the changes calculated during composition are disposed and a new composition is scheduled |
| * to be calculated again. |
| * |
| * @param readObserver called when any state object is read in the lambda passed to |
| * [Snapshot.enter] or in the [Snapshot.enter] of any nested snapshots. |
| * |
| * Composition, layout and draw use [readObserver] to implicitly subscribe to changes to state |
| * objects to know when to update. |
| * |
| * @param writeObserver called when a state object is created or just before it is written to the |
| * first time in the snapshot or a nested mutable snapshot. This might be called several times |
| * for the same object if nested mutable snapshots are created. |
| * |
| * Composition uses [writeObserver] to track when a state object is modified during composition |
| * in order to invalidate the reads that have not yet occurred. This allows a single pass of |
| * composition for state objects that are written to before they are read (such as modifying the |
| * value of a dynamic ambient provider). |
| * |
| * @see takeSnapshot |
| * @see Snapshot |
| * @see MutableSnapshot |
| */ |
| @ExperimentalComposeApi |
| fun takeMutableSnapshot( |
| readObserver: SnapshotReadObserver? = null, |
| writeObserver: SnapshotWriteObserver? = null |
| ): MutableSnapshot = |
| (currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot(readObserver, writeObserver) |
| ?: error("Cannot create a mutable snapshot of an read-only snapshot") |
| |
| /** |
| * A snapshot of the values return by mutable states and other state objects. All state object |
| * will have the same value in the snapshot as they had when the snapshot was created unless they |
| * are explicitly changed in the snapshot. |
| * |
| * To enter a snapshot call [enter]. The snapshot is the current snapshot as returned by |
| * [currentSnapshot] until the control returns from the lambda (or until a nested [enter] is |
| * called). All state objects will return the values associated with this snapshot, locally in the |
| * thread, until [enter] returns. All other threads are unaffected. |
| * |
| * Snapshots can be nested by calling [takeNestedSnapshot]. |
| * |
| * @see takeSnapshot |
| * @see takeMutableSnapshot |
| * @see androidx.compose.runtime.mutableStateOf |
| * @see androidx.compose.runtime.mutableStateListOf |
| * @see androidx.compose.runtime.mutableStateMapOf |
| */ |
| @ExperimentalComposeApi |
| sealed class Snapshot( |
| id: Int, |
| invalid: SnapshotIdSet |
| ) { |
| /** |
| * The snapshot id of the snapshot. This is a unique number from a monotonically increasing |
| * value for each snapshot taken. |
| */ |
| open var id: Int = id |
| internal set |
| |
| /** |
| * The root snapshot for this snapshot. For non-nested snapshots this is always `this`. For |
| * nested snapshot it is the parent's [root]. |
| */ |
| abstract val root: Snapshot |
| |
| /** |
| * True if any change to a state object in this snapshot will throw. |
| */ |
| abstract val readonly: Boolean |
| |
| /** |
| * Dispose the snapshot. Neglecting to dispose a snapshot will result in difficult to |
| * diagnose memory leaks as it indirectly causes all state objects to maintain its value for |
| * the un-disposed snapshot. |
| */ |
| open fun dispose() { |
| disposed = true |
| } |
| |
| /** |
| * Take a snapshot of the state values in this snapshot. The resulting [Snapshot] is read-only. |
| * All nested snapshots need to be disposed by calling [dispose] before resources associated |
| * with this snapshot can be collected. Nested snapshots are still valid after the parent has |
| * been disposed. |
| */ |
| abstract fun takeNestedSnapshot(readObserver: SnapshotReadObserver? = null): Snapshot |
| |
| /** |
| * Whether there are any pending changes in this snapshot. These changes are not visible |
| * until the snapshot is applied. |
| */ |
| abstract fun hasPendingChanges(): Boolean |
| |
| /** |
| * Enter the snapshot. In [block] all state objects have the value associated with this |
| * snapshot. The value of [currentSnapshot] will be this snapshot until this [block] returns |
| * or a nested call to [enter] is called. When [block] returns, the previous current snapshot |
| * is restored if there was one. |
| * |
| * All changes to state object inside [block] are isolated to this snapshot and are not |
| * visible to other snapshot or as global state. If this is a [readonly] snapshot, any |
| * changes to state objects will throw an [IllegalStateException]. |
| * |
| * For a [MutableSnapshot], changes made to a snapshot inside [block] can be applied |
| * atomically to the global state (or to its parent snapshot if it is a nested snapshot) by |
| * calling [MutableSnapshot.apply]. |
| * |
| * @see androidx.compose.runtime.mutableStateOf |
| * @see androidx.compose.runtime.mutableStateListOf |
| * @see androidx.compose.runtime.mutableStateMapOf |
| */ |
| inline fun <T> enter(block: () -> T): T { |
| val previous = makeCurrent() |
| try { |
| return block() |
| } finally { |
| restoreCurrent(previous) |
| } |
| } |
| |
| @PublishedApi |
| internal open fun makeCurrent(): Snapshot? { |
| val previous = threadSnapshot.get() |
| threadSnapshot.set(this) |
| return previous |
| } |
| |
| @PublishedApi |
| internal open fun restoreCurrent(snapshot: Snapshot?) { |
| threadSnapshot.set(snapshot) |
| } |
| |
| internal var disposed = false |
| |
| /* |
| * The read observer for the snapshot if there is one. |
| */ |
| internal abstract val readObserver: SnapshotReadObserver? |
| |
| /** |
| * The write observer for the snapshot if there is one. |
| */ |
| internal abstract val writeObserver: SnapshotWriteObserver? |
| |
| /** |
| * A set of all the snapshots that should be treated as invalid. |
| */ |
| internal open var invalid: SnapshotIdSet = invalid |
| internal set |
| |
| /** |
| * Called when a nested snapshot of this snapshot is activated |
| */ |
| internal abstract fun nestedActivated(snapshot: Snapshot) |
| |
| /** |
| * Called when a nested snapshot of this snapshot is deactivated |
| */ |
| internal abstract fun nestedDeactivated(snapshot: Snapshot) |
| |
| /** |
| * Record that state was modified in the snapshot. |
| */ |
| internal abstract fun recordModified(state: StateObject) |
| |
| /** |
| * The set of state objects that have been modified in this snapshot. |
| */ |
| internal abstract val modified: MutableSet<StateObject>? |
| |
| /** |
| * Notify the snapshot that all objects created in this snapshot to this point should be |
| * considered initialized. If any state object is are modified passed this point it will |
| * appear as modified in the snapshot and any applicable [SnapshotWriteObserver] will be |
| * called for the object and the object will be part of the a set of mutated objects sent to |
| * any applicable [SnapshotApplyObserver]. |
| * |
| * Unless [notifyObjectsInitialized] is called, state objects created in a snapshot are not |
| * considered modified by the snapshot even if they are modified after construction. |
| */ |
| internal abstract fun notifyObjectsInitialized() |
| |
| /** |
| * Closes the snapshot by removing the snapshot id (an any previous id's) from the list of |
| * open snapshots. |
| */ |
| internal open fun close() { |
| sync { |
| openSnapshots = openSnapshots.clear(id) |
| } |
| } |
| |
| internal fun validateNotDisposed() { |
| require(!disposed) { "Cannot use a disposed snapshot" } |
| } |
| |
| companion object { |
| /** |
| * Return the thread's active snapshot. If no thread snapshot is active then the current |
| * global snapshot is used. |
| */ |
| val current get() = currentSnapshot() |
| |
| /** |
| * Escape the current snapshot, if there is one. All state objects will have the value |
| * associated with the global while the [block] lambda is executing. |
| * |
| * @return the result of [block] |
| */ |
| inline fun <T> global(block: () -> T): T { |
| val previous = removeCurrent() |
| return block().also { restoreCurrent(previous) } |
| } |
| |
| /** |
| * Observe reads and or write of state objects in the current thread. |
| * |
| * This only affects the current snapshot (if any) and any new snapshots create from |
| * [takeSnapshot] and [takeMutableSnapshot]. It will not affect any snapshots previous |
| * created even if [Snapshot.enter] is called in [block]. |
| * |
| * @param readObserver called when any state object is read. |
| * @param writeObserver called when a state object is created or just before it is |
| * written to the first time in the snapshot or a nested mutable snapshot. This might be |
| * called several times for the same object if nested mutable snapshots are created. |
| */ |
| fun <T> observe( |
| readObserver: SnapshotReadObserver? = null, |
| writeObserver: SnapshotWriteObserver? = null, |
| block: () -> T |
| ): T { |
| if (readObserver != null || writeObserver != null) { |
| val currentSnapshot = threadSnapshot.get() |
| val snapshot = |
| if (currentSnapshot == null || currentSnapshot is MutableSnapshot) |
| TransparentObserverMutableSnapshot( |
| currentSnapshot as? MutableSnapshot, |
| readObserver, |
| writeObserver |
| ) |
| else if (readObserver == null) return block() |
| else currentSnapshot.takeNestedSnapshot(readObserver) |
| try { |
| return snapshot.enter(block) |
| } finally { |
| snapshot.dispose() |
| } |
| } else return block() |
| } |
| |
| /** |
| * Register an apply listener that is called back when snapshots are applied to the |
| * global state. |
| * |
| * @return a lambda that, when called, unregisters [observer]. |
| */ |
| fun registerApplyObserver(observer: SnapshotApplyObserver): () -> Unit { |
| // Ensure observer does not see changes before this call. |
| advanceGlobalSnapshot(emptyLambda) |
| |
| sync { |
| applyObservers.add(observer) |
| } |
| return { |
| sync { |
| applyObservers.remove(observer) |
| } |
| } |
| } |
| |
| /** |
| * Register an observer of the first write to the global state of a global state object |
| * since the last call to [sendApplyNotifications]. |
| * |
| * Composition uses this to schedule a new composition whenever a state object that |
| * was read in composition is modified. |
| * |
| * State objects can be sent to the apply observer that have not been sent to global write |
| * observers. This happens for state objects inside [MutableSnapshot] that is later |
| * applied by calling [MutableSnapshot.apply]. |
| * |
| * This should only be used to determine if a call to [sendApplyNotifications] should be |
| * scheduled to be called. |
| * |
| * @return a lambda that, when called, unregisters [observer]. |
| */ |
| @ExperimentalComposeApi |
| fun registerGlobalWriteObserver(observer: SnapshotWriteObserver): () -> Unit { |
| sync { |
| globalWriteObservers.add(observer) |
| } |
| advanceGlobalSnapshot() |
| return { |
| sync { |
| globalWriteObservers.remove(observer) |
| } |
| advanceGlobalSnapshot() |
| } |
| } |
| |
| /** |
| * Notify the snapshot that all objects created in this snapshot to this point should be |
| * considered initialized. If any state object is are modified passed this point it will |
| * appear as modified in the snapshot and any applicable [SnapshotWriteObserver] will be |
| * called for the object and the object will be part of the a set of mutated objects sent to |
| * any applicable [SnapshotApplyObserver]. |
| * |
| * Unless [notifyObjectsInitialized] is called, state objects created in a snapshot are not |
| * considered modified by the snapshot even if they are modified after construction. |
| * |
| * Compose uses this between phases of composition to allow observing changes to state |
| * objects create in a previous phase. |
| */ |
| @ExperimentalComposeApi |
| fun notifyObjectsInitialized() = currentSnapshot().notifyObjectsInitialized() |
| |
| /** |
| * Send any pending apply notifications for state objects changed outside a snapshot. |
| * |
| * Apply notifications for state objects modified outside snapshot are deferred until method |
| * is called. This method is implicitly called whenever a non-nested [MutableSnapshot] |
| * is applied making its changes visible to all new, non-nested snapshots. |
| * |
| * Composition schedules this to be called after changes to state objects are |
| * detected an observer registered with [registerGlobalWriteObserver]. |
| */ |
| @ExperimentalComposeApi |
| fun sendApplyNotifications() { |
| val changes = sync { |
| currentGlobalSnapshot.modified?.isNotEmpty() == true |
| } |
| if (changes) |
| advanceGlobalSnapshot() |
| } |
| |
| @InternalComposeApi |
| fun openSnapshotCount() = openSnapshots.toList().size |
| |
| @PublishedApi |
| internal fun removeCurrent(): Snapshot? { |
| val previous = threadSnapshot.get() |
| if (previous != null) threadSnapshot.set(null) |
| return previous |
| } |
| |
| @PublishedApi |
| internal fun restoreCurrent(previous: Snapshot?) { |
| if (previous != null) threadSnapshot.set(previous) |
| } |
| } |
| } |
| |
| /** |
| * A snapshot of the values return by mutable states and other state objects. All state object |
| * will have the same value in the snapshot as they had when the snapshot was created unless they |
| * are explicitly changed in the snapshot. |
| |
| * To enter a snapshot call [enter]. The snapshot is the current snapshot as returned by |
| * [currentSnapshot] until the control returns from the lambda (or until a nested [enter] is |
| * called. All state objects will return the values associated with this snapshot, locally in the |
| * thread, until [enter] returns. All other threads are unaffected. |
| * |
| * All changes made in a [MutableSnapshot] are snapshot isolated from all other snapshots and |
| * their changes can only be seen globally, or by new shots, after [MutableSnapshot.apply] as been |
| * called. |
| * |
| * Snapshots can be nested by calling [takeNestedSnapshot] or |
| * [MutableSnapshot.takeNestedMutableSnapshot]. |
| * |
| * @see takeMutableSnapshot |
| * @see androidx.compose.runtime.mutableStateOf |
| * @see androidx.compose.runtime.mutableStateListOf |
| * @see androidx.compose.runtime.mutableStateMapOf |
| */ |
| @ExperimentalComposeApi |
| open class MutableSnapshot internal constructor( |
| id: Int, |
| invalid: SnapshotIdSet, |
| override val readObserver: SnapshotReadObserver?, |
| override val writeObserver: SnapshotWriteObserver? |
| ) : Snapshot(id, invalid) { |
| /** |
| * Whether there are any pending changes in this snapshot. These changes are not visible |
| * until the snapshot is applied. |
| */ |
| override fun hasPendingChanges(): Boolean = modified?.isNotEmpty() == true |
| |
| /** |
| * Take a mutable snapshot of the state values in this snapshot. Entering this snapshot by |
| * calling [enter] allows state objects to be modified that are not visible to the this, the |
| * parent snapshot, until the [apply] is called. |
| * |
| * Applying a nested snapshot, by calling [apply], applies its change to, this, the parent |
| * snapshot. For a change to be visible globally, all the parent snapshots need to be applied |
| * until the root snapshot is applied to the global state. |
| * |
| * All nested snapshots need to be disposed by calling [dispose] before resources associated |
| * with this snapshot can be collected. Nested active snapshots are still valid after the parent |
| * has been disposed but calling [apply] will fail. |
| */ |
| open fun takeNestedMutableSnapshot( |
| readObserver: SnapshotReadObserver? = null, |
| writeObserver: SnapshotWriteObserver? = null |
| ): MutableSnapshot { |
| validateNotDisposed() |
| validateNotApplied() |
| return advance { |
| sync { |
| val newId = nextSnapshotId++ |
| openSnapshots = openSnapshots.set(newId) |
| val invalid = invalid |
| this.invalid = invalid.set(newId) |
| NestedMutableSnapshot( |
| newId, |
| invalid, |
| mergedReadObserver(readObserver, this.readObserver), |
| mergedWriteObserver(writeObserver, this.writeObserver), |
| this |
| ) |
| } |
| } |
| } |
| |
| /** |
| * Apply the changes made to state objects in this snapshot to the global state, or to the |
| * parent snapshot if this is a nested mutable snapshot. |
| * |
| * Once this method returns all changes made to this snapshot are atomically visible as the |
| * global state of the state object or to the parent snapshot. |
| * |
| * While a snapshot is active (after it is created but before [apply] or [dispose] is called) |
| * requires resources to track the values in the snapshot. Once a snapshot is no longer |
| * needed it should be either applied by calling [apply] or disposed by calling [dispose]. A |
| * snapshot that has been had is [apply] called can also have [dispose] called on it. However, |
| * calling [apply] after calling [dispose] will throw an exception. |
| * |
| * Leaving a snapshot active could cause hard to diagnose memory leaks values are maintained |
| * by state objects for unneeded snapshots. Take care to always call [dispose] on any snapshot. |
| */ |
| open fun apply(): SnapshotApplyResult { |
| // NOTE: the this algorithm is currently does not guarantee serializable snapshots as it |
| // doesn't prevent crossing writes as described here https://arxiv.org/pdf/1412.2324.pdf |
| |
| // Just removing the snapshot from the active snapshot set is enough to make it part of the |
| // next snapshot, however, this should only be done after first determining that there are no |
| // colliding writes are being applied. |
| |
| // A write is considered colliding if any write occurred in a state object in a snapshot |
| // applied since the snapshot was taken. |
| val modified = modified |
| val optimisticMerges = if (modified != null) optimisticMerges( |
| currentGlobalSnapshot, |
| this, |
| openSnapshots.clear(currentGlobalSnapshot.id) |
| ) else null |
| val (observers, globalModified) = sync { |
| validateOpen(this) |
| if (modified == null || modified.size == 0) { |
| close() |
| val previousGlobalSnapshot = currentGlobalSnapshot |
| takeNewGlobalSnapshot(previousGlobalSnapshot, emptyLambda) |
| val globalModified = previousGlobalSnapshot.modified |
| if (globalModified != null && globalModified.isNotEmpty()) |
| applyObservers.toList() to globalModified |
| else |
| emptyList<SnapshotApplyObserver>() to null |
| } else { |
| val previousGlobalSnapshot = currentGlobalSnapshot |
| val result = innerApply( |
| nextSnapshotId, |
| optimisticMerges, |
| openSnapshots.clear(previousGlobalSnapshot.id) |
| ) |
| if (result != SnapshotApplyResult.Success) return result |
| |
| // Close this snapshot |
| close() |
| |
| // Take a new global snapshot that includes this one. |
| takeNewGlobalSnapshot(previousGlobalSnapshot, emptyLambda) |
| val globalModified = previousGlobalSnapshot.modified |
| this.modified = null |
| previousGlobalSnapshot.modified = null |
| |
| applyObservers.toList() to globalModified |
| } |
| } |
| |
| // Mark as applied |
| applied = true |
| |
| // Notify any apply observers that changes applied were seen |
| if (globalModified != null && globalModified.isNotEmpty()) { |
| observers.fastForEach { |
| it(globalModified, this) |
| } |
| } |
| |
| if (modified != null && modified.isNotEmpty()) { |
| observers.fastForEach { |
| it(modified, this) |
| } |
| } |
| |
| return SnapshotApplyResult.Success |
| } |
| |
| override val readonly: Boolean get() = false |
| |
| override val root: Snapshot get() = this |
| |
| override fun dispose() { |
| if (!disposed) { |
| super.dispose() |
| nestedDeactivated(this) |
| } |
| } |
| |
| override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?): Snapshot { |
| validateNotDisposed() |
| validateNotApplied() |
| return advance { |
| sync { |
| val readonlyId = nextSnapshotId++ |
| openSnapshots = openSnapshots.set(readonlyId) |
| NestedReadonlySnapshot( |
| readonlyId, |
| invalid, |
| readObserver, |
| this |
| ) |
| } |
| } |
| } |
| |
| override fun nestedActivated(snapshot: Snapshot) { snapshots++ } |
| |
| override fun nestedDeactivated(snapshot: Snapshot) { |
| require(snapshots > 0) |
| if (--snapshots == 0) { |
| if (!applied) { |
| abandon() |
| } |
| } |
| } |
| |
| override fun notifyObjectsInitialized() { |
| advance() |
| } |
| |
| override fun close() { |
| sync { |
| // Remove itself and previous ids from the open set. |
| openSnapshots = openSnapshots.clear(id).andNot(previousIds) |
| } |
| } |
| |
| internal fun validateNotApplied() { |
| require(!applied) { |
| "Unsupported operation on a snapshot that has been applied" |
| } |
| } |
| |
| /** |
| * Abandon the snapshot. |
| */ |
| internal fun abandon() { |
| val modified = modified |
| if (modified != null) { |
| validateNotApplied() |
| |
| // Mark all state records created in this snapshot as invalid. This allows the snapshot |
| // id to be forgotten as no state records will refer to it. |
| this.modified = null |
| val id = id |
| for (state in modified) { |
| var current: StateRecord? = state.firstStateRecord |
| while (current != null) { |
| if (current.snapshotId == id || current.snapshotId in previousIds) { |
| current.snapshotId = INVALID_SNAPSHOT |
| } |
| current = current.next |
| } |
| } |
| } |
| |
| // The snapshot can now be closed. |
| close() |
| } |
| |
| internal fun innerApply( |
| snapshotId: Int, |
| optimisticMerges: Map<StateRecord, StateRecord>?, |
| invalidSnapshots: SnapshotIdSet |
| ): SnapshotApplyResult { |
| // This must be called in a synchronized block |
| |
| // If there are modifications we need to ensure none of the modifications have |
| // collisions. |
| |
| // A record is guaranteed not collide if no other write was performed to the record |
| // by an applied snapshot since this snapshot was taken. No writes to a state object |
| // occurred if, ignoring this snapshot, the readable records for the snapshots are |
| // the same. If they are different then there is a potential collision and the state |
| // object is asked if it can resolve the collision. If it can the updated state record |
| // is for the apply. |
| var mergedRecords: MutableList<Pair<StateObject, StateRecord>>? = null |
| val start = this.invalid.set(id).or(this.previousIds) |
| val modified = modified!! |
| var statesToRemove: MutableList<StateObject>? = null |
| for (state in modified) { |
| val first = state.firstStateRecord |
| // If either current or previous cannot be calculated the object was created |
| // in a nested snapshot that was committed then changed. |
| val current = readable(first, snapshotId, invalidSnapshots) ?: continue |
| val previous = readable(first, id, start) ?: continue |
| if (current != previous) { |
| val applied = readable(first, id, this.invalid) ?: readError() |
| val merged = optimisticMerges?.get(current) ?: run { |
| state.mergeRecords(previous, current, applied) |
| } |
| when (merged) { |
| null -> return SnapshotApplyResult.Failure(this) |
| applied -> { |
| // Nothing to do the merge policy says that the current changes |
| // obscure the current value so ignore the conflict |
| } |
| current -> { |
| (mergedRecords ?: mutableListOf<Pair<StateObject, StateRecord>>().also { |
| mergedRecords = it |
| }).add(state to current.create()) |
| |
| // If we revert to current then the state is no longer modified. |
| (statesToRemove ?: mutableListOf<StateObject>().also { |
| statesToRemove = it |
| }).add(state) |
| } |
| else -> { |
| (mergedRecords ?: mutableListOf<Pair<StateObject, StateRecord>>().also { |
| mergedRecords = it |
| }).add( |
| if (merged != previous) state to merged |
| else state to previous.create() |
| ) |
| } |
| } |
| } |
| } |
| |
| mergedRecords?.let { |
| // Ensure we have a new snapshot id |
| advance() |
| |
| // Update all the merged records to have the new id. |
| it.fastForEach { merged -> |
| val (state, stateRecord) = merged |
| stateRecord.snapshotId = id |
| sync { |
| stateRecord.next = state.firstStateRecord |
| state.prependStateRecord(stateRecord) |
| } |
| } |
| } |
| |
| statesToRemove?.let { |
| // Remove from modified any state objects that have reverted to the parent value. |
| modified.removeAll(it) |
| } |
| |
| return SnapshotApplyResult.Success |
| } |
| |
| internal inline fun <T> advance(block: () -> T): T { |
| recordPrevious(id) |
| return block().also { |
| sync { |
| id = nextSnapshotId++ |
| openSnapshots = openSnapshots.set(id) |
| } |
| } |
| } |
| |
| internal fun advance(): Unit = advance { } |
| |
| internal fun recordPrevious(id: Int) { |
| sync { |
| previousIds = previousIds.set(id) |
| } |
| } |
| |
| internal fun recordPreviousList(snapshots: SnapshotIdSet) { |
| sync { |
| previousIds = previousIds.or(snapshots) |
| } |
| } |
| |
| override fun recordModified(state: StateObject) { |
| (modified ?: HashSet<StateObject>().also { modified = it }).add(state) |
| } |
| |
| override var modified: MutableSet<StateObject>? = null |
| |
| /** |
| * A set of the id's previously associated with this snapshot. When this snapshot closes |
| * then these ids must be removed from the global as well. |
| */ |
| internal var previousIds: SnapshotIdSet = SnapshotIdSet.EMPTY |
| |
| /** |
| * The number of pending nested snapshots of this snapshot. To simplifythe code, this |
| * snapshot it, itself, counted as its own nested snapshot. |
| */ |
| private var snapshots = 1 |
| |
| /** |
| * Tracks whether the snapshot has been applied. |
| */ |
| internal var applied = false |
| } |
| |
| /** |
| * The result of a applying a mutable snapshot. [Success] indicates that the snapshot was |
| * successfully applied and is not visible as the global state of the state object (or visible |
| * in the parent snapshot for a nested snapshot). [Failure] indicates one or more state objects |
| * were modified by both this snapshot and in the global (or parent) snapshot. |
| */ |
| sealed class SnapshotApplyResult { |
| /** |
| * Check the result of an apply. If the result is [Success] then this does does nothing. If |
| * the result is [Failure] then a [SnapshotApplyConflictException] exception is thrown. Once |
| * [check] as been called the snapshot is disposed. |
| */ |
| abstract fun check() |
| |
| /** |
| * True if the result is [Success]. |
| */ |
| abstract val succeeded: Boolean |
| |
| object Success : SnapshotApplyResult() { |
| /** |
| * Check the result of a snapshot apply. Calling [check] on a [Success] result is a noop. |
| */ |
| override fun check() { } |
| |
| override val succeeded: Boolean get() = true |
| } |
| |
| class Failure(val snapshot: Snapshot) : SnapshotApplyResult() { |
| /** |
| * Check the result of a snapshot apply. Calling [check] on a [Failure] result throws a |
| * [SnapshotApplyConflictException] exception. |
| */ |
| override fun check() { |
| snapshot.dispose() |
| throw SnapshotApplyConflictException(snapshot) |
| } |
| |
| override val succeeded: Boolean get() = false |
| } |
| } |
| |
| /** |
| * Callback type for observing reads of state objects in a snapshot. |
| * |
| * @see takeSnapshot |
| * @see takeMutableSnapshot |
| */ |
| typealias SnapshotReadObserver = (state: Any) -> Unit |
| |
| /** |
| * Callback type for observing writes to state objects in a snapshot. |
| * |
| * @see Snapshot.registerGlobalWriteObserver |
| */ |
| typealias SnapshotWriteObserver = (state: Any) -> Unit |
| |
| /** |
| * Callback type for observing when a non-nested mutable snapshot is applied by calling |
| * [MutableSnapshot.apply], modifying the visible global state of state objects. Changes to a |
| * state object outside of a snapshot are also sent to the snapshot apply observer whenever |
| * [Snapshot.sendApplyNotifications] is called. |
| * |
| * The committed parameter is a collection of state objects modified in the snapshot. |
| * The snapshot parameter is the snapshot that was applied. |
| * |
| * @see takeMutableSnapshot |
| * @see MutableSnapshot.apply |
| * @see Snapshot |
| * @see Snapshot.registerApplyObserver |
| * @see Snapshot.sendApplyNotifications |
| */ |
| typealias SnapshotApplyObserver = (changed: Set<Any>, snapshot: Snapshot) -> Unit |
| |
| /** |
| * Return the thread's active snapshot. If no thread snapshot is active then the current global |
| * snapshot is used. |
| */ |
| internal fun currentSnapshot(): Snapshot = |
| threadSnapshot.get() ?: sync { currentGlobalSnapshot } |
| |
| /** |
| * An exception that is thrown when [SnapshotApplyResult.check] is called on a result of a |
| * [MutableSnapshot.apply] that fails to apply. |
| */ |
| @ExperimentalComposeApi |
| class SnapshotApplyConflictException( |
| @Suppress("unused") val snapshot: Snapshot |
| ) : Exception() |
| |
| /** |
| * Snapshot local value of a state object. |
| */ |
| @ExperimentalComposeApi |
| abstract class StateRecord { |
| /** |
| * The snapshot id of the snapshot in which the record was created. |
| */ |
| internal var snapshotId: Int = currentSnapshot().id |
| |
| /** |
| * Reference of the next state record. State records are stored in a linked list. |
| * |
| * Changes to [next] must preserve all existing records to all threads even during |
| * intermediately changes. For example, it is safe to add the beginning or end of the list |
| * but adding to the middle requires care. First the new record must have its [next] updated |
| * then the [next] of its new predecessor can then be set to point to it. This implies that |
| * records that are already in the list cannot be moved in the list as this the change must |
| * be atomic to all threads that cannot happen without a lock which this list cannot afford. |
| * |
| * It is unsafe to remove a record as it might be in the process of being reused (see [used]). |
| * If a record is removed care must be taken to ensure that it is not being claimed by some |
| * other thread. This would require changes to [used]. |
| */ |
| internal var next: StateRecord? = null |
| |
| /** |
| * Copy the value into this state record from another for the same state object. |
| */ |
| abstract fun assign(value: StateRecord) |
| |
| /** |
| * Create a new state record for the same state object. |
| */ |
| abstract fun create(): StateRecord |
| } |
| |
| /** |
| * Interface implemented by all snapshot aware state objects. Used by this module to maintain the |
| * state records of a state object. |
| */ |
| @ExperimentalComposeApi |
| interface StateObject { |
| /** |
| * The first state record in a linked list of state records. |
| */ |
| val firstStateRecord: StateRecord |
| |
| /** |
| * Add a new state record to the beginning of a list. After this call [firstStateRecord] should |
| * be [value]. |
| */ |
| fun prependStateRecord(value: StateRecord) |
| |
| /** |
| * Produce a merged state based on the conflicting state changes. |
| * |
| * This method must not modify any of the records received and should treat the state records |
| * as immutable, even the [applied] record. |
| * |
| * @param previous the state record that was used to create the [applied] record and is a state |
| * that also (though indirectly) produced the [current] record. |
| * |
| * @param current the state record of the parent snapshot or global state. |
| * |
| * @param applied the state record that is being applied of the parent snapshot or global |
| * state. |
| * |
| * @return the modified state or `null` if the values cannot be merged. If the states cannot |
| * be merged the current apply will fail. Any of the parameters can be returned as a result. |
| * If it is not one of the parameter values then it *must* be a new value that is created by |
| * calling [StateRecord.create] on one of the records passed and then can be modified |
| * to have the merged value before being returned. If a new record is returned |
| * [MutableSnapshot.apply] will update the internal snapshot id and call |
| * [prependStateRecord] if the record is used. |
| */ |
| fun mergeRecords( |
| previous: StateRecord, |
| current: StateRecord, |
| applied: StateRecord |
| ): StateRecord? = null |
| } |
| |
| /** |
| * A snapshot whose state objects cannot be modified. If a state object is modified when in a |
| * read-only snapshot a [IllegalStateException] is thrown. |
| */ |
| internal class ReadonlySnapshot internal constructor( |
| id: Int, |
| invalid: SnapshotIdSet, |
| override val readObserver: SnapshotReadObserver? |
| ) : Snapshot(id, invalid) { |
| /** |
| * The number of nested snapshots that are active. To simplify the code, this snapshot counts |
| * itself as a nested snapshot. |
| */ |
| private var snapshots = 1 |
| override val readonly: Boolean get() = true |
| override val root: Snapshot get() = this |
| override fun hasPendingChanges(): Boolean = false |
| override val writeObserver: SnapshotWriteObserver? get() = null |
| |
| override var modified: HashSet<StateObject>? |
| get() = null |
| @Suppress("UNUSED_PARAMETER") |
| set(value) = unsupported() |
| |
| override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?): Snapshot { |
| validateOpen(this) |
| return NestedReadonlySnapshot(id, invalid, readObserver, this) |
| } |
| |
| override fun notifyObjectsInitialized() { |
| // Nothing to do for read-only snapshots |
| } |
| |
| override fun dispose() { |
| if (!disposed) { |
| nestedDeactivated(this) |
| super.dispose() |
| } |
| } |
| |
| override fun nestedActivated(snapshot: Snapshot) { snapshots++ } |
| |
| override fun nestedDeactivated(snapshot: Snapshot) { |
| if (--snapshots == 0) { |
| // A read-only snapshot can be just be closed as it has no modifications. |
| close() |
| } |
| } |
| |
| override fun recordModified(state: StateObject) { |
| error("Cannot modify a state object in a read-only snapshot") |
| } |
| } |
| |
| internal class NestedReadonlySnapshot( |
| id: Int, |
| invalid: SnapshotIdSet, |
| readObserver: SnapshotReadObserver?, |
| val parent: Snapshot |
| ) : Snapshot(id, invalid) { |
| init { parent.nestedActivated(this) } |
| override val readonly get() = true |
| override val root: Snapshot get() = parent.root |
| @OptIn(ExperimentalComposeApi::class) |
| override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?) = |
| parent.takeNestedSnapshot(readObserver) |
| override fun notifyObjectsInitialized() { |
| // Nothing to do for read-only snapshots |
| } |
| override fun hasPendingChanges(): Boolean = false |
| override val readObserver: SnapshotReadObserver? = |
| // Merge the read observers if necessary |
| readObserver?.let { |
| parent.readObserver?.let { |
| { state: Any -> |
| readObserver(state) |
| it(state) |
| } |
| } ?: readObserver |
| } ?: parent.readObserver |
| |
| override fun dispose() { |
| if (!disposed) { |
| if (id != parent.id) { |
| close() |
| } |
| parent.nestedDeactivated(this) |
| super.dispose() |
| } |
| } |
| |
| override val modified: HashSet<StateObject>? get() = null |
| override val writeObserver: SnapshotWriteObserver? get() = null |
| @OptIn(ExperimentalComposeApi::class) |
| override fun recordModified(state: StateObject) = parent.recordModified(state) |
| |
| override fun nestedDeactivated(snapshot: Snapshot) = unsupported() |
| override fun nestedActivated(snapshot: Snapshot) = unsupported() |
| } |
| |
| private val emptyLambda: (invalid: SnapshotIdSet) -> Unit = { } |
| |
| /** |
| * A snapshot object that simplifies the code by treating the global state as a mutable snapshot. |
| */ |
| internal class GlobalSnapshot(id: Int, invalid: SnapshotIdSet) : |
| MutableSnapshot(id, invalid, null, sync { |
| // Take a defensive copy of the globalWriteObservers list. This then avoids having to |
| // synchronized access to writerObserver in places it is called and allows the list to |
| // change while notifications are being dispatched. Changes to globalWriteObservers force |
| // a new global snapshot to be created. |
| (if (globalWriteObservers.isNotEmpty()) { |
| globalWriteObservers.toMutableList() |
| } else null)?.let { |
| it.firstOrNull() ?: { state: Any -> |
| it.forEach { it(state) } |
| } |
| } |
| }) { |
| |
| override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?): Snapshot = |
| takeNewSnapshot { invalid -> |
| ReadonlySnapshot( |
| id = sync { nextSnapshotId++ }, |
| invalid = invalid, |
| readObserver = readObserver |
| ) |
| } |
| |
| override fun takeNestedMutableSnapshot( |
| readObserver: SnapshotReadObserver?, |
| writeObserver: SnapshotWriteObserver? |
| ): MutableSnapshot = takeNewSnapshot { invalid -> |
| MutableSnapshot( |
| id = sync { nextSnapshotId++ }, |
| invalid = invalid, |
| |
| // It is intentional that the global read observers are not merged with mutable |
| // snapshots read observers. |
| readObserver = readObserver, |
| |
| // It is intentional that global write observers are not merged with mutable |
| // snapshots write observers. |
| writeObserver = writeObserver |
| ) |
| } |
| |
| override fun notifyObjectsInitialized() { |
| advanceGlobalSnapshot() |
| } |
| |
| override fun nestedDeactivated(snapshot: Snapshot) = unsupported() |
| override fun nestedActivated(snapshot: Snapshot) = unsupported() |
| override fun apply(): SnapshotApplyResult = |
| error("Cannot apply the global snapshot directly. Call Snapshot.advanceGlobalSnapshot") |
| |
| override fun dispose() { |
| // Disposing the global snapshot is a no-op. |
| |
| // The dispose behavior is preformed by advancing the global snapshot. This method is |
| // squelched so calling it from `currentSnapshot` doesn't cause incorrect behavior |
| } |
| } |
| |
| /** |
| * A nested mutable snapshot created by [MutableSnapshot.takeNestedMutableSnapshot]. |
| */ |
| internal class NestedMutableSnapshot( |
| id: Int, |
| invalid: SnapshotIdSet, |
| readObserver: SnapshotReadObserver?, |
| writeObserver: SnapshotWriteObserver?, |
| val parent: MutableSnapshot |
| ) : MutableSnapshot(id, invalid, readObserver, writeObserver) { |
| |
| init { parent.nestedActivated(this) } |
| |
| override val root: Snapshot get() = parent.root |
| |
| override fun dispose() { |
| if (!disposed) { |
| super.dispose() |
| parent.nestedDeactivated(this) |
| } |
| } |
| |
| override fun apply(): SnapshotApplyResult { |
| if (parent.applied || parent.disposed) return SnapshotApplyResult.Failure(this) |
| |
| // Applying a nested mutable snapshot applies its changes to the parent snapshot. |
| |
| // See MutableSnapshot.apply() for implantation notes. |
| |
| // The apply observer notification are for applying to the global scope so it is elided |
| // here making this code a bit simpler than MutableSnapshot.apply. |
| |
| val modified = modified |
| val id = id |
| val optimisticMerges = if (modified != null) |
| optimisticMerges(parent, this, parent.invalid) |
| else null |
| sync { |
| validateOpen(this) |
| if (modified == null || modified.size == 0) { |
| close() |
| } else { |
| val result = innerApply(parent.id, optimisticMerges, parent.invalid) |
| if (result != SnapshotApplyResult.Success) return result |
| |
| // Add all modified objects in this set to the parent |
| (parent.modified ?: HashSet<StateObject>().also { |
| parent.modified = it |
| }).addAll(modified) |
| } |
| |
| // Make the snapshot visible in the parent snapshot |
| parent.invalid = parent.invalid.clear(id).andNot(previousIds) |
| |
| // Ensure the ids associated with this snapshot are also applied by the parent. |
| parent.recordPrevious(id) |
| parent.recordPreviousList(previousIds) |
| } |
| |
| applied = true |
| return SnapshotApplyResult.Success |
| } |
| } |
| |
| /** |
| * A pseudo snapshot that doesn't introduce isolation but does introduce observers. |
| */ |
| internal class TransparentObserverMutableSnapshot( |
| private val previousSnapshot: MutableSnapshot?, |
| internal val specifiedReadObserver: SnapshotReadObserver?, |
| internal val specifiedWriteObserver: SnapshotWriteObserver? |
| ) : MutableSnapshot( |
| INVALID_SNAPSHOT, |
| SnapshotIdSet.EMPTY, |
| mergedReadObserver( |
| specifiedReadObserver, |
| previousSnapshot?.readObserver ?: currentGlobalSnapshot.readObserver |
| ), |
| mergedWriteObserver( |
| specifiedWriteObserver, |
| previousSnapshot?.writeObserver ?: currentGlobalSnapshot.writeObserver |
| ) |
| ) { |
| private val currentSnapshot: MutableSnapshot |
| get() = previousSnapshot ?: currentGlobalSnapshot |
| |
| override fun dispose() { |
| // Explicitly don't call super.dispose() |
| disposed = true |
| } |
| |
| override var id: Int |
| get() = currentSnapshot.id |
| @Suppress("UNUSED_PARAMETER") |
| set(value) { unsupported() } |
| |
| @OptIn(ExperimentalComposeApi::class) |
| override var invalid get() = currentSnapshot.invalid |
| @Suppress("UNUSED_PARAMETER") |
| set(value) = unsupported() |
| |
| override fun hasPendingChanges(): Boolean = currentSnapshot.hasPendingChanges() |
| |
| override var modified: MutableSet<StateObject>? |
| get() = currentSnapshot.modified |
| @Suppress("UNUSED_PARAMETER") |
| set(value) = unsupported() |
| |
| override val readonly: Boolean |
| get() = currentSnapshot.readonly |
| |
| override fun apply(): SnapshotApplyResult = |
| currentSnapshot.apply() |
| |
| @OptIn(ExperimentalComposeApi::class) |
| override fun recordModified(state: StateObject) = |
| currentSnapshot.recordModified(state) |
| |
| override fun takeNestedSnapshot(readObserver: SnapshotReadObserver?): Snapshot = |
| currentSnapshot.takeNestedSnapshot(mergedReadObserver(readObserver, this.readObserver)) |
| |
| override fun takeNestedMutableSnapshot( |
| readObserver: SnapshotReadObserver?, |
| writeObserver: SnapshotWriteObserver? |
| ): MutableSnapshot = currentSnapshot.takeNestedMutableSnapshot( |
| mergedReadObserver(readObserver, this.readObserver), |
| mergedWriteObserver(writeObserver, this.writeObserver) |
| ) |
| |
| @OptIn(ExperimentalComposeApi::class) |
| override fun notifyObjectsInitialized() = currentSnapshot.notifyObjectsInitialized() |
| |
| // The following should never be called. |
| override fun nestedActivated(snapshot: Snapshot) = unsupported() |
| |
| override fun nestedDeactivated(snapshot: Snapshot) = unsupported() |
| } |
| |
| private fun mergedReadObserver( |
| readObserver: SnapshotReadObserver?, |
| parentObserver: SnapshotReadObserver? |
| ): SnapshotReadObserver? = |
| if (readObserver != null && parentObserver != null && readObserver != parentObserver) { |
| { state: Any -> |
| readObserver(state) |
| parentObserver(state) |
| } |
| } else readObserver ?: parentObserver |
| |
| private fun mergedWriteObserver( |
| writeObserver: SnapshotWriteObserver?, |
| parentObserver: SnapshotWriteObserver? |
| ): SnapshotWriteObserver? = |
| if (writeObserver != null && parentObserver != null && writeObserver != parentObserver) { |
| { state: Any -> |
| writeObserver(state) |
| parentObserver(state) |
| } |
| } else writeObserver ?: parentObserver |
| |
| /** |
| * Snapshot id of `0` is reserved as invalid and no state record with snapshot `0` is considered |
| * valid. |
| * |
| * The value `0` was chosen as it is the default value of the Int snapshot id type and records |
| * initially created will naturally have a snapshot id of 0. If this wasn't considered invalid |
| * adding such a record to a state object will make the state record immediately visible to the |
| * snapshots instead of being born invalid. Using `0` ensures all state records are created invalid |
| * and must be explicitly marked as valid in to be visible in a snapshot. |
| */ |
| private const val INVALID_SNAPSHOT = 0 |
| |
| private fun validateNotInSnapshot() { |
| threadSnapshot.get().let { |
| if (it != null && it !is TransparentObserverMutableSnapshot) |
| error("In an existing snapshot") |
| } |
| } |
| |
| private val threadSnapshot = ThreadLocal<Snapshot>() |
| |
| // A global synchronization object. This synchronization object should be taken before modifying any |
| // of the fields below. |
| @PublishedApi |
| internal val lock = Any() |
| |
| @PublishedApi |
| @Suppress("DEPRECATION_ERROR") |
| internal inline fun <T> sync(block: () -> T): T = synchronized(lock, block) |
| |
| // The following variables should only be written when sync is taken |
| |
| // A set of snapshots that are currently open and should be considered invalid for new snapshots. |
| private var openSnapshots = SnapshotIdSet.EMPTY |
| |
| // The first snapshot created must be at least on more than the INVALID_SNAPSHOT |
| private var nextSnapshotId = INVALID_SNAPSHOT + 1 |
| |
| // A list of apply observers |
| private val applyObservers = mutableListOf<SnapshotApplyObserver>() |
| |
| // A list of observers of writes to the global state. |
| private val globalWriteObservers = mutableListOf<SnapshotWriteObserver>() |
| |
| private var currentGlobalSnapshot = GlobalSnapshot( |
| id = nextSnapshotId++, |
| invalid = SnapshotIdSet.EMPTY |
| ).also { |
| openSnapshots = openSnapshots.set(it.id) |
| } |
| |
| private fun <T> takeNewGlobalSnapshot( |
| previousGlobalSnapshot: Snapshot, |
| block: (invalid: SnapshotIdSet) -> T |
| ): T { |
| // Deactivate global snapshot. It is safe to just deactivate it because it cannot have |
| // any conflicting writes as it is always closed before another snapshot is taken. |
| val result = block(openSnapshots.clear(previousGlobalSnapshot.id)) |
| |
| sync { |
| val globalId = nextSnapshotId++ |
| openSnapshots = openSnapshots.clear(previousGlobalSnapshot.id) |
| currentGlobalSnapshot = GlobalSnapshot( |
| id = globalId, |
| invalid = openSnapshots |
| ) |
| openSnapshots = openSnapshots.set(globalId) |
| } |
| |
| return result |
| } |
| |
| private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T { |
| val previousGlobalSnapshot = currentGlobalSnapshot |
| val result = sync { |
| takeNewGlobalSnapshot(previousGlobalSnapshot, block) |
| } |
| |
| // If the previous global snapshot had any modified states then notify the registered apply |
| // observers. |
| val modified = previousGlobalSnapshot.modified |
| if (modified != null) { |
| val observers = sync { applyObservers.toList() } |
| for (observer in observers) { |
| observer(modified, previousGlobalSnapshot) |
| } |
| } |
| |
| // Update the transparent snapshot if necessary |
| // This doesn't need to take the sync because it is updating thread local state. |
| (threadSnapshot.get() as? TransparentObserverMutableSnapshot)?.let { |
| threadSnapshot.set(TransparentObserverMutableSnapshot( |
| currentGlobalSnapshot, |
| it.specifiedReadObserver, |
| it.specifiedWriteObserver |
| )) |
| it.dispose() |
| } |
| return result |
| } |
| |
| private fun advanceGlobalSnapshot() = advanceGlobalSnapshot { } |
| |
| private fun <T : Snapshot> takeNewSnapshot(block: (invalid: SnapshotIdSet) -> T): T = |
| advanceGlobalSnapshot { invalid -> |
| val result = block(invalid) |
| sync { |
| openSnapshots = openSnapshots.set(result.id) |
| } |
| result |
| } |
| |
| private fun validateOpen(snapshot: Snapshot) { |
| if (!openSnapshots.get(snapshot.id)) error("Snapshot is not open") |
| } |
| |
| private fun valid(currentSnapshot: Int, candidateSnapshot: Int, invalid: SnapshotIdSet): Boolean { |
| // A candidate snapshot is valid if the it is less than or equal to the current snapshot |
| // and it wasn't specifically marked as invalid when the snapshot started. |
| // |
| // All snapshot active at when the snapshot was taken considered invalid for the snapshot |
| // (they have not been applied and therefore are considered invalid). |
| // |
| // All snapshots taken after the current snapshot are considered invalid since they where taken |
| // after the current snapshot was taken. |
| // |
| // INVALID_SNAPSHOT is reserved as an invalid snapshot id. |
| return candidateSnapshot != INVALID_SNAPSHOT && candidateSnapshot <= currentSnapshot && |
| !invalid.get(candidateSnapshot) |
| } |
| |
| // Determine if the given data is valid for the snapshot. |
| private fun valid(data: StateRecord, snapshot: Int, invalid: SnapshotIdSet): Boolean { |
| return valid(snapshot, data.snapshotId, invalid) |
| } |
| |
| private fun <T : StateRecord> readable(r: T, id: Int, invalid: SnapshotIdSet): T? { |
| // The readable record is the valid record with the highest snapshotId |
| var current: StateRecord? = r |
| var candidate: StateRecord? = null |
| while (current != null) { |
| if (valid(current, id, invalid)) { |
| candidate = if (candidate == null) current |
| else if (candidate.snapshotId < current.snapshotId) current else candidate |
| } |
| current = current.next |
| } |
| if (candidate != null) { |
| @Suppress("UNCHECKED_CAST") |
| return candidate as T |
| } |
| return null |
| } |
| |
| /** |
| * Return the current readable state record for the current snapshot. It is assumed that [this] |
| * is the first record of [state] |
| */ |
| fun <T : StateRecord> T.readable(state: StateObject): T = |
| readable(state, currentSnapshot()) |
| |
| /** |
| * Return the current readable state record for the [snapshot]. It is assumed that [this] |
| * is the first record of [state] |
| */ |
| fun <T : StateRecord> T.readable(state: StateObject, snapshot: Snapshot): T { |
| // invoke the observer associated with the current snapshot. |
| snapshot.readObserver?.invoke(state) |
| return readable(this, snapshot.id, snapshot.invalid) ?: readError() |
| } |
| |
| private fun readError(): Nothing { |
| error("Reading a state that was created after the snapshot was taken or in a snapshot that " + |
| "has not yet been applied") |
| } |
| |
| /** |
| * A record can be reused if no other snapshot will see it as valid. This is always true for a |
| * record created in an abandoned snapshot. It is also true if the record is valid in the |
| * previous snapshot and is obscured by another record also valid in the previous state record. |
| */ |
| private fun used(state: StateObject, id: Int, invalid: SnapshotIdSet): StateRecord? { |
| var current: StateRecord? = state.firstStateRecord |
| var validRecord: StateRecord? = null |
| while (current != null) { |
| val currentId = current.snapshotId |
| if (currentId == INVALID_SNAPSHOT) { |
| // Any records that were marked invalid by an abandoned snapshot can be used |
| // immediately. |
| return current |
| } |
| if (valid(current, id - 1, invalid)) { |
| if (validRecord == null) { |
| validRecord = current |
| } else { |
| // If we have two valid records one must obscure the other. Return the |
| // record with the lowest id |
| return if (current.snapshotId < validRecord.snapshotId) current else validRecord |
| } |
| } |
| current = current.next |
| } |
| return null |
| } |
| |
| @PublishedApi |
| internal fun <T : StateRecord> T.writableRecord(state: StateObject, snapshot: Snapshot): T { |
| if (snapshot.readonly) { |
| // If the snapshot is read-only, use the snapshot recordModified to report it. |
| snapshot.recordModified(state) |
| } |
| val id = snapshot.id |
| val readData = readable<T>(this, id, snapshot.invalid) ?: readError() |
| |
| // If the readable data was born in this snapshot, it is writable. |
| if (readData.snapshotId == snapshot.id) return readData |
| |
| // Otherwise, make a copy of the readable data and mark it as born in this snapshot, making it |
| // writable. |
| |
| // Calling used() on a state object might return the same record for each thread calling |
| // used() therefore selecting the record to reuse should be guarded. |
| |
| // Note: setting the snapshotId to Int.MAX_VALUE will make it invalid for all snapshots. |
| // This means the lock can be released as used() will no longer select it. Using id could |
| // also be used but it puts the object into a state where the reused value appears to be |
| // the current valid value for the snapshot. This is not an issue if the snapshot is only |
| // being read from a single thread but using Int.MAX_VALUE allows multiple readers, |
| // single writer, of a snapshot. Note that threads reading a mutating snapshot should not |
| // cache the result of readable() as the mutating thread calls to writable() can change the |
| // result of readable(). |
| @Suppress("UNCHECKED_CAST") |
| val newData = (used(state, id, openSnapshots) as T?)?.apply { |
| snapshotId = Int.MAX_VALUE |
| } ?: readData.create().apply { |
| snapshotId = Int.MAX_VALUE |
| this.next = state.firstStateRecord |
| state.prependStateRecord(this as T) |
| } as T |
| newData.assign(readData) |
| newData.snapshotId = id |
| |
| snapshot.recordModified(state) |
| |
| return newData |
| } |
| |
| @PublishedApi |
| internal fun notifyWrite(snapshot: Snapshot, state: StateObject) { |
| snapshot.writeObserver?.invoke(state) |
| } |
| |
| /** |
| * Call [block] with a writable state record for [snapshot] of the given record. It is |
| * assumed that this is called for the first state record in a state object. If the snapshot is |
| * read-only calling this will throw. |
| */ |
| inline fun <T : StateRecord, R> T.writable( |
| state: StateObject, |
| snapshot: Snapshot, |
| block: T.() -> R |
| ): R { |
| // A writable record will always be the readable record (as all newer records are invalid it |
| // must be the newest valid record). This means that if the readable record is not from the |
| // current snapshot, a new record must be created. To create a new writable record, a record |
| // can be reused, if possible, and the readable record is applied to it. If a record cannot |
| // be reused, a new record is created and the readable record is applied to it. Once the |
| // values are correct the record is made live by giving it the current snapshot id. |
| |
| // Writes need to be in a `sync` block as all writes in flight must be completed before a new |
| // snapshot is take. Writing in a sync block ensures this is the case because new snapshots |
| // are also in a sync block. |
| return sync { |
| this.writableRecord(state, snapshot).block() |
| }.also { notifyWrite(snapshot, state) } |
| } |
| |
| /** |
| * Call [block] with a writable state record for the given record. It is assumed that this is |
| * called for the first state record in a state object. A record is writable if it was created in |
| * the current mutable snapshot. |
| */ |
| inline fun <T : StateRecord, R> T.writable(state: StateObject, block: T.() -> R): R = |
| this.writable(state, Snapshot.current, block) |
| |
| /** |
| * Produce a set of optimistic merges of the state records, this is performed outside the |
| * a synchronization block to reduce the amount of time taken in the synchronization block |
| * reducing the thread contention of merging state values. |
| */ |
| private fun optimisticMerges( |
| currentSnapshot: MutableSnapshot, |
| applyingSnapshot: MutableSnapshot, |
| invalidSnapshots: SnapshotIdSet |
| ): Map<StateRecord, StateRecord>? { |
| val modified = applyingSnapshot.modified |
| val id = currentSnapshot.id |
| if (modified == null) return null |
| val start = applyingSnapshot.invalid.set(applyingSnapshot.id).or(applyingSnapshot.previousIds) |
| var result: MutableMap<StateRecord, StateRecord>? = null |
| for (state in modified) { |
| val first = state.firstStateRecord |
| val current = readable(first, id, invalidSnapshots) ?: continue |
| val previous = readable(first, id, start) ?: continue |
| if (current != previous) { |
| // Try to produce a merged state record |
| val applied = readable(first, applyingSnapshot.id, applyingSnapshot.invalid) |
| ?: readError() |
| val merged = state.mergeRecords(previous, current, applied) |
| if (merged != null) { |
| (result ?: hashMapOf<StateRecord, StateRecord>().also { |
| result = it |
| })[current] = merged |
| } else { |
| // If one fails don't bother calculating the others as they are likely not going |
| // to be used. There is an unlikely case that a optimistic merge cannot be |
| // produced but the snapshot will apply because, once the synchronization is taken, |
| // the current state can be merge. This routine errors on the side of reduced |
| // overall work by not performing work that is likely to be ignored. |
| return null |
| } |
| } |
| } |
| return result |
| } |
| |
| /** |
| * Returns the current record without notifying any read observers. |
| */ |
| @PublishedApi |
| internal fun <T : StateRecord> current(r: T, snapshot: Snapshot) = |
| readable(r, snapshot.id, snapshot.invalid) ?: readError() |
| |
| /** |
| * Provides a [block] with the current record, without notifying any read observers. |
| * |
| * @see readable |
| */ |
| @ExperimentalComposeApi |
| inline fun <T : StateRecord, R> T.withCurrent(block: (r: T) -> R): R = |
| block(current(this, Snapshot.current)) |