v0.9.15
This commit is contained in:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user