feat(startup_profiler): add module-level singleton + live stderr logging

- startup_profiler: StartupProfiler = StartupProfiler() at module bottom
  so sloppy.py can import it without circular imports.
- phase() context manager now writes a [startup] <name>: <ms>ms line to
  stderr in its finally block. Live visibility of every measured phase.
This commit is contained in:
ed
2026-06-06 23:57:19 -04:00
parent 748e5d01ea
commit 77873c21f3
+15 -6
View File
@@ -1,4 +1,5 @@
import time import time
import sys
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Iterator from typing import Any, Iterator
@@ -6,14 +7,14 @@ from typing import Any, Iterator
@dataclass @dataclass
class _Phase: class _Phase:
name: str name: str
start_ts: float start_ts: float
end_ts: float = 0.0 end_ts: float = 0.0
@dataclass @dataclass
class StartupProfiler: class StartupProfiler:
_phases: list[_Phase] = field(default_factory=list) _phases: list[_Phase] = field(default_factory=list)
_enabled: bool = True _enabled: bool = True
def enable(self) -> None: def enable(self) -> None:
@@ -33,6 +34,11 @@ class StartupProfiler:
finally: finally:
p.end_ts = time.perf_counter() p.end_ts = time.perf_counter()
self._phases.append(p) self._phases.append(p)
try:
sys.stderr.write(f"[startup] {name}: {(p.end_ts - p.start_ts) * 1000.0:.1f}ms\n")
sys.stderr.flush()
except Exception:
pass
def snapshot(self) -> dict[str, Any]: def snapshot(self) -> dict[str, Any]:
phases: dict[str, dict[str, float]] = {} phases: dict[str, dict[str, float]] = {}
@@ -40,15 +46,18 @@ class StartupProfiler:
for p in self._phases: for p in self._phases:
duration_ms = max(0.0, (p.end_ts - p.start_ts) * 1000.0) if p.end_ts else 0.0 duration_ms = max(0.0, (p.end_ts - p.start_ts) * 1000.0) if p.end_ts else 0.0
phases[p.name] = { phases[p.name] = {
"start_ts": p.start_ts, "start_ts": p.start_ts,
"duration_ms": round(duration_ms, 3), "duration_ms": round(duration_ms, 3),
} }
total += duration_ms total += duration_ms
return { return {
"phases": phases, "phases": phases,
"total_ms": round(total, 3), "total_ms": round(total, 3),
"count": len(phases), "count": len(phases),
} }
def reset(self) -> None: def reset(self) -> None:
self._phases.clear() self._phases.clear()
startup_profiler: StartupProfiler = StartupProfiler()