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