Source code for simvx.core.assets.cache
"""Byte-budget LRU cache used by typed loaders.
Keys are ``(uri, version)`` so the same URI with a new version (etag,
mtime) becomes a fresh entry; consumers don't need explicit invalidation
to pick up updated content.
"""
from __future__ import annotations
import sys
from collections import OrderedDict
from typing import Any
[docs]
class LRUCache:
"""Simple byte-budget LRU.
Estimates per-entry size via ``sys.getsizeof`` for bytes-like values
and falls back to a constant for arbitrary objects. Loaders that
cache structured assets (textures, sounds) should pass ``size_hint``
so eviction tracks GPU/audio memory rather than the Python wrapper.
"""
def __init__(self, max_bytes: int) -> None:
if max_bytes <= 0:
raise ValueError(f"max_bytes must be > 0, got {max_bytes}")
self.max_bytes = max_bytes
self._items: OrderedDict[tuple[str, str | None], tuple[Any, int]] = OrderedDict()
self._used = 0
[docs]
def __len__(self) -> int:
return len(self._items)
[docs]
@property
def used_bytes(self) -> int:
return self._used
[docs]
def get(self, uri: str, version: str | None) -> Any | None:
key = (uri, version)
if key not in self._items:
return None
value, _ = self._items[key]
self._items.move_to_end(key)
return value
[docs]
def put(self, uri: str, version: str | None, value: Any, size_hint: int | None = None) -> None:
key = (uri, version)
if key in self._items:
old_value, old_size = self._items.pop(key)
self._used -= old_size
size = size_hint if size_hint is not None else _estimate_size(value)
if size > self.max_bytes:
return # value too big to cache; skip silently rather than evict everything
self._items[key] = (value, size)
self._used += size
while self._used > self.max_bytes and self._items:
_, (_, evicted_size) = self._items.popitem(last=False)
self._used -= evicted_size
[docs]
def invalidate(self, uri: str, version: str | None = None) -> None:
"""Drop one entry. If ``version`` is None, drop every version of ``uri``."""
if version is not None:
self._items.pop((uri, version), None)
return
for key in [k for k in self._items if k[0] == uri]:
_, size = self._items.pop(key)
self._used -= size
[docs]
def clear(self) -> None:
self._items.clear()
self._used = 0
def _estimate_size(value: Any) -> int:
if isinstance(value, (bytes, bytearray, memoryview)):
return len(value)
if hasattr(value, "nbytes"): # numpy / similar
try:
return int(value.nbytes)
except (AttributeError, TypeError):
pass
return sys.getsizeof(value)