Run GlobalSnapshotManager on trampolineDispatcher#3175
Run GlobalSnapshotManager on trampolineDispatcher#3175Ivan Matkov (MatkovIvan) wants to merge 6 commits into
GlobalSnapshotManager on trampolineDispatcher#3175Conversation
567510e to
41293b5
Compare
| "FrameRecomposer requires a ContinuationInterceptor in its coroutineContext" | ||
| } | ||
| val hostDispatcher = coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher | ||
| if (hostDispatcher != null && !hostDispatcher.isDispatchNeeded(hostDispatcher)) { |
There was a problem hiding this comment.
In what cases it's needed? Is it a temporary solution?
Could you please add comment there?
There was a problem hiding this comment.
Not temporary. For things like Dispatchers.Unconfined (default in ImageComposeScene) or Dispatchers.Main.immediate,
Also, it's not CoroutineDispatcher in our test framework
There was a problem hiding this comment.
Please add comment then... because it looks a bit weird (or maybe update constructor params to make the usage clear.
There was a problem hiding this comment.
But it already has comments about that (on the next line), so I'm not sure what else to add here. Any particular suggestions?
For the "public" parameters - it doesn't seem that adjustment is required, it's an implementation detail, and previous behavior is still here
There was a problem hiding this comment.
Reading the code, it looks like it just skipping the flush. But if we just use FlushCoroutineDispatcher, the flush shouldn't do anything, because the dispatcher should perform the task during the dispatch.
If I am correct, and behavior is the same, I would prefer the old code - it looks cleaner and understandable. If I am not correct - can you explain what difference does this code make?
There was a problem hiding this comment.
If FlushCoroutineDispatcher doesn't work correctly with Dispatchers.Unconfined - it looks like a bug of FlushCoroutineDispatcher that we should fix
There was a problem hiding this comment.
The issue here is that GSM doesn't work with Dispatchers.Unconfined. Register GSM on FlushCoroutineDispatcher that inlines the block leads to a deadlock. This particular behavior is explicitly marked as unsupported in compose:runtime, so it's about ensuring that
- No GSM on
FlushCoroutineDispatcher - No inlining in
FlushCoroutineDispatcherif it reportsisDispatchNeeded == true
Currently these checks and behaviors made explicit and in one place (not across 3 classes)
Creating FlushCoroutineDispatcher that has only one function - flush in cases where it does not work does not seem as readability improvement, but vice versa - it makes it quite tricky to understand what's going on there.
If we do not want such a cleanup and explicitness, it might be revered back, but do we?
There was a problem hiding this comment.
Reserved most of it for now and replaced by delegation implementation of FlushCoroutineDispatcher.isDispatchNeeded
| "FrameRecomposer requires a ContinuationInterceptor in its coroutineContext" | ||
| } | ||
| val hostDispatcher = coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher | ||
| if (hostDispatcher != null && !hostDispatcher.isDispatchNeeded(hostDispatcher)) { |
There was a problem hiding this comment.
Reading the code, it looks like it just skipping the flush. But if we just use FlushCoroutineDispatcher, the flush shouldn't do anything, because the dispatcher should perform the task during the dispatch.
If I am correct, and behavior is the same, I would prefer the old code - it looks cleaner and understandable. If I am not correct - can you explain what difference does this code make?
a89f262 to
205f010
Compare
205f010 to
8bd66ac
Compare
This reverts commit 96897e9.
…rceptor` in tests
0a151ca to
0c01547
Compare
|
rebased on jb-main + reverted #3171 |
| private class Registration(val dispatcher: CoroutineDispatcher) { | ||
| /** Number of live handles. Guarded by [GlobalSnapshotManager.lock]. */ | ||
| var refCount = 0 | ||
| var threadId: Long? = null |
There was a problem hiding this comment.
Should probably be @Volatile.
There was a problem hiding this comment.
Done
| threadId = getCurrentThreadId() | ||
| warnIfMultipleThreads() |
There was a problem hiding this comment.
You could add this also inside the channel.consumeEach to catch multi-thread dispatchers (like Dispatchers.Default).
There was a problem hiding this comment.
I'm avoiding this for a few reasons:
- No guarantee that it will be printed once without more complex tracking
.distinct()is not so cheap to run it on each snapshot writing
| } | ||
|
|
||
| private fun warnIfMultipleThreads() = synchronized(lock) { | ||
| if (registrations.values.mapNotNull { it.threadId }.distinct().size > 1) { |
There was a problem hiding this comment.
This could be much faster.
It's ok if it's only done on registration, but if you also call it on each value in channel, it should be rewritten.
There was a problem hiding this comment.
I don't really want to store additional sets with complicated logic of clearing this. One time shot is enough for prinitting a warning purposes
|
It would help to explain what is a/the "trampoline dispatcher". |
It's in KDoc to it: |
CMP-10397 Compose drops frame on iOS when dragging
CMP-10411 Register
GlobalSnapshotManageron the trampoline dispatcherSupersedes #3171
It registers
GlobalSnapshotManageron the trampoline dispatcher, so a scheduled global apply becomes just another trampoline task — and the flush drains in a loop that keeps polling until the queue is empty. That means an apply notification enqueued by running an earlier task (or a write made during pointer dispatch) is picked up in the same synchronous flush, instead of leaking to the next frame.Release Notes
Fixes - iOS