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

Add Unit tests for Concurrency in ImageComposeScene #1396

Open
jimgoog opened this issue Nov 13, 2021 · 6 comments · Fixed by JetBrains/compose-multiplatform-core#551
Open

Comments

@jimgoog
Copy link
Collaborator

jimgoog commented Nov 13, 2021

A couple of unit tests that we should probably add to our test suite to ensure we don't regress concurrency.

The unit tests are currently failing... possibly because of #1392
but I'm not positive because one of the stack traces I saw when running this lead me to believe that it might be unrelated. We might know more after #1392 is fixed, and either way, we should ensure these tests are also passing and don't regress.

@OptIn(ExperimentalTime::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
fun main() {
    val service = Executors.newFixedThreadPool(50)

    for(i in 1..10000) {
        service.submit {
            val scene = ImageComposeScene(50, 50) {
                Box(Modifier.fillMaxSize().background(Color.White)) {
                    CircularProgressIndicator()
                }
            }
            scene.render() // start animation
            scene.render(milliseconds(50)).close()
            scene.close()
        }
    }

    service.shutdown()
}


@OptIn(ExperimentalTime::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
fun main() {
    val scene = ImageComposeScene(50, 50) {
        Box(Modifier.fillMaxSize().background(Color.White)) {
            CircularProgressIndicator()
        }
    }
    scene.render() // start animation
    val service = Executors.newFixedThreadPool(8)
    (500 downTo 1 step 1).map { service.submit { scene.render(milliseconds(it)).close() } }.map { it.get() }
    service.shutdown()
    scene.close()
}
@igordmn
Copy link
Collaborator

igordmn commented Feb 17, 2022

After JetBrains/compose-multiplatform-core#199 the first test is still failing with:

java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
	at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
	at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
	at androidx.compose.ui.ImageComposeSceneTest.multithreading 1(ImageComposeSceneTest.kt:148)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at androidx.compose.ui.test.junit4.ScreenshotTestRule$apply$1.evaluate(SkiaTest.desktop.kt:191)
	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
	at androidx.compose.runtime.snapshots.SnapshotKt.readError(Snapshot.kt:1527)
	at androidx.compose.runtime.snapshots.SnapshotKt.readable(Snapshot.kt:1522)
	at androidx.compose.runtime.snapshots.SnapshotKt.readable(Snapshot.kt:1513)
	at androidx.compose.runtime.SnapshotMutableStateImpl.getValue(SnapshotState.kt:130)
	at androidx.compose.material.Colors.getPrimary-0d7_KjU(Colors.kt:338)
	at androidx.compose.material.ProgressIndicatorKt.CircularProgressIndicator-aM-cp0Q(ProgressIndicator.kt:258)
	at androidx.compose.ui.ComposableSingletons$ImageComposeSceneTestKt$lambda-6$1.invoke(ImageComposeSceneTest.kt:140)
	at androidx.compose.ui.ComposableSingletons$ImageComposeSceneTestKt$lambda-6$1.invoke(ImageComposeSceneTest.kt:138)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.ComposeScene$setContent$5.invoke(ComposeScene.skiko.kt:331)
	at androidx.compose.ui.ComposeScene$setContent$5.invoke(ComposeScene.skiko.kt:330)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:166)
	at androidx.compose.ui.platform.Wrapper_skikoKt$setContent$2$1.invoke(Wrapper.skiko.kt:47)
	at androidx.compose.ui.platform.Wrapper_skikoKt$setContent$2$1.invoke(Wrapper.skiko.kt:46)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.ui.platform.Wrapper_skikoKt.provide(Wrapper.skiko.kt:65)
	at androidx.compose.ui.platform.Wrapper_skikoKt.access$provide(Wrapper.skiko.kt:1)
	at androidx.compose.ui.platform.Wrapper_skikoKt$setContent$2.invoke(Wrapper.skiko.kt:46)
	at androidx.compose.ui.platform.Wrapper_skikoKt$setContent$2.invoke(Wrapper.skiko.kt:45)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:72)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2582)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2571)
	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:247)
	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2571)
	at androidx.compose.runtime.ComposerImpl.composeContent$runtime(Composer.kt:2522)
	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:478)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime(Recomposer.kt:748)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:433)
	at androidx.compose.ui.platform.Wrapper_skikoKt.setContent(Wrapper.skiko.kt:45)
	at androidx.compose.ui.ComposeScene.setContent$ui(ComposeScene.skiko.kt:330)
	at androidx.compose.ui.ComposeScene.setContent$ui$default(ComposeScene.skiko.kt:309)
	at androidx.compose.ui.ComposeScene.setContent(ComposeScene.skiko.kt:291)
	at androidx.compose.ui.ImageComposeScene.<init>(ImageComposeScene.desktop.kt:118)
	at androidx.compose.ui.ImageComposeScene.<init>(ImageComposeScene.desktop.kt:104)
	at androidx.compose.ui.ImageComposeSceneTest.multithreading_1$lambda-7$lambda-6(ImageComposeSceneTest.kt:138)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)

