coil-kt / coil

Image loading for Android and Compose Multiplatform.
https://coil-kt.github.io/coil/
Apache License 2.0
10.78k stars 660 forks source link

Displaying local images (from Gallery) stops at one point or completely stalls for 10-20 seconds #1345

Closed kroussevrb closed 2 years ago

kroussevrb commented 2 years ago

This is an updated version of a prematurely closed issue https://github.com/coil-kt/coil/issues/1337. The update includes a repo with a project that reproduces the issue - https://github.com/kroussevrb/coil-gallery-example the lack of which was the reason for the closure the last time around.

Describe the bug Implement the simplest possible grid of gallery images from the device's storage either using compose (LazyColumn with Rows or LazyVerticalGrid (but its performance is even worse) or Android Views (RecyclerView and ImageView). Use AsyncImage for the Compose implementation or Coil loader into ImageView, doesn't matter.

Start scrolling. Some images (absolutely regular .jpg files with regular sizes) load extremely slowly - anywhere between 0.5 seconds and 5 seconds each. No explanation. No errors in the logs.

Try every possible combination of parameters in the image request builders. Different dispatchers, thread pools with different thread sizes, thread priorities, turning on/off memory and disk cache, setting up memory cache with different options, passing lifecycles, turning hardware on/off, bitmap rendering options changes.. literally everything that the builder provides. No success. Try with a release build (for Compose) - same result.

Additionally, sometimes there is a single image that throws an error while being decoded. The error is caught internally and displayed in LogCat. For some reason all image loading stops.

To Reproduce As described above a vanilla implementation using compose or android view will do, and use a real device with real photos made with camera and/or downloaded from the internet.

Logs/Screenshots When profiling this, it becomes apparent how all the fetcher/interceptor/decoder threads start sleeping while there are no other threads in the app working - this causes the delay in image loading. There is no obvious explanation of why that happens.

Version Latest at time of writing

Here is the code https://github.com/kroussevrb/coil-gallery-example

Screen Shot 2022-06-24 at 10 27 12

Reproduced on Huawei P20 Pro with Android 10 and on a coworker's device (don't know what it was)


AndroidView implementation with RecyclerView

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val itemViewModel:ItemViewModel = imageList[position]
        runCatching {
            holder.imageView.load(
                itemViewModel.image,
                builder = {
                    this.size(360)
                        .placeholder(R.drawable.ic_launcher_background)
                        .error(R.drawable.ic_launcher_foreground)
                        .allowHardware(true)
                        //.fetcherDispatcher(Executors.newFixedThreadPool(5).asCoroutineDispatcher())
                        //.decoderDispatcher(Executors.newFixedThreadPool(5).asCoroutineDispatcher())
                        .interceptorDispatcher(Dispatchers.IO)
                        .fetcherDispatcher(Dispatchers.IO)
                        .decoderDispatcher(Dispatchers.IO)
                        .memoryCachePolicy(CachePolicy.DISABLED)
                        .diskCachePolicy(CachePolicy.DISABLED)
                        .precision(Precision.INEXACT)
                        .networkCachePolicy(CachePolicy.DISABLED)
                }
            )
        }
    }

