Skip to content

Bound decoded-image cache memory and deprioritize ingest threads - #1648

Open
reville wants to merge 2 commits into
CyberTimon:mainfrom
reville:codex/reduce-memory-pressure
Open

Bound decoded-image cache memory and deprioritize ingest threads#1648
reville wants to merge 2 commits into
CyberTimon:mainfrom
reville:codex/reduce-memory-pressure

Conversation

@reville

@reville reville commented Aug 30, 2026

Copy link
Copy Markdown

Description

The decoded-image cache evicted by item count alone and stored every entry as f32, so a handful of large RAWs could pin several gigabytes. A 48.8 MP image costs 559 MB once decoded to Rgb32F, and the default cache holds 5, so the cache alone can reach 2.9 GB before anything else is accounted for. On unified-memory Macs that also starves the GPU, which matches the reports in #515 and #521.

Three changes:

  1. Cache entries are stored at half precision. Pixels convert to f16 on insert and back on read. The GPU pipeline already samples through Rgba16Float textures, so rendered output is unchanged. Images that are not f32 are stored untouched.
  2. Eviction enforces a byte budget as well as a count. The budget is physical RAM divided by 8, clamped to [256 MB, 4 GB]. That is 3 GB on a 24 GB machine and 1 GB on an 8 GB machine, so low-RAM machines stop caching before they swap instead of always holding 5 entries regardless of size. An image larger than the whole budget is not cached.
  3. Thumbnail and metadata workers run at Utility QoS on macOS. Folder ingest was saturating performance cores at default QoS. set_utility_thread_qos is a no-op on other platforms.

is_image_cached now calls a non-mutating contains(). It previously called get(), so a boolean probe reordered LRU position and, under this change, would have rebuilt pixels for a question that only needed a yes or no.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Performance improvement
  • Code refactoring
  • Documentation update
  • Build/CI or Dependency update

Changes Made

  • cache_utils.rs: DecodedImageCache stores CachedImage (f16 pixels plus dimensions and byte size), tracks total bytes, and evicts on both limits. Adds contains() and unit tests.
  • image_loader.rs: is_image_cached uses contains().
  • file_management.rs: adds set_utility_thread_qos, called by the thumbnail and metadata workers.

Testing

Measured on the hardware below, comparing this branch against main at 7ac8d50.

Cache footprint, inserting seven 48.8 MP images into a 5-entry cache and reading ri_phys_footprint:

per image process footprint
main 559 MB 3356 MB
this branch 280 MB 2240 MB

Application run, same 7-image folder opened, walked through, and revisited in both builds:

peak footprint final footprint
main 6065 MB 3026 MB
this branch 5217 MB 2570 MB

The cost. A cache hit is no longer free, because pixels are rebuilt from f16:

time
cache hit before 42 ns
cache hit now 8 ms
full decode (cache miss) 184-243 ms

So a hit still avoids roughly 95% of the work a miss costs, and the 8 ms lands when switching to a previously viewed image, not during editing. The image being edited is held separately in original_image and never round-trips through the cache.

Seven unit tests cover LRU order, byte-budget eviction, the oversized-image case, f16 round-trip accuracy, non-f32 passthrough, and QoS assignment. The round-trip tests use non-square images in both orientations and assert restored dimensions, so a transposed width and height cannot pass.

Cross-platform: the only platform-specific code is set_utility_thread_qos. I verified the non-macOS branch compiles by forcing it active on macOS. Everything else uses existing unconditional dependencies (half, rayon, sysinfo, image). I could not build Windows or Linux locally, so CI is the real check there.

  • These changes were tested locally by a human and confirmed to work.
  • I haven't added any automated tests to the code because the codebase currently lacks a test suite.

Test Configuration:

  • OS: macOS 26.6.2
  • Hardware: Apple M5 Pro, 24 GB

Checklist

  • My code follows the project's code style
  • I haven't added unnecessary AI-generated code comments
  • My changes generate no new warnings or errors

cargo fmt --check and cargo clippy are clean on the changed files. The two clippy warnings in exif_processing.rs are pre-existing on main.

Additional Notes

The byte budget is deliberately a fraction of RAM rather than a user setting, so the existing imageCacheSize preference keeps its meaning as a maximum. If you would rather expose the budget in settings, or pick a different fraction, I am happy to change it.

I left the two f16 conversions on rayon. They are ~22 ms on insert and ~8 ms on read for a 48.8 MP image, and both already run off the UI thread.

AI Disclaimer:

Please state the involvement of AI in this PR:

  • This PR is created by an AI agent
  • This PR is mostly AI-generated but edited/merged together by a human
  • This PR was handwritten with AI assistance (spell check, logic suggestions, error resolving)
  • This PR contains only blood, sweat, and coffee (AI-free)

Written by Claude Code, directed and reviewed by the repository owner of the fork.

reville and others added 2 commits August 30, 2026 16:13
The decoded-image cache previously evicted by item count alone and held
every image as f32, so a few large RAWs could pin several gigabytes and
push lower-RAM machines into swap (CyberTimon#515, CyberTimon#521). Cache entries are now
stored at half precision (the GPU pipeline already samples f16, so
rendered output is unchanged) and eviction also enforces a byte budget
derived from physical RAM (total/8, clamped to 256 MB - 4 GB).

Thumbnail and metadata workers now run at Utility QoS on macOS so folder
ingest yields to the UI and other apps instead of saturating performance
cores; this is a no-op on other platforms.

is_image_cached uses a new non-mutating contains() instead of get(), so
a boolean probe no longer reorders LRU eviction or rebuilds pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The f16 round-trip test used a square image, so a transposed width and
height would have passed. Cover a non-square image in both orientations,
assert the restored dimensions, and add an RGBA case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant