Skip to content

Release v0.4 — more improvements#6

Merged
AlanRockefeller merged 1 commit intomainfrom
test
Nov 3, 2025
Merged

Release v0.4 — more improvements#6
AlanRockefeller merged 1 commit intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Nov 3, 2025

New Features

  • Two-tier caching system: Implemented a two-tier caching system to prefetch display-sized images, significantly improving performance and reducing GPU memory
    usage.
  • "Preload All Images" feature: Added a new menu option under "Actions" to preload all images in the current directory into the cache, ensuring quick access ev
    en for unviewed images.
  • Progress bar for preloading: Introduced a visual progress bar in the footer to display the status of the "Preload All Images" operation.

Changes

  • Theming improvements: Adjusted the Material theme to ensure the menubar background is black in dark mode, providing a more consistent user experience.
  • Window behavior: Changed the application window to a borderless fullscreen mode, allowing for normal Alt-Tab behavior and better integration with the operati
    ng system.

Summary by CodeRabbit

Release Notes - Version 0.4

  • New Features

    • Two-tier caching system that prefetches display-sized images for improved performance and memory usage
    • Preload All Images action in the Actions menu to cache all images in the current directory
    • Progress bar in footer displaying preload operation status
  • Changes

    • Dark-mode menubar background now properly displays as black
    • Window behavior updated to borderless fullscreen for improved Alt-Tab and OS integration

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 3, 2025

Walkthrough

This PR introduces version 0.4 with a two-tier display-aware caching system, asynchronous preload-all-images feature with progress tracking, display size change notifications, Material theming support for dark/light modes, and borderless fullscreen window mode.

Changes

Cohort / File(s) Summary
Version & Metadata
faststack/ChangeLog.md, faststack/README.md, faststack/pyproject.toml
Version bumped from 0.3 to 0.4; ChangeLog updated with new features (two-tier caching, preload all images, progress bar); README header version updated.
Image Decoding
faststack/faststack/imaging/jpeg.py
Added decode_jpeg_resized() function to decode JPEG bytes and downsample to target display dimensions using LANCZOS resampling, with fallback to full decode if dimensions are zero.
Prefetching Logic
faststack/faststack/imaging/prefetch.py
Extended Prefetcher to accept get_display_info callable; modified submit_task() to fetch display dimensions; updated _decode_and_cache() to accept display width/height/generation, use decode_jpeg_resized(), and include generation in cache keys to prevent stale cache corruption.
Application Controller
faststack/faststack/app.py
Added ProgressReporter signal class; added display state fields (display_width, display_height, display_generation); added preload_all_images() method with background ThreadPoolExecutor; added on_display_size_changed() to invalidate cache and refresh prefetch on display dimension changes; extended cache key generation with display generation suffix; set Material UI style in main(); added executor shutdown in shutdown().
Logging Configuration
faststack/faststack/logging_setup.py
Changed root logger level from INFO to DEBUG; added PIL logging at INFO level.
UI State Management
faststack/faststack/ui/provider.py
Added preloadingStateChanged and preloadProgressChanged signals; added isPreloading and preloadProgress properties with getters/setters; added preloadAllImages() slot to trigger controller preloading; added onDisplaySizeChanged() slot to forward display changes to controller.
QML UI Components
faststack/faststack/qml/Components.qml
Added handlers on Image component (Component.onCompleted, onWidthChanged, onHeightChanged) to call uiState.onDisplaySizeChanged() when display dimensions are known, enabling responsive UI updates.
QML Main UI
faststack/faststack/qml/Main.qml
Added Material theme import with dynamic dark/light mode binding; changed window initialization to windowed mode with explicit positioning; added dynamic theme-driven color properties; added preload progress indicator in footer with ProgressBar (visible when isPreloading is true); added "Preload All Images" action in Actions menu triggering uiState.preloadAllImages().

Sequence Diagram

