""" Per-Stage-Latenz-Metriken für die Voice/Lucy-Pipeline. Misst die Server-seitige Dauer jeder Stufe (STT, Vision-Beschreibung, Chat-TTFB, TTS) und hält rollende Statistiken (avg/p50/p95/last) im Speicher. Macht aus Latenz-VERMUTUNGEN gemessene Fakten — die eigentliche Voraussetzung, um gezielt zu optimieren (Stufe 5/C2 des Reviews). Anzeige im Frontend-Overhaul (E) analog zur TokenPerformanceCard. In-Memory + thread-safe (keine Datei-I/O — Latenz-Telemetrie ist transient, Restart = Reset). """ import threading import time from collections import deque _LOCK = threading.Lock() _MAX = 200 _STAGES: dict[str, deque] = {} # Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst. STAGES = ("stt", "vision", "chat_ttfb", "tts") def record_stage(stage: str, ms: float) -> None: """Eine gemessene Stage-Dauer (ms) verbuchen. No-op bei negativen Werten.""" if ms is None or ms < 0: return with _LOCK: dq = _STAGES.get(stage) if dq is None: dq = _STAGES[stage] = deque(maxlen=_MAX) dq.append(float(ms)) class Timer: """Context-Manager: misst die verstrichene Zeit und verbucht sie auf `stage`. Funktioniert um `await`-Aufrufe herum (enter → await → exit).""" def __init__(self, stage: str) -> None: self.stage = stage self._t0 = 0.0 def __enter__(self) -> "Timer": self._t0 = time.perf_counter() return self def __exit__(self, *exc) -> None: record_stage(self.stage, (time.perf_counter() - self._t0) * 1000.0) def _summary(vals: list[float]) -> dict: if not vals: return {"count": 0} s = sorted(vals) n = len(s) return { "count": n, "avg_ms": round(sum(s) / n, 1), "p50_ms": round(s[n // 2], 1), "p95_ms": round(s[min(n - 1, int(n * 0.95))], 1), "last_ms": round(vals[-1], 1), } def get_metrics() -> dict: """Rollende Zusammenfassung je Stufe.""" with _LOCK: return {stage: _summary(list(dq)) for stage, dq in _STAGES.items()}