IacobIonut01 / Gallery

Light-weight Media Gallery app for Android made with Jetpack Compose
Apache License 2.0
1.16k stars 58 forks source link

[Enhancement] Display media using database as first source #185

Open st4ycool opened 10 months ago

st4ycool commented 10 months ago

Is your feature request related to a problem? Please describe. Media in timeline screen is loading too long. 5-10 seconds in my case (Pixel 6)

Describe the solution you'd like I think that storing Media in database will solve this problem

Additional context Actually, first time I noticed is when you added placeholders to TimelineScreen. After that media loading time increased drastically. Also, mediaState should really be distributed using SharedFlow, because rn every ViewModel is using content resolver to query whole media again.

IacobIonut01 commented 10 months ago

Thanks, I'll look into it about the SharedFlow.

The database mirroring is already planned but I didn't quite have the time to go through it right now

st4ycool commented 10 months ago

Something is also wrong with displaying. The gallery is heavily lagging during fast scroll. Do you know where the bottleneck is, maybe I will try to fix?

IacobIonut01 commented 10 months ago

It's a memory handling issue with Glide, I'm still trying to figure it out, so far locally managed to diminish the issue, but not fix it completely.

If you have any suggestions lmk, fyi I've tried coil (it doesn't cache on disk) in the past and it wasn't as fast as glide, but I'll give it one more try, and maybe some library that wraps around compose to integrate glide

st4ycool commented 10 months ago

My first thought was that the issue was just about the absence of pagination. But for the memory handling ... I am not sure. It feels like it's not about caching, but about preloading in the right time

IacobIonut01 commented 10 months ago

The pagination is already handled by the lazygrid, preloading as well by glide compose, I've tested with various devices and while the scrolling wasn't an issue for them, I've found other issues regarding the data update (a lot of blocking GC allocs to free up the memory).

I'm working to try to keep the data transfer to a minimum

st4ycool commented 10 months ago

I searched, and there are no built-it paging mechanisms. You need to use Paging3 for that...

IacobIonut01 commented 10 months ago

By pagination I meant the lazy loading of the data.

The lazygrid displays the data the fastest without Pagination library, this is not the issue

With Memory Analyzer from AS you can deep dive and see a huge spike of the allocated bitmaps, which is the main issue. I'm trying to figure out a way to avoid blocking GC calls

st4ycool commented 10 months ago

So, Glide has preloader that preloads bitmaps as you scroll, and lazy nature of Grid makes this work as pagination, right? Do you have examples where this approach works? Maybe it's just wrong usecase

IacobIonut01 commented 10 months ago

Glide has an example on glide-compose on their docs / and GitHub

st4ycool commented 10 months ago

I looked through it. Some notes:

  1. You request glide to make a thumbnail. As pictures nowadays are enormous, you basically ask Glide to resize each picture. That's a lot of CPU work. Instead, thumbnails should be requested from MediaStore - thumbnails there are generated once and stored for each pic. I tried a simple solution and it seems like it works.
  2. Glide memory caching - this is an error. You can't cache the user's gallery, it just doesn't make sense. Filling up the cache and clearing it frequently - that's what causes GC (maybe). I tried to disable it with .setSkipCache(true).
  3. I went through Glide's documentation and no, it's not really pagination. Yes, they support it for LazyLists, but for grids - no. For lists they use GlideLazyListPreloader, which knows in what direction the user scrolls and prepares images. In the Gallery scenario, you are using preloadingData to wrap the whole collection (also, lazy initialization from a huge collection is not fast), and then requestBuilder, but it doesn't seem to me like pagination at all... If you could explain to me how it is supposed to work, pls do. But I guess the solution will be to add real pagination, that will preload images in batches according to where the user scrolls
  4. Long initial loading of gallery is caused by long parsing time of Cursor. Media objects are returned back to repository only when they all are parsed. I suppose this should be done in batches
IacobIonut01 commented 10 months ago

Can you push somewhere the first thing?

st4ycool commented 10 months ago

yeah ok. But tell me what you think about other 3!

st4ycool commented 10 months ago

Initial loading time from HEAD to older commits:

~HEAD 13:19:04.178 getMediaDebug I get media in search vm 13:19:04.518 getMediaDebug I get media in repeat on resume media view model 13:19:04.529 getMediaDebug I resolving media started at 1698142744529 13:19:04.600 getMediaDebug I resolving media started at 1698142744600 13:19:11.272 getMediaDebug I resolving media started at 1698142751272 took 6 seconds 13:19:11.350 getMediaDebug I resolving media started at 1698142751350 took 6 seconds