sequenceDiagram
    actor User
    participant QML as QML UI
    participant UIState as UIState (Provider)
    participant AppCtrl as AppController
    participant Prefetcher
    participant Worker as ThreadPoolExecutor
    participant Cache as Image Cache

    User->>QML: Click "Preload All Images"
    QML->>UIState: preloadAllImages()
    UIState->>AppCtrl: preload_all_images()
    
    rect rgb(200, 220, 255)
    Note over AppCtrl,Worker: Async Preload Phase
    AppCtrl->>Worker: submit image decode tasks
    
    par Background Processing
        Worker->>Prefetcher: _decode_and_cache (for each image)
        Prefetcher->>Prefetcher: get_display_info()
        Prefetcher->>Prefetcher: decode_jpeg_resized(width, height)
        Prefetcher->>Cache: store with generation-aware key
        Worker->>AppCtrl: emit progress_updated signal
    end
    end
    
    AppCtrl->>UIState: _update_preload_progress(progress %)
    UIState->>UIState: setIsPreloading(true), setPreloadProgress(%)
    UIState->>QML: preloadProgressChanged, preloadingStateChanged
    QML->>QML: Update ProgressBar, show indicator
    
    Worker->>AppCtrl: emit finished signal
    AppCtrl->>UIState: _finish_preloading()
    UIState->>UIState: setIsPreloading(false)
    UIState->>QML: preloadingStateChanged
    QML->>QML: Hide indicator, clear progress
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • Display generation tracking logic (app.py and prefetch.py): Verify that generation counters correctly prevent stale prefetch tasks from corrupting the cache when display dimensions change mid-operation
  • ThreadPoolExecutor lifecycle: Ensure preload_executor.shutdown(wait=False) is safe and doesn't leak background tasks on app exit
  • Signal/slot wiring: Validate that ProgressReporter signals properly connect to UI state slots and avoid race conditions in progress updates
  • Cache key format change: Confirm that display-generation suffix in cache keys doesn't inadvertently cause cache misses or bloat under repeated resize scenarios

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Release v0.4 — more improvements" is partially related to the changeset. It accurately identifies the version bump to 0.4 and references improvements, which are indeed present throughout the changes including the new two-tier caching system, preload all images feature, progress bar, theming adjustments, and window behavior updates. However, the term "improvements" is somewhat generic and doesn't specify the main points of the release, such as the caching system or new preload functionality. While not fully summarizing the primary changes, the title does refer to real aspects of the changeset and is not misleading or off-topic.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test

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

🧹 Nitpick comments (6)
faststack/README.md (1)

3-3: Optional: Consider adding comma after year per style guidelines.

Some style guides recommend a comma after the year in month-day-year dates.

Apply this diff if you wish to follow that convention:

-# Version 0.4 - November 2, 2025
+# Version 0.4 - November 2, 2025,
faststack/faststack/logging_setup.py (1)

30-30: Consider making root log level configurable.

Setting the root logger to DEBUG will significantly increase log verbosity. While this is helpful for debugging the new caching and preloading features, consider making the log level configurable (e.g., via environment variable or config file) to allow users to reduce verbosity in production use.

faststack/faststack/qml/Components.qml (1)

18-34: Consider debouncing display size change notifications.

The implementation correctly reports display size changes to the UI state. However, when the window is resized, both onWidthChanged and onHeightChanged handlers will fire separately, potentially causing two rapid calls to onDisplaySizeChanged. While this may be acceptable if the backend handles it efficiently, consider using a Timer to debounce these notifications during rapid resizing.

Example debounced approach:

Timer {
    id: resizeDebounceTimer
    interval: 100
    repeat: false
    onTriggered: {
        if (mainImage.width > 0 && mainImage.height > 0) {
            uiState.onDisplaySizeChanged(mainImage.width, mainImage.height)
        }
    }
}

Component.onCompleted: {
    if (width > 0 && height > 0) {
        uiState.onDisplaySizeChanged(width, height)
    }
}

onWidthChanged: {
    resizeDebounceTimer.restart()
}

onHeightChanged: {
    resizeDebounceTimer.restart()
}
faststack/faststack/imaging/jpeg.py (1)

77-93: Improve exception handling and consider PyTurboJPEG support.

The function works correctly but has a few areas for improvement:

  1. Exception handling: Use logging.exception instead of logging.error to capture the full stack trace, which aids debugging.

  2. PyTurboJPEG support: Unlike decode_jpeg_rgb and decode_jpeg_thumb_rgb, this function only uses Pillow. Consider adding PyTurboJPEG support for consistency and potential performance gains.

Apply this diff to improve exception handling:

     except Exception as e:
-        log.error(f"Pillow failed to decode and resize image: {e}")
+        log.exception(f"Pillow failed to decode and resize image: {e}")
         return None

Consider adding PyTurboJPEG support similar to other decode functions:

