From cc2448fb3e53edeff3b7d710a7ce6be6570c90e8 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 18 Jun 2026 20:11:18 -0400 Subject: [PATCH] refactor(app_controller): migrate cold_start_ts to Result[float] + classify 4 rethrow sites (Phase 4) Phase 4: 5 sites resolved per spec.md FR3 + FR4. FR4: Migrate INTERNAL_OPTIONAL_RETURN site (L1378 cold_start_ts): - Changed return type from Optional[float] to Result[float] (data=timestamp, errors=[...] if not exposed) - Updated 3 callers in startup_timeline() to use .ok and .data - The 'not exposed' case returns Result with kind=NOT_READY FR3: Classify 4 INTERNAL_RETHROW sites (all legitimate per pattern analysis): - L1246 __getattr__ dunder raise: Pattern 3 (legitimate) - supports Python attribute lookup protocol - L1272 __getattr__ final raise: Pattern 3 (legitimate) - supports hasattr() and __setattr__ routing - L3048 load_context_preset: Pattern 1 (legitimate) - convert Result.ok=False to RuntimeError; preserves caller signature - L3051 load_context_preset: Pattern 1 (legitimate) - raise KeyError for not-found condition; preserves caller signature The 4 rethrow sites stay as-is per the convention's 'Pattern 1: catch + convert + raise as different type is legitimate'. Changing the signatures would require updating all callers (significant scope expansion beyond this track's mandate). The cold_start_ts migration changes Optional[float] -> Result[float] per spec.md FR4. Callers updated to check .ok before using .data. Tests: 18/18 test_warmup_canaries.py pass; 5/5 test_app_controller_result.py pass. Refs: spec.md FR3+FR4, plan.md Task 4.1-4.3 --- src/app_controller.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) 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]"""