devsquad 3.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,635 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ WarmupManager - 启动预热管理器
5
+
6
+ 分层异步预热 + 懒加载 + 进程级缓存,将冷启动时间从 ~1.7s 优化到 < 1s。
7
+
8
+ 核心策略:
9
+ - L1 Eager: 同步阻塞,导入时立即执行(~15ms)
10
+ - L2 Async: 后台线程异步执行(~300ms,非阻塞)
11
+ - LAZY: 首次访问时按需触发(< 200ms)
12
+ - ProcessCache: TTL + LRU 淘汰的进程级单例缓存
13
+
14
+ 使用示例:
15
+ from collaboration.warmup_manager import WarmupManager, WarmupConfig
16
+
17
+ wm = WarmupManager.instance(WarmupConfig.default())
18
+ report = wm.warmup()
19
+
20
+ coordinator = wm.get_or_load("coordinator", lambda: Coordinator())
21
+ """
22
+
23
+ import os
24
+ import sys
25
+ import time
26
+ import threading
27
+ import statistics
28
+ import concurrent.futures
29
+ from collections import deque
30
+ from dataclasses import dataclass, field
31
+ from typing import Any, Callable, Dict, List, Optional, ClassVar
32
+ from enum import Enum
33
+ from datetime import datetime
34
+
35
+
36
+ class WarmupLayer(Enum):
37
+ EAGER = "eager"
38
+ ASYNC = "async"
39
+ LAZY = "lazy"
40
+
41
+
42
+ class WarmupStatus(Enum):
43
+ SUCCESS = "success"
44
+ TIMEOUT = "timeout"
45
+ ERROR = "error"
46
+ SKIPPED = "skipped"
47
+ PENDING = "pending"
48
+
49
+
50
+ @dataclass
51
+ class WarmupConfig:
52
+ enabled: bool = True
53
+ eager_timeout_ms: int = 200
54
+ async_timeout_ms: int = 5000
55
+ async_workers: int = 4
56
+ cache_enabled: bool = True
57
+ cache_max_size: int = 200
58
+ cache_ttl_seconds: float = 3600.0
59
+ preload_roles: Optional[List[str]] = None
60
+ preload_stages: Optional[List[str]] = None
61
+ lazy_load_threshold: int = 3
62
+ metrics_enabled: bool = False
63
+
64
+ @classmethod
65
+ def default(cls) -> 'WarmupConfig':
66
+ return cls()
67
+
68
+ @classmethod
69
+ def fast(cls) -> 'WarmupConfig':
70
+ return cls(
71
+ async_workers=0,
72
+ cache_max_size=50,
73
+ eager_timeout_ms=100,
74
+ async_timeout_ms=1000,
75
+ )
76
+
77
+ @classmethod
78
+ def full(cls) -> 'WarmupConfig':
79
+ return cls(
80
+ async_workers=8,
81
+ cache_max_size=500,
82
+ cache_ttl_seconds=7200.0,
83
+ eager_timeout_ms=500,
84
+ async_timeout_ms=10000,
85
+ )
86
+
87
+ @classmethod
88
+ def from_env(cls) -> 'WarmupConfig':
89
+ mode = os.environ.get("WARMUP_MODE", "DEFAULT").upper()
90
+ if mode == "FAST":
91
+ return cls.fast()
92
+ elif mode == "FULL":
93
+ return cls.full()
94
+ elif mode == "DISABLED":
95
+ return cls(enabled=False)
96
+ else:
97
+ return cls.default()
98
+
99
+
100
+ @dataclass
101
+ class WarmupTask:
102
+ task_id: str
103
+ name: str
104
+ priority: int = 1
105
+ layer: WarmupLayer = WarmupLayer.LAZY
106
+ dependencies: List[str] = field(default_factory=list)
107
+ executor: Optional[Callable[[], Any]] = None
108
+ timeout_ms: int = 5000
109
+ retry_count: int = 1
110
+
111
+
112
+ @dataclass
113
+ class WarmupResult:
114
+ task_id: str
115
+ status: WarmupStatus = WarmupStatus.PENDING
116
+ duration_ms: float = 0.0
117
+ error: Optional[str] = None
118
+ cache_hit: bool = False
119
+
120
+
121
+ @dataclass
122
+ class WarmupReport:
123
+ total_tasks: int = 0
124
+ completed: int = 0
125
+ failed: int = 0
126
+ cached: int = 0
127
+ total_duration_ms: float = 0.0
128
+ tasks: List[WarmupResult] = field(default_factory=list)
129
+ timestamp: datetime = field(default_factory=datetime.now)
130
+
131
+
132
+ @dataclass
133
+ class CacheEntry:
134
+ key: str
135
+ value: Any
136
+ created_at: float = field(default_factory=time.time)
137
+ last_accessed: float = field(default_factory=time.time)
138
+ access_count: int = 0
139
+ size_bytes: int = 0
140
+ ttl_seconds: float = 3600.0
141
+ source: str = ""
142
+
143
+ @property
144
+ def is_expired(self) -> bool:
145
+ if self.ttl_seconds <= 0:
146
+ return False
147
+ return (time.time() - self.created_at) > self.ttl_seconds
148
+
149
+ @property
150
+ def age_seconds(self) -> float:
151
+ return time.time() - self.created_at
152
+
153
+
154
+ @dataclass
155
+ class WarmupMetrics:
156
+ startup_time_ms: float = 0.0
157
+ eager_duration_ms: float = 0.0
158
+ async_duration_ms: float = 0.0
159
+ cache_hit_rate: float = 0.0
160
+ cache_size: int = 0
161
+ memory_usage_mb: float = 0.0
162
+ tasks_completed: int = 0
163
+ tasks_failed: int = 0
164
+ lazy_loads_triggered: int = 0
165
+
166
+
167
+ class WarmupManager:
168
+ _instance: ClassVar[Optional['WarmupManager']] = None
169
+ _lock: ClassVar[threading.RLock] = threading.RLock()
170
+ _eager_task_ids: ClassVar[set] = set()
171
+
172
+ def __init__(self, config: Optional[WarmupConfig] = None):
173
+ self.config = config or WarmupConfig.from_env()
174
+ self._tasks: Dict[str, WarmupTask] = {}
175
+ self._results: Dict[str, WarmupResult] = {}
176
+ self._cache: Dict[str, CacheEntry] = {}
177
+ self._ready_flags: Dict[str, threading.Event] = {}
178
+ self._executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
179
+ self._start_time: float = 0.0
180
+ self._is_warming_up: bool = False
181
+ self._shutdown_flag: bool = False
182
+ self._lazy_load_count: int = 0
183
+ self._inner_lock = threading.RLock()
184
+ self._eager_task_ids_local: set = set()
185
+
186
+ @classmethod
187
+ def instance(cls, config: Optional[WarmupConfig] = None) -> 'WarmupManager':
188
+ with cls._lock:
189
+ if cls._instance is None:
190
+ cls._instance = cls(config=config or WarmupConfig.from_env())
191
+ cls._instance._register_builtin_tasks()
192
+ cls._eager_task_ids = {
193
+ t.task_id for t in cls._instance._tasks.values()
194
+ if t.layer == WarmupLayer.EAGER
195
+ }
196
+ return cls._instance
197
+
198
+ @classmethod
199
+ def reset(cls) -> None:
200
+ with cls._lock:
201
+ if cls._instance:
202
+ cls._instance.shutdown()
203
+ cls._instance = None
204
+ cls._eager_task_ids = set()
205
+
206
+ def _register_builtin_tasks(self):
207
+ self.register_task(WarmupTask(
208
+ task_id="core-models",
209
+ name="核心数据模型加载",
210
+ priority=0,
211
+ layer=WarmupLayer.EAGER,
212
+ executor=self._load_core_models,
213
+ timeout_ms=200,
214
+ ))
215
+ self.register_task(WarmupTask(
216
+ task_id="role-metadata",
217
+ name="角色元数据加载",
218
+ priority=0,
219
+ layer=WarmupLayer.EAGER,
220
+ dependencies=["core-models"],
221
+ executor=self._load_role_metadata,
222
+ timeout_ms=100,
223
+ ))
224
+
225
+ def _load_core_models(self) -> Dict[str, str]:
226
+ return {"models_loaded": True, "timestamp": time.time()}
227
+
228
+ def _load_role_metadata(self) -> Dict[str, Any]:
229
+ try:
230
+ from prompts.registry import ROLE_METADATA, STAGE_METADATA
231
+ return {
232
+ "roles": list(ROLE_METADATA.keys()),
233
+ "stages": list(STAGE_METADATA.keys()),
234
+ "count": len(ROLE_METADATA),
235
+ }
236
+ except Exception:
237
+ return {"roles": [], "stages": [], "count": 0, "error": "registry_import_failed"}
238
+
239
+ def register_task(self, task: WarmupTask) -> None:
240
+ with self._inner_lock:
241
+ self._tasks[task.task_id] = task
242
+ self._results[task.task_id] = WarmupResult(task_id=task.task_id)
243
+ if task.layer == WarmupLayer.EAGER:
244
+ self._eager_task_ids_local.add(task.task_id)
245
+
246
+ def warmup(self, layers: Optional[List[WarmupLayer]] = None) -> WarmupReport:
247
+ if not self.config.enabled:
248
+ return WarmupReport(timestamp=datetime.now())
249
+ self._start_time = time.perf_counter()
250
+ eager_results = []
251
+ if layers is None or WarmupLayer.EAGER in layers:
252
+ eager_results = self.warmup_eager()
253
+ if layers is None or WarmupLayer.ASYNC in layers:
254
+ self.warmup_async()
255
+ all_results = list(self._results.values())
256
+ total_dur = (time.perf_counter() - self._start_time) * 1000
257
+ completed = sum(1 for r in all_results if r.status == WarmupStatus.SUCCESS)
258
+ failed = sum(1 for r in all_results if r.status in (WarmupStatus.ERROR, WarmupStatus.TIMEOUT))
259
+ cached = sum(1 for r in all_results if r.cache_hit)
260
+ return WarmupReport(
261
+ total_tasks=len(all_results),
262
+ completed=completed,
263
+ failed=failed,
264
+ cached=cached,
265
+ total_duration_ms=total_dur,
266
+ tasks=all_results,
267
+ timestamp=datetime.now(),
268
+ )
269
+
270
+ def warmup_eager(self) -> List[WarmupResult]:
271
+ eager_tasks = [t for t in self._tasks.values() if t.layer == WarmupLayer.EAGER]
272
+ sorted_tasks = self._topological_sort(eager_tasks)
273
+ results = []
274
+ for task in sorted_tasks:
275
+ start = time.perf_counter()
276
+ try:
277
+ if task.executor is None:
278
+ result_val = None
279
+ else:
280
+ result_val = task.executor()
281
+ duration = (time.perf_counter() - start) * 1000
282
+ entry = CacheEntry(
283
+ key=task.task_id,
284
+ value=result_val,
285
+ created_at=time.time(),
286
+ last_accessed=time.time(),
287
+ source=f"eager:{task.name}",
288
+ ttl_seconds=self.config.cache_ttl_seconds,
289
+ )
290
+ with self._inner_lock:
291
+ self._cache[task.task_id] = entry
292
+ wr = WarmupResult(
293
+ task_id=task.task_id,
294
+ status=WarmupStatus.SUCCESS,
295
+ duration_ms=duration,
296
+ )
297
+ with self._inner_lock:
298
+ self._results[task.task_id] = wr
299
+ results.append(wr)
300
+ except Exception as e:
301
+ duration = (time.perf_counter() - start) * 1000
302
+ wr = WarmupResult(
303
+ task_id=task.task_id,
304
+ status=WarmupStatus.ERROR,
305
+ duration_ms=duration,
306
+ error=str(e),
307
+ )
308
+ with self._inner_lock:
309
+ self._results[task.task_id] = wr
310
+ results.append(wr)
311
+ return results
312
+
313
+ def warmup_async(self) -> None:
314
+ if self._executor is not None or self._is_warming_up:
315
+ return
316
+ if self.config.async_workers <= 0:
317
+ return
318
+ if self._shutdown_flag:
319
+ return
320
+ async_tasks = [t for t in self._tasks.values() if t.layer == WarmupLayer.ASYNC]
321
+ if not async_tasks:
322
+ return
323
+ sorted_tasks = self._topological_sort(async_tasks)
324
+ self._is_warming_up = True
325
+ self._executor = concurrent.futures.ThreadPoolExecutor(
326
+ max_workers=self.config.async_workers,
327
+ thread_name_prefix="warmup",
328
+ )
329
+ futures_map: Dict[concurrent.futures.Future, WarmupTask] = {}
330
+
331
+ def _run_task(task: WarmupTask):
332
+ start = time.perf_counter()
333
+ try:
334
+ if task.executor is None:
335
+ result_val = None
336
+ else:
337
+ result_val = task.executor()
338
+ duration = (time.perf_counter() - start) * 1000
339
+ entry = CacheEntry(
340
+ key=task.task_id,
341
+ value=result_val,
342
+ created_at=time.time(),
343
+ last_accessed=time.time(),
344
+ source=f"async:{task.name}",
345
+ ttl_seconds=self.config.cache_ttl_seconds,
346
+ )
347
+ with self._inner_lock:
348
+ self._cache[task.task_id] = entry
349
+ wr = WarmupResult(
350
+ task_id=task.task_id,
351
+ status=WarmupStatus.SUCCESS,
352
+ duration_ms=duration,
353
+ )
354
+ with self._inner_lock:
355
+ self._results[task.task_id] = wr
356
+ event = self._ready_flags.get(task.task_id)
357
+ if event:
358
+ event.set()
359
+ except Exception as e:
360
+ duration = (time.perf_counter() - start) * 1000
361
+ wr = WarmupResult(
362
+ task_id=task.task_id,
363
+ status=WarmupStatus.ERROR,
364
+ duration_ms=duration,
365
+ error=str(e),
366
+ )
367
+ with self._inner_lock:
368
+ self._results[task.task_id] = wr
369
+ event = self._ready_flags.get(task.task_id)
370
+ if event:
371
+ event.set()
372
+
373
+ for task in sorted_tasks:
374
+ future = self._executor.submit(_run_task, task)
375
+ futures_map[future] = task
376
+
377
+ def _on_done(future: concurrent.futures.Future):
378
+ task = futures_map.pop(future, None)
379
+ if task:
380
+ try:
381
+ future.result(timeout=task.timeout_ms / 1000.0)
382
+ except (concurrent.futures.TimeoutError, Exception):
383
+ pass
384
+
385
+ for future in list(futures_map.keys()):
386
+ future.add_done_callback(_on_done)
387
+
388
+ def _wait_all():
389
+ for f in list(futures_map.keys()):
390
+ try:
391
+ f.result(timeout=max(t.timeout_ms for t in sorted_tasks) / 1000.0)
392
+ except Exception:
393
+ pass
394
+ self._is_warming_up = False
395
+
396
+ wait_thread = threading.Thread(target=_wait_all, daemon=True, name="warmup-waiter")
397
+ wait_thread.start()
398
+
399
+ def get(self, key: str, default: Any = None) -> Any:
400
+ if not self.config.cache_enabled:
401
+ return default
402
+ entry = self._cache.get(key)
403
+ if entry is None:
404
+ return default
405
+ if entry.is_expired:
406
+ del self._cache[key]
407
+ return default
408
+ entry.last_accessed = time.time()
409
+ entry.access_count += 1
410
+ return entry.value
411
+
412
+ def get_or_load(self, key: str, loader: Callable[[], Any],
413
+ layer: WarmupLayer = WarmupLayer.LAZY) -> Any:
414
+ value = self.get(key)
415
+ if value is not None:
416
+ return value
417
+ self._lazy_load_count += 1
418
+ if key not in self._ready_flags:
419
+ with self._inner_lock:
420
+ if key not in self._ready_flags:
421
+ self._ready_flags[key] = threading.Event()
422
+ event = self._ready_flags[key]
423
+ if not event.is_set():
424
+ with self._inner_lock:
425
+ if not event.is_set():
426
+ try:
427
+ result = loader()
428
+ self._cache[key] = CacheEntry(
429
+ key=key,
430
+ value=result,
431
+ created_at=time.time(),
432
+ last_accessed=time.time(),
433
+ source=f"lazy:{key}",
434
+ ttl_seconds=self.config.cache_ttl_seconds,
435
+ )
436
+ finally:
437
+ event.set()
438
+ event.wait(timeout=30)
439
+ return self.get(key, default=None)
440
+
441
+ def set_cache(self, key: str, value: Any, source: str = "",
442
+ ttl: Optional[float] = None) -> None:
443
+ if not self.config.cache_enabled:
444
+ return
445
+ entry = CacheEntry(
446
+ key=key,
447
+ value=value,
448
+ created_at=time.time(),
449
+ last_accessed=time.time(),
450
+ source=source or f"manual:{key}",
451
+ ttl_seconds=ttl if ttl is not None else self.config.cache_ttl_seconds,
452
+ )
453
+ with self._inner_lock:
454
+ self._cache[key] = entry
455
+ self._evict_if_needed()
456
+
457
+ def is_ready(self, task_id: str) -> bool:
458
+ result = self._results.get(task_id)
459
+ if result is None:
460
+ return False
461
+ return result.status == WarmupStatus.SUCCESS
462
+
463
+ def is_fully_warmed(self) -> bool:
464
+ if not self._tasks:
465
+ return True
466
+ for result in self._results.values():
467
+ if result.status in (WarmupStatus.PENDING,):
468
+ return False
469
+ if self._is_warming_up:
470
+ return False
471
+ return True
472
+
473
+ def get_report(self) -> WarmupReport:
474
+ all_results = list(self._results.values())
475
+ completed = sum(1 for r in all_results if r.status == WarmupStatus.SUCCESS)
476
+ failed = sum(1 for r in all_results if r.status in (WarmupStatus.ERROR, WarmupStatus.TIMEOUT))
477
+ cached = sum(1 for r in all_results if r.cache_hit)
478
+ return WarmupReport(
479
+ total_tasks=len(all_results),
480
+ completed=completed,
481
+ failed=failed,
482
+ cached=cached,
483
+ total_duration_ms=(time.perf_counter() - self._start_time) * 1000 if self._start_time else 0,
484
+ tasks=all_results,
485
+ timestamp=datetime.now(),
486
+ )
487
+
488
+ def get_metrics(self) -> WarmupMetrics:
489
+ if not self.config.metrics_enabled:
490
+ return WarmupMetrics()
491
+ total_hits = sum(e.access_count for e in self._cache.values())
492
+ total_entries = len(self._cache)
493
+ hit_rate = total_hits / max(total_hits + max(total_entries, 1), 1)
494
+ eager_dur = sum(
495
+ r.duration_ms for tid, r in self._results.items()
496
+ if tid in self._eager_task_ids and r.status == WarmupStatus.SUCCESS
497
+ )
498
+ async_dur = sum(
499
+ r.duration_ms for tid, r in self._results.items()
500
+ if tid not in self._eager_task_ids and r.status == WarmupStatus.SUCCESS
501
+ )
502
+ completed = sum(1 for r in self._results.values() if r.status == WarmupStatus.SUCCESS)
503
+ failed = sum(1 for r in self._results.values()
504
+ if r.status in (WarmupStatus.ERROR, WarmupStatus.TIMEOUT))
505
+ return WarmupMetrics(
506
+ startup_time_ms=(time.perf_counter() - self._start_time) * 1000 if self._start_time else 0,
507
+ eager_duration_ms=eager_dur,
508
+ async_duration_ms=async_dur,
509
+ cache_hit_rate=hit_rate,
510
+ cache_size=len(self._cache),
511
+ memory_usage_mb=self._estimate_memory(),
512
+ tasks_completed=completed,
513
+ tasks_failed=failed,
514
+ lazy_loads_triggered=self._lazy_load_count,
515
+ )
516
+
517
+ def print_diagnostics(self) -> str:
518
+ m = self.get_metrics()
519
+ lines = [
520
+ "=== WarmupManager Diagnostics ===",
521
+ f"Startup: {m.startup_time_ms:.1f}ms",
522
+ f"Eager: {m.eager_duration_ms:.1f}ms | Async: {m.async_duration_ms:.1f}ms",
523
+ f"Cache: {m.cache_size} entries | Hit Rate: {m.cache_hit_rate:.1%}",
524
+ f"Memory: {m.memory_usage_mb:.1f}MB",
525
+ f"Tasks: {m.tasks_completed}/{m.tasks_completed + m.tasks_failed}",
526
+ f"Lazy loads triggered: {m.lazy_loads_triggered}",
527
+ "--- Task Details ---",
528
+ ]
529
+ status_icon = {
530
+ WarmupStatus.SUCCESS: "\u2705",
531
+ WarmupStatus.ERROR: "\u274c",
532
+ WarmupStatus.TIMEOUT: "\u23f3",
533
+ WarmupStatus.PENDING: "\u23f3",
534
+ WarmupStatus.SKIPPED: "\u23ef",
535
+ }
536
+ for rid in sorted(self._results.keys()):
537
+ r = self._results[rid]
538
+ icon = status_icon.get(r.status, "?")
539
+ lines.append(f" {icon} {rid}: {r.duration_ms:.1f}ms")
540
+ if r.error:
541
+ lines.append(f" error: {r.error[:80]}")
542
+ return "\n".join(lines)
543
+
544
+ def benchmark(self, iterations: int = 5) -> Dict[str, Any]:
545
+ times = []
546
+ for _ in range(iterations):
547
+ self.invalidate_all()
548
+ with self._inner_lock:
549
+ for tid in self._results:
550
+ self._results[tid] = WarmupResult(task_id=tid)
551
+ self._start_time = time.perf_counter()
552
+ self.warmup()
553
+ max_wait = self.config.async_timeout_ms / 1000.0 + 1.0
554
+ deadline = time.monotonic() + max_wait
555
+ while not self.is_fully_warmed() and time.monotonic() < deadline:
556
+ time.sleep(0.01)
557
+ elapsed = (time.perf_counter() - self._start_time) * 1000
558
+ times.append(elapsed)
559
+ if not times:
560
+ return {"mean_ms": 0, "min_ms": 0, "max_ms": 0, "p50_ms": 0, "p95_ms": 0, "iterations": 0}
561
+ sorted_times = sorted(times)
562
+ p50_idx = int(len(sorted_times) * 0.5)
563
+ p95_idx = min(int(len(sorted_times) * 0.95), len(sorted_times) - 1)
564
+ return {
565
+ "mean_ms": statistics.mean(times),
566
+ "min_ms": min(times),
567
+ "max_ms": max(times),
568
+ "p50_ms": sorted_times[p50_idx],
569
+ "p95_ms": sorted_times[p95_idx],
570
+ "iterations": iterations,
571
+ }
572
+
573
+ def invalidate(self, key: str) -> None:
574
+ with self._inner_lock:
575
+ self._cache.pop(key, None)
576
+
577
+ def invalidate_all(self) -> None:
578
+ with self._inner_lock:
579
+ self._cache.clear()
580
+
581
+ def shutdown(self) -> None:
582
+ self._shutdown_flag = True
583
+ self._is_warming_up = False
584
+ if self._executor is not None:
585
+ self._executor.shutdown(wait=False, cancel_futures=True)
586
+ self._executor = None
587
+ with self._inner_lock:
588
+ self._cache.clear()
589
+
590
+ def _topological_sort(self, tasks: List[WarmupTask]) -> List[WarmupTask]:
591
+ if not tasks:
592
+ return []
593
+ task_map = {t.task_id: t for t in tasks}
594
+ in_degree = {t.task_id: 0 for t in tasks}
595
+ adj: Dict[str, List[str]] = {t.task_id: [] for t in tasks}
596
+ for task in tasks:
597
+ for dep in task.dependencies:
598
+ if dep in in_degree:
599
+ adj.setdefault(dep, []).append(task.task_id)
600
+ in_degree[task.task_id] += 1
601
+ queue = deque(tid for tid, deg in in_degree.items() if deg == 0)
602
+ result = []
603
+ while queue:
604
+ tid = queue.popleft()
605
+ if tid in task_map:
606
+ result.append(task_map[tid])
607
+ for neighbor in adj.get(tid, []):
608
+ in_degree[neighbor] -= 1
609
+ if in_degree[neighbor] == 0:
610
+ queue.append(neighbor)
611
+ if len(result) != len(tasks):
612
+ remaining = [t.name for t in tasks if t not in result]
613
+ raise ValueError(f"Circular dependency detected: {remaining}")
614
+ return result
615
+
616
+ def _evict_if_needed(self) -> None:
617
+ if not self.config.cache_enabled or self.config.cache_max_size <= 0:
618
+ return
619
+ now = time.time()
620
+ expired = [k for k, v in self._cache.items() if v.is_expired]
621
+ for k in expired:
622
+ self._cache.pop(k, None)
623
+ if len(self._cache) > self.config.cache_max_size:
624
+ sorted_by_lru = sorted(self._cache.items(), key=lambda x: x[1].last_accessed)
625
+ excess = len(self._cache) - self.config.cache_max_size
626
+ for k, _ in sorted_by_lru[:excess]:
627
+ self._cache.pop(k, None)
628
+
629
+ @staticmethod
630
+ def _estimate_memory() -> float:
631
+ import tracemalloc
632
+ if not tracemalloc.is_tracing():
633
+ return 0.0
634
+ current, peak = tracemalloc.get_traced_memory()
635
+ return current / (1024 * 1024)