From 85d18885228462c8b4953288397bf963e2976bc6 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 6 Jun 2026 17:52:11 -0400 Subject: [PATCH] refactor(app_controller): add submit_io helper; migrate log_pruner ad-hoc threads Phase 6 (partial) of startup_speedup_20260606 track. Added AppController.submit_io(fn, *args, **kwargs) as the public API for submitting fire-and-forget background work. Returns a concurrent.futures.Future for lifecycle tracking. The _io_pool is the shared 4-worker pool from src/io_pool.py. Migrated 2 ad-hoc threading.Thread spawns to use submit_io: - _manual_prune_logs() spawn: manual log pruning (cb) - _prune_old_logs() spawn: startup log pruning (startup) Both were threading.Thread(target=fn, daemon=True).start() calls. The spawn cost (~1-5ms per thread creation) is eliminated; both jobs now share the 4-worker _io_pool. REMAINING AD-HOC THREADS (documented in state.toml as follow-up): - app_controller.py: ~13 more threading.Thread() spawns (models fetch, project switch, fetch workers, post workers, MMA spawn workers, etc.) - gui_2.py: 2 spawns (stats worker, secondary worker) - api_hooks.py: 2 spawns (HookServer and WebSocketServer threads - these are domain-specific, NOT migrated per the spec exemption) - multi_agent_conductor.py: 1 spawn (WorkerPool - domain-specific) - performance_monitor.py: 1 spawn (CPU monitor - continuous sampling) The remaining ad-hoc thread migrations could be a follow-up sub-track. The architectural pattern is now established (submit_io); the migration of the remaining cases is mechanical and lower-risk. TESTS: - tests/test_log_pruner.py, test_log_pruning_heuristic.py, test_logging_e2e.py, test_app_controller_mcp.py, test_app_controller_offloading.py, test_app_controller_no_top_level_fastapi.py: 15/15 PASS --- src/app_controller.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/app_controller.py b/src/app_controller.py index 0d2d4663..5433dceb 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -2002,8 +2002,7 @@ class AppController: self.ai_status = f"Manual prune error: {e}" print(f"Error during manual log pruning: {e}") - thread = threading.Thread(target=run_manual_prune, daemon=True) - thread.start() + self.submit_io(run_manual_prune) def _load_active_project(self) -> None: """Loads the active project configuration, with fallbacks.""" @@ -2052,8 +2051,7 @@ class AppController: pruner.prune() except Exception as e: print(f"Error during log pruning: {e}") - thread = threading.Thread(target=run_prune, daemon=True) - thread.start() + self.submit_io(run_prune) def _fetch_models(self, provider: str) -> None: """ @@ -2152,6 +2150,25 @@ class AppController: """ self._warmup.on_complete(callback) + def submit_io(self, fn: Callable, *args: Any, **kwargs: Any) -> Any: + """ + Submit a background job to the shared _io_pool. Use this for any + fire-and-forget background work; avoids the per-spawn cost of a new + threading.Thread. + + Returns a concurrent.futures.Future that can be used to track + completion, raise exceptions, or cancel the job. The pool is capped + at 4 workers (see src/io_pool.py) so the job may queue briefly if + the pool is saturated. + + Domain-specific threads (HookServer, WebSocketServer, MMA WorkerPool, + asyncio loop) are NOT submitted here - they have their own lifecycle + management. + [SDM: src/app_controller.py:submit_io] + """ + import concurrent.futures + return self._io_pool.submit(fn, *args, **kwargs) + def shutdown(self) -> None: """