Spaces:
Running
Running
Commit ·
4bb6afa
1
Parent(s): 80fb861
Add model caching infrastructure (#35)
Browse filesImplemented singleton model cache with LRU eviction:
1. backend/core/model_cache.py:
- Singleton pattern for global cache
- Thread-safe operations
- LRU eviction policy
- Memory limit enforcement
- Cache statistics tracking
2. backend/config/cache_config.py:
- Configurable cache limits
- Model size estimates
- Enable/disable toggle
3. backend/tests/test_model_cache.py:
- 7 comprehensive tests
- Tests singleton, LRU, memory limits
- Statistics validation
Features:
- Max 10 models cached
- Max 8GB memory usage
- Automatic LRU eviction
- Thread-safe access
- Real-time statistics
Part 1 of 2 - Next: Integrate with detectors
Part of #35
- backend/config/__init__.py +0 -0
- backend/config/cache_config.py +36 -0
- backend/core/model_cache.py +203 -0
- backend/tests/test_model_cache.py +150 -0
backend/config/__init__.py
ADDED
|
File without changes
|
backend/config/cache_config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cache configuration for VeriFile-X model caching.
|
| 3 |
+
"""
|
| 4 |
+
from typing import Dict, Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class CacheConfig:
|
| 8 |
+
"""Model cache configuration."""
|
| 9 |
+
|
| 10 |
+
# Maximum number of models to cache
|
| 11 |
+
MAX_MODELS = 10
|
| 12 |
+
|
| 13 |
+
# Maximum memory usage in MB
|
| 14 |
+
MAX_MEMORY_MB = 8000 # 8GB
|
| 15 |
+
|
| 16 |
+
# Enable/disable caching
|
| 17 |
+
ENABLE_CACHE = True
|
| 18 |
+
|
| 19 |
+
# Cache eviction policy
|
| 20 |
+
EVICTION_POLICY = "lru" # least-recently-used
|
| 21 |
+
|
| 22 |
+
# Model size estimates (in MB)
|
| 23 |
+
MODEL_SIZES = {
|
| 24 |
+
'stable-diffusion-2-1': 4000,
|
| 25 |
+
'clip-vit-b-32': 350,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
@classmethod
|
| 29 |
+
def to_dict(cls) -> Dict[str, Any]:
|
| 30 |
+
"""Convert config to dictionary."""
|
| 31 |
+
return {
|
| 32 |
+
'max_models': cls.MAX_MODELS,
|
| 33 |
+
'max_memory_mb': cls.MAX_MEMORY_MB,
|
| 34 |
+
'enable_cache': cls.ENABLE_CACHE,
|
| 35 |
+
'eviction_policy': cls.EVICTION_POLICY,
|
| 36 |
+
}
|
backend/core/model_cache.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Singleton model cache manager for VeriFile-X.
|
| 3 |
+
|
| 4 |
+
Provides thread-safe model caching with LRU eviction to improve
|
| 5 |
+
performance by avoiding repeated model loading.
|
| 6 |
+
"""
|
| 7 |
+
import threading
|
| 8 |
+
from typing import Any, Dict, Optional, OrderedDict
|
| 9 |
+
from collections import OrderedDict as OrderedDictType
|
| 10 |
+
import time
|
| 11 |
+
from backend.core.logger import setup_logger
|
| 12 |
+
|
| 13 |
+
logger = setup_logger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ModelCache:
|
| 17 |
+
"""Singleton model cache with LRU eviction and memory limits."""
|
| 18 |
+
|
| 19 |
+
_instance = None
|
| 20 |
+
_lock = threading.Lock()
|
| 21 |
+
|
| 22 |
+
def __new__(cls):
|
| 23 |
+
"""Ensure only one instance exists (singleton pattern)."""
|
| 24 |
+
if cls._instance is None:
|
| 25 |
+
with cls._lock:
|
| 26 |
+
if cls._instance is None:
|
| 27 |
+
cls._instance = super().__new__(cls)
|
| 28 |
+
cls._instance._initialized = False
|
| 29 |
+
return cls._instance
|
| 30 |
+
|
| 31 |
+
def __init__(self):
|
| 32 |
+
"""Initialize cache on first creation."""
|
| 33 |
+
if self._initialized:
|
| 34 |
+
return
|
| 35 |
+
|
| 36 |
+
self._cache: OrderedDictType[str, Dict[str, Any]] = OrderedDict()
|
| 37 |
+
self._lock = threading.Lock()
|
| 38 |
+
self._stats = {
|
| 39 |
+
'hits': 0,
|
| 40 |
+
'misses': 0,
|
| 41 |
+
'evictions': 0,
|
| 42 |
+
'total_memory_mb': 0.0
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
# Configuration (can be overridden)
|
| 46 |
+
self.max_models = 10
|
| 47 |
+
self.max_memory_mb = 8000 # 8GB
|
| 48 |
+
self.enable_cache = True
|
| 49 |
+
|
| 50 |
+
self._initialized = True
|
| 51 |
+
logger.info("ModelCache initialized")
|
| 52 |
+
|
| 53 |
+
def get(self, key: str) -> Optional[Any]:
|
| 54 |
+
"""
|
| 55 |
+
Get model from cache.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
key: Model identifier (e.g., 'stable-diffusion-2-1')
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Cached model or None if not found
|
| 62 |
+
"""
|
| 63 |
+
if not self.enable_cache:
|
| 64 |
+
return None
|
| 65 |
+
|
| 66 |
+
with self._lock:
|
| 67 |
+
if key in self._cache:
|
| 68 |
+
# Move to end (most recently used)
|
| 69 |
+
self._cache.move_to_end(key)
|
| 70 |
+
self._stats['hits'] += 1
|
| 71 |
+
|
| 72 |
+
model_info = self._cache[key]
|
| 73 |
+
logger.debug(
|
| 74 |
+
f"Cache HIT: {key} "
|
| 75 |
+
f"(size: {model_info['size_mb']:.1f}MB, "
|
| 76 |
+
f"age: {time.time() - model_info['timestamp']:.1f}s)"
|
| 77 |
+
)
|
| 78 |
+
return model_info['model']
|
| 79 |
+
else:
|
| 80 |
+
self._stats['misses'] += 1
|
| 81 |
+
logger.debug(f"Cache MISS: {key}")
|
| 82 |
+
return None
|
| 83 |
+
|
| 84 |
+
def set(self, key: str, model: Any, size_mb: float):
|
| 85 |
+
"""
|
| 86 |
+
Store model in cache.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
key: Model identifier
|
| 90 |
+
model: Model object to cache
|
| 91 |
+
size_mb: Estimated memory size in MB
|
| 92 |
+
"""
|
| 93 |
+
if not self.enable_cache:
|
| 94 |
+
return
|
| 95 |
+
|
| 96 |
+
with self._lock:
|
| 97 |
+
# Check if we need to evict
|
| 98 |
+
while (len(self._cache) >= self.max_models or
|
| 99 |
+
self._stats['total_memory_mb'] + size_mb > self.max_memory_mb):
|
| 100 |
+
if not self._cache:
|
| 101 |
+
logger.warning("Cannot cache: size exceeds max_memory_mb")
|
| 102 |
+
return
|
| 103 |
+
self._evict_lru()
|
| 104 |
+
|
| 105 |
+
# Store model
|
| 106 |
+
self._cache[key] = {
|
| 107 |
+
'model': model,
|
| 108 |
+
'size_mb': size_mb,
|
| 109 |
+
'timestamp': time.time()
|
| 110 |
+
}
|
| 111 |
+
self._cache.move_to_end(key)
|
| 112 |
+
self._stats['total_memory_mb'] += size_mb
|
| 113 |
+
|
| 114 |
+
logger.info(
|
| 115 |
+
f"Cached model: {key} "
|
| 116 |
+
f"(size: {size_mb:.1f}MB, "
|
| 117 |
+
f"total: {self._stats['total_memory_mb']:.1f}MB)"
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
def _evict_lru(self):
|
| 121 |
+
"""Evict least recently used model."""
|
| 122 |
+
if not self._cache:
|
| 123 |
+
return
|
| 124 |
+
|
| 125 |
+
# Remove first item (least recently used)
|
| 126 |
+
key, model_info = self._cache.popitem(last=False)
|
| 127 |
+
self._stats['total_memory_mb'] -= model_info['size_mb']
|
| 128 |
+
self._stats['evictions'] += 1
|
| 129 |
+
|
| 130 |
+
logger.info(
|
| 131 |
+
f"Evicted LRU model: {key} "
|
| 132 |
+
f"(freed: {model_info['size_mb']:.1f}MB)"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Clean up model if it has cleanup method
|
| 136 |
+
if hasattr(model_info['model'], 'cleanup'):
|
| 137 |
+
try:
|
| 138 |
+
model_info['model'].cleanup()
|
| 139 |
+
except Exception as e:
|
| 140 |
+
logger.warning(f"Error cleaning up {key}: {e}")
|
| 141 |
+
|
| 142 |
+
def clear(self):
|
| 143 |
+
"""Clear all cached models."""
|
| 144 |
+
with self._lock:
|
| 145 |
+
count = len(self._cache)
|
| 146 |
+
memory_freed = self._stats['total_memory_mb']
|
| 147 |
+
|
| 148 |
+
# Clean up all models
|
| 149 |
+
for key, model_info in self._cache.items():
|
| 150 |
+
if hasattr(model_info['model'], 'cleanup'):
|
| 151 |
+
try:
|
| 152 |
+
model_info['model'].cleanup()
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logger.warning(f"Error cleaning up {key}: {e}")
|
| 155 |
+
|
| 156 |
+
self._cache.clear()
|
| 157 |
+
self._stats['total_memory_mb'] = 0.0
|
| 158 |
+
|
| 159 |
+
logger.info(
|
| 160 |
+
f"Cleared cache: {count} models, "
|
| 161 |
+
f"{memory_freed:.1f}MB freed"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def stats(self) -> Dict[str, Any]:
|
| 165 |
+
"""
|
| 166 |
+
Get cache statistics.
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
Dictionary with cache metrics
|
| 170 |
+
"""
|
| 171 |
+
with self._lock:
|
| 172 |
+
total_requests = self._stats['hits'] + self._stats['misses']
|
| 173 |
+
hit_rate = (self._stats['hits'] / total_requests
|
| 174 |
+
if total_requests > 0 else 0.0)
|
| 175 |
+
|
| 176 |
+
return {
|
| 177 |
+
'total_models': len(self._cache),
|
| 178 |
+
'memory_mb': self._stats['total_memory_mb'],
|
| 179 |
+
'max_memory_mb': self.max_memory_mb,
|
| 180 |
+
'cache_hits': self._stats['hits'],
|
| 181 |
+
'cache_misses': self._stats['misses'],
|
| 182 |
+
'hit_rate': hit_rate,
|
| 183 |
+
'evictions': self._stats['evictions'],
|
| 184 |
+
'enabled': self.enable_cache,
|
| 185 |
+
'models': list(self._cache.keys())
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
def reset_stats(self):
|
| 189 |
+
"""Reset statistics counters."""
|
| 190 |
+
with self._lock:
|
| 191 |
+
self._stats['hits'] = 0
|
| 192 |
+
self._stats['misses'] = 0
|
| 193 |
+
self._stats['evictions'] = 0
|
| 194 |
+
logger.info("Cache statistics reset")
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# Global cache instance
|
| 198 |
+
_cache = ModelCache()
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def get_model_cache() -> ModelCache:
|
| 202 |
+
"""Get the global model cache instance."""
|
| 203 |
+
return _cache
|
backend/tests/test_model_cache.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests for model cache system.
|
| 3 |
+
"""
|
| 4 |
+
import pytest
|
| 5 |
+
import time
|
| 6 |
+
from backend.core.model_cache import ModelCache, get_model_cache
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_cache_singleton():
|
| 10 |
+
"""Test that ModelCache is a singleton."""
|
| 11 |
+
cache1 = ModelCache()
|
| 12 |
+
cache2 = ModelCache()
|
| 13 |
+
cache3 = get_model_cache()
|
| 14 |
+
|
| 15 |
+
assert cache1 is cache2
|
| 16 |
+
assert cache2 is cache3
|
| 17 |
+
|
| 18 |
+
# All should reference the same instance
|
| 19 |
+
cache1.set('test', 'value', 1.0)
|
| 20 |
+
assert cache2.get('test') == 'value'
|
| 21 |
+
assert cache3.get('test') == 'value'
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_cache_basic_operations():
|
| 25 |
+
"""Test basic cache get/set operations."""
|
| 26 |
+
cache = ModelCache()
|
| 27 |
+
cache.clear()
|
| 28 |
+
|
| 29 |
+
# Cache miss
|
| 30 |
+
assert cache.get('model1') is None
|
| 31 |
+
|
| 32 |
+
# Cache set
|
| 33 |
+
cache.set('model1', {'data': 'test'}, 100.0)
|
| 34 |
+
|
| 35 |
+
# Cache hit
|
| 36 |
+
result = cache.get('model1')
|
| 37 |
+
assert result == {'data': 'test'}
|
| 38 |
+
|
| 39 |
+
# Statistics
|
| 40 |
+
stats = cache.stats()
|
| 41 |
+
assert stats['total_models'] == 1
|
| 42 |
+
assert stats['memory_mb'] == 100.0
|
| 43 |
+
assert stats['cache_hits'] >= 1
|
| 44 |
+
assert stats['cache_misses'] >= 1
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_cache_lru_eviction():
|
| 48 |
+
"""Test LRU eviction when cache is full."""
|
| 49 |
+
cache = ModelCache()
|
| 50 |
+
cache.clear()
|
| 51 |
+
cache.max_models = 3 # Small limit for testing
|
| 52 |
+
|
| 53 |
+
# Add 3 models
|
| 54 |
+
cache.set('model1', 'data1', 100.0)
|
| 55 |
+
cache.set('model2', 'data2', 100.0)
|
| 56 |
+
cache.set('model3', 'data3', 100.0)
|
| 57 |
+
|
| 58 |
+
assert cache.stats()['total_models'] == 3
|
| 59 |
+
|
| 60 |
+
# Access model1 to make it recently used
|
| 61 |
+
cache.get('model1')
|
| 62 |
+
|
| 63 |
+
# Add 4th model - should evict model2 (least recently used)
|
| 64 |
+
cache.set('model4', 'data4', 100.0)
|
| 65 |
+
|
| 66 |
+
assert cache.stats()['total_models'] == 3
|
| 67 |
+
assert cache.get('model1') is not None # Still there (recently used)
|
| 68 |
+
assert cache.get('model2') is None # Evicted
|
| 69 |
+
assert cache.get('model3') is not None # Still there
|
| 70 |
+
assert cache.get('model4') is not None # Newly added
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_cache_memory_limit():
|
| 74 |
+
"""Test memory limit enforcement."""
|
| 75 |
+
cache = ModelCache()
|
| 76 |
+
cache.clear()
|
| 77 |
+
cache.max_memory_mb = 500 # 500MB limit
|
| 78 |
+
cache.max_models = 10
|
| 79 |
+
|
| 80 |
+
# Add models totaling 300MB
|
| 81 |
+
cache.set('model1', 'data1', 100.0)
|
| 82 |
+
cache.set('model2', 'data2', 100.0)
|
| 83 |
+
cache.set('model3', 'data3', 100.0)
|
| 84 |
+
|
| 85 |
+
assert cache.stats()['memory_mb'] == 300.0
|
| 86 |
+
|
| 87 |
+
# Try to add 300MB more (total would be 600MB > 500MB limit)
|
| 88 |
+
cache.set('model4', 'data4', 300.0)
|
| 89 |
+
|
| 90 |
+
# Should have evicted models to stay under limit
|
| 91 |
+
stats = cache.stats()
|
| 92 |
+
assert stats['memory_mb'] <= 500.0
|
| 93 |
+
assert stats['evictions'] >= 1
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def test_cache_clear():
|
| 97 |
+
"""Test cache clearing."""
|
| 98 |
+
cache = ModelCache()
|
| 99 |
+
|
| 100 |
+
cache.set('model1', 'data1', 100.0)
|
| 101 |
+
cache.set('model2', 'data2', 100.0)
|
| 102 |
+
|
| 103 |
+
assert cache.stats()['total_models'] == 2
|
| 104 |
+
|
| 105 |
+
cache.clear()
|
| 106 |
+
|
| 107 |
+
stats = cache.stats()
|
| 108 |
+
assert stats['total_models'] == 0
|
| 109 |
+
assert stats['memory_mb'] == 0.0
|
| 110 |
+
assert cache.get('model1') is None
|
| 111 |
+
assert cache.get('model2') is None
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def test_cache_statistics():
|
| 115 |
+
"""Test cache statistics tracking."""
|
| 116 |
+
cache = ModelCache()
|
| 117 |
+
cache.clear()
|
| 118 |
+
cache.reset_stats()
|
| 119 |
+
|
| 120 |
+
# Generate some cache activity
|
| 121 |
+
cache.get('missing') # miss
|
| 122 |
+
cache.set('model1', 'data', 100.0)
|
| 123 |
+
cache.get('model1') # hit
|
| 124 |
+
cache.get('model1') # hit
|
| 125 |
+
cache.get('missing2') # miss
|
| 126 |
+
|
| 127 |
+
stats = cache.stats()
|
| 128 |
+
|
| 129 |
+
assert stats['cache_hits'] >= 2
|
| 130 |
+
assert stats['cache_misses'] >= 2
|
| 131 |
+
assert stats['hit_rate'] > 0
|
| 132 |
+
assert 'models' in stats
|
| 133 |
+
assert 'model1' in stats['models']
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def test_cache_disabled():
|
| 137 |
+
"""Test cache when disabled."""
|
| 138 |
+
cache = ModelCache()
|
| 139 |
+
cache.enable_cache = False
|
| 140 |
+
|
| 141 |
+
# Set should not cache
|
| 142 |
+
cache.set('model1', 'data1', 100.0)
|
| 143 |
+
|
| 144 |
+
# Get should return None
|
| 145 |
+
assert cache.get('model1') is None
|
| 146 |
+
|
| 147 |
+
# Re-enable
|
| 148 |
+
cache.enable_cache = True
|
| 149 |
+
cache.set('model2', 'data2', 100.0)
|
| 150 |
+
assert cache.get('model2') == 'data2'
|