"""WarmupManager: import heavy modules on a background thread pool. Per spec.md:2.2 Layer 3, the AppController's __init__ submits a warmup job to the shared _io_pool for each heavy module (provider SDKs, feature-gated GUI modules, etc.). After warmup completes, those modules are in sys.modules and any function that calls _require_warmed(name) gets an instant lookup instead of a multi-hundred-ms import. Public API on the manager (and exposed on AppController via delegation): mgr.submit(modules) - start warmup jobs (call once at init) mgr.status() - {pending, completed, failed} mgr.is_done() - bool mgr.wait(timeout) - block until done mgr.on_complete(callback) - register completion callback mgr.reset() - clear state (for re-warmup, e.g. in tests) """ import importlib import threading from concurrent.futures import Future, ThreadPoolExecutor from typing import Callable, Optional CompletionCallback = Callable[[dict], None] class WarmupManager: def __init__(self, pool: ThreadPoolExecutor) -> None: self._pool = pool self._lock = threading.Lock() self._done_event = threading.Event() self._pending: list[str] = [] self._completed: list[str] = [] self._failed: list[str] = [] self._callbacks: list[CompletionCallback] = [] self._started = False def submit(self, modules: list[str]) -> None: with self._lock: if self._started: raise RuntimeError("WarmupManager.submit() called twice; call reset() first") self._pending = list(modules) self._completed = [] self._failed = [] self._done_event.clear() self._started = True for name in modules: self._pool.submit(self._warmup_one, name) def status(self) -> dict[str, list[str]]: with self._lock: return { "pending": list(self._pending), "completed": list(self._completed), "failed": list(self._failed), } def is_done(self) -> bool: return self._done_event.is_set() def wait(self, timeout: Optional[float] = None) -> bool: return self._done_event.wait(timeout=timeout) def on_complete(self, callback: CompletionCallback) -> None: fire_now = False with self._lock: if self._done_event.is_set(): fire_now = True snap = self._snapshot() else: self._callbacks.append(callback) if fire_now: try: callback(snap) except Exception: pass def reset(self) -> None: with self._lock: self._pending = [] self._completed = [] self._failed = [] self._done_event.clear() self._callbacks = [] self._started = False def _warmup_one(self, name: str) -> None: try: importlib.import_module(name) except BaseException as e: self._record_failure(name, e) else: self._record_success(name) def _record_success(self, name: str) -> None: callbacks: list[CompletionCallback] = [] with self._lock: if name in self._pending: self._pending.remove(name) self._completed.append(name) done = self._started and not self._pending if done: self._done_event.set() callbacks = list(self._callbacks) for cb in callbacks: try: cb(self._snapshot()) except Exception: pass def _record_failure(self, name: str, _err: BaseException) -> None: callbacks: list[CompletionCallback] = [] with self._lock: if name in self._pending: self._pending.remove(name) self._failed.append(name) done = self._started and not self._pending if done: self._done_event.set() callbacks = list(self._callbacks) for cb in callbacks: try: cb(self._snapshot()) except Exception: pass def _snapshot(self) -> dict[str, list[str]]: return { "pending": list(self._pending), "completed": list(self._completed), "failed": list(self._failed), }