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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- 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)
|