From e4fab6af3ad649f02b4e89157a71f76e0ede9cf3 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 30 Jul 2026 21:48:06 -0700 Subject: [PATCH] fix(mcp): cap live sessions to avoid OOM live_connect wrapped Worker(addr, port) with no memoryLimit, so the binding default (-1, unlimited) applied. A long-lived live session on a busy target grows unbounded -- every zone/message/ memory event stays resident until disconnect -- and can OOM-kill the whole server process rather than just that one instance. Worker already has a graceful cap: TracyWorker.cpp's receive loop checks memoryLimit and calls QueryTerminate() + a clean disconnect once exceeded, instead of continuing to grow. Just wasn't wired up from Python. Adds memory_limit_mb to live_connect (defaults to TRACY_MCP_LIVE_MEMORY_LIMIT_MB, 8192 if unset; 0 disables), converts to bytes for the Worker constructor, and reports the active limit in the connect response. --- extra/mcp/eval_guide.md | 5 +++++ extra/mcp/tracy_mcp.py | 26 ++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/extra/mcp/eval_guide.md b/extra/mcp/eval_guide.md index bb290a5f..95688147 100644 --- a/extra/mcp/eval_guide.md +++ b/extra/mcp/eval_guide.md @@ -94,6 +94,11 @@ frees it just because your conversation ends. - Call `unload_capture(instance_id)` as soon as you're done analyzing a capture. Don't rely on automatic eviction as your primary cleanup path — treat it as a backstop for forgotten sessions, not a substitute. +- `live_connect` caps a live session at `TRACY_MCP_LIVE_MEMORY_LIMIT_MB` + (default 8192, override per-call or set 0 to disable) — a long-lived + session on a busy target otherwise grows unbounded and can OOM-kill the + whole server process. Hitting the cap disconnects that instance cleanly; + data collected so far stays queryable and `save_trace`-able. - `list_instances` reports `idle_seconds`, `connected`, and `background_done` per instance; use it to spot stale ones before starting a new session, especially if you're about to load several captures for comparison. diff --git a/extra/mcp/tracy_mcp.py b/extra/mcp/tracy_mcp.py index 28a8e787..eb927ee1 100644 --- a/extra/mcp/tracy_mcp.py +++ b/extra/mcp/tracy_mcp.py @@ -222,6 +222,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")) _SWEEP_INTERVAL_S = float(os.environ.get("TRACY_MCP_SWEEP_INTERVAL_S", "300")) +_DEFAULT_LIVE_MEMORY_LIMIT_MB = int(os.environ.get("TRACY_MCP_LIVE_MEMORY_LIMIT_MB", "8192")) class Task: @@ -432,11 +433,25 @@ async def discover_instances(port_range: str = "8086-8095") -> list[dict]: @mcp_server.tool() -async def live_connect(address: str = "127.0.0.1", port: int = 8086, alias: str | None = None) -> str: +async def live_connect( + address: str = "127.0.0.1", + port: int = 8086, + alias: str | None = None, + memory_limit_mb: int | None = None, +) -> str: """ Connect to a live running Tracy-instrumented application. - Wraps Worker(addr, port, memoryLimit=-1). Returns the instance_id. + Wraps Worker(addr, port, memoryLimit). A long-lived session on a busy + target grows unbounded otherwise -- every zone/message/memory event + stays resident until disconnect, which can OOM-kill this whole process + rather than just the one instance. memory_limit_mb defaults to + TRACY_MCP_LIVE_MEMORY_LIMIT_MB (8192 if unset); pass 0 to disable. + Hitting the limit disconnects that Worker cleanly (Tracy's own + QueryTerminate path) -- already-collected data stays queryable and + save_trace-able, it just stops growing. + + Returns the instance_id. """ if not tracy_server: return "Error: Tracy Server bindings not found." @@ -458,8 +473,10 @@ async def live_connect(address: str = "127.0.0.1", port: int = 8086, alias: str f"the target against a matching Tracy version." ) + limit_mb = _DEFAULT_LIVE_MEMORY_LIMIT_MB if memory_limit_mb is None else memory_limit_mb + memory_limit = limit_mb * 1024 * 1024 if limit_mb > 0 else -1 try: - w = tracy_server.Worker(address, port) + w = tracy_server.Worker(address, port, memory_limit) except Exception as e: return f"Failed to connect: {str(e)}" @@ -508,8 +525,9 @@ async def live_connect(address: str = "127.0.0.1", port: int = 8086, alias: str evicted = _evict_for_capacity(exclude=name) instances[name] = TracyInstance(name, w) note = f" (evicted idle instance '{evicted}' to stay under the {_MAX_INSTANCES}-instance cap)" if evicted else "" + limit_note = f"{limit_mb}MB memory limit" if limit_mb > 0 else "no memory limit" return ( - f"Connected to live instance as '{name}'{note}. " + f"Connected to live instance as '{name}'{note} ({limit_note}). " f"Before your first eval, read resources tracy://prompt " f"(analysis guidance) and tracy://eval-guide (ctx object model, " f"ns time units, srcloc IDs)."