def decode_jpeg_resized(
    jpeg_bytes: bytes, width: int, height: int
) -> Optional[np.ndarray]:
    """Decodes and resizes a JPEG to fit within the given dimensions."""
    if width == 0 or height == 0:
        return decode_jpeg_rgb(jpeg_bytes)

    if TURBO_AVAILABLE and jpeg_decoder:
        try:
            # Get dimensions and find appropriate scaling factor
            img_width, img_height, _, _ = jpeg_decoder.decode_header(jpeg_bytes)
            scaling_factor = _get_turbojpeg_scaling_factor(img_width, img_height, max(width, height))
            
            if scaling_factor:
                return jpeg_decoder.decode(
                    jpeg_bytes, 
                    scaling_factor=scaling_factor, 
                    pixel_format=TJPF_RGB, 
                    flags=TJFLAG_FASTDCT
                )
        except Exception as e:
            log.exception(f"PyTurboJPEG failed to decode resized image: {e}. Trying Pillow.")

    # Fallback to Pillow
    try:
        from io import BytesIO
        img = Image.open(BytesIO(jpeg_bytes))
        img.thumbnail((width, height), Image.Resampling.LANCZOS)
        return np.array(img.convert("RGB"))
    except Exception as e:
        log.exception(f"Pillow failed to decode and resize image: {e}")
        return None
faststack/faststack/qml/Main.qml (1)

141-197: Consider cleaning up MenuBar formatting.

The MenuBar section has excessive blank lines and unusual indentation that makes it harder to read. While this doesn't affect functionality, consider normalizing the formatting for better maintainability.

Example cleaned-up version:

MenuBar {
    id: menuBar
    Layout.preferredWidth: 300
    
    background: Rectangle {
        color: root.currentBackgroundColor
    }
    
    palette.buttonText: root.currentTextColor
    palette.button: root.currentBackgroundColor
    palette.window: root.currentBackgroundColor
    palette.text: root.currentTextColor
    
    Menu {
        title: "&File"
        // ... menu items
    }
    // ... other menus
}
faststack/faststack/app.py (1)

78-80: Remove the duplicate get_display_info.

This second definition just overwrites the first one and triggers Ruff F811. Please drop the extra definition and keep a single method.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a667c57 and 6119d92.

📒 Files selected for processing (10)
  • faststack/ChangeLog.md (1 hunks)
  • faststack/README.md (1 hunks)
  • faststack/faststack/app.py (9 hunks)
  • faststack/faststack/imaging/jpeg.py (1 hunks)
  • faststack/faststack/imaging/prefetch.py (4 hunks)
  • faststack/faststack/logging_setup.py (1 hunks)
  • faststack/faststack/qml/Components.qml (1 hunks)
  • faststack/faststack/qml/Main.qml (4 hunks)
  • faststack/faststack/ui/provider.py (2 hunks)
  • faststack/pyproject.toml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
faststack/faststack/ui/provider.py (1)
faststack/faststack/app.py (2)
  • preload_all_images (361-405)
  • on_display_size_changed (81-91)
faststack/faststack/imaging/prefetch.py (3)
faststack/faststack/imaging/jpeg.py (2)
  • decode_jpeg_rgb (22-39)
  • decode_jpeg_resized (77-93)
faststack/faststack/models.py (1)
  • ImageFile (8-12)
faststack/faststack/app.py (2)
  • get_display_info (75-76)
  • get_display_info (78-79)
faststack/faststack/app.py (3)
faststack/faststack/config.py (1)
  • getint (62-63)
faststack/faststack/imaging/prefetch.py (3)
  • update_prefetch (31-51)
  • submit_task (53-64)
  • shutdown (116-120)
faststack/faststack/ui/provider.py (4)
  • isPreloading (70-71)
  • isPreloading (74-77)
  • preloadProgress (80-81)
  • preloadProgress (84-87)
🪛 LanguageTool
faststack/README.md

[style] ~3-~3: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ... FastStack # Version 0.4 - November 2, 2025 # By Alan Rockefeller Ultra-fast, cach...

(MISSING_COMMA_AFTER_YEAR)

🪛 Ruff (0.14.2)
faststack/faststack/imaging/jpeg.py

91-91: Do not catch blind exception: Exception

(BLE001)


92-92: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

faststack/faststack/app.py