commit 75355357 13:40:10.885 getMediaDebug I resolving media started at 1698144010885 13:40:10.890 getMediaDebug I resolving media started at 1698144010890 13:40:10.925 getMediaDebug I get media in search vm 13:40:10.930 getMediaDebug I resolving media started at 1698144010930 13:40:13.672 getMediaDebug I get media in repeat on resume media view model 13:40:13.681 getMediaDebug I resolving media started at 1698144013681 13:40:19.311 getMediaDebug I resolving 3731 media started at 1698144019311 took 8 seconds 13:40:19.538 getMediaDebug I resolving 3731 media started at 1698144019538 took 8 seconds 13:40:19.538 getMediaDebug I resolving 3731 media started at 1698144019538 took 8 seconds 13:40:20.181 getMediaDebug I resolving 3731 media started at 1698144020181 took 6 seconds 13:40:35.620 getMediaDebug I get media in search vm

commit 43e09b10 14:00:05.811 getMediaDebug I resolving media started at 1698145205811 14:00:05.881 getMediaDebug I get media in search vm 14:00:06.188 getMediaDebug I resolving media started at 1698145206188 14:00:09.676 getMediaDebug I resolving 3731 media started at 1698145209676 took 3 seconds 14:00:09.974 getMediaDebug I resolving 3731 media started at 1698145209973 took 3 seconds

IacobIonut01 commented 10 months ago

I looked through it. Some notes:

  1. You request glide to make a thumbnail. As pictures nowadays are enormous, you basically ask Glide to resize each picture. That's a lot of CPU work. Instead, thumbnails should be requested from MediaStore - thumbnails there are generated once and stored for each pic. I tried a simple solution and it seems like it works.
  2. Glide memory caching - this is an error. You can't cache the user's gallery, it just doesn't make sense. Filling up the cache and clearing it frequently - that's what causes GC (maybe). I tried to disable it with .setSkipCache(true).
  3. I went through Glide's documentation and no, it's not really pagination. Yes, they support it for LazyLists, but for grids - no. For lists they use GlideLazyListPreloader, which knows in what direction the user scrolls and prepares images. In the Gallery scenario, you are using preloadingData to wrap the whole collection (also, lazy initialization from a huge collection is not fast), and then requestBuilder, but it doesn't seem to me like pagination at all... If you could explain to me how it is supposed to work, pls do. But I guess the solution will be to add real pagination, that will preload images in batches according to where the user scrolls
  4. Long initial loading of gallery is caused by long parsing time of Cursor. Media objects are returned back to repository only when they all are parsed. I suppose this should be done in batches
  1. I've tried disabling the memory cache with skipMemoryCache(true), but as glide's docs, this is not a guarantee the bitmaps won't be stored in memory. You can get blocking GC allocs consistently by moving to trash a large amount of items (10-12+), going to Trash screen, moving them out of the trash and go back to main screen (check logs)

  2. glide's doc is not up to date, their latest impl is no longer using lazylist's state, but their own impl

    val preloadingData = rememberGlidePreloadingData(
        data = mediaState.media,
        preloadImageSize = Size(24f, 24f)
    ) { media: Media, requestBuilder: RequestBuilder<Drawable> ->
        requestBuilder
            .signature(MediaKey(media.id, media.timestamp, media.mimeType, media.orientation))
            .load(media.uri)
    }
  3. if you can do a reliable way to parse them in batches while also keeping the selections as well, go for it, I didn't give a try since the time it takes parsing the data from the cursor is quite small, in comparison with bitmap loading.

About the thumbnails of the mediastore, I've been trying to get the thumbnail to be loaded first, but you should also know that only items from Internal/DCIM and Pictures have thumbnails generated by the MediaStore, everything else is skipped.

Also a personal opinion: I don't think so pagination is required on a local database, would make more sense on a remote (internet) one.

About the last comment: Please link those commits since I can't find those sha's locally. Btw, compose acts the best while compiling in release flavor (staging is better than debug, but slower than release)

IacobIonut01 commented 10 months ago

Did my own timing (on staging flavor) and got: [1698268130432] Count: 2256. Time: 182ms

IacobIonut01 commented 10 months ago

100-200ms pretty consistently, also pushed an update to the mediastore observer to fetch new cursor data only when MediaStore version changes

IacobIonut01 commented 10 months ago

https://github.com/IacobIonut01/Gallery/commit/16cefa02af838574bfdf3f05bdd562135f2382fe This basically fixes the blocking GC Allocs happening after batch operations, I've found out the cursor is triggered way too often because of the uri content observer

st4ycool commented 10 months ago

What device do you use for testing? It's insane that timings differ so much. I tested on Pixel 6 and I was measuring just Cursor parsing time for all gallery files

IacobIonut01 commented 10 months ago

What device do you use for testing? It's insane that timings differ so much. I tested on Pixel 6 and I was measuring just Cursor parsing time for all gallery files

Pixel 6 with a gallery size of 2200~

IacobIonut01 commented 10 months ago

What device do you use for testing? It's insane that timings differ so much. I tested on Pixel 6 and I was measuring just Cursor parsing time for all gallery files

I'm measuring inside getMedia, timestampStart = before starting query, timestampEnd = after final sort of the media (at the return line)

st4ycool commented 10 months ago

I was measuring with kotlin's mesureTime. Ok let's try simpler approach