1 /*
<lambda>null2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.runtime
18 
19 import androidx.collection.MutableObjectList
20 import androidx.collection.MutableScatterSet
21 import androidx.collection.ScatterSet
22 import androidx.collection.emptyObjectList
23 import androidx.collection.emptyScatterSet
24 import androidx.collection.mutableScatterMapOf
25 import androidx.collection.mutableScatterSetOf
26 import androidx.compose.runtime.collection.MultiValueMap
27 import androidx.compose.runtime.collection.fastForEach
28 import androidx.compose.runtime.collection.fastMap
29 import androidx.compose.runtime.collection.mutableVectorOf
30 import androidx.compose.runtime.collection.wrapIntoSet
31 import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
32 import androidx.compose.runtime.internal.AtomicReference
33 import androidx.compose.runtime.internal.SnapshotThreadLocal
34 import androidx.compose.runtime.internal.logError
35 import androidx.compose.runtime.internal.trace
36 import androidx.compose.runtime.platform.SynchronizedObject
37 import androidx.compose.runtime.platform.makeSynchronizedObject
38 import androidx.compose.runtime.platform.synchronized
39 import androidx.compose.runtime.snapshots.MutableSnapshot
40 import androidx.compose.runtime.snapshots.ReaderKind
41 import androidx.compose.runtime.snapshots.Snapshot
42 import androidx.compose.runtime.snapshots.SnapshotApplyResult
43 import androidx.compose.runtime.snapshots.StateObjectImpl
44 import androidx.compose.runtime.snapshots.TransparentObserverMutableSnapshot
45 import androidx.compose.runtime.snapshots.TransparentObserverSnapshot
46 import androidx.compose.runtime.snapshots.fastAll
47 import androidx.compose.runtime.snapshots.fastAny
48 import androidx.compose.runtime.snapshots.fastFilterIndexed
49 import androidx.compose.runtime.snapshots.fastForEach
50 import androidx.compose.runtime.snapshots.fastGroupBy
51 import androidx.compose.runtime.snapshots.fastMap
52 import androidx.compose.runtime.snapshots.fastMapNotNull
53 import androidx.compose.runtime.tooling.CompositionData
54 import androidx.compose.runtime.tooling.CompositionObserverHandle
55 import androidx.compose.runtime.tooling.CompositionRegistrationObserver
56 import kotlin.collections.removeLast as removeLastKt
57 import kotlin.coroutines.Continuation
58 import kotlin.coroutines.CoroutineContext
59 import kotlin.coroutines.EmptyCoroutineContext
60 import kotlin.coroutines.coroutineContext
61 import kotlin.coroutines.resume
62 import kotlinx.coroutines.CancellableContinuation
63 import kotlinx.coroutines.CancellationException
64 import kotlinx.coroutines.CoroutineScope
65 import kotlinx.coroutines.Job
66 import kotlinx.coroutines.cancelAndJoin
67 import kotlinx.coroutines.coroutineScope
68 import kotlinx.coroutines.flow.Flow
69 import kotlinx.coroutines.flow.MutableStateFlow
70 import kotlinx.coroutines.flow.StateFlow
71 import kotlinx.coroutines.flow.collect
72 import kotlinx.coroutines.flow.first
73 import kotlinx.coroutines.flow.takeWhile
74 import kotlinx.coroutines.job
75 import kotlinx.coroutines.launch
76 import kotlinx.coroutines.suspendCancellableCoroutine
77 import kotlinx.coroutines.withContext
78 
79 // TODO: Can we use rootKey for this since all compositions will have an eventual Recomposer parent?
80 private inline val RecomposerCompoundHashKey
81     get() = CompositeKeyHashCode(1000)
82 
83 /**
84  * Runs [block] with a new, active [Recomposer] applying changes in the calling [CoroutineContext].
85  * The [Recomposer] will be [closed][Recomposer.close] after [block] returns.
86  * [withRunningRecomposer] will return once the [Recomposer] is [Recomposer.State.ShutDown] and all
87  * child jobs launched by [block] have [joined][Job.join].
88  */
89 suspend fun <R> withRunningRecomposer(
90     block: suspend CoroutineScope.(recomposer: Recomposer) -> R
91 ): R = coroutineScope {
92     val recomposer = Recomposer(coroutineContext)
93     // Will be cancelled when recomposerJob cancels
94     launch { recomposer.runRecomposeAndApplyChanges() }
95     block(recomposer).also {
96         recomposer.close()
97         recomposer.join()
98     }
99 }
100 
101 /**
102  * Read-only information about a [Recomposer]. Used when code should only monitor the activity of a
103  * [Recomposer], and not attempt to alter its state or create new compositions from it.
104  */
105 interface RecomposerInfo {
106     /** The current [State] of the [Recomposer]. See each [State] value for its meaning. */
107     // TODO: Mirror the currentState/StateFlow API change here once we can safely add
108     // default interface methods. https://youtrack.jetbrains.com/issue/KT-47000
109     val state: Flow<Recomposer.State>
110 
111     /**
112      * `true` if the [Recomposer] has been assigned work to do and it is currently performing that
113      * work or awaiting an opportunity to do so.
114      */
115     val hasPendingWork: Boolean
116 
117     /**
118      * The running count of the number of times the [Recomposer] awoke and applied changes to one or
119      * more [Composer]s. This count is unaffected if the composer awakes and recomposed but
120      * composition did not produce changes to apply.
121      */
122     val changeCount: Long
123 }
124 
125 /** Read only information about [Recomposer] error state. */
126 @InternalComposeApi
127 internal interface RecomposerErrorInfo {
128     /** Exception which forced recomposition to halt. */
129     val cause: Throwable
130 
131     /**
132      * Whether composition can recover from the error by itself. If the error is not recoverable,
133      * recomposer will not react to invalidate calls until state is reloaded.
134      */
135     val recoverable: Boolean
136 }
137 
138 /**
139  * The scheduler for performing recomposition and applying updates to one or more [Composition]s.
140  */
141 // RedundantVisibilityModifier suppressed because metalava picks up internal function overrides
142 // if 'internal' is not explicitly specified - b/171342041
143 // NotCloseable suppressed because this is Kotlin-only common code; [Auto]Closeable not available.
144 @Suppress("RedundantVisibilityModifier", "NotCloseable")
145 @OptIn(InternalComposeApi::class)
146 class Recomposer(effectCoroutineContext: CoroutineContext) : CompositionContext() {
147     /**
148      * This is a running count of the number of times the recomposer awoke and applied changes to
149      * one or more composers. This count is unaffected if the composer awakes and recomposed but
150      * composition did not produce changes to apply.
151      */
152     var changeCount = 0L
153         private set
154 
<lambda>null155     private val broadcastFrameClock = BroadcastFrameClock {
156         synchronized(stateLock) {
157                 deriveStateLocked().also {
158                     if (_state.value <= State.ShuttingDown)
159                         throw CancellationException(
160                             "Recomposer shutdown; frame clock awaiter will never resume",
161                             closeCause
162                         )
163                 }
164             }
165             ?.resume(Unit)
166     }
167 
168     /** Valid operational states of a [Recomposer]. */
169     enum class State {
170         /**
171          * [cancel] was called on the [Recomposer] and all cleanup work has completed. The
172          * [Recomposer] is no longer available for use.
173          */
174         ShutDown,
175 
176         /**
177          * [cancel] was called on the [Recomposer] and it is no longer available for use. Cleanup
178          * work has not yet been fully completed and composition effect coroutines may still be
179          * running.
180          */
181         ShuttingDown,
182 
183         /**
184          * The [Recomposer] is not tracking invalidations for known composers and it will not
185          * recompose them in response to changes. Call [runRecomposeAndApplyChanges] to await and
186          * perform work. This is the initial state of a newly constructed [Recomposer].
187          */
188         Inactive,
189 
190         /**
191          * The [Recomposer] is [Inactive] but at least one effect associated with a managed
192          * composition is awaiting a frame. This frame will not be produced until the [Recomposer]
193          * is [running][runRecomposeAndApplyChanges].
194          */
195         InactivePendingWork,
196 
197         /**
198          * The [Recomposer] is tracking composition and snapshot invalidations but there is
199          * currently no work to do.
200          */
201         Idle,
202 
203         /**
204          * The [Recomposer] has been notified of pending work it must perform and is either actively
205          * performing it or awaiting the appropriate opportunity to perform it. This work may
206          * include invalidated composers that must be recomposed, snapshot state changes that must
207          * be presented to known composers to check for invalidated compositions, or coroutines
208          * awaiting a frame using the Recomposer's [MonotonicFrameClock].
209          */
210         PendingWork
211     }
212 
213     private val stateLock = makeSynchronizedObject()
214 
215     // Begin properties guarded by stateLock
216     private var runnerJob: Job? = null
217     private var closeCause: Throwable? = null
218     private val _knownCompositions = mutableListOf<ControlledComposition>()
219     private var _knownCompositionsCache: List<ControlledComposition>? = null
220     private val knownCompositions
221         get() =
222             _knownCompositionsCache
<lambda>null223                 ?: run {
224                     val compositions = _knownCompositions
225                     val newCache =
226                         if (compositions.isEmpty()) emptyList() else ArrayList(compositions)
227                     _knownCompositionsCache = newCache
228                     newCache
229                 }
230 
231     private var snapshotInvalidations = MutableScatterSet<Any>()
232     private val compositionInvalidations = mutableVectorOf<ControlledComposition>()
233     private val compositionsAwaitingApply = mutableListOf<ControlledComposition>()
234     private val movableContentAwaitingInsert = mutableListOf<MovableContentStateReference>()
235     private val movableContentRemoved =
236         MultiValueMap<MovableContent<Any?>, MovableContentStateReference>()
237     private val movableContentNestedStatesAvailable = NestedContentMap()
238     private val movableContentStatesAvailable =
239         mutableScatterMapOf<MovableContentStateReference, MovableContentState>()
240     private val movableContentNestedExtractionsPending =
241         MultiValueMap<MovableContentStateReference, MovableContentStateReference>()
242     private var failedCompositions: MutableList<ControlledComposition>? = null
243     private var compositionsRemoved: MutableSet<ControlledComposition>? = null
244     private var workContinuation: CancellableContinuation<Unit>? = null
245     private var concurrentCompositionsOutstanding = 0
246     private var isClosed: Boolean = false
247     private var errorState: RecomposerErrorState? = null
248     private var frameClockPaused: Boolean = false
249     // End properties guarded by stateLock
250 
251     private val _state = MutableStateFlow(State.Inactive)
252     private val pausedScopes = SnapshotThreadLocal<MutableScatterSet<RecomposeScopeImpl>?>()
253 
254     /**
255      * A [Job] used as a parent of any effects created by this [Recomposer]'s compositions. Its
256      * cleanup is used to advance to [State.ShuttingDown] or [State.ShutDown].
257      *
258      * Initialized after other state above, since it is possible for [Job.invokeOnCompletion] to run
259      * synchronously during construction if the [Recomposer] is constructed with a completed or
260      * cancelled [Job].
261      */
262     private val effectJob =
<lambda>null263         Job(effectCoroutineContext[Job]).apply {
264             invokeOnCompletion { throwable ->
265                 // Since the running recompose job is operating in a disjoint job if present,
266                 // kick it out and make sure no new ones start if we have one.
267                 val cancellation =
268                     CancellationException("Recomposer effect job completed", throwable)
269 
270                 var continuationToResume: CancellableContinuation<Unit>? = null
271                 synchronized(stateLock) {
272                     val runnerJob = runnerJob
273                     if (runnerJob != null) {
274                         _state.value = State.ShuttingDown
275                         // If the recomposer is closed we will let the runnerJob return from
276                         // runRecomposeAndApplyChanges normally and consider ourselves shut down
277                         // immediately.
278                         if (!isClosed) {
279                             // This is the job hosting frameContinuation; no need to resume it
280                             // otherwise
281                             runnerJob.cancel(cancellation)
282                         } else if (workContinuation != null) {
283                             continuationToResume = workContinuation
284                         }
285                         workContinuation = null
286                         runnerJob.invokeOnCompletion { runnerJobCause ->
287                             synchronized(stateLock) {
288                                 closeCause =
289                                     throwable?.apply {
290                                         runnerJobCause
291                                             ?.takeIf { it !is CancellationException }
292                                             ?.let { addSuppressed(it) }
293                                     }
294                                 _state.value = State.ShutDown
295                             }
296                         }
297                     } else {
298                         closeCause = cancellation
299                         _state.value = State.ShutDown
300                     }
301                 }
302                 continuationToResume?.resume(Unit)
303             }
304         }
305 
306     /** The [effectCoroutineContext] is derived from the parameter of the same name. */
307     override val effectCoroutineContext: CoroutineContext =
308         effectCoroutineContext + broadcastFrameClock + effectJob
309 
310     internal override val recomposeCoroutineContext: CoroutineContext
311         get() = EmptyCoroutineContext
312 
313     private val hasBroadcastFrameClockAwaitersLocked: Boolean
314         get() = !frameClockPaused && broadcastFrameClock.hasAwaiters
315 
316     private val hasBroadcastFrameClockAwaiters: Boolean
<lambda>null317         get() = synchronized(stateLock) { hasBroadcastFrameClockAwaitersLocked }
318 
319     @ExperimentalComposeRuntimeApi
320     private var registrationObservers: MutableObjectList<CompositionRegistrationObserver>? = null
321 
322     /**
323      * Determine the new value of [_state]. Call only while locked on [stateLock]. If it returns a
324      * continuation, that continuation should be resumed after releasing the lock.
325      */
deriveStateLockednull326     private fun deriveStateLocked(): CancellableContinuation<Unit>? {
327         if (_state.value <= State.ShuttingDown) {
328             clearKnownCompositionsLocked()
329             snapshotInvalidations = MutableScatterSet()
330             compositionInvalidations.clear()
331             compositionsAwaitingApply.clear()
332             movableContentAwaitingInsert.clear()
333             failedCompositions = null
334             workContinuation?.cancel()
335             workContinuation = null
336             errorState = null
337             return null
338         }
339 
340         val newState =
341             when {
342                 errorState != null -> {
343                     State.Inactive
344                 }
345                 runnerJob == null -> {
346                     snapshotInvalidations = MutableScatterSet()
347                     compositionInvalidations.clear()
348                     if (hasBroadcastFrameClockAwaitersLocked) State.InactivePendingWork
349                     else State.Inactive
350                 }
351                 compositionInvalidations.isNotEmpty() ||
352                     snapshotInvalidations.isNotEmpty() ||
353                     compositionsAwaitingApply.isNotEmpty() ||
354                     movableContentAwaitingInsert.isNotEmpty() ||
355                     concurrentCompositionsOutstanding > 0 ||
356                     hasBroadcastFrameClockAwaitersLocked -> State.PendingWork
357                 else -> State.Idle
358             }
359 
360         _state.value = newState
361         return if (newState == State.PendingWork) {
362             workContinuation.also { workContinuation = null }
363         } else null
364     }
365 
366     /** `true` if there is still work to do for an active caller of [runRecomposeAndApplyChanges] */
367     private val shouldKeepRecomposing: Boolean
<lambda>null368         get() = synchronized(stateLock) { !isClosed } || effectJob.children.any { it.isActive }
369 
370     /** The current [State] of this [Recomposer]. See each [State] value for its meaning. */
371     @Deprecated("Replaced by currentState as a StateFlow", ReplaceWith("currentState"))
372     public val state: Flow<State>
373         get() = currentState
374 
375     /** The current [State] of this [Recomposer], available synchronously. */
376     public val currentState: StateFlow<State>
377         get() = _state
378 
379     // A separate private object to avoid the temptation of casting a RecomposerInfo
380     // to a Recomposer if Recomposer itself were to implement RecomposerInfo.
381     private inner class RecomposerInfoImpl : RecomposerInfo {
382         override val state: Flow<State>
383             get() = this@Recomposer.currentState
384 
385         override val hasPendingWork: Boolean
386             get() = this@Recomposer.hasPendingWork
387 
388         override val changeCount: Long
389             get() = this@Recomposer.changeCount
390 
391         val currentError: RecomposerErrorInfo?
<lambda>null392             get() = synchronized(stateLock) { this@Recomposer.errorState }
393 
invalidateGroupsWithKeynull394         fun invalidateGroupsWithKey(key: Int) {
395             val compositions: List<ControlledComposition> =
396                 synchronized(stateLock) { knownCompositions }
397             compositions
398                 .fastMapNotNull { it as? CompositionImpl }
399                 .fastForEach { it.invalidateGroupsWithKey(key) }
400         }
401 
saveStateAndDisposeForHotReloadnull402         fun saveStateAndDisposeForHotReload(): List<HotReloadable> {
403             val compositions: List<ControlledComposition> =
404                 synchronized(stateLock) { knownCompositions }
405             return compositions
406                 .fastMapNotNull { it as? CompositionImpl }
407                 .fastMap { HotReloadable(it).apply { clearContent() } }
408         }
409 
resetErrorStatenull410         fun resetErrorState(): RecomposerErrorState? = this@Recomposer.resetErrorState()
411 
412         fun retryFailedCompositions() = this@Recomposer.retryFailedCompositions()
413     }
414 
415     private class HotReloadable(private val composition: CompositionImpl) {
416         private var composable: @Composable () -> Unit = composition.composable
417 
418         fun clearContent() {
419             if (composition.isRoot) {
420                 composition.setContent {}
421             }
422         }
423 
424         fun resetContent() {
425             composition.composable = composable
426         }
427 
428         fun recompose() {
429             if (composition.isRoot) {
430                 composition.setContent(composable)
431             }
432         }
433     }
434 
435     private class RecomposerErrorState(
436         override val recoverable: Boolean,
437         override val cause: Throwable
438     ) : RecomposerErrorInfo
439 
440     private val recomposerInfo = RecomposerInfoImpl()
441 
442     /** Obtain a read-only [RecomposerInfo] for this [Recomposer]. */
asRecomposerInfonull443     fun asRecomposerInfo(): RecomposerInfo = recomposerInfo
444 
445     /**
446      * Propagate all invalidations from `snapshotInvalidations` to all the known compositions.
447      *
448      * @return `true` if the frame has work to do (e.g. [hasFrameWorkLocked])
449      */
450     private fun recordComposerModifications(): Boolean {
451         val changes =
452             synchronized(stateLock) {
453                 if (snapshotInvalidations.isEmpty()) return hasFrameWorkLocked
454                 snapshotInvalidations.wrapIntoSet().also {
455                     snapshotInvalidations = MutableScatterSet()
456                 }
457             }
458         val compositions = synchronized(stateLock) { knownCompositions }
459         var complete = false
460         try {
461             run {
462                 compositions.fastForEach { composition ->
463                     composition.recordModificationsOf(changes)
464 
465                     // Stop dispatching if the recomposer if we detect the recomposer
466                     // is shutdown.
467                     if (_state.value <= State.ShuttingDown) return@run
468                 }
469             }
470             synchronized(stateLock) { snapshotInvalidations = MutableScatterSet() }
471             complete = true
472         } finally {
473             if (!complete) {
474                 // If the previous loop was not complete, we have not sent all of theses
475                 // changes to all the composers so try again after the exception that caused
476                 // the early exit is handled and we can then retry sending the changes.
477                 synchronized(stateLock) { snapshotInvalidations.addAll(changes) }
478             }
479         }
480         return synchronized(stateLock) {
481             if (deriveStateLocked() != null) {
482                 error("called outside of runRecomposeAndApplyChanges")
483             }
484             hasFrameWorkLocked
485         }
486     }
487 
recordComposerModificationsnull488     private inline fun recordComposerModifications(
489         onEachInvalidComposition: (ControlledComposition) -> Unit
490     ) {
491         val changes =
492             synchronized(stateLock) {
493                     snapshotInvalidations.also {
494                         if (it.isNotEmpty()) snapshotInvalidations = MutableScatterSet()
495                     }
496                 }
497                 .wrapIntoSet()
498         if (changes.isNotEmpty()) {
499             knownCompositions.fastForEach { composition ->
500                 composition.recordModificationsOf(changes)
501             }
502         }
503         compositionInvalidations.forEach(onEachInvalidComposition)
504         compositionInvalidations.clear()
505         synchronized(stateLock) {
506             if (deriveStateLocked() != null) {
507                 error("called outside of runRecomposeAndApplyChanges")
508             }
509         }
510     }
511 
registerRunnerJobnull512     private fun registerRunnerJob(callingJob: Job) {
513         synchronized(stateLock) {
514             closeCause?.let { throw it }
515             if (_state.value <= State.ShuttingDown) error("Recomposer shut down")
516             if (runnerJob != null) error("Recomposer already running")
517             runnerJob = callingJob
518             deriveStateLocked()
519         }
520     }
521 
522     /**
523      * Await the invalidation of any associated [Composer]s, recompose them, and apply their changes
524      * to their associated [Composition]s if recomposition is successful.
525      *
526      * While [runRecomposeAndApplyChanges] is running, [awaitIdle] will suspend until there are no
527      * more invalid composers awaiting recomposition.
528      *
529      * This method will not return unless the [Recomposer] is [close]d and all effects in managed
530      * compositions complete. Unhandled failure exceptions from child coroutines will be thrown by
531      * this method.
532      */
parentFrameClocknull533     suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->
534         val toRecompose = mutableListOf<ControlledComposition>()
535         val toInsert = mutableListOf<MovableContentStateReference>()
536         val toApply = mutableListOf<ControlledComposition>()
537         val toLateApply = mutableScatterSetOf<ControlledComposition>()
538         val toComplete = mutableScatterSetOf<ControlledComposition>()
539         val modifiedValues = MutableScatterSet<Any>()
540         val modifiedValuesSet = modifiedValues.wrapIntoSet()
541         val alreadyComposed = mutableScatterSetOf<ControlledComposition>()
542 
543         fun clearRecompositionState() {
544             synchronized(stateLock) {
545                 toRecompose.clear()
546                 toInsert.clear()
547 
548                 toApply.fastForEach {
549                     it.abandonChanges()
550                     recordFailedCompositionLocked(it)
551                 }
552                 toApply.clear()
553 
554                 toLateApply.forEach {
555                     it.abandonChanges()
556                     recordFailedCompositionLocked(it)
557                 }
558                 toLateApply.clear()
559 
560                 toComplete.forEach { it.changesApplied() }
561                 toComplete.clear()
562 
563                 modifiedValues.clear()
564 
565                 alreadyComposed.forEach {
566                     it.abandonChanges()
567                     recordFailedCompositionLocked(it)
568                 }
569                 alreadyComposed.clear()
570             }
571         }
572 
573         fun fillToInsert() {
574             toInsert.clear()
575             synchronized(stateLock) {
576                 movableContentAwaitingInsert.fastForEach { toInsert += it }
577                 movableContentAwaitingInsert.clear()
578             }
579         }
580 
581         while (shouldKeepRecomposing) {
582             awaitWorkAvailable()
583 
584             // Don't await a new frame if we don't have frame-scoped work
585             if (!recordComposerModifications()) continue
586 
587             // Align work with the next frame to coalesce changes.
588             // Note: it is possible to resume from the above with no recompositions pending,
589             // instead someone might be awaiting our frame clock dispatch below.
590             // We use the cached frame clock from above not just so that we don't locate it
591             // each time, but because we've installed the broadcastFrameClock as the scope
592             // clock above for user code to locate.
593             parentFrameClock.withFrameNanos { frameTime ->
594                 // Dispatch MonotonicFrameClock frames first; this may produce new
595                 // composer invalidations that we must handle during the same frame.
596                 if (hasBroadcastFrameClockAwaiters) {
597                     trace("Recomposer:animation") {
598                         // Propagate the frame time to anyone who is awaiting from the
599                         // recomposer clock.
600                         broadcastFrameClock.sendFrame(frameTime)
601 
602                         // Ensure any global changes are observed
603                         Snapshot.sendApplyNotifications()
604                     }
605                 }
606 
607                 trace("Recomposer:recompose") {
608                     // Drain any composer invalidations from snapshot changes and record
609                     // composers to work on
610                     recordComposerModifications()
611                     synchronized(stateLock) {
612                         compositionInvalidations.forEach { toRecompose += it }
613                         compositionInvalidations.clear()
614                     }
615 
616                     // Perform recomposition for any invalidated composers
617                     modifiedValues.clear()
618                     alreadyComposed.clear()
619                     while (toRecompose.isNotEmpty() || toInsert.isNotEmpty()) {
620                         try {
621                             toRecompose.fastForEach { composition ->
622                                 performRecompose(composition, modifiedValues)?.let { toApply += it }
623                                 alreadyComposed.add(composition)
624                             }
625                         } catch (e: Throwable) {
626                             processCompositionError(e, recoverable = true)
627                             clearRecompositionState()
628                             return@withFrameNanos
629                         } finally {
630                             toRecompose.clear()
631                         }
632 
633                         // Find any trailing recompositions that need to be composed because
634                         // of a value change by a composition. This can happen, for example, if
635                         // a CompositionLocal changes in a parent and was read in a child
636                         // composition that was otherwise valid.
637                         if (modifiedValues.isNotEmpty() || compositionInvalidations.isNotEmpty()) {
638                             synchronized(stateLock) {
639                                 knownCompositions.fastForEach { value ->
640                                     if (
641                                         value !in alreadyComposed &&
642                                             value.observesAnyOf(modifiedValuesSet)
643                                     ) {
644                                         toRecompose += value
645                                     }
646                                 }
647 
648                                 // Composable lambda is a special kind of value that is not observed
649                                 // by the snapshot system, but invalidates composition scope
650                                 // directly instead.
651                                 compositionInvalidations.removeIf { value ->
652                                     if (value !in alreadyComposed && value !in toRecompose) {
653                                         toRecompose += value
654                                         true
655                                     } else {
656                                         false
657                                     }
658                                 }
659                             }
660                         }
661 
662                         if (toRecompose.isEmpty()) {
663                             try {
664                                 fillToInsert()
665                                 while (toInsert.isNotEmpty()) {
666                                     toLateApply += performInsertValues(toInsert, modifiedValues)
667                                     fillToInsert()
668                                 }
669                             } catch (e: Throwable) {
670                                 processCompositionError(e, recoverable = true)
671                                 clearRecompositionState()
672                                 return@withFrameNanos
673                             }
674                         }
675                     }
676 
677                     // This is an optimization to avoid reallocating TransparentSnapshot for each
678                     // observeChanges within `apply`. Many modifiers use observation in `onAttach`
679                     // and other lifecycle methods, and allocations can be mitigated by updating
680                     // read observer in the snapshot allocated here.
681                     withTransparentSnapshot {
682                         if (toApply.isNotEmpty()) {
683                             changeCount++
684 
685                             // Perform apply changes
686                             try {
687                                 // We could do toComplete += toApply but doing it like below
688                                 // avoids unnecessary allocations since toApply is a mutable list
689                                 // toComplete += toApply
690                                 toApply.fastForEach { composition -> toComplete.add(composition) }
691                                 toApply.fastForEach { composition -> composition.applyChanges() }
692                             } catch (e: Throwable) {
693                                 processCompositionError(e)
694                                 clearRecompositionState()
695                                 return@withFrameNanos
696                             } finally {
697                                 toApply.clear()
698                             }
699                         }
700 
701                         if (toLateApply.isNotEmpty()) {
702                             try {
703                                 toComplete += toLateApply
704                                 toLateApply.forEach { composition ->
705                                     composition.applyLateChanges()
706                                 }
707                             } catch (e: Throwable) {
708                                 processCompositionError(e)
709                                 clearRecompositionState()
710                                 return@withFrameNanos
711                             } finally {
712                                 toLateApply.clear()
713                             }
714                         }
715 
716                         if (toComplete.isNotEmpty()) {
717                             try {
718                                 toComplete.forEach { composition -> composition.changesApplied() }
719                             } catch (e: Throwable) {
720                                 processCompositionError(e)
721                                 clearRecompositionState()
722                                 return@withFrameNanos
723                             } finally {
724                                 toComplete.clear()
725                             }
726                         }
727                     }
728 
729                     synchronized(stateLock) { deriveStateLocked() }
730 
731                     // Ensure any state objects that were written during apply changes, e.g. nodes
732                     // with state-backed properties, get sent apply notifications to invalidate
733                     // anything observing the nodes. Call this method instead of
734                     // sendApplyNotifications to ensure that objects that were _created_ in this
735                     // snapshot are also considered changed after this point.
736                     Snapshot.notifyObjectsInitialized()
737                     alreadyComposed.clear()
738                     modifiedValues.clear()
739                     compositionsRemoved = null
740                 }
741             }
742 
743             discardUnusedMovableContentState()
744         }
745     }
746 
processCompositionErrornull747     private fun processCompositionError(
748         e: Throwable,
749         failedInitialComposition: ControlledComposition? = null,
750         recoverable: Boolean = false,
751     ) {
752         if (_hotReloadEnabled.get() && e !is ComposeRuntimeError) {
753             synchronized(stateLock) {
754                 logError("Error was captured in composition while live edit was enabled.", e)
755 
756                 compositionsAwaitingApply.clear()
757                 compositionInvalidations.clear()
758                 snapshotInvalidations = MutableScatterSet()
759 
760                 movableContentAwaitingInsert.clear()
761                 movableContentRemoved.clear()
762                 movableContentStatesAvailable.clear()
763 
764                 errorState = RecomposerErrorState(recoverable = recoverable, cause = e)
765 
766                 if (failedInitialComposition != null) {
767                     recordFailedCompositionLocked(failedInitialComposition)
768                 }
769 
770                 deriveStateLocked()
771             }
772         } else {
773             // withFrameNanos uses `runCatching` to ensure that crashes are not propagated to
774             // AndroidUiDispatcher. This means that errors that happen during recomposition might
775             // be delayed by a frame and swallowed if composed into inconsistent state caused by
776             // the error.
777             // Common case is subcomposition: if measure occurs after recomposition has thrown,
778             // composeInitial will throw because of corrupted composition while original exception
779             // won't be recorded.
780             synchronized(stateLock) {
781                 val errorState = errorState
782                 if (errorState == null) {
783                     // Record exception if current error state is empty.
784                     this.errorState = RecomposerErrorState(recoverable = false, e)
785                 } else {
786                     // Re-throw original cause if we recorded it previously.
787                     throw errorState.cause
788                 }
789             }
790 
791             throw e
792         }
793     }
794 
withTransparentSnapshotnull795     private inline fun withTransparentSnapshot(block: () -> Unit) {
796         val currentSnapshot = Snapshot.current
797 
798         val snapshot =
799             if (currentSnapshot is MutableSnapshot) {
800                 TransparentObserverMutableSnapshot(
801                     currentSnapshot,
802                     null,
803                     null,
804                     mergeParentObservers = true,
805                     ownsParentSnapshot = false
806                 )
807             } else {
808                 TransparentObserverSnapshot(
809                     currentSnapshot,
810                     null,
811                     mergeParentObservers = true,
812                     ownsParentSnapshot = false
813                 )
814             }
815         try {
816             snapshot.enter(block)
817         } finally {
818             snapshot.dispose()
819         }
820     }
821 
822     @OptIn(ExperimentalComposeRuntimeApi::class)
clearKnownCompositionsLockednull823     private fun clearKnownCompositionsLocked() {
824         registrationObservers?.forEach { observer ->
825             knownCompositions.forEach { composition ->
826                 observer.onCompositionUnregistered(this, composition)
827             }
828         }
829         _knownCompositions.clear()
830         _knownCompositionsCache = emptyList()
831     }
832 
833     @OptIn(ExperimentalComposeRuntimeApi::class)
removeKnownCompositionLockednull834     private fun removeKnownCompositionLocked(composition: ControlledComposition) {
835         if (_knownCompositions.remove(composition)) {
836             _knownCompositionsCache = null
837             registrationObservers?.forEach { it.onCompositionUnregistered(this, composition) }
838         }
839     }
840 
841     @OptIn(ExperimentalComposeRuntimeApi::class)
addKnownCompositionLockednull842     private fun addKnownCompositionLocked(composition: ControlledComposition) {
843         _knownCompositions += composition
844         _knownCompositionsCache = null
845         registrationObservers?.forEach { it.onCompositionRegistered(this, composition) }
846     }
847 
848     @ExperimentalComposeRuntimeApi
addCompositionRegistrationObservernull849     internal fun addCompositionRegistrationObserver(
850         observer: CompositionRegistrationObserver
851     ): CompositionObserverHandle {
852         synchronized(stateLock) {
853             val observers =
854                 registrationObservers
855                     ?: MutableObjectList<CompositionRegistrationObserver>().also {
856                         registrationObservers = it
857                     }
858 
859             observers += observer
860             _knownCompositions.fastForEach { composition ->
861                 observer.onCompositionRegistered(this@Recomposer, composition)
862             }
863         }
864 
865         return object : CompositionObserverHandle {
866             override fun dispose() {
867                 synchronized(stateLock) { registrationObservers?.remove(observer) }
868             }
869         }
870     }
871 
resetErrorStatenull872     private fun resetErrorState(): RecomposerErrorState? {
873         val errorState =
874             synchronized(stateLock) {
875                 val error = errorState
876                 if (error != null) {
877                     errorState = null
878                     deriveStateLocked()
879                 }
880                 error
881             }
882         return errorState
883     }
884 
retryFailedCompositionsnull885     private fun retryFailedCompositions() {
886         val compositionsToRetry =
887             synchronized(stateLock) { failedCompositions.also { failedCompositions = null } }
888                 ?: return
889         try {
890             while (compositionsToRetry.isNotEmpty()) {
891                 val composition = compositionsToRetry.removeLastKt()
892                 if (composition !is CompositionImpl) continue
893 
894                 composition.invalidateAll()
895                 composition.setContent(composition.composable)
896 
897                 if (errorState != null) break
898             }
899         } finally {
900             if (compositionsToRetry.isNotEmpty()) {
901                 // If we did not complete the last list then add the remaining compositions back
902                 // into the failedCompositions list
903                 synchronized(stateLock) {
904                     compositionsToRetry.fastForEach { recordFailedCompositionLocked(it) }
905                 }
906             }
907         }
908     }
909 
recordFailedCompositionLockednull910     private fun recordFailedCompositionLocked(composition: ControlledComposition) {
911         val failedCompositions =
912             failedCompositions
913                 ?: mutableListOf<ControlledComposition>().also { failedCompositions = it }
914 
915         if (composition !in failedCompositions) {
916             failedCompositions += composition
917         }
918         removeKnownCompositionLocked(composition)
919     }
920 
921     /**
922      * Await the invalidation of any associated [Composer]s, recompose them, and apply their changes
923      * to their associated [Composition]s if recomposition is successful.
924      *
925      * While [runRecomposeConcurrentlyAndApplyChanges] is running, [awaitIdle] will suspend until
926      * there are no more invalid composers awaiting recomposition.
927      *
928      * Recomposition of invalidated composers will occur in [recomposeCoroutineContext].
929      * [recomposeCoroutineContext] must not contain a [Job].
930      *
931      * This method will not return unless the [Recomposer] is [close]d and all effects in managed
932      * compositions complete. Unhandled failure exceptions from child coroutines will be thrown by
933      * this method.
934      */
935     @ExperimentalComposeApi
runRecomposeConcurrentlyAndApplyChangesnull936     suspend fun runRecomposeConcurrentlyAndApplyChanges(
937         recomposeCoroutineContext: CoroutineContext
938     ) = recompositionRunner { parentFrameClock ->
939         requirePrecondition(recomposeCoroutineContext[Job] == null) {
940             "recomposeCoroutineContext may not contain a Job; found " +
941                 recomposeCoroutineContext[Job]
942         }
943         val recomposeCoroutineScope =
944             CoroutineScope(coroutineContext + recomposeCoroutineContext + Job(coroutineContext.job))
945         val frameSignal = ProduceFrameSignal()
946         val frameLoop = launch { runFrameLoop(parentFrameClock, frameSignal) }
947         while (shouldKeepRecomposing) {
948             awaitWorkAvailable()
949 
950             // Don't await a new frame if we don't have frame-scoped work
951             recordComposerModifications { composition ->
952                 synchronized(stateLock) { concurrentCompositionsOutstanding++ }
953                 recomposeCoroutineScope.launch(composition.recomposeCoroutineContext) {
954                     val changedComposition = performRecompose(composition, null)
955                     synchronized(stateLock) {
956                             changedComposition?.let { compositionsAwaitingApply += it }
957                             concurrentCompositionsOutstanding--
958                             deriveStateLocked()
959                         }
960                         ?.resume(Unit)
961                 }
962             }
963             synchronized(stateLock) {
964                     if (hasConcurrentFrameWorkLocked) frameSignal.requestFrameLocked() else null
965                 }
966                 ?.resume(Unit)
967         }
968         recomposeCoroutineScope.coroutineContext.job.cancelAndJoin()
969         frameLoop.cancelAndJoin()
970     }
971 
runFrameLoopnull972     private suspend fun runFrameLoop(
973         parentFrameClock: MonotonicFrameClock,
974         frameSignal: ProduceFrameSignal
975     ) {
976         val toRecompose = mutableListOf<ControlledComposition>()
977         val toApply = mutableListOf<ControlledComposition>()
978         while (true) {
979             frameSignal.awaitFrameRequest(stateLock)
980             // Align applying changes to the frame.
981             // Note: it is possible to resume from the above with no recompositions pending,
982             // instead someone might be awaiting our frame clock dispatch below.
983             // We use the cached frame clock from above not just so that we don't locate it
984             // each time, but because we've installed the broadcastFrameClock as the scope
985             // clock above for user code to locate.
986             parentFrameClock.withFrameNanos { frameTime ->
987                 // Dispatch MonotonicFrameClock frames first; this may produce new
988                 // composer invalidations that we must handle during the same frame.
989                 if (hasBroadcastFrameClockAwaiters) {
990                     trace("Recomposer:animation") {
991                         // Propagate the frame time to anyone who is awaiting from the
992                         // recomposer clock.
993                         broadcastFrameClock.sendFrame(frameTime)
994 
995                         // Ensure any global changes are observed
996                         Snapshot.sendApplyNotifications()
997                     }
998                 }
999 
1000                 trace("Recomposer:recompose") {
1001                     // Drain any composer invalidations from snapshot changes and record
1002                     // composers to work on.
1003                     // We'll do these synchronously to make the current frame.
1004                     recordComposerModifications()
1005                     synchronized(stateLock) {
1006                         compositionsAwaitingApply.fastForEach { toApply += it }
1007                         compositionsAwaitingApply.clear()
1008                         compositionInvalidations.forEach { toRecompose += it }
1009                         compositionInvalidations.clear()
1010                         frameSignal.takeFrameRequestLocked()
1011                     }
1012 
1013                     // Perform recomposition for any invalidated composers
1014                     val modifiedValues = MutableScatterSet<Any>()
1015                     try {
1016                         toRecompose.fastForEach { composer ->
1017                             performRecompose(composer, modifiedValues)?.let { toApply += it }
1018                         }
1019                     } finally {
1020                         toRecompose.clear()
1021                     }
1022 
1023                     // Perform any value inserts
1024 
1025                     if (toApply.isNotEmpty()) changeCount++
1026 
1027                     // Perform apply changes
1028                     try {
1029                         toApply.fastForEach { composition -> composition.applyChanges() }
1030                     } finally {
1031                         toApply.clear()
1032                     }
1033 
1034                     synchronized(stateLock) { deriveStateLocked() }
1035                 }
1036             }
1037         }
1038     }
1039 
1040     private val hasSchedulingWork: Boolean
1041         get() =
<lambda>null1042             synchronized(stateLock) {
1043                 snapshotInvalidations.isNotEmpty() ||
1044                     compositionInvalidations.isNotEmpty() ||
1045                     hasBroadcastFrameClockAwaitersLocked
1046             }
1047 
awaitWorkAvailablenull1048     private suspend fun awaitWorkAvailable() {
1049         if (!hasSchedulingWork) {
1050             // NOTE: Do not remove the `<Unit>` from the next line even if the IDE reports it as
1051             // redundant. Removing this causes the Kotlin compiler to crash without reporting
1052             // an error message
1053             suspendCancellableCoroutine<Unit> { co ->
1054                 synchronized(stateLock) {
1055                         if (hasSchedulingWork) {
1056                             co
1057                         } else {
1058                             workContinuation = co
1059                             null
1060                         }
1061                     }
1062                     ?.resume(Unit)
1063             }
1064         }
1065     }
1066 
1067     @OptIn(ExperimentalComposeApi::class)
recompositionRunnernull1068     private suspend fun recompositionRunner(
1069         block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit
1070     ) {
1071         val parentFrameClock = coroutineContext.monotonicFrameClock
1072         withContext(broadcastFrameClock) {
1073             // Enforce mutual exclusion of callers; register self as current runner
1074             val callingJob = coroutineContext.job
1075             registerRunnerJob(callingJob)
1076 
1077             // Observe snapshot changes and propagate them to known composers only from
1078             // this caller's dispatcher, never working with the same composer in parallel.
1079             // unregisterApplyObserver is called as part of the big finally below
1080             val unregisterApplyObserver =
1081                 Snapshot.registerApplyObserver { changed, _ ->
1082                     synchronized(stateLock) {
1083                             if (_state.value >= State.Idle) {
1084                                 val snapshotInvalidations = snapshotInvalidations
1085                                 changed.fastForEach {
1086                                     if (
1087                                         it is StateObjectImpl &&
1088                                             !it.isReadIn(ReaderKind.Composition)
1089                                     ) {
1090                                         // continue if we know that state is never read in
1091                                         // composition
1092                                         return@fastForEach
1093                                     }
1094                                     snapshotInvalidations.add(it)
1095                                 }
1096                                 deriveStateLocked()
1097                             } else null
1098                         }
1099                         ?.resume(Unit)
1100                 }
1101 
1102             addRunning(recomposerInfo)
1103 
1104             try {
1105                 // Invalidate all registered composers when we start since we weren't observing
1106                 // snapshot changes on their behalf. Assume anything could have changed.
1107                 synchronized(stateLock) { knownCompositions }.fastForEach { it.invalidateAll() }
1108 
1109                 coroutineScope { block(parentFrameClock) }
1110             } finally {
1111                 unregisterApplyObserver.dispose()
1112                 synchronized(stateLock) {
1113                     if (runnerJob === callingJob) {
1114                         runnerJob = null
1115                     }
1116                     deriveStateLocked()
1117                 }
1118                 removeRunning(recomposerInfo)
1119             }
1120         }
1121     }
1122 
1123     /**
1124      * Permanently shut down this [Recomposer] for future use. [currentState] will immediately
1125      * reflect [State.ShuttingDown] (or a lower state) before this call returns. All ongoing
1126      * recompositions will stop, new composer invalidations with this [Recomposer] at the root will
1127      * no longer occur, and any [LaunchedEffect]s currently running in compositions managed by this
1128      * [Recomposer] will be cancelled. Any [rememberCoroutineScope] scopes from compositions managed
1129      * by this [Recomposer] will also be cancelled. See [join] to await the completion of all of
1130      * these outstanding tasks.
1131      */
cancelnull1132     fun cancel() {
1133         // Move to State.ShuttingDown immediately rather than waiting for effectJob to join
1134         // if we're cancelling to shut down the Recomposer. This permits other client code
1135         // to use `state.first { it < State.Idle }` or similar to reliably and immediately detect
1136         // that the recomposer can no longer be used.
1137         // It looks like a CAS loop would be more appropriate here, but other occurrences
1138         // of taking stateLock assume that the state cannot change without holding it.
1139         synchronized(stateLock) {
1140             if (_state.value >= State.Idle) {
1141                 _state.value = State.ShuttingDown
1142             }
1143         }
1144         effectJob.cancel()
1145     }
1146 
1147     /**
1148      * Close this [Recomposer]. Once all effects launched by managed compositions complete, any
1149      * active call to [runRecomposeAndApplyChanges] will return normally and this [Recomposer] will
1150      * be [State.ShutDown]. See [join] to await the completion of all of these outstanding tasks.
1151      */
closenull1152     fun close() {
1153         if (effectJob.complete()) {
1154             synchronized(stateLock) { isClosed = true }
1155         }
1156     }
1157 
1158     /** Await the completion of a [cancel] operation. */
joinnull1159     suspend fun join() {
1160         currentState.first { it == State.ShutDown }
1161     }
1162 
1163     internal override fun composeInitial(
1164         composition: ControlledComposition,
1165         content: @Composable () -> Unit
1166     ) {
1167         val composerWasComposing = composition.isComposing
1168         try {
<lambda>null1169             composing(composition, null) { composition.composeContent(content) }
1170         } catch (e: Throwable) {
1171             processCompositionError(e, composition, recoverable = true)
1172             return
1173         }
1174 
1175         // TODO(b/143755743)
1176         if (!composerWasComposing) {
1177             Snapshot.notifyObjectsInitialized()
1178         }
1179 
<lambda>null1180         synchronized(stateLock) {
1181             if (_state.value > State.ShuttingDown) {
1182                 if (composition !in knownCompositions) {
1183                     addKnownCompositionLocked(composition)
1184                 }
1185             }
1186         }
1187 
1188         try {
1189             performInitialMovableContentInserts(composition)
1190         } catch (e: Throwable) {
1191             processCompositionError(e, composition, recoverable = true)
1192             return
1193         }
1194 
1195         try {
1196             composition.applyChanges()
1197             composition.applyLateChanges()
1198         } catch (e: Throwable) {
1199             processCompositionError(e)
1200             return
1201         }
1202 
1203         if (!composerWasComposing) {
1204             // Ensure that any state objects created during applyChanges are seen as changed
1205             // if modified after this call.
1206             Snapshot.notifyObjectsInitialized()
1207         }
1208     }
1209 
1210     internal override fun composeInitialPaused(
1211         composition: ControlledComposition,
1212         shouldPause: ShouldPauseCallback,
1213         content: @Composable () -> Unit
1214     ): ScatterSet<RecomposeScopeImpl> {
1215         return try {
<lambda>null1216             composition.pausable(shouldPause) {
1217                 composeInitial(composition, content)
1218                 pausedScopes.get() ?: emptyScatterSet()
1219             }
1220         } finally {
1221             pausedScopes.set(null)
1222         }
1223     }
1224 
recomposePausednull1225     internal override fun recomposePaused(
1226         composition: ControlledComposition,
1227         shouldPause: ShouldPauseCallback,
1228         invalidScopes: ScatterSet<RecomposeScopeImpl>
1229     ): ScatterSet<RecomposeScopeImpl> {
1230         return try {
1231             recordComposerModifications()
1232             composition.recordModificationsOf(invalidScopes.wrapIntoSet())
1233             composition.pausable(shouldPause) {
1234                 val needsApply = performRecompose(composition, null)
1235                 if (needsApply != null) {
1236                     performInitialMovableContentInserts(composition)
1237                     needsApply.applyChanges()
1238                     needsApply.applyLateChanges()
1239                 }
1240                 pausedScopes.get() ?: emptyScatterSet()
1241             }
1242         } finally {
1243             pausedScopes.set(null)
1244         }
1245     }
1246 
reportPausedScopenull1247     override fun reportPausedScope(scope: RecomposeScopeImpl) {
1248         val scopes =
1249             pausedScopes.get()
1250                 ?: run {
1251                     val newScopes = mutableScatterSetOf<RecomposeScopeImpl>()
1252                     pausedScopes.set(newScopes)
1253                     newScopes
1254                 }
1255         scopes.add(scope)
1256     }
1257 
performInitialMovableContentInsertsnull1258     private fun performInitialMovableContentInserts(composition: ControlledComposition) {
1259         synchronized(stateLock) {
1260             if (!movableContentAwaitingInsert.fastAny { it.composition == composition }) return
1261         }
1262         val toInsert = mutableListOf<MovableContentStateReference>()
1263         fun fillToInsert() {
1264             toInsert.clear()
1265             synchronized(stateLock) {
1266                 val iterator = movableContentAwaitingInsert.iterator()
1267                 while (iterator.hasNext()) {
1268                     val value = iterator.next()
1269                     if (value.composition == composition) {
1270                         toInsert.add(value)
1271                         iterator.remove()
1272                     }
1273                 }
1274             }
1275         }
1276         fillToInsert()
1277         while (toInsert.isNotEmpty()) {
1278             performInsertValues(toInsert, null)
1279             fillToInsert()
1280         }
1281     }
1282 
performRecomposenull1283     private fun performRecompose(
1284         composition: ControlledComposition,
1285         modifiedValues: MutableScatterSet<Any>?
1286     ): ControlledComposition? {
1287         if (
1288             composition.isComposing ||
1289                 composition.isDisposed ||
1290                 compositionsRemoved?.contains(composition) == true
1291         )
1292             return null
1293 
1294         return if (
1295             composing(composition, modifiedValues) {
1296                 if (modifiedValues?.isNotEmpty() == true) {
1297                     // Record write performed by a previous composition as if they happened during
1298                     // composition.
1299                     composition.prepareCompose {
1300                         modifiedValues.forEach { composition.recordWriteOf(it) }
1301                     }
1302                 }
1303                 composition.recompose()
1304             }
1305         )
1306             composition
1307         else null
1308     }
1309 
1310     @OptIn(ExperimentalComposeApi::class)
performInsertValuesnull1311     private fun performInsertValues(
1312         references: List<MovableContentStateReference>,
1313         modifiedValues: MutableScatterSet<Any>?
1314     ): List<ControlledComposition> {
1315         val tasks = references.fastGroupBy { it.composition }
1316         for ((composition, refs) in tasks) {
1317             runtimeCheck(!composition.isComposing)
1318             composing(composition, modifiedValues) {
1319                 // Map insert movable content to movable content states that have been released
1320                 // during `performRecompose`.
1321                 val pairs =
1322                     synchronized(stateLock) {
1323                         refs
1324                             .fastMap { reference ->
1325                                 reference to
1326                                     movableContentRemoved.removeLast(reference.content).also {
1327                                         if (it != null) {
1328                                             movableContentNestedStatesAvailable.usedContainer(it)
1329                                         }
1330                                     }
1331                             }
1332                             .let { pairs ->
1333                                 // Check for any nested states
1334                                 if (
1335                                     ComposeRuntimeFlags.isMovingNestedMovableContentEnabled &&
1336                                         pairs.fastAny {
1337                                             it.second == null &&
1338                                                 it.first.content in
1339                                                     movableContentNestedStatesAvailable
1340                                         }
1341                                 ) {
1342                                     // We have at least one nested state we could use, if a state
1343                                     // is available for the container then schedule the state to be
1344                                     // removed from the container when it is released.
1345                                     pairs.map { pair ->
1346                                         if (pair.second == null) {
1347                                             val nestedContentReference =
1348                                                 movableContentNestedStatesAvailable.removeLast(
1349                                                     pair.first.content
1350                                                 )
1351                                             if (nestedContentReference == null) return@map pair
1352                                             val content = nestedContentReference.content
1353                                             val container = nestedContentReference.container
1354                                             movableContentNestedExtractionsPending.add(
1355                                                 container,
1356                                                 content
1357                                             )
1358                                             pair.first to content
1359                                         } else pair
1360                                     }
1361                                 } else pairs
1362                             }
1363                     }
1364 
1365                 // Avoid mixing creating new content with moving content as the moved content
1366                 // may release content when it is moved as it is recomposed when move.
1367                 val toInsert =
1368                     if (
1369                         pairs.fastAll { it.second == null } || pairs.fastAll { it.second != null }
1370                     ) {
1371                         pairs
1372                     } else {
1373                         // Return the content not moving to the awaiting list. These will come back
1374                         // here in the next iteration of the caller's loop and either have content
1375                         // to move or by still needing to create the content.
1376                         val toReturn =
1377                             pairs.fastMapNotNull { item ->
1378                                 if (item.second == null) item.first else null
1379                             }
1380                         synchronized(stateLock) { movableContentAwaitingInsert += toReturn }
1381 
1382                         // Only insert the moving content this time
1383                         pairs.fastFilterIndexed { _, item -> item.second != null }
1384                     }
1385 
1386                 // toInsert is guaranteed to be not empty as,
1387                 // 1) refs is guaranteed to be not empty as a condition of groupBy
1388                 // 2) pairs is guaranteed to be not empty as it is a map of refs
1389                 // 3) toInsert is guaranteed to not be empty because the toReturn and toInsert
1390                 //    lists have at least one item by the condition of the guard in the if
1391                 //    expression. If one would be empty the condition is true and the filter is not
1392                 //    performed. As both have at least one item toInsert has at least one item. If
1393                 //    the filter is not performed the list is pairs which has at least one item.
1394                 composition.insertMovableContent(toInsert)
1395             }
1396         }
1397         return tasks.keys.toList()
1398     }
1399 
discardUnusedMovableContentStatenull1400     private fun discardUnusedMovableContentState() {
1401         val unusedValues =
1402             synchronized(stateLock) {
1403                 if (movableContentRemoved.isNotEmpty()) {
1404                     val references = movableContentRemoved.values()
1405                     movableContentRemoved.clear()
1406                     movableContentNestedStatesAvailable.clear()
1407                     movableContentNestedExtractionsPending.clear()
1408                     val unusedValues =
1409                         references.fastMap { it to movableContentStatesAvailable[it] }
1410                     movableContentStatesAvailable.clear()
1411                     unusedValues
1412                 } else emptyObjectList()
1413             }
1414         unusedValues.forEach { (reference, state) ->
1415             if (state != null) {
1416                 reference.composition.disposeUnusedMovableContent(state)
1417             }
1418         }
1419     }
1420 
readObserverOfnull1421     private fun readObserverOf(composition: ControlledComposition): (Any) -> Unit {
1422         return { value -> composition.recordReadOf(value) }
1423     }
1424 
writeObserverOfnull1425     private fun writeObserverOf(
1426         composition: ControlledComposition,
1427         modifiedValues: MutableScatterSet<Any>?
1428     ): (Any) -> Unit {
1429         return { value ->
1430             composition.recordWriteOf(value)
1431             modifiedValues?.add(value)
1432         }
1433     }
1434 
composingnull1435     private inline fun <T> composing(
1436         composition: ControlledComposition,
1437         modifiedValues: MutableScatterSet<Any>?,
1438         block: () -> T
1439     ): T {
1440         val snapshot =
1441             Snapshot.takeMutableSnapshot(
1442                 readObserverOf(composition),
1443                 writeObserverOf(composition, modifiedValues)
1444             )
1445         try {
1446             return snapshot.enter(block)
1447         } finally {
1448             applyAndCheck(snapshot)
1449         }
1450     }
1451 
applyAndChecknull1452     private fun applyAndCheck(snapshot: MutableSnapshot) {
1453         try {
1454             val applyResult = snapshot.apply()
1455             if (applyResult is SnapshotApplyResult.Failure) {
1456                 error(
1457                     "Unsupported concurrent change during composition. A state object was " +
1458                         "modified by composition as well as being modified outside composition."
1459                 )
1460             }
1461         } finally {
1462             snapshot.dispose()
1463         }
1464     }
1465 
1466     /**
1467      * `true` if this [Recomposer] has any pending work scheduled, regardless of whether or not it
1468      * is currently [running][runRecomposeAndApplyChanges].
1469      */
1470     val hasPendingWork: Boolean
1471         get() =
<lambda>null1472             synchronized(stateLock) {
1473                 snapshotInvalidations.isNotEmpty() ||
1474                     compositionInvalidations.isNotEmpty() ||
1475                     concurrentCompositionsOutstanding > 0 ||
1476                     compositionsAwaitingApply.isNotEmpty() ||
1477                     hasBroadcastFrameClockAwaitersLocked
1478             }
1479 
1480     private val hasFrameWorkLocked: Boolean
1481         get() = compositionInvalidations.isNotEmpty() || hasBroadcastFrameClockAwaitersLocked
1482 
1483     private val hasConcurrentFrameWorkLocked: Boolean
1484         get() = compositionsAwaitingApply.isNotEmpty() || hasBroadcastFrameClockAwaitersLocked
1485 
1486     /**
1487      * Suspends until the currently pending recomposition frame is complete. Any recomposition for
1488      * this recomposer triggered by actions before this call begins will be complete and applied (if
1489      * recomposition was successful) when this call returns.
1490      *
1491      * If [runRecomposeAndApplyChanges] is not currently running the [Recomposer] is considered idle
1492      * and this method will not suspend.
1493      */
awaitIdlenull1494     suspend fun awaitIdle() {
1495         currentState.takeWhile { it > State.Idle }.collect()
1496     }
1497 
1498     /**
1499      * Pause broadcasting the frame clock while recomposing. This effectively pauses animations, or
1500      * any other use of the [withFrameNanos], while the frame clock is paused.
1501      *
1502      * [pauseCompositionFrameClock] should be called when the recomposer is not being displayed for
1503      * some reason such as not being the current activity in Android, for example.
1504      *
1505      * Calls to [pauseCompositionFrameClock] are thread-safe and idempotent (calling it when the
1506      * frame clock is already paused is a no-op).
1507      */
pauseCompositionFrameClocknull1508     fun pauseCompositionFrameClock() {
1509         synchronized(stateLock) { frameClockPaused = true }
1510     }
1511 
1512     /**
1513      * Resume broadcasting the frame clock after is has been paused. Pending calls to
1514      * [withFrameNanos] will start receiving frame clock broadcasts at the beginning of the frame
1515      * and a frame will be requested if there are pending calls to [withFrameNanos] if a frame has
1516      * not already been scheduled.
1517      *
1518      * Calls to [resumeCompositionFrameClock] are thread-safe and idempotent (calling it when the
1519      * frame clock is running is a no-op).
1520      */
resumeCompositionFrameClocknull1521     fun resumeCompositionFrameClock() {
1522         synchronized(stateLock) {
1523                 if (frameClockPaused) {
1524                     frameClockPaused = false
1525                     deriveStateLocked()
1526                 } else null
1527             }
1528             ?.resume(Unit)
1529     }
1530 
1531     // Recomposer always starts with a constant compound hash
1532     internal override val compositeKeyHashCode: CompositeKeyHashCode
1533         get() = RecomposerCompoundHashKey
1534 
1535     internal override val collectingCallByInformation: Boolean
1536         get() = _hotReloadEnabled.get()
1537 
1538     // Collecting parameter happens at the level of a composer; starts as false
1539     internal override val collectingParameterInformation: Boolean
1540         get() = false
1541 
1542     internal override val collectingSourceInformation: Boolean
1543         get() = composeStackTraceEnabled
1544 
recordInspectionTablenull1545     internal override fun recordInspectionTable(table: MutableSet<CompositionData>) {
1546         // TODO: The root recomposer might be a better place to set up inspection
1547         // than the current configuration with an CompositionLocal
1548     }
1549 
registerCompositionnull1550     internal override fun registerComposition(composition: ControlledComposition) {
1551         // Do nothing.
1552     }
1553 
unregisterCompositionnull1554     internal override fun unregisterComposition(composition: ControlledComposition) {
1555         synchronized(stateLock) {
1556             removeKnownCompositionLocked(composition)
1557             compositionInvalidations -= composition
1558             compositionsAwaitingApply -= composition
1559         }
1560     }
1561 
invalidatenull1562     internal override fun invalidate(composition: ControlledComposition) {
1563         synchronized(stateLock) {
1564                 if (composition !in compositionInvalidations) {
1565                     compositionInvalidations += composition
1566                     deriveStateLocked()
1567                 } else null
1568             }
1569             ?.resume(Unit)
1570     }
1571 
invalidateScopenull1572     internal override fun invalidateScope(scope: RecomposeScopeImpl) {
1573         synchronized(stateLock) {
1574                 snapshotInvalidations.add(scope)
1575                 deriveStateLocked()
1576             }
1577             ?.resume(Unit)
1578     }
1579 
insertMovableContentnull1580     internal override fun insertMovableContent(reference: MovableContentStateReference) {
1581         synchronized(stateLock) {
1582                 movableContentAwaitingInsert += reference
1583                 deriveStateLocked()
1584             }
1585             ?.resume(Unit)
1586     }
1587 
deletedMovableContentnull1588     internal override fun deletedMovableContent(reference: MovableContentStateReference) {
1589         synchronized(stateLock) {
1590             movableContentRemoved.add(reference.content, reference)
1591             if (reference.nestedReferences != null) {
1592                 val container = reference
1593                 fun recordNestedStatesOf(reference: MovableContentStateReference) {
1594                     reference.nestedReferences?.fastForEach { nestedReference ->
1595                         movableContentNestedStatesAvailable.add(
1596                             nestedReference.content,
1597                             NestedMovableContent(nestedReference, container)
1598                         )
1599                         recordNestedStatesOf(nestedReference)
1600                     }
1601                 }
1602                 recordNestedStatesOf(reference)
1603             }
1604         }
1605     }
1606 
movableContentStateReleasednull1607     internal override fun movableContentStateReleased(
1608         reference: MovableContentStateReference,
1609         data: MovableContentState,
1610         applier: Applier<*>,
1611     ) {
1612         synchronized(stateLock) {
1613             movableContentStatesAvailable[reference] = data
1614             val extractions = movableContentNestedExtractionsPending[reference]
1615             if (extractions.isNotEmpty()) {
1616                 val states = data.extractNestedStates(applier, extractions)
1617                 states.forEach { reference, state ->
1618                     movableContentStatesAvailable[reference] = state
1619                 }
1620             }
1621         }
1622     }
1623 
reportRemovedCompositionnull1624     internal override fun reportRemovedComposition(composition: ControlledComposition) {
1625         synchronized(stateLock) {
1626             val compositionsRemoved =
1627                 compositionsRemoved
1628                     ?: mutableSetOf<ControlledComposition>().also { compositionsRemoved = it }
1629             compositionsRemoved.add(composition)
1630         }
1631     }
1632 
movableContentStateResolvenull1633     override fun movableContentStateResolve(
1634         reference: MovableContentStateReference
1635     ): MovableContentState? =
1636         synchronized(stateLock) { movableContentStatesAvailable.remove(reference) }
1637 
1638     override val composition: Composition?
1639         get() = null
1640 
1641     /**
1642      * hack: the companion object is thread local in Kotlin/Native to avoid freezing
1643      * [_runningRecomposers] with the current memory model. As a side effect, recomposers are now
1644      * forced to be single threaded in Kotlin/Native targets.
1645      *
1646      * This annotation WILL BE REMOVED with the new memory model of Kotlin/Native.
1647      */
1648     @kotlin.native.concurrent.ThreadLocal
1649     companion object {
1650 
1651         private val _runningRecomposers = MutableStateFlow(persistentSetOf<RecomposerInfoImpl>())
1652 
1653         private val _hotReloadEnabled = AtomicReference(false)
1654 
1655         /**
1656          * An observable [Set] of [RecomposerInfo]s for currently
1657          * [running][runRecomposeAndApplyChanges] [Recomposer]s. Emitted sets are immutable.
1658          */
1659         val runningRecomposers: StateFlow<Set<RecomposerInfo>>
1660             get() = _runningRecomposers
1661 
currentRunningRecomposersnull1662         internal fun currentRunningRecomposers(): Set<RecomposerInfo> {
1663             return _runningRecomposers.value
1664         }
1665 
setHotReloadEnablednull1666         internal fun setHotReloadEnabled(value: Boolean) {
1667             _hotReloadEnabled.set(value)
1668         }
1669 
addRunningnull1670         private fun addRunning(info: RecomposerInfoImpl) {
1671             while (true) {
1672                 val old = _runningRecomposers.value
1673                 val new = old.add(info)
1674                 if (old === new || _runningRecomposers.compareAndSet(old, new)) break
1675             }
1676         }
1677 
removeRunningnull1678         private fun removeRunning(info: RecomposerInfoImpl) {
1679             while (true) {
1680                 val old = _runningRecomposers.value
1681                 val new = old.remove(info)
1682                 if (old === new || _runningRecomposers.compareAndSet(old, new)) break
1683             }
1684         }
1685 
saveStateAndDisposeForHotReloadnull1686         internal fun saveStateAndDisposeForHotReload(): Any {
1687             // NOTE: when we move composition/recomposition onto multiple threads, we will want
1688             // to ensure that we pause recompositions before this call.
1689             _hotReloadEnabled.set(true)
1690             return _runningRecomposers.value.flatMap { it.saveStateAndDisposeForHotReload() }
1691         }
1692 
loadStateAndComposeForHotReloadnull1693         internal fun loadStateAndComposeForHotReload(token: Any) {
1694             // NOTE: when we move composition/recomposition onto multiple threads, we will want
1695             // to ensure that we pause recompositions before this call.
1696             _hotReloadEnabled.set(true)
1697 
1698             _runningRecomposers.value.forEach { it.resetErrorState() }
1699 
1700             @Suppress("UNCHECKED_CAST") val holders = token as List<HotReloadable>
1701             holders.fastForEach { it.resetContent() }
1702             holders.fastForEach { it.recompose() }
1703 
1704             _runningRecomposers.value.forEach { it.retryFailedCompositions() }
1705         }
1706 
invalidateGroupsWithKeynull1707         internal fun invalidateGroupsWithKey(key: Int) {
1708             _hotReloadEnabled.set(true)
1709             _runningRecomposers.value.forEach {
1710                 if (it.currentError?.recoverable == false) {
1711                     return@forEach
1712                 }
1713 
1714                 it.resetErrorState()
1715 
1716                 it.invalidateGroupsWithKey(key)
1717 
1718                 it.retryFailedCompositions()
1719             }
1720         }
1721 
getCurrentErrorsnull1722         internal fun getCurrentErrors(): List<RecomposerErrorInfo> =
1723             _runningRecomposers.value.mapNotNull { it.currentError }
1724 
clearErrorsnull1725         internal fun clearErrors() {
1726             _runningRecomposers.value.mapNotNull { it.resetErrorState() }
1727         }
1728     }
1729 }
1730 
1731 /** Sentinel used by [ProduceFrameSignal] */
1732 private val ProduceAnotherFrame = Any()
1733 private val FramePending = Any()
1734 
1735 /**
1736  * Multiple producer, single consumer conflated signal that tells concurrent composition when it
1737  * should try to produce another frame. This class is intended to be used along with a lock shared
1738  * between producers and consumer.
1739  */
1740 private class ProduceFrameSignal {
1741     private var pendingFrameContinuation: Any? = null
1742 
1743     /**
1744      * Suspend until a frame is requested. After this method returns the signal is in a
1745      * [FramePending] state which must be acknowledged by a call to [takeFrameRequestLocked] once
1746      * all data that will be used to produce the frame has been claimed.
1747      */
awaitFrameRequestnull1748     suspend fun awaitFrameRequest(lock: SynchronizedObject) {
1749         synchronized(lock) {
1750             if (pendingFrameContinuation === ProduceAnotherFrame) {
1751                 pendingFrameContinuation = FramePending
1752                 return
1753             }
1754         }
1755         suspendCancellableCoroutine<Unit> { co ->
1756             synchronized(lock) {
1757                     if (pendingFrameContinuation === ProduceAnotherFrame) {
1758                         pendingFrameContinuation = FramePending
1759                         co
1760                     } else {
1761                         pendingFrameContinuation = co
1762                         null
1763                     }
1764                 }
1765                 ?.resume(Unit)
1766         }
1767     }
1768 
1769     /**
1770      * Signal from the frame request consumer that the frame is beginning with data that was
1771      * available up until this point. (Synchronizing access to that data is up to the caller.)
1772      */
takeFrameRequestLockednull1773     fun takeFrameRequestLocked() {
1774         checkPrecondition(pendingFrameContinuation === FramePending) { "frame not pending" }
1775         pendingFrameContinuation = null
1776     }
1777 
requestFrameLockednull1778     fun requestFrameLocked(): Continuation<Unit>? =
1779         when (val co = pendingFrameContinuation) {
1780             is Continuation<*> -> {
1781                 pendingFrameContinuation = FramePending
1782                 @Suppress("UNCHECKED_CAST")
1783                 co as Continuation<Unit>
1784             }
1785             ProduceAnotherFrame,
1786             FramePending -> null
1787             null -> {
1788                 pendingFrameContinuation = ProduceAnotherFrame
1789                 null
1790             }
1791             else -> error("invalid pendingFrameContinuation $co")
1792         }
1793 }
1794 
1795 @OptIn(InternalComposeApi::class)
1796 private class NestedContentMap {
1797     private val contentMap = MultiValueMap<MovableContent<Any?>, NestedMovableContent>()
1798     private val containerMap = MultiValueMap<MovableContentStateReference, MovableContent<Any?>>()
1799 
addnull1800     fun add(content: MovableContent<Any?>, nestedContent: NestedMovableContent) {
1801         contentMap.add(content, nestedContent)
1802         containerMap.add(nestedContent.container, content)
1803     }
1804 
clearnull1805     fun clear() {
1806         contentMap.clear()
1807         containerMap.clear()
1808     }
1809 
removeLastnull1810     fun removeLast(key: MovableContent<Any?>) =
1811         contentMap.removeLast(key).also { if (contentMap.isEmpty()) containerMap.clear() }
1812 
containsnull1813     operator fun contains(key: MovableContent<Any?>) = key in contentMap
1814 
1815     fun usedContainer(reference: MovableContentStateReference) {
1816         containerMap.forEachValue(reference) { value ->
1817             contentMap.removeValueIf(value) { it.container == reference }
1818         }
1819     }
1820 }
1821 
1822 @InternalComposeApi
1823 private class NestedMovableContent(
1824     val content: MovableContentStateReference,
1825     val container: MovableContentStateReference
1826 )
1827