78-78: Redefinition of unused get_display_info from line 75

(F811)


375-375: f-string without any placeholders

Remove extraneous f prefix

(F541)


392-392: Unused function argument: future

(ARG001)

🔇 Additional comments (9)
faststack/faststack/logging_setup.py (1)

36-36: Good addition of PIL logging configuration.

Setting PIL to INFO level prevents excessive debug noise while still capturing important information.

faststack/pyproject.toml (1)

8-8: Version bump looks good.

The version update to 0.4 is consistent with the changes documented in the ChangeLog and README.

faststack/ChangeLog.md (1)

3-12: ChangeLog entry is comprehensive and well-structured.

The Version 0.4 entry clearly documents all the new features and changes introduced in this PR.

faststack/faststack/qml/Main.qml (4)

4-4: Material theming support looks good.

The addition of Material theme import and binding correctly implements dark/light mode theming for the UI components.

Also applies to: 18-18


83-96: Preload progress indicator looks good.

The progress bar correctly displays preloading status and progress. The visual design is functional and integrates well with the footer.


251-251: "Preload All Images" action is well-integrated.

The new menu action correctly triggers the preload functionality through the UI state.


12-14: No critical multi-monitor issue found; current behavior is intentional.

The borderless fullscreen implementation (x: 0, y: 0 with Screen.width/Screen.height) is a deliberate design choice documented in the ChangeLog as providing "better integration with the operating system" and "normal Alt-Tab behavior." The application uses only primary screen dimensions, which is standard for most desktop applications. No multi-monitor support is documented as a requirement in the README or project configuration, and the codebase contains no such logic.

If multi-monitor support becomes a user requirement, Screen.virtualX/Y could be considered as an enhancement; however, for the current single-monitor-focused workflow of this JPG viewer, the existing implementation is appropriate.

faststack/faststack/ui/provider.py (2)

60-87: Preloading state management is well-implemented.

The new signals, properties, and backing fields follow Qt best practices:

  • Properties have notify signals
  • Setters check for value changes before emitting
  • Type annotations are correct
  • Consistent with existing patterns in the file

212-218: New slots are correctly implemented.

Both preloadAllImages() and onDisplaySizeChanged() properly delegate to the app controller, maintaining the separation of concerns between UI state and application logic.

Comment on lines +89 to +91
self.image_cache.clear()
self.prefetcher.update_prefetch(self.current_index)
self.sync_ui_state() # To refresh the image
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear stale futures before re-prefetching after display changes.

After self.image_cache.clear() the Prefetcher.futures dict still contains completed entries from the previous display generation. Because update_prefetch() only submits when an index is missing from self.futures, none of those images get re-decoded for the new display size, so navigation becomes synchronous (and slow) until each image is forced. Call self.prefetcher.cancel_all() (or otherwise clear/cancel the futures) before invoking update_prefetch() so the new display size is prefetched immediately.

🤖 Prompt for AI Agents
In faststack/faststack/app.py around lines 89 to 91, after clearing
self.image_cache you must also clear or cancel existing Prefetcher futures so
completed/stale futures from the previous display don't prevent re-decoding;
call self.prefetcher.cancel_all() (or otherwise clear self.prefetcher.futures)
before self.prefetcher.update_prefetch(self.current_index) so the prefetcher
will submit new decode tasks for the current display size, then call
self.sync_ui_state() as before.

@AlanRockefeller
Copy link
Copy Markdown
Owner Author

@coderabbitai What else could be done to improve performance?

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 3, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Examine the cache implementation
fd -t f cache.py --exec cat {}

Length of output: 3481


🏁 Script executed:

#!/bin/bash
# Check the threading configuration and executor setup
rg -n "ThreadPoolExecutor\|executor\s*=" --type=py -C3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the image file structure and I/O patterns
rg -n "class ImageFile\|def read\|open.*rb" --type=py -C3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check memory profiling or optimization attempts
rg -n "memory\|gc\.collect\|del\s+\w+\[" --type=py -C3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the JPEG decoding implementation details
cat faststack/faststack/imaging/jpeg.py

Length of output: 3517


🏁 Script executed:

#!/bin/bash
# Look for ThreadPoolExecutor usage with better pattern
rg -n "ThreadPool\|concurrent\.futures" --type=py -C5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Examine the models/data structures
fd -t f models.py --exec cat {}

Length of output: 1116


