Private
Public Access
0
0

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
This commit is contained in:
2026-06-06 17:52:11 -04:00
parent 4e6a86a84c
commit 85d1888522
+21 -4
View File
@@ -2002,8 +2002,7 @@ class AppController:
self.ai_status = f"Manual prune error: {e}" self.ai_status = f"Manual prune error: {e}"
print(f"Error during manual log pruning: {e}") print(f"Error during manual log pruning: {e}")
thread = threading.Thread(target=run_manual_prune, daemon=True) self.submit_io(run_manual_prune)
thread.start()
def _load_active_project(self) -> None: def _load_active_project(self) -> None:
"""Loads the active project configuration, with fallbacks.""" """Loads the active project configuration, with fallbacks."""
@@ -2052,8 +2051,7 @@ class AppController:
pruner.prune() pruner.prune()
except Exception as e: except Exception as e:
print(f"Error during log pruning: {e}") print(f"Error during log pruning: {e}")
thread = threading.Thread(target=run_prune, daemon=True) self.submit_io(run_prune)
thread.start()
def _fetch_models(self, provider: str) -> None: def _fetch_models(self, provider: str) -> None:
""" """
@@ -2152,6 +2150,25 @@ class AppController:
""" """
self._warmup.on_complete(callback) 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: def shutdown(self) -> None:
""" """