diff --git a/extra/mcp/eval_guide.md b/extra/mcp/eval_guide.md index 6d51722c..0f929cf9 100644 --- a/extra/mcp/eval_guide.md +++ b/extra/mcp/eval_guide.md @@ -21,6 +21,11 @@ data surface. Common entry points: - Threads: `get_threads()`, `get_thread_name(tid)`, `get_thread_context_switches(tid)` - Messages / plots / locks / memory / callstacks: `get_messages()`, `get_plots()`, `get_locks()`, `get_memory_events()`, `get_callstack_frames(...)` +- Plot samples: `get_plot_samples(name, start_ns=0, end_ns=max, max_samples=100000)` returns + `(time_ns, value)` pairs, thinned to at most `max_samples`; + `get_plot_window_stats(name, start_ns, end_ns)` returns a `PlotSummary` + (count/min/max/sum/avg) over just that window. `get_plots()` only + summarises the whole trace. - Sections: `get_sections()` — timed code sections from `TracySectionEnter`/`TracySectionLeave` instrumentation. Returns a list of `{category, start, end, text}` dicts (start/end in ns); `category` is the diff --git a/extra/mcp/test_plot_samples.py b/extra/mcp/test_plot_samples.py new file mode 100644 index 00000000..d4c9c1d3 --- /dev/null +++ b/extra/mcp/test_plot_samples.py @@ -0,0 +1,48 @@ +"""Needs a saved trace that contains a plot: + TRACY_TEST_TRACE=/path/to.tracy TRACY_TEST_PLOT= python -m pytest extra/mcp/test_plot_samples.py +""" +import os +import time + +import pytest + +import tracy_mcp + +TRACE = os.environ.get("TRACY_TEST_TRACE") +PLOT = os.environ.get("TRACY_TEST_PLOT") +pytestmark = pytest.mark.skipif(not (TRACE and PLOT), reason="TRACY_TEST_TRACE and TRACY_TEST_PLOT not set") + + +@pytest.fixture(scope="module") +def worker(): + server = tracy_mcp.tracy_server + w = server.create_worker_from_file(server.open_file(TRACE)) + while not w.is_background_done(): + time.sleep(0.1) + yield w + w.shutdown() + + +def test_whole_window_matches_the_summary(worker): + summary = next(p for p in worker.get_plots() if p.name == PLOT) + window = worker.get_plot_window_stats(PLOT) + assert (window.count, window.min, window.max) == (summary.count, summary.min, summary.max) + + +def test_two_windows_partition_the_samples(worker): + first, last = worker.get_first_time(), worker.get_last_time() + mid = (first + last) // 2 + a = worker.get_plot_window_stats(PLOT, first, mid) + b = worker.get_plot_window_stats(PLOT, mid, last + 1) + assert a.count + b.count == worker.get_plot_window_stats(PLOT).count + + +def test_samples_are_thinned_and_ordered(worker): + samples = worker.get_plot_samples(PLOT, max_samples=10) + assert 0 < len(samples) <= 10 + assert [t for t, _ in samples] == sorted(t for t, _ in samples) + + +def test_unknown_plot_is_empty(worker): + assert worker.get_plot_samples("no such plot") == [] + assert worker.get_plot_window_stats("no such plot").count == 0 diff --git a/python/bindings/ServerModule.cpp b/python/bindings/ServerModule.cpp index bf91186f..7f82ffd4 100644 --- a/python/bindings/ServerModule.cpp +++ b/python/bindings/ServerModule.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #ifdef _MSC_VER @@ -1016,6 +1018,46 @@ PYBIND11_MODULE( TracyServerBindings, m ) return result; } ) ) + .def( "get_plot_samples", locked( +[]( const Worker& w, const std::string& name, int64_t startNs, int64_t endNs, size_t maxSamples ) { + std::vector> result; + for( const auto* pd : w.GetPlots() ) + { + if( !pd || w.GetString( pd->name ) != name ) continue; + auto first = std::lower_bound( pd->data.begin(), pd->data.end(), startNs, + []( const PlotItem& item, int64_t t ) { return item.time.Val() < t; } ); + auto last = std::lower_bound( first, pd->data.end(), endNs, + []( const PlotItem& item, int64_t t ) { return item.time.Val() < t; } ); + const size_t n = last - first; + const size_t step = maxSamples && n > maxSamples ? ( n + maxSamples - 1 ) / maxSamples : 1; + for( auto it = first; it < last; it += std::min( step, last - it ) ) + result.emplace_back( it->time.Val(), it->val ); + break; + } + return result; + } ), py::arg( "name" ), py::arg( "start_ns" ) = 0, py::arg( "end_ns" ) = std::numeric_limits::max(), py::arg( "max_samples" ) = 100000 ) + + .def( "get_plot_window_stats", locked( +[]( const Worker& w, const std::string& name, int64_t startNs, int64_t endNs ) { + PlotSummary result{ name, 0, 0, 0, 0, 0, "User" }; + static const char* plotTypeStr[] = { "User", "Memory", "SysTime", "Power" }; + for( const auto* pd : w.GetPlots() ) + { + if( !pd || w.GetString( pd->name ) != name ) continue; + result.type = (uint8_t)pd->type < 4 ? plotTypeStr[(uint8_t)pd->type] : "Unknown"; + auto first = std::lower_bound( pd->data.begin(), pd->data.end(), startNs, + []( const PlotItem& item, int64_t t ) { return item.time.Val() < t; } ); + for( auto it = first; it != pd->data.end() && it->time.Val() < endNs; ++it ) + { + if( result.count == 0 || it->val < result.min ) result.min = it->val; + if( result.count == 0 || it->val > result.max ) result.max = it->val; + result.sum += it->val; + result.count++; + } + result.avg = result.count ? result.sum / result.count : 0.0; + break; + } + return result; + } ), py::arg( "name" ), py::arg( "start_ns" ) = 0, py::arg( "end_ns" ) = std::numeric_limits::max() ) + // --- Memory pools --- .def( "get_memory_pools", locked( +[]( const Worker& w ) { std::vector result;