Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix "Unsupported concurrent change during composition" #1592

Merged
merged 3 commits into from
Sep 25, 2024

Conversation

igordmn
Copy link
Collaborator

@igordmn igordmn commented Sep 25, 2024

Fixes https://youtrack.jetbrains.com/issue/CMP-6729/iOS-Runtime-crash.-Unsupported-concurrent-change-during-composition

The issue was because of this concurrent calls:

performRecompose()
  val snapshot = Snapshot.takeMutableSnapshot()
    Snapshot.sendApplyNotifications() // update the Global snapshot, called because we need to pass it fresh to the local Recompose snapshot
      drawState.value = Unit // called because some other state captured in `draw` was changed
  snapshot.enter {
    ...
    Snapshot.sendApplyNotifications() // it is valid to call it anywhere
      drawState.value = Unit // called because some other state state captured in `draw` was changed again
  }
  snapshot.apply() // fails with "Unsupported concurrent change during composition" because it can't merge to state changes

Regression after #1574

A synthetic reproducer of the issue for history (skip it if details are already understood):

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()
}

Doesn't represent real case, as we capture and change drawState of the same layer. In a real case layer captures only children drawState. Still it shows similar chain of calls

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)

@igordmn igordmn requested a review from MatkovIvan September 25, 2024 10:25
@igordmn igordmn merged commit 338ef95 into jb-main Sep 25, 2024
6 checks passed
@igordmn igordmn deleted the igor.demin/fix-concurrent-state-crash-2 branch September 25, 2024 11:10
igordmn added a commit that referenced this pull request Sep 25, 2024
Fixes
https://youtrack.jetbrains.com/issue/CMP-6729/iOS-Runtime-crash.-Unsupported-concurrent-change-during-composition

The issue was because of this concurrent calls:
```
performRecompose()
  val snapshot = Snapshot.takeMutableSnapshot()
    Snapshot.sendApplyNotifications() // update the Global snapshot, called because we need to pass it fresh to the local Recompose snapshot
      drawState.value = Unit // called because some other state captured in `draw` was changed
  snapshot.enter {
    ...
    Snapshot.sendApplyNotifications() // it is valid to call it anywhere
      drawState.value = Unit // called because some other state state captured in `draw` was changed again
  }
  snapshot.apply() // fails with "Unsupported concurrent change during composition" because it can't merge to state changes
```

Regression after
#1574

A synthetic reproducer of the issue for history (skip it if details are
already understood):
```
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()
}
```
_Doesn't represent real case, as we capture and change `drawState` of
the same layer. In a real case layer captures only children `drawState`.
Still it shows similar chain of calls_

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)
```

---------

Co-authored-by: Ivan Matkov <ivan.matkov@jetbrains.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants