From a418ec6a03c54ffc2e4c46cb2cff6a3a5e8cc131 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 19 Jul 2026 02:04:50 -0700 Subject: [PATCH] fix(mcp): evict idle file-loaded captures too Automatic eviction only ever covered disconnected live instances (_evict_disconnected_idle explicitly skipped anything with a path), so a file-loaded capture -- potentially many GB -- stayed resident forever until the TRACY_MCP_MAX_INSTANCES cap forced an LRU eviction to make room. list_instances already documented "unload_capture instead of waiting for automatic eviction" as the alternative, but no automatic path actually existed for this case. Generalizes the sweep into _evict_idle: file-loaded instances now get their own idle-since-last-use TTL (TRACY_MCP_FILE_IDLE_TTL_S, default 1800s matching the disconnected-live TTL). Safe to evict on a timer since they're already durably on disk -- load_capture brings them back. Connected live instances are untouched, same as before. --- extra/mcp/tracy_mcp.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/extra/mcp/tracy_mcp.py b/extra/mcp/tracy_mcp.py index f3cf167e..aa71c16c 100644 --- a/extra/mcp/tracy_mcp.py +++ b/extra/mcp/tracy_mcp.py @@ -251,6 +251,7 @@ except ImportError: _MAX_INSTANCES = int(os.environ.get("TRACY_MCP_MAX_INSTANCES", "4")) _DISCONNECTED_TTL_S = float(os.environ.get("TRACY_MCP_DISCONNECTED_TTL_S", "1800")) +_FILE_IDLE_TTL_S = float(os.environ.get("TRACY_MCP_FILE_IDLE_TTL_S", "1800")) _SWEEP_INTERVAL_S = float(os.environ.get("TRACY_MCP_SWEEP_INTERVAL_S", "300")) @@ -303,17 +304,22 @@ def _shutdown_worker(worker: object | None) -> None: pass -def _evict_disconnected_idle() -> list[str]: - """Unload live instances that disconnected and have sat idle past the TTL. - - Runs on the periodic sweep. A grace period (_DISCONNECTED_TTL_S) is kept - so a capture is still around for post-session analysis right after the - target disconnects — this only reclaims sessions that were forgotten. +def _evict_idle() -> list[str]: + """Unload instances idle past their TTL: disconnected live instances + past _DISCONNECTED_TTL_S, file-loaded captures past _FILE_IDLE_TTL_S + (safe — already durably on disk). Connected live instances are never + touched here. """ now = time.time() evicted = [] for name, inst in list(instances.items()): - if inst.path is not None or inst.worker is None: + if inst.path is not None: + if now - inst.last_used >= _FILE_IDLE_TTL_S: + del instances[name] + _shutdown_worker(inst.worker) + evicted.append(name) + continue + if inst.worker is None: continue try: connected = inst.worker.is_connected() @@ -359,7 +365,7 @@ async def _sweep_loop() -> None: while True: await asyncio.sleep(_SWEEP_INTERVAL_S) try: - evicted = _evict_disconnected_idle() + evicted = _evict_idle() except Exception: evicted = None print( @@ -424,10 +430,12 @@ async def list_instances() -> list[dict]: process runs (it's a singleton shared across all MCP clients) until unloaded. `idle_seconds` is how long since this instance was last used via `eval`/`save_trace` — call `unload_capture` on ones you no longer - need instead of waiting for automatic eviction. Automatic eviction only - kicks in at the `TRACY_MCP_MAX_INSTANCES` cap (LRU, connected live - instances are never evicted) or after a disconnected live instance sits - idle past `TRACY_MCP_DISCONNECTED_TTL_S`. + need instead of waiting for automatic eviction. Automatic eviction kicks + in at the `TRACY_MCP_MAX_INSTANCES` cap (LRU, connected live instances + are never evicted), after a disconnected live instance sits idle past + `TRACY_MCP_DISCONNECTED_TTL_S`, or after a file-loaded capture sits idle + past `TRACY_MCP_FILE_IDLE_TTL_S` — it's already durably on disk, so + nothing is lost; just `load_capture` it again. `background_done: false` means a just-loaded capture is still building zone/symbol statistics on a background thread — `get_all_zone_stats`