Wip: Fix/library memory and shuffle anr - #1437
Open
mmarca-tech wants to merge 5 commits into
Open
Conversation
4 tasks
|
mmarca-tech
force-pushed
the
fix/library-memory-and-shuffle-anr
branch
2 times, most recently
from
August 8, 2026 14:58
38811cc to
588dde0
Compare
Two independent causes, both measured on device with a scripted scrollbar drag. scrollToThumbOffset ended in scrollBy, which does not jump -- it asks the layout manager to travel that distance, laying out and binding every row it passes over. One pixel of thumb travel maps to hundreds of pixels of content on a large library, so a single touch event could lay out thousands of rows. Frame traces put 2.5s of a 2529ms frame between HandleInputStart and AnimationStart, entirely inside touch handling before any drawing began. Keep scrollBy for the small deltas a slow drag produces so fine positioning stays exact, and jump straight to the target row past a couple of screenfuls. That left the bottleneck in measure/layout rather than input. Coil defaults its fetch and decode contexts to Dispatchers.IO, which is 64 threads wide, so flinging past a row per frame let dozens of cover decodes run at once and take every core with them. No individual decode is slow; the UI thread simply never got scheduled. Both stages now share one bounded dispatcher, capping total concurrent cover work. janky frames 28 -> 3 -> 0.26% worst frame 2529ms -> 828ms -> 12ms (99th percentile) Disabling cover loading outright scored the same as the bounded dispatcher, so between them these account for all of the measured jank. Also memoize the fast scroll popup text against the position it came from, rather than re-deriving it on every draw pass (it re-read the sort from SharedPreferences and could allocate a Calendar or MeasureFormat). This one is a cleanup, not a fix: measurement ruled it out as a cause of the jank above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Aimed at the OutOfMemoryError and ANR reports from users with large libraries. Each of these was found by reading the code against the reported stack traces; none could be reproduced on a 693-song test library, so treat the reasoning rather than any measurement as the justification. CoverCompositionFetcher decoded source covers with a bare decodeStream, so artist/genre/playlist art was decoded at native resolution and four were held at once before compositing. A 2000x2000 cover is 16MB as ARGB_8888, and these run concurrently while a list scrolls. Decode bounds first and sample to the composition size; nothing above it was ever drawn. DBCache memoized the entire cache table so a load would not issue a query per file, but never released it, and the holder is a singleton -- a second complete copy of every tag of every song retained for the lifetime of the process. Release it once cleanup ends the load. MediaSessionHolder resolved a description for every song in the queue and handed the whole list to setQueue, which parcels it to the system. Shuffling all songs makes that the entire library, on the main thread, on every queue mutation, large enough to risk TransactionTooLargeException. Publish a window around the current song and let it follow the index; ids stay absolute so skip-to-item still addresses the right song. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThemeOverlay.Auxio.UncheckableIconButton defined colorContainer and colorOnContainer but not the Checked/Unchecked variants. Material's icon button color selectors reference those regardless of whether the button can actually be checked, and the missing attributes do not fall back gracefully: inflating the ColorStateList throws UnsupportedOperationException, which ResourcesCompat catches and logs with a full stack trace -- on the main thread, every time one of these buttons is laid out. ThemeOverlay.Auxio.Button.PrimaryTonal right below it already defined the full set. Caught while tracing a multi-second UI freeze. In the eight seconds the main thread was blocked, ResourcesCompat accounted for 8856 of roughly 10000 log lines from the app, from 144 separate throw-and-dump events. Over the whole capture it was 68304 lines from 1168 events. Defining the full set takes that to zero, verified by reproducing the same interaction: ResourcesCompat log lines 68304 -> 0 ColorStateList inflation errors 584 -> 0 This affects release builds too; the logging is not debug-gated. Note the failed inflation also meant these buttons were falling back to framework default colors rather than the intended ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WidgetComponent asked for Size.ORIGINAL, so every widget update decoded the full-resolution album art, ran three transformations over it, and marshalled the result into a RemoteViews bound for the launcher. The widget draws that cover at a couple of hundred pixels -- the default widget is 180x100dp -- so nearly all of it was wasted. update() runs on almost every playback event, including onProgressionChanged, so this fires constantly. Shuffling repeatedly produced 164 updates in one capture and drove total PSS past 1GB while the Java heap stayed under 130MB, because bitmap memory is native rather than Java. MediaSessionHolder already clamps its own bitmap via getBitmapDimensionLimit; the widget path never did. Bound it to 512px. Measured over ten rapid shuffles: PSS growth +175MB -> +7MB Graphics growth +53MB -> unchanged janky frames 14.76% -> 0.61% The corner radius applied to this bitmap is an absolute pixel value sized for the widget, so bounding the bitmap also makes that radius land at roughly its intended proportion instead of being invisibly small on a multi-thousand-pixel cover. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Queue ownership moves from ExoPlayer into ExoPlaybackStateHolder. The player now only ever holds a window of up to 51 MediaItems centered on the current song, slid as playback advances; the full queue lives in plain heap/mapping/index fields that resolveQueue() snapshots, in the same representation RawQueue always used. Handing the player a MediaItem per queue entry was the largest single consumer behind the large-library OutOfMemoryErrors: measured at ~77MB of Java heap for a 40k-song "play from all" queue, transiently doubled on every reshuffle, on top of a library model that already runs ~100MB at that size. Playback buffers allocate from the same heap, which is why the reported traces died inside PlaylistTimeline construction and shouldContinueLoading. Measured on device with the library inflated to 40194 songs: steady heap after shuffle-all 118MB -> 53MB (baseline 47MB) transient peak 159MB -> 83MB after skips + 4 reshuffles 219MB -> 89MB Notes on the mechanics: - Queues that fit entirely in the window behave exactly as before, including native repeat handling. - On larger queues RepeatMode.ALL is emulated by letting the window wrap circularly around the queue edges, so skips and automatic advances cross the seam without the player ever repeating on its own. - Window slides append/remove at the edges only, so gapless preloading of the next song survives ordinary playback. Queue mutations rebuild the window around the current item without interrupting it. - BetterShuffleOrder and the timeline unscrambling walk existed only to read the queue back out of the player, and are deleted; the shuffle mapping is now generated and owned directly. Verified on device: shuffle-all, 30 rapid skips across the window edge, prev, and state persistence across process death all behave; the media session queue index tracked exactly through all of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mmarca-tech
force-pushed
the
fix/library-memory-and-shuffle-anr
branch
from
August 8, 2026 15:15
ccc38f8 to
4b34339
Compare
|
You should read #1325, I doubt this is going to be merged as it's authored by Claude. |
|
For what it's worth, the PR addresses long standing OOM crashes and hangs which have been in the app since at least june 2024 #957 (comment) |
Author
|
@swedneck its still a work in progress, I'm trying to see which possible solutions can be applied on large music stacks with the least intromission of code possible , also we are taking some time to test it out with @foss- that has a +40k song list. If you see the commits I'm not doing any crazy llm bloating
However, if it is not wanted I will drop it for sure. Do not want to disturb. |
|
trying this out, will report back 🙏🙏 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is it?
Description of changes
BitmapFactory.decodeStream with no BitmapFactory.Options. That decodes album art at its native resolution, and it holds four of them live simultaneously before compositing. A 2000×2000 cover is 16MB as ARGB_8888, so one artist thumbnail could transiently need 64MB+. This runs for every artist, genre, and playlist row concurrently across Coil's dispatcher while you scroll. The handed the full-resolution bitmap straight back as the list thumbnail.
Shuffling a library-sized queue means roughly N²/4 element moves on the main thread. It's not a cold path either: resolveQueue() calls it on every ack while shuffled, including every automatic track transition. Switched to an ArrayDeque with addFirst/addLast, which is O(1) amortized.
DBCache.kt memorizes selectAllSongs() into a Map so a load doesn't issue a query per file but never releases it. I added a release() called from cleanup(), which terminates every load; it repopulates lazily if another load starts.
Fixes the following issues
#957
APK testing
https://drive.proton.me/urls/0GFWF4DK00#h3UwSGnRo2iS
Due diligence