| /* |
| * 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 |
| |
| import androidx.collection.MutableObjectList |
| import androidx.collection.MutableScatterSet |
| import androidx.collection.ScatterSet |
| import androidx.collection.emptyObjectList |
| import androidx.collection.emptyScatterSet |
| import androidx.collection.mutableScatterMapOf |
| import androidx.collection.mutableScatterSetOf |
| import androidx.compose.runtime.collection.MultiValueMap |
| import androidx.compose.runtime.collection.fastForEach |
| import androidx.compose.runtime.collection.fastMap |
| import androidx.compose.runtime.collection.mutableVectorOf |
| import androidx.compose.runtime.collection.wrapIntoSet |
| import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf |
| import androidx.compose.runtime.internal.AtomicReference |
| import androidx.compose.runtime.internal.SnapshotThreadLocal |
| import androidx.compose.runtime.internal.logError |
| import androidx.compose.runtime.internal.trace |
| import androidx.compose.runtime.platform.SynchronizedObject |
| import androidx.compose.runtime.platform.makeSynchronizedObject |
| import androidx.compose.runtime.platform.synchronized |
| import androidx.compose.runtime.snapshots.MutableSnapshot |
| import androidx.compose.runtime.snapshots.ReaderKind |
| import androidx.compose.runtime.snapshots.Snapshot |
| import androidx.compose.runtime.snapshots.SnapshotApplyResult |
| import androidx.compose.runtime.snapshots.StateObjectImpl |
| import androidx.compose.runtime.snapshots.TransparentObserverMutableSnapshot |
| import androidx.compose.runtime.snapshots.TransparentObserverSnapshot |
| import androidx.compose.runtime.snapshots.fastAll |
| import androidx.compose.runtime.snapshots.fastAny |
| import androidx.compose.runtime.snapshots.fastFilterIndexed |
| import androidx.compose.runtime.snapshots.fastForEach |
| import androidx.compose.runtime.snapshots.fastGroupBy |
| import androidx.compose.runtime.snapshots.fastMap |
| import androidx.compose.runtime.snapshots.fastMapNotNull |
| import androidx.compose.runtime.tooling.ComposeStackTraceMode |
| import androidx.compose.runtime.tooling.ComposeToolingApi |
| import androidx.compose.runtime.tooling.CompositionData |
| import androidx.compose.runtime.tooling.CompositionObserverHandle |
| import androidx.compose.runtime.tooling.CompositionRegistrationObserver |
| import androidx.compose.runtime.tooling.ObservableComposition |
| import androidx.compose.runtime.tooling.observe |
| import kotlin.collections.removeLast as removeLastKt |
| import kotlin.coroutines.Continuation |
| import kotlin.coroutines.CoroutineContext |
| import kotlin.coroutines.coroutineContext |
| import kotlin.coroutines.resume |
| import kotlin.native.concurrent.ThreadLocal |
| import kotlinx.coroutines.CancellableContinuation |
| import kotlinx.coroutines.CancellationException |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.Job |
| import kotlinx.coroutines.coroutineScope |
| import kotlinx.coroutines.flow.Flow |
| import kotlinx.coroutines.flow.MutableStateFlow |
| import kotlinx.coroutines.flow.StateFlow |
| import kotlinx.coroutines.flow.collect |
| import kotlinx.coroutines.flow.first |
| import kotlinx.coroutines.flow.takeWhile |
| import kotlinx.coroutines.job |
| import kotlinx.coroutines.launch |
| import kotlinx.coroutines.suspendCancellableCoroutine |
| import kotlinx.coroutines.withContext |
| |
| internal const val recomposerKey = 1000 |
| |
| // TODO: Can we use rootKey for this since all compositions will have an eventual Recomposer parent? |
| private inline val RecomposerCompoundHashKey |
| get() = CompositeKeyHashCode(recomposerKey) |
| |
| /** |
| * Runs [block] with a new, active [Recomposer] applying changes in the calling [CoroutineContext]. |
| * The [Recomposer] will be [closed][Recomposer.close] after [block] returns. |
| * [withRunningRecomposer] will return once the [Recomposer] is [Recomposer.State.ShutDown] and all |
| * child jobs launched by [block] have [joined][Job.join]. |
| */ |
| public suspend fun <R> withRunningRecomposer( |
| block: suspend CoroutineScope.(recomposer: Recomposer) -> R |
| ): R = coroutineScope { |
| val recomposer = Recomposer(coroutineContext) |
| // Will be cancelled when recomposerJob cancels |
| launch { recomposer.runRecomposeAndApplyChanges() } |
| block(recomposer).also { |
| recomposer.close() |
| recomposer.join() |
| } |
| } |
| |
| /** |
| * Read-only information about a [Recomposer]. Used when code should only monitor the activity of a |
| * [Recomposer], and not attempt to alter its state or create new compositions from it. |
| */ |
| public interface RecomposerInfo { |
| /** The current [State] of the [Recomposer]. See each [State] value for its meaning. */ |
| // TODO: Mirror the currentState/StateFlow API change here once we can safely add |
| // default interface methods. https://youtrack.jetbrains.com/issue/KT-47000 |
| public val state: Flow<Recomposer.State> |
| |
| /** |
| * `true` if the [Recomposer] has been assigned work to do and it is currently performing that |
| * work or awaiting an opportunity to do so. |
| */ |
| public val hasPendingWork: Boolean |
| |
| /** |
| * The running count of the number of times the [Recomposer] awoke and applied changes to one or |
| * more [Composer]s. This count is unaffected if the composer awakes and recomposed but |
| * composition did not produce changes to apply. |
| */ |
| public val changeCount: Long |
| |
| /** |
| * Get flow of error states captured in composition. This flow is only available when recomposer |
| * is in hot reload mode. |
| * |
| * @return a flow of error states captured during composition |
| */ |
| @ComposeToolingApi |
| public val errorState: StateFlow<RecomposerErrorInformation?> |
| get() = DefaultErrorStateFlow |
| |
| /** |
| * Register an observer to be notified when a composition is added to or removed from the given |
| * [Recomposer]. When this method is called, the observer will be notified of all currently |
| * registered compositions per the documentation in |
| * [CompositionRegistrationObserver.onCompositionRegistered]. |
| */ |
| @ExperimentalComposeRuntimeApi |
| public fun observe(observer: CompositionRegistrationObserver): CompositionObserverHandle? = null |
| |
| private companion object { |
| @ComposeToolingApi |
| private val DefaultErrorStateFlow: StateFlow<RecomposerErrorInformation?> = |
| MutableStateFlow(null) |
| } |
| } |
| |
| /** Read only information about [Recomposer] error state. */ |
| @ComposeToolingApi |
| public interface RecomposerErrorInformation { |
| /** Exception which forced recomposition to halt. */ |
| public val cause: Throwable |
| |
| /** |
| * Whether composition can recover from the error by itself. If the error is not recoverable, |
| * recomposer will not react to invalidate calls until state is reloaded. |
| */ |
| public val isRecoverable: Boolean |
| } |
| |
| /** |
| * Read only information about [Recomposer] error state. This is an internal API only kept for |
| * backward compatibility. |
| */ |
| // TODO(b/469471141): Remove when Live Edit no longer depends on this API. |
| @InternalComposeApi |
| internal interface RecomposerErrorInfo { |
| /** Exception which forced recomposition to halt. */ |
| val cause: Throwable |
| |
| /** |
| * Whether composition can recover from the error by itself. If the error is not recoverable, |
| * recomposer will not react to invalidate calls until state is reloaded. |
| */ |
| val recoverable: Boolean |
| } |
| |
| /** |
| * The scheduler for performing recomposition and applying updates to one or more [Composition]s. |
| */ |
| // RedundantVisibilityModifier suppressed because metalava picks up internal function overrides |
| // if 'internal' is not explicitly specified - b/171342041 |
| // NotCloseable suppressed because this is Kotlin-only common code; [Auto]Closeable not available. |
| @Suppress("RedundantVisibilityModifier", "NotCloseable") |
| @OptIn(InternalComposeApi::class) |
| public class Recomposer(effectCoroutineContext: CoroutineContext) : CompositionContext() { |
| /** |
| * This is a running count of the number of times the recomposer awoke and applied changes to |
| * one or more composers. This count is unaffected if the composer awakes and recomposed but |
| * composition did not produce changes to apply. |
| */ |
| public var changeCount: Long = 0L |
| private set |
| |
| private val broadcastFrameClock = BroadcastFrameClock { onNewFrameAwaiter() } |
| private val nextFrameEndCallbackQueue = NextFrameEndCallbackQueue { onNewFrameAwaiter() } |
| |
| /** Valid operational states of a [Recomposer]. */ |
| public enum class State { |
| /** |
| * [cancel] was called on the [Recomposer] and all cleanup work has completed. The |
| * [Recomposer] is no longer available for use. |
| */ |
| ShutDown, |
| |
| /** |
| * [cancel] was called on the [Recomposer] and it is no longer available for use. Cleanup |
| * work has not yet been fully completed and composition effect coroutines may still be |
| * running. |
| */ |
| ShuttingDown, |
| |
| /** |
| * The [Recomposer] is not tracking invalidations for known composers and it will not |
| * recompose them in response to changes. Call [runRecomposeAndApplyChanges] to await and |
| * perform work. This is the initial state of a newly constructed [Recomposer]. |
| */ |
| Inactive, |
| |
| /** |
| * The [Recomposer] is [Inactive] but at least one effect associated with a managed |
| * composition is awaiting a frame. This frame will not be produced until the [Recomposer] |
| * is [running][runRecomposeAndApplyChanges]. |
| */ |
| InactivePendingWork, |
| |
| /** |
| * The [Recomposer] is tracking composition and snapshot invalidations but there is |
| * currently no work to do. |
| */ |
| Idle, |
| |
| /** |
| * The [Recomposer] has been notified of pending work it must perform and is either actively |
| * performing it or awaiting the appropriate opportunity to perform it. This work may |
| * include invalidated composers that must be recomposed, snapshot state changes that must |
| * be presented to known composers to check for invalidated compositions, or coroutines |
| * awaiting a frame using the Recomposer's [MonotonicFrameClock]. |
| */ |
| PendingWork, |
| } |
| |
| private val stateLock = makeSynchronizedObject() |
| |
| // Begin properties guarded by stateLock |
| private var runnerJob: Job? = null |
| private var closeCause: Throwable? = null |
| private val _knownCompositions = mutableListOf<ControlledComposition>() |
| private var _knownCompositionsCache: List<ControlledComposition>? = null |
| private var snapshotInvalidations = MutableScatterSet<Any>() |
| private val compositionInvalidations = mutableVectorOf<ControlledComposition>() |
| private val compositionsAwaitingApply = mutableListOf<ControlledComposition>() |
| private val movableContentAwaitingInsert = mutableListOf<MovableContentStateReference>() |
| private val movableContentRemoved = |
| MultiValueMap<MovableContent<Any?>, MovableContentStateReference>() |
| private val movableContentNestedStatesAvailable = NestedContentMap() |
| private val movableContentStatesAvailable = |
| mutableScatterMapOf<MovableContentStateReference, MovableContentState>() |
| private val movableContentNestedExtractionsPending = |
| MultiValueMap<MovableContentStateReference, MovableContentStateReference>() |
| private var failedCompositions: MutableList<ControlledComposition>? = null |
| private var compositionsRemoved: MutableScatterSet<ControlledComposition>? = null |
| private var workContinuation: CancellableContinuation<Unit>? = null |
| private var concurrentCompositionsOutstanding = 0 |
| private var isClosed: Boolean = false |
| private var errorState = MutableStateFlow<RecomposerErrorState?>(null) |
| |
| private var frameClockPaused: Boolean = false |
| // End properties guarded by stateLock |
| |
| private val _state = MutableStateFlow(State.Inactive) |
| private val pausedScopes = SnapshotThreadLocal<MutableScatterSet<RecomposeScopeImpl>?>() |
| |
| /** |
| * A [Job] used as a parent of any effects created by this [Recomposer]'s compositions. Its |
| * cleanup is used to advance to [State.ShuttingDown] or [State.ShutDown]. |
| * |
| * Initialized after other state above, since it is possible for [Job.invokeOnCompletion] to run |
| * synchronously during construction if the [Recomposer] is constructed with a completed or |
| * cancelled [Job]. |
| */ |
| private val effectJob = |
| Job(effectCoroutineContext[Job]).apply { |
| invokeOnCompletion { throwable -> |
| // Since the running recompose job is operating in a disjoint job if present, |
| // kick it out and make sure no new ones start if we have one. |
| val cancellation = |
| CancellationException("Recomposer effect job completed", throwable) |
| |
| var continuationToResume: CancellableContinuation<Unit>? = null |
| synchronized(stateLock) { |
| val runnerJob = runnerJob |
| if (runnerJob != null) { |
| _state.value = State.ShuttingDown |
| // If the recomposer is closed we will let the runnerJob return from |
| // runRecomposeAndApplyChanges normally and consider ourselves shut down |
| // immediately. |
| if (!isClosed) { |
| // This is the job hosting frameContinuation; no need to resume it |
| // otherwise |
| runnerJob.cancel(cancellation) |
| } else if (workContinuation != null) { |
| continuationToResume = workContinuation |
| } |
| workContinuation = null |
| runnerJob.invokeOnCompletion { runnerJobCause -> |
| synchronized(stateLock) { |
| closeCause = |
| throwable?.apply { |
| runnerJobCause |
| ?.takeIf { it !is CancellationException } |
| ?.let { addSuppressed(it) } |
| } |
| _state.value = State.ShutDown |
| } |
| } |
| } else { |
| closeCause = cancellation |
| _state.value = State.ShutDown |
| } |
| } |
| continuationToResume?.resume(Unit) |
| } |
| } |
| |
| /** The [effectCoroutineContext] is derived from the parameter of the same name. */ |
| override val effectCoroutineContext: CoroutineContext = |
| effectCoroutineContext + broadcastFrameClock + effectJob |
| |
| private val hasBroadcastFrameClockAwaitersLocked: Boolean |
| get() = !frameClockPaused && broadcastFrameClock.hasAwaiters |
| |
| private val hasNextFrameEndAwaitersLocked: Boolean |
| get() = !frameClockPaused && nextFrameEndCallbackQueue.hasAwaiters |
| |
| private val hasBroadcastFrameClockAwaiters: Boolean |
| get() = synchronized(stateLock) { hasBroadcastFrameClockAwaitersLocked } |
| |
| @OptIn(ExperimentalComposeRuntimeApi::class) |
| private var registrationObservers: MutableObjectList<CompositionRegistrationObserver>? = null |
| |
| /** |
| * Determine the new value of [_state]. Call only while locked on [stateLock]. If it returns a |
| * continuation, that continuation should be resumed after releasing the lock. |
| */ |
| private fun deriveStateLocked(): CancellableContinuation<Unit>? { |
| if (_state.value <= State.ShuttingDown) { |
| clearKnownCompositionsLocked() |
| snapshotInvalidations = MutableScatterSet() |
| compositionInvalidations.clear() |
| compositionsAwaitingApply.clear() |
| movableContentAwaitingInsert.clear() |
| failedCompositions = null |
| workContinuation?.cancel() |
| workContinuation = null |
| errorState.value = null |
| return null |
| } |
| |
| val newState = |
| when { |
| errorState.value != null -> { |
| State.Inactive |
| } |
| runnerJob == null -> { |
| snapshotInvalidations = MutableScatterSet() |
| compositionInvalidations.clear() |
| if (hasBroadcastFrameClockAwaitersLocked || hasNextFrameEndAwaitersLocked) |
| State.InactivePendingWork |
| else State.Inactive |
| } |
| compositionInvalidations.isNotEmpty() || |
| snapshotInvalidations.isNotEmpty() || |
| compositionsAwaitingApply.isNotEmpty() || |
| movableContentAwaitingInsert.isNotEmpty() || |
| concurrentCompositionsOutstanding > 0 || |
| hasBroadcastFrameClockAwaitersLocked || |
| hasNextFrameEndAwaitersLocked || |
| movableContentRemoved.isNotEmpty() -> State.PendingWork |
| else -> State.Idle |
| } |
| |
| _state.value = newState |
| return if (newState == State.PendingWork) { |
| workContinuation.also { workContinuation = null } |
| } else null |
| } |
| |
| private fun onNewFrameAwaiter() { |
| synchronized(stateLock) { |
| deriveStateLocked().also { |
| if (_state.value <= State.ShuttingDown) |
| throw CancellationException( |
| "Recomposer shutdown; frame clock awaiter will never resume", |
| closeCause, |
| ) |
| } |
| } |
| ?.resume(Unit) |
| } |
| |
| /** `true` if there is still work to do for an active caller of [runRecomposeAndApplyChanges] */ |
| private val shouldKeepRecomposing: Boolean |
| get() = synchronized(stateLock) { !isClosed } || effectJob.children.any { it.isActive } |
| |
| /** The current [State] of this [Recomposer]. See each [State] value for its meaning. */ |
| @Deprecated("Replaced by currentState as a StateFlow", ReplaceWith("currentState")) |
| public val state: Flow<State> |
| get() = currentState |
| |
| /** The current [State] of this [Recomposer], available synchronously. */ |
| public val currentState: StateFlow<State> |
| get() = _state |
| |
| // A separate private object to avoid the temptation of casting a RecomposerInfo |
| // to a Recomposer if Recomposer itself were to implement RecomposerInfo. |
| private inner class RecomposerInfoImpl : RecomposerInfo { |
| override val state: Flow<State> |
| get() = this@Recomposer.currentState |
| |
| override val hasPendingWork: Boolean |
| get() = this@Recomposer.hasPendingWork |
| |
| override val changeCount: Long |
| get() = this@Recomposer.changeCount |
| |
| @ComposeToolingApi |
| override val errorState: StateFlow<RecomposerErrorInformation?> |
| get() = this@Recomposer.errorState |
| |
| @ComposeToolingApi |
| val currentError: RecomposerErrorInformation? |
| get() = synchronized(stateLock) { this@Recomposer.errorState.value } |
| |
| @OptIn(ExperimentalComposeRuntimeApi::class) |
| override fun observe(observer: CompositionRegistrationObserver): CompositionObserverHandle = |
| this@Recomposer.observe(observer) |
| |
| fun invalidateGroupsWithKey(key: Int) { |
| val compositions: List<ControlledComposition> = knownCompositions() |
| compositions |
| .fastMapNotNull { it as? CompositionImpl } |
| .fastForEach { it.invalidateGroupsWithKey(key) } |
| } |
| |
| fun saveStateAndDisposeForHotReload(): List<HotReloadable> { |
| val compositions: List<ControlledComposition> = knownCompositions() |
| return compositions |
| .fastMapNotNull { it as? CompositionImpl } |
| .fastMap { HotReloadable(it).apply { clearContent() } } |
| } |
| |
| fun resetErrorState(): RecomposerErrorState? = this@Recomposer.resetErrorState() |
| |
| fun retryFailedCompositions() = this@Recomposer.retryFailedCompositions() |
| } |
| |
| private class HotReloadable(private val composition: CompositionImpl) { |
| private var composable: @Composable () -> Unit = composition.composable |
| |
| fun clearContent() { |
| if (composition.isRoot) { |
| composition.setContent {} |
| } |
| } |
| |
| fun resetContent() { |
| composition.composable = composable |
| } |
| |
| fun recompose() { |
| if (composition.isRoot) { |
| composition.setContent(composable) |
| } |
| } |
| } |
| |
| @OptIn(ComposeToolingApi::class) |
| private class RecomposerErrorState( |
| override val cause: Throwable, |
| override val isRecoverable: Boolean, |
| ) : RecomposerErrorInfo, RecomposerErrorInformation { |
| override val recoverable: Boolean |
| get() = isRecoverable |
| } |
| |
| private val recomposerInfo = RecomposerInfoImpl() |
| |
| /** Obtain a read-only [RecomposerInfo] for this [Recomposer]. */ |
| public fun asRecomposerInfo(): RecomposerInfo = recomposerInfo |
| |
| /** |
| * Propagate all invalidations from `snapshotInvalidations` to all the known compositions. |
| * |
| * @return `true` if the frame has work to do (e.g. [hasFrameWorkLocked]) |
| */ |
| private fun recordComposerModifications(): Boolean { |
| var compositions: List<ControlledComposition> = emptyList() |
| val changes = |
| synchronized(stateLock) { |
| if (snapshotInvalidations.isEmpty()) return hasFrameWorkLocked |
| compositions = knownCompositionsLocked() |
| snapshotInvalidations.wrapIntoSet().also { |
| snapshotInvalidations = MutableScatterSet() |
| } |
| } |
| var complete = false |
| try { |
| run { |
| compositions.fastForEach { composition -> |
| composition.recordModificationsOf(changes) |
| |
| // Stop dispatching if the recomposer if we detect the recomposer |
| // is shutdown. |
| if (_state.value <= State.ShuttingDown) return@run |
| } |
| } |
| complete = true |
| } finally { |
| if (!complete) { |
| // If the previous loop was not complete, we have not sent all of theses |
| // changes to all the composers so try again after the exception that caused |
| // the early exit is handled and we can then retry sending the changes. |
| synchronized(stateLock) { snapshotInvalidations.addAll(changes) } |
| } |
| } |
| return synchronized(stateLock) { |
| if (deriveStateLocked() != null) { |
| error("called outside of runRecomposeAndApplyChanges") |
| } |
| hasFrameWorkLocked |
| } |
| } |
| |
| private fun registerRunnerJob(callingJob: Job) { |
| synchronized(stateLock) { |
| closeCause?.let { throw it } |
| if (_state.value <= State.ShuttingDown) error("Recomposer shut down") |
| if (runnerJob != null) error("Recomposer already running") |
| runnerJob = callingJob |
| if (deriveStateLocked() != null) { |
| composeImmediateRuntimeError("called outside of runRecomposeAndApplyChanges") |
| } |
| } |
| } |
| |
| /** |
| * Await the invalidation of any associated [Composer]s, recompose them, and apply their changes |
| * to their associated [Composition]s if recomposition is successful. |
| * |
| * While [runRecomposeAndApplyChanges] is running, [awaitIdle] will suspend until there are no |
| * more invalid composers awaiting recomposition. |
| * |
| * This method will not return unless the [Recomposer] is [close]d and all effects in managed |
| * compositions complete. Unhandled failure exceptions from child coroutines will be thrown by |
| * this method. |
| */ |
| public suspend fun runRecomposeAndApplyChanges(): Unit = |
| recompositionRunner { parentFrameClock -> |
| val toRecompose = mutableListOf<ControlledComposition>() |
| val toInsert = mutableListOf<MovableContentStateReference>() |
| val toApply = mutableListOf<ControlledComposition>() |
| val toLateApply = mutableScatterSetOf<ControlledComposition>() |
| val toComplete = mutableScatterSetOf<ControlledComposition>() |
| val modifiedValues = MutableScatterSet<Any>() |
| val modifiedValuesSet = modifiedValues.wrapIntoSet() |
| val alreadyComposed = mutableScatterSetOf<ControlledComposition>() |
| |
| fun clearRecompositionState() { |
| synchronized(stateLock) { |
| toRecompose.clear() |
| toInsert.clear() |
| |
| toApply.fastForEach { |
| it.abandonChanges() |
| recordFailedCompositionLocked(it) |
| } |
| toApply.clear() |
| |
| toLateApply.forEach { |
| it.abandonChanges() |
| recordFailedCompositionLocked(it) |
| } |
| toLateApply.clear() |
| |
| toComplete.forEach { it.changesApplied() } |
| toComplete.clear() |
| |
| modifiedValues.clear() |
| |
| alreadyComposed.forEach { |
| it.abandonChanges() |
| recordFailedCompositionLocked(it) |
| } |
| alreadyComposed.clear() |
| } |
| } |
| |
| fun fillToInsert() { |
| toInsert.clear() |
| synchronized(stateLock) { |
| movableContentAwaitingInsert.fastForEach { toInsert += it } |
| movableContentAwaitingInsert.clear() |
| } |
| } |
| |
| while (shouldKeepRecomposing) { |
| awaitWorkAvailable() |
| |
| // Don't await a new frame if we don't have frame-scoped work |
| if (!recordComposerModifications()) continue |
| |
| // Align work with the next frame to coalesce changes. |
| // Note: it is possible to resume from the above with no recompositions pending, |
| // instead someone might be awaiting our frame clock dispatch below. |
| // We use the cached frame clock from above not just so that we don't locate it |
| // each time, but because we've installed the broadcastFrameClock as the scope |
| // clock above for user code to locate. |
| parentFrameClock.withFrameNanos { frameTime -> |
| // Dispatch MonotonicFrameClock frames first; this may produce new |
| // composer invalidations that we must handle during the same frame. |
| if (hasBroadcastFrameClockAwaiters) { |
| trace("Recomposer:animation") { |
| // Propagate the frame time to anyone who is awaiting from the |
| // recomposer clock. |
| broadcastFrameClock.sendFrame(frameTime) |
| |
| // Ensure any global changes are observed |
| Snapshot.sendApplyNotifications() |
| } |
| } |
| |
| trace("Recomposer:recompose") { |
| // Drain any composer invalidations from snapshot changes and record |
| // composers to work on |
| recordComposerModifications() |
| synchronized(stateLock) { |
| compositionInvalidations.forEach { toRecompose += it } |
| compositionInvalidations.clear() |
| } |
| |
| // Perform recomposition for any invalidated composers |
| modifiedValues.clear() |
| alreadyComposed.clear() |
| while (toRecompose.isNotEmpty() || toInsert.isNotEmpty()) { |
| try { |
| toRecompose.fastForEach { composition -> |
| performRecompose(composition, modifiedValues)?.let { |
| toApply += it |
| } |
| alreadyComposed.add(composition) |
| } |
| } catch (e: Throwable) { |
| processCompositionError(e, recoverable = true) |
| clearRecompositionState() |
| return@withFrameNanos |
| } finally { |
| toRecompose.clear() |
| } |
| |
| // Find any trailing recompositions that need to be composed because |
| // of a value change by a composition. This can happen, for example, if |
| // a CompositionLocal changes in a parent and was read in a child |
| // composition that was otherwise valid. |
| if ( |
| modifiedValues.isNotEmpty() || compositionInvalidations.isNotEmpty() |
| ) { |
| synchronized(stateLock) { |
| knownCompositionsLocked().fastForEach { value -> |
| if ( |
| value !in alreadyComposed && |
| value.observesAnyOf(modifiedValuesSet) |
| ) { |
| toRecompose += value |
| } |
| } |
| |
| // Composable lambda is a special kind of value that is not |
| // observed |
| // by the snapshot system, but invalidates composition scope |
| // directly instead. |
| compositionInvalidations.removeIf { value -> |
| if (value !in alreadyComposed && value !in toRecompose) { |
| toRecompose += value |
| true |
| } else { |
| false |
| } |
| } |
| } |
| } |
| |
| if (toRecompose.isEmpty()) { |
| try { |
| fillToInsert() |
| while (toInsert.isNotEmpty()) { |
| toLateApply += performInsertValues(toInsert, modifiedValues) |
| fillToInsert() |
| } |
| } catch (e: Throwable) { |
| processCompositionError(e, recoverable = true) |
| clearRecompositionState() |
| return@withFrameNanos |
| } |
| } |
| } |
| |
| // This is an optimization to avoid reallocating TransparentSnapshot for |
| // each observeChanges within `apply`. Many modifiers use observation in |
| // `onAttach` and other lifecycle methods, and allocations can be mitigated |
| // by updating read observer in the snapshot allocated here. |
| withTransparentSnapshot { |
| if (toApply.isNotEmpty()) { |
| changeCount++ |
| |
| // Perform apply changes |
| try { |
| // We could do toComplete += toApply but doing it like below |
| // avoids unnecessary allocations since toApply is a mutable |
| // list |
| // toComplete += toApply |
| toApply.fastForEach { composition -> |
| toComplete.add(composition) |
| } |
| toApply.fastForEach { composition -> |
| composition.applyChanges() |
| } |
| } catch (e: Throwable) { |
| processCompositionError(e) |
| clearRecompositionState() |
| return@withFrameNanos |
| } finally { |
| toApply.clear() |
| } |
| } |
| |
| if (toLateApply.isNotEmpty()) { |
| try { |
| toComplete += toLateApply |
| toLateApply.forEach { composition -> |
| composition.applyLateChanges() |
| } |
| } catch (e: Throwable) { |
| processCompositionError(e) |
| clearRecompositionState() |
| return@withFrameNanos |
| } finally { |
| toLateApply.clear() |
| } |
| } |
| |
| if (toComplete.isNotEmpty()) { |
| try { |
| toComplete.forEach { composition -> |
| composition.changesApplied() |
| } |
| } catch (e: Throwable) { |
| processCompositionError(e) |
| clearRecompositionState() |
| return@withFrameNanos |
| } finally { |
| toComplete.clear() |
| } |
| } |
| } |
| |
| synchronized(stateLock) { |
| runtimeCheck(deriveStateLocked() == null) { |
| "unexpected to get continuation here" |
| } |
| } |
| |
| // Ensure any state objects that were written during apply changes, e.g. |
| // nodes with state-backed properties, get sent apply notifications to |
| // invalidate anything observing the nodes. Call this method instead of |
| // sendApplyNotifications to ensure that objects that were _created_ in this |
| // snapshot are also considered changed after this point. |
| Snapshot.notifyObjectsInitialized() |
| alreadyComposed.clear() |
| modifiedValues.clear() |
| compositionsRemoved = null |
| } |
| } |
| |
| discardUnusedMovableContentState() |
| nextFrameEndCallbackQueue.markFrameComplete() |
| } |
| } |
| |
| private fun processCompositionError( |
| e: Throwable, |
| failedInitialComposition: ControlledComposition? = null, |
| recoverable: Boolean = false, |
| ) { |
| if (_hotReloadEnabled.get() && e !is ComposeRuntimeError) { |
| synchronized(stateLock) { |
| logError("Error was captured in composition while live edit was enabled.", e) |
| |
| compositionsAwaitingApply.clear() |
| compositionInvalidations.clear() |
| snapshotInvalidations = MutableScatterSet() |
| |
| movableContentAwaitingInsert.clear() |
| movableContentRemoved.clear() |
| movableContentStatesAvailable.clear() |
| |
| errorState.value = RecomposerErrorState(isRecoverable = recoverable, cause = e) |
| |
| failedInitialComposition?.let { recordFailedCompositionChainLocked(it) } |
| |
| if (deriveStateLocked() != null) { |
| composeImmediateRuntimeError( |
| "expected to go to inactive state due to composition error" |
| ) |
| } |
| } |
| } else { |
| // withFrameNanos uses `runCatching` to ensure that crashes are not propagated to |
| // AndroidUiDispatcher. This means that errors that happen during recomposition might |
| // be delayed by a frame and swallowed if composed into inconsistent state caused by |
| // the error. |
| // Common case is subcomposition: if measure occurs after recomposition has thrown, |
| // composeInitial will throw because of corrupted composition while original exception |
| // won't be recorded. |
| synchronized(stateLock) { |
| logError("Error was captured in composition.", e) |
| val errorState = errorState.value |
| if (errorState == null) { |
| // Record exception if current error state is empty. |
| this.errorState.value = RecomposerErrorState(isRecoverable = false, cause = e) |
| } else { |
| // Re-throw original cause if we recorded it previously. |
| throw errorState.cause |
| } |
| } |
| |
| throw e |
| } |
| } |
| |
| private inline fun withTransparentSnapshot(block: () -> Unit) { |
| val currentSnapshot = Snapshot.current |
| |
| val snapshot = |
| if (currentSnapshot is MutableSnapshot) { |
| TransparentObserverMutableSnapshot( |
| currentSnapshot, |
| null, |
| null, |
| mergeParentObservers = true, |
| ownsParentSnapshot = false, |
| ) |
| } else { |
| TransparentObserverSnapshot( |
| currentSnapshot, |
| null, |
| mergeParentObservers = true, |
| ownsParentSnapshot = false, |
| ) |
| } |
| try { |
| snapshot.enter(block) |
| } finally { |
| snapshot.dispose() |
| } |
| } |
| |
| /** |
| * Returns a cached copy of the list of known compositions that can be iterated safely without |
| * holding the `stateLock`. |
| */ |
| private fun knownCompositions(): List<ControlledComposition> { |
| return synchronized(stateLock) { knownCompositionsLocked() } |
| } |
| |
| private fun knownCompositionsLocked(): List<ControlledComposition> { |
| val cache = _knownCompositionsCache |
| if (cache != null) return cache |
| |
| val compositions = _knownCompositions |
| val newCache = if (compositions.isEmpty()) emptyList() else ArrayList(compositions) |
| _knownCompositionsCache = newCache |
| return newCache |
| } |
| |
| @OptIn(ExperimentalComposeRuntimeApi::class) |
| private fun clearKnownCompositionsLocked() { |
| knownCompositionsLocked().fastForEach { composition -> |
| unregisterCompositionLocked(composition) |
| } |
| _knownCompositions.clear() |
| _knownCompositionsCache = emptyList() |
| } |
| |
| private fun removeKnownCompositionLocked(composition: ControlledComposition) { |
| if (_knownCompositions.remove(composition)) { |
| _knownCompositionsCache = null |
| unregisterCompositionLocked(composition) |
| } |
| } |
| |
| private fun addKnownCompositionLocked(composition: ControlledComposition) { |
| _knownCompositions += composition |
| _knownCompositionsCache = null |
| } |
| |
| @OptIn(ExperimentalComposeRuntimeApi::class) |
| private fun registerCompositionLocked(composition: ControlledComposition) { |
| registrationObservers?.forEach { |
| if (composition is ObservableComposition) { |
| it.onCompositionRegistered(composition) |
| } |
| } |
| } |
| |
| @OptIn(ExperimentalComposeRuntimeApi::class) |
| private fun unregisterCompositionLocked(composition: ControlledComposition) { |
| registrationObservers?.forEach { |
| if (composition is ObservableComposition) { |
| it.onCompositionUnregistered(composition) |
| } |
| } |
| } |
| |
| @OptIn(ExperimentalComposeRuntimeApi::class) |
| internal fun addCompositionRegistrationObserver( |
| observer: CompositionRegistrationObserver |
| ): CompositionObserverHandle { |
| synchronized(stateLock) { |
| val observers = |
| registrationObservers |
| ?: MutableObjectList<CompositionRegistrationObserver>().also { |
| registrationObservers = it |
| } |
| |
| observers += observer |
| _knownCompositions.fastForEach { composition -> |
| if (composition is ObservableComposition) { |
| observer.onCompositionRegistered(composition) |
| } |
| } |
| } |
| |
| return object : CompositionObserverHandle { |
| override fun dispose() { |
| synchronized(stateLock) { registrationObservers?.remove(observer) } |
| } |
| } |
| } |
| |
| private fun resetErrorState(): RecomposerErrorState? { |
| var error: RecomposerErrorState? = null |
| synchronized(stateLock) { |
| error = errorState.value |
| if (error != null) { |
| errorState.value = null |
| deriveStateLocked() |
| } else { |
| null |
| } |
| } |
| ?.resume(Unit) |
| return error |
| } |
| |
| private fun retryFailedCompositions() { |
| val compositionsToRetry = |
| synchronized(stateLock) { failedCompositions.also { failedCompositions = null } } |
| ?: return |
| try { |
| while (compositionsToRetry.isNotEmpty()) { |
| val composition = compositionsToRetry.removeNextCompositionToRetry() |
| if (composition !is CompositionImpl) continue |
| if ( |
| composition.isDisposed || |
| composition.isRemoved || |
| composition.hasRemovedAncestor |
| ) { |
| continue |
| } |
| |
| composition.invalidateAll() |
| composition.setContent(composition.composable) |
| |
| if (errorState.value != null) break |
| } |
| } finally { |
| if (compositionsToRetry.isNotEmpty()) { |
| // If we did not complete the last list then add the remaining compositions back |
| // into the failedCompositions list |
| synchronized(stateLock) { |
| compositionsToRetry.fastForEach { recordFailedCompositionLocked(it) } |
| } |
| } |
| } |
| } |
| |
| private fun MutableList<ControlledComposition>.removeNextCompositionToRetry(): |
| ControlledComposition { |
| val nextIndex = indexOfLast { composition -> |
| composition !is CompositionImpl || !composition.hasFailedAncestorIn(this) |
| } |
| return if (nextIndex >= 0) removeAt(nextIndex) else removeLastKt() |
| } |
| |
| private fun CompositionImpl.hasFailedAncestorIn( |
| failedCompositions: List<ControlledComposition> |
| ): Boolean = anyAncestor { it in failedCompositions } |
| |
| private val CompositionImpl.hasRemovedAncestor: Boolean |
| get() = anyAncestor { it.isRemoved } |
| |
| private inline fun CompositionImpl.anyAncestor( |
| predicate: (CompositionImpl) -> Boolean |
| ): Boolean { |
| var parent = parent.composition as? CompositionImpl |
| while (parent != null) { |
| if (predicate(parent)) return true |
| parent = parent.parent.composition as? CompositionImpl |
| } |
| return false |
| } |
| |
| private val ControlledComposition.isRemoved: Boolean |
| get() = synchronized(stateLock) { compositionsRemoved?.contains(this) == true } |
| |
| private fun recordFailedCompositionChainLocked(composition: ControlledComposition) { |
| var current: ControlledComposition? = composition |
| while (current != null) { |
| recordFailedCompositionLocked(current) |
| current = (current as? CompositionImpl)?.parent?.composition as? ControlledComposition |
| } |
| } |
| |
| private fun recordFailedCompositionLocked(composition: ControlledComposition) { |
| val failedCompositions = |
| failedCompositions |
| ?: mutableListOf<ControlledComposition>().also { failedCompositions = it } |
| |
| if (composition !in failedCompositions) { |
| failedCompositions += composition |
| } |
| removeKnownCompositionLocked(composition) |
| } |
| |
| private val hasSchedulingWork: Boolean |
| get() = |
| synchronized(stateLock) { |
| snapshotInvalidations.isNotEmpty() || |
| compositionInvalidations.isNotEmpty() || |
| hasBroadcastFrameClockAwaitersLocked || |
| hasNextFrameEndAwaitersLocked |
| } |
| |
| private suspend fun awaitWorkAvailable() { |
| if (!hasSchedulingWork) { |
| // NOTE: Do not remove the `<Unit>` from the next line even if the IDE reports it as |
| // redundant. Removing this causes reports it cannot infer the type. (KT-79553) |
| @Suppress("RemoveExplicitTypeArguments") // See note above |
| suspendCancellableCoroutine<Unit> { co -> |
| synchronized(stateLock) { |
| if (hasSchedulingWork) { |
| co |
| } else { |
| workContinuation = co |
| null |
| } |
| } |
| ?.resume(Unit) |
| } |
| } |
| } |
| |
| @OptIn(ExperimentalComposeApi::class) |
| private suspend fun recompositionRunner( |
| block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit |
| ) { |
| val parentFrameClock = coroutineContext.monotonicFrameClock |
| withContext(broadcastFrameClock) { |
| // Enforce mutual exclusion of callers; register self as current runner |
| val callingJob = coroutineContext.job |
| registerRunnerJob(callingJob) |
| |
| // Observe snapshot changes and propagate them to known composers only from |
| // this caller's dispatcher, never working with the same composer in parallel. |
| // unregisterApplyObserver is called as part of the big finally below |
| val unregisterApplyObserver = |
| Snapshot.registerApplyObserver { changed, _ -> |
| synchronized(stateLock) { |
| if (_state.value >= State.Idle) { |
| val snapshotInvalidations = snapshotInvalidations |
| changed.fastForEach { |
| if ( |
| it is StateObjectImpl && |
| !it.isReadIn(ReaderKind.Composition) |
| ) { |
| // continue if we know that state is never read in |
| // composition |
| return@fastForEach |
| } |
| snapshotInvalidations.add(it) |
| } |
| deriveStateLocked() |
| } else null |
| } |
| ?.resume(Unit) |
| } |
| |
| addRunning(recomposerInfo) |
| |
| try { |
| // Invalidate all registered composers when we start since we weren't observing |
| // snapshot changes on their behalf. Assume anything could have changed. |
| knownCompositions().fastForEach { it.invalidateAll() } |
| |
| coroutineScope { block(parentFrameClock) } |
| } finally { |
| unregisterApplyObserver.dispose() |
| synchronized(stateLock) { |
| if (runnerJob === callingJob) { |
| runnerJob = null |
| } |
| if (deriveStateLocked() != null) { |
| composeImmediateRuntimeError( |
| "called outside of runRecomposeAndApplyChanges" |
| ) |
| } |
| } |
| removeRunning(recomposerInfo) |
| } |
| } |
| } |
| |
| /** |
| * Permanently shut down this [Recomposer] for future use. [currentState] will immediately |
| * reflect [State.ShuttingDown] (or a lower state) before this call returns. All ongoing |
| * recompositions will stop, new composer invalidations with this [Recomposer] at the root will |
| * no longer occur, and any [LaunchedEffect]s currently running in compositions managed by this |
| * [Recomposer] will be cancelled. Any [rememberCoroutineScope] scopes from compositions managed |
| * by this [Recomposer] will also be cancelled. See [join] to await the completion of all of |
| * these outstanding tasks. |
| */ |
| public fun cancel() { |
| // Move to State.ShuttingDown immediately rather than waiting for effectJob to join |
| // if we're cancelling to shut down the Recomposer. This permits other client code |
| // to use `state.first { it < State.Idle }` or similar to reliably and immediately detect |
| // that the recomposer can no longer be used. |
| // It looks like a CAS loop would be more appropriate here, but other occurrences |
| // of taking stateLock assume that the state cannot change without holding it. |
| synchronized(stateLock) { |
| if (_state.value >= State.Idle) { |
| _state.value = State.ShuttingDown |
| } |
| } |
| effectJob.cancel() |
| } |
| |
| /** |
| * Close this [Recomposer]. Once all effects launched by managed compositions complete, any |
| * active call to [runRecomposeAndApplyChanges] will return normally and this [Recomposer] will |
| * be [State.ShutDown]. See [join] to await the completion of all of these outstanding tasks. |
| */ |
| public fun close() { |
| if (effectJob.complete()) { |
| synchronized(stateLock) { isClosed = true } |
| } |
| } |
| |
| /** Await the completion of a [cancel] operation. */ |
| public suspend fun join() { |
| currentState.first { it == State.ShutDown } |
| } |
| |
| /** |
| * Schedules an [action] to be invoked when the recomposer finishes the next composition of a |
| * frame (including the completion of subcompositions). If a frame is currently in-progress, |
| * [action] will be invoked when the current frame fully finishes composing. If a frame isn't |
| * currently in-progress, a new frame will be scheduled (if one hasn't been already) and |
| * [action] will execute at the completion of the next frame's composition. If a new frame is |
| * scheduled and there is no other work to execute, [action] will still execute. |
| * |
| * [action] will always execute on the applier thread. |
| * |
| * @return A [CancellationHandle] that can be used to unregister the [action]. The returned |
| * handle is thread-safe and may be cancelled from any thread. Cancelling the handle only |
| * removes the callback from the queue. If [action] is currently executing, it will not be |
| * cancelled by this handle. |
| */ |
| public override fun scheduleFrameEndCallback(action: () -> Unit): CancellationHandle { |
| return nextFrameEndCallbackQueue.scheduleFrameEndCallback(action) |
| } |
| |
| internal override fun composeInitial( |
| composition: ControlledComposition, |
| content: @Composable () -> Unit, |
| ) { |
| val composerWasComposing = composition.isComposing |
| |
| val newComposition = |
| synchronized(stateLock) { |
| if (_state.value > State.ShuttingDown) { |
| val new = composition !in knownCompositionsLocked() |
| if (new) { |
| registerCompositionLocked(composition) |
| } |
| new |
| } else { |
| true |
| } |
| } |
| |
| try { |
| composing(composition, null) { composition.composeContent(content) } |
| } catch (e: Throwable) { |
| if (newComposition) { |
| synchronized(stateLock) { unregisterCompositionLocked(composition) } |
| } |
| |
| processCompositionError(e, composition, recoverable = true) |
| return |
| } |
| |
| synchronized(stateLock) { |
| if (_state.value > State.ShuttingDown) { |
| if (composition !in knownCompositionsLocked()) { |
| addKnownCompositionLocked(composition) |
| } |
| } else { |
| unregisterCompositionLocked(composition) |
| } |
| } |
| |
| // TODO(b/143755743) |
| if (!composerWasComposing) { |
| Snapshot.notifyObjectsInitialized() |
| } |
| |
| try { |
| performInitialMovableContentInserts(composition) |
| } catch (e: Throwable) { |
| processCompositionError(e, composition, recoverable = true) |
| return |
| } |
| |
| try { |
| composition.applyChanges() |
| composition.applyLateChanges() |
| } catch (e: Throwable) { |
| processCompositionError(e) |
| return |
| } |
| |
| if (!composerWasComposing) { |
| // Ensure that any state objects created during applyChanges are seen as changed |
| // if modified after this call. |
| Snapshot.notifyObjectsInitialized() |
| } |
| } |
| |
| internal override fun composeInitialPaused( |
| composition: ControlledComposition, |
| shouldPause: ShouldPauseCallback, |
| content: @Composable () -> Unit, |
| ): ScatterSet<RecomposeScopeImpl> { |
| return try { |
| composition.pausable(shouldPause) { |
| composeInitial(composition, content) |
| pausedScopes.get() ?: emptyScatterSet() |
| } |
| } finally { |
| pausedScopes.set(null) |
| } |
| } |
| |
| internal override fun recomposePaused( |
| composition: ControlledComposition, |
| shouldPause: ShouldPauseCallback, |
| invalidScopes: ScatterSet<RecomposeScopeImpl>, |
| ): ScatterSet<RecomposeScopeImpl> { |
| return try { |
| recordComposerModifications() |
| composition.recordModificationsOf(invalidScopes.wrapIntoSet()) |
| composition.pausable(shouldPause) { |
| val needsApply = performRecompose(composition, null) |
| if (needsApply != null) { |
| performInitialMovableContentInserts(composition) |
| needsApply.applyChanges() |
| needsApply.applyLateChanges() |
| } |
| pausedScopes.get() ?: emptyScatterSet() |
| } |
| } finally { |
| pausedScopes.set(null) |
| } |
| } |
| |
| override fun reportPausedScope(scope: RecomposeScopeImpl) { |
| val scopes = |
| pausedScopes.get() |
| ?: run { |
| val newScopes = mutableScatterSetOf<RecomposeScopeImpl>() |
| pausedScopes.set(newScopes) |
| newScopes |
| } |
| scopes.add(scope) |
| } |
| |
| private fun performInitialMovableContentInserts(composition: ControlledComposition) { |
| synchronized(stateLock) { |
| if (!movableContentAwaitingInsert.fastAny { it.composition == composition }) return |
| } |
| val toInsert = mutableListOf<MovableContentStateReference>() |
| fun fillToInsert() { |
| toInsert.clear() |
| synchronized(stateLock) { |
| val iterator = movableContentAwaitingInsert.iterator() |
| while (iterator.hasNext()) { |
| val value = iterator.next() |
| if (value.composition == composition) { |
| toInsert.add(value) |
| iterator.remove() |
| } |
| } |
| } |
| } |
| fillToInsert() |
| while (toInsert.isNotEmpty()) { |
| performInsertValues(toInsert, null) |
| fillToInsert() |
| } |
| } |
| |
| private fun performRecompose( |
| composition: ControlledComposition, |
| modifiedValues: MutableScatterSet<Any>?, |
| ): ControlledComposition? { |
| if ( |
| composition.isComposing || |
| composition.isDisposed || |
| compositionsRemoved?.contains(composition) == true |
| ) |
| return null |
| |
| return if ( |
| composing(composition, modifiedValues) { |
| if (modifiedValues?.isNotEmpty() == true) { |
| // Record write performed by a previous composition as if they happened during |
| // composition. |
| composition.prepareCompose { |
| modifiedValues.forEach { composition.recordWriteOf(it) } |
| } |
| } |
| composition.recompose() |
| } |
| ) |
| composition |
| else null |
| } |
| |
| @OptIn(ExperimentalComposeApi::class) |
| private fun performInsertValues( |
| references: List<MovableContentStateReference>, |
| modifiedValues: MutableScatterSet<Any>?, |
| ): List<ControlledComposition> { |
| val tasks = references.fastGroupBy { it.composition } |
| for ((composition, refs) in tasks) { |
| runtimeCheck(!composition.isComposing) |
| composing(composition, modifiedValues) { |
| // Map insert movable content to movable content states that have been released |
| // during `performRecompose`. |
| val pairs = |
| synchronized(stateLock) { |
| refs |
| .fastMap { reference -> |
| reference to |
| movableContentRemoved.removeLast(reference.content).also { |
| if (it != null) { |
| movableContentNestedStatesAvailable.usedContainer(it) |
| } |
| } |
| } |
| .let { pairs -> |
| // Check for any nested states |
| if ( |
| pairs.fastAny { |
| it.second == null && |
| it.first.content in movableContentNestedStatesAvailable |
| } |
| ) { |
| // We have at least one nested state we could use, if a state |
| // is available for the container then schedule the state to be |
| // removed from the container when it is released. |
| pairs.fastMap { pair -> |
| if (pair.second == null) { |
| val nestedContentReference = |
| movableContentNestedStatesAvailable.removeLast( |
| pair.first.content |
| ) |
| if (nestedContentReference == null) return@fastMap pair |
| val content = nestedContentReference.content |
| val container = nestedContentReference.container |
| movableContentNestedExtractionsPending.add( |
| container, |
| content, |
| ) |
| pair.first to content |
| } else pair |
| } |
| } else pairs |
| } |
| } |
| |
| // Avoid mixing creating new content with moving content as the moved content |
| // may release content when it is moved as it is recomposed when move. |
| val toInsert = |
| if ( |
| pairs.fastAll { it.second == null } || pairs.fastAll { it.second != null } |
| ) { |
| pairs |
| } else { |
| // Return the content not moving to the awaiting list. These will come back |
| // here in the next iteration of the caller's loop and either have content |
| // to move or by still needing to create the content. |
| val toReturn = |
| pairs.fastMapNotNull { item -> |
| if (item.second == null) item.first else null |
| } |
| synchronized(stateLock) { movableContentAwaitingInsert += toReturn } |
| |
| // Only insert the moving content this time |
| pairs.fastFilterIndexed { _, item -> item.second != null } |
| } |
| |
| // toInsert is guaranteed to be not empty as, |
| // 1) refs is guaranteed to be not empty as a condition of groupBy |
| // 2) pairs is guaranteed to be not empty as it is a map of refs |
| // 3) toInsert is guaranteed to not be empty because the toReturn and toInsert |
| // lists have at least one item by the condition of the guard in the if |
| // expression. If one would be empty the condition is true and the filter is not |
| // performed. As both have at least one item toInsert has at least one item. If |
| // the filter is not performed the list is pairs which has at least one item. |
| composition.insertMovableContent(toInsert) |
| } |
| } |
| return tasks.keys.toList() |
| } |
| |
| private fun discardUnusedMovableContentState() { |
| val unusedValues = |
| synchronized(stateLock) { |
| if (movableContentRemoved.isNotEmpty()) { |
| val references = movableContentRemoved.values() |
| movableContentRemoved.clear() |
| movableContentNestedStatesAvailable.clear() |
| movableContentNestedExtractionsPending.clear() |
| val unusedValues = |
| references.fastMap { it to movableContentStatesAvailable[it] } |
| movableContentStatesAvailable.clear() |
| unusedValues |
| } else emptyObjectList() |
| } |
| unusedValues.forEach { (reference, state) -> |
| if (state != null) { |
| reference.composition.disposeUnusedMovableContent(state) |
| } |
| } |
| } |
| |
| private fun readObserverOf(composition: ControlledComposition): (Any) -> Unit { |
| return { value -> composition.recordReadOf(value) } |
| } |
| |
| private fun writeObserverOf( |
| composition: ControlledComposition, |
| modifiedValues: MutableScatterSet<Any>?, |
| ): (Any) -> Unit { |
| return { value -> |
| composition.recordWriteOf(value) |
| modifiedValues?.add(value) |
| } |
| } |
| |
| private inline fun <T> composing( |
| composition: ControlledComposition, |
| modifiedValues: MutableScatterSet<Any>?, |
| block: () -> T, |
| ): T { |
| val snapshot = |
| Snapshot.takeMutableSnapshot( |
| readObserverOf(composition), |
| writeObserverOf(composition, modifiedValues), |
| ) |
| try { |
| return snapshot.enter(block) |
| } finally { |
| applyAndCheck(snapshot) |
| } |
| } |
| |
| private fun applyAndCheck(snapshot: MutableSnapshot) { |
| try { |
| val applyResult = snapshot.apply() |
| if (applyResult is SnapshotApplyResult.Failure) { |
| error( |
| "Unsupported concurrent change during composition. A state object was " + |
| "modified by composition as well as being modified outside composition." |
| ) |
| } |
| } finally { |
| snapshot.dispose() |
| } |
| } |
| |
| /** |
| * `true` if this [Recomposer] has any pending work scheduled, regardless of whether or not it |
| * is currently [running][runRecomposeAndApplyChanges]. |
| */ |
| public val hasPendingWork: Boolean |
| get() = |
| synchronized(stateLock) { |
| snapshotInvalidations.isNotEmpty() || |
| compositionInvalidations.isNotEmpty() || |
| concurrentCompositionsOutstanding > 0 || |
| compositionsAwaitingApply.isNotEmpty() || |
| hasBroadcastFrameClockAwaitersLocked || |
| hasNextFrameEndAwaitersLocked || |
| movableContentRemoved.isNotEmpty() |
| } |
| |
| private val hasFrameWorkLocked: Boolean |
| get() = |
| compositionInvalidations.isNotEmpty() || |
| hasBroadcastFrameClockAwaitersLocked || |
| hasNextFrameEndAwaitersLocked || |
| movableContentRemoved.isNotEmpty() |
| |
| /** |
| * Suspends until the currently pending recomposition frame is complete. Any recomposition for |
| * this recomposer triggered by actions before this call begins will be complete and applied (if |
| * recomposition was successful) when this call returns. |
| * |
| * If [runRecomposeAndApplyChanges] is not currently running the [Recomposer] is considered idle |
| * and this method will not suspend. |
| */ |
| public suspend fun awaitIdle() { |
| currentState.takeWhile { it > State.Idle }.collect() |
| } |
| |
| /** |
| * Pause broadcasting the frame clock while recomposing. This effectively pauses animations, or |
| * any other use of the [withFrameNanos], while the frame clock is paused. |
| * |
| * [pauseCompositionFrameClock] should be called when the recomposer is not being displayed for |
| * some reason such as not being the current activity in Android, for example. |
| * |
| * Calls to [pauseCompositionFrameClock] are thread-safe and idempotent (calling it when the |
| * frame clock is already paused is a no-op). |
| */ |
| public fun pauseCompositionFrameClock() { |
| synchronized(stateLock) { frameClockPaused = true } |
| } |
| |
| /** |
| * Resume broadcasting the frame clock after is has been paused. Pending calls to |
| * [withFrameNanos] will start receiving frame clock broadcasts at the beginning of the frame |
| * and a frame will be requested if there are pending calls to [withFrameNanos] if a frame has |
| * not already been scheduled. |
| * |
| * Calls to [resumeCompositionFrameClock] are thread-safe and idempotent (calling it when the |
| * frame clock is running is a no-op). |
| */ |
| public fun resumeCompositionFrameClock() { |
| synchronized(stateLock) { |
| if (frameClockPaused) { |
| frameClockPaused = false |
| deriveStateLocked() |
| } else null |
| } |
| ?.resume(Unit) |
| } |
| |
| // Recomposer always starts with a constant compound hash |
| internal override val compositeKeyHashCode: CompositeKeyHashCode |
| get() = RecomposerCompoundHashKey |
| |
| internal override val collectingCallByInformation: Boolean |
| get() = _hotReloadEnabled.get() |
| |
| // Collecting parameter happens at the level of a composer; starts as false |
| internal override val collectingParameterInformation: Boolean |
| get() = false |
| |
| internal override val collectingSourceInformation: Boolean |
| get() = composeStackTraceMode == ComposeStackTraceMode.SourceInformation |
| |
| internal override val stackTraceEnabled: Boolean |
| get() = composeStackTraceMode != ComposeStackTraceMode.None |
| |
| internal override fun recordInspectionTable(table: MutableSet<CompositionData>) { |
| // TODO: The root recomposer might be a better place to set up inspection |
| // than the current configuration with an CompositionLocal |
| } |
| |
| internal override fun registerComposition(composition: ControlledComposition) { |
| // Do nothing. |
| } |
| |
| internal override fun unregisterComposition(composition: ControlledComposition) { |
| synchronized(stateLock) { |
| removeKnownCompositionLocked(composition) |
| compositionInvalidations -= composition |
| compositionsAwaitingApply -= composition |
| } |
| } |
| |
| internal override fun invalidate(composition: ControlledComposition) { |
| synchronized(stateLock) { |
| if (composition !in compositionInvalidations) { |
| compositionInvalidations += composition |
| deriveStateLocked() |
| } else null |
| } |
| ?.resume(Unit) |
| } |
| |
| internal override fun invalidateScope(scope: RecomposeScopeImpl) { |
| synchronized(stateLock) { |
| snapshotInvalidations.add(scope) |
| deriveStateLocked() |
| } |
| ?.resume(Unit) |
| } |
| |
| internal override fun insertMovableContent(reference: MovableContentStateReference) { |
| synchronized(stateLock) { |
| movableContentAwaitingInsert += reference |
| deriveStateLocked() |
| } |
| ?.resume(Unit) |
| } |
| |
| internal override fun deletedMovableContent(reference: MovableContentStateReference) { |
| synchronized(stateLock) { |
| movableContentRemoved.add(reference.content, reference) |
| if (reference.nestedReferences != null) { |
| val container = reference |
| fun recordNestedStatesOf(reference: MovableContentStateReference) { |
| reference.nestedReferences?.fastForEach { nestedReference -> |
| movableContentNestedStatesAvailable.add( |
| nestedReference.content, |
| NestedMovableContent(nestedReference, container), |
| ) |
| recordNestedStatesOf(nestedReference) |
| } |
| } |
| recordNestedStatesOf(reference) |
| } |
| deriveStateLocked() |
| } |
| ?.resume(Unit) |
| } |
| |
| internal override fun movableContentStateReleased( |
| reference: MovableContentStateReference, |
| data: MovableContentState, |
| applier: Applier<*>, |
| ) { |
| synchronized(stateLock) { |
| movableContentStatesAvailable[reference] = data |
| val extractions = movableContentNestedExtractionsPending[reference] |
| if (extractions.isNotEmpty()) { |
| val states = data.slotStorage.extractNestedStates(applier, extractions) |
| states.forEach { reference, state -> |
| movableContentStatesAvailable[reference] = state |
| } |
| } |
| } |
| } |
| |
| internal override fun reportRemovedComposition(composition: ControlledComposition) { |
| synchronized(stateLock) { |
| val compositionsRemoved = |
| compositionsRemoved |
| ?: mutableScatterSetOf<ControlledComposition>().also { |
| compositionsRemoved = it |
| } |
| compositionsRemoved.add(composition) |
| } |
| } |
| |
| override fun movableContentStateResolve( |
| reference: MovableContentStateReference |
| ): MovableContentState? = |
| synchronized(stateLock) { movableContentStatesAvailable.remove(reference) } |
| |
| override val composition: Composition? |
| get() = null |
| |
| /** |
| * hack: the companion object is thread local in Kotlin/Native to avoid freezing |
| * [_runningRecomposers] with the current memory model. As a side effect, recomposers are now |
| * forced to be single threaded in Kotlin/Native targets. |
| * |
| * This annotation WILL BE REMOVED with the new memory model of Kotlin/Native. |
| */ |
| @ThreadLocal |
| public companion object { |
| |
| private val _runningRecomposers = MutableStateFlow(persistentSetOf<RecomposerInfoImpl>()) |
| |
| private val _hotReloadEnabled = AtomicReference(false) |
| |
| /** |
| * An observable [Set] of [RecomposerInfo]s for currently |
| * [running][runRecomposeAndApplyChanges] [Recomposer]s. Emitted sets are immutable. |
| */ |
| public val runningRecomposers: StateFlow<Set<RecomposerInfo>> |
| get() = _runningRecomposers |
| |
| internal fun setHotReloadEnabled(value: Boolean) { |
| _hotReloadEnabled.set(value) |
| } |
| |
| private fun addRunning(info: RecomposerInfoImpl) { |
| while (true) { |
| val old = _runningRecomposers.value |
| val new = old.add(info) |
| if (old === new || _runningRecomposers.compareAndSet(old, new)) break |
| } |
| } |
| |
| private fun removeRunning(info: RecomposerInfoImpl) { |
| while (true) { |
| val old = _runningRecomposers.value |
| val new = old.remove(info) |
| if (old === new || _runningRecomposers.compareAndSet(old, new)) break |
| } |
| } |
| |
| internal fun saveStateAndDisposeForHotReload(): Any { |
| // NOTE: when we move composition/recomposition onto multiple threads, we will want |
| // to ensure that we pause recompositions before this call. |
| _hotReloadEnabled.set(true) |
| return _runningRecomposers.value.flatMap { it.saveStateAndDisposeForHotReload() } |
| } |
| |
| internal fun loadStateAndComposeForHotReload(token: Any) { |
| // NOTE: when we move composition/recomposition onto multiple threads, we will want |
| // to ensure that we pause recompositions before this call. |
| _hotReloadEnabled.set(true) |
| |
| _runningRecomposers.value.forEach { it.resetErrorState() } |
| |
| @Suppress("UNCHECKED_CAST") val holders = token as List<HotReloadable> |
| holders.fastForEach { it.resetContent() } |
| holders.fastForEach { it.recompose() } |
| |
| _runningRecomposers.value.forEach { it.retryFailedCompositions() } |
| } |
| |
| @OptIn(ComposeToolingApi::class) |
| internal fun invalidateGroupsWithKey(key: Int) { |
| _hotReloadEnabled.set(true) |
| _runningRecomposers.value.forEach { |
| if (it.currentError?.isRecoverable == false) { |
| return@forEach |
| } |
| |
| it.resetErrorState() |
| |
| it.invalidateGroupsWithKey(key) |
| |
| it.retryFailedCompositions() |
| } |
| } |
| |
| /** This is an internal API only kept for backward compatibility. */ |
| @OptIn(ComposeToolingApi::class) |
| internal fun getCurrentErrors(): List<RecomposerErrorInfo> = |
| _runningRecomposers.value.mapNotNull { it.currentError as? RecomposerErrorInfo } |
| |
| @OptIn(ComposeToolingApi::class) |
| internal fun getRecomposerErrors(): List<RecomposerErrorInformation> = |
| _runningRecomposers.value.mapNotNull { it.currentError } |
| |
| internal fun clearErrors() { |
| _runningRecomposers.value.mapNotNull { it.resetErrorState() } |
| } |
| } |
| } |
| |
| /** Sentinel used by [ProduceFrameSignal] */ |
| private val ProduceAnotherFrame = Any() |
| private val FramePending = Any() |
| |
| /** |
| * Multiple producer, single consumer conflated signal that tells concurrent composition when it |
| * should try to produce another frame. This class is intended to be used along with a lock shared |
| * between producers and consumer. |
| */ |
| private class ProduceFrameSignal { |
| private var pendingFrameContinuation: Any? = null |
| |
| /** |
| * Suspend until a frame is requested. After this method returns the signal is in a |
| * [FramePending] state which must be acknowledged by a call to [takeFrameRequestLocked] once |
| * all data that will be used to produce the frame has been claimed. |
| */ |
| suspend fun awaitFrameRequest(lock: SynchronizedObject) { |
| synchronized(lock) { |
| if (pendingFrameContinuation === ProduceAnotherFrame) { |
| pendingFrameContinuation = FramePending |
| return |
| } |
| } |
| suspendCancellableCoroutine<Unit> { co -> |
| synchronized(lock) { |
| if (pendingFrameContinuation === ProduceAnotherFrame) { |
| pendingFrameContinuation = FramePending |
| co |
| } else { |
| pendingFrameContinuation = co |
| null |
| } |
| } |
| ?.resume(Unit) |
| } |
| } |
| |
| /** |
| * Signal from the frame request consumer that the frame is beginning with data that was |
| * available up until this point. (Synchronizing access to that data is up to the caller.) |
| */ |
| fun takeFrameRequestLocked() { |
| checkPrecondition(pendingFrameContinuation === FramePending) { "frame not pending" } |
| pendingFrameContinuation = null |
| } |
| |
| fun requestFrameLocked(): Continuation<Unit>? = |
| when (val co = pendingFrameContinuation) { |
| is Continuation<*> -> { |
| pendingFrameContinuation = FramePending |
| @Suppress("UNCHECKED_CAST") |
| co as Continuation<Unit> |
| } |
| ProduceAnotherFrame, |
| FramePending -> null |
| null -> { |
| pendingFrameContinuation = ProduceAnotherFrame |
| null |
| } |
| else -> error("invalid pendingFrameContinuation $co") |
| } |
| } |
| |
| @OptIn(InternalComposeApi::class) |
| private class NestedContentMap { |
| private val contentMap = MultiValueMap<MovableContent<Any?>, NestedMovableContent>() |
| private val containerMap = MultiValueMap<MovableContentStateReference, MovableContent<Any?>>() |
| |
| fun add(content: MovableContent<Any?>, nestedContent: NestedMovableContent) { |
| contentMap.add(content, nestedContent) |
| containerMap.add(nestedContent.container, content) |
| } |
| |
| fun clear() { |
| contentMap.clear() |
| containerMap.clear() |
| } |
| |
| fun removeLast(key: MovableContent<Any?>) = |
| contentMap.removeLast(key).also { if (contentMap.isEmpty()) containerMap.clear() } |
| |
| operator fun contains(key: MovableContent<Any?>) = key in contentMap |
| |
| fun usedContainer(reference: MovableContentStateReference) { |
| containerMap.forEachValue(reference) { value -> |
| contentMap.removeValueIf(value) { it.container == reference } |
| } |
| } |
| } |
| |
| @InternalComposeApi |
| private class NestedMovableContent( |
| val content: MovableContentStateReference, |
| val container: MovableContentStateReference, |
| ) |