3849d30441
Phase 4 T4.1-T4.4 of startup_speedup_20260606 track. DEVIATION FROM ORIGINAL SPEC: spec.md said fastapi was in src/api_hooks.py but it was actually in src/app_controller.py (lines 17, 21). api_hooks.py uses stdlib http.server. Phase 4 target corrected to app_controller. LIFTED _require_warmed TO SHARED MODULE: created src/module_loader.py to avoid duplicating the lookup logic and the cross-module import smell (app_controller -> ai_client). src/ai_client.py re-exports it so the T3.1 test (which asserts hasattr(src.ai_client, '_require_warmed')) continues to work. src/app_controller.py changes: - Added 'from __future__ import annotations' (enables lazy type annotations; -> FastAPI return type now a forward reference) - Removed 'from fastapi import FastAPI, Depends, HTTPException' (line 17) - Removed 'from fastapi.security.api_key import APIKeyHeader' (line 21) - Added 'from src.module_loader import _require_warmed' (cross-module via shared utility, not via ai_client) - create_api(): added lookups at top of function body - 7 _api_* helper functions (_api_get_key, _api_generate, _api_stream, _api_confirm_action, _api_get_session, _api_delete_session, _api_get_context): added 'HTTPException = _require_warmed(...).HTTPException' at top of each function body EFFECTIVENESS: - import src.app_controller no longer triggers fastapi import (saves ~470ms in main thread; only loaded when --enable-test-hooks is set) - When --enable-test-hooks is set, the AppController's warmup pre-loads fastapi on the _io_pool, so create_api()'s lookup is O(1) TESTS: - tests/test_app_controller_no_top_level_fastapi.py: 4/4 PASS (was 3 RED + 1 pass) - tests/test_ai_client_no_top_level_sdk_imports.py: 9/9 still PASS (re-export works) - tests/test_app_controller_mcp.py, test_app_controller_offloading.py: pass - tests/test_headless_service.py: 10/11 PASS (1 pre-existing failure test_generate_endpoint is a circular-import issue in google.genai, reproduces identically on stashed pre-Phase-4 state - NOT a regression from this change) - tests/test_hooks.py: pass NEXT: Phase 5 (feature-gated GUI module imports - command palette, NERV theme, markdown table), then Phase 6 (ad-hoc threads -> _io_pool).
54 lines
2.3 KiB
Python
54 lines
2.3 KiB
Python
"""Shared module loader for heavy imports.
|
|
|
|
Per startup_speedup_20260606 spec §2.2 Layer 1, 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`
|
|
at use sites. The warmup mechanism in `src/warmup.py` (driven by
|
|
`AppController.__init__`) pre-loads them on the `_io_pool` so the first
|
|
user-triggered call is instant — the import cost is paid during startup on
|
|
a background thread, not on the user's first click.
|
|
|
|
This module is the single home of the `_require_warmed` helper so that
|
|
multiple files (src/ai_client.py, src/app_controller.py, future Phase 5
|
|
feature-gated modules) share one canonical implementation instead of
|
|
duplicating the lookup logic. The earlier practice of defining the helper
|
|
locally in src/ai_client.py created a cross-module import smell
|
|
(app_controller -> ai_client) when other modules needed the same primitive.
|
|
|
|
Public API:
|
|
_require_warmed(name) -> module
|
|
O(1) sys.modules lookup if warmup ran; importlib fallback otherwise.
|
|
|
|
Why a function instead of a `from X import Y` inside each call site?
|
|
Per the spec, lazy-loading on first use causes user-perceptible lag when
|
|
a UI action propagates to a controller method that triggers the first
|
|
import. Proactive warmup on bg threads means the cost is paid during the
|
|
visible startup window, not on the user's click.
|
|
"""
|
|
|
|
import importlib
|
|
import sys
|
|
from typing import Any
|
|
|
|
|
|
def _require_warmed(name: str) -> Any:
|
|
"""Return a heavy module that the AppController's warmup should have loaded.
|
|
|
|
Heavy SDKs (anthropic, google.genai, openai, google.genai.types, requests,
|
|
fastapi, fastapi.security.api_key, src.command_palette, src.theme_nerv,
|
|
src.theme_nerv_fx, src.markdown_table, numpy) are warmed on
|
|
AppController's _io_pool at startup. This function expects them to
|
|
already be in sys.modules and just returns the cached module object. If
|
|
the module is NOT in sys.modules (e.g. in tests where warmup didn't
|
|
run), falls back to importlib so the call still works.
|
|
|
|
In production: this is an O(1) sys.modules lookup. The 1+ second
|
|
import cost is paid during startup on a bg thread, NOT on the first
|
|
user-triggered AI call.
|
|
"""
|
|
mod = sys.modules.get(name)
|
|
if mod is not None:
|
|
return mod
|
|
return importlib.import_module(name)
|