Closed jakobkmar closed 1 month ago
Unfortunately this is not currently a supported use-case.
We tried to allow concurrent use of ComposeScene
here, but ran into limitations (mentioned in that ticket).
It's possible that we have since introduced our own, additional, limitations, since we've stopped trying to support concurrent use of ComposeScene
ourselves.
Hasn't the linked issue been caused by multiple threads being used for the same scene due to Dispatchers.Unconfined
being the default context for ImageComposeScene
? If yes, then this is not what I want. I do not want to use the same scene concurrently, but multiple scenes each on their own separate (but only that single) thread.
No, it's about using multiple scenes concurrently.
Why do you need to use multiple ComposeScenes
each in their own thread?
I have been using multiple scenes for a server-side GUI, where the server handles multiple users at once.
The performance degraded severely now that everything has to use MainUIDispatcher
.
Can you try using a single-thread dispatcher instead of Dispatchers.Default.limitedParallelism(1)
, e.g.
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
?
Yes, I tried that as well - it results in the same deadlock between that single-thread and the AWT event queue.
Also, does this mean that using Compose to render into GLFW windows (for example using LWJGL) is unsupported now as well? (you have to use the glfw thread there, which clashes with the requirement for MainUIDispatcher
)
We can't support this use-case well because of the aforementioned limitations upstream (i.e. you could still get deadlocks before), and it's complicated to support it badly.
The LWJGL thing mentioned in my last comment is something different though, no concurrent use of ComposeScene
s, still not possible anymore due to the newly introduced requirement for MainUIDispatcher
. Is this use-case also not supported, even though there is only a single ComposeScene
in use?
I don't think it caused any issues or deadlocks before - it was basically the same just with the difference that the entire logic did not have to run on MainUIDispatcher
(= AWT event queue).
No multithreading there, no concurrent scenes - it simply needs a different (single) thread because you cannot start a GLFW window in the AWT event queue.
Is this use-case also not supported, even though there is only a single ComposeScene in use?
@igordmn Can you answer? Is this a use-case we need to support?
I don't think it caused any issues or deadlocks before
It did; perhaps you just haven't encountered them. See the issues described here: https://github.com/JetBrains/compose-multiplatform/issues/1396
In all shared code snippets in the linked issue there are always thousand concurrent scenes started on different threads. I am not sure how one scene on a single thread could lead to the same issues described there.
There are global structures in Compose that are shared between all instances, as explained in these two tickets: https://issuetracker.google.com/issues/283162626 https://issuetracker.google.com/issues/283216580
But I am not talking about multiple instances right now. Only a single scene, a single application, a single thread - so only one instance.
The main issue is that a requirement for MainUIDispatcher
was introduced, which is not related to the concurrent scene issue.
For example, requiring MainUIDispatcher
makes Compose not usable with GLFW anymore, which it previously was. This is not at all related to the concurrent scene issues, since there are no concurrent scenes in a single thread and single scene GLFW application.
I opened this issue because of the requirement for MainUIDispatcher
, not because of scene concurrency.
Also, this is not an upstream issue, since upstream does not work with the AWT event queue and therefore cannot be the reason why it is required (except if somewhere in upstream Compose the coroutineContext is ignored and MainUIDispatcher has been hardcoded).
I don't know whether supporting GLFW is an important goal for us. @igordmn ?
We discussed it with @igordmn recently - it was a conscious, explicit decision to not depend on a specific dispatcher. ComposeScene
should allow to be reused in GLFW or JavaFX environments.
So I guess the issue is valid
The problem is likely that GlobalSnapshotManager
runs on the AWT thread.
ComposeScene
is by design should be independent of any hardcoded Main thread. The issue that it interferes with GlobalSnapshotManager
and have races with it is a Compose Runtime bug that needs to be fixed. Though, it is not trivial, and isn't in priority unfortunately.
A simple alternative fix can be adding ability to redefine GlobalSnapshotManager's dispatcher
A simple alternative fix can be adding ability to redefine GlobalSnapshotManager's dispatcher
Or:
@InternalComposeUiApi public fun startGlobalSnapshotManager(coroutineDispatcher)
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.
Describe the bug
MultiLayerComposeScene
andSingleLayerComposeScene
provide the option to set acoroutineContext
parameter.FrameDispatcher
also provides the option to pass aCoroutineScope
orCoroutineContext
.However, using a scene with any other dispatcher other than
MainUIDispatcher
(provided by Skiko) will result in deadlock at some point when using the application.Since the render call on a scene must also be called from the same thread, this also means that the
FrameDispatcher
must useMainUIDispatcher
as well.This causes several issues. For example, GLFW requires the use of a specific thread to be able to draw to a Window, which clashes with the requirement for
MainUIDispatcher
which is an AWT event queue.The following seems to be unsupported for now:
I consider this a major bug, since this makes it impossible to render multiple scenes at once using different threads (asMainUIDispatcher
is always the same). Multiple scenes are needed if you wish to render multiple completely separate scenes in one Kotlin application. The coroutineContext paramters are also completly obsolete this way.Please note: In past Compose versions (for example
1.2.0
)ComposeScene
worked fine with other dispatchers, so this is a new bug which has been introduced with recent updates.To Reproduce
MultiLayerComposeScene
Dispatchers.Default.limitedParallelism(1)
to itFrameDispatcher
FrameDispatcher
Deadlock:
Detailed Thread Dump Stack Traces
``` "AWT-EventQueue-0 @coroutine#8@24000" tid=0x9a nid=NA waiting for monitor entry java.lang.Thread.State: BLOCKED blocks DefaultDispatcher-worker-5 @coroutine#5@23693 waiting for DefaultDispatcher-worker-5 @coroutine#5@23693 to release lock on <0x5e17> (a androidx.compose.runtime.SynchronizedObject) at androidx.compose.runtime.BroadcastFrameClock.getHasAwaiters(Synchronization.kt:33) at androidx.compose.runtime.Recomposer.getHasBroadcastFrameClockAwaitersLocked(Recomposer.kt:289) at androidx.compose.runtime.Recomposer.deriveStateLocked(Recomposer.kt:326) at androidx.compose.runtime.Recomposer.access$deriveStateLocked(Recomposer.kt:127) at androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1.invoke(Recomposer.kt:989) - locked <0x5e19> (a androidx.compose.runtime.SynchronizedObject) at androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1.invoke(Recomposer.kt:976) at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot(Snapshot.kt:1816) at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot(Snapshot.kt:1831) at androidx.compose.runtime.snapshots.SnapshotKt.access$advanceGlobalSnapshot(Snapshot.kt:1) at androidx.compose.runtime.snapshots.Snapshot$Companion.sendApplyNotifications(Snapshot.kt:584) at androidx.compose.ui.platform.GlobalSnapshotManager$ensureStarted$1.invokeSuspend(GlobalSnapshotManager.skiko.kt:46) ``` ``` "DefaultDispatcher-worker-5 @coroutine#5@23693" tid=0x73 nid=NA waiting for monitor entry java.lang.Thread.State: BLOCKED blocks AWT-EventQueue-0 @coroutine#8@24000 waiting for AWT-EventQueue-0 @coroutine#8@24000 to release lock on <0x5e19> (a androidx.compose.runtime.SynchronizedObject) at androidx.compose.runtime.Recomposer.composeInitial$runtime(Synchronization.kt:33) at androidx.compose.runtime.ComposerImpl$CompositionContextImpl.composeInitial$runtime(Composer.kt:3600) at androidx.compose.runtime.CompositionImpl.composeInitial(Composition.kt:633) at androidx.compose.runtime.CompositionImpl.setContentWithReuse(Composition.kt:625) at androidx.compose.ui.layout.LayoutNodeSubcompositionsState.subcomposeInto(SubcomposeLayout.kt:502) at androidx.compose.ui.layout.LayoutNodeSubcompositionsState.subcompose(SubcomposeLayout.kt:472) at androidx.compose.ui.layout.LayoutNodeSubcompositionsState.subcompose(SubcomposeLayout.kt:463) at androidx.compose.ui.layout.LayoutNodeSubcompositionsState.subcompose(SubcomposeLayout.kt:447) at androidx.compose.ui.layout.LayoutNodeSubcompositionsState$Scope.subcompose(SubcomposeLayout.kt:872) at androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScopeImpl.measure-0kLqBqw(LazyLayoutMeasureScope.kt:125) at androidx.compose.foundation.lazy.LazyListMeasuredItemProvider.getAndMeasure(LazyListMeasuredItemProvider.kt:48) at androidx.compose.foundation.lazy.LazyListMeasureKt.measureLazyList-5IMabDg(LazyListMeasure.kt:195) at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.invoke-0kLqBqw(LazyList.kt:313) at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.invoke(LazyList.kt:178) at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$3$2$1.invoke-0kLqBqw(LazyLayout.kt:107) at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$3$2$1.invoke(LazyLayout.kt:100) at androidx.compose.ui.layout.LayoutNodeSubcompositionsState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:709) at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:126) at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:646) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:252) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:251) at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:132) at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:504) at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:260) at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:133) at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:113) at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1617) at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:36) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:620) at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui(LayoutNode.kt:1145) at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:354) at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout-0kLqBqw(MeasureAndLayoutDelegate.kt:439) at androidx.compose.ui.node.RootNodeOwner$OwnerImpl.measureAndLayout-0kLqBqw(RootNodeOwner.skiko.kt:322) at androidx.compose.ui.node.LayoutNode.forceRemeasure(LayoutNode.kt:1219) at androidx.compose.foundation.lazy.LazyListState.onScroll$foundation(LazyListState.kt:352) at androidx.compose.foundation.lazy.LazyListState$scrollableState$1.invoke(LazyListState.kt:187) at androidx.compose.foundation.lazy.LazyListState$scrollableState$1.invoke(LazyListState.kt:187) at androidx.compose.foundation.gestures.DefaultScrollableState$scrollScope$1.scrollBy(ScrollableState.kt:166) at androidx.compose.foundation.gestures.ScrollingLogic$dispatchScroll$performScroll$1.invoke-MK-Hz9U(Scrollable.kt:693) at androidx.compose.foundation.gestures.ScrollingLogic$dispatchScroll$performScroll$1.invoke(Scrollable.kt:683) at androidx.compose.foundation.gestures.ScrollingLogic.dispatchScroll-3eAAhYA(Scrollable.kt:707) at androidx.compose.foundation.gestures.MouseWheelScrollNode.dispatchMouseWheelScroll(MouseWheelScrollable.kt:312) at androidx.compose.foundation.gestures.MouseWheelScrollNode.access$dispatchMouseWheelScroll(MouseWheelScrollable.kt:54) at androidx.compose.foundation.gestures.MouseWheelScrollNode$animateMouseWheelScroll$2.invoke(MouseWheelScrollable.kt:297) at androidx.compose.foundation.gestures.MouseWheelScrollNode$animateMouseWheelScroll$2.invoke(MouseWheelScrollable.kt:287) at androidx.compose.animation.core.SuspendAnimationKt.doAnimationFrame(SuspendAnimation.kt:361) at androidx.compose.animation.core.SuspendAnimationKt.doAnimationFrameWithScale(SuspendAnimation.kt:339) at androidx.compose.animation.core.SuspendAnimationKt.access$doAnimationFrameWithScale(SuspendAnimation.kt:1) at androidx.compose.animation.core.SuspendAnimationKt$animate$9.invoke(SuspendAnimation.kt:279) at androidx.compose.animation.core.SuspendAnimationKt$animate$9.invoke(SuspendAnimation.kt:278) at androidx.compose.animation.core.SuspendAnimationKt$callWithFrameNanos$2.invoke(SuspendAnimation.kt:304) at androidx.compose.animation.core.SuspendAnimationKt$callWithFrameNanos$2.invoke(SuspendAnimation.kt:303) at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:42) at androidx.compose.runtime.BroadcastFrameClock.sendFrame(BroadcastFrameClock.kt:71) - locked <0x5e17> (a androidx.compose.runtime.SynchronizedObject) at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:558) at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:551) at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:42) at androidx.compose.runtime.BroadcastFrameClock.sendFrame(BroadcastFrameClock.kt:71) - locked <0x5e18> (a androidx.compose.runtime.SynchronizedObject) at androidx.compose.ui.scene.BaseComposeScene.render(BaseComposeScene.skiko.kt:160) ```Expected behavior
It should be possible to pass a custom dispatcher to the scene APIs. E.g. your own
Dispatchers.Default.limitedParallelism(1)
. Alternatively, it could be possible to configure whatMainUIDispatcher
actually is under the hood.This is needed for having multiple and completely separate scenes at once in one Kotlin application.(not supported, but it is still needed for reasons explained above)Custom scenes should not depend on the AWT event queue, since they won't be rendered to an AWT or Swing Window anyways.
Affected platforms
Versions
1.6.2
and1.6.10-rc01
1.9.23
x86
21
Additional context
ComposeScene
was still completely provided by Skiko) the API worked perfectly fine with any dispatcherMainUIDispatcher
, which would explain why this issue has not occurred yet.