Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix "Unsupported concurrent change during composition"
Fixes https://youtrack.jetbrains.com/issue/CMP-6729/iOS-Runtime-crash.-Unsupported-concurrent-change-during-composition Regression after #1574 The draw call is similar to: ``` // NodeCoordinator.drawBlock observer.observeReads(Unit, { RenderNodeLayer.invalidate() drawState.value = Unit }) { RenderNodeLayer.drawLayer() drawState.value } ``` So, triggering the observer in response to the state change also changes this state, which triggers observer again. It is sometimes manifested via this call: ``` performRecompose() val snapshot = Snapshot.takeMutableSnapshot() Snapshot.sendApplyNotifications() // triggers drawState.value = Unit snapshot.enter { ... Snapshot.sendApplyNotifications() // can be called anywhere, triggers the observer of `drawState` because we changed `drawState` during the previous `sendApplyNotifications` } snapshot.apply() // fails with "Unsupported concurrent change during composition" ``` (I haven't figured why sometimes and not always) Considering this, *we shouldn't change any state we read in `draw` inside `invalidate`* (because *we shouldn't change any state we read in `observeReads(block=)`) inside observeReads(onValueChangedForScope=)`*) We added this state not to trigger `RenderNodeLayer.drawLayer` after `RenderNodeLayer.invalidate`, but to trigger `SkiaGraphicsLayer.invalidate` after we called `RenderNodeLayer.drawLayer` inside `SkiaGraphicsLayer.draw`. Because: - we still need some way to notify `SkiaGraphicsLayer.invalidate` - only `RenderNodeLayer.invalidate` knows that `RenderNodeLayer.drawLayer` is changed We notify via `isDirty` state and a separate check outside `invalidate` <details> <summary>A synthetic reproducer of the issue for history (skip it if details are already understood)</summary> ``` import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.neverEqualPolicy import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.SnapshotStateObserver fun main() { val drawState = mutableStateOf(Unit, neverEqualPolicy()) val observer = SnapshotStateObserver { it.invoke() } observer.start() fun draw() { observer.observeReads(Unit, { println("invalidate draw (drawState.value = Unit)") drawState.value = Unit }) { println("\ndraw") drawState.value } } fun externalChange() { println("\nexternalChange") drawState.value = Unit Snapshot.sendApplyNotifications() } fun composition() { println("\ncomposition BEFORE") Snapshot.takeMutableSnapshot().apply { enter { println("composition BEGIN") println("sendApplyNotifications") Snapshot.sendApplyNotifications() println("composition END") } println("applying composition state to global state") if (!apply().succeeded) { throw RuntimeException("Composition apply failed") } } } // frame 0 draw() externalChange() // frame 1 composition() } ``` Output: ``` draw externalChange invalidate draw (drawState.value = Unit) composition BEFORE invalidate draw (drawState.value = Unit) composition BEGIN sendApplyNotifications invalidate draw (drawState.value = Unit) composition END applying composition state to global state Exception in thread "main" java.lang.RuntimeException: Composition apply failed at MainKt.main$composition(main.kt:39) at MainKt.main(main.kt:50) at MainKt.main(main.kt) ``` </details> ## Release Notes ### Fixes - Multiplatform - _(prerelease fix)_ Fix "Unsupported concurrent change during composition"
- Loading branch information