Coverage for agentos/tools/memory_optimizer.py: 0%

213 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 21:19 +0800

1""" 

2Memory Optimization Tools for AgentOS. 

3Object pooling, LRU caching, memory monitoring, and smart caching with TTL. 

4""" 

5 

6import threading 

7import time 

8from collections import OrderedDict 

9from collections.abc import Callable 

10from dataclasses import dataclass 

11from typing import Any, Generic, TypeVar 

12 

13T = TypeVar("T") 

14 

15 

16# ============================================================================ 

17# ObjectPool 

18# ============================================================================ 

19 

20 

21class ObjectPool(Generic[T]): 

22 """Thread-safe object pool with auto-expiry and size limits. 

23 

24 Reuses pre-allocated objects instead of creating/destroying them repeatedly. 

25 """ 

26 

27 def __init__( 

28 self, 

29 factory: Callable[[], T], 

30 max_size: int = 100, 

31 max_idle: int = 30, 

32 idle_timeout: float = 300.0, 

33 ): 

34 self._factory = factory 

35 self._max_size = max_size 

36 self._max_idle = max_idle 

37 self._idle_timeout = idle_timeout 

38 self._pool: list[_PooledItem[T]] = [] 

39 self._lock = threading.Lock() 

40 self._created: int = 0 

41 self._borrowed: int = 0 

42 self._returned: int = 0 

43 

44 def acquire(self) -> T: 

45 """Borrow an object from the pool or create a new one.""" 

46 with self._lock: 

47 now = time.monotonic() 

48 self._evict_expired(now) 

49 

50 if self._pool: 

51 item = self._pool.pop() 

52 item.idle = False 

53 self._borrowed += 1 

54 return item.obj 

55 

56 if self._created < self._max_size: 

57 self._created += 1 

58 self._borrowed += 1 

59 return self._factory() 

60 

61 # Pool fully allocated; create temporary object outside pool 

62 self._borrowed += 1 

63 return self._factory() 

64 

65 def release(self, obj: T) -> None: 

66 """Return an object to the pool for reuse.""" 

67 with self._lock: 

68 self._returned += 1 

69 self._evict_expired(time.monotonic()) 

70 

71 if len(self._pool) < self._max_idle: 

72 self._pool.append(_PooledItem(obj=obj, idle=True, acquired_at=time.monotonic())) 

73 # else: discard excess objects 

74 

75 def _evict_expired(self, now: float) -> None: 

76 self._pool[:] = [item for item in self._pool if now - item.acquired_at < self._idle_timeout] 

77 

78 @property 

79 def stats(self) -> dict[str, Any]: 

80 with self._lock: 

81 return { 

82 "created": self._created, 

83 "borrowed": self._borrowed, 

84 "returned": self._returned, 

85 "idle": len(self._pool), 

86 "active": self._borrowed - self._returned, 

87 } 

88 

89 def __len__(self) -> int: 

90 return len(self._pool) 

91 

92 

93@dataclass 

94class _PooledItem(Generic[T]): 

95 obj: T 

96 idle: bool 

97 acquired_at: float 

98 

99 

100# ============================================================================ 

101# LRUCache 

102# ============================================================================ 

103 

104 

105class LRUCache(Generic[T]): 

106 """Thread-safe LRU cache with capacity limit and optional TTL.""" 

107 

108 def __init__(self, capacity: int = 1024, ttl: float | None = None): 

109 self._capacity = capacity 

110 self._ttl = ttl 

111 self._cache: OrderedDict[str, _CacheEntry[T]] = OrderedDict() 

112 self._lock = threading.Lock() 

113 self._hits: int = 0 

114 self._misses: int = 0 

115 self._evictions: int = 0 

116 

117 def get(self, key: str) -> T | None: 

118 with self._lock: 

119 entry = self._cache.get(key) 

120 if entry is None: 

121 self._misses += 1 

122 return None 

123 if self._ttl and time.monotonic() - entry.timestamp > self._ttl: 

124 del self._cache[key] 

125 self._misses += 1 

126 self._evictions += 1 

127 return None 

128 self._cache.move_to_end(key) 

129 self._hits += 1 

130 return entry.value 

131 

132 def put(self, key: str, value: T) -> None: 

133 with self._lock: 

134 if key in self._cache: 

135 self._cache.move_to_end(key) 

136 self._cache[key] = _CacheEntry(value=value, timestamp=time.monotonic()) 

137 return 

138 if len(self._cache) >= self._capacity: 

139 self._cache.popitem(last=False) 

140 self._evictions += 1 

141 self._cache[key] = _CacheEntry(value=value, timestamp=time.monotonic()) 

142 

143 def remove(self, key: str) -> bool: 

144 with self._lock: 

145 if key in self._cache: 

146 del self._cache[key] 

147 return True 

148 return False 

149 

150 def clear(self) -> None: 

151 with self._lock: 

152 self._cache.clear() 

153 

154 @property 

155 def hit_rate(self) -> float: 

156 total = self._hits + self._misses 

157 return self._hits / total if total > 0 else 0.0 

158 

159 @property 

160 def stats(self) -> dict[str, Any]: 

161 with self._lock: 

162 return { 

163 "size": len(self._cache), 

164 "capacity": self._capacity, 

165 "hits": self._hits, 

166 "misses": self._misses, 

167 "hit_rate": round(self.hit_rate, 4), 

168 "evictions": self._evictions, 

169 } 

170 

171 def __len__(self) -> int: 

172 return len(self._cache) 

173 

174 def __contains__(self, key: str) -> bool: 

175 return key in self._cache 

176 

177 

178@dataclass 

179class _CacheEntry(Generic[T]): 

180 value: T 

181 timestamp: float 

182 

183 

