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

Using ComposeScenes with anything but MainUIDispatcher results in a deadlock #4788

Open
jakobkmar opened this issue May 9, 2024 · 19 comments
Assignees
Labels
bug Something isn't working runtime Compose runtime problem

Comments

@jakobkmar
Copy link

jakobkmar commented May 9, 2024

Describe the bug

MultiLayerComposeScene and SingleLayerComposeScene provide the option to set a coroutineContext parameter.
FrameDispatcher also provides the option to pass a CoroutineScope or CoroutineContext.

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 use MainUIDispatcher 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 (as MainUIDispatcher 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

  1. create a custom MultiLayerComposeScene
  2. pass a Dispatchers.Default.limitedParallelism(1) to it
  3. use the same limited dispatcher for the FrameDispatcher
  4. render the scene continuously using the FrameDispatcher
  5. do something, e.g. scroll a lot etc
  6. observe deadlock in IntelliJ ThreadDump using debugger

Deadlock:

image

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 what MainUIDispatcher 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

  • Desktop (Windows, Linux, macOS)

Versions

  • Libraries:
    • Compose Multiplatform version: 1.6.2 and 1.6.10-rc01
  • Kotlin version: 1.9.23
  • OS version(s) (required for Desktop and iOS issues): Windows 11 and Linux
  • OS architecture (x86 or arm64): x86
  • JDK (for desktop issues): 21

Additional context

  • with previous versions (when ComposeScene was still completely provided by Skiko) the API worked perfectly fine with any dispatcher
  • Compose Multiplatform with its own use of FrameDispatcher and ComposeScenes on the desktop seems to always use the MainUIDispatcher, which would explain why this issue has not occurred yet.
@jakobkmar jakobkmar added bug Something isn't working submitted labels May 9, 2024
@m-sasha
Copy link
Contributor

m-sasha commented May 9, 2024

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.

@jakobkmar
Copy link
Author

jakobkmar commented May 9, 2024

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.

@m-sasha
Copy link
Contributor

m-sasha commented May 9, 2024

No, it's about using multiple scenes concurrently.

Why do you need to use multiple ComposeScenes each in their own thread?

@m-sasha m-sasha added wait for reply Further information is requested and removed submitted labels May 9, 2024
@jakobkmar
Copy link
Author

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.

@m-sasha m-sasha removed the wait for reply Further information is requested label May 9, 2024
@m-sasha
Copy link
Contributor

m-sasha commented May 9, 2024

Can you try using a single-thread dispatcher instead of Dispatchers.Default.limitedParallelism(1), e.g.

Executors.newSingleThreadExecutor().asCoroutineDispatcher()

?

@jakobkmar
Copy link
Author

Yes, I tried that as well - it results in the same deadlock between that single-thread and the AWT event queue.

@jakobkmar
Copy link
Author

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)

@m-sasha
Copy link
Contributor

m-sasha commented May 13, 2024

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.

@m-sasha m-sasha closed this as not planned Won't fix, can't repro, duplicate, stale May 13, 2024
@jakobkmar
Copy link
Author

jakobkmar commented May 13, 2024

The LWJGL thing mentioned in my last comment is something different though, no concurrent use of ComposeScenes, 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.

@m-sasha
Copy link
Contributor

m-sasha commented May 13, 2024

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: #1396

@jakobkmar
Copy link
Author

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.

@m-sasha
Copy link
Contributor

m-sasha commented May 13, 2024

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

@jakobkmar
Copy link
Author

jakobkmar commented May 13, 2024

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.

@jakobkmar
Copy link
Author

jakobkmar commented May 13, 2024

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

@m-sasha
Copy link
Contributor

m-sasha commented May 13, 2024

I don't know whether supporting GLFW is an important goal for us. @igordmn ?

@MatkovIvan
Copy link
Member

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

@MatkovIvan MatkovIvan reopened this May 13, 2024
@m-sasha m-sasha removed their assignment May 13, 2024
@m-sasha
Copy link
Contributor

m-sasha commented May 13, 2024

The problem is likely that GlobalSnapshotManager runs on the AWT thread.

@igordmn
Copy link
Collaborator

igordmn commented May 14, 2024

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

@igordmn igordmn added the runtime Compose runtime problem label May 14, 2024
@igordmn
Copy link
Collaborator

igordmn commented May 14, 2024

A simple alternative fix can be adding ability to redefine GlobalSnapshotManager's dispatcher

Or:

  1. make a flag as suggested here
  2. make a @InternalComposeUiApi public fun startGlobalSnapshotManager(coroutineDispatcher)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working runtime Compose runtime problem
Projects
None yet
Development

No branches or pull requests

4 participants