Hey guys, after several hours of debugging I encou...
# coroutines
s
Hey guys, after several hours of debugging I encountered a behavior of Coroutines that leaves me confused and questions my understanding of Coroutines.
So in the UI on a button click I was starting a network request in the background (through a ViewModel) and display an animation (in Jetpack Compose) at the same time. The animation was stuttering and after several hours of debugging I found out that the network request is the problem. The call in the ViewModel looks something like this:
Copy code
fun onButtonClick() {
  mainScope.launch {
    performNetworkRequestUseCase()
  }
}
mainScope
is a singleton (injected) instance of
MainScope
and
performNetworkRequestUseCase
is a class that looks like this
Copy code
suspend fun invoke() {
  withContext(Dispatchers.IO) {
    doNetworkRequest()
  }
}
The default dispatcher of
MainScope
is
Main
so obviously anything that runs in there might interfere with UI rendering. Since I'm aware of that I used
withContext(<http://Dispatchers.IO|Dispatchers.IO>)
in
invoke
of the use case. However, the stuttering only disappeared after I changed the code in the ViewModel to the following
Copy code
fun onButtonClick() {
  mainScope.launch(Dispatchers.IO) { // note the Dispatchers.IO here
    performNetworkRequestUseCase()
  }
}
Now I'm confused. Why do I have to explicitly declare a dispatcher for
launch
when the suspending functions does the same with
withContext
? Why does it make a difference? There is just this single call of the use case. There's no other operation in the coroutine.
Before you ask: I deliberately use
MainScope
in the ViewModel and not the provided
viewModelScope
because this operation should keep running even when the ViewModel was disposed.
If we leave out the abstraction it would mean that
Copy code
mainScope.launch {
  withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
    doNetworkRequest()
  }
}
is different to
Copy code
mainScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
  withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
    doNetworkRequest()
  }
}
Does that make sense? ๐Ÿ˜ตโ€๐Ÿ’ซ
l
1. If you use a couroutines-aware networking library, like Retrofit or Ktor, no need to set
<http://Dispatchers.IO|Dispatchers.IO>
at all when calling the network request. The libraries do it for you under the hood. 2. If you need to have a request which survives ViewModel being canceled/cleared, then better solution is to use something like this
Copy code
object ApplicationScope {
    val instance = CoroutineScope(SupervisorJob())
}
Also I'm not sure how correct it is to launch a request, which needs to survive the ViewModel, on injected
mainScope
inside a ViewModel. Wouldn't it hold a reference to the
mainScope
instance of the destroyed ViewModel? Probably that's not an issue though, only the ViewModel cannot be garbage collected, but this should not be your problem here. We do the requests which need to survive the ViewModel being destroyed inside a Repository. Repository lives the whole application lifecycle.
s
I don't think this
Copy code
object ApplicationScope {
    val instance = CoroutineScope(SupervisorJob())
}
is any different to how I do it already.
mainScope
is a singleton managed via dependency injection.
MainScope
is a Coroutine with a
SupervisorJob
so it's basically the same? Anyway, I don't think the issue is the injected
mainScope
. The network call is a Firebase Functions call
Copy code
functions.getHttpsCallable("function").call().await()
where
await
is a
suspend
extension function on
Task<T>
, which is returned by
getHttpsCallable
.
l
Have you tried completely removing
<http://Dispatchers.IO|Dispatchers.IO>
?
For this call
s
Yes, then the animation is stuttering again.
l
MainScope
is different. It has
Dispatchers.Main
. My
ApplicationScope
doesn't set a dispatcher.
I suppose the Firebase extension function is also setting
<http://Dispatchers.IO|Dispatchers.IO>
correctly for itself.
The rule in coroutines is that a suspending function should never be blocking. Setting
<http://Dispatchers.IO|Dispatchers.IO>
is only needed for blocking functions.
s
Okay. But what I basically want to know is why
launch(<http://Dispatchers.IO|Dispatchers.IO>)
is required when there is already a
withContext(<http://Dispatchers.IO|Dispatchers.IO>)
? Regardless of what Dispatcher the CoroutineScope has set, the
withContext
should "override" that, no?
By the way,
await
from the
kotlinx-coroutines-play-services
artifact (and
awaitImpl
) does not set a Dispatcher.
Task
is from the Play Services SDK and does not necessarily have to be a network request.
l
I'm not sure if
awaitImpl
has to set a Dispatcher at all, because it's not blocking. It's just suspending a coroutine, that's different.
It's not blocking the thread.
Can you try with my application scope?
I.e. not setting a dispatcher at all?
Because
MainScope
sets a
Dispatchers.Main
first
I found this in the documentation of `withContext`:
Calls to withContext whose context argument provides a CoroutineDispatcher that is different from the current one, by necessity, perform additional dispatches: the block can not be executed immediately and needs to be dispatched for execution on the passed CoroutineDispatcher, and then when the block completes, the execution has to shift back to the original dispatcher.
So you have an (unnecessary) additional dispatch if you switch from
Dispatchers.Main
to
<http://Dispatchers.IO|Dispatchers.IO>
s
> Can you try with my application scope? Yes but I think we disgress. Regardless of what Dispatcher the scope has, I want to know why
withContext
apparently isn't working.
So you have an (unnecessary) additional dispatch if you switch from
Dispatchers.Main
to
<http://Dispatchers.IO|Dispatchers.IO>
Might be true but I still want to know how and why this impacts the
Main
dispatcher.
l
๐Ÿคท
s
I don't want a better solution right now, I want to understand the problem ๐Ÿ˜‰
l
Theory is when it doesn't work but you know why, practice is when it works but you don't know why ๐Ÿ˜‰
We use this solution and it works
Unfortunately I cannot explain why.
m
Sorry, could not resist. This is from the Android docs: โ€œA coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously.โ€
๐Ÿ˜… 1
s
Did you run a profiler/system-tracer to see what calls makes your main UI thread stutter, which blocking calls are causing this?
s
No
m
Just out of curiosity, in how far is using
object ApplicationScope
any different from using
GlobalScope
.
l
GlobalScope doesn't have a job, so it cannot be canceled. It cannot be configured with a CoroutineContext / dispatcher. Although in this case it should behave similarly, as we don't want to cancel the call anyway.
๐Ÿ™ 1
s
Changing from
MainScope()
to
CoroutineScope(SupervisorJob())
for our global application scope did actually "solve" the problem of having to specify a dispatcher for
launch
, but I would still like to know why
withContext
alone doesn't seem to work or rather has an impact on the main (UI) thread?
l
Probably because the first, external coroutine is still started on
Main
dispatcher, and only then
withContext
dispatches it on another thread. I don't know how big the impact is though and how much stuttering you experienced.
c
But shouldn't it immediately suspend on the
withContext
call and leave the main thread available for whatever else? I don't understand how this would cause stuttering.
โ˜๐Ÿผ 1
s
That confuses me, too.
l
@svenjacobs did you check recompositions? Maybe there was something recomposing too much and it caused stuttering. Although that would not be fixed with changing dispatcher.
s
Due to the animation there was quite some recomposition and first I thought that was the reason for the stuttering. But changing the Dispatcher solved the stuttering without changing anything on the UI front.
l
Anyway animation always causes recomposition every frame, that's normal.
๐Ÿ‘๐Ÿผ 1
s
@svenjacobs Try running your code with the profiler and see what code holds up the main thread. I'd be very curious of what that could be
z
Did you profile to see what method calls are actually happening on the main thread?
It sounds like you're inferring from the UI stutter that in the
withContext
case the network call is happening on the main thread, but i'd want to add some logging or debug or profile to actually see what's causing the UI stutter before jumping to assumptions.
This does seem surprising though, launching a coroutine definitely isn't enough to cause frame drops.
s
I'm sorry guys but I'm not very experienced with profiling apps. Never had to do it before. In "View Live Telemetry" I can see that during the frame drops the main thread is active but when I hover over it it says "Details Unavailable" even for a debuggable app. What do I have to do?
s
Did you follow these steps to profile? https://developer.android.com/studio/profile
s
Yes, I started the app with "Profile 'app' with complete data" and then selected View Live Telemetry.
This is what I see
s
Did you figure out how to see the system trace UI as shown in Figure 2. of this page:? https://developer.android.com/topic/performance/tracing
r
This is a shot in the dark, but can you compare the content of your
coroutineContext
for both cases in your code? Just doing
println(coroutineContext)
inside of
launch(<http://Dispatchers.IO|Dispatchers.IO>) { ... }
vs
launch { withContext(<http://Dispatchers.IO|Dispatchers.IO>) { โ€ฆ } }
would be enough I just want to make sure that the correct
CoroutineDispatcher
is indeed present in the context
s
So this is the output for just
launch
without any dispatcher where
First context
is right after
launch
and
Second context
is inside the
withContext(<http://Dispatchers.IO|Dispatchers.IO>)
Copy code
First context [StandaloneCoroutine{Active}@5f2d8ef, Dispatchers.Main]
Second context [DispatchedCoroutine{Active}@e595ffc, <http://Dispatchers.IO]|Dispatchers.IO]>
and this is with
launch(<http://Dispatchers.IO|Dispatchers.IO>)
Copy code
First context [StandaloneCoroutine{Active}@f2a1881, <http://Dispatchers.IO]|Dispatchers.IO]>
Second context [kotlinx.coroutines.UndispatchedMarker@d29ed26, UndispatchedCoroutine{Active}@27fbd67, <http://Dispatchers.IO]|Dispatchers.IO]>
@streetsofboston Here's the system trace. At second 00:12.339 we can see that frame 539530 missed a deadline. Around the same time the "Firebase Backgr" thread becomes active and keeps running past the deadline of the frame. That is the network call I was talking about because we're calling a Firebase Function here, which is a HTTP request. But I still don't understand how and why the Firebase background thread impacts the main thread.
s
This is a super wide ass guess, but could it be that the Firebase thread(s), that serve the Firebase service, have a super high priority, starving the main UI thread?
๐Ÿคท๐Ÿผโ€โ™‚๏ธ 1
l
Maybe you could open an issue/question in the Firebase SDK Github?
s
I'll wait till at least Monday. Maybe someone else has an idea or can better interpret the system trace ๐Ÿ™‚