conductor(plan): flip track from lazy-loading to proactive warmup
Architectural shift driven by user clarification: lazy-loading on first
use causes user-perceptible lag when the user-triggered action (e.g.
provider switch) propagates to a controller method that triggers the
first import. The fix is to pre-import heavy modules on a bg thread
at startup and have functions access them via _require_warmed().
Old design (rejected):
- from google import genai inside _send_gemini (lazy on first call)
- First user action that triggers this pays the cost; UI feels laggy
New design (this commit):
- Top-level heavy imports REMOVED from main-thread-reachable files
- AppController.__init__ submits warmup jobs to _io_pool (4 threads,
named 'controller-io-N')
- Each warmup worker imports its module and updates a thread-safe
warmup_status dict
- Functions access modules via _require_warmed(name), which assumes
the module is in sys.modules (warmed at startup)
- When all jobs complete, _warmup_done_event is set and registered
on_warmup_complete callbacks fire
- GUI shows status indicator + toast when warmup completes
- Hook API exposes /api/warmup_status and /api/warmup_wait
- Tests can call controller.wait_for_warmup() before exercising
warmup-dependent functionality
Phase 2 now bundles job pool + warmup (T2.3+T2.4 add warmup tests +
implementation). Phases 3-5 do 'remove top-level imports' instead of
'lazy-load'. Phase 7 is the notification surface (Hook API + GUI).
Definition of Done includes warmup-completion criteria, the
'no function-body imports' check, and an end-to-end 'provider switch
is INSTANT' smoke test.
No code changes; this is a planning update only.
This commit is contained in:
@@ -11,12 +11,14 @@
|
|||||||
"src/startup_profiler.py",
|
"src/startup_profiler.py",
|
||||||
"scripts/audit_main_thread_imports.py",
|
"scripts/audit_main_thread_imports.py",
|
||||||
"scripts/audit_gui2_imports.py",
|
"scripts/audit_gui2_imports.py",
|
||||||
"tests/test_ai_client_lazy_imports.py",
|
"tests/test_ai_client_no_top_level_sdk_imports.py",
|
||||||
"tests/test_hook_server_lazy_fastapi.py",
|
"tests/test_hook_server_no_top_level_fastapi.py",
|
||||||
"tests/test_app_controller_io_pool.py",
|
"tests/test_app_controller_io_pool.py",
|
||||||
"tests/test_command_palette_lazy.py",
|
"tests/test_warmup_mechanism.py",
|
||||||
"tests/test_theme_nerv_lazy.py",
|
"tests/test_command_palette_no_top_level_import.py",
|
||||||
"tests/test_markdown_helper_lazy.py",
|
"tests/test_theme_nerv_no_top_level_import.py",
|
||||||
|
"tests/test_markdown_helper_no_top_level_import.py",
|
||||||
|
"tests/test_api_hooks_warmup.py",
|
||||||
"tests/test_main_thread_purity.py",
|
"tests/test_main_thread_purity.py",
|
||||||
"tests/test_startup_profiler.py",
|
"tests/test_startup_profiler.py",
|
||||||
"tests/test_io_pool_endpoint.py"
|
"tests/test_io_pool_endpoint.py"
|
||||||
@@ -42,19 +44,26 @@
|
|||||||
"estimated_phases": 9,
|
"estimated_phases": 9,
|
||||||
"spec": "spec.md",
|
"spec": "spec.md",
|
||||||
"plan": "plan.md",
|
"plan": "plan.md",
|
||||||
"architectural_invariant": "The main thread (the one that enters immapp.run()) must NEVER import a module heavier than imgui_bundle and the lean gui_2 skeleton. Enforced by scripts/audit_main_thread_imports.py (static CI gate) and tests/test_main_thread_purity.py (runtime audit-hook test).",
|
"architectural_invariant": "The main thread (the one that enters immapp.run()) must NEVER import a module heavier than imgui_bundle and the lean gui_2 skeleton. Heavy modules are removed from main-thread-reachable files entirely and accessed via _require_warmed(name) at use sites, which assumes the module is in sys.modules because AppController's warmup pre-loaded it on the _io_pool. Enforced by scripts/audit_main_thread_imports.py (static CI gate) and tests/test_main_thread_purity.py (runtime audit-hook test).",
|
||||||
"threading_constraint": "NO new threading.Thread(...) calls in src/. All background work must go through AppController._io_pool (ThreadPoolExecutor, max_workers=4, thread_name_prefix='controller-io').",
|
"threading_constraint": "NO new threading.Thread(...) calls in src/. All background work must go through AppController._io_pool (ThreadPoolExecutor, max_workers=4, thread_name_prefix='controller-io'). The _io_pool is also the home of the heavy-module warmup jobs submitted in AppController.__init__.",
|
||||||
|
"warmup_mechanism": "AppController.__init__ submits one job per heavy module to _io_pool. Each job imports its module and updates a thread-safe warmup_status dict. When the last job completes, _warmup_done_event is set and registered on_warmup_complete callbacks fire. The GUI polls warmup_status() each frame for a status-bar indicator. /api/warmup_status and /api/warmup_wait expose the state to tests and external clients. The user is notified via a toast on completion: 'All providers ready (M modules).'",
|
||||||
"verification_criteria": [
|
"verification_criteria": [
|
||||||
"import src.ai_client < 50ms cold start (from ~1800ms)",
|
"import src.ai_client < 50ms cold start (from ~1800ms)",
|
||||||
"import src.gui_2 < 500ms cold start (from ~3000ms)",
|
"import src.gui_2 < 500ms cold start (from ~3000ms)",
|
||||||
"import src.app_controller < 300ms cold start (from ~700ms)",
|
"import src.app_controller < 300ms cold start (from ~700ms)",
|
||||||
"uv run sloppy.py --enable-test-hooks reaches immapp.run() in < 1.5s",
|
"uv run sloppy.py --enable-test-hooks reaches immapp.run() in < 1.5s",
|
||||||
"live_gui.wait_for_server(timeout=15) passes for all tests",
|
"live_gui.wait_for_server(timeout=15) passes for all tests",
|
||||||
"scripts/audit_main_thread_imports.py exits 0 (no main-thread heavy imports)",
|
"scripts/audit_main_thread_imports.py exits 0 (no heavy imports on main)",
|
||||||
"tests/test_main_thread_purity.py passes (runtime audit hook confirms invariant)",
|
"tests/test_main_thread_purity.py passes (runtime audit hook confirms invariant)",
|
||||||
|
"controller.wait_for_warmup(timeout=10) returns True",
|
||||||
|
"All warmup modules in sys.modules after warmup completes",
|
||||||
|
"User-triggered provider switch is INSTANT (proves warmup worked)",
|
||||||
|
"GUI shows 'Warming up... (N/M)' then 'All imports ready' with green dot, then a toast",
|
||||||
|
"GET /api/warmup_status returns {pending: [], completed: [...], failed: []}",
|
||||||
|
"NO `import X` statements inside function bodies for heavy modules (grep-verified)",
|
||||||
"No regressions in 273+ existing tests",
|
"No regressions in 273+ existing tests",
|
||||||
"ZERO new threading.Thread(...) calls in src/ (after Phase 6 migration)",
|
"ZERO new threading.Thread(...) calls in src/ (after Phase 6 migration)",
|
||||||
"Startup profile + io_pool status visible via /api/startup_profile and /api/io_pool_status"
|
"Startup profile + io_pool status visible via /api/startup_profile, /api/io_pool_status"
|
||||||
],
|
],
|
||||||
"links": {
|
"links": {
|
||||||
"backlog_entry": "conductor/tracks.md:152",
|
"backlog_entry": "conductor/tracks.md:152",
|
||||||
|
|||||||
@@ -19,11 +19,15 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 2: Job Pool Foundation (the "no new threads" rule)
|
## Phase 2: Job Pool + Warmup Foundation (the "no new threads" + "no lazy-loading" rules)
|
||||||
|
|
||||||
The user constraint: no new `threading.Thread(...)` per task, per import, per
|
Two user constraints, addressed together:
|
||||||
ad-hoc job. The codebase gets ONE shared `ThreadPoolExecutor` on `AppController`,
|
1. **No new `threading.Thread(...)`** per task, per import, per ad-hoc job.
|
||||||
named `_io_pool`, used by any subsystem that needs background work.
|
2. **No lazy-loading** in function bodies. Heavy imports are warmed on bg
|
||||||
|
threads at startup, not loaded on first use.
|
||||||
|
|
||||||
|
The codebase gets ONE shared `ThreadPoolExecutor` on `AppController` named
|
||||||
|
`_io_pool`, used for warmup AND any future background work.
|
||||||
|
|
||||||
- [ ] **T2.1 (Red)** `tests/test_app_controller_io_pool.py`:
|
- [ ] **T2.1 (Red)** `tests/test_app_controller_io_pool.py`:
|
||||||
- `test_app_controller_has_io_pool`: instantiate `AppController`, assert `hasattr(controller, '_io_pool')` and it's a `ThreadPoolExecutor`
|
- `test_app_controller_has_io_pool`: instantiate `AppController`, assert `hasattr(controller, '_io_pool')` and it's a `ThreadPoolExecutor`
|
||||||
@@ -32,81 +36,94 @@ named `_io_pool`, used by any subsystem that needs background work.
|
|||||||
- `test_io_pool_shuts_down_on_close`: call `controller.shutdown()`, assert the pool is shut down
|
- `test_io_pool_shuts_down_on_close`: call `controller.shutdown()`, assert the pool is shut down
|
||||||
- Confirm FAIL (no `_io_pool` yet)
|
- Confirm FAIL (no `_io_pool` yet)
|
||||||
- [ ] **T2.2 (Green)** In `src/app_controller.py`:
|
- [ ] **T2.2 (Green)** In `src/app_controller.py`:
|
||||||
- Add `from concurrent.futures import ThreadPoolExecutor` at top
|
- Add `from concurrent.futures import ThreadPoolExecutor` and `import importlib` at top
|
||||||
- In `__init__`, after the asyncio loop starts and BEFORE the existing HookServer block: `self._io_pool = ThreadPoolExecutor(max_workers=4, thread_name_prefix="controller-io")`
|
- In `__init__`, after the asyncio loop starts and BEFORE the existing HookServer block: `self._io_pool = ThreadPoolExecutor(max_workers=4, thread_name_prefix="controller-io")`
|
||||||
|
- Add warmup state: `self._warmup_lock`, `self._warmup_done_event`, `self._warmup_status` (dict with `pending`/`completed`/`failed` lists), `self._warmup_callbacks`
|
||||||
|
- Call `self._submit_warmup_jobs()` at the end of `__init__`
|
||||||
- In `shutdown()` (already exists in `App.shutdown` for the GUI; ensure the AppController has a matching shutdown that calls `self._io_pool.shutdown(wait=False)`)
|
- In `shutdown()` (already exists in `App.shutdown` for the GUI; ensure the AppController has a matching shutdown that calls `self._io_pool.shutdown(wait=False)`)
|
||||||
- Add `controller.submit_io(fn, *args)` helper: `return self._io_pool.submit(fn, *args)` (with a docstring saying "use this instead of `threading.Thread` for new background work")
|
- [ ] **T2.3 (Red)** `tests/test_warmup_mechanism.py`:
|
||||||
- [ ] **T2.3** Run T2.1 tests; confirm PASS
|
- `test_warmup_jobs_submitted_on_init`: after `AppController.__init__`, assert `len(controller.warmup_status()['pending']) > 0`
|
||||||
- [ ] **T2.4** Commit: `feat(app_controller): add shared _io_pool ThreadPoolExecutor` + git note
|
- `test_warmup_jobs_complete_within_timeout`: call `controller.wait_for_warmup(timeout=10.0)`, assert True
|
||||||
|
- `test_warmup_status_reflects_completion`: after `wait_for_warmup`, assert `controller.is_warmup_done() == True` and `len(warmup_status()['pending']) == 0`
|
||||||
|
- `test_warmup_callback_fires_on_completion`: register a callback via `controller.on_warmup_complete(cb)`, assert it was called once warmup done
|
||||||
|
- `test_warmup_does_not_block_init`: time `__init__` with a 4-worker pool, assert it returns in < 200ms even though warmup takes longer
|
||||||
|
- Confirm FAIL (no warmup yet)
|
||||||
|
- [ ] **T2.4 (Green)** Implement `_submit_warmup_jobs()`, `_compute_warmup_list()`, `_warmup_one()`, `warmup_status()`, `is_warmup_done()`, `wait_for_warmup()`, `on_warmup_complete()` per spec §3.2. Warmup list includes: `google.genai`, `anthropic`, `openai`, `requests`, `src.command_palette`, `src.theme_nerv`, `src.theme_nerv_fx`, `src.markdown_table`, `numpy`. Conditionally adds `fastapi`, `fastapi.security.api_key` if `enable_test_hooks` or `web_host` is set.
|
||||||
|
- [ ] **T2.5** Run T2.1 and T2.3 tests; confirm PASS
|
||||||
|
- [ ] **T2.6** Commit: `feat(app_controller): add _io_pool + proactive warmup mechanism` + git note
|
||||||
|
|
||||||
**Phase 2 checkpoint:** `AppController` owns a 4-thread named pool. `controller.submit_io(fn)` is the sanctioned way to do background work. Existing ad-hoc threads still exist (will be migrated in Phase 5).
|
**Phase 2 checkpoint:** `AppController` owns a 4-thread named pool. Warmup jobs are submitted in `__init__` and complete in the background. `controller.wait_for_warmup()`, `controller.warmup_status()`, and `controller.on_warmup_complete(cb)` are the public API. Main thread does NOT block waiting for warmup.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 3: Lazy-load AI Provider SDKs (TDD)
|
## Phase 3: Remove top-level heavy imports from `src/ai_client.py` (TDD)
|
||||||
|
|
||||||
- [ ] **T3.1 (Red)** Write `tests/test_ai_client_lazy_imports.py`:
|
The current `src/ai_client.py` has `from google import genai` etc. at the top,
|
||||||
- `test_ai_client_does_not_import_genai_at_module_level`: spawn fresh subprocess, `import src.ai_client`, assert `'google.genai' not in sys.modules` (or `google.genai` in modules but `_gemini_client` is `None`)
|
which puts the main thread in the import chain. Phase 3 removes these and
|
||||||
|
swaps to `_require_warmed(name)`.
|
||||||
|
|
||||||
|
- [ ] **T3.1 (Red)** Write `tests/test_ai_client_no_top_level_sdk_imports.py`:
|
||||||
|
- `test_ai_client_does_not_import_genai_at_module_level`: spawn fresh subprocess, `import src.ai_client`, assert `'google.genai' not in sys.modules` (warmup hasn't run in this subprocess)
|
||||||
- `test_ai_client_does_not_import_anthropic_at_module_level`
|
- `test_ai_client_does_not_import_anthropic_at_module_level`
|
||||||
- `test_ai_client_does_not_import_openai_at_module_level`
|
- `test_ai_client_does_not_import_openai_at_module_level`
|
||||||
- `test_ai_client_does_not_import_requests_at_module_level`
|
- `test_ai_client_does_not_import_requests_at_module_level`
|
||||||
- Confirm tests FAIL (proves the imports are currently eager)
|
- Confirm tests FAIL (proves the imports are currently eager)
|
||||||
- [ ] **T3.2 (Green)** In `src/ai_client.py`:
|
- [ ] **T3.2 (Green)** In `src/ai_client.py`:
|
||||||
- Remove `from google import genai` from top
|
- Add `import sys, importlib, threading` at top
|
||||||
- Remove `import anthropic` from top
|
- Remove `from google import genai`, `import anthropic`, `import openai`, `import requests` from top
|
||||||
- Remove `import openai` from top
|
- Add `_require_warmed(name)` helper: returns `sys.modules[name]` or raises `RuntimeError`
|
||||||
- Remove `import requests` from top
|
- Each `_send_*` function calls `_require_warmed("google.genai")` etc. instead of using the module directly
|
||||||
- Add lazy imports inside `_send_gemini`, `_send_anthropic`, `_send_deepseek`, `_send_minimax`
|
- Provider client globals stay as `None` until first `_send_*` initializes them via `_ensure_<provider>_client()` (extracted from current top-level logic, uses the warmed module)
|
||||||
- Provider client globals stay as `None` until first `_ensure_<provider>_client()` call
|
- [ ] **T3.3** Run existing `tests/test_ai_client.py`; fix any breakage. Tests that relied on top-level import side effects need a fixture that warms the modules (or a fallback for test mode).
|
||||||
- [ ] **T3.3** Run existing `tests/test_ai_client.py`; fix any breakage. Most likely issue: tests that rely on top-level import side effects need a fixture that triggers lazy init.
|
|
||||||
- [ ] **T3.4** Re-run T3.1 tests, confirm PASS
|
- [ ] **T3.4** Re-run T3.1 tests, confirm PASS
|
||||||
- [ ] **T3.5** Commit: `git commit -m "refactor(ai_client): lazy-load provider SDKs to defer ~1800ms off main thread"` + git note
|
- [ ] **T3.5** Commit: `refactor(ai_client): remove top-level SDK imports; use _require_warmed` + git note
|
||||||
- [ ] **T3.6** Update `conductor/tracks.md` T3 row with SHA
|
- [ ] **T3.6** Update `conductor/tracks.md` T3 row with SHA
|
||||||
|
|
||||||
**Phase 3 checkpoint:** `import src.ai_client` < 50ms cold. All 273 existing tests still pass.
|
**Phase 3 checkpoint:** `import src.ai_client` < 50ms cold. When run inside an `AppController` whose warmup has completed, `_send_*` functions find the SDKs in `sys.modules` and execute instantly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 4: Lazy-load FastAPI in HookServer (TDD)
|
## Phase 4: Remove top-level FastAPI imports from `src/api_hooks.py` (TDD)
|
||||||
|
|
||||||
- [ ] **T4.1 (Red)** Write `tests/test_hook_server_lazy_fastapi.py`:
|
Same pattern as Phase 3, for the FastAPI imports.
|
||||||
|
|
||||||
|
- [ ] **T4.1 (Red)** Write `tests/test_hook_server_no_top_level_fastapi.py`:
|
||||||
- `test_hook_server_does_not_import_fastapi_at_module_level`: subprocess test
|
- `test_hook_server_does_not_import_fastapi_at_module_level`: subprocess test
|
||||||
- `test_hook_server_does_not_import_fastapi_security_at_module_level`
|
- `test_hook_server_does_not_import_fastapi_security_at_module_level`
|
||||||
- Confirm FAIL
|
- Confirm FAIL
|
||||||
- [ ] **T4.2 (Green)** In `src/api_hooks.py`:
|
- [ ] **T4.2 (Green)** In `src/api_hooks.py`:
|
||||||
- Remove `from fastapi import ...` from top
|
- Remove `from fastapi import ...`, `from fastapi.security.api_key import ...` from top
|
||||||
- Remove `from fastapi.security.api_key import APIKeyHeader` from top
|
- Add `_require_warmed(name)` calls inside the methods that need them (FastAPI app construction, route registration)
|
||||||
- Add lazy imports inside the methods that need them (FastAPI app construction, route registration)
|
- [ ] **T4.3** Run existing `tests/test_api_hooks.py`; fix breakage (similar fallback strategy as Phase 3)
|
||||||
- [ ] **T4.3** Run existing `tests/test_api_hooks.py`; fix breakage
|
|
||||||
- [ ] **T4.4** Confirm T4.1 tests PASS
|
- [ ] **T4.4** Confirm T4.1 tests PASS
|
||||||
- [ ] **T4.5** Commit: `git commit -m "refactor(api_hooks): lazy-load fastapi to defer ~470ms off main thread"` + git note
|
- [ ] **T4.5** Commit: `refactor(api_hooks): remove top-level fastapi imports; use _require_warmed` + git note
|
||||||
|
|
||||||
**Phase 4 checkpoint:** `from src.api_hooks import HookServer` does not import fastapi.
|
**Phase 4 checkpoint:** `from src.api_hooks import HookServer` does not import fastapi. The HookServer is fully constructed only after AppController's warmup has loaded fastapi (or after `_require_warmed("fastapi")` triggers the import in test mode).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 5: Lazy-load Feature-gated GUI Modules (TDD per module)
|
## Phase 5: Remove top-level imports for feature-gated GUI modules (TDD per module)
|
||||||
|
|
||||||
### 5A: Command Palette
|
### 5A: Command Palette
|
||||||
|
|
||||||
- [ ] **T5A.1 (Red)** `tests/test_command_palette_lazy.py`: `from src.commands import COMMANDS` (or whatever the eager import is) does not import `src.command_palette`. Confirm FAIL.
|
- [ ] **T5A.1 (Red)** `tests/test_command_palette_no_top_level_import.py`: `from src.commands import COMMANDS` does not import `src.command_palette`. Confirm FAIL.
|
||||||
- [ ] **T5A.2 (Green)** In `src/commands.py`: move `from src.command_palette import ...` inside the command functions that open the palette (`_open_command_palette`, `_toggle_command_palette`).
|
- [ ] **T5A.2 (Green)** In `src/commands.py`: remove `from src.command_palette import ...` from top. The command functions (`_open_command_palette`, `_toggle_command_palette`) call `_require_warmed("src.command_palette")` to access the module.
|
||||||
- [ ] **T5A.3** Run `tests/test_command_palette.py`; fix.
|
- [ ] **T5A.3** Run `tests/test_command_palette.py`; fix.
|
||||||
- [ ] **T5A.4** Commit: `refactor(commands): lazy-load command_palette to defer 244ms`
|
- [ ] **T5A.4** Commit: `refactor(commands): remove top-level command_palette import; use _require_warmed`
|
||||||
|
|
||||||
### 5B: NERV Theme
|
### 5B: NERV Theme
|
||||||
|
|
||||||
- [ ] **T5B.1 (Red)** `tests/test_theme_nerv_lazy.py`: `from src.theme_2 import *` (or whatever) does not import `src.theme_nerv` or `src.theme_nerv_fx`. Confirm FAIL.
|
- [ ] **T5B.1 (Red)** `tests/test_theme_nerv_no_top_level_import.py`: `from src.theme_2 import *` does not import `src.theme_nerv` or `src.theme_nerv_fx`. Confirm FAIL.
|
||||||
- [ ] **T5B.2 (Green)** In `src/theme_2.py`: move `from src.theme_nerv import ...` and `from src.theme_nerv_fx import ...` inside `apply_nerv_theme()` (or whichever function activates the theme).
|
- [ ] **T5B.2 (Green)** In `src/theme_2.py`: remove `from src.theme_nerv import ...` and `from src.theme_nerv_fx import ...` from top. `apply_nerv_theme()` (or whichever function activates the theme) calls `_require_warmed("src.theme_nerv")` and `_require_warmed("src.theme_nerv_fx")`.
|
||||||
- [ ] **T5B.3** Run `tests/test_theme_2.py` and `tests/test_theme_nerv.py`; fix.
|
- [ ] **T5B.3** Run `tests/test_theme_2.py` and `tests/test_theme_nerv.py`; fix.
|
||||||
- [ ] **T5B.4** Commit: `refactor(theme): lazy-load nerv theme to defer 485ms off non-nerv path`
|
- [ ] **T5B.4** Commit: `refactor(theme): remove top-level nerv theme imports; use _require_warmed`
|
||||||
|
|
||||||
### 5C: Markdown Table
|
### 5C: Markdown Table
|
||||||
|
|
||||||
- [ ] **T5C.1 (Red)** `tests/test_markdown_helper_lazy.py`: `from src.markdown_helper import MarkdownRenderer` does not import `src.markdown_table`. Confirm FAIL.
|
- [ ] **T5C.1 (Red)** `tests/test_markdown_helper_no_top_level_import.py`: `from src.markdown_helper import MarkdownRenderer` does not import `src.markdown_table`. Confirm FAIL.
|
||||||
- [ ] **T5C.2 (Green)** In `src/markdown_helper.py`: move `from src.markdown_table import ...` inside the table-detection branch of `render()`.
|
- [ ] **T5C.2 (Green)** In `src/markdown_helper.py`: remove `from src.markdown_table import ...` from top. The table-detection branch of `render()` calls `_require_warmed("src.markdown_table")`.
|
||||||
- [ ] **T5C.3** Run `tests/test_markdown_helper.py`; fix.
|
- [ ] **T5C.3** Run `tests/test_markdown_helper.py`; fix.
|
||||||
- [ ] **T5C.4** Commit: `refactor(markdown): lazy-load markdown_table to defer 250ms off non-table markdown`
|
- [ ] **T5C.4** Commit: `refactor(markdown): remove top-level markdown_table import; use _require_warmed`
|
||||||
|
|
||||||
### 5D: GUI module feature-gated imports
|
### 5D: GUI module feature-gated imports
|
||||||
|
|
||||||
@@ -115,16 +132,14 @@ named `_io_pool`, used by any subsystem that needs background work.
|
|||||||
- [ ] **T5D.3** Run full GUI test suite; fix.
|
- [ ] **T5D.3** Run full GUI test suite; fix.
|
||||||
- [ ] **T5D.4** Commit per feature group
|
- [ ] **T5D.4** Commit per feature group
|
||||||
|
|
||||||
**Phase 5 checkpoint:** Feature-gated imports are lazy. Default-theme / non-palette / non-table path is lean.
|
**Phase 5 checkpoint:** All heavy imports removed from main-thread-reachable source files. Default-theme / non-palette / non-table path is lean. Warmup pre-loads all of them in the background.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 6: Migrate Ad-hoc Threads to `_io_pool`
|
## Phase 6: Migrate Ad-hoc Threads to `_io_pool`
|
||||||
|
|
||||||
The codebase has several ad-hoc `threading.Thread(...)` calls. Per the user
|
The codebase has several ad-hoc `threading.Thread(...)` calls. Per the user
|
||||||
constraint, these should migrate to `controller.submit_io(fn)`. **This phase
|
constraint, these should migrate to `controller.submit_io(fn)`.
|
||||||
audits and migrates them, but does NOT add new prefetch threads** (the heavy
|
|
||||||
SDKs are lazy-only per spec §2.2 Layer 3).
|
|
||||||
|
|
||||||
- [ ] **T6.1** Audit: `grep -rn "threading.Thread(" src/` to find all ad-hoc thread spawns. Document each in `state.toml` (a new `[ad_hoc_threads]` section).
|
- [ ] **T6.1** Audit: `grep -rn "threading.Thread(" src/` to find all ad-hoc thread spawns. Document each in `state.toml` (a new `[ad_hoc_threads]` section).
|
||||||
- [ ] **T6.2** For each ad-hoc thread in `src/log_pruner.py`, `src/project_manager.py`, etc., refactor to use `controller.submit_io(fn)` instead. Wrap the callable body in a try/except (the pool's default behavior is to surface exceptions via the Future; preserve existing error logging).
|
- [ ] **T6.2** For each ad-hoc thread in `src/log_pruner.py`, `src/project_manager.py`, etc., refactor to use `controller.submit_io(fn)` instead. Wrap the callable body in a try/except (the pool's default behavior is to surface exceptions via the Future; preserve existing error logging).
|
||||||
@@ -135,36 +150,55 @@ SDKs are lazy-only per spec §2.2 Layer 3).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 7: Enforcement (Runtime Audit Hook)
|
## Phase 7: Warmup Notification (Hook API + GUI)
|
||||||
|
|
||||||
|
The user said: *"the app controller should post to test clients or the user
|
||||||
|
when its threads are warmed up with imports — that way the user knows 'hey
|
||||||
|
you have the ui first, but now you have all the functionality.'"* This phase
|
||||||
|
implements the notification surfaces.
|
||||||
|
|
||||||
|
### 7A: Hook API endpoints
|
||||||
|
|
||||||
|
- [ ] **T7A.1 (Red)** `tests/test_api_hooks_warmup.py`:
|
||||||
|
- `test_warmup_status_endpoint`: hit `GET /api/warmup_status`, assert response has `pending`/`completed`/`failed` keys
|
||||||
|
- `test_warmup_wait_endpoint`: hit `GET /api/warmup_wait?timeout=10`, assert response includes the completion state
|
||||||
|
- Confirm FAIL (endpoints don't exist yet)
|
||||||
|
- [ ] **T7A.2 (Green)** In `src/api_hooks.py`:
|
||||||
|
- Add `GET /api/warmup_status` returning `controller.warmup_status()`
|
||||||
|
- Add `GET /api/warmup_wait` accepting `?timeout=N` (default 30s), calling `controller.wait_for_warmup(timeout)` then returning the final status
|
||||||
|
- Register `warmup_status` in `_gettable_fields` so the existing Hook API client can fetch it
|
||||||
|
- [ ] **T7A.3** Run T7A.1 tests; confirm PASS
|
||||||
|
- [ ] **T7A.4** Commit: `feat(api_hooks): add /api/warmup_status and /api/warmup_wait` + git note
|
||||||
|
|
||||||
|
### 7B: GUI status indicator + toast
|
||||||
|
|
||||||
|
- [ ] **T7B.1** In `src/gui_2.py` (in the status bar render function), poll `controller.warmup_status()` once per frame. While `pending` is non-empty: show "Warming up... (N/M)" text. When `pending` is empty AND `failed` is empty: show "All imports ready" with a green dot. When `failed` is non-empty: show "Imports: N failed" with a yellow dot.
|
||||||
|
- [ ] **T7B.2** Register a callback via `controller.on_warmup_complete(cb)` that:
|
||||||
|
- On transition to done (with no failures): queue a toast notification "All providers ready (M modules)" via the existing toast system
|
||||||
|
- On transition to done (with failures): queue a warning toast "Warmup finished with N failures — see Diagnostics"
|
||||||
|
- [ ] **T7B.3** Update `docs/guide_gui_2.md` (or wherever status bar is documented) to describe the new indicator
|
||||||
|
- [ ] **T7B.4** Commit: `feat(gui_2): warmup status indicator + completion toast` + git note
|
||||||
|
|
||||||
|
**Phase 7 checkpoint:** Tests can poll `/api/warmup_status` to know when the system is fully ready. The GUI shows progress during startup and a toast when complete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: Enforcement (Runtime Audit Hook)
|
||||||
|
|
||||||
The static gate (T1.4) catches known imports at audit time. This phase adds
|
The static gate (T1.4) catches known imports at audit time. This phase adds
|
||||||
empirical enforcement: a test that spawns `sloppy.py` and verifies NO heavy
|
empirical enforcement: a test that spawns `sloppy.py` and verifies NO heavy
|
||||||
import happens on the main thread at runtime.
|
import happens on the main thread at runtime.
|
||||||
|
|
||||||
- [ ] **T7.1 (Red)** `tests/test_main_thread_purity.py`:
|
- [ ] **T8.1 (Red)** `tests/test_main_thread_purity.py`:
|
||||||
- `test_headless_startup_no_heavy_imports_on_main`: spawn `uv run python sloppy.py --headless --enable-test-hooks` with a `sitecustomize.py` shim that installs `sys.addaudithook` to log every `import` event with the calling thread. The hook writes to a temp file as JSON-L.
|
- `test_headless_startup_no_heavy_imports_on_main`: spawn `uv run python sloppy.py --headless --enable-test-hooks` with a `sitecustomize.py` shim that installs `sys.addaudithook` to log every `import` event with the calling thread. The hook writes to a temp file as JSON-L.
|
||||||
- Wait for headless server ready (5s timeout via `ApiHookClient`).
|
- Wait for headless server ready (5s timeout via `ApiHookClient`).
|
||||||
- Read the audit log. Assert: no event with `thread_name == "MainThread"` for any module in the heavy denylist (`google.genai`, `anthropic`, `openai`, `fastapi`, `requests`, `numpy`, `tkinter`, `psutil`, `pydantic`, `tree_sitter_*`, `src.command_palette`, `src.theme_nerv`, `src.theme_nerv_fx`, `src.markdown_table`, `src.ai_client.send_*`-direct).
|
- Read the audit log. Assert: no event with `thread_name == "MainThread"` for any module in the heavy denylist (`google.genai`, `anthropic`, `openai`, `fastapi`, `requests`, `numpy`, `tkinter`, `psutil`, `pydantic`, `tree_sitter_*`, `src.command_palette`, `src.theme_nerv`, `src.theme_nerv_fx`, `src.markdown_table`).
|
||||||
- Kill subprocess. Confirm FAIL (current state imports these on main).
|
- Kill subprocess. Confirm FAIL (current state imports these on main).
|
||||||
- [ ] **T7.2** Once Phase 3-5 land and the static gate passes, this test should start passing. If it doesn't, debug and add more lazy imports.
|
- [ ] **T8.2** Once Phase 3-5 land and the static gate passes, this test should start passing. If it doesn't, debug and add more top-level import removals.
|
||||||
- [ ] **T7.3** Wire `test_main_thread_purity.py` into CI as a gating test (it'll be slow, ~10s, so mark with `@pytest.mark.slow` and only run in batched CI).
|
- [ ] **T8.3** Wire `test_main_thread_purity.py` into CI as a gating test (it'll be slow, ~10s, so mark with `@pytest.mark.slow` and only run in batched CI).
|
||||||
- [ ] **T7.4** Commit: `test: empirical main-thread purity check via sys.audit hook` + git note
|
- [ ] **T8.4** Commit: `test: empirical main-thread purity check via sys.audit hook` + git note
|
||||||
|
|
||||||
**Phase 7 checkpoint:** CI fails if a future commit re-introduces a heavy main-thread import.
|
**Phase 8 checkpoint:** CI fails if a future commit re-introduces a heavy main-thread import.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 8: Hook API + Diagnostics
|
|
||||||
|
|
||||||
- [ ] **T8.1** Add `/api/startup_profile` endpoint in `src/api_hooks.py` returning `controller.startup_profiler.snapshot()`
|
|
||||||
- [ ] **T8.2** Register `startup_profile` in `_gettable_fields`
|
|
||||||
- [ ] **T8.3** Add a "Startup Profile" section to the Diagnostics panel (`src/gui_2.py` `_render_diagnostics` or similar). Show: phase name, duration, % of total.
|
|
||||||
- [ ] **T8.4** Add `/api/io_pool_status` endpoint returning `{max_workers, active_threads, queued, completed}` so the user can see the job pool is alive.
|
|
||||||
- [ ] **T8.5** Update `docs/guide_api_hooks.md` with both new endpoints.
|
|
||||||
- [ ] **T8.6** Tests: extend `tests/test_api_hooks.py` + new `tests/test_startup_profiler.py` + new `tests/test_io_pool_endpoint.py`.
|
|
||||||
- [ ] **T8.7** Commit: `feat(diagnostics): expose startup profile and io_pool status via Hook API` + git note
|
|
||||||
|
|
||||||
**Phase 8 checkpoint:** User can see per-phase startup cost + job-pool liveness in the GUI.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -175,30 +209,36 @@ import happens on the main thread at runtime.
|
|||||||
- `import src.gui_2` < 500ms
|
- `import src.gui_2` < 500ms
|
||||||
- `import src.app_controller` < 300ms (includes `_io_pool` creation; should still be < 300ms)
|
- `import src.app_controller` < 300ms (includes `_io_pool` creation; should still be < 300ms)
|
||||||
- [ ] **T9.2** Re-run `scripts/audit_main_thread_imports.py` (T1.4). Confirm exit 0. No violations.
|
- [ ] **T9.2** Re-run `scripts/audit_main_thread_imports.py` (T1.4). Confirm exit 0. No violations.
|
||||||
- [ ] **T9.3** Run `live_gui` test batch (per `conductor/workflow.md:147-150`: max 4 test files per batch, long timeout):
|
- [ ] **T9.3** Run `tests/test_warmup_mechanism.py` (T2.3); confirm warmup completes and notifications fire
|
||||||
|
- [ ] **T9.4** Run `live_gui` test batch (per `conductor/workflow.md:147-150`: max 4 test files per batch, long timeout):
|
||||||
- `uv run pytest tests/test_live_gui_*.py --timeout=60 -v` in batches
|
- `uv run pytest tests/test_live_gui_*.py --timeout=60 -v` in batches
|
||||||
- Confirm `wait_for_server(timeout=15)` does not time out
|
- Confirm `wait_for_server(timeout=15)` does not time out
|
||||||
- [ ] **T9.4** Manual smoke:
|
- Optionally: tests can call `controller.wait_for_warmup()` before exercising functionality that depends on warmed modules
|
||||||
- `uv run sloppy.py` (normal mode): time-to-first-frame
|
- [ ] **T9.5** Manual smoke:
|
||||||
- `uv run sloppy.py --enable-test-hooks` (test mode): time-to-first-frame
|
- `uv run sloppy.py` (normal mode): time-to-first-frame, observe "Warming up... (N/M)" status, then "All imports ready" toast
|
||||||
|
- `uv run sloppy.py --enable-test-hooks` (test mode): same observations, plus `/api/warmup_status` returns `completed`
|
||||||
- `uv run sloppy.py --headless` (headless): time-to-server-ready
|
- `uv run sloppy.py --headless` (headless): time-to-server-ready
|
||||||
- [ ] **T9.5** Phase checkpoint commit: `conductor(checkpoint): Phase 9 complete - sloppy.py startup speedup track` + git note with full verification report
|
- Verify a user action that switches provider (or other warmup-dependent operation) is INSTANT, not 1s-delayed
|
||||||
- [ ] **T9.6** Update `conductor/tracks.md`: mark track complete, link to archived folder
|
- [ ] **T9.6** Phase checkpoint commit: `conductor(checkpoint): Phase 9 complete - sloppy.py startup speedup track` + git note with full verification report
|
||||||
|
- [ ] **T9.7** Update `conductor/tracks.md`: mark track complete, link to archived folder
|
||||||
|
|
||||||
**Phase 9 checkpoint:** All verification criteria in `spec.md:6` met.
|
**Phase 9 checkpoint:** All verification criteria in `spec.md:6` met. User can switch providers with zero perceptible lag because warmup already loaded the SDK.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
|
|
||||||
- [ ] All Phase 1-9 tasks checked
|
- [ ] All Phase 1-9 tasks checked
|
||||||
- [ ] All tests pass (273+ existing + new TDD tests including `test_main_thread_purity`)
|
- [ ] All tests pass (273+ existing + new TDD tests including `test_main_thread_purity` and `test_warmup_mechanism`)
|
||||||
- [ ] `uv run ruff check .` and `uv run mypy --explicit-package-bases .` clean (per `mma-tier2-tech-lead` skill)
|
- [ ] `uv run ruff check .` and `uv run mypy --explicit-package-bases .` clean (per `mma-tier2-tech-lead` skill)
|
||||||
- [ ] `uv run python scripts/audit_main_thread_imports.py` exits 0
|
- [ ] `uv run python scripts/audit_main_thread_imports.py` exits 0
|
||||||
- [ ] `docs/startup_baseline_20260606.txt` and `docs/startup_after_20260606.txt` archived
|
- [ ] `docs/startup_baseline_20260606.txt` and `docs/startup_after_20260606.txt` archived
|
||||||
- [ ] Phase 9 git note contains: baseline diff, audit script result, runtime audit hook result, full test batch results, manual smoke timings, file inventory
|
- [ ] Phase 9 git note contains: baseline diff, audit script result, runtime audit hook result, full test batch results, manual smoke timings, file inventory
|
||||||
- [ ] Track moved to `conductor/tracks/archive/`
|
- [ ] Track moved to `conductor/tracks/archive/`
|
||||||
- [ ] **NO new `threading.Thread(...)` calls in `src/`** (verified by `grep -rn "threading.Thread(" src/`)
|
- [ ] **NO new `threading.Thread(...)` calls in `src/`** (verified by `grep -rn "threading.Thread(" src/`)
|
||||||
|
- [ ] **NO `import X` statements in function bodies for heavy modules** — verified by `grep -rn "^\s*import \(google\|anthropic\|openai\|fastapi\|src\.command_palette\|src\.theme_nerv\|src\.markdown_table\)" src/`
|
||||||
|
- [ ] **Warmup completion notification works** — GUI shows toast, Hook API returns `completed`, `controller.is_warmup_done()` returns True within 10s of startup
|
||||||
|
- [ ] **User action latency is zero for warmup-dependent operations** — manual smoke test switching providers / opening palette / rendering NERV is instant
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -88,9 +88,11 @@ mechanism that does not run on the main thread.
|
|||||||
|
|
||||||
### 2.2 Four layers of protection
|
### 2.2 Four layers of protection
|
||||||
|
|
||||||
#### Layer 1 — Pure lazy loading (the load-bearing wall, non-negotiable)
|
#### Layer 1 — Explicit warmup-aware module access (the load-bearing wall, non-negotiable)
|
||||||
|
|
||||||
Move heavy imports from module top-level into the function body that needs them:
|
Remove heavy imports from the top of source files reachable from the main
|
||||||
|
thread. Functions that need them use a `_require_warmed(name)` helper that
|
||||||
|
assumes the module is already in `sys.modules` (because warmup put it there):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# BEFORE (src/ai_client.py, current)
|
# BEFORE (src/ai_client.py, current)
|
||||||
@@ -100,18 +102,44 @@ import openai
|
|||||||
# ... 5 provider SDKs loaded unconditionally
|
# ... 5 provider SDKs loaded unconditionally
|
||||||
|
|
||||||
# AFTER
|
# AFTER
|
||||||
def _send_gemini(md_content, user_message, ...):
|
import sys
|
||||||
from google import genai # 955ms, paid once, on the first call's thread
|
import importlib
|
||||||
...
|
from typing import Any
|
||||||
|
|
||||||
def _send_anthropic(...):
|
def _require_warmed(name: str) -> Any:
|
||||||
import anthropic
|
"""Get a module that AppController's warmup should have loaded.
|
||||||
...
|
|
||||||
|
Raises RuntimeError if the module is not in sys.modules. This is the
|
||||||
|
explicit contract: heavy modules MUST be warmed at startup. No lazy
|
||||||
|
loading on first use — the import is paid upfront on a bg thread.
|
||||||
|
"""
|
||||||
|
mod = sys.modules.get(name)
|
||||||
|
if mod is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Module {name!r} is not warmed. "
|
||||||
|
f"AppController.__init__ must have run first (which submits warmup jobs)."
|
||||||
|
)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
def _send_gemini(md_content, user_message, ...):
|
||||||
|
genai = _require_warmed("google.genai")
|
||||||
|
# ... use genai ...
|
||||||
```
|
```
|
||||||
|
|
||||||
**Main-thread cost: zero.** First call still pays the latency, but it happens on
|
**Why no `import X` inside the function body?** Because that would be lazy
|
||||||
the asyncio worker thread (per `guide_architecture.md:215-234`), so the GUI never
|
loading on first use. If the first use is triggered by a user UI action
|
||||||
sees it.
|
(e.g. switching the provider from MiniMax to Gemini, the controller enqueues
|
||||||
|
an action that propagates to the first call), the user sees a 955ms lag
|
||||||
|
between their click and any visible response. That's the bad case the user
|
||||||
|
called out: *"lazy loading introduces latencies when interacting with the UI
|
||||||
|
state vs the bg state."*
|
||||||
|
|
||||||
|
By warming proactively, the first user-triggered call is instant. The cost
|
||||||
|
is paid during startup on a bg thread, before the user can interact.
|
||||||
|
|
||||||
|
**Main-thread cost: zero.** The main thread's import chain is fully lean
|
||||||
|
(none of the heavy modules are imported top-level). The warmup jobs run on
|
||||||
|
`_io_pool` workers in parallel with the main thread's remaining init.
|
||||||
|
|
||||||
#### Layer 2 — Shared job pool on AppController (no new threads per task)
|
#### Layer 2 — Shared job pool on AppController (no new threads per task)
|
||||||
|
|
||||||
@@ -124,22 +152,22 @@ The codebase already has these dedicated / shared threads:
|
|||||||
- Ad-hoc `threading.Thread` calls — used for one-off tasks; the user wants to
|
- Ad-hoc `threading.Thread` calls — used for one-off tasks; the user wants to
|
||||||
**MINIMIZE** these
|
**MINIMIZE** these
|
||||||
|
|
||||||
**User constraint:** no new daemon threads per import prefetch, per I/O task, per
|
**User constraint:** no new daemon threads per import warmup, per I/O task, per
|
||||||
log-prune. We add ONE shared `ThreadPoolExecutor` to `AppController` named
|
log-prune. We add ONE shared `ThreadPoolExecutor` to `AppController` named
|
||||||
`_io_pool`, and any subsystem that needs background work submits jobs to it.
|
`_io_pool`, and any subsystem that needs background work submits jobs to it.
|
||||||
This includes:
|
This includes:
|
||||||
- Initial RAG index warm-up (if applicable)
|
- Initial RAG index warm-up (if applicable)
|
||||||
- Log pruning (currently a one-shot thread — refactor to use the pool)
|
- Log pruning (currently a one-shot thread — refactor to use the pool)
|
||||||
- Disk-bound subsystem initialization (e.g., TOML re-read on persona switch)
|
- Disk-bound subsystem initialization (e.g., TOML re-read on persona switch)
|
||||||
- Any other ad-hoc I/O
|
- **Heavy module warmup (the primary use case for this track)**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# In AppController.__init__
|
# In AppController.__init__
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
self._io_pool = ThreadPoolExecutor(
|
self._io_pool = ThreadPoolExecutor(
|
||||||
max_workers=4,
|
max_workers=4,
|
||||||
thread_name_prefix="controller-io",
|
thread_name_prefix="controller-io",
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -147,25 +175,95 @@ self._io_pool = ThreadPoolExecutor(
|
|||||||
import, not 1 per subsystem. Just 4 long-lived threads that all background work
|
import, not 1 per subsystem. Just 4 long-lived threads that all background work
|
||||||
shares. Future work that needs a bg thread should `controller._io_pool.submit(fn)`.
|
shares. Future work that needs a bg thread should `controller._io_pool.submit(fn)`.
|
||||||
|
|
||||||
#### Layer 3 — NO prefetch of the heaviest SDKs (deliberate)
|
#### Layer 3 — Proactive warmup + completion notification (the new mechanism)
|
||||||
|
|
||||||
The original Phase 5 of this plan proposed a `import-prefetch` daemon thread that
|
This is the core of the track. In `AppController.__init__`, immediately after
|
||||||
warms `google.genai` (~955ms) on a background thread. **This has been explicitly
|
`_io_pool` is created, the controller submits a job to the pool for each heavy
|
||||||
rejected** for the heavy SDKs, and the reasoning is sound:
|
module that needs warming. The main thread does NOT wait for these to complete.
|
||||||
|
|
||||||
- A 955ms import on a background thread holds the GIL for ~10-50ms at a time
|
```python
|
||||||
during C extension init. Each hold stalls the main thread's render loop.
|
# In AppController.__init__, right after self._io_pool is created
|
||||||
- The user pays 955ms total either way: prefetch = 955ms of background stutter
|
self._warmup_status: dict[str, list[str]] = {
|
||||||
+ instant first call; lazy-only = 955ms of stutter on the first call only,
|
"pending": [], "completed": [], "failed": [],
|
||||||
with the GUI fully interactive in between.
|
}
|
||||||
- Prefetching wastes the import cost when the user never uses that provider
|
self._warmup_lock = threading.Lock()
|
||||||
(e.g., default is Gemini but the user actually only uses Anthropic).
|
self._warmup_done_event = threading.Event()
|
||||||
|
self._warmup_callbacks: list[Callable] = []
|
||||||
|
self._submit_warmup_jobs()
|
||||||
|
```
|
||||||
|
|
||||||
**Rule: heavy SDKs (`google.genai`, `anthropic`, `openai`, `fastapi`) are
|
```python
|
||||||
lazy-only, never prefetched.** Lighter modules (themes, command palette,
|
def _submit_warmup_jobs(self) -> None:
|
||||||
markdown table) MAY be optionally warmed on the `_io_pool` if profiling shows
|
"""Submit bg jobs to import heavy modules. Notifies subscribers on completion."""
|
||||||
they're commonly used, but it's not a hard requirement and the default is
|
heavy = self._compute_warmup_list()
|
||||||
"don't warm."
|
with self._warmup_lock:
|
||||||
|
self._warmup_status["pending"] = list(heavy)
|
||||||
|
self._warmup_status["completed"] = []
|
||||||
|
self._warmup_status["failed"] = []
|
||||||
|
self._warmup_done_event.clear()
|
||||||
|
for module_name in heavy:
|
||||||
|
self._io_pool.submit(self._warmup_one, module_name)
|
||||||
|
|
||||||
|
def _compute_warmup_list(self) -> list[str]:
|
||||||
|
result = [
|
||||||
|
# AI provider SDKs
|
||||||
|
"google.genai", "anthropic", "openai", "requests",
|
||||||
|
# Feature-gated GUI (used by main thread but not on first frame)
|
||||||
|
"src.command_palette",
|
||||||
|
"src.theme_nerv", "src.theme_nerv_fx",
|
||||||
|
"src.markdown_table",
|
||||||
|
]
|
||||||
|
if self._enable_test_hooks or self._web_host:
|
||||||
|
result.extend(["fastapi", "fastapi.security.api_key"])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _warmup_one(self, module_name: str) -> None:
|
||||||
|
try:
|
||||||
|
importlib.import_module(module_name)
|
||||||
|
with self._warmup_lock:
|
||||||
|
self._warmup_status["pending"].remove(module_name)
|
||||||
|
self._warmup_status["completed"].append(module_name)
|
||||||
|
except Exception as e:
|
||||||
|
with self._warmup_lock:
|
||||||
|
self._warmup_status["pending"].remove(module_name)
|
||||||
|
self._warmup_status["failed"].append(module_name)
|
||||||
|
finally:
|
||||||
|
with self._warmup_lock:
|
||||||
|
done = not self._warmup_status["pending"]
|
||||||
|
callbacks = list(self._warmup_callbacks) if done else []
|
||||||
|
if done:
|
||||||
|
self._warmup_done_event.set()
|
||||||
|
for cb in callbacks:
|
||||||
|
try:
|
||||||
|
cb(self._warmup_status)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Completion notification** is critical for the user-visible UX. Three surfaces:
|
||||||
|
|
||||||
|
1. **GUI status indicator** — the status bar shows "Warming up... (5/8)" while
|
||||||
|
the bg jobs run, then "All imports ready" with a green dot when complete.
|
||||||
|
The GUI never blocks waiting; the indicator is updated by polling
|
||||||
|
`controller.warmup_status()` once per frame (cheap, lock-guarded).
|
||||||
|
|
||||||
|
2. **GUI toast notification** — when warmup completes, show a toast:
|
||||||
|
"All providers ready" with the count of modules loaded. User can dismiss.
|
||||||
|
|
||||||
|
3. **Hook API endpoint** — `GET /api/warmup_status` returns the current state;
|
||||||
|
`GET /api/warmup_wait?timeout=N` blocks until done (for tests).
|
||||||
|
|
||||||
|
The user said: *"the app controller should post to test clients or the user
|
||||||
|
when its threads are warmed up with imports — that way the user knows 'hey
|
||||||
|
you have the ui first, but now you have all the functionality.'"* This is
|
||||||
|
exactly what the notification surfaces achieve.
|
||||||
|
|
||||||
|
**Why this beats lazy-loading:** if a user clicks "switch to Gemini" and the
|
||||||
|
controller lazy-loads `google.genai` on that action, the user sees ~1s of
|
||||||
|
nothing happening between the click and the visible response. With warmup,
|
||||||
|
the click is instant because `google.genai` is already in `sys.modules`. The
|
||||||
|
1s of cost was paid during startup, when the user was looking at a splash or
|
||||||
|
otherwise not waiting on input.
|
||||||
|
|
||||||
#### Layer 4 — Worker-process isolation (future, out of scope)
|
#### Layer 4 — Worker-process isolation (future, out of scope)
|
||||||
|
|
||||||
@@ -181,18 +279,19 @@ the GUI's thread?"* The answer is:
|
|||||||
|
|
||||||
| Scenario | Blocks GUI? |
|
| Scenario | Blocks GUI? |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Module top-level import of heavy X, then main imports X | **YES** (X's import is in main's chain) |
|
| Module top-level import of heavy X, then main imports X | **YES** (X's import is in main's chain). This is why we remove heavy imports from main-thread-reachable files. |
|
||||||
| Lazy import of X inside a function called from the asyncio thread | **NO** (asyncio thread blocks, not main) |
|
| `_io_pool` worker warming X while main thread renders | **NO direct block, but GIL contention causes micro-stutters** (~5-50ms each). Acceptable because the pool is capped at 4 threads and the main thread is mostly idle in `immapp.run()`. |
|
||||||
| Lazy import of X inside a function called from the main thread | **YES** (first call only; the function caller blocks) |
|
| `_io_pool` worker warms X; main thread later calls `_require_warmed("X")` (X already in `sys.modules`) | **NO** (the lookup is a `dict.get()` — instant, no import lock contention). |
|
||||||
| `_io_pool` worker importing X while main thread renders | **NO direct block, but GIL contention causes micro-stutters** (~5-50ms each). Acceptable because the pool is capped at 4 threads. |
|
| User-triggered UI action (e.g. provider switch) propagates to controller which calls `_require_warmed` on a warmed module | **NO** (lookup is instant). This is the win the user explicitly called out: no user-perceptible lag. |
|
||||||
| `_io_pool` worker imports X; main thread later imports X (same module) | **YES** (main blocks on per-module import lock until worker finishes). This is why Layer 1 must come first. |
|
| `wait_for_warmup()` blocks the asyncio thread waiting for warmup | **NO direct block on GUI** (different thread). Asyncio thread waits; main thread renders. Acceptable but rarely needed if user waits for warmup notification first. |
|
||||||
| Spawning a new `threading.Thread` for each import prefetch | **Wasteful** (thread creation ~1-5ms each; thread count explodes). Use the `_io_pool` instead. |
|
| Spawning a new `threading.Thread` for each import warmup | **Wasteful** (thread creation ~1-5ms each; thread count explodes). Use the `_io_pool` instead. |
|
||||||
|
|
||||||
This means: **Layer 1 is non-negotiable.** Even with the `_io_pool`, if the
|
This means: **Layer 1 is non-negotiable.** Even with warmup on `_io_pool`, if
|
||||||
heavy import is also in the main thread's import chain, the main thread will
|
the heavy import is also in the main thread's import chain, the main thread
|
||||||
block on the import lock the moment it tries to use the module. Layer 1 removes
|
will block on the import lock the moment it tries to use the module. Layer 1
|
||||||
the heavy imports from the main thread's chain; Layer 2 reuses threads
|
removes the heavy imports from the main thread's chain; Layer 2 reuses
|
||||||
efficiently; Layer 3 deliberately avoids prefetching the heaviest.
|
threads efficiently; Layer 3 proactively warms on bg threads so the FIRST
|
||||||
|
user-triggered use is instant.
|
||||||
|
|
||||||
### 2.4 Enforcement: the "main thread purity" audit
|
### 2.4 Enforcement: the "main thread purity" audit
|
||||||
|
|
||||||
@@ -236,95 +335,119 @@ not just at static analysis time.
|
|||||||
|
|
||||||
### 3.1 Per-file import plan
|
### 3.1 Per-file import plan
|
||||||
|
|
||||||
|
For each source file reachable from the main thread's import chain, we
|
||||||
|
**remove top-level heavy imports** and have functions access them via
|
||||||
|
`_require_warmed(name)`. The warmup jobs (§3.2) put the modules in
|
||||||
|
`sys.modules` before any function is called.
|
||||||
|
|
||||||
#### `src/ai_client.py` (the biggest win: ~1800ms)
|
#### `src/ai_client.py` (the biggest win: ~1800ms)
|
||||||
|
|
||||||
Top-level today: `from google import genai`, `import anthropic`, `import openai`,
|
Top-level today: `from google import genai`, `import anthropic`, `import openai`,
|
||||||
`import requests` (used by deepseek/minimax).
|
`import requests` (used by deepseek/minimax).
|
||||||
|
|
||||||
After:
|
After:
|
||||||
- Drop `from google import genai` from top — lazy in `_send_gemini()`
|
- **Drop all four heavy imports from the top.** Add `_require_warmed(name)`
|
||||||
- Drop `import anthropic` from top — lazy in `_send_anthropic()`
|
helper at the top.
|
||||||
- Drop `import openai` from top — lazy in `_send_deepseek()` and `_send_minimax()`
|
- `_send_gemini()` calls `_require_warmed("google.genai")` to get the module
|
||||||
- Drop `import requests` from top — lazy in those two providers' HTTP code
|
- `_send_anthropic()` calls `_require_warmed("anthropic")`
|
||||||
- Provider client objects (`_gemini_client`, `_anthropic_client`, etc.) stay as
|
- `_send_deepseek()` and `_send_minimax()` call `_require_warmed("openai")` and `_require_warmed("requests")`
|
||||||
module globals but are now `None` until first use
|
- Provider client objects (`_gemini_client`, `_anthropic_client`, etc.) stay
|
||||||
- The `_send_*` functions check their provider client is initialized and call a
|
as module globals but are now `None` until `_send_*` initializes them
|
||||||
new `_ensure_<provider>_client()` lazy initializer (extracted from the current
|
(extracted from current top-level logic into a new
|
||||||
top-level logic)
|
`_ensure_<provider>_client()` that uses the warmed module)
|
||||||
|
- The warmup list in `AppController._compute_warmup_list()` includes
|
||||||
|
`google.genai`, `anthropic`, `openai`, `requests` (always warmed)
|
||||||
|
|
||||||
**Result:** ~1800ms off the main thread. First AI call still pays it, but on
|
**Result:** ~1800ms off the main thread. The bg threads pay this cost during
|
||||||
the asyncio worker.
|
startup. By the time the first AI call happens (which is always async, on
|
||||||
|
the asyncio thread), the modules are in `sys.modules` and the lookup is
|
||||||
|
instant. No user-perceptible lag.
|
||||||
|
|
||||||
#### `src/app_controller.py` (FastAPI in headless/web only)
|
#### `src/api_hooks.py` (FastAPI in headless/web only)
|
||||||
|
|
||||||
Top-level today: `from fastapi import ...`, `from fastapi.security.api_key import ...`
|
Top-level today: `from fastapi import ...`, `from fastapi.security.api_key import ...`
|
||||||
(only needed if `--enable-test-hooks` or `--web-host`).
|
(only needed if `--enable-test-hooks` or `--web-host`).
|
||||||
|
|
||||||
After:
|
After:
|
||||||
- Drop these from top — lazy inside `HookServer.__init__` (which is itself lazy
|
- **Drop these from top.** Add `_require_warmed(name)` calls inside the
|
||||||
in the controller: `if enable_test_hooks: from src.api_hooks import HookServer; ...`)
|
methods that need them.
|
||||||
|
- The warmup list in `AppController._compute_warmup_list()` includes
|
||||||
|
`fastapi`, `fastapi.security.api_key` **conditionally** — only when
|
||||||
|
`enable_test_hooks` or `web_host` is set
|
||||||
|
|
||||||
**Result:** ~470ms off the main thread for non-test, non-web launches. Critical
|
**Result:** ~470ms off the main thread for non-test, non-web launches.
|
||||||
because `live_gui` tests launch with `--enable-test-hooks` but the FastAPI work
|
For `live_gui` tests (`--enable-test-hooks`), the warmup loads fastapi
|
||||||
can be deferred until the asyncio loop is ready.
|
during the same startup window, so the hook server is ready when the
|
||||||
|
process announces readiness.
|
||||||
|
|
||||||
#### `src/commands.py` and `src/command_palette.py` (command palette lazy)
|
#### `src/commands.py` (command palette warmup-aware)
|
||||||
|
|
||||||
Top-level today: `from src.command_palette import ...` at `src/commands.py:1`.
|
Top-level today: `from src.command_palette import ...` at `src/commands.py:1`.
|
||||||
|
|
||||||
After:
|
After:
|
||||||
- Lazy in each `_*_command()` function in `src/commands.py` that actually
|
- **Drop the top-level import.** The command functions call
|
||||||
opens the palette
|
`_require_warmed("src.command_palette")` to access the module
|
||||||
- The CommandRegistry decorator can keep module-level function references, but
|
- The warmup list includes `src.command_palette`
|
||||||
the *body* of the command does the heavy import
|
|
||||||
|
|
||||||
**Result:** ~244ms off if user doesn't open palette during the first session.
|
**Result:** ~244ms off the main thread's import chain. The bg thread
|
||||||
|
warms it during startup; the first `Ctrl+Shift+P` is instant.
|
||||||
|
|
||||||
#### `src/theme_2.py` and `src/theme_nerv.py` / `src/theme_nerv_fx.py` (NERV theme lazy)
|
#### `src/theme_2.py` (NERV theme warmup-aware)
|
||||||
|
|
||||||
Top-level today: NERV modules imported at `src/theme_2.py` module top.
|
Top-level today: `from src.theme_nerv import ...`, `from src.theme_nerv_fx import ...`
|
||||||
|
at the top of `src/theme_2.py`.
|
||||||
|
|
||||||
After:
|
After:
|
||||||
- Lazy in `apply_nerv_theme()` (the function that activates NERV)
|
- **Drop the top-level imports.** `apply_nerv_theme()` (or the function
|
||||||
- The default theme path stays lean (uses only `src/theme_2.py` + `src/theme_models.py`)
|
that activates NERV) calls `_require_warmed("src.theme_nerv")` and
|
||||||
|
`_require_warmed("src.theme_nerv_fx")`
|
||||||
|
- The warmup list includes both NERV modules
|
||||||
|
|
||||||
**Result:** ~485ms off if user doesn't pick NERV theme (the default path).
|
**Result:** ~485ms off the main thread's import chain (the default
|
||||||
|
non-NERV path is lean). User pays the cost during startup; theme switch
|
||||||
|
is instant when they pick NERV.
|
||||||
|
|
||||||
#### `src/markdown_helper.py` (markdown table lazy)
|
#### `src/markdown_helper.py` (markdown table warmup-aware)
|
||||||
|
|
||||||
Top-level today: `from src.markdown_table import ...` at `src/markdown_helper.py:1`.
|
Top-level today: `from src.markdown_table import ...` at `src/markdown_helper.py:1`.
|
||||||
|
|
||||||
After:
|
After:
|
||||||
- Lazy in `_render_table_block()` (or wherever GFM table detection happens)
|
- **Drop the top-level import.** The table-detection branch of `render()`
|
||||||
- The first markdown render that hits a table pays the 250ms; subsequent hits are
|
calls `_require_warmed("src.markdown_table")`
|
||||||
cached in `sys.modules`
|
- The warmup list includes `src.markdown_table`
|
||||||
|
|
||||||
**Result:** ~250ms off the first markdown render that lacks tables (typical).
|
**Result:** ~250ms off the main thread's import chain. First markdown
|
||||||
|
table render is instant.
|
||||||
|
|
||||||
#### `src/imgui_scopes.py`, `src/gui_2.py`, `src/bg_shader.py` (KEEP `imgui_bundle`)
|
#### `src/imgui_scopes.py`, `src/gui_2.py`, `src/bg_shader.py` (KEEP `imgui_bundle`)
|
||||||
|
|
||||||
These MUST keep `import imgui_bundle` at top — the ImGui render loop is the hot
|
These MUST keep `import imgui_bundle` at top — the ImGui render loop is the
|
||||||
path and needs the module on first frame. There is no way to defer this without
|
hot path and needs the module on first frame. There is no way to defer
|
||||||
breaking the render loop.
|
this without breaking the render loop.
|
||||||
|
|
||||||
What CAN be deferred inside `src/gui_2.py`:
|
What CAN be deferred inside `src/gui_2.py`:
|
||||||
- `import numpy` (only needed for `bg_shader`; the GUI itself doesn't need numpy
|
- `import numpy` (only needed for `bg_shader`; the GUI itself doesn't
|
||||||
on the first frame)
|
need numpy on the first frame) — move to `_require_warmed("numpy")` in
|
||||||
- Other feature-gated imports
|
the bg shader call site, add `numpy` to the warmup list
|
||||||
|
- Other feature-gated imports — same pattern
|
||||||
|
|
||||||
#### `src/gui_2.py` direct heavy imports (audit)
|
#### `src/gui_2.py` direct heavy imports (audit)
|
||||||
|
|
||||||
We will use AST to audit which `import X` statements at `src/gui_2.py` top-level
|
We will use AST to audit which `import X` statements at `src/gui_2.py`
|
||||||
are reachable from the first-frame render path (`render_main_window`,
|
top-level are reachable from the first-frame render path
|
||||||
`render_main_menu_bar`, etc.) and which are feature-gated. Feature-gated ones
|
(`render_main_window`, `render_main_menu_bar`, etc.) and which are
|
||||||
move inside the function that gates them.
|
feature-gated. First-frame imports stay top-level. Feature-gated ones
|
||||||
|
move to `_require_warmed(...)` calls at the use site, with the module
|
||||||
|
added to the warmup list.
|
||||||
|
|
||||||
### 3.2 Job pool scaffolding
|
### 3.2 Job pool + warmup scaffolding
|
||||||
|
|
||||||
New code in `src/app_controller.py`:
|
New code in `src/app_controller.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import importlib
|
||||||
|
import threading
|
||||||
|
|
||||||
# In AppController.__init__, after the asyncio loop starts:
|
# In AppController.__init__, after the asyncio loop starts:
|
||||||
self._io_pool = ThreadPoolExecutor(
|
self._io_pool = ThreadPoolExecutor(
|
||||||
@@ -332,16 +455,108 @@ self._io_pool = ThreadPoolExecutor(
|
|||||||
thread_name_prefix="controller-io",
|
thread_name_prefix="controller-io",
|
||||||
)
|
)
|
||||||
|
|
||||||
def submit_io(self, fn, *args, **kwargs):
|
# Warmup state
|
||||||
"""Submit a background job to the shared I/O pool. Use this instead of
|
self._warmup_lock = threading.Lock()
|
||||||
threading.Thread for new background work.
|
self._warmup_done_event = threading.Event()
|
||||||
|
self._warmup_status: dict[str, list[str]] = {
|
||||||
Returns a concurrent.futures.Future. Caller can .result() if they need
|
"pending": [], "completed": [], "failed": [],
|
||||||
to block, or .add_done_callback for fire-and-forget with error handling.
|
}
|
||||||
"""
|
self._warmup_callbacks: list[Callable] = []
|
||||||
return self._io_pool.submit(fn, *args, **kwargs)
|
self._submit_warmup_jobs()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`_submit_warmup_jobs()` computes the warmup list and submits one job per
|
||||||
|
module to the pool:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _submit_warmup_jobs(self) -> None:
|
||||||
|
heavy = self._compute_warmup_list()
|
||||||
|
with self._warmup_lock:
|
||||||
|
self._warmup_status["pending"] = list(heavy)
|
||||||
|
self._warmup_status["completed"] = []
|
||||||
|
self._warmup_status["failed"] = []
|
||||||
|
self._warmup_done_event.clear()
|
||||||
|
for name in heavy:
|
||||||
|
self._io_pool.submit(self._warmup_one, name)
|
||||||
|
|
||||||
|
def _compute_warmup_list(self) -> list[str]:
|
||||||
|
result = [
|
||||||
|
"google.genai", "anthropic", "openai", "requests",
|
||||||
|
"src.command_palette",
|
||||||
|
"src.theme_nerv", "src.theme_nerv_fx",
|
||||||
|
"src.markdown_table",
|
||||||
|
"numpy", # used by bg_shader; warmed for first invocation
|
||||||
|
]
|
||||||
|
if self._enable_test_hooks or self._web_host:
|
||||||
|
result.extend(["fastapi", "fastapi.security.api_key"])
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
Each warmup worker imports the module, updates the status, and on the
|
||||||
|
last one fires the completion callbacks (so the GUI status indicator and
|
||||||
|
toast notification can react):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _warmup_one(self, name: str) -> None:
|
||||||
|
try:
|
||||||
|
importlib.import_module(name)
|
||||||
|
with self._warmup_lock:
|
||||||
|
self._warmup_status["pending"].remove(name)
|
||||||
|
self._warmup_status["completed"].append(name)
|
||||||
|
except Exception:
|
||||||
|
with self._warmup_lock:
|
||||||
|
self._warmup_status["pending"].remove(name)
|
||||||
|
self._warmup_status["failed"].append(name)
|
||||||
|
finally:
|
||||||
|
with self._warmup_lock:
|
||||||
|
done = not self._warmup_status["pending"]
|
||||||
|
cbs = list(self._warmup_callbacks) if done else []
|
||||||
|
if done:
|
||||||
|
self._warmup_done_event.set()
|
||||||
|
for cb in cbs:
|
||||||
|
try:
|
||||||
|
cb(dict(self._warmup_status))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Public API on `AppController`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def warmup_status(self) -> dict[str, list[str]]:
|
||||||
|
"""Snapshot the current warmup state. Cheap (lock-guarded copy)."""
|
||||||
|
with self._warmup_lock:
|
||||||
|
return {k: list(v) for k, v in self._warmup_status.items()}
|
||||||
|
|
||||||
|
def is_warmup_done(self) -> bool:
|
||||||
|
return self._warmup_done_event.is_set()
|
||||||
|
|
||||||
|
def wait_for_warmup(self, timeout: float | None = None) -> bool:
|
||||||
|
"""Block until warmup completes. Returns True on done, False on timeout."""
|
||||||
|
return self._warmup_done_event.wait(timeout=timeout)
|
||||||
|
|
||||||
|
def on_warmup_complete(self, callback: Callable[[dict], None]) -> None:
|
||||||
|
"""Register a callback for warmup completion. If already done, fires immediately."""
|
||||||
|
with self._warmup_lock:
|
||||||
|
if self._warmup_done_event.is_set():
|
||||||
|
snap = {k: list(v) for k, v in self._warmup_status.items()}
|
||||||
|
if "snap" in dir(): # already done
|
||||||
|
callback(snap)
|
||||||
|
else:
|
||||||
|
with self._warmup_lock:
|
||||||
|
self._warmup_callbacks.append(callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
Hook API endpoints (added in `src/api_hooks.py`):
|
||||||
|
|
||||||
|
- `GET /api/warmup_status` → `controller.warmup_status()`
|
||||||
|
- `GET /api/warmup_wait?timeout=N` → blocks until done, returns final status
|
||||||
|
|
||||||
|
GUI integration (in `src/gui_2.py`):
|
||||||
|
|
||||||
|
- Status bar: "Warming up... (5/8)" while in flight, "All imports ready" + green dot when done. Polled once per frame from `controller.warmup_status()` (cheap, ~microseconds).
|
||||||
|
- On transition to done: show a toast notification "All providers ready (8 modules)" for 5 seconds.
|
||||||
|
|
||||||
In `AppController.shutdown()` (or wherever lifecycle cleanup lives):
|
In `AppController.shutdown()` (or wherever lifecycle cleanup lives):
|
||||||
`self._io_pool.shutdown(wait=False)`. Non-blocking because the pool's
|
`self._io_pool.shutdown(wait=False)`. Non-blocking because the pool's
|
||||||
workers are daemon threads and will die with the process anyway.
|
workers are daemon threads and will die with the process anyway.
|
||||||
@@ -381,37 +596,50 @@ Used at every major init step in `AppController.__init__` and `App.__init__`.
|
|||||||
- T1.4: Add `scripts/audit_main_thread_imports.py` (static gate)
|
- T1.4: Add `scripts/audit_main_thread_imports.py` (static gate)
|
||||||
- T1.5: Commit baseline + audit script
|
- T1.5: Commit baseline + audit script
|
||||||
|
|
||||||
### Phase 2: Job Pool Foundation (Day 1) — the "no new threads" rule
|
### Phase 2: Job Pool + Warmup Foundation (Day 1)
|
||||||
- T2.1 (TDD Red): Write `tests/test_app_controller_io_pool.py` asserting
|
- T2.1 (TDD Red): `tests/test_app_controller_io_pool.py` — assert
|
||||||
`AppController` has a `_io_pool: ThreadPoolExecutor` with 4 workers, named
|
`AppController` has a 4-worker `_io_pool` named `controller-io-*`
|
||||||
`controller-io-*`
|
- T2.2 (Green): Add `_io_pool` to `AppController.__init__` with named threads
|
||||||
- T2.2 (Green): Add `self._io_pool = ThreadPoolExecutor(max_workers=4,
|
- T2.3 (TDD Red): `tests/test_warmup_mechanism.py` — assert warmup jobs are
|
||||||
thread_name_prefix="controller-io")` to `AppController.__init__`. Add
|
submitted in `__init__`, complete within 10s, fire the done event, support
|
||||||
`submit_io(fn, *args)` helper. Wire shutdown into `controller.shutdown()`.
|
callbacks, don't block init
|
||||||
- T2.3: Verify T2.1 tests pass + full suite still passes
|
- T2.4 (Green): Implement `_submit_warmup_jobs()`, `_compute_warmup_list()`,
|
||||||
|
`_warmup_one()`, `warmup_status()`, `is_warmup_done()`, `wait_for_warmup()`,
|
||||||
|
`on_warmup_complete()` per spec §3.2
|
||||||
|
- T2.5: Run T2.1 + T2.3 tests, confirm PASS
|
||||||
|
- T2.6: Commit
|
||||||
|
|
||||||
### Phase 3: Lazy-load AI provider SDKs (Day 2)
|
### Phase 3: Remove top-level heavy SDK imports from `src/ai_client.py` (Day 2)
|
||||||
- T3.1 (TDD Red): Write `tests/test_ai_client_lazy_imports.py` asserting
|
- T3.1 (TDD Red): `tests/test_ai_client_no_top_level_sdk_imports.py` — assert
|
||||||
`import src.ai_client` does NOT import any provider SDK
|
`import src.ai_client` does NOT load `google.genai` / `anthropic` / `openai` /
|
||||||
- T3.2 (Green): Move `from google import genai` / `import anthropic` /
|
`requests` (warmup hasn't run in the subprocess)
|
||||||
`import openai` / `import requests` into their respective `_send_*` functions
|
- T3.2 (Green): Remove the four heavy imports from the top of `ai_client.py`.
|
||||||
- T3.3: Verify existing `tests/test_ai_client.py` still passes
|
Add `_require_warmed(name)` helper. Each `_send_*` uses
|
||||||
- T3.4: Commit, re-run benchmark, expect `import src.ai_client` < 50ms
|
`_require_warmed("google.genai")` etc.
|
||||||
|
- T3.3: Run existing `tests/test_ai_client.py`; fix any breakage (tests
|
||||||
|
relying on top-level import side effects need a fixture that warms or a
|
||||||
|
fallback for test mode)
|
||||||
|
- T3.4: Confirm T3.1 tests PASS
|
||||||
|
- T3.5: Commit
|
||||||
|
|
||||||
### Phase 4: Lazy-load FastAPI in `HookServer` (Day 2)
|
### Phase 4: Remove top-level FastAPI imports from `src/api_hooks.py` (Day 2)
|
||||||
- T4.1 (TDD Red): Write `tests/test_hook_server_lazy_fastapi.py` asserting
|
- T4.1 (TDD Red): `tests/test_hook_server_no_top_level_fastapi.py` — assert
|
||||||
`from src.api_hooks import HookServer` does NOT import fastapi
|
`from src.api_hooks import HookServer` does NOT import fastapi
|
||||||
- T4.2 (Green): Move `from fastapi import ...` inside the methods that need them
|
- T4.2 (Green): Remove the fastapi imports from top. Use `_require_warmed`
|
||||||
- T4.3: Verify existing `tests/test_api_hooks.py` still passes
|
inside the methods that need them
|
||||||
|
- T4.3: Run existing `tests/test_api_hooks.py`; fix
|
||||||
- T4.4: Commit
|
- T4.4: Commit
|
||||||
|
|
||||||
### Phase 5: Lazy-load feature-gated GUI modules (Day 3)
|
### Phase 5: Remove top-level imports for feature-gated GUI modules (Day 3)
|
||||||
- T5.1: Lazy-load `src.command_palette` in `src/commands.py`
|
- T5A: Command Palette — `tests/test_command_palette_no_top_level_import.py`
|
||||||
- T5.2: Lazy-load `src.theme_nerv` and `src.theme_nerv_fx` in `src/theme_2.py`
|
+ remove from `src/commands.py` + use `_require_warmed("src.command_palette")`
|
||||||
- T5.3: Lazy-load `src.markdown_table` in `src/markdown_helper.py`
|
- T5B: NERV Theme — `tests/test_theme_nerv_no_top_level_import.py` + remove
|
||||||
- T5.4: Audit and lazy-load feature-gated imports in `src/gui_2.py`
|
from `src/theme_2.py` + use `_require_warmed("src.theme_nerv")` etc.
|
||||||
- T5.5: Run all GUI tests; fix any circular imports
|
- T5C: Markdown Table — `tests/test_markdown_helper_no_top_level_import.py` +
|
||||||
- T5.6: Commit per task
|
remove from `src/markdown_helper.py` + use `_require_warmed("src.markdown_table")`
|
||||||
|
- T5D: GUI feature-gated — audit `src/gui_2.py` via the T1.2 script, apply
|
||||||
|
same pattern. `numpy` migrates to `_require_warmed` in `bg_shader` call site.
|
||||||
|
- T5E: Commit per module (4 atomic commits)
|
||||||
|
|
||||||
### Phase 6: Migrate ad-hoc threads to `_io_pool` (Day 4)
|
### Phase 6: Migrate ad-hoc threads to `_io_pool` (Day 4)
|
||||||
- T6.1: Audit: `grep -rn "threading.Thread(" src/` to find all ad-hoc
|
- T6.1: Audit: `grep -rn "threading.Thread(" src/` to find all ad-hoc
|
||||||
@@ -419,30 +647,49 @@ Used at every major init step in `AppController.__init__` and `App.__init__`.
|
|||||||
- T6.2: Refactor each ad-hoc thread to use `controller.submit_io(fn)` instead
|
- T6.2: Refactor each ad-hoc thread to use `controller.submit_io(fn)` instead
|
||||||
- T6.3: Per-migration commit
|
- T6.3: Per-migration commit
|
||||||
- T6.4: Final `grep -rn "threading.Thread(" src/` shows ZERO new spawns
|
- T6.4: Final `grep -rn "threading.Thread(" src/` shows ZERO new spawns
|
||||||
(the grep result should be identical to the T6.1 audit list, no new entries)
|
|
||||||
|
|
||||||
### Phase 7: Enforcement — Runtime Audit Hook (Day 4)
|
### Phase 7: Warmup Notification (Hook API + GUI) (Day 4)
|
||||||
- T7.1 (TDD Red): `tests/test_main_thread_purity.py` — spawn `sloppy.py
|
- T7A.1 (TDD Red): `tests/test_api_hooks_warmup.py` — assert
|
||||||
|
`GET /api/warmup_status` and `GET /api/warmup_wait` work
|
||||||
|
- T7A.2 (Green): Add the two endpoints in `src/api_hooks.py` and register
|
||||||
|
`warmup_status` in `_gettable_fields`
|
||||||
|
- T7B.1: In `src/gui_2.py`, add a status-bar indicator that polls
|
||||||
|
`controller.warmup_status()` each frame: "Warming up... (N/M)" while
|
||||||
|
pending, "All imports ready" with green dot on completion
|
||||||
|
- T7B.2: Register a callback via `controller.on_warmup_complete(cb)` that
|
||||||
|
shows a toast "All providers ready (M modules)" on success
|
||||||
|
- T7B.3: Update docs (status bar, toast, hook API)
|
||||||
|
- T7B.4: Commit
|
||||||
|
|
||||||
|
### Phase 8: Enforcement — Runtime Audit Hook (Day 4)
|
||||||
|
- T8.1 (TDD Red): `tests/test_main_thread_purity.py` — spawn `sloppy.py
|
||||||
--headless --enable-test-hooks` with a `sys.addaudithook` shim, verify no
|
--headless --enable-test-hooks` with a `sys.addaudithook` shim, verify no
|
||||||
heavy import happens on the main thread
|
heavy import happens on the main thread
|
||||||
- T7.2: Once Phase 3-5 land, this test should start passing. Wire into CI.
|
- T8.2: Once Phase 3-5 land, this test should start passing. Wire into CI
|
||||||
- T7.3: Commit
|
as a gating test (`@pytest.mark.slow`).
|
||||||
|
- T8.3: Commit
|
||||||
### Phase 8: Hook API + Diagnostics (Day 5)
|
|
||||||
- T8.1: Add `/api/startup_profile` endpoint
|
|
||||||
- T8.2: Add `/api/io_pool_status` endpoint
|
|
||||||
- T8.3: Add to `_gettable_fields` and the Diagnostics panel
|
|
||||||
- T8.4: Document in `docs/guide_api_hooks.md`
|
|
||||||
- T8.5: Tests + commit
|
|
||||||
|
|
||||||
### Phase 9: Verify + Checkpoint (Day 5)
|
### Phase 9: Verify + Checkpoint (Day 5)
|
||||||
- T9.1: Re-run `scripts/benchmark_imports.py`; confirm `import src.gui_2` and
|
- T9.1: Re-run `scripts/benchmark_imports.py --runs=3`; confirm
|
||||||
`import src.ai_client` are now < 100ms each
|
`import src.ai_client` < 50ms, `import src.gui_2` < 500ms,
|
||||||
|
`import src.app_controller` < 300ms
|
||||||
- T9.2: Re-run `scripts/audit_main_thread_imports.py`; exit 0
|
- T9.2: Re-run `scripts/audit_main_thread_imports.py`; exit 0
|
||||||
- T9.3: Run `tests/test_main_thread_purity.py`; pass
|
- T9.3: Run `tests/test_warmup_mechanism.py`; warmup completes and notifications fire
|
||||||
- T9.4: Run full `live_gui` test batch; `wait_for_server(timeout=15)` no
|
- T9.4: Run `tests/test_main_thread_purity.py`; pass
|
||||||
longer times out
|
- T9.5: Run full `live_gui` test batch; `wait_for_server(timeout=15)` no
|
||||||
- T9.5: Manual smoke test: `uv run sloppy.py` and
|
longer times out. Tests can call `controller.wait_for_warmup()` before
|
||||||
|
exercising warmup-dependent functionality.
|
||||||
|
- T9.6: Manual smoke:
|
||||||
|
- `uv run sloppy.py`: time-to-first-frame < 1.5s, observe status indicator
|
||||||
|
"Warming up... (N/M)" → "All imports ready" + toast
|
||||||
|
- `uv run sloppy.py --enable-test-hooks`: same, plus `/api/warmup_status`
|
||||||
|
returns `completed` after a brief wait
|
||||||
|
- `uv run sloppy.py --headless`: time-to-server-ready
|
||||||
|
- **Provider switch test**: switch from MiniMax to Gemini in the GUI
|
||||||
|
after warmup. The action must be INSTANT, not 1s-delayed (proves
|
||||||
|
warmup did its job)
|
||||||
|
- T9.7: Phase checkpoint commit + git note with full verification report
|
||||||
|
- T9.8: Update `conductor/tracks.md`; archive track
|
||||||
`uv run sloppy.py --enable-test-hooks` both feel snappier
|
`uv run sloppy.py --enable-test-hooks` both feel snappier
|
||||||
- T9.6: Phase checkpoint commit with full verification report
|
- T9.6: Phase checkpoint commit with full verification report
|
||||||
|
|
||||||
@@ -486,9 +733,22 @@ The track is complete when:
|
|||||||
- [ ] `scripts/audit_main_thread_imports.py` exits 0 (no heavy imports on main)
|
- [ ] `scripts/audit_main_thread_imports.py` exits 0 (no heavy imports on main)
|
||||||
- [ ] `tests/test_main_thread_purity.py` passes (runtime audit hook confirms invariant)
|
- [ ] `tests/test_main_thread_purity.py` passes (runtime audit hook confirms invariant)
|
||||||
- [ ] `scripts/benchmark_imports.py` shows no new red entries in the top-20
|
- [ ] `scripts/benchmark_imports.py` shows no new red entries in the top-20
|
||||||
- [ ] First AI call latency on the asyncio thread is < 1500ms (pays the SDK load once,
|
- [ ] **`controller.wait_for_warmup(timeout=10.0)` returns True** — warmup completed
|
||||||
then the user has a snappy first call forever after). Main thread sees ZERO
|
within 10s of `AppController.__init__`
|
||||||
of this cost.
|
- [ ] **All modules in the warmup list are in `sys.modules` after warmup** —
|
||||||
|
`controller.warmup_status()['pending']` is empty, `'completed'` contains
|
||||||
|
all expected module names
|
||||||
|
- [ ] **User-triggered actions on warmed modules are instant** — manual test
|
||||||
|
switching providers (e.g. MiniMax → Gemini) after warmup completes shows
|
||||||
|
NO perceptible lag (was ~1s with lazy-loading)
|
||||||
|
- [ ] **GUI status indicator transitions** — observe "Warming up... (N/M)" in
|
||||||
|
the status bar, then "All imports ready" with green dot, then a toast
|
||||||
|
notification fires via `controller.on_warmup_complete(...)`
|
||||||
|
- [ ] **Hook API exposes warmup state** — `GET /api/warmup_status` returns
|
||||||
|
`{pending: [], completed: [...], failed: []}`; `GET /api/warmup_wait?timeout=10`
|
||||||
|
returns the final state
|
||||||
|
- [ ] **NO `import X` statements inside function bodies for heavy modules** —
|
||||||
|
verified by `grep -rn "^\s*import \(google\|anthropic\|openai\|fastapi\|src\.command_palette\|src\.theme_nerv\|src\.markdown_table\)" src/`
|
||||||
- [ ] No regressions in the existing 272/273 passing tests
|
- [ ] No regressions in the existing 272/273 passing tests
|
||||||
- [ ] `grep -rn "threading.Thread(" src/` shows ZERO new spawns after Phase 6
|
- [ ] `grep -rn "threading.Thread(" src/` shows ZERO new spawns after Phase 6
|
||||||
migration (only the existing project scaffolding threads like `HookServer`
|
migration (only the existing project scaffolding threads like `HookServer`
|
||||||
@@ -505,9 +765,8 @@ The track is complete when:
|
|||||||
- Importing on the main thread for the lean `gui_2` skeleton (~300ms unavoidable)
|
- Importing on the main thread for the lean `gui_2` skeleton (~300ms unavoidable)
|
||||||
- `pydantic` lazy loading (used by `src/models.py` which is imported by 16 files;
|
- `pydantic` lazy loading (used by `src/models.py` which is imported by 16 files;
|
||||||
the cost is already amortized and deferring it would cascade)
|
the cost is already amortized and deferring it would cascade)
|
||||||
- Prefetch / warm-up of the heavy SDKs in the background (Layer 3 in §2.2 is
|
- Lazy-loading heavy modules in function bodies (Layer 1 in §2.2 — explicitly
|
||||||
deliberately the "do nothing" layer; the user pays the import cost once on
|
rejected by the user; warmup is the only mechanism)
|
||||||
first use, on the asyncio thread, not in the background)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ last_updated = "2026-06-06"
|
|||||||
|
|
||||||
[phases]
|
[phases]
|
||||||
phase_1 = { status = "in_progress", checkpoint_sha = "", name = "Audit + Benchmark + Foundation" }
|
phase_1 = { status = "in_progress", checkpoint_sha = "", name = "Audit + Benchmark + Foundation" }
|
||||||
phase_2 = { status = "pending", checkpoint_sha = "", name = "Job Pool Foundation (no new threads)" }
|
phase_2 = { status = "pending", checkpoint_sha = "", name = "Job Pool + Warmup Foundation" }
|
||||||
phase_3 = { status = "pending", checkpoint_sha = "", name = "Lazy-load AI provider SDKs" }
|
phase_3 = { status = "pending", checkpoint_sha = "", name = "Remove top-level SDK imports (ai_client)" }
|
||||||
phase_4 = { status = "pending", checkpoint_sha = "", name = "Lazy-load FastAPI in HookServer" }
|
phase_4 = { status = "pending", checkpoint_sha = "", name = "Remove top-level FastAPI imports" }
|
||||||
phase_5 = { status = "pending", checkpoint_sha = "", name = "Lazy-load feature-gated GUI modules" }
|
phase_5 = { status = "pending", checkpoint_sha = "", name = "Remove top-level feature-gated GUI imports" }
|
||||||
phase_6 = { status = "pending", checkpoint_sha = "", name = "Migrate ad-hoc threads to _io_pool" }
|
phase_6 = { status = "pending", checkpoint_sha = "", name = "Migrate ad-hoc threads to _io_pool" }
|
||||||
phase_7 = { status = "pending", checkpoint_sha = "", name = "Enforcement: runtime audit hook" }
|
phase_7 = { status = "pending", checkpoint_sha = "", name = "Warmup Notification (Hook API + GUI)" }
|
||||||
phase_8 = { status = "pending", checkpoint_sha = "", name = "Hook API + Diagnostics" }
|
phase_8 = { status = "pending", checkpoint_sha = "", name = "Enforcement: runtime audit hook" }
|
||||||
phase_9 = { status = "pending", checkpoint_sha = "", name = "Verify + Checkpoint" }
|
phase_9 = { status = "pending", checkpoint_sha = "", name = "Verify + Checkpoint" }
|
||||||
|
|
||||||
[tasks]
|
[tasks]
|
||||||
@@ -26,42 +26,41 @@ t1_2 = { status = "pending", commit_sha = "", description = "Audit src/gui_2.py
|
|||||||
t1_3 = { status = "pending", commit_sha = "", description = "Add StartupProfiler and instrument init" }
|
t1_3 = { status = "pending", commit_sha = "", description = "Add StartupProfiler and instrument init" }
|
||||||
t1_4 = { status = "pending", commit_sha = "", description = "Write scripts/audit_main_thread_imports.py (static CI gate)" }
|
t1_4 = { status = "pending", commit_sha = "", description = "Write scripts/audit_main_thread_imports.py (static CI gate)" }
|
||||||
t1_5 = { status = "pending", commit_sha = "", description = "Commit baseline + audit script" }
|
t1_5 = { status = "pending", commit_sha = "", description = "Commit baseline + audit script" }
|
||||||
# Phase 2: Job Pool Foundation
|
# Phase 2: Job Pool + Warmup Foundation
|
||||||
t2_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_app_controller_io_pool.py" }
|
t2_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_app_controller_io_pool.py" }
|
||||||
t2_2 = { status = "pending", commit_sha = "", description = "Green: add _io_pool ThreadPoolExecutor + submit_io helper to AppController" }
|
t2_2 = { status = "pending", commit_sha = "", description = "Green: add _io_pool ThreadPoolExecutor to AppController" }
|
||||||
t2_3 = { status = "pending", commit_sha = "", description = "Confirm T2.1 tests pass + full suite still passes" }
|
t2_3 = { status = "pending", commit_sha = "", description = "Red: tests/test_warmup_mechanism.py" }
|
||||||
t2_4 = { status = "pending", commit_sha = "", description = "Commit T2" }
|
t2_4 = { status = "pending", commit_sha = "", description = "Green: implement _submit_warmup_jobs, _compute_warmup_list, _warmup_one, warmup_status, is_warmup_done, wait_for_warmup, on_warmup_complete" }
|
||||||
# Phase 3: Lazy-load AI Provider SDKs
|
t2_5 = { status = "pending", commit_sha = "", description = "Confirm T2.1 + T2.3 tests pass" }
|
||||||
t3_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_ai_client_lazy_imports.py" }
|
t2_6 = { status = "pending", commit_sha = "", description = "Commit T2" }
|
||||||
t3_2 = { status = "pending", commit_sha = "", description = "Green: move provider SDK imports into _send_* funcs" }
|
# Phase 3: Remove top-level SDK imports
|
||||||
|
t3_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_ai_client_no_top_level_sdk_imports.py" }
|
||||||
|
t3_2 = { status = "pending", commit_sha = "", description = "Green: remove top-level SDK imports from src/ai_client.py; add _require_warmed helper; update _send_* to use it" }
|
||||||
t3_3 = { status = "pending", commit_sha = "", description = "Fix existing test_ai_client.py breakage" }
|
t3_3 = { status = "pending", commit_sha = "", description = "Fix existing test_ai_client.py breakage" }
|
||||||
t3_4 = { status = "pending", commit_sha = "", description = "Confirm T3.1 tests PASS" }
|
t3_4 = { status = "pending", commit_sha = "", description = "Confirm T3.1 tests PASS" }
|
||||||
t3_5 = { status = "pending", commit_sha = "", description = "Commit T3" }
|
t3_5 = { status = "pending", commit_sha = "", description = "Commit T3" }
|
||||||
t3_6 = { status = "pending", commit_sha = "", description = "Update tracks.md T3 row" }
|
t3_6 = { status = "pending", commit_sha = "", description = "Update tracks.md T3 row" }
|
||||||
# Phase 4: Lazy-load FastAPI
|
# Phase 4: Remove top-level FastAPI imports
|
||||||
t4_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_hook_server_lazy_fastapi.py" }
|
t4_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_hook_server_no_top_level_fastapi.py" }
|
||||||
t4_2 = { status = "pending", commit_sha = "", description = "Green: move fastapi imports into HookServer methods" }
|
t4_2 = { status = "pending", commit_sha = "", description = "Green: remove fastapi imports from src/api_hooks.py; use _require_warmed" }
|
||||||
t4_3 = { status = "pending", commit_sha = "", description = "Fix existing test_api_hooks.py breakage" }
|
t4_3 = { status = "pending", commit_sha = "", description = "Fix existing test_api_hooks.py breakage" }
|
||||||
t4_4 = { status = "pending", commit_sha = "", description = "Confirm T4.1 tests PASS" }
|
t4_4 = { status = "pending", commit_sha = "", description = "Confirm T4.1 tests PASS" }
|
||||||
t4_5 = { status = "pending", commit_sha = "", description = "Commit T4" }
|
t4_5 = { status = "pending", commit_sha = "", description = "Commit T4" }
|
||||||
# Phase 5A: Command Palette
|
# Phase 5: Remove top-level feature-gated GUI imports
|
||||||
t5a_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_command_palette_lazy.py" }
|
t5a_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_command_palette_no_top_level_import.py" }
|
||||||
t5a_2 = { status = "pending", commit_sha = "", description = "Green: lazy-load in src/commands.py" }
|
t5a_2 = { status = "pending", commit_sha = "", description = "Green: remove from src/commands.py; use _require_warmed" }
|
||||||
t5a_3 = { status = "pending", commit_sha = "", description = "Fix existing test_command_palette.py" }
|
t5a_3 = { status = "pending", commit_sha = "", description = "Fix existing test_command_palette.py" }
|
||||||
t5a_4 = { status = "pending", commit_sha = "", description = "Commit T5A" }
|
t5a_4 = { status = "pending", commit_sha = "", description = "Commit T5A" }
|
||||||
# Phase 5B: NERV Theme
|
t5b_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_theme_nerv_no_top_level_import.py" }
|
||||||
t5b_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_theme_nerv_lazy.py" }
|
t5b_2 = { status = "pending", commit_sha = "", description = "Green: remove from src/theme_2.py; use _require_warmed" }
|
||||||
t5b_2 = { status = "pending", commit_sha = "", description = "Green: lazy-load in src/theme_2.py" }
|
|
||||||
t5b_3 = { status = "pending", commit_sha = "", description = "Fix existing test_theme_2.py + test_theme_nerv.py" }
|
t5b_3 = { status = "pending", commit_sha = "", description = "Fix existing test_theme_2.py + test_theme_nerv.py" }
|
||||||
t5b_4 = { status = "pending", commit_sha = "", description = "Commit T5B" }
|
t5b_4 = { status = "pending", commit_sha = "", description = "Commit T5B" }
|
||||||
# Phase 5C: Markdown Table
|
t5c_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_markdown_helper_no_top_level_import.py" }
|
||||||
t5c_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_markdown_helper_lazy.py" }
|
t5c_2 = { status = "pending", commit_sha = "", description = "Green: remove from src/markdown_helper.py; use _require_warmed" }
|
||||||
t5c_2 = { status = "pending", commit_sha = "", description = "Green: lazy-load in src/markdown_helper.py" }
|
|
||||||
t5c_3 = { status = "pending", commit_sha = "", description = "Fix existing test_markdown_helper.py" }
|
t5c_3 = { status = "pending", commit_sha = "", description = "Fix existing test_markdown_helper.py" }
|
||||||
t5c_4 = { status = "pending", commit_sha = "", description = "Commit T5C" }
|
t5c_4 = { status = "pending", commit_sha = "", description = "Commit T5C" }
|
||||||
# Phase 5D: gui_2 feature-gated imports
|
|
||||||
t5d_1 = { status = "pending", commit_sha = "", description = "Run audit_gui2_imports.py and collect feature-gated list" }
|
t5d_1 = { status = "pending", commit_sha = "", description = "Run audit_gui2_imports.py and collect feature-gated list" }
|
||||||
t5d_2 = { status = "pending", commit_sha = "", description = "Apply TDD pattern per feature-gated import" }
|
t5d_2 = { status = "pending", commit_sha = "", description = "Apply same pattern per feature-gated import in src/gui_2.py; numpy in bg_shader" }
|
||||||
t5d_3 = { status = "pending", commit_sha = "", description = "Run full GUI test suite; fix" }
|
t5d_3 = { status = "pending", commit_sha = "", description = "Run full GUI test suite; fix" }
|
||||||
t5d_4 = { status = "pending", commit_sha = "", description = "Commit per feature group" }
|
t5d_4 = { status = "pending", commit_sha = "", description = "Commit per feature group" }
|
||||||
# Phase 6: Migrate ad-hoc threads
|
# Phase 6: Migrate ad-hoc threads
|
||||||
@@ -69,26 +68,29 @@ t6_1 = { status = "pending", commit_sha = "", description = "Audit threading.Thr
|
|||||||
t6_2 = { status = "pending", commit_sha = "", description = "Refactor each ad-hoc thread to use controller.submit_io" }
|
t6_2 = { status = "pending", commit_sha = "", description = "Refactor each ad-hoc thread to use controller.submit_io" }
|
||||||
t6_3 = { status = "pending", commit_sha = "", description = "Run full test suite; fix" }
|
t6_3 = { status = "pending", commit_sha = "", description = "Run full test suite; fix" }
|
||||||
t6_4 = { status = "pending", commit_sha = "", description = "Commit per migration; final grep shows zero new spawns" }
|
t6_4 = { status = "pending", commit_sha = "", description = "Commit per migration; final grep shows zero new spawns" }
|
||||||
# Phase 7: Enforcement - Runtime Audit Hook
|
# Phase 7: Warmup Notification (Hook API + GUI)
|
||||||
t7_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_main_thread_purity.py" }
|
t7a_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_api_hooks_warmup.py" }
|
||||||
t7_2 = { status = "pending", commit_sha = "", description = "Confirm test passes after Phase 3-5" }
|
t7a_2 = { status = "pending", commit_sha = "", description = "Green: add /api/warmup_status and /api/warmup_wait endpoints" }
|
||||||
t7_3 = { status = "pending", commit_sha = "", description = "Wire into CI as @pytest.mark.slow gating test" }
|
t7a_3 = { status = "pending", commit_sha = "", description = "Register warmup_status in _gettable_fields" }
|
||||||
t7_4 = { status = "pending", commit_sha = "", description = "Commit T7" }
|
t7a_4 = { status = "pending", commit_sha = "", description = "Commit T7A" }
|
||||||
# Phase 8: Hook API + Diagnostics
|
t7b_1 = { status = "pending", commit_sha = "", description = "Add status-bar indicator in src/gui_2.py that polls warmup_status each frame" }
|
||||||
t8_1 = { status = "pending", commit_sha = "", description = "Add /api/startup_profile endpoint" }
|
t7b_2 = { status = "pending", commit_sha = "", description = "Register on_warmup_complete callback that shows toast on success" }
|
||||||
t8_2 = { status = "pending", commit_sha = "", description = "Add /api/io_pool_status endpoint" }
|
t7b_3 = { status = "pending", commit_sha = "", description = "Update docs for status bar + hook API" }
|
||||||
t8_3 = { status = "pending", commit_sha = "", description = "Add startup profile + io_pool status to Diagnostics panel" }
|
t7b_4 = { status = "pending", commit_sha = "", description = "Commit T7B" }
|
||||||
t8_4 = { status = "pending", commit_sha = "", description = "Update docs/guide_api_hooks.md" }
|
# Phase 8: Enforcement - Runtime Audit Hook
|
||||||
t8_5 = { status = "pending", commit_sha = "", description = "Tests for endpoints + profiler round-trip" }
|
t8_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_main_thread_purity.py" }
|
||||||
t8_6 = { status = "pending", commit_sha = "", description = "Commit T8" }
|
t8_2 = { status = "pending", commit_sha = "", description = "Confirm test passes after Phase 3-5" }
|
||||||
|
t8_3 = { status = "pending", commit_sha = "", description = "Wire into CI as @pytest.mark.slow gating test" }
|
||||||
|
t8_4 = { status = "pending", commit_sha = "", description = "Commit T8" }
|
||||||
# Phase 9: Verify + Checkpoint
|
# Phase 9: Verify + Checkpoint
|
||||||
t9_1 = { status = "pending", commit_sha = "", description = "Re-run benchmark; diff vs baseline" }
|
t9_1 = { status = "pending", commit_sha = "", description = "Re-run benchmark; diff vs baseline" }
|
||||||
t9_2 = { status = "pending", commit_sha = "", description = "Re-run audit_main_thread_imports.py; exit 0" }
|
t9_2 = { status = "pending", commit_sha = "", description = "Re-run audit_main_thread_imports.py; exit 0" }
|
||||||
t9_3 = { status = "pending", commit_sha = "", description = "Run test_main_thread_purity.py; pass" }
|
t9_3 = { status = "pending", commit_sha = "", description = "Run test_warmup_mechanism.py; warmup completes and notifications fire" }
|
||||||
t9_4 = { status = "pending", commit_sha = "", description = "Run live_gui test batch; confirm wait_for_server passes" }
|
t9_4 = { status = "pending", commit_sha = "", description = "Run test_main_thread_purity.py; pass" }
|
||||||
t9_5 = { status = "pending", commit_sha = "", description = "Manual smoke (normal, test-hooks, headless modes)" }
|
t9_5 = { status = "pending", commit_sha = "", description = "Run live_gui test batch; confirm wait_for_server passes" }
|
||||||
t9_6 = { status = "pending", commit_sha = "", description = "Phase checkpoint commit + git note" }
|
t9_6 = { status = "pending", commit_sha = "", description = "Manual smoke: provider-switch is INSTANT after warmup" }
|
||||||
t9_7 = { status = "pending", commit_sha = "", description = "Update tracks.md; archive track" }
|
t9_7 = { status = "pending", commit_sha = "", description = "Phase checkpoint commit + git note" }
|
||||||
|
t9_8 = { status = "pending", commit_sha = "", description = "Update tracks.md; archive track" }
|
||||||
|
|
||||||
[verification]
|
[verification]
|
||||||
# To be filled at Phase 9
|
# To be filled at Phase 9
|
||||||
@@ -98,13 +100,31 @@ baseline_gui_2_ms = 0
|
|||||||
after_gui_2_ms = 0
|
after_gui_2_ms = 0
|
||||||
baseline_app_controller_ms = 0
|
baseline_app_controller_ms = 0
|
||||||
after_app_controller_ms = 0
|
after_app_controller_ms = 0
|
||||||
|
warmup_completes_within_seconds = 0
|
||||||
|
warmup_modules_in_sys_modules = 0
|
||||||
|
provider_switch_latency_ms_after_warmup = 0
|
||||||
live_gui_passed = 0
|
live_gui_passed = 0
|
||||||
live_gui_failed = 0
|
live_gui_failed = 0
|
||||||
audit_main_thread_violations = 0
|
audit_main_thread_violations = 0
|
||||||
io_pool_max_workers = 4
|
io_pool_max_workers = 4
|
||||||
io_pool_thread_name_prefix = "controller-io"
|
io_pool_thread_name_prefix = "controller-io"
|
||||||
new_threading_thread_calls = 0
|
new_threading_thread_calls = 0
|
||||||
|
function_body_heavy_imports = 0
|
||||||
|
|
||||||
[ad_hoc_threads]
|
[ad_hoc_threads]
|
||||||
# Filled in Phase 6 T6.1 audit
|
# Filled in Phase 6 T6.1 audit
|
||||||
# Format: {file = "src/foo.py", line = 42, current_target = "lambda", proposed_target = "controller.submit_io(...)"}
|
# Format: {file = "src/foo.py", line = 42, current_target = "lambda", proposed_target = "controller.submit_io(...)"}
|
||||||
|
|
||||||
|
[warmup_list]
|
||||||
|
# Filled in Phase 2 T2.4 implementation
|
||||||
|
google_genai = true
|
||||||
|
anthropic = true
|
||||||
|
openai = true
|
||||||
|
requests = true
|
||||||
|
src_command_palette = true
|
||||||
|
src_theme_nerv = true
|
||||||
|
src_theme_nerv_fx = true
|
||||||
|
src_markdown_table = true
|
||||||
|
numpy = true
|
||||||
|
fastapi = "conditional" # only when enable_test_hooks or web_host
|
||||||
|
fastapi_security_api_key = "conditional"
|
||||||
|
|||||||
Reference in New Issue
Block a user