184# ============================================================================ 

185# SmartCache 

186# ============================================================================ 

187 

188 

189class SmartCache(Generic[T]): 

190 """Multi-tier cache with compute-on-miss and automatic invalidation.""" 

191 

192 def __init__(self, compute: Callable[[str], T], capacity: int = 1024, ttl: float = 300.0): 

193 self._lru = LRUCache[T](capacity=capacity, ttl=ttl) 

194 self._compute = compute 

195 self._lock = threading.Lock() 

196 

197 def get(self, key: str) -> T: 

198 """Get from cache or compute and cache on miss.""" 

199 cached = self._lru.get(key) 

200 if cached is not None: 

201 return cached 

202 with self._lock: 

203 # Double-check after acquiring lock 

204 cached = self._lru.get(key) 

205 if cached is not None: 

206 return cached 

207 value = self._compute(key) 

208 self._lru.put(key, value) 

209 return value 

210 

211 def prefetch(self, keys: list[str]) -> int: 

212 """Pre-compute and cache values for a list of keys. Returns count cached.""" 

213 count = 0 

214 for key in keys: 

215 if key not in self._lru: 

216 try: 

217 self.get(key) 

218 count += 1 

219 except Exception: 

220 pass 

221 return count 

222 

223 def invalidate(self, key: str) -> bool: 

224 return self._lru.remove(key) 

225 

226 def invalidate_pattern(self, pattern: str) -> int: 

227 """Invalidate all keys containing pattern substring. Returns count removed.""" 

228 count = 0 

229 for key in list(self._lru._cache.keys()): 

230 if pattern in key: 

231 if self._lru.remove(key): 

232 count += 1 

233 return count 

234 

235 def clear(self) -> None: 

236 self._lru.clear() 

237 

238 @property 

239 def stats(self) -> dict[str, Any]: 

240 return self._lru.stats 

241 

242 def __len__(self) -> int: 

243 return len(self._lru) 

244 

245 

246# ============================================================================ 

247# MemoryMonitor 

248# ============================================================================ 

249 

250 

251class MemoryMonitor: 

252 """Monitor per-component memory usage with high-water mark tracking.""" 

253 

254 _singleton = None 

255 _lock = threading.Lock() 

256 

257 def __new__(cls): 

258 if cls._singleton is None: 

259 with cls._lock: 

260 if cls._singleton is None: 

261 cls._singleton = super().__new__(cls) 

262 cls._singleton._initialized = False 

263 return cls._singleton 

264 

265 def __init__(self): 

266 if self._initialized: 

267 return 

268 self._initialized = True 

269 self._components: dict[str, _ComponentMetrics] = {} 

270 self._lock = threading.Lock() 

271 

272 def register(self, name: str) -> None: 

273 with self._lock: 

274 if name not in self._components: 

275 self._components[name] = _ComponentMetrics(name=name) 

276 

277 def record_alloc(self, name: str, size_bytes: int) -> None: 

278 with self._lock: 

279 comp = self._components.get(name) 

280 if comp: 

281 comp.current_bytes += size_bytes 

282 comp.total_allocations += 1 

283 comp.peak_bytes = max(comp.peak_bytes, comp.current_bytes) 

284 

285 def record_free(self, name: str, size_bytes: int) -> None: 

286 with self._lock: 

287 comp = self._components.get(name) 

288 if comp: 

289 comp.current_bytes = max(0, comp.current_bytes - size_bytes) 

290 

291 def snapshot(self) -> dict[str, dict[str, Any]]: 

292 with self._lock: 

293 return {name: comp.to_dict() for name, comp in self._components.items()} 

294 

295 def alert(self, name: str, threshold_bytes: int) -> bool: 

296 """Check if a component exceeds memory threshold.""" 

297 with self._lock: 

298 comp = self._components.get(name) 

299 if comp: 

300 return comp.current_bytes > threshold_bytes 

301 return False 

302 

303 @property 

304 def total_current(self) -> int: 

305 with self._lock: 

306 return sum(c.current_bytes for c in self._components.values()) 

307 

308 

309@dataclass 

310class _ComponentMetrics: 

311 name: str 

312 current_bytes: int = 0 

313 peak_bytes: int = 0 

314 total_allocations: int = 0 

315 

316 def to_dict(self) -> dict[str, Any]: 

317 return { 

318 "name": self.name, 

319 "current_bytes": self.current_bytes, 

320 "peak_bytes": self.peak_bytes, 

321 "total_allocations": self.total_allocations, 

322 } 

323 

324 

325# ============================================================================ 

326# Convenience Functions 

327# ============================================================================ 

328 

329 

330def create_object_pool( 

331 factory: Callable[[], T], 

332 max_size: int = 100, 

333 max_idle: int = 30, 

334 idle_timeout: float = 300.0, 

335) -> ObjectPool[T]: 

336 """Create a thread-safe object pool.""" 

337 return ObjectPool(factory, max_size=max_size, max_idle=max_idle, idle_timeout=idle_timeout) 

338 

339 

340def create_lru_cache(capacity: int = 1024, ttl: float | None = None) -> LRUCache[Any]: 

341 """Create a thread-safe LRU cache.""" 

342 return LRUCache(capacity=capacity, ttl=ttl) 

343 

344 

345def create_smart_cache( 

346 compute: Callable[[str], T], 

347 capacity: int = 1024, 

348 ttl: float = 300.0, 

349) -> SmartCache[T]: 

350 """Create a smart cache with compute-on-miss.""" 

351 return SmartCache(compute, capacity=capacity, ttl=ttl) 

352 

353 

354def get_memory_monitor() -> MemoryMonitor: 

355 """Get the singleton memory monitor.""" 

356 return MemoryMonitor()