blob: 7382015d25fdc302b97ee8ace4207cde70281180 [file]
/*
* 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.
*/
package androidx.compose.runtime.snapshots
import androidx.collection.MutableScatterSet
import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.ExperimentalComposeRuntimeApi
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.checkPrecondition
import androidx.compose.runtime.collection.wrapIntoSet
import androidx.compose.runtime.internal.AtomicInt
import androidx.compose.runtime.internal.JvmDefaultWithCompatibility
import androidx.compose.runtime.internal.SnapshotThreadLocal
import androidx.compose.runtime.internal.currentThreadId
import androidx.compose.runtime.platform.SynchronizedObject
import androidx.compose.runtime.platform.makeSynchronizedObject
import androidx.compose.runtime.platform.synchronized
import androidx.compose.runtime.requirePrecondition
import androidx.compose.runtime.snapshots.Snapshot.Companion.takeMutableSnapshot
import androidx.compose.runtime.snapshots.Snapshot.Companion.takeSnapshot
import androidx.compose.runtime.snapshots.tooling.creatingSnapshot
import androidx.compose.runtime.snapshots.tooling.dispatchObserverOnApplied
import androidx.compose.runtime.snapshots.tooling.dispatchObserverOnPreDispose
import androidx.compose.runtime.tooling.verboseTrace
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* 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
*/
public sealed class Snapshot(
snapshotId: SnapshotId,
/** A set of all the snapshots that should be treated as invalid. */
internal open var invalid: SnapshotIdSet,
) {
@Deprecated("Use id: Long constructor instead", level = DeprecationLevel.HIDDEN)
protected constructor(id: Int, invalid: SnapshotIdSet) : this(id.toSnapshotId(), invalid)
/**
* The snapshot id of the snapshot. This is a unique number from a monotonically increasing
* value for each snapshot taken.
*
* [id] will is identical to [snapshotId] if the value of [snapshotId] is less than or equal to
* [Int.MAX_VALUE]. For [snapshotId] value greater than [Int.MAX_VALUE], this value will return
* a negative value.
*/
@Deprecated("Use snapshotId instead", replaceWith = ReplaceWith("snapshotId"))
public open val id: Int
get() = snapshotId.toInt()
/**
* The snapshot id of the snapshot. This is a unique number from a monotonically increasing
* value for each snapshot taken.
*/
public open var snapshotId: SnapshotId = snapshotId
internal set
internal open var writeCount: Int
get() = 0
@Suppress("UNUSED_PARAMETER")
set(value) {
error("Updating write count is not supported for this snapshot")
}
/**
* The root snapshot for this snapshot. For non-nested snapshots this is always `this`. For
* nested snapshot it is the parent's [root].
*/
public abstract val root: Snapshot
/** True if any change to a state object in this snapshot will throw. */
public 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.
*/
public open fun dispose() {
disposed = true
sync { releasePinnedSnapshotLocked() }
}
/**
* 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.
*/
public abstract fun takeNestedSnapshot(readObserver: ((Any) -> Unit)? = null): Snapshot
/**
* Whether there are any pending changes in this snapshot. These changes are not visible until
* the snapshot is applied.
*/
public 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 objects 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
*/
public 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)
}
/**
* Enter the snapshot, returning the previous [Snapshot] for leaving this snapshot later using
* [unsafeLeave]. Prefer [enter] or [asContextElement] instead of using [unsafeEnter] directly
* to prevent mismatched [unsafeEnter]/[unsafeLeave] calls.
*
* After returning all state objects have the value associated with this snapshot. The value of
* [currentSnapshot] will be this snapshot until [unsafeLeave] is called with the returned
* [Snapshot] or another call to [unsafeEnter] or [enter] is made.
*
* All changes to state objects until another snapshot is entered or this snapshot is left 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 can be applied atomically to the global
* state (or to its parent snapshot if it is a nested snapshot) by calling
* [MutableSnapshot.apply].
*/
public fun unsafeEnter(): Snapshot? = makeCurrent()
/** Leave the snapshot, restoring the [oldSnapshot] before returning. See [unsafeEnter]. */
public fun unsafeLeave(oldSnapshot: Snapshot?) {
checkPrecondition(threadSnapshot.get() === this) {
"Cannot leave snapshot; $this is not the current snapshot"
}
restoreCurrent(oldSnapshot)
}
internal var disposed = false
/*
* Handle to use when unpinning this snapshot. -1 if this snapshot has been unpinned.
*/
@Suppress("LeakingThis")
private var pinningTrackingHandle =
if (snapshotId != INVALID_SNAPSHOT) trackPinning(snapshotId, invalid) else -1
internal inline val isPinned
get() = pinningTrackingHandle >= 0
/*
* The read observer for the snapshot if there is one.
*/
@PublishedApi internal abstract val readObserver: ((Any) -> Unit)?
/** The write observer for the snapshot if there is one. */
internal abstract val writeObserver: ((Any) -> Unit)?
/** 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: MutableScatterSet<StateObject>?
/**
* Notify the snapshot that all objects created in this snapshot to this point should be
* considered initialized. If any state object is modified after this point it will appear as
* modified in the snapshot. Any applicable snapshot write observer will be called for the
* object and the object will be part of the a set of mutated objects sent to any applicable
* snapshot apply observer.
*
* 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 and unpinning snapshots that no longer are referenced by this snapshot.
*/
internal fun closeAndReleasePinning() {
sync {
closeLocked()
releasePinnedSnapshotsForCloseLocked()
}
}
/**
* Closes the snapshot by removing the snapshot id (and any previous ids) from the list of open
* snapshots. Does not release pinned snapshots. See [releasePinnedSnapshotsForCloseLocked] for
* the second half of [closeAndReleasePinning].
*
* Call while holding a `sync {}` lock.
*/
internal open fun closeLocked() {
openSnapshots = openSnapshots.clear(snapshotId)
}
/**
* Releases all pinned snapshots required to perform a clean [closeAndReleasePinning].
*
* Call while holding a `sync {}` lock.
*
* See [closeAndReleasePinning], [closeLocked].
*/
internal open fun releasePinnedSnapshotsForCloseLocked() {
releasePinnedSnapshotLocked()
}
internal fun validateNotDisposed() {
requirePrecondition(!disposed) { "Cannot use a disposed snapshot" }
}
internal fun releasePinnedSnapshotLocked() {
if (pinningTrackingHandle >= 0) {
releasePinningLocked(pinningTrackingHandle)
pinningTrackingHandle = -1
}
}
internal fun takeoverPinnedSnapshot(): Int =
pinningTrackingHandle.also { pinningTrackingHandle = -1 }
public companion object {
/**
* Return the thread's active snapshot. If no thread snapshot is active then the current
* global snapshot is used.
*/
public val current: Snapshot
get() = currentSnapshot()
/** Return `true` if the thread is currently in the context of a snapshot. */
public val isInSnapshot: Boolean
get() = threadSnapshot.get() != null
/**
* Returns whether any threads are currently in the process of notifying observers about
* changes to the global snapshot.
*/
public val isApplyObserverNotificationPending: Boolean
get() = pendingApplyObserverCount.get() > 0
/**
* All new state objects initial state records should be [PreexistingSnapshotId] which then
* allows snapshots outside the creating snapshot to access the object with its initial
* state.
*/
@Suppress("ConstPropertyName") public const val PreexistingSnapshotId: Int = 1
/**
* 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 snapshot apply observer 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
*/
public fun takeSnapshot(readObserver: ((Any) -> Unit)? = 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 Snapshot.takeSnapshot
* @see Snapshot
* @see MutableSnapshot
*/
public fun takeMutableSnapshot(
readObserver: ((Any) -> Unit)? = null,
writeObserver: ((Any) -> Unit)? = null,
): MutableSnapshot =
(currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot(
readObserver,
writeObserver,
) ?: error("Cannot create a mutable snapshot of an read-only snapshot")
/**
* 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]
*/
public inline fun <T> global(block: () -> T): T {
val previous = removeCurrent()
try {
return block()
} finally {
restoreCurrent(previous)
}
}
/**
* Take a [MutableSnapshot] and run [block] within it. When [block] returns successfully,
* attempt to [MutableSnapshot.apply] the snapshot. Returns the result of [block] or throws
* [SnapshotApplyConflictException] if snapshot changes attempted by [block] could not be
* applied.
*
* Prior to returning, any changes made to snapshot state (e.g. state holders returned by
* [androidx.compose.runtime.mutableStateOf] are not visible to other threads. When
* [withMutableSnapshot] returns successfully those changes will be made visible to other
* threads and any snapshot observers (e.g. [androidx.compose.runtime.snapshotFlow]) will be
* notified of changes.
*
* [block] must not suspend if [withMutableSnapshot] is called from a suspend function.
*/
// TODO: determine a good way to prevent/discourage suspending in an inlined [block]
public inline fun <R> withMutableSnapshot(block: () -> R): R =
takeMutableSnapshot().run {
var hasError = false
try {
enter(block)
} catch (e: Throwable) {
hasError = true
throw e
} finally {
if (!hasError) {
apply().check()
}
dispose()
}
}
/**
* 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
* [Snapshot.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.
* @param block the code the [readObserver] and [writeObserver] will be observing. Once
* [block] returns, the [readObserver] and [writeObserver] will no longer be called.
*/
public fun <T> observe(
readObserver: ((Any) -> Unit)? = null,
writeObserver: ((Any) -> Unit)? = null,
block: () -> T,
): T = observeInternal(readObserver, writeObserver, block)
@Suppress("NOTHING_TO_INLINE")
// marked as inline to use as part of SnapshotStateObserver without adding extra function
// call overhead.
internal inline fun <T> observeInternal(
noinline readObserver: ((Any) -> Unit)? = null,
noinline writeObserver: ((Any) -> Unit)? = null,
noinline block: () -> T,
): T {
if (readObserver == null && writeObserver == null) {
// No observer change, just execute the block
return block()
}
val previous = threadSnapshot.get()
if (previous is TransparentObserverMutableSnapshot && previous.canBeReused) {
// Change observers in place without allocating new snapshots.
val previousReadObserver = previous.readObserver
val previousWriteObserver = previous.writeObserver
try {
previous.readObserver = mergedReadObserver(readObserver, previousReadObserver)
previous.writeObserver =
mergedWriteObserver(writeObserver, previousWriteObserver)
return block()
} finally {
previous.readObserver = previousReadObserver
previous.writeObserver = previousWriteObserver
}
} else {
// The snapshot is not already transparent, observe in a new transparent snapshot
val snapshot =
when {
previous == null || previous is MutableSnapshot -> {
TransparentObserverMutableSnapshot(
parentSnapshot = previous as? MutableSnapshot,
specifiedReadObserver = readObserver,
specifiedWriteObserver = writeObserver,
mergeParentObservers = true,
ownsParentSnapshot = false,
)
}
readObserver == null -> {
return block()
}
else -> {
previous.takeNestedSnapshot(readObserver)
}
}
try {
return snapshot.enter(block)
} finally {
snapshot.dispose()
}
}
}
@Suppress("unused") // left here for binary compatibility
@PublishedApi
internal fun createNonObservableSnapshot(): Snapshot =
createTransparentSnapshotWithNoParentReadObserver(
previousSnapshot = threadSnapshot.get()
)
@PublishedApi
internal val currentThreadSnapshot: Snapshot?
get() = threadSnapshot.get()
private inline val TransparentObserverMutableSnapshot.canBeReused: Boolean
get() = threadId == currentThreadId()
private inline val TransparentObserverSnapshot.canBeReused: Boolean
get() = threadId == currentThreadId()
@PublishedApi
internal fun makeCurrentNonObservable(previous: Snapshot?): Snapshot =
when {
previous is TransparentObserverMutableSnapshot && previous.canBeReused -> {
previous.readObserver = null
previous
}
previous is TransparentObserverSnapshot && previous.canBeReused -> {
previous.readObserver = null
previous
}
else -> {
val snapshot =
createTransparentSnapshotWithNoParentReadObserver(
previousSnapshot = previous
)
snapshot.makeCurrent()
snapshot
}
}
@PublishedApi
internal fun restoreNonObservable(
previous: Snapshot?,
nonObservable: Snapshot,
observer: ((Any) -> Unit)?,
) {
if (previous === nonObservable) {
when (previous) {
is TransparentObserverMutableSnapshot -> {
previous.readObserver = observer
}
is TransparentObserverSnapshot -> {
previous.readObserver = observer
}
else -> {
error("Non-transparent snapshot was reused: $previous")
}
}
} else {
nonObservable.restoreCurrent(previous)
nonObservable.dispose()
}
}
/**
* Passed [block] will be run with all the currently set snapshot read observers disabled.
*/
@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
@OptIn(ExperimentalContracts::class)
public inline fun <T> withoutReadObservation(block: @DisallowComposableCalls () -> T): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
val previousSnapshot = currentThreadSnapshot
val observer = previousSnapshot?.readObserver
val newSnapshot = makeCurrentNonObservable(previousSnapshot)
try {
return block()
} finally {
restoreNonObservable(previousSnapshot, newSnapshot, observer)
}
}
/**
* Register an apply listener that is called back when snapshots are applied to the global
* state.
*
* @return [ObserverHandle] to unregister [observer].
*/
public fun registerApplyObserver(observer: (Set<Any>, Snapshot) -> Unit): ObserverHandle {
// Ensure observer does not see changes before this call.
advanceGlobalSnapshot(emptyLambda)
sync { applyObservers += observer }
return ObserverHandle { sync { applyObservers -= 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 [ObserverHandle] to unregister [observer].
*/
public fun registerGlobalWriteObserver(observer: ((Any) -> Unit)): ObserverHandle {
sync { globalWriteObservers += observer }
advanceGlobalSnapshot()
return ObserverHandle {
sync { globalWriteObservers -= 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 snapshot write observer will be
* called for the object and the object will be part of the a set of mutated objects sent to
* any applicable snapshot apply observer.
*
* 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.
*/
public fun notifyObjectsInitialized(): Unit = 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].
*/
public fun sendApplyNotifications() {
val changes = sync { globalSnapshot.hasPendingChanges() }
if (changes) advanceGlobalSnapshot()
}
@InternalComposeApi public fun openSnapshotCount(): Int = 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)
}
}
}
/**
* Pin the snapshot and invalid set.
*
* @return returns a handle that should be passed to [releasePinningLocked] when the snapshot closes
* or is disposed.
*/
internal fun trackPinning(snapshotId: SnapshotId, invalid: SnapshotIdSet): Int {
val pinned = invalid.lowest(snapshotId)
return sync { pinningTable.add(pinned) }
}
/** Release the [handle] returned by [trackPinning] */
internal fun releasePinningLocked(handle: Int) {
pinningTable.remove(handle)
}
/**
* 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 Snapshot.takeMutableSnapshot
* @see androidx.compose.runtime.mutableStateOf
* @see androidx.compose.runtime.mutableStateListOf
* @see androidx.compose.runtime.mutableStateMapOf
*/
public open class MutableSnapshot
internal constructor(
snapshotId: SnapshotId,
invalid: SnapshotIdSet,
override val readObserver: ((Any) -> Unit)?,
override val writeObserver: ((Any) -> Unit)?,
) : Snapshot(snapshotId, 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.
*/
@OptIn(ExperimentalComposeRuntimeApi::class)
public open fun takeNestedMutableSnapshot(
readObserver: ((Any) -> Unit)? = null,
writeObserver: ((Any) -> Unit)? = null,
): MutableSnapshot {
validateNotDisposed()
validateNotAppliedOrPinned()
return creatingSnapshot(this, readObserver, writeObserver, readonly = false) {
actualReadObserver,
actualWriteObserver ->
advance {
sync {
val newId = nextSnapshotId
nextSnapshotId += 1
openSnapshots = openSnapshots.set(newId)
val currentInvalid = invalid
this.invalid = currentInvalid.set(newId)
NestedMutableSnapshot(
newId,
currentInvalid.addRange(snapshotId + 1, newId),
mergedReadObserver(actualReadObserver, this.readObserver),
mergedWriteObserver(actualWriteObserver, 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.
*/
public 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) {
val globalSnapshot = globalSnapshot
optimisticMerges(
globalSnapshot.snapshotId,
this,
openSnapshots.clear(globalSnapshot.snapshotId),
)
} else null
var observers = emptyList<(Set<Any>, Snapshot) -> Unit>()
var globalModified: MutableScatterSet<StateObject>? = null
sync {
validateOpen(this)
if (modified == null || modified.size == 0) {
closeLocked()
val globalSnapshot = globalSnapshot
val previousModified = globalSnapshot.modified
resetGlobalSnapshotLocked(globalSnapshot, emptyLambda)
if (previousModified != null && previousModified.isNotEmpty()) {
observers = applyObservers
globalModified = previousModified
}
} else {
val globalSnapshot = globalSnapshot
val result =
innerApplyLocked(
nextSnapshotId,
modified,
optimisticMerges,
openSnapshots.clear(globalSnapshot.snapshotId),
)
if (result != SnapshotApplyResult.Success) return result
closeLocked()
// Take a new global snapshot that includes this one.
val previousModified = globalSnapshot.modified
resetGlobalSnapshotLocked(globalSnapshot, emptyLambda)
this.modified = null
globalSnapshot.modified = null
observers = applyObservers
globalModified = previousModified
}
}
// Mark as applied
applied = true
// Notify any apply observers that changes applied were seen
if (globalModified != null) {
val nonNullGlobalModified = globalModified!!.wrapIntoSet()
if (nonNullGlobalModified.isNotEmpty()) {
verboseTrace("Compose:applyObservers") {
observers.fastForEach { it(nonNullGlobalModified, this) }
}
}
}
if (modified != null && modified.isNotEmpty()) {
val modifiedSet = modified.wrapIntoSet()
verboseTrace("Compose:applyObservers") {
observers.fastForEach { it(modifiedSet, this) }
}
}
dispatchObserverOnApplied(this, modified)
// Wait to release pinned snapshots until after running observers.
// This permits observers to safely take a nested snapshot of the one that was just applied
// before unpinning records that need to be retained in this case.
sync {
releasePinnedSnapshotsForCloseLocked()
checkAndOverwriteUnusedRecordsLocked()
globalModified?.forEach { processForUnusedRecordsLocked(it) }
modified?.forEach { processForUnusedRecordsLocked(it) }
merged?.fastForEach { processForUnusedRecordsLocked(it) }
merged = null
}
return SnapshotApplyResult.Success
}
override val readOnly: Boolean
get() = false
override val root: Snapshot
get() = this
override fun dispose() {
if (!disposed) {
super.dispose()
nestedDeactivated(this)
dispatchObserverOnPreDispose(this)
}
}
@OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot {
validateNotDisposed()
validateNotAppliedOrPinned()
val previousId = snapshotId
return creatingSnapshot(
if (this is GlobalSnapshot) null else this,
readObserver = readObserver,
writeObserver = null,
readonly = true,
) { actualReadObserver, _ ->
advance {
sync {
val readonlyId = nextSnapshotId.also { nextSnapshotId += 1 }
openSnapshots = openSnapshots.set(readonlyId)
NestedReadonlySnapshot(
snapshotId = readonlyId,
invalid = invalid.addRange(previousId + 1, readonlyId),
readObserver = mergedReadObserver(actualReadObserver, this.readObserver),
parent = this,
)
}
}
}
}
override fun nestedActivated(snapshot: Snapshot) {
snapshots++
}
override fun nestedDeactivated(snapshot: Snapshot) {
requirePrecondition(snapshots > 0) { "no pending nested snapshots" }
if (--snapshots == 0) {
if (!applied) {
abandon()
}
}
}
override fun notifyObjectsInitialized() {
if (applied || disposed) return
advance()
}
override fun closeLocked() {
// Remove itself and previous ids from the open set.
openSnapshots = openSnapshots.clear(snapshotId).andNot(previousIds)
}
override fun releasePinnedSnapshotsForCloseLocked() {
releasePreviouslyPinnedSnapshotsLocked()
super.releasePinnedSnapshotsForCloseLocked()
}
private fun validateNotApplied() {
checkPrecondition(!applied) { "Unsupported operation on a snapshot that has been applied" }
}
private fun validateNotAppliedOrPinned() {
checkPrecondition(!applied || isPinned) {
"Unsupported operation on a disposed or applied snapshot"
}
}
/**
* Abandon the snapshot. This does NOT [closeAndReleasePinning], which must be done as an
* additional step by callers.
*/
private 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 = snapshotId
modified.forEach { state ->
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.
closeAndReleasePinning()
}
internal fun innerApplyLocked(
nextId: SnapshotId,
modified: MutableScatterSet<StateObject>,
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 them have collisions.
// A record is guaranteed not to collide if no other write was performed to it since this
// snapshot was taken. No writes to a state object occurred if, ignoring this snapshot,
// the readable records for the snapshots remain the same. If they are different then
// there is a potential collision, and the state object is asked if it can resolve
// it. If it can, the updated state record is used for the apply.
// Determining if there is a collision and resolving it requires finding:
// 1) the applying record state (i.e., the record being applied by this snapshot)
// 2) the current record state (i.e., the record seen by the global snapshot)
// 3) the previous state record (i.e., the record originally copied)
// The applying record is the readable record this snapshot observes. It is found by calling
// readable with this snapshot's ignore set and id (which what the state object also does).
// The current record can be found by asking what would the next snapshot observes. This is
// found by calling readable with the invalidSnapshots (the set of all currently open
// snapshots) which is what the next snapshot would have in its invalid, and nextId, which
// is the id the next snapshot will have.
// The previous record can be found by looking for the record that this snapshot would
// observe had it not modified it. This is done by excluding the snapshot itself from
// invalid (an all previous ids the snapshot had because it advanced) while still using its
// id to find the record.
// Once these records are found, a record is in a merge conflict if both the applying record
// and the current record have a different record and neither of them is the previous
// record.
// If the record is not changed outside this snapshot (the most likely scenario), then
// there is no conflict, and there is no reason to determine the applying record. For this
// reason, the current and previous are determined first the applied record is only
// determined if there is a conflict (as it is assumed applied record is different from the
// previous record since this code would not execute if they were equal).
// A state object's mutation policy controls how conflicts are resolved. By default, all
// conflicts cannot be resolved and the snapshot will not be applied. However, given the
// previous, current, and next records, sometimes conflicts can be resolved (e.g. similar to
// merge conflicts in a git commit) and, if so, a new value can be provided by the mutation
// policy that merges the changes. If all changed objects can be merged, then the snapshot
// will apply but with the new, merged values (e.g., conflict-free data types are an example
// of types that can be merged).
var mergedRecords: MutableList<Pair<StateObject, StateRecord>>? = null
val start = this.invalid.set(this.snapshotId).or(this.previousIds)
var statesToRemove: MutableList<StateObject>? = null
modified.forEach { state ->
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, nextId, invalidSnapshots) ?: return@forEach
val previous = readable(first, this.snapshotId, start) ?: return@forEach
if (previous.snapshotId == PreexistingSnapshotId.toSnapshotId()) {
// A previous record might not be found if the state object was created in a
// nested snapshot that didn't have any other modifications. The `apply()` for
// a nested snapshot considers such snapshots no-op snapshots and just closes them
// which allows this object's previous record to be missing or be the record created
// during initial construction. In these cases taking applied is the right choice
// this indicates there was no conflicting writes.
return@forEach
}
if (current != previous) {
val applied = readable(first, this.snapshotId, 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(snapshotId))
// 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(snapshotId)
)
}
}
}
}
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 = nextId
sync {
stateRecord.next = state.firstStateRecord
state.prependStateRecord(stateRecord)
}
}
}
statesToRemove?.let { list ->
list.fastForEach { modified.remove(it) }
val mergedList = merged
merged = if (mergedList == null) list else mergedList + list
}
return SnapshotApplyResult.Success
}
internal inline fun <T> advance(block: () -> T): T {
recordPrevious(snapshotId)
return block().also {
// Only advance this snapshot if it's possible for it to be applied later,
// otherwise we don't need to bother.
// This simplifies tracking of open snapshots when an apply observer takes
// a nested snapshot of the snapshot that was just applied.
if (!applied && !disposed) {
val previousId = snapshotId
sync {
snapshotId = nextSnapshotId.also { nextSnapshotId += 1 }
openSnapshots = openSnapshots.set(snapshotId)
}
invalid = invalid.addRange(previousId + 1, snapshotId)
}
}
}
internal fun advance(): Unit = advance {}
internal fun recordPrevious(id: SnapshotId) {
sync { previousIds = previousIds.set(id) }
}
internal fun recordPreviousPinnedSnapshot(id: Int) {
if (id >= 0) previousPinnedSnapshots += id
}
internal fun recordPreviousPinnedSnapshots(handles: IntArray) {
// Avoid unnecessary copies implied by the `+` below.
if (handles.isEmpty()) return
val pinned = previousPinnedSnapshots
previousPinnedSnapshots = if (pinned.isEmpty()) handles else pinned + handles
}
private fun releasePreviouslyPinnedSnapshotsLocked() {
for (index in previousPinnedSnapshots.indices) {
releasePinningLocked(previousPinnedSnapshots[index])
}
}
internal fun recordPreviousList(snapshots: SnapshotIdSet) {
sync { previousIds = previousIds.or(snapshots) }
}
override fun recordModified(state: StateObject) {
(modified ?: mutableScatterSetOf<StateObject>().also { modified = it }).add(state)
}
override var writeCount: Int = 0
override var modified: MutableScatterSet<StateObject>? = null
internal var merged: List<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
/** A list of the pinned snapshots handles that must be released by this snapshot */
internal var previousPinnedSnapshots: IntArray = EmptyIntArray
/**
* The number of pending nested snapshots of this snapshot. To simplify the 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
private companion object {
private val EmptyIntArray = IntArray(0)
}
}
/**
* The result of a applying a mutable snapshot. [Success] indicates that the snapshot was
* successfully applied and is now 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, and the changes from this
* snapshot are **not** visible in the global or parent snapshot.
*/
public 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.
*/
public abstract fun check()
/** True if the result is [Success]. */
public abstract val succeeded: Boolean
public 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
}
public class Failure(public 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
}
}
/**
* The type returned by observer registration methods that unregisters the observer when it is
* disposed.
*/
@Suppress("CallbackName")
public fun interface ObserverHandle {
/** Dispose the observer causing it to be unregistered from the snapshot system. */
public fun dispose()
}
/**
* 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() ?: globalSnapshot
/**
* An exception that is thrown when [SnapshotApplyResult.check] is called on a result of a
* [MutableSnapshot.apply] that fails to apply.
*/
public class SnapshotApplyConflictException(@Suppress("unused") public val snapshot: Snapshot) :
Exception()
/** Snapshot local value of a state object. */
public abstract class StateRecord(
/** The snapshot id of the snapshot in which the record was created. */
internal var snapshotId: SnapshotId
) {
public constructor() : this(currentSnapshot().snapshotId)
@Deprecated("Use snapshotId: Long constructor instead")
public constructor(id: Int) : this(id.toSnapshotId())
/**
* 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
* [usedLocked]). 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 [usedLocked].
*/
internal var next: StateRecord? = null
/** Copy the value into this state record from another for the same state object. */
public abstract fun assign(value: StateRecord)
/**
* Create a new state record for the same state object. Consider also implementing the [create]
* overload that provides snapshotId for faster record construction when snapshot id is known.
*/
public abstract fun create(): StateRecord
/**
* Create a new state record for the same state object and provided [snapshotId]. This allows to
* implement an optimized version of [create] to avoid accessing [currentSnapshot] when snapshot
* id is known. The default implementation provides a backwards compatible behavior, and should
* be overridden if [StateRecord] subclass supports this optimization.
*/
@Deprecated("Use snapshotId: Long version instead", level = DeprecationLevel.HIDDEN)
public open fun create(snapshotId: Int): StateRecord =
create().also { it.snapshotId = snapshotId.toSnapshotId() }
/**
* Create a new state record for the same state object and provided [snapshotId]. This allows to
* implement an optimized version of [create] to avoid accessing [currentSnapshot] when snapshot
* id is known. The default implementation provides a backwards compatible behavior, and should
* be overridden if [StateRecord] subclass supports this optimization.
*/
public open fun create(snapshotId: SnapshotId): StateRecord =
create().also { it.snapshotId = snapshotId }
}
/**
* Interface implemented by all snapshot aware state objects. Used by this module to maintain the
* state records of a state object.
*/
@JvmDefaultWithCompatibility
public interface StateObject {
/** The first state record in a linked list of state records. */
public val firstStateRecord: StateRecord
/**
* Add a new state record to the beginning of a list. After this call [firstStateRecord] should
* be [value].
*/
public 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.
*/
public 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(
snapshotId: SnapshotId,
invalid: SnapshotIdSet,
override val readObserver: ((Any) -> Unit)?,
) : Snapshot(snapshotId, 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: ((Any) -> Unit)?
get() = null
override var modified: MutableScatterSet<StateObject>?
get() = null
@Suppress("UNUSED_PARAMETER") set(value) = unsupported()
@OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot {
validateOpen(this)
return creatingSnapshot(
parent = this,
readObserver = readObserver,
writeObserver = null,
readonly = true,
) { actualReadObserver, _ ->
NestedReadonlySnapshot(
snapshotId = snapshotId,
invalid = invalid,
readObserver = mergedReadObserver(actualReadObserver, this.readObserver),
parent = this,
)
}
}
override fun notifyObjectsInitialized() {
// Nothing to do for read-only snapshots
}
override fun dispose() {
if (!disposed) {
nestedDeactivated(this)
super.dispose()
dispatchObserverOnPreDispose(this)
}
}
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.
closeAndReleasePinning()
}
}
override fun recordModified(state: StateObject) {
reportReadonlySnapshotWrite()
}
}
internal class NestedReadonlySnapshot(
snapshotId: SnapshotId,
invalid: SnapshotIdSet,
override val readObserver: ((Any) -> Unit)?,
val parent: Snapshot,
) : Snapshot(snapshotId, invalid) {
init {
parent.nestedActivated(this)
}
override val readOnly
get() = true
override val root: Snapshot
get() = parent.root
@OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?) =
creatingSnapshot(
parent = this,
readObserver = readObserver,
writeObserver = null,
readonly = true,
) { actualReadObserver, _ ->
NestedReadonlySnapshot(
snapshotId = snapshotId,
invalid = invalid,
readObserver = mergedReadObserver(actualReadObserver, this.readObserver),
parent = parent,
)
}
override fun notifyObjectsInitialized() {
// Nothing to do for read-only snapshots
}
override fun hasPendingChanges(): Boolean = false
override fun dispose() {
if (!disposed) {
if (snapshotId != parent.snapshotId) {
closeAndReleasePinning()
}
parent.nestedDeactivated(this)
super.dispose()
dispatchObserverOnPreDispose(this)
}
}
override val modified: MutableScatterSet<StateObject>?
get() = null
override val writeObserver: ((Any) -> Unit)?
get() = null
override fun recordModified(state: StateObject) = reportReadonlySnapshotWrite()
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(snapshotId: SnapshotId, invalid: SnapshotIdSet) :
MutableSnapshot(
snapshotId,
invalid,
null,
{ state -> sync { globalWriteObservers.fastForEach { it(state) } } },
) {
@OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot =
creatingSnapshot(
parent = null,
readonly = true,
readObserver = readObserver,
writeObserver = null,
) { actualReadObserver, _ ->
takeNewSnapshot { invalid ->
ReadonlySnapshot(
snapshotId = sync { nextSnapshotId.also { nextSnapshotId += 1 } },
invalid = invalid,
readObserver = actualReadObserver,
)
}
}
@OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedMutableSnapshot(
readObserver: ((Any) -> Unit)?,
writeObserver: ((Any) -> Unit)?,
): MutableSnapshot =
creatingSnapshot(
parent = null,
readonly = false,
readObserver = readObserver,
writeObserver = writeObserver,
) { actualReadObserver, actualWriteObserver ->
takeNewSnapshot { invalid ->
MutableSnapshot(
snapshotId = sync { nextSnapshotId.also { nextSnapshotId += 1 } },
invalid = invalid,
// It is intentional that the global read observers are not merged with mutable
// snapshots read observers.
readObserver = actualReadObserver,
// It is intentional that global write observers are not merged with mutable
// snapshots write observers.
writeObserver = actualWriteObserver,
)
}
}
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() {
sync { releasePinnedSnapshotLocked() }
}
}
/** A nested mutable snapshot created by [MutableSnapshot.takeNestedMutableSnapshot]. */
internal class NestedMutableSnapshot(
snapshotId: SnapshotId,
invalid: SnapshotIdSet,
readObserver: ((Any) -> Unit)?,
writeObserver: ((Any) -> Unit)?,
val parent: MutableSnapshot,
) : MutableSnapshot(snapshotId, invalid, readObserver, writeObserver) {
private var deactivated = false
init {
parent.nestedActivated(this)
}
override val root: Snapshot
get() = parent.root
override fun dispose() {
if (!disposed) {
super.dispose()
deactivate()
}
}
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 = snapshotId
val optimisticMerges =
if (modified != null) optimisticMerges(parent.snapshotId, this, parent.invalid)
else null
sync {
validateOpen(this)
if (modified == null || modified.size == 0) {
closeAndReleasePinning()
} else {
val result =
innerApplyLocked(parent.snapshotId, modified, optimisticMerges, parent.invalid)
if (result != SnapshotApplyResult.Success) return result
parent.modified?.apply { addAll(modified) }
?: modified.also {
// Ensure modified reference is only used by one snapshot
parent.modified = it
this.modified = null
}
}
// Ensure the parent is newer than the current snapshot
if (parent.snapshotId < id) {
parent.advance()
}
// 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.recordPreviousPinnedSnapshot(takeoverPinnedSnapshot())
parent.recordPreviousList(previousIds)
parent.recordPreviousPinnedSnapshots(previousPinnedSnapshots)
}
applied = true
deactivate()
dispatchObserverOnApplied(this, modified)
return SnapshotApplyResult.Success
}
private fun deactivate() {
if (!deactivated) {
deactivated = true
parent.nestedDeactivated(this)
}
}
}
/** A pseudo snapshot that doesn't introduce isolation but does introduce observers. */
internal class TransparentObserverMutableSnapshot(
private val parentSnapshot: MutableSnapshot?,
specifiedReadObserver: ((Any) -> Unit)?,
specifiedWriteObserver: ((Any) -> Unit)?,
private val mergeParentObservers: Boolean,
private val ownsParentSnapshot: Boolean,
) :
MutableSnapshot(
INVALID_SNAPSHOT,
SnapshotIdSet.EMPTY,
mergedReadObserver(
specifiedReadObserver,
parentSnapshot?.readObserver ?: globalSnapshot.readObserver,
mergeParentObservers,
),
mergedWriteObserver(
specifiedWriteObserver,
parentSnapshot?.writeObserver ?: globalSnapshot.writeObserver,
),
) {
override var readObserver: ((Any) -> Unit)? = super.readObserver
override var writeObserver: ((Any) -> Unit)? = super.writeObserver
internal val threadId: Long = currentThreadId()
private val currentSnapshot: MutableSnapshot
get() = parentSnapshot ?: globalSnapshot
override fun dispose() {
// Explicitly don't call super.dispose()
disposed = true
if (ownsParentSnapshot) {
parentSnapshot?.dispose()
}
}
override var snapshotId: SnapshotId
get() = currentSnapshot.snapshotId
@Suppress("UNUSED_PARAMETER")
set(value) {
unsupported()
}
override var invalid
get() = currentSnapshot.invalid
@Suppress("UNUSED_PARAMETER") set(value) = unsupported()
override fun hasPendingChanges(): Boolean = currentSnapshot.hasPendingChanges()
override var modified: MutableScatterSet<StateObject>?
get() = currentSnapshot.modified
@Suppress("UNUSED_PARAMETER") set(value) = unsupported()
override var writeCount: Int
get() = currentSnapshot.writeCount
set(value) {
currentSnapshot.writeCount = value
}
override val readOnly: Boolean
get() = currentSnapshot.readOnly
override fun apply(): SnapshotApplyResult = currentSnapshot.apply()
override fun recordModified(state: StateObject) = currentSnapshot.recordModified(state)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot {
val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver)
return if (!mergeParentObservers) {
createTransparentSnapshotWithNoParentReadObserver(
previousSnapshot = currentSnapshot.takeNestedSnapshot(null),
readObserver = mergedReadObserver,
ownsPreviousSnapshot = true,
)
} else {
currentSnapshot.takeNestedSnapshot(mergedReadObserver)
}
}
override fun takeNestedMutableSnapshot(
readObserver: ((Any) -> Unit)?,
writeObserver: ((Any) -> Unit)?,
): MutableSnapshot {
val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver)
val mergedWriteObserver = mergedWriteObserver(writeObserver, this.writeObserver)
return if (!mergeParentObservers) {
val nestedSnapshot =
currentSnapshot.takeNestedMutableSnapshot(
readObserver = null,
writeObserver = mergedWriteObserver,
)
TransparentObserverMutableSnapshot(
parentSnapshot = nestedSnapshot,
specifiedReadObserver = mergedReadObserver,
specifiedWriteObserver = mergedWriteObserver,
mergeParentObservers = false,
ownsParentSnapshot = true,
)
} else {
currentSnapshot.takeNestedMutableSnapshot(mergedReadObserver, mergedWriteObserver)
}
}
override fun notifyObjectsInitialized() = currentSnapshot.notifyObjectsInitialized()
/** Should never be called. */
override fun nestedActivated(snapshot: Snapshot) = unsupported()
override fun nestedDeactivated(snapshot: Snapshot) = unsupported()
}
/** A pseudo snapshot that doesn't introduce isolation but does introduce observers. */
internal class TransparentObserverSnapshot(
private val parentSnapshot: Snapshot?,
specifiedReadObserver: ((Any) -> Unit)?,
private val mergeParentObservers: Boolean,
private val ownsParentSnapshot: Boolean,
) : Snapshot(INVALID_SNAPSHOT, SnapshotIdSet.EMPTY) {
override var readObserver: ((Any) -> Unit)? =
mergedReadObserver(
specifiedReadObserver,
parentSnapshot?.readObserver ?: globalSnapshot.readObserver,
mergeParentObservers,
)
override val writeObserver: ((Any) -> Unit)? = null
internal val threadId: Long = currentThreadId()
override val root: Snapshot = this
private val currentSnapshot: Snapshot
get() = parentSnapshot ?: globalSnapshot
override fun dispose() {
// Explicitly don't call super.dispose()
disposed = true
if (ownsParentSnapshot) {
parentSnapshot?.dispose()
}
}
override var snapshotId: SnapshotId
get() = currentSnapshot.snapshotId
@Suppress("UNUSED_PARAMETER")
set(value) {
unsupported()
}
override var invalid
get() = currentSnapshot.invalid
@Suppress("UNUSED_PARAMETER") set(value) = unsupported()
override fun hasPendingChanges(): Boolean = currentSnapshot.hasPendingChanges()
override var modified: MutableScatterSet<StateObject>?
get() = currentSnapshot.modified
@Suppress("UNUSED_PARAMETER") set(value) = unsupported()
override val readOnly: Boolean
get() = currentSnapshot.readOnly
override fun recordModified(state: StateObject) = currentSnapshot.recordModified(state)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot {
val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver)
return if (!mergeParentObservers) {
createTransparentSnapshotWithNoParentReadObserver(
currentSnapshot.takeNestedSnapshot(null),
mergedReadObserver,
ownsPreviousSnapshot = true,
)
} else {
currentSnapshot.takeNestedSnapshot(mergedReadObserver)
}
}
override fun notifyObjectsInitialized() = currentSnapshot.notifyObjectsInitialized()
/** Should never be called. */
override fun nestedActivated(snapshot: Snapshot) = unsupported()
override fun nestedDeactivated(snapshot: Snapshot) = unsupported()
}
private fun createTransparentSnapshotWithNoParentReadObserver(
previousSnapshot: Snapshot?,
readObserver: ((Any) -> Unit)? = null,
ownsPreviousSnapshot: Boolean = false,
): Snapshot =
if (previousSnapshot is MutableSnapshot || previousSnapshot == null) {
TransparentObserverMutableSnapshot(
parentSnapshot = previousSnapshot as? MutableSnapshot,
specifiedReadObserver = readObserver,
specifiedWriteObserver = null,
mergeParentObservers = false,
ownsParentSnapshot = ownsPreviousSnapshot,
)
} else {
TransparentObserverSnapshot(
parentSnapshot = previousSnapshot,
specifiedReadObserver = readObserver,
mergeParentObservers = false,
ownsParentSnapshot = ownsPreviousSnapshot,
)
}
internal fun mergedReadObserver(
readObserver: ((Any) -> Unit)?,
parentObserver: ((Any) -> Unit)?,
mergeReadObserver: Boolean = true,
): ((Any) -> Unit)? {
@Suppress("NAME_SHADOWING") val parentObserver = if (mergeReadObserver) parentObserver else null
return if (readObserver != null && parentObserver != null && readObserver !== parentObserver) {
{ state: Any ->
readObserver(state)
parentObserver(state)
}
} else readObserver ?: parentObserver
}
internal fun mergedWriteObserver(
writeObserver: ((Any) -> Unit)?,
parentObserver: ((Any) -> Unit)?,
): ((Any) -> Unit)? =
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 val INVALID_SNAPSHOT = SnapshotIdZero
/** Current thread snapshot */
private val threadSnapshot = SnapshotThreadLocal<Snapshot>()
/**
* A global synchronization object. This synchronization object should be taken before modifying any
* of the fields below.
*/
@PublishedApi internal val lock: SynchronizedObject = makeSynchronizedObject()
@Suppress("BanInlineOptIn", "LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
@OptIn(ExperimentalContracts::class)
@PublishedApi
internal inline fun <T> sync(block: () -> T): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return 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 [Snapshot.PreexistingSnapshotId] */
private var nextSnapshotId = Snapshot.PreexistingSnapshotId.toSnapshotId() + 1
/**
* A tracking table for pinned snapshots. A pinned snapshot is the lowest snapshot id that the
* snapshot is ignoring by considering them invalid. This is used to calculate when a snapshot
* record can be reused.
*/
private val pinningTable = SnapshotDoubleIndexHeap()
/**
* The set of objects who have more than one active state record. These are traversed during apply
* of mutable snapshots and when the global snapshot is advanced to determine if any of the records
* can be cleared.
*/
private val extraStateObjects = SnapshotWeakSet<StateObject>()
/** A list of apply observers */
private var applyObservers = emptyList<(Set<Any>, Snapshot) -> Unit>()
/** A list of observers of writes to the global state. */
private var globalWriteObservers = emptyList<(Any) -> Unit>()
private val globalSnapshot =
GlobalSnapshot(
snapshotId = nextSnapshotId.also { nextSnapshotId += 1 },
invalid = SnapshotIdSet.EMPTY,
)
.also { openSnapshots = openSnapshots.set(it.snapshotId) }
// Unused, kept for API compat
@Suppress("unused") @PublishedApi internal val snapshotInitializer: Snapshot = globalSnapshot
private fun <T> resetGlobalSnapshotLocked(
globalSnapshot: GlobalSnapshot,
block: (invalid: SnapshotIdSet) -> T,
): T {
val snapshotId = globalSnapshot.snapshotId
val result = block(openSnapshots.clear(snapshotId))
val nextGlobalSnapshotId = nextSnapshotId
nextSnapshotId += 1
openSnapshots = openSnapshots.clear(snapshotId)
globalSnapshot.snapshotId = nextGlobalSnapshotId
globalSnapshot.invalid = openSnapshots
globalSnapshot.writeCount = 0
globalSnapshot.modified = null
globalSnapshot.releasePinnedSnapshotLocked()
openSnapshots = openSnapshots.set(nextGlobalSnapshotId)
return result
}
/**
* Counts the number of threads currently inside `advanceGlobalSnapshot`, notifying observers of
* changes to the global snapshot.
*/
private var pendingApplyObserverCount = AtomicInt(0)
private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
val globalSnapshot = globalSnapshot
val modified: MutableScatterSet<StateObject>?
val result = sync {
modified = globalSnapshot.modified
if (modified != null) {
pendingApplyObserverCount.add(1)
}
resetGlobalSnapshotLocked(globalSnapshot, block)
}
// If the previous global snapshot had any modified states then notify the registered apply
// observers.
modified?.let {
try {
val observers = applyObservers
val modifiedSet = it.wrapIntoSet()
verboseTrace("Compose:applyObservers") {
observers.fastForEach { observer -> observer(modifiedSet, globalSnapshot) }
}
} finally {
pendingApplyObserverCount.add(-1)
}
}
sync {
checkAndOverwriteUnusedRecordsLocked()
modified?.forEach { processForUnusedRecordsLocked(it) }
}
return result
}
private fun advanceGlobalSnapshot() = advanceGlobalSnapshot(emptyLambda)
private fun <T : Snapshot> takeNewSnapshot(block: (invalid: SnapshotIdSet) -> T): T =
advanceGlobalSnapshot { invalid ->
val result = block(invalid)
sync { openSnapshots = openSnapshots.set(result.snapshotId) }
result
}
private fun validateOpen(snapshot: Snapshot) {
val openSnapshots = openSnapshots
if (!openSnapshots.get(snapshot.snapshotId)) {
error(
"Snapshot is not open: snapshotId=${
snapshot.snapshotId
}, disposed=${
snapshot.disposed
}, applied=${
(snapshot as? MutableSnapshot)?.applied ?: "read-only"
}, lowestPin=${
sync { pinningTable.lowestOrDefault(SnapshotIdInvalidValue) }
}"
)
}
}
/**
* 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.
*/
private fun valid(
currentSnapshot: SnapshotId,
candidateSnapshot: SnapshotId,
invalid: SnapshotIdSet,
): Boolean {
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: SnapshotId, invalid: SnapshotIdSet): Boolean {
return valid(snapshot, data.snapshotId, invalid)
}
private fun <T : StateRecord> readable(r: T, id: SnapshotId, 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]
*/
public fun <T : StateRecord> T.readable(state: StateObject): T {
val snapshot = Snapshot.current
snapshot.readObserver?.invoke(state)
return readable(this, snapshot.snapshotId, snapshot.invalid)
?: sync {
// Readable can return null when the global snapshot has been advanced by another thread
// and state written to the object was overwritten while this thread was paused.
// Repeating the read is valid here as either this will return the same result as
// the previous call or will find a valid record. Being in a sync block prevents other
// threads from writing to this state object until the read completes.
val syncSnapshot = Snapshot.current
@Suppress("UNCHECKED_CAST")
readable(state.firstStateRecord as T, syncSnapshot.snapshotId, syncSnapshot.invalid)
?: readError()
}
}
// unused, still here for API compat.
/**
* Return the current readable state record for the [snapshot]. It is assumed that [this] is the
* first record of [state]
*/
public 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.snapshotId, snapshot.invalid)
?: sync {
// Readable can return null when the global snapshot has been advanced by another thread
// See T.readable(state: StateObject) for more info.
val syncSnapshot = Snapshot.current
@Suppress("UNCHECKED_CAST")
readable(state.firstStateRecord as T, syncSnapshot.snapshotId, syncSnapshot.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 usedLocked(state: StateObject): StateRecord? {
var current: StateRecord? = state.firstStateRecord
var validRecord: StateRecord? = null
val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) - 1
val invalid = SnapshotIdSet.EMPTY
while (current != null) {
val currentId = current.snapshotId
if (currentId == INVALID_SNAPSHOT) {
// Any records that were marked invalid by an abandoned snapshot or is marked reachable
// can be used immediately.
return current
}
if (valid(current, reuseLimit, 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
}
/**
* Clear records that cannot be selected in any currently open snapshot.
*
* This method uses the same technique as [usedLocked] which uses the [pinningTable] to determine
* lowest id in the invalid set for all snapshots. Only the record with the greatest id of all
* records less or equal to this lowest id can possibly be selected in any snapshot and all other
* records below that number can be overwritten.
*
* However, this technique doesn't find all records that will not be selected by any open snapshot
* as a record that has an id above that number could be reusable but will not be found.
*
* For example if snapshot 1 is open and 2 is created and modifies [state] then is applied, 3 is
* open and then 4 is open, and then 1 is applied. When 3 modifies [state] and then applies, as 1 is
* pinned by 4, it is uncertain whether the record for 2 is needed by 4 so it must be kept even if 4
* also modified [state] and would not select 2. Accurately determine if a record is selectable
* would require keeping a list of all open [Snapshot] instances which currently is not kept and
* traversing that list for each record.
*
* If any such records are possible this method returns true. In other words, this method returns
* true if any records might be reusable but this function could not prove there were or not.
*/
private fun overwriteUnusedRecordsLocked(state: StateObject): Boolean {
var current: StateRecord? = state.firstStateRecord
var overwriteRecord: StateRecord? = null
var validRecord: StateRecord? = null
val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId)
var retainedRecords = 0
while (current != null) {
val currentId = current.snapshotId
if (currentId != INVALID_SNAPSHOT) {
if (currentId < reuseLimit) {
if (validRecord == null) {
// If any records are below [reuseLimit] then we must keep the highest one
// so the lowest snapshot can select it.
validRecord = current
retainedRecords++
} else {
// If [validRecord] is from an earlier snapshot, overwrite it instead
val recordToOverwrite =
if (current.snapshotId < validRecord.snapshotId) {
current
} else {
// We cannot use `.also { }` here as it prevents smart casting of other
// uses of [validRecord].
val result = validRecord
validRecord = current
result
}
if (overwriteRecord == null) {
// Find a record we will definitely keep
overwriteRecord =
state.firstStateRecord.findYoungestOr { it.snapshotId >= reuseLimit }
}
recordToOverwrite.snapshotId = INVALID_SNAPSHOT
recordToOverwrite.assign(overwriteRecord)
}
} else {
retainedRecords++
}
}
current = current.next
}
return retainedRecords > 1
}
private inline fun StateRecord.findYoungestOr(predicate: (StateRecord) -> Boolean): StateRecord {
var current: StateRecord? = this
var youngest = this
while (current != null) {
if (predicate(current)) return current
if (youngest.snapshotId < current.snapshotId) youngest = current
current = current.next
}
return youngest
}
private fun checkAndOverwriteUnusedRecordsLocked() {
extraStateObjects.removeIf { !overwriteUnusedRecordsLocked(it) }
}
private fun processForUnusedRecordsLocked(state: StateObject) {
if (overwriteUnusedRecordsLocked(state)) {
extraStateObjects.add(state)
}
}
@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.snapshotId
val readData =
readable(this, id, snapshot.invalid)
?: sync {
// If a state record is prepended by another thread and then
// [overwriteUnusedRecordsLocked] is called by another thread before this thread
// reaches the `readable` call above, the call will return null. When the call
// returns null, we fall back to making the `readable` call in a [sync] block,
// ensuring that the head of the state record list is passed as the first argument.
// The fallback call is valid as it will either return the same result as the
// previous call or find a valid record.
val syncSnapshot = Snapshot.current
@Suppress("UNCHECKED_CAST")
readable(state.firstStateRecord as T, syncSnapshot.snapshotId, syncSnapshot.invalid)
?: readError()
}
// If the readable data was born in this snapshot, it is writable.
if (readData.snapshotId == snapshot.snapshotId) return readData
// Otherwise, make a copy of the readable data and mark it as born in this snapshot, making it
// writable.
@Suppress("UNCHECKED_CAST")
val newData =
sync {
// Verify that some other thread didn't already create this.
val newReadData = readable(state.firstStateRecord, id, snapshot.invalid) ?: readError()
if (newReadData.snapshotId == id) newReadData
else newReadData.newWritableRecordLocked(state, snapshot)
}
as T
snapshot.recordModified(state)
return newData
}
internal fun <T : StateRecord> T.overwritableRecord(
state: StateObject,
snapshot: Snapshot,
candidate: T,
): T {
if (snapshot.readOnly) {
// If the snapshot is read-only, use the snapshot recordModified to report it.
snapshot.recordModified(state)
}
val id = snapshot.snapshotId
if (candidate.snapshotId == id) return candidate
val newData = sync { newOverwritableRecordLocked(state) }
newData.snapshotId = id
snapshot.recordModified(state)
return newData
}
internal fun <T : StateRecord> T.newWritableRecord(state: StateObject, snapshot: Snapshot) = sync {
newWritableRecordLocked(state, snapshot)
}
private fun <T : StateRecord> T.newWritableRecordLocked(state: StateObject, snapshot: Snapshot): T {
// 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().
val newData = newOverwritableRecordLocked(state)
newData.assign(this)
newData.snapshotId = snapshot.snapshotId
return newData
}
internal fun <T : StateRecord> T.newOverwritableRecordLocked(state: StateObject): T {
// 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")
return (usedLocked(state) as T?)?.apply { snapshotId = SnapshotIdMax }
?: create(SnapshotIdMax).apply {
this.next = state.firstStateRecord
state.prependStateRecord(this as T)
} as T
}
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeCount += 1
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.
*/
public 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.
*/
public inline fun <T : StateRecord, R> T.writable(state: StateObject, block: T.() -> R): R {
val snapshot: Snapshot
return sync {
snapshot = Snapshot.current
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. This should only be used when the record will be overwritten in its
* entirety (such as having only one field and that field is written to).
*
* WARNING: If the caller doesn't overwrite all the fields in the state record the object will be
* inconsistent and the fields not written are almost guaranteed to be incorrect. If it is possible
* that [block] will not write to all the fields use [writable] instead.
*
* @param state The object that has this record in its record list.
* @param candidate The current for the snapshot record returned by [withCurrent]
* @param block The block that will mutate all the field of the record.
*/
internal inline fun <T : StateRecord, R> T.overwritable(
state: StateObject,
candidate: T,
block: T.() -> R,
): R {
val snapshot: Snapshot
return sync {
snapshot = Snapshot.current
this.overwritableRecord(state, snapshot, candidate).block()
}
.also { notifyWrite(snapshot, state) }
}
/**
* 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.
*
* How sets and ids are used to determine a merged record is explained in
* [MutableSnapshot.innerApplyLocked].
*
* @see MutableSnapshot.innerApplyLocked
*/
private fun optimisticMerges(
currentSnapshotId: SnapshotId,
applyingSnapshot: MutableSnapshot,
invalidSnapshots: SnapshotIdSet,
): Map<StateRecord, StateRecord>? {
val modified = applyingSnapshot.modified
if (modified == null) return null
val applyingSnapshotId = applyingSnapshot.snapshotId
val start = applyingSnapshot.invalid.set(applyingSnapshotId).or(applyingSnapshot.previousIds)
var result: MutableMap<StateRecord, StateRecord>? = null
modified.forEach { state ->
val first = state.firstStateRecord
val current = readable(first, currentSnapshotId, invalidSnapshots) ?: return@forEach
val previous = readable(first, applyingSnapshotId, start) ?: return@forEach
if (current != previous) {
// Try to produce a merged state record
val applied =
readable(first, applyingSnapshotId, 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
}
private fun reportReadonlySnapshotWrite(): Nothing {
error("Cannot modify a state object in a read-only snapshot")
}
/** Returns the current record without notifying any read observers. */
@PublishedApi
internal fun <T : StateRecord> current(r: T, snapshot: Snapshot): T =
readable(r, snapshot.snapshotId, snapshot.invalid)
?: sync {
// Global snapshot could have been advanced
// see StateRecord.readable for more details
readable(r, snapshot.snapshotId, snapshot.invalid)
}
?: readError()
@PublishedApi
internal fun <T : StateRecord> current(r: T): T =
Snapshot.current.let { snapshot ->
readable(r, snapshot.snapshotId, snapshot.invalid)
?: sync {
// Global snapshot could have been advanced
// see StateRecord.readable for more details
Snapshot.current.let { syncSnapshot ->
readable(r, syncSnapshot.snapshotId, syncSnapshot.invalid)
}
}
?: readError()
}
@PublishedApi
internal fun <T : StateRecord> current(r: T, state: StateObject): T =
Snapshot.current.let { snapshot ->
readable(r, snapshot.snapshotId, snapshot.invalid)
?: sync {
Snapshot.current.let { syncSnapshot ->
@Suppress("UNCHECKED_CAST")
readable(
state.firstStateRecord as T,
syncSnapshot.snapshotId,
syncSnapshot.invalid,
)
}
}
?: readError()
}
/**
* Provides a [block] with the current record, without notifying any read observers.
*
* @see readable
*/
@Deprecated(
"Use the overload that has a StateObject parameter instead; for example, " +
"next.withCurrent(this) { ... }"
)
public inline fun <T : StateRecord, R> T.withCurrent(block: (r: T) -> R): R = block(current(this))
/**
* Provides a [block] with the current record, without notifying any read observers.
*
* @param state the state object for which the receiver is a state record. It is assumed that [this]
* is the first record of [state] (e.g. `next.withCurrent(this) { ... }`).
* @param block a block to be evaluated with the current state record as its parameter. The result
* of [block] is the result of [withCurrent]. It is expected, but not required, that the result of
* block is either [Unit] or derives it value from the content of the state record.
* @return the result returned by the [block] lambda.
* @see readable
*/
public inline fun <T : StateRecord, R> T.withCurrent(state: StateObject, block: (r: T) -> R): R =
block(current(this, state))
/** Helper routine to add a range of values ot a snapshot set */
internal fun SnapshotIdSet.addRange(from: SnapshotId, until: SnapshotId): SnapshotIdSet {
var result = this
var invalidId = from
while (invalidId < until) {
result = result.set(invalidId)
invalidId += 1
}
return result
}