In both Compose and AndroidView implementations, I swapped Coil for Glide and it worked extremely well. However Glide has its own problems (doesn't support KSP among other things).

kroussevrb commented 2 years ago

@colinrtwhite (sorry I pinged you in the closed counterpart of this issue that hadn't met your expectations. Deleted that ping)

colinrtwhite commented 2 years ago

@kroussevrb I'm not able to reproduce the issue you described with the repro app. Here's a video of me scrolling through several images from my gallery on my test Pixel 3 device running Android 12. I disabled the memory cache so the image is re-decoded every time.

https://user-images.githubusercontent.com/5580160/180663816-adaf2df7-ee2f-46f1-86e5-813a4ebb5bc1.mp4

Additionally, sometimes there is a single image that throws an error while being decoded. The error is caught internally and displayed in LogCat. For some reason all image loading stops.

What is the stack trace?

kroussevrb commented 2 years ago

Hi, I will try to get you the stacktrace as soon as possible. In my tests I noticed the delay only for certain batches of pictures a lot further down in my gallery. For this test you gotta use a real person's device with real photos with various sources from the past few months. The batches that didn't work out for me were 4-5 "pages" down. If I only had 1-2 pages worth of photos on my device I wouldn't've noticed that issue too.

colinrtwhite commented 2 years ago

@kroussevrb Do you have the stack trace? I tested the app on a much larger photo gallery and wasn't able to reproduce the issue.

kroussevrb commented 2 years ago

So sometimes I see this in correlation with very slowly loading batches of images (like 3-5 seconds)

2022-08-08 10:47:14.478 10431-10631/com.asd.android.sandbox D/skia: ---- read threw an exception
2022-08-08 10:47:14.478 10431-10631/com.asd.android.sandbox D/skia: libjpeg error 105 <  Ss=0, Se=63, Ah=0, Al=0> from Incomplete image data

(Those same images load immediately with Glide)

Sometimes a screen will be empty for a few seconds, then the following will be printed and simultaneously with it most images will load almost immediately:

2022-08-08 10:47:28.261 10431-10806/com.asd.android.sandbox W/System.err: java.io.InterruptedIOException: interrupted
2022-08-08 10:47:28.262 10431-10806/com.asd.android.sandbox W/System.err:     at okio.Timeout.throwIfReached(Timeout.kt:98)
2022-08-08 10:47:28.262 10431-10806/com.asd.android.sandbox W/System.err:     at okio.InputStreamSource.read(JvmOkio.kt:91)
2022-08-08 10:47:28.262 10431-10806/com.asd.android.sandbox W/System.err:     at okio.RealBufferedSource.read(RealBufferedSource.kt:189)
2022-08-08 10:47:28.262 10431-10806/com.asd.android.sandbox W/System.err:     at okio.ForwardingSource.read(ForwardingSource.kt:29)
2022-08-08 10:47:28.262 10431-10806/com.asd.android.sandbox W/System.err:     at coil.decode.BitmapFactoryDecoder$ExceptionCatchingSource.read(BitmapFactoryDecoder.kt:193)
2022-08-08 10:47:28.262 10431-10806/com.asd.android.sandbox W/System.err:     at okio.RealBufferedSource$inputStream$1.read(RealBufferedSource.kt:158)
2022-08-08 10:47:28.262 10431-10806/com.asd.android.sandbox W/System.err:     at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at android.graphics.BitmapFactory.decodeStreamInternal(BitmapFactory.java:876)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:845)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at coil.decode.BitmapFactoryDecoder.decode(BitmapFactoryDecoder.kt:62)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at coil.decode.BitmapFactoryDecoder.access$decode(BitmapFactoryDecoder.kt:25)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at coil.decode.BitmapFactoryDecoder$decode$2$1.invoke(BitmapFactoryDecoder.kt:32)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at coil.decode.BitmapFactoryDecoder$decode$2$1.invoke(BitmapFactoryDecoder.kt:32)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.InterruptibleKt.runInterruptibleInExpectedContext(Interruptible.kt:51)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.InterruptibleKt.access$runInterruptibleInExpectedContext(Interruptible.kt:1)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.InterruptibleKt$runInterruptible$2.invokeSuspend(Interruptible.kt:43)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.InterruptibleKt$runInterruptible$2.invoke(Unknown Source:8)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.InterruptibleKt$runInterruptible$2.invoke(Unknown Source:4)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:158)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.InterruptibleKt.runInterruptible(Interruptible.kt:42)
2022-08-08 10:47:28.263 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.InterruptibleKt.runInterruptible$default(Interruptible.kt:39)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.decode.BitmapFactoryDecoder.decode(BitmapFactoryDecoder.kt:32)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.intercept.EngineInterceptor.decode(EngineInterceptor.kt:199)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.intercept.EngineInterceptor.access$decode(EngineInterceptor.kt:41)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.intercept.EngineInterceptor$execute$executeResult$1.invokeSuspend(EngineInterceptor.kt:127)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.intercept.EngineInterceptor$execute$executeResult$1.invoke(Unknown Source:8)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.intercept.EngineInterceptor$execute$executeResult$1.invoke(Unknown Source:4)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.intercept.EngineInterceptor.execute(EngineInterceptor.kt:126)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.intercept.EngineInterceptor.access$execute(EngineInterceptor.kt:41)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at coil.intercept.EngineInterceptor$intercept$2.invokeSuspend(EngineInterceptor.kt:75)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
2022-08-08 10:47:28.264 10431-10806/com.asd.android.sandbox W/System.err:     at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
colinrtwhite commented 2 years ago

@kroussevrb InterruptedIOException is expected as it's thrown when an image load is cancelled while we're reading the data from an InputStream. It sounds like the stream isn't interrupting promptly when the stream is cancelled? That would prevent subsequent requests from executing.

It's tough to say what the issue is without a way to reproduce the issue, but if that's the case I don't think there's much Coil can do since thread interruption is a low level mechanism.

You could try setting ImageLoader.Builder.bitmapFactoryMaxParallelism(Int.MAX_VALUE), though that will likely cause UI lag.

colinrtwhite commented 2 years ago

Closing this out as I haven't been able to reproduce the issue in my testing. This issue might be related to client code or it could be device specific.

kroussevrb commented 2 years ago

I tried the max bitmap parallelism thing, didn't work. Can't be client code as I've provided a green field project. I've tried this with both compose and android legacy view system. It doesn't work with Coil either way and it works with Glide and in all other apps on my phone.

colinrtwhite commented 2 years ago

@kroussevrb Coil accepts pull requests. If you're able to reproduce the issue, it would be helpful to submit a fix. As I mentioned the green field project worked without issue for me.