Files

128 lines
4.2 KiB
Python

# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
# Copyright (C) 2026 Artificial Sweetener and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Apply global LRU policy to inactive SimpleSyrup quant cache artifacts."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from ..runtime.quant_cache_leases import (
GLOBAL_QUANT_CACHE_LEASES,
QuantCacheLeaseRegistry,
)
from ..runtime.quant_cache_repository import QuantCacheArtifact, QuantCacheRepository
from ..shared.logging import get_logger
LOGGER = get_logger(__name__)
@dataclass(frozen=True)
class QuantCacheStatus:
"""Summarize the global cache for settings and diagnostics."""
path: str
usage_bytes: int
artifact_count: int
active_artifact_count: int
def to_payload(self, limit_bytes: int) -> dict[str, object]:
"""Return the settings-route JSON representation."""
return {
"path": self.path,
"usage_bytes": self.usage_bytes,
"limit_bytes": limit_bytes,
"artifact_count": self.artifact_count,
"active_artifact_count": self.active_artifact_count,
}
@dataclass(frozen=True)
class QuantCacheEvictionResult:
"""Summarize one bounded eviction attempt."""
removed_artifacts: int
removed_bytes: int
remaining_bytes: int
class QuantCacheService:
"""Own global cache accounting, LRU eviction, and safe clearing."""
def __init__(
self,
repository: QuantCacheRepository | None = None,
leases: QuantCacheLeaseRegistry | None = None,
) -> None:
"""Create a cache service with injectable persistence and leases."""
self._repository = repository or QuantCacheRepository()
self._leases = leases or GLOBAL_QUANT_CACHE_LEASES
def status(self) -> QuantCacheStatus:
"""Return current managed artifact usage."""
artifacts = self._repository.list_artifacts()
return QuantCacheStatus(
path=self._repository.relative_display_path(),
usage_bytes=sum(item.manifest.artifact_size_bytes for item in artifacts),
artifact_count=len(artifacts),
active_artifact_count=self._leases.active_count(),
)
def enforce_limit(
self,
limit_bytes: int,
protected_paths: frozenset[Path] = frozenset(),
) -> QuantCacheEvictionResult:
"""Evict least-recently-used inactive artifacts until within the limit."""
if limit_bytes < 0:
raise ValueError("Quant cache limit must not be negative.")
artifacts = list(self._repository.list_artifacts())
remaining = sum(item.manifest.artifact_size_bytes for item in artifacts)
removed_count = 0
removed_bytes = 0
protected = {path.resolve() for path in protected_paths}
for artifact in sorted(artifacts, key=_last_used):
if remaining <= limit_bytes:
break
if artifact.path.resolve() in protected or self._leases.is_leased(
artifact.path
):
continue
if not self._repository.remove(artifact):
continue
removed_count += 1
removed_bytes += artifact.manifest.artifact_size_bytes
remaining -= artifact.manifest.artifact_size_bytes
if removed_count:
LOGGER.info(
"quant cache eviction completed",
extra={
"removed_artifacts": removed_count,
"removed_bytes": removed_bytes,
"remaining_bytes": remaining,
"limit_bytes": limit_bytes,
},
)
return QuantCacheEvictionResult(removed_count, removed_bytes, remaining)
def clear_inactive(self) -> QuantCacheEvictionResult:
"""Remove every inactive managed artifact and preserve loaded models."""
return self.enforce_limit(0)
def _last_used(artifact: QuantCacheArtifact) -> float:
"""Return the manifest timestamp used for deterministic LRU ordering."""
try:
return datetime.fromisoformat(artifact.manifest.last_used_at).timestamp()
except ValueError:
return float("-inf")