feat(mcp): expose background-load readiness

Smoke-testing the lock fix against a real 143M-zone capture
surfaced a second concurrent-mutation source: file loads spawn a
background thread that finishes populating zone/symbol statistics
after Worker construction returns, so get_all_zone_stats() et al.
could silently come back empty or partial with no way to tell
"not built yet" from "genuinely empty".

Adds is_background_done (a relaxed atomic, same as is_connected,
so no locked() wrapper needed) and surfaces it as background_done
in list_instances, with load_capture's docstring pointing callers
at it.
This commit is contained in:
Alan Tse
2026-07-19 01:59:48 -07:00
parent 15bc3fce1a
commit bfcec08e93
2 changed files with 30 additions and 7 deletions

View File

@@ -385,6 +385,11 @@ async def list_instances() -> list[dict]:
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`.
`background_done: false` means a just-loaded capture is still building
zone/symbol statistics on a background thread — `get_all_zone_stats`
and similar stats-based `eval` queries can return empty or partial
results until this flips to true.
"""
now = time.time()
return [
@@ -394,6 +399,7 @@ async def list_instances() -> list[dict]:
"mtime": inst.mtime,
"live": inst.path is None,
"connected": inst.worker.is_connected() if inst.path is None and inst.worker else None,
"background_done": inst.worker.is_background_done() if inst.worker else None,
"idle_seconds": now - inst.last_used,
}
for name, inst in instances.items()
@@ -523,6 +529,12 @@ async def load_capture(path: str, alias: str | None = None) -> str:
If you don't already have a path, call `list_captures` first — it lists
.tracy files in the TRACY_CAPTURES_DIR environment directory.
Loading returns before zone/symbol statistics finish building — a
background thread populates them after the file itself is read. Check
`background_done` in `list_instances` (or poll it) before relying on
`get_all_zone_stats` and similar stats-based `eval` queries; they can
return empty or partial results until it's true.
"""
if not tracy_server:
return "Error: Tracy Server bindings not found."

View File

@@ -230,14 +230,21 @@ PYBIND11_MODULE( TracyServerBindings, m )
// --- Sections ---
.def( "get_sections", locked( +[]( const Worker& w ) {
// GetSections() is keyed by category (multiple named section sets);
// flatten across categories, tagging each item with its category name.
py::list result;
for( auto& s : w.GetSections() )
for( const auto& kv : w.GetSections() )
{
py::dict d;
d["start"] = s.start.Val();
d["end"] = s.end.Val();
d["text"] = std::string( w.GetString( s.text ) );
result.append( d );
const char* category = w.GetSectionCategoryDescription( kv.first );
for( const auto& s : kv.second )
{
py::dict d;
d["category"] = std::string( category );
d["start"] = s.start.Val();
d["end"] = s.end.Val();
d["text"] = std::string( w.GetString( s.text ) );
result.append( d );
}
}
return result;
} ) )
@@ -1098,7 +1105,11 @@ PYBIND11_MODULE( TracyServerBindings, m )
// --- Connection control ---
.def( "is_connected", &Worker::IsConnected )
.def( "shutdown", &Worker::Shutdown )
.def( "disconnect", &Worker::Disconnect );
.def( "disconnect", &Worker::Disconnect )
// Plain relaxed atomic like IsConnected() above — no locked()
// needed. True once post-load zone/symbol stats have finished
// building; get_all_zone_stats() et al. can be empty until then.
.def( "is_background_done", &Worker::IsBackgroundDone );
// -------------------------------------------------------------------------
// FileRead