Skip to content

Android: await frame #1474

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

Closed
ansman opened this issue Aug 26, 2019 · 9 comments
Closed

Android: await frame #1474

ansman opened this issue Aug 26, 2019 · 9 comments

Comments

@ansman
Copy link
Contributor

ansman commented Aug 26, 2019

It seems like #737 might have regressed, yielding on the immediate main dispatcher while on the main thread is currently a no-op as shown by this example:

        Log.d("Application", "Before")
        GlobalScope.launch(Dispatchers.Main.immediate) {
            Log.d("Application", "Before yield")
            yield()
            Log.d("Application", "After yield")
        }
        Log.d("Application", "After")

The example above is placed inside the Application.onCreate which is already running on the main thread.

The expected output would be:

Before
Before yield
After
After yield

But the actual output is:

Before
Before yield
After yield
After
@elizarov
Copy link
Contributor

This is not a regression. It never worked this way after #737. The improvement that we did in #737 was to support yielding into the "unconfined" virtual event loop that is being created for things like Dispatcher.Unconfined and for immediate dispatchers when there are multiple "immediately dispatched" events are working. We do not currently support yielding to the "true dispatcher's event loop" from immediate dispatchers. I'll keep your issue open as an enhancement.

Can you, please, go into a little bit more detail of what is your use-cases? Why would you need to be able to use yield() like that?

@ansman
Copy link
Contributor Author

ansman commented Aug 27, 2019

My Android code looks like this:

viewCreatedScope.launch {
  // Suspends until the predraw listener fires just before rendering a frame
  view.awaitPreDraw() 
  prepareViewForAnimation()
  // This is needed because we need to finish rendering the frame we just set up
  // before starting the animation. Ideally this would be a `yield()` instead.
  delay(1)
  TransitionManager.beginDelayedTransition(view)
  moveViewsToEndState()
}

It would also be nice to let other things on the main thread run.

@elizarov
Copy link
Contributor

elizarov commented Aug 27, 2019

Frankly, I don't think that yield is appropriate for your case. Even if we implement it somehow, there will be zero guarantee that "rendering frame is finished" after call to yield(). It looks like you you need some Android-specific suspending function awaitRenderedFrame() here. If you know how to implement it on android (what exactly are you waiting), then you are welcome to contribute it to the kotlinx-coroutines-android module.

@ansman
Copy link
Contributor Author

ansman commented Aug 27, 2019

That might be. But posting on a main handler will definitely allow the rendering to complete as it will happen when returning from the pre draw listener.

@JakeWharton
Copy link
Contributor

That's only true when the message sent to the handler is waiting for vsync (aka a frame). Coroutines dispatchers wrapped around handlers by default will set the async flag on the message which will not wait for a frame.

@elizarov elizarov changed the title yield should always perform a dispatch Android: await frame Sep 18, 2019
@keyboardr
Copy link

This caught me when updating my fragment-ktx dependency. I had previously been doing something like this:

lifecycleScope.launch {
  while(conditionNotMet()) {
    yield()
  }
  doTheThingThatRequiresTheCondition()
}

lifecycleScope's dispatcher switched from Main to Main.immediate (generally a good thing), but that meant my while loop caused it to ANR instead of making progress. It took me quite a while to realize that yield() wasn't doing what I expected.

@JakeWharton
Copy link
Contributor

This change is breaking Retrofit's mitigation against exceptions that are thrown synchronously which relies on yield().

@elizarov
Copy link
Contributor

Uh. Oh. We'll see what we can do.

@elizarov elizarov self-assigned this Nov 20, 2019
elizarov added a commit that referenced this issue Nov 20, 2019
As special internal fun CoroutineDispatcher.isDispatchSupported is
introduced to speical-case Unconfined dispatcher that cannot be
dispatched to. All other dispatcher return `true`. This gives a little
more freedom in distinguishing dispatchers

           | isDispNeeded | isDispSupported
Regular    | true         | true
Immediate  | depends      | true
Unconfined | false        | false

So yield now checks for "isDispatchNeeded" and works properly for
immediate dispatchers.

Fixes #1474
elizarov added a commit that referenced this issue Nov 20, 2019
As special internal fun CoroutineDispatcher.isDispatchSupported is
introduced to special-case Unconfined dispatcher that cannot be
dispatched to. All other dispatcher return `true`. This gives a little
more freedom in distinguishing dispatchers

           | isDispNeeded | isDispSupported
Regular    | true         | true
Immediate  | depends      | true
Unconfined | false        | false

So yield now checks for "isDispatchSupported" and works properly for
immediate dispatchers.

Fixes #1474
elizarov added a commit that referenced this issue Nov 20, 2019
As special internal fun CoroutineDispatcher.isDispatchSupported is
introduced to special-case Unconfined dispatcher that cannot be
dispatched to. All other dispatcher return `true`. This gives a little
more freedom in distinguishing dispatchers

           | isDispNeeded | isDispSupported
Regular    | true         | true
Immediate  | depends      | true
Unconfined | false        | false

So yield now checks for "isDispatchSupported" and works properly for
immediate dispatchers.

Fixes #1474
elizarov added a commit that referenced this issue Nov 23, 2019
So yield now checks for "Unconfined" dispatcher instead of
"isDispatchNeeded" and works properly for immediate dispatchers.

Fixes #1474
elizarov added a commit that referenced this issue Nov 23, 2019
So yield now checks for "Unconfined" dispatcher instead of
"isDispatchNeeded" and works properly for immediate dispatchers.

Fixes #1474
@ansman
Copy link
Contributor Author

ansman commented Sep 18, 2020

This caught me when updating my fragment-ktx dependency. I had previously been doing something like this:

lifecycleScope.launch {
  while(conditionNotMet()) {
    yield()
  }
  doTheThingThatRequiresTheCondition()
}

lifecycleScope's dispatcher switched from Main to Main.immediate (generally a good thing), but that meant my while loop caused it to ANR instead of making progress. It took me quite a while to realize that yield() wasn't doing what I expected.

@keyboardr Be aware that this issue still exists if you yield inside a whenResumed block. This will be fixed when https://issuetracker.google.com/issues/168777346 is resolved (hopefully next lifecycle release).

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

No branches or pull requests

4 participants