diff --git a/src/app_controller.py b/src/app_controller.py index bc6ec06d..b571811c 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -1326,18 +1326,19 @@ class AppController: def startup_timeline(self) -> dict: """Returns a dict with all startup timestamps and precomputed deltas. Fields: init_start_ts, appcontroller_init_done_ts, gui_run_started_ts, warmup_done_ts, first_frame_ts, warmup_ms, appcontroller_init_ms, gui_setup_ms, first_render_ms, first_frame_after_init_ms, first_frame_after_warmup_ms. The 3 phase breakdowns answer 'which main-thread phase dominated?': AppController init, GUI bundle setup, first render. [SDM: src/app_controller.py:startup_timeline] [C: src/api_hooks.py:HookHandler.do_GET /api/startup_timeline]""" + cold_start = self.cold_start_ts result: dict = { - "cold_start_ts": self.cold_start_ts, + "cold_start_ts": cold_start.data if cold_start.ok else None, "init_start_ts": self._init_start_ts, "appcontroller_init_done_ts": self._appcontroller_init_done_ts, "gui_run_started_ts": self._gui_run_started_ts, "warmup_done_ts": self._warmup_done_ts, "first_frame_ts": self._first_frame_ts, } - if self.cold_start_ts is not None: - result["module_imports_ms"] = (self._init_start_ts - self.cold_start_ts) * 1000 + if cold_start.ok: + result["module_imports_ms"] = (self._init_start_ts - cold_start.data) * 1000 if self._first_frame_ts is not None: - result["cold_start_to_first_frame_ms"] = (self._first_frame_ts - self.cold_start_ts) * 1000 + result["cold_start_to_first_frame_ms"] = (self._first_frame_ts - cold_start.data) * 1000 else: result["module_imports_ms"] = None if self._warmup_done_ts is not None: @@ -1372,13 +1373,25 @@ class AppController: def cold_start_ts(self) -> "Optional[float]": """Timestamp captured at the very first line of sloppy.py (the entry point). Used to compute the full 'Python start to first frame' latency, - which is dominated by module imports. None if the entry point didn't - expose _SLOPPY_COLD_START_TS. [SDM: src/app_controller.py:cold_start_ts]""" + which is dominated by module imports. Returns Result with errors if the + entry point didn't expose _SLOPPY_COLD_START_TS. [SDM: src/app_controller.py:cold_start_ts]""" try: import sloppy as _sloppy - return getattr(_sloppy, "_SLOPPY_COLD_START_TS", None) - except (ImportError, AttributeError): - return None + ts = getattr(_sloppy, "_SLOPPY_COLD_START_TS", None) + if ts is None: + return Result(data=0.0, errors=[ErrorInfo( + kind=ErrorKind.NOT_READY, + message="cold start timestamp not exposed by sloppy entry point", + source="app_controller.cold_start_ts", + )]) + return Result(data=float(ts)) + except (ImportError, AttributeError) as e: + return Result(data=0.0, errors=[ErrorInfo( + kind=ErrorKind.NOT_READY, + message=str(e), + source="app_controller.cold_start_ts", + original=e, + )]) def _on_warmup_complete_for_timeline(self, snap: dict) -> None: """Callback registered with the WarmupManager. Stamps warmup_done_ts and logs the timeline to stderr. [C: src/app_controller.py:startup_timeline]"""