Hey folks, I've got an incredibly simple Compose M...
# compose-android
s
Hey folks, I've got an incredibly simple Compose Multiplatform demo app for my library, and for some reason when I run the Android build for this demo, the network calls cause the UI to stutter, as if they're blocking the main thread. I'm making the network call using a
suspend fun
in a
LaunchedEffect
, and the client is backed by Ktor (via Ktorfit) and the OkHttp engine. As far as I can tell, these network calls shouldn't be blocking the main thread, but still the UI stutters when I scroll (triggering network calls as list items load). The problem only happens on Android. The demo on iOS, Desktop (JVM), macOS (native), JS, and WASM builds all have a totally smooth UI. I've tried: • Explicitly setting
<http://Dispatchers.IO|Dispatchers.IO>
◦ No change. Verified in Ktor source that the default is IO, so that makes sense. • Switching to the
CIO
engine on the JVM target. ◦ No change, still stutters when loading data on Android • Running as profileable to debug further ◦ The problem disappears when I run a profileable build; scrolling is smooth while data loads, so it's unclear to me how to debug further or gather data on the issue You can repro by cloning https://github.com/PokeAPI/pokekotlin and running the demo app (the
:demo-app
module) (x-post from #C0B8M7BUY)
s
I don't see anything immediately obvious, but maybe loading data causes layout changes that are visible only on Android
Loading data per item is generally not a good idea, you probably want to preload items in advance
s
> Loading data per item is generally not a good idea I get that. This is a small demo app for a library, not a production app that's ever going to get more complex than this. > you probably want to preload items in advance Well if I were making a real app, I could embed all this static data directly in composeResources. Otherwise, loading it all from the network in advance would have the screen take ~1min to load, making thousands of small network calls ahead of time. Or, more realistically, add some pagination observing whatever is visible in the LazyColumn
The problem happens even when there's only one network call loading a single item; it's just harder to see unless the network call is deliberately slowed down (say, from the developer settings toggle to throttle network speed). The problem I'm trying to solve here is: why is the UI freezing during a network call? > maybe loading data causes layout changes that are visible only on Android That's an interesting thought; maybe something different about how the UI works on Compose Multiplatform (non-Android) vs Jetpack Compose (Android)? My code itself is just swapping out the ListItem call for a different one when data loads. Maybe I can hardcode some data and load it on a delay and see if it still freezes; then I'll know whether the network call is actually the problem, or if it's more UI related
s
I think the simplest way to learn why that happens is to use a profiler
s
I've tried:
...
• Running as profileable to debug further
◦ The problem disappears when I run a profileable build; scrolling is smooth while data loads, so it's unclear to me how to debug further or gather data on the issue
s
You can profile debuggable apps fwiw
s
Oh interesting; I wasn't getting any data when I tried. Which profiler tool would you suggest? Live Telemetry and Find CPU Hotspots didn't give me any data with a debuggable build
s
See https://developer.android.com/develop/ui/compose/tooling/tracing It is not all encompassing, but might help you as well.
s
Will take a look, thanks!
s
I usually use this one, but it has a lot of runtime overhead (so the app will appear even more laggy) It records every method that is called, however, so you will see exactly what your app is doing
s
Okay, so I've found that if I use the profiler for more than ~5 seconds, I get no data out of it with a debuggable build, but it works if I run it for only a few seconds. In this 200MB (!) trace over four seconds, I see the main thread is spending a whole lot of time with deserialization and the streaming json decoder. I wonder if even though the HTTP request happens on the OkHTTP threads, the response JSON is actually decoded on the main thread? and perhaps it's blocking on the streaming HTTP response body?
s
It probably should not deserialize on the main thread, yeah
s
Yeah. These JSON blobs aren't tiny (say, ~15-20 JSON blobs load on the first page of the LazyColumn, each ~50KB, so nearly a megabyte of JSON). So maybe the problem exists on release too, but the debug overhead is really prominent in JSON serialization for some reason but fast enough on release that we don't notice
though actually, even throttling network didn't cause stutter on the release build so I suspect the deserializer is not blocking on the streaming json response, but the issue is really just the deserialization work itself (+ debug build overhead)
Although, even with
<http://Dispatchers.IO|Dispatchers.IO>
explicitly set in the result converter, the outcome is the same. Code snippet:
Copy code
override suspend fun convert(result: KtorfitResult): Result<Any> {
    return when (result) {
      is KtorfitResult.Failure -> Result.failure(result.throwable)
      is KtorfitResult.Success -> {
        withContext(getDispatcherForDeserialization()) {
          when {
            result.response.status.isSuccess() ->
              Result.success(result.response.body(typeData.typeArgs.first().typeInfo))
            // we configure the client with expectSuccess
            else -> error("impossible: " + result.response)
          }
        }
      }
    }
  }
Copy code
// non-browser platforms
internal actual fun getDispatcherForDeserialization() = Dispatchers.IO
Oh wait, I did
with
instead of
withContext
, lol
Though even with that fixed, still the same
s
Does it work if you add
withContext
inside launched effects?
s
Nope, it's the same still:
Copy code
LaunchedEffect(Unit) {
  withContext(getIoDispatcher()) { result = PokeApi.getPokemonSpecies(summary.id) }
}
Copy code
// androidMain
actual fun getIoDispatcher() = <http://Dispatchers.IO|Dispatchers.IO>
s
Not sure, maybe Ktor needs some setup to do this on the right thread, never used it 🙂
s
I guess I'll have to be satisfied with it performing fine in release only for now thanks for your help!
a
I've checked the solution to set the dispatcher explicitly, and it works on my machine in the emulator:
Copy code
LaunchedEffect(Unit) { withContext(Dispatchers.Default) { result = PokeApi.getPokemonSpeciesList(0, 100000) } }
In the debugger, I can see that the thread where the deserialization happens is now
DefaultDispatcher-worker-1
.
s
Does it actually stop freezing on network calls? I also saw the thread name change, but the behavior of the UI was the same.
a
I don't observe freezes
c
I couldn've sworn reading in the past that okhttp will deserialize on whatever thread the http client was created on or something