Illustrations are decoded at 4× — ship them in density buckets #41

Closed
opened 2026-08-18 22:47:09 -05:00 by null · 1 comment
Owner

A live memory defect, present before any artwork change and made much worse by one.

What is true now

core/designsystem/src/main/res/ has exactly two folders — drawable/ and drawable-night/ — with no density qualifier on either. Android therefore treats every illustration as mdpi and pre-scales it at decode time by the device's density bucket.

On an xxxhdpi device that is a 4× upscale: art_forecast.webp at 552 × 480 decodes to roughly 2208 × 1920, which is about 17 MB for a single ARGB_8888 bitmap. The app declares no android:largeHeap, and a non-large heap can be as low as 96–192 MB.

The contrast is instructive: the launcher icon is correctly bucketed across mipmap-mdpi through mipmap-xxxhdpi. The illustrations are not bucketed at all.

What it costs

Today it is survivable because each screen shows one illustration and the sources are small. It stops being survivable the moment the artwork ships at the higher source resolution a full-bleed hero needs — and OnboardingPreviews.kt already stacks all eight in one preview.

This is also the error-handling story. There is no try/catch, no fallback, no image-loading library and no error boundary anywhere in the app: painterResource decodes inside composition, so an OOM or a decode failure throws on the onboarding screen — on first launch, before the user has seen anything. A runCatching around a resource compiled into the APK would hide the failure rather than remove it; sizing the bitmaps correctly removes it.

What to do

  1. Generate drawable-hdpi, -xhdpi, -xxhdpi, -xxxhdpi and their -night twins from the masters, and delete the density-less copies.
  2. State the decoded cost in the commit message. At 411 dp full width the budget is roughly 1.8 MB at 2×, 4.0 MB at 3× and 7.0 MB at 4× — against ~17 MB today.
  3. Keep WebP at q82–86; the current set is ~130 KB of a 2.08 MB release APK.

Traps

  • Deleting the density-less copies is part of the fix, not tidying. A file left in drawable/ still resolves as the mdpi fallback and will be picked on any device whose bucket you missed.
  • isShrinkResources = true on release will drop unreferenced density variants, so check the release APK rather than only the debug one.
  • The masters are 3:2 landscape; do not letterbox them into the buckets.

Why filed separately

It is a defect that exists today and is worth fixing whether or not the artwork is ever redrawn.

Verify: core/designsystem/src/main/res/ contains no density-less drawable/ illustration, dumpsys meminfo on PeriodMinSdk26 shows no growth beyond the stated budget while stepping through onboarding, and the whole flow completes on the API 26 device without a crash.

A live memory defect, present before any artwork change and made much worse by one. ## What is true now `core/designsystem/src/main/res/` has exactly two folders — `drawable/` and `drawable-night/` — with **no density qualifier on either**. Android therefore treats every illustration as mdpi and pre-scales it at decode time by the device's density bucket. On an xxxhdpi device that is a 4× upscale: `art_forecast.webp` at 552 × 480 decodes to roughly 2208 × 1920, which is **about 17 MB for a single ARGB_8888 bitmap**. The app declares no `android:largeHeap`, and a non-large heap can be as low as 96–192 MB. The contrast is instructive: the launcher icon is correctly bucketed across `mipmap-mdpi` through `mipmap-xxxhdpi`. The illustrations are not bucketed at all. ## What it costs Today it is survivable because each screen shows one illustration and the sources are small. It stops being survivable the moment the artwork ships at the higher source resolution a full-bleed hero needs — and `OnboardingPreviews.kt` already stacks all eight in one preview. **This is also the error-handling story.** There is no `try`/`catch`, no fallback, no image-loading library and no error boundary anywhere in the app: `painterResource` decodes inside composition, so an OOM or a decode failure throws on the onboarding screen — on first launch, before the user has seen anything. A `runCatching` around a resource compiled into the APK would hide the failure rather than remove it; sizing the bitmaps correctly removes it. ## What to do 1. Generate `drawable-hdpi`, `-xhdpi`, `-xxhdpi`, `-xxxhdpi` and their `-night` twins from the masters, and delete the density-less copies. 2. State the decoded cost in the commit message. At 411 dp full width the budget is roughly 1.8 MB at 2×, 4.0 MB at 3× and 7.0 MB at 4× — against ~17 MB today. 3. Keep WebP at q82–86; the current set is ~130 KB of a 2.08 MB release APK. ## Traps - **Deleting the density-less copies is part of the fix, not tidying.** A file left in `drawable/` still resolves as the mdpi fallback and will be picked on any device whose bucket you missed. - `isShrinkResources = true` on release will drop unreferenced density variants, so check the release APK rather than only the debug one. - The masters are 3:2 landscape; do not letterbox them into the buckets. ## Why filed separately It is a defect that exists today and is worth fixing whether or not the artwork is ever redrawn. Verify: `core/designsystem/src/main/res/` contains no density-less `drawable/` illustration, `dumpsys meminfo` on `PeriodMinSdk26` shows no growth beyond the stated budget while stepping through onboarding, and the whole flow completes on the API 26 device without a crash.
null added this to the Batch 08 — Polish milestone 2026-08-18 22:47:09 -05:00
null added the
P1
label 2026-08-18 22:47:09 -05:00
null closed this issue 2026-08-19 01:02:10 -05:00
Author
Owner

Shipped in ae27ef6. Four density buckets per theme (617/822/1233/1644 px) and the density-less drawable/ folders are gone, so nothing is decoded at the mdpi assumption any more — that was ~17 MB of heap for one bitmap on an xxxhdpi device. 2.3 MB across every bucket; the release is an AAB and Play splits by density, so a device downloads 130-500 KB. Also the error-handling fix: painterResource decodes inside composition with no boundary anywhere, so correct sizing removes the OOM rather than hiding it. Driven on PeriodMinSdk26 with no crash or OOM.

Shipped in ae27ef6. Four density buckets per theme (617/822/1233/1644 px) and the density-less drawable/ folders are gone, so nothing is decoded at the mdpi assumption any more — that was ~17 MB of heap for one bitmap on an xxxhdpi device. 2.3 MB across every bucket; the release is an AAB and Play splits by density, so a device downloads 130-500 KB. Also the error-handling fix: painterResource decodes inside composition with no boundary anywhere, so correct sizing removes the OOM rather than hiding it. Driven on PeriodMinSdk26 with no crash or OOM.
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#41
No description provided.