Skip to content

Make thumbnails faster#82

Merged
AlanRockefeller merged 2 commits intomainfrom
pr/faster-thumbnails
Apr 17, 2026
Merged

Make thumbnails faster#82
AlanRockefeller merged 2 commits intomainfrom
pr/faster-thumbnails

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Apr 17, 2026

Summary by CodeRabbit

  • New Features

    • Thumbnail cache size configuration is now publicly available as a configurable constant.
  • Refactor

    • Thumbnail caching system improved by storing Qt image objects instead of bytes for better performance.
    • Import statements reorganized and cleaned up across the codebase.
  • Tests

    • Updated test suite to validate new image-based caching behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Warning

Rate limit exceeded

@AlanRockefeller has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 33 minutes and 43 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 33 minutes and 43 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b3d4157-e4ee-4942-850d-85d8d96537da

📥 Commits

Reviewing files that changed from the base of the PR and between 692b854 and 52e9f99.

📒 Files selected for processing (1)
  • faststack/repro.py

Walkthrough

This pull request refactors the thumbnail caching system from storing raw bytes to storing Qt QImage objects, introducing size-aware cache management. Supporting changes include externalizing cache size constants, updating related test expectations for the new cache format, and minor import/code cleanup across the codebase.

Changes

Cohort / File(s) Summary
Thumbnail Cache Refactoring
faststack/thumbnail_view/prefetcher.py, faststack/thumbnail_view/provider.py, faststack/tests/thumbnail_view/test_prefetcher.py, faststack/tests/thumbnail_view/test_provider_logic.py
Replaced byte-based cache storage with QImage objects. Added DEFAULT_THUMBNAIL_CACHE_BYTES constant, size-aware LRU eviction via _thumbnail_cache_item_size(), and Qt conversion utilities (_rgb_to_qimage()). Updated ThumbnailCache to track computed value_size per entry and changed cache getter/setter signatures from bytes to generic object. Added _cached_to_image() helper to ThumbnailProvider for unified QImage/bytes handling. Test updates verify QImage materialization and pixel buffer footprint accounting.
Configuration & Constants
faststack/app.py, faststack/thumbnail_view/__init__.py
Extracted hardcoded thumbnail cache byte limit into DEFAULT_THUMBNAIL_CACHE_BYTES constant and exported it from the thumbnail_view module. Updated app.py to reference the shared constant instead of inline magic number.
Import & Code Cleanup
faststack/imaging/editor.py, faststack/repro.py, faststack/tests/test_editor_reopening.py, faststack/tests/test_mask.py
Reorganized and cleaned up module-level imports: removed duplicate/commented lines, moved top-level imports before conditional usage, and eliminated unused local variables and redundant import statements. No functional behavior changes.
Test Expectations Update
faststack/tests/thumbnail_view/test_folder_stats.py
Changed coverage_buckets test assertions from 3-tuple to 4-tuple structure (e.g., (uploaded, todo, stacked, raw) instead of 3-element tuples), reflecting an expanded coverage tracking dimension.

Sequence Diagram

sequenceDiagram
    participant Prefetcher as ThumbnailPrefetcher
    participant Cache as ThumbnailCache
    participant Provider as ThumbnailProvider
    participant UI as Qt UI

    Note over Prefetcher,UI: Image Decode & Cache Flow (New QImage-based)
    
    Prefetcher->>Prefetcher: _decode_image(path) → QImage
    Prefetcher->>Prefetcher: _rgb_to_qimage(array) → QImage
    Prefetcher->>Cache: put(key, qimage_value)
    
    activate Cache
    Cache->>Cache: _thumbnail_cache_item_size(qimage) → bytes_used
    Cache->>Cache: Track (value, size) in LRU
    Cache->>Cache: Evict entries if exceeds max_bytes
    deactivate Cache
    
    Note over Provider: Cache Retrieval & Rendering
    
    Provider->>Cache: get(key)
    Cache-->>Provider: Optional[QImage]
    
    alt Cache Hit
        Provider->>Provider: _cached_to_image(qimage) → QImage
        Provider->>UI: Return QImage to renderer
    else Cache Miss
        Provider->>Prefetcher: submit(path)
        Provider->>UI: Return placeholder while decoding
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Release v0.4 — more improvements #6: Makes directly related code-level changes to the thumbnail caching subsystem components (ThumbnailCache, ThumbnailPrefetcher, ThumbnailProvider) and the DEFAULT_THUMBNAIL_CACHE_BYTES constant.
  • Version 1.5.6 #41: Modifies overlapping thumbnail_view package files (prefetcher.py, provider.py) and app.py cache initialization logic.
  • Fix crop bug #71: Test expectations in this PR's coverage_buckets assertions directly align with changes to FolderStats.coverage_buckets (adding a fourth tuple element).
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Make thumbnails faster' is vague and generic—it describes a performance goal but lacks specificity about what changed or how. The actual changes involve refactoring thumbnail caching from JPEG bytes to Qt QImage objects and adjusting cache semantics, which is not conveyed by this title. Consider using a more specific title that reflects the core technical change, such as 'Refactor thumbnail cache to store QImage objects instead of JPEG bytes' or 'Replace JPEG byte caching with Qt QImage objects in thumbnail system'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 84.85% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr/faster-thumbnails

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
faststack/thumbnail_view/provider.py (1)

