f2f5ee1197
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.
80 lines
4.2 KiB
JSON
80 lines
4.2 KiB
JSON
{
|
|
"track_id": "startup_speedup_20260606",
|
|
"name": "Sloppy.py Startup Speedup",
|
|
"initialized": "2026-06-06",
|
|
"owner": "tier2-tech-lead",
|
|
"priority": "high",
|
|
"status": "active",
|
|
"type": "refactor + performance",
|
|
"scope": {
|
|
"new_files": [
|
|
"src/startup_profiler.py",
|
|
"scripts/audit_main_thread_imports.py",
|
|
"scripts/audit_gui2_imports.py",
|
|
"tests/test_ai_client_no_top_level_sdk_imports.py",
|
|
"tests/test_hook_server_no_top_level_fastapi.py",
|
|
"tests/test_app_controller_io_pool.py",
|
|
"tests/test_warmup_mechanism.py",
|
|
"tests/test_command_palette_no_top_level_import.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_startup_profiler.py",
|
|
"tests/test_io_pool_endpoint.py"
|
|
],
|
|
"modified_files": [
|
|
"src/ai_client.py",
|
|
"src/api_hooks.py",
|
|
"src/app_controller.py",
|
|
"src/commands.py",
|
|
"src/command_palette.py",
|
|
"src/theme_2.py",
|
|
"src/theme_nerv.py",
|
|
"src/theme_nerv_fx.py",
|
|
"src/markdown_helper.py",
|
|
"src/markdown_table.py",
|
|
"src/gui_2.py",
|
|
"src/log_pruner.py",
|
|
"src/project_manager.py"
|
|
]
|
|
},
|
|
"blocked_by": [],
|
|
"blocks": [],
|
|
"estimated_phases": 9,
|
|
"spec": "spec.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. 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'). 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": [
|
|
"import src.ai_client < 50ms cold start (from ~1800ms)",
|
|
"import src.gui_2 < 500ms cold start (from ~3000ms)",
|
|
"import src.app_controller < 300ms cold start (from ~700ms)",
|
|
"uv run sloppy.py --enable-test-hooks reaches immapp.run() in < 1.5s",
|
|
"live_gui.wait_for_server(timeout=15) passes for all tests",
|
|
"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)",
|
|
"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",
|
|
"ZERO new threading.Thread(...) calls in src/ (after Phase 6 migration)",
|
|
"Startup profile + io_pool status visible via /api/startup_profile, /api/io_pool_status"
|
|
],
|
|
"links": {
|
|
"backlog_entry": "conductor/tracks.md:152",
|
|
"benchmark_script": "scripts/benchmark_imports.py",
|
|
"audit_script": "scripts/audit_main_thread_imports.py",
|
|
"related_docs": [
|
|
"docs/guide_architecture.md",
|
|
"docs/guide_app_controller.md",
|
|
"docs/guide_hot_reload.md",
|
|
"docs/guide_testing.md"
|
|
]
|
|
}
|
|
}
|