🏁 Script executed:

#!/bin/bash
# Look at file I/O patterns
rg -n "\.read\(\)|open\(|Path\(" --type=py faststack/faststack/imaging/ -C2

Length of output: 1596


🏁 Script executed:

#!/bin/bash
# Check prefetch radius and cache size configuration
rg -n "prefetch_radius|max_bytes|cache.*size|CACHE" --type=py -C2

Length of output: 9309


🏁 Script executed:

#!/bin/bash
# Examine app.py threading and executor details
cat faststack/faststack/app.py

Length of output: 19533


@AlanRockefeller: Based on my analysis of the codebase, here are several performance improvements you could implement:

1. Use PyTurboJPEG for Resized Decoding ⚡ (High Impact)

Currently, decode_jpeg_resized only uses Pillow. PyTurboJPEG supports fast scaling during decode, which can be 2-5x faster:

def decode_jpeg_resized(
    jpeg_bytes: bytes, width: int, height: int
) -> Optional[np.ndarray]:
    """Decodes and resizes a JPEG to fit within the given dimensions."""
    if width == 0 or height == 0:
        return decode_jpeg_rgb(jpeg_bytes)

    if TURBO_AVAILABLE and jpeg_decoder:
        try:
            # Get image header to determine dimensions
            img_width, img_height, _, _ = jpeg_decoder.decode_header(jpeg_bytes)
            
            # Calculate best scaling factor for TurboJPEG (supports 1/8, 1/4, 1/2, etc.)
            scale_factor = _get_turbojpeg_scaling_factor(img_width, img_height, max(width, height))
            
            if scale_factor:
                decoded = jpeg_decoder.decode(
                    jpeg_bytes, 
                    scaling_factor=scale_factor,
                    pixel_format=TJPF_RGB, 
                    flags=TJFLAG_FASTDCT
                )
                
                # Only use Pillow for final resize if needed
                if decoded.shape[0] > height or decoded.shape[1] > width:
                    from io import BytesIO
                    img = Image.fromarray(decoded)
                    img.thumbnail((width, height), Image.Resampling.LANCZOS)
                    return np.array(img)
                return decoded
        except Exception as e:
            log.error(f"PyTurboJPEG failed: {e}")
    
    # Fallback to Pillow (existing code)
    try:
        from io import BytesIO
        img = Image.open(BytesIO(jpeg_bytes))
        img.thumbnail((width, height), Image.Resampling.LANCZOS)
        return np.array(img.convert("RGB"))
    except Exception as e:
        log.error(f"Pillow failed to decode and resize image: {e}")
        return None

2. Tune ThreadPool Workers Based on System Resources

The prefetcher uses a fixed max_workers=3. Optimize based on CPU cores:

import os

class Prefetcher:
    def __init__(self, image_files: List[ImageFile], cache_put: Callable, 
                 prefetch_radius: int, get_display_info: Callable):
        # Use CPU count for I/O-bound JPEG decoding
        # Rule of thumb: 2x CPU cores for I/O bound, 1x for CPU bound
        optimal_workers = min(os.cpu_count() * 2, 8)  # Cap at 8
        
        self.executor = ThreadPoolExecutor(
            max_workers=optimal_workers,
            thread_name_prefix="Prefetch"
        )

3. Memory-Mapped File I/O 💾

Replace synchronous file reads with memory-mapped I/O for better performance:

import mmap

def _decode_and_cache(self, image_file: ImageFile, index: int, generation: int,
                     display_width: int, display_height: int, 
                     display_generation: int) -> Optional[tuple[int, int]]:
    try:
        # Memory-mapped file reading (faster than traditional read)
        with open(image_file.path, "rb") as f:
            with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mmapped:
                jpeg_bytes = mmapped[:]
        
        buffer = decode_jpeg_resized(jpeg_bytes, display_width, display_height)
        # ... rest of the method

4. Share Executor for Preloading

The preload uses a separate single-worker executor. Share the prefetch executor instead:

def preload_all_images(self):
    if self.ui_state.isPreloading:
        log.info("Preloading is already in progress.")
        return

    log.info("Starting to preload all images.")
    self.ui_state.isPreloading = True
    self.ui_state.preloadProgress = 0

    # Use existing prefetch executor (better resource utilization)
    total = len(self.image_files)
    completed = 0
    
    def _on_done(future):
        nonlocal completed
        completed += 1
        progress = int((completed / total) * 100)
        self.reporter.progress_updated.emit(progress)
        if completed == total:
            self.reporter.finished.emit()
    
    for i in range(total):
        future = self.prefetcher.submit_task(i, self.prefetcher.generation)
        if future:
            future.add_done_callback(_on_done)

5. Implement Disk Cache for Resized Images 💿

Add a disk cache layer to avoid re-decoding images on application restart:

import hashlib
from pathlib import Path

class DiskCache:
    def __init__(self, cache_dir: Path, max_size_gb: float = 5.0):
        self.cache_dir = cache_dir / ".faststack_cache"
        self.cache_dir.mkdir(exist_ok=True)
        self.max_size = int(max_size_gb * 1024**3)
    
    def get_cache_key(self, filepath: Path, width: int, height: int) -> str:
        """Generate cache key from file path, size, and mtime."""
        stat = filepath.stat()
        key_str = f"{filepath}_{width}x{height}_{stat.st_mtime}_{stat.st_size}"
        return hashlib.sha256(key_str.encode()).hexdigest()[:16]
    
    def get(self, cache_key: str) -> Optional[np.ndarray]:
        cache_file = self.cache_dir / f"{cache_key}.npy"
        if cache_file.exists():
            try:
                return np.load(cache_file)
            except:
                cache_file.unlink()
        return None
    
    def put(self, cache_key: str, data: np.ndarray):
        cache_file = self.cache_dir / f"{cache_key}.npy"
        np.save(cache_file, data)

6. Adaptive Prefetch Radius 🎯

Dynamically adjust prefetch based on navigation speed:

class Prefetcher:
    def __init__(self, ...):
        # ... existing code ...
        self.navigation_history = []
        self.adaptive_radius = prefetch_radius
    
    def update_prefetch(self, current_index: int):
        import time
        now = time.time()
        self.navigation_history.append((current_index, now))
        
        # Keep last 10 navigations
        self.navigation_history = self.navigation_history[-10:]
        
        # Calculate navigation speed
        if len(self.navigation_history) >= 3:
            time_span = now - self.navigation_history[0][1]
            nav_count = len(self.navigation_history) - 1
            
            if time_span > 0:
                speed = nav_count / time_span  # navigations per second
                
                # Adjust radius: faster navigation = larger radius
                if speed > 2:  # More than 2 images per second
                    self.adaptive_radius = min(self.prefetch_radius * 2, 12)
                elif speed < 0.5:  # Less than 0.5 images per second
                    self.adaptive_radius = max(self.prefetch_radius // 2, 2)
                else:
                    self.adaptive_radius = self.prefetch_radius
        
        # Use adaptive_radius instead of prefetch_radius in calculations

7. Use LANCZOS Only When Necessary

Switch to faster resampling for large scale factors:

# In decode_jpeg_resized:
scale_factor_ratio = min(img.width / width, img.height / height)

# Use faster BILINEAR for large downscales, LANCZOS for smaller
if scale_factor_ratio > 4:
    resampling = Image.Resampling.BILINEAR  # Much faster
else:
    resampling = Image.Resampling.LANCZOS  # Higher quality

img.thumbnail((width, height), resampling)

8. Add Performance Profiling

Add timing metrics to identify bottlenecks:

import time
from contextlib import contextmanager

@contextmanager
def timed_operation(operation_name: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        log.debug(f"{operation_name} took {elapsed*1000:.2f}ms")

# Usage in _decode_and_cache:
with timed_operation(f"Decode image {index}"):
    buffer = decode_jpeg_resized(jpeg_bytes, display_width, display_height)

Priority Recommendations:

  1. Implement Version 0.1 #1 (TurboJPEG for resizing) - Immediate 2-5x speedup
  2. Tune Release v0.2 — working Helicon Focus integration and viewer improvements #2 (Thread pool workers) - Better CPU utilization
  3. Add Release v0.6 — more improvements #8 (Profiling) - Measure actual bottlenecks in your environment
  4. Consider Release v0.3 — more improvements #5 (Disk cache) - Faster startup times

Would you like me to help implement any of these optimizations?

🐰✨

@AlanRockefeller AlanRockefeller merged commit 0076329 into main Nov 3, 2025
1 check passed
This was referenced Nov 20, 2025
Merged
Closed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 17, 2026
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