This commit is contained in:
Ignacio Serantes
2026-03-28 07:54:59 +01:00
parent d4f3732aa4
commit ff7c1aa373
3 changed files with 143 additions and 43 deletions

View File

@@ -36,7 +36,7 @@ from PySide6.QtGui import QImage, QImageReader, QImageIOHandler
from constants import (
APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CACHE_MAX_RAM_BYTES, CONFIG_DIR,
DISK_CACHE_MAX_BYTES, HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS,
MIN_FREE_RAM_PERCENT, DISK_CACHE_MAX_BYTES, HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS,
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES,
UITexts
)
@@ -644,18 +644,80 @@ class ThumbnailCache(QObject):
self._cache_lock.unlock()
def _ensure_cache_limit(self):
"""Enforces cache size limit by evicting oldest entries.
"""Enforces cache size limit and reacts to system memory pressure.
Must be called with a write lock held."""
# 1. Enforce internal limits using tiered strategy
while len(self._thumbnail_cache) > 0 and (
len(self._thumbnail_cache) >= CACHE_MAX_SIZE or
self._cache_bytes_size > CACHE_MAX_RAM_BYTES):
oldest_path = next(iter(self._thumbnail_cache))
cached_sizes = self._thumbnail_cache.pop(oldest_path)
for img, _ in cached_sizes.values():
self._evict_tiered()
# Check system-wide memory pressure (Low RAM fallback)
try:
import psutil
mem = psutil.virtual_memory()
if (mem.available / mem.total) * 100 < MIN_FREE_RAM_PERCENT:
logger.warning(f"Low system memory detected (< {MIN_FREE_RAM_PERCENT}%). "
"Applying aggressive tiered pruning.")
# Strategy: first clear ALL cached high-res tiers to free space quickly
# while keeping the 128px grid thumbnails intact.
for tier in [512, 256]:
self._prune_tier(tier)
# Re-check if pressure relieved
mem = psutil.virtual_memory()
if (mem.available / mem.total) * 100 >= MIN_FREE_RAM_PERCENT:
return
# If still under pressure, remove oldest 10% of remaining entries
items_to_prune = max(1, len(self._thumbnail_cache) // 10)
for _ in range(items_to_prune):
if not self._thumbnail_cache:
break
self._evict_oldest_path_entirely()
except (ImportError, Exception):
pass
def _evict_tiered(self):
"""
Removes content from cache targeting larger tiers first from the oldest entries.
Must be called with write lock held.
"""
# We check the oldest 100 entries for large tiers to avoid O(N) scans
# on every call while still being very effective for LRU behavior.
check_limit = 100
for tier in [512, 256]:
count = 0
for path, sizes in self._thumbnail_cache.items():
if tier in sizes:
img, _ = sizes.pop(tier)
if img:
self._cache_bytes_size -= img.sizeInBytes()
return
count += 1
if count >= check_limit:
break
self._evict_oldest_path_entirely()
def _prune_tier(self, tier):
"""Removes all thumbnails of a specific tier from cache to free RAM quickly."""
for path, sizes in self._thumbnail_cache.items():
if tier in sizes:
img, _ = sizes.pop(tier)
if img:
self._cache_bytes_size -= img.sizeInBytes()
self._path_to_inode.pop(oldest_path, None)
def _evict_oldest_path_entirely(self):
"""Removes the oldest cache entry completely. Must be called with write lock."""
oldest_path = next(iter(self._thumbnail_cache))
cached_sizes = self._thumbnail_cache.pop(oldest_path)
for img, _ in cached_sizes.values():
if img:
self._cache_bytes_size -= img.sizeInBytes()
self._path_to_inode.pop(oldest_path, None)
def _get_tier_for_size(self, requested_size):
"""Determines the ideal thumbnail tier based on the requested size."""