268-272: ⚠️ Potential issue | 🟡 Minor

Stale comment: most cache entries are QImage, not bytes.

After this PR, the common "bad entry" path here is an unsupported cached type (via _cached_to_image returning None with a warning) or a QImage that turned out to be null — not a failed bytes decode. Consider rewording so the rationale stays accurate:

✏️ Suggested rewording
-                # Decode of cached bytes failed — evict the bad entry so
-                # the prefetcher can re-decode on the next request.
+                # Cached entry could not be materialized into a usable QImage
+                # (null QImage or unsupported payload) — evict so the prefetcher
+                # can re-decode on the next request.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/thumbnail_view/provider.py` around lines 268 - 272, The comment
beside the cache-eviction branch is stale — the failure path is typically an
unsupported cached type (when _cached_to_image(...) returns None with a warning)
or a null QImage, not a bytes decode error; update the comment near the
discard(cache_key) call in provider.py (the block that logs "Evicted bad cache
entry: %s") to describe evicting entries that are unsupported types or null
QImage instances so the rationale matches _cached_to_image and the actual cache
contents.
🧹 Nitpick comments (3)
faststack/thumbnail_view/prefetcher.py (2)

48-52: Minor robustness: len(value) fallback can raise for non-sized payloads.

ThumbnailCache is typed as Dict[str, Tuple[object, int]] and put() calls self._size_of(value) unconditionally. If anything other than a QImage or a sized bytes-like lands here (e.g., a numpy array is fine via len, but an arbitrary object or None isn't), put will raise TypeError and abort the insert. A small guard keeps the cache well-behaved under unexpected inputs:

🛡️ Suggested guard
 def _thumbnail_cache_item_size(value: object) -> int:
     """Return the memory footprint we want to charge against the cache."""
     if QImage is not None and isinstance(value, QImage):
         return value.bytesPerLine() * value.height()
-    return len(value)
+    try:
+        return len(value)  # bytes / bytearray / memoryview / ndarray
+    except TypeError:
+        # Unknown payload — don't blow up the cache; just don't charge it.
+        return 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/thumbnail_view/prefetcher.py` around lines 48 - 52, The
_thumbnail_cache_item_size function can raise TypeError for objects without a
length; update it to defensively compute size: if value is a QImage use
bytesPerLine()*height(), otherwise attempt to call len(value) inside a
try/except (or check hasattr(value, "__len__")) and return that length, and fall
back to a safe default (e.g., 0) when neither works so ThumbnailCache.put()
won't raise on unexpected payloads; change only the body of
_thumbnail_cache_item_size to implement this guard.

25-25: Operational note: 768 MiB default cache budget.

A decoded 200×200 RGB888 thumbnail is ~120 KB, so this budget is plausible for ~5–6k items at target size — but on low-RAM machines (or when target_size is larger, e.g., high-DPI) this can dominate resident memory. Two suggestions to de-risk:

  • Consider exposing this via config (or an env var) so it can be tuned without a code change.
  • Emit a one-shot log.info at ThumbnailCache construction reporting max_bytes/max_items so field reports can correlate memory pressure with the chosen budget.

Not blocking — just want to make sure this is deliberate and observable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/thumbnail_view/prefetcher.py` at line 25, Replace the hardcoded
DEFAULT_THUMBNAIL_CACHE_BYTES with a configurable value (read from a config
object or environment variable) and fall back to the current 768 * 1024 * 1024
if not provided; update any code that references DEFAULT_THUMBNAIL_CACHE_BYTES
so it reads the configurable setting (e.g., where ThumbnailCache is constructed
and where target_size is used for sizing). Also add a single log.info call in
the ThumbnailCache constructor that emits the chosen max_bytes and max_items
(and include target_size/scale if available) so field reports can correlate
memory pressure with the budget; ensure the log is one-shot at construction
time.
faststack/tests/thumbnail_view/test_prefetcher.py (1)

234-244: Optional: seed with a QImage to reflect the new cache contract.

test_submit_skips_if_cached still pre-populates the cache with b"cached_data". It still exercises the "skip if cached" code path (the prefetcher only checks is not None), but it no longer mirrors what actually lives in the cache after the refactor. Using a QImage would keep this test aligned with the new semantics you just asserted in test_qimage_size_accounting.

♻️ Suggested tweak
-        # Pre-populate cache
-        cache.put(cache_key, b"cached_data")
+        # Pre-populate cache
+        cache.put(cache_key, QImage(10, 10, QImage.Format.Format_RGB888))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/tests/thumbnail_view/test_prefetcher.py` around lines 234 - 244,
Replace the cached payload in test_submit_skips_if_cached with a QImage instance
to match the new cache contract: compute the same cache_key (using
compute_path_hash(test_image) and mtime_ns), create a QImage sized/filled
similarly to what's stored by the prefetcher, and call cache.put(cache_key,
qimage) so prefetcher.submit(test_image, mtime_ns) sees a QImage in the cache
and returns False; update references in the test to use QImage instead of
b"cached_data".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@faststack/repro.py`:
- Line 12: Remove the leftover debug print in repro.py: delete the statement
print(f"DEBUG: sys.path[-1] is {sys.path[-1]}") or, if you want to keep it
temporarily, prefix it with a clear "# DEBUG" comment so it is obviously an
investigative artifact; ensure no other debug-only prints remain in the module
and run tests to confirm no side effects from removing the print.

---

Outside diff comments:
In `@faststack/thumbnail_view/provider.py`:
- Around line 268-272: The comment beside the cache-eviction branch is stale —
the failure path is typically an unsupported cached type (when
_cached_to_image(...) returns None with a warning) or a null QImage, not a bytes
decode error; update the comment near the discard(cache_key) call in provider.py
(the block that logs "Evicted bad cache entry: %s") to describe evicting entries
that are unsupported types or null QImage instances so the rationale matches
_cached_to_image and the actual cache contents.

---

Nitpick comments:
In `@faststack/tests/thumbnail_view/test_prefetcher.py`:
- Around line 234-244: Replace the cached payload in test_submit_skips_if_cached
with a QImage instance to match the new cache contract: compute the same
cache_key (using compute_path_hash(test_image) and mtime_ns), create a QImage
sized/filled similarly to what's stored by the prefetcher, and call
cache.put(cache_key, qimage) so prefetcher.submit(test_image, mtime_ns) sees a
QImage in the cache and returns False; update references in the test to use
QImage instead of b"cached_data".

In `@faststack/thumbnail_view/prefetcher.py`:
- Around line 48-52: The _thumbnail_cache_item_size function can raise TypeError
for objects without a length; update it to defensively compute size: if value is
a QImage use bytesPerLine()*height(), otherwise attempt to call len(value)
inside a try/except (or check hasattr(value, "__len__")) and return that length,
and fall back to a safe default (e.g., 0) when neither works so
ThumbnailCache.put() won't raise on unexpected payloads; change only the body of
_thumbnail_cache_item_size to implement this guard.
- Line 25: Replace the hardcoded DEFAULT_THUMBNAIL_CACHE_BYTES with a
configurable value (read from a config object or environment variable) and fall
back to the current 768 * 1024 * 1024 if not provided; update any code that
references DEFAULT_THUMBNAIL_CACHE_BYTES so it reads the configurable setting
(e.g., where ThumbnailCache is constructed and where target_size is used for
sizing). Also add a single log.info call in the ThumbnailCache constructor that
emits the chosen max_bytes and max_items (and include target_size/scale if
available) so field reports can correlate memory pressure with the budget;
ensure the log is one-shot at construction time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 376dfdbd-1bc3-48f7-90d1-25fc8be14516

📥 Commits

Reviewing files that changed from the base of the PR and between ef2a6fc and 692b854.

📒 Files selected for processing (11)
  • faststack/app.py
  • faststack/imaging/editor.py
  • faststack/repro.py
  • faststack/tests/test_editor_reopening.py
  • faststack/tests/test_mask.py
  • faststack/tests/thumbnail_view/test_folder_stats.py
  • faststack/tests/thumbnail_view/test_prefetcher.py
  • faststack/tests/thumbnail_view/test_provider_logic.py
  • faststack/thumbnail_view/__init__.py
  • faststack/thumbnail_view/prefetcher.py
  • faststack/thumbnail_view/provider.py

Comment thread faststack/repro.py Outdated
@AlanRockefeller AlanRockefeller merged commit 2e930c1 into main Apr 17, 2026
3 checks passed
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