The second test isn't a valid test for the current implementation of ComposeScene. It doesn't support concurrent access from different threads. I have added info to the doc about that. Writing a test for not supporting multi-threading is probably impossible. Multi-threading is a hard thing to write and support for mutable entities, and in most cases isn't needed.

@ScottPierce
Copy link
Contributor

I'm seeing this while running lots of tests in parallel as well.

We're using this for more than tests. We're using this for server side rendering of images, and because of this I'm scared to allow rendering of more than one image at a time.

@ScottPierce
Copy link
Contributor

ScottPierce commented May 19, 2022

Using a mutex seems to work as a workaround for the time being:

private val IMAGE_COMPOSE_SCENE_MUTEX = Mutex()

suspend fun SafeImageComposeScene(
    width: Int,
    height: Int,
    density: Density = Density(1f),
    coroutineContext: CoroutineContext = Dispatchers.Unconfined,
    content: @Composable () -> Unit = {},
): ImageComposeScene {
    return IMAGE_COMPOSE_SCENE_MUTEX.withLock {
        ImageComposeScene(
            width = width,
            height = height,
            density = density,
            coroutineContext = coroutineContext,
            content = content
        )
    }
}

@m-sasha
Copy link
Contributor

m-sasha commented May 17, 2023

Reopened as

    val service = Executors.newFixedThreadPool(50)

    for(i in 1..10000) {
        service.submit {
            val scene = ImageComposeScene(50, 50) {
                Box(Modifier.fillMaxSize().background(Color.White)) {
                    CircularProgressIndicator()
                }
            }
            scene.render() // start animation
            scene.render(milliseconds(50)).close()
            scene.close()
        }
    }

    service.shutdown()

was succeeding just because ExecutorService.submit eats any exceptions thrown in the Runnable it's given. Running this:

        val exception: AtomicReference<Throwable?> = AtomicReference(null)
        val block = {
            try{
                val scene = ImageComposeScene(50, 50) {
                    Box(Modifier.fillMaxSize().background(Color.White)) {
                        CircularProgressIndicator()
                    }
                }
                scene.render() // start animation
                scene.render(50.milliseconds).close()
                scene.close()
            } catch (e: Throwable){
                exception.set(e)
            }
        }

        thread(block = block)
        thread(block = block)
        thread(block = block)

        Thread.sleep(1000)

        exception.get()?.printStackTrace()

will print

java.lang.IllegalStateException: Check failed.
	at androidx.compose.ui.Modifier$Node.attach$ui(Modifier.kt:200)
	at androidx.compose.ui.node.NodeChain.attach(NodeChain.kt:270)
	at androidx.compose.ui.node.LayoutNode.attach$ui(LayoutNode.kt:414)
	at androidx.compose.ui.platform.SkiaBasedOwner.<init>(SkiaBasedOwner.skiko.kt:191)
	at androidx.compose.ui.platform.SkiaBasedOwner.<init>(SkiaBasedOwner.skiko.kt:69)
	at androidx.compose.ui.ComposeScene.setContent$ui(ComposeScene.skiko.kt:366)
	at androidx.compose.ui.ComposeScene.setContent$ui$default(ComposeScene.skiko.kt:355)
	at androidx.compose.ui.ComposeScene.setContent(ComposeScene.skiko.kt:337)
	at androidx.compose.ui.ImageComposeScene.<init>(ImageComposeScene.skikoMain.kt:120)
	at androidx.compose.ui.ImageComposeScene.<init>(ImageComposeScene.skikoMain.kt:106)
	at androidx.compose.ui.ImageComposeSceneTest$run multiple ImageComposeScenes concurrently$block$1.invoke(ImageComposeSceneTest.kt:110)
	at androidx.compose.ui.ImageComposeSceneTest$run multiple ImageComposeScenes concurrently$block$1.invoke(ImageComposeSceneTest.kt:108)
	at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

@m-sasha
Copy link
Contributor

m-sasha commented May 18, 2023

The reason for the above failure is a race condition in NodeChain.padChain() and NodeChain.trimChain() due to SentinelHead.

@m-sasha
Copy link
Contributor

m-sasha commented May 19, 2023

Currently blocking concurrent use of ImageComposeScene:
https://issuetracker.google.com/issues/283162626
https://issuetracker.google.com/issues/283216580

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants