mirror of
https://github.com/wolfpld/tracy.git
synced 2026-07-25 07:29:14 +00:00
Every Worker-reading Python binding except save_worker() read m_data's zones/plots/threads/etc. without taking Worker::ObtainLockForMainThread(), racing the live receive thread that mutates the same structures. This matches alandtse/tracy#2: crashes only ever hit live instances, never file-loaded ones (no concurrent writer there), and cluster right after a big save_trace when the receive thread is catching up on backlog. Routes every Worker accessor through a new locked() wrapper using the same cooperative lock TracyView.cpp already takes once per frame, so reads can't race live writes without introducing new stalls on the profiled client.
1146 lines
46 KiB
C++
1146 lines
46 KiB
C++
#include <pybind11/pybind11.h>
|
|
#include <pybind11/stl.h>
|
|
#include <unordered_map>
|
|
|
|
#ifdef _MSC_VER
|
|
# pragma warning( push )
|
|
# pragma warning( disable : 4244 4267 ) // third-party ppqsort: narrowing conversions
|
|
#elif defined( __GNUC__ )
|
|
# pragma GCC diagnostic push
|
|
# pragma GCC diagnostic ignored "-Wconversion"
|
|
# pragma GCC diagnostic ignored "-Wnarrowing"
|
|
#endif
|
|
#include "../../server/TracyFileRead.hpp"
|
|
#include "../../server/TracyFileWrite.hpp"
|
|
#include "../../server/TracyWorker.hpp"
|
|
#ifdef _MSC_VER
|
|
# pragma warning( pop )
|
|
#elif defined( __GNUC__ )
|
|
# pragma GCC diagnostic pop
|
|
#endif
|
|
|
|
namespace py = pybind11;
|
|
using namespace pybind11::literals;
|
|
|
|
namespace tracy
|
|
{
|
|
|
|
// locked() wraps a capture-less Worker-reading lambda with
|
|
// Worker::ObtainLockForMainThread() before it touches w — the same
|
|
// cooperative lock the GUI already takes once per frame, so reads
|
|
// can't race the receive thread mutating live data underneath them.
|
|
template <typename Ret, typename... Args>
|
|
auto locked( Ret (*f)( const Worker&, Args... ) )
|
|
{
|
|
return [f]( Worker& w, Args... args ) -> Ret
|
|
{
|
|
auto lock = w.ObtainLockForMainThread();
|
|
return f( w, args... );
|
|
};
|
|
}
|
|
|
|
PYBIND11_MODULE( TracyServerBindings, m )
|
|
{
|
|
m.doc() = "Tracy Server (Analysis) Bindings";
|
|
|
|
// -------------------------------------------------------------------------
|
|
// SourceLocation
|
|
// -------------------------------------------------------------------------
|
|
py::class_<SourceLocation>( m, "SourceLocation" )
|
|
.def_readonly( "line", &SourceLocation::line );
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ZoneStats — POD summary returned by zone stat helpers
|
|
// -------------------------------------------------------------------------
|
|
struct ZoneStats
|
|
{
|
|
int64_t min;
|
|
int64_t max;
|
|
int64_t total;
|
|
double sumSq;
|
|
size_t count;
|
|
double avg;
|
|
};
|
|
|
|
py::class_<ZoneStats>( m, "ZoneStats" )
|
|
.def_readonly( "min", &ZoneStats::min )
|
|
.def_readonly( "max", &ZoneStats::max )
|
|
.def_readonly( "total", &ZoneStats::total )
|
|
.def_readonly( "sum_sq", &ZoneStats::sumSq )
|
|
.def_readonly( "count", &ZoneStats::count )
|
|
.def_readonly( "avg", &ZoneStats::avg );
|
|
|
|
// GpuZoneStats — GPU timestamps are the same int64_t nanosecond type;
|
|
// reuse ZoneStats rather than duplicating the struct.
|
|
using GpuZoneStats = ZoneStats;
|
|
|
|
// -------------------------------------------------------------------------
|
|
// FrameStats — per-frame-set timing summary
|
|
// -------------------------------------------------------------------------
|
|
struct FrameStats
|
|
{
|
|
std::string name;
|
|
int64_t min;
|
|
int64_t max;
|
|
int64_t total;
|
|
double sumSq;
|
|
size_t count;
|
|
double avg;
|
|
};
|
|
|
|
py::class_<FrameStats>( m, "FrameStats" )
|
|
.def_readonly( "name", &FrameStats::name )
|
|
.def_readonly( "min", &FrameStats::min )
|
|
.def_readonly( "max", &FrameStats::max )
|
|
.def_readonly( "total", &FrameStats::total )
|
|
.def_readonly( "sum_sq", &FrameStats::sumSq )
|
|
.def_readonly( "count", &FrameStats::count )
|
|
.def_readonly( "avg", &FrameStats::avg );
|
|
|
|
// -------------------------------------------------------------------------
|
|
// PlotSummary
|
|
// -------------------------------------------------------------------------
|
|
struct PlotSummary
|
|
{
|
|
std::string name;
|
|
double min;
|
|
double max;
|
|
double sum;
|
|
size_t count;
|
|
double avg;
|
|
std::string type;
|
|
};
|
|
|
|
py::class_<PlotSummary>( m, "PlotSummary" )
|
|
.def_readonly( "name", &PlotSummary::name )
|
|
.def_readonly( "min", &PlotSummary::min )
|
|
.def_readonly( "max", &PlotSummary::max )
|
|
.def_readonly( "sum", &PlotSummary::sum )
|
|
.def_readonly( "count", &PlotSummary::count )
|
|
.def_readonly( "avg", &PlotSummary::avg )
|
|
.def_readonly( "type", &PlotSummary::type );
|
|
|
|
// -------------------------------------------------------------------------
|
|
// MemPoolSummary
|
|
// -------------------------------------------------------------------------
|
|
struct MemPoolSummary
|
|
{
|
|
std::string name;
|
|
uint64_t high;
|
|
uint64_t low;
|
|
uint64_t usage;
|
|
size_t alloc_count;
|
|
};
|
|
|
|
py::class_<MemPoolSummary>( m, "MemPoolSummary" )
|
|
.def_readonly( "name", &MemPoolSummary::name )
|
|
.def_readonly( "high", &MemPoolSummary::high )
|
|
.def_readonly( "low", &MemPoolSummary::low )
|
|
.def_readonly( "usage", &MemPoolSummary::usage )
|
|
.def_readonly( "alloc_count", &MemPoolSummary::alloc_count );
|
|
|
|
// -------------------------------------------------------------------------
|
|
// LockSummary
|
|
// -------------------------------------------------------------------------
|
|
struct LockSummary
|
|
{
|
|
std::string name;
|
|
bool is_contended;
|
|
std::string type;
|
|
int64_t time_announce;
|
|
int64_t time_terminate;
|
|
std::vector<uint64_t> threads;
|
|
};
|
|
|
|
py::class_<LockSummary>( m, "LockSummary" )
|
|
.def_readonly( "name", &LockSummary::name )
|
|
.def_readonly( "is_contended", &LockSummary::is_contended )
|
|
.def_readonly( "type", &LockSummary::type )
|
|
.def_readonly( "time_announce", &LockSummary::time_announce )
|
|
.def_readonly( "time_terminate", &LockSummary::time_terminate )
|
|
.def_readonly( "threads", &LockSummary::threads );
|
|
|
|
// -------------------------------------------------------------------------
|
|
// GpuContextSummary
|
|
// -------------------------------------------------------------------------
|
|
struct GpuContextSummary
|
|
{
|
|
std::string name;
|
|
uint64_t count;
|
|
std::string type;
|
|
uint64_t thread;
|
|
};
|
|
|
|
py::class_<GpuContextSummary>( m, "GpuContextSummary" )
|
|
.def_readonly( "name", &GpuContextSummary::name )
|
|
.def_readonly( "count", &GpuContextSummary::count )
|
|
.def_readonly( "type", &GpuContextSummary::type )
|
|
.def_readonly( "thread", &GpuContextSummary::thread );
|
|
|
|
// -------------------------------------------------------------------------
|
|
// MessageInfo
|
|
// -------------------------------------------------------------------------
|
|
struct MessageInfo
|
|
{
|
|
int64_t time;
|
|
std::string text;
|
|
uint32_t color;
|
|
uint64_t thread;
|
|
};
|
|
|
|
py::class_<MessageInfo>( m, "MessageInfo" )
|
|
.def_readonly( "time", &MessageInfo::time )
|
|
.def_readonly( "text", &MessageInfo::text )
|
|
.def_readonly( "color", &MessageInfo::color )
|
|
.def_readonly( "thread", &MessageInfo::thread );
|
|
|
|
// ThreadData — get_threads() returns plain dicts to avoid pybind11
|
|
// raw-pointer ownership issues, so no class registration is needed.
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Worker
|
|
// -------------------------------------------------------------------------
|
|
auto worker_cls = py::class_<Worker>( m, "Worker" );
|
|
worker_cls
|
|
// Construction
|
|
.def( py::init<const char*, uint16_t, int64_t>(), "addr"_a, "port"_a, "memoryLimit"_a = -1 )
|
|
|
|
// --- Capture metadata ---
|
|
.def( "get_capture_name", locked( +[]( const Worker& w ) { return w.GetCaptureName(); } ) )
|
|
.def( "get_capture_program", locked( +[]( const Worker& w ) { return w.GetCaptureProgram(); } ) )
|
|
.def( "get_capture_time", locked( +[]( const Worker& w ) { return w.GetCaptureTime(); } ) )
|
|
.def( "get_host_info", locked( +[]( const Worker& w ) { return w.GetHostInfo(); } ) )
|
|
.def( "get_pid", locked( +[]( const Worker& w ) { return w.GetPid(); } ) )
|
|
.def( "get_resolution", locked( +[]( const Worker& w ) { return w.GetResolution(); } ) )
|
|
.def( "get_first_time", locked( +[]( const Worker& w ) { return w.GetFirstTime(); } ) )
|
|
.def( "get_last_time", locked( +[]( const Worker& w ) { return w.GetLastTime(); } ) )
|
|
.def( "get_cpu_manufacturer", locked( +[]( const Worker& w ) { return w.GetCpuManufacturer(); } ) )
|
|
|
|
// --- Counts ---
|
|
.def( "get_zone_count", locked( +[]( const Worker& w ) { return w.GetZoneCount(); } ) )
|
|
.def( "get_gpu_zone_count", locked( +[]( const Worker& w ) { return w.GetGpuZoneCount(); } ) )
|
|
.def( "get_lock_count", locked( +[]( const Worker& w ) { return w.GetLockCount(); } ) )
|
|
.def( "get_plot_count", locked( +[]( const Worker& w ) { return w.GetPlotCount(); } ) )
|
|
.def( "get_context_switch_count", locked( +[]( const Worker& w ) { return w.GetContextSwitchCount(); } ) )
|
|
.def( "get_src_loc_count", locked( +[]( const Worker& w ) { return w.GetSrcLocCount(); } ) )
|
|
.def( "get_callstack_sample_count", locked( +[]( const Worker& w ) { return w.GetCallstackSampleCount(); } ) )
|
|
.def( "get_message_count", locked( +[]( const Worker& w ) {
|
|
return w.GetMessages().size();
|
|
} ) )
|
|
|
|
// --- Sections ---
|
|
.def( "get_sections", locked( +[]( const Worker& w ) {
|
|
py::list result;
|
|
for( auto& s : 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 );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Source locations / zones ---
|
|
.def( "get_src_loc", locked( +[]( const Worker& w, int16_t id ) {
|
|
return w.GetSourceLocation( id );
|
|
} ) ).def( "get_zone_name", locked( +[]( const Worker& w, int16_t id ) {
|
|
return w.GetZoneName( w.GetSourceLocation( id ) );
|
|
} ) )
|
|
#ifndef TRACY_NO_STATISTICS
|
|
.def( "get_zone_stats", locked( +[]( const Worker& w, int16_t id ) {
|
|
const auto& stats = w.GetZonesForSourceLocation( id );
|
|
const size_t cnt = stats.zones.size();
|
|
return ZoneStats{ stats.min, stats.max, stats.total, stats.sumSq, cnt, cnt ? (double)stats.total / cnt : 0.0 };
|
|
} ) )
|
|
#endif
|
|
.def( "get_all_zone_stats", locked( +[]( const Worker& w ) {
|
|
py::dict result;
|
|
#ifndef TRACY_NO_STATISTICS
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
const auto& stats = kv.second;
|
|
if( stats.zones.size() == 0 ) continue;
|
|
const auto& sl = w.GetSourceLocation( kv.first );
|
|
const char* name = w.GetZoneName( sl );
|
|
const size_t cnt = stats.zones.size();
|
|
result[name] = ZoneStats{ stats.min, stats.max, stats.total, stats.sumSq, cnt, (double)stats.total / cnt };
|
|
}
|
|
#endif
|
|
return result;
|
|
} ) ).def( "get_root_zone_stats", locked( +[]( const Worker& w ) {
|
|
// Aggregate stats for top-level (root) zones only — no nesting, safe to sum
|
|
// File-loaded data uses is_magic() — zones stored inline, not as short_ptr
|
|
struct Acc
|
|
{
|
|
int64_t min = INT64_MAX, max = INT64_MIN, total = 0;
|
|
double sumSq = 0;
|
|
size_t count = 0;
|
|
};
|
|
std::unordered_map<int16_t, Acc> acc;
|
|
auto processRoot = [&]( const ZoneEvent& z ) {
|
|
if( !z.IsEndValid() ) return;
|
|
const int64_t dur = z.End() - z.Start();
|
|
auto& s = acc[z.SrcLoc()];
|
|
s.total += dur;
|
|
s.count++;
|
|
if( dur < s.min ) s.min = dur;
|
|
if( dur > s.max ) s.max = dur;
|
|
};
|
|
for( const auto* td : w.GetThreadData() )
|
|
{
|
|
if( !td ) continue;
|
|
if( td->timeline.is_magic() )
|
|
{
|
|
for( const auto& z : *(const Vector<ZoneEvent>*)&td->timeline ) processRoot( z );
|
|
}
|
|
else
|
|
{
|
|
for( const auto& zptr : td->timeline )
|
|
{
|
|
if( const ZoneEvent* z = zptr.get() ) processRoot( *z );
|
|
}
|
|
}
|
|
}
|
|
py::dict result;
|
|
for( const auto& kv : acc )
|
|
{
|
|
const auto& s = kv.second;
|
|
const double avg = (double)s.total / s.count;
|
|
const char* name = w.GetZoneName( w.GetSourceLocation( kv.first ) );
|
|
result[name] = ZoneStats{ s.min, s.max, s.total, s.sumSq, s.count, avg };
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Per-occurrence zone data (for temporal correlation / distribution) ---
|
|
.def( "get_zone_durations", locked( +[]( const Worker& w, const std::string& name, size_t maxSamples ) {
|
|
// Accumulates across ALL srclocs with this name (same name can appear at multiple srclocs)
|
|
std::vector<int64_t> result;
|
|
#ifndef TRACY_NO_STATISTICS
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( result.size() >= maxSamples ) goto done_durations;
|
|
const auto* z = ztd.Zone();
|
|
if( z && z->IsEndValid() ) result.push_back( z->End() - z->Start() );
|
|
}
|
|
}
|
|
done_durations:;
|
|
#endif
|
|
return result;
|
|
} ), "name"_a, "max_samples"_a = 100000 )
|
|
.def( "get_zone_occurrences", locked( +[]( const Worker& w, const std::string& name, size_t maxSamples ) {
|
|
// Returns list of (start_ns, duration_ns) — accumulates across all srclocs with this name
|
|
std::vector<std::pair<int64_t, int64_t>> result;
|
|
#ifndef TRACY_NO_STATISTICS
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( result.size() >= maxSamples ) goto done_occurrences;
|
|
const auto* z = ztd.Zone();
|
|
if( z && z->IsEndValid() ) result.emplace_back( z->Start(), z->End() - z->Start() );
|
|
}
|
|
}
|
|
done_occurrences:;
|
|
#endif
|
|
return result;
|
|
} ), "name"_a, "max_samples"_a = 100000 )
|
|
.def( "get_zone_annotations", locked( +[]( const Worker& w, const std::string& name, size_t maxSamples ) {
|
|
// Returns text annotations attached to individual zone occurrences
|
|
std::vector<std::string> result;
|
|
#ifndef TRACY_NO_STATISTICS
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( result.size() >= maxSamples ) goto done_annotations;
|
|
const auto* z = ztd.Zone();
|
|
if( z && w.HasZoneExtra( *z ) )
|
|
{
|
|
const auto& extra = w.GetZoneExtra( *z );
|
|
if( extra.text.Active() ) result.push_back( w.GetString( extra.text ) );
|
|
}
|
|
}
|
|
}
|
|
done_annotations:;
|
|
#endif
|
|
return result;
|
|
} ), "name"_a, "max_samples"_a = 10000 )
|
|
.def( "get_gpu_zone_durations", locked( +[]( const Worker& w, const std::string& name, size_t maxSamples ) {
|
|
std::vector<int64_t> result;
|
|
for( const auto& kv : w.GetGpuSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( result.size() >= maxSamples ) goto done_gpu_dur;
|
|
const auto* z = ztd.Zone();
|
|
if( z && z->GpuEnd() >= 0 ) result.push_back( z->GpuEnd() - z->GpuStart() );
|
|
}
|
|
}
|
|
done_gpu_dur:;
|
|
return result;
|
|
} ), "name"_a, "max_samples"_a = 100000 )
|
|
.def( "get_gpu_zone_occurrences", locked( +[]( const Worker& w, const std::string& name, size_t maxSamples ) {
|
|
std::vector<std::pair<int64_t, int64_t>> result;
|
|
for( const auto& kv : w.GetGpuSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( result.size() >= maxSamples ) goto done_gpu_occ;
|
|
const auto* z = ztd.Zone();
|
|
if( z && z->GpuEnd() >= 0 ) result.emplace_back( z->GpuStart(), z->GpuEnd() - z->GpuStart() );
|
|
}
|
|
}
|
|
done_gpu_occ:;
|
|
return result;
|
|
} ), "name"_a, "max_samples"_a = 100000 )
|
|
|
|
// --- Callstack resolution ---
|
|
.def( "get_callstack_frames", locked( +[]( const Worker& w, uint32_t callstackIdx ) {
|
|
py::list result;
|
|
const auto& cs = w.GetCallstack( callstackIdx );
|
|
for( size_t i = 0; i < cs.size(); ++i )
|
|
{
|
|
const auto* fd = w.GetCallstackFrame( cs[i] );
|
|
if( !fd ) continue;
|
|
for( uint8_t j = 0; j < fd->size; ++j )
|
|
{
|
|
const auto& frame = fd->data[j];
|
|
py::dict d;
|
|
d["name"] = std::string( w.GetString( frame.name ) );
|
|
d["file"] = std::string( w.GetString( frame.file ) );
|
|
d["line"] = frame.line;
|
|
d["addr"] = frame.symAddr;
|
|
result.append( d );
|
|
}
|
|
}
|
|
return result;
|
|
} ), "callstack_idx"_a )
|
|
|
|
// --- Context switches per thread ---
|
|
.def( "get_thread_context_switches", locked( +[]( const Worker& w, uint64_t tid, size_t maxSamples ) {
|
|
py::list result;
|
|
const auto* cs = const_cast<Worker&>( w ).GetContextSwitchData( tid );
|
|
if( !cs ) return result;
|
|
for( const auto& ev : cs->v )
|
|
{
|
|
if( (size_t)result.size() >= maxSamples ) break;
|
|
if( !ev.IsEndValid() ) continue;
|
|
py::dict d;
|
|
d["start"] = ev.Start();
|
|
d["end"] = ev.End();
|
|
d["cpu"] = (int)ev.Cpu();
|
|
d["reason"] = (int)ev.Reason();
|
|
result.append( d );
|
|
}
|
|
return result;
|
|
} ), "tid"_a, "max_samples"_a = 50000 )
|
|
|
|
// --- CPU thread running time / migrations ---
|
|
.def( "get_cpu_thread_data", locked( +[]( const Worker& w ) {
|
|
py::dict result;
|
|
for( const auto& kv : w.GetCpuThreadData() )
|
|
{
|
|
py::dict d;
|
|
d["running_time"] = kv.second.runningTime;
|
|
d["running_regions"] = kv.second.runningRegions;
|
|
d["migrations"] = kv.second.migrations;
|
|
result[py::int_( kv.first )] = d;
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Zone occurrences with thread attribution ---
|
|
.def( "get_zone_occurrences_with_thread", locked( +[]( const Worker& w, const std::string& name, size_t maxSamples ) {
|
|
// Returns list of (start_ns, duration_ns, thread_id) — thread_id is the OS thread ID
|
|
std::vector<std::tuple<int64_t, int64_t, uint64_t>> result;
|
|
#ifndef TRACY_NO_STATISTICS
|
|
const auto& threads = w.GetThreadData();
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( result.size() >= maxSamples ) goto done_occ_thread;
|
|
const auto* z = ztd.Zone();
|
|
if( !z || !z->IsEndValid() ) continue;
|
|
const uint16_t tidx = ztd.Thread();
|
|
const uint64_t tid = ( tidx < threads.size() && threads[tidx] ) ? threads[tidx]->id : 0;
|
|
result.emplace_back( z->Start(), z->End() - z->Start(), tid );
|
|
}
|
|
}
|
|
done_occ_thread:;
|
|
#endif
|
|
return result;
|
|
} ), "name"_a, "max_samples"_a = 100000 )
|
|
|
|
// --- Child zone stats: aggregate direct children of all occurrences of a parent zone ---
|
|
.def( "get_child_zone_stats", locked( +[]( const Worker& w, const std::string& name, size_t maxParents ) {
|
|
// Uses SourceLocationZones for O(occurrences) lookup — avoids walking the full zone tree.
|
|
// File-loaded data sets is_magic() on child vectors (inline ZoneEvent, not short_ptr).
|
|
struct Acc
|
|
{
|
|
int64_t min = INT64_MAX, max = INT64_MIN, total = 0;
|
|
double sumSq = 0.0;
|
|
size_t count = 0;
|
|
};
|
|
std::unordered_map<int16_t, Acc> acc;
|
|
size_t parentCount = 0;
|
|
|
|
auto accumChild = [&]( const ZoneEvent& c ) {
|
|
if( !c.IsEndValid() ) return;
|
|
const int64_t dur = c.End() - c.Start();
|
|
auto& s = acc[c.SrcLoc()];
|
|
s.total += dur;
|
|
s.count++;
|
|
s.sumSq += (double)dur * dur;
|
|
if( dur < s.min ) s.min = dur;
|
|
if( dur > s.max ) s.max = dur;
|
|
};
|
|
|
|
#ifndef TRACY_NO_STATISTICS
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( parentCount >= maxParents ) goto done_children;
|
|
const auto* z = ztd.Zone();
|
|
if( !z || !z->IsEndValid() || !z->HasChildren() ) continue;
|
|
parentCount++;
|
|
const auto& ch = w.GetZoneChildren( z->Child() );
|
|
if( ch.is_magic() )
|
|
{
|
|
for( const auto& c : *(const Vector<ZoneEvent>*)&ch ) accumChild( c );
|
|
}
|
|
else
|
|
{
|
|
for( const auto& cptr : ch )
|
|
{
|
|
if( const ZoneEvent* c = cptr.get() ) accumChild( *c );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
done_children:;
|
|
#endif
|
|
py::dict result;
|
|
for( const auto& kv : acc )
|
|
{
|
|
const auto& s = kv.second;
|
|
if( s.count == 0 ) continue;
|
|
const double avg = (double)s.total / (double)s.count;
|
|
const char* cname = w.GetZoneName( w.GetSourceLocation( kv.first ) );
|
|
result[cname] = ZoneStats{ s.min, s.max, s.total, s.sumSq, s.count, avg };
|
|
}
|
|
return result;
|
|
} ), "name"_a, "max_parents"_a = 100000 )
|
|
|
|
// --- Zone source location (file / line / function for LLM code navigation) ---
|
|
.def( "get_zone_source_location", locked( +[]( const Worker& w, const std::string& name ) {
|
|
py::dict result;
|
|
#ifndef TRACY_NO_STATISTICS
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
const auto& sl = w.GetSourceLocation( kv.first );
|
|
if( std::string( w.GetZoneName( sl ) ) != name ) continue;
|
|
result["name"] = name;
|
|
result["function"] = std::string( w.GetString( sl.function ) );
|
|
result["file"] = std::string( w.GetString( sl.file ) );
|
|
result["line"] = sl.line;
|
|
result["color"] = sl.color;
|
|
break;
|
|
}
|
|
#endif
|
|
return result;
|
|
} ), "name"_a )
|
|
.def( "get_all_zone_source_locations", locked( +[]( const Worker& w ) {
|
|
// Returns {zone_name: {file, line, function, color}} for every unique zone name.
|
|
// Uses first srcloc found per name — sufficient for navigation purposes.
|
|
py::dict result;
|
|
#ifndef TRACY_NO_STATISTICS
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
const auto& sl = w.GetSourceLocation( kv.first );
|
|
const char* name = w.GetZoneName( sl );
|
|
if( result.contains( name ) ) continue;
|
|
py::dict d;
|
|
d["function"] = std::string( w.GetString( sl.function ) );
|
|
d["file"] = std::string( w.GetString( sl.file ) );
|
|
d["line"] = sl.line;
|
|
d["color"] = sl.color;
|
|
result[name] = d;
|
|
}
|
|
#endif
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Per-zone callstack samples (call paths leading into a zone) ---
|
|
.def( "get_zone_callstacks", locked( +[]( const Worker& w, const std::string& name, size_t maxSamples ) {
|
|
py::list result;
|
|
#ifndef TRACY_NO_STATISTICS
|
|
for( const auto& kv : w.GetSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( (size_t)result.size() >= maxSamples ) goto done_callstacks;
|
|
const auto* z = ztd.Zone();
|
|
if( !z || !w.HasZoneExtra( *z ) ) continue;
|
|
const auto& extra = w.GetZoneExtra( *z );
|
|
const uint32_t csIdx = extra.callstack.Val();
|
|
if( csIdx == 0 ) continue;
|
|
py::list frames;
|
|
const auto& cs = w.GetCallstack( csIdx );
|
|
for( size_t i = 0; i < cs.size(); ++i )
|
|
{
|
|
const auto* fd = w.GetCallstackFrame( cs[i] );
|
|
if( !fd ) continue;
|
|
for( uint8_t j = 0; j < fd->size; ++j )
|
|
{
|
|
const auto& frame = fd->data[j];
|
|
py::dict d;
|
|
d["name"] = std::string( w.GetString( frame.name ) );
|
|
d["file"] = std::string( w.GetString( frame.file ) );
|
|
d["line"] = frame.line;
|
|
d["addr"] = frame.symAddr;
|
|
frames.append( d );
|
|
}
|
|
}
|
|
result.append( frames );
|
|
}
|
|
}
|
|
done_callstacks:;
|
|
#endif
|
|
return result;
|
|
} ), "name"_a, "max_samples"_a = 1000 )
|
|
|
|
// --- Symbol-level sampling stats (inclusive / exclusive counts from call-stack profiling) ---
|
|
.def( "get_symbol_stats", locked( +[]( const Worker& w ) {
|
|
py::list result;
|
|
for( const auto& kv : w.GetSymbolStats() )
|
|
{
|
|
const uint64_t addr = kv.first;
|
|
const auto& stats = kv.second;
|
|
py::dict d;
|
|
d["addr"] = addr;
|
|
d["incl"] = stats.incl;
|
|
d["excl"] = stats.excl;
|
|
const auto* sym = w.GetSymbolData( addr );
|
|
if( sym )
|
|
{
|
|
d["name"] = std::string( w.GetString( sym->name ) );
|
|
d["file"] = std::string( w.GetString( sym->file ) );
|
|
d["line"] = sym->line;
|
|
d["image"] = std::string( w.GetString( sym->imageName ) );
|
|
}
|
|
result.append( d );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Timestamps of all call-stack samples hitting a specific symbol ---
|
|
.def( "get_samples_for_symbol", locked( +[]( const Worker& w, uint64_t symAddr ) {
|
|
py::list result;
|
|
const auto* samples = w.GetSamplesForSymbol( symAddr );
|
|
if( !samples ) return result;
|
|
for( const auto& s : *samples )
|
|
{
|
|
py::dict d;
|
|
d["time"] = s.time.Val();
|
|
d["thread"] = (uint32_t)s.thread;
|
|
result.append( d );
|
|
}
|
|
return result;
|
|
} ), "sym_addr"_a )
|
|
|
|
// --- Hardware performance counter summary per symbol (IPC, cache-miss rate, branch-miss rate) ---
|
|
.def( "get_hw_sample_summary", locked( +[]( const Worker& w ) {
|
|
py::list result;
|
|
for( const auto& kv : w.GetSymbolStats() )
|
|
{
|
|
const uint64_t addr = kv.first;
|
|
auto* hw = const_cast<Worker&>( w ).GetHwSampleData( addr );
|
|
if( !hw || ( hw->cycles.empty() && hw->retired.empty() ) ) continue;
|
|
auto mean = []( const auto& v ) -> double {
|
|
if( v.empty() ) return 0.0;
|
|
double sum = 0.0;
|
|
for( const auto& x : v ) sum += (double)x.Val();
|
|
return sum / (double)v.size();
|
|
};
|
|
const double cyc = mean( hw->cycles );
|
|
const double ret = mean( hw->retired );
|
|
const double cmr = mean( hw->cacheRef );
|
|
const double cmm = mean( hw->cacheMiss );
|
|
const double brr = mean( hw->branchRetired );
|
|
const double brm = mean( hw->branchMiss );
|
|
py::dict d;
|
|
d["addr"] = addr;
|
|
d["samples"] = hw->cycles.empty() ? hw->retired.size() : hw->cycles.size();
|
|
d["cycles_mean"] = cyc;
|
|
d["retired_mean"] = ret;
|
|
d["cache_ref_mean"] = cmr;
|
|
d["cache_miss_mean"] = cmm;
|
|
d["branch_ret_mean"] = brr;
|
|
d["branch_miss_mean"] = brm;
|
|
d["ipc"] = ( cyc > 0.0 && ret > 0.0 ) ? ret / cyc : -1.0;
|
|
d["cache_miss_rate"] = ( cmr > 0.0 ) ? cmm / cmr : -1.0;
|
|
d["branch_miss_rate"] = ( brr > 0.0 ) ? brm / brr : -1.0;
|
|
const auto* sym = w.GetSymbolData( addr );
|
|
d["name"] = sym ? std::string( w.GetString( sym->name ) ) : std::string( "" );
|
|
d["file"] = sym ? std::string( w.GetString( sym->file ) ) : std::string( "" );
|
|
d["line"] = sym ? sym->line : 0u;
|
|
d["image"] = sym ? std::string( w.GetString( sym->imageName ) ) : std::string( "" );
|
|
result.append( d );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Raw memory allocation events (ptr, size, timestamps) for temporal zone correlation ---
|
|
.def( "get_memory_events", locked( +[]( const Worker& w, size_t maxCount, const std::string& poolName ) {
|
|
py::list result;
|
|
for( const auto& kv : w.GetMemNameMap() )
|
|
{
|
|
const std::string name = kv.first == 0
|
|
? std::string( "(default)" )
|
|
: std::string( w.GetString( kv.first ) );
|
|
if( !poolName.empty() && name != poolName ) continue;
|
|
const MemData* md = kv.second;
|
|
for( const auto& ev : md->data )
|
|
{
|
|
if( (size_t)result.size() >= maxCount ) break;
|
|
py::dict d;
|
|
d["pool"] = name;
|
|
d["ptr"] = ev.Ptr();
|
|
d["size"] = ev.Size();
|
|
d["time_alloc"] = ev.TimeAlloc();
|
|
d["time_free"] = ev.TimeFree();
|
|
d["thread_alloc"] = (uint32_t)ev.ThreadAlloc();
|
|
d["callstack_idx"] = (uint32_t)ev.CsAlloc();
|
|
result.append( d );
|
|
}
|
|
if( !poolName.empty() ) break;
|
|
}
|
|
return result;
|
|
} ), "max_count"_a = 100000, "pool_name"_a = "" )
|
|
|
|
// --- Per-lock wait/contention stats (total and average wait time) ---
|
|
.def( "get_lock_wait_stats", locked( +[]( const Worker& w ) {
|
|
py::list result;
|
|
for( const auto& kv : w.GetLockMap() )
|
|
{
|
|
const LockMap* lm = kv.second;
|
|
if( !lm || !lm->valid || !lm->isContended ) continue;
|
|
std::string name;
|
|
if( lm->customName.Active() )
|
|
name = w.GetString( lm->customName );
|
|
else
|
|
name = w.GetZoneName( w.GetSourceLocation( lm->srcloc ) );
|
|
int64_t totalWaitNs = 0;
|
|
uint64_t contentionCount = 0;
|
|
std::unordered_map<uint8_t, int64_t> pendingWait;
|
|
for( const auto& evPtr : lm->timeline )
|
|
{
|
|
const auto* ev = evPtr.ptr.get();
|
|
if( !ev ) continue;
|
|
if( ev->type == LockEvent::Type::Wait || ev->type == LockEvent::Type::WaitShared )
|
|
{
|
|
pendingWait[ev->thread] = ev->Time();
|
|
}
|
|
else if( ev->type == LockEvent::Type::Obtain || ev->type == LockEvent::Type::ObtainShared )
|
|
{
|
|
auto it = pendingWait.find( ev->thread );
|
|
if( it != pendingWait.end() )
|
|
{
|
|
totalWaitNs += ev->Time() - it->second;
|
|
contentionCount++;
|
|
pendingWait.erase( it );
|
|
}
|
|
}
|
|
}
|
|
if( contentionCount == 0 ) continue;
|
|
py::dict d;
|
|
d["name"] = name;
|
|
d["total_wait_ns"] = totalWaitNs;
|
|
d["avg_wait_ns"] = (double)totalWaitNs / (double)contentionCount;
|
|
d["contention_count"] = contentionCount;
|
|
d["threads"] = lm->threadList;
|
|
result.append( d );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- GPU zone stats ---
|
|
.def( "get_all_gpu_zone_stats", locked( +[]( const Worker& w ) {
|
|
py::dict result;
|
|
for( const auto& kv : w.GetGpuSourceLocationZones() )
|
|
{
|
|
const auto& sl = w.GetSourceLocation( kv.first );
|
|
const char* name = w.GetZoneName( sl );
|
|
const auto& s = kv.second;
|
|
const size_t cnt = s.zones.size();
|
|
if( cnt > 0 )
|
|
result[name] = GpuZoneStats{ s.min, s.max, s.total, s.sumSq, cnt, (double)s.total / cnt };
|
|
}
|
|
return result;
|
|
} ) ).def( "get_gpu_child_zone_stats", locked( +[]( const Worker& w, const std::string& name, size_t maxParents ) {
|
|
// GPU equivalent of get_child_zone_stats — returns per-child-name GPU duration stats
|
|
// for all occurrences of the named parent GPU zone.
|
|
struct Acc
|
|
{
|
|
int64_t min = INT64_MAX, max = INT64_MIN, total = 0;
|
|
double sumSq = 0.0;
|
|
size_t count = 0;
|
|
};
|
|
std::unordered_map<int16_t, Acc> acc;
|
|
size_t parentCount = 0;
|
|
|
|
auto accumChild = [&]( const GpuEvent& c ) {
|
|
if( c.GpuEnd() < 0 ) return;
|
|
const int64_t dur = c.GpuEnd() - c.GpuStart();
|
|
if( dur < 0 ) return;
|
|
auto& s = acc[c.SrcLoc()];
|
|
s.total += dur;
|
|
s.count++;
|
|
s.sumSq += (double)dur * dur;
|
|
if( dur < s.min ) s.min = dur;
|
|
if( dur > s.max ) s.max = dur;
|
|
};
|
|
|
|
for( const auto& kv : w.GetGpuSourceLocationZones() )
|
|
{
|
|
if( std::string( w.GetZoneName( w.GetSourceLocation( kv.first ) ) ) != name ) continue;
|
|
for( const auto& ztd : kv.second.zones )
|
|
{
|
|
if( parentCount >= maxParents ) goto done_gpu_child;
|
|
const auto* z = ztd.Zone();
|
|
if( !z || z->GpuEnd() < 0 || z->Child() < 0 ) continue;
|
|
parentCount++;
|
|
for( const auto& cptr : w.GetGpuChildren( z->Child() ) )
|
|
{
|
|
if( const GpuEvent* c = cptr.get() ) accumChild( *c );
|
|
}
|
|
}
|
|
}
|
|
done_gpu_child:;
|
|
|
|
py::dict result;
|
|
for( const auto& kv : acc )
|
|
{
|
|
const auto& s = kv.second;
|
|
if( s.count == 0 ) continue;
|
|
const char* cname = w.GetZoneName( w.GetSourceLocation( kv.first ) );
|
|
result[cname] = GpuZoneStats{ s.min, s.max, s.total, s.sumSq, s.count, (double)s.total / s.count };
|
|
}
|
|
return result;
|
|
} ), "name"_a, "max_parents"_a = 100000 )
|
|
|
|
// --- Frame sets ---
|
|
.def( "get_frame_count", locked( +[]( const Worker& w ) -> size_t {
|
|
auto frames = w.GetFramesBase();
|
|
return frames ? w.GetFrameCount( *frames ) : 0;
|
|
} ) ).def( "get_all_frame_stats", locked( +[]( const Worker& w ) {
|
|
std::vector<FrameStats> result;
|
|
for( const auto* fd : w.GetFrames() )
|
|
{
|
|
if( !fd ) continue;
|
|
const size_t cnt = fd->frames.size();
|
|
const std::string name = w.GetString( fd->name );
|
|
result.push_back( FrameStats{
|
|
name, fd->min, fd->max, fd->total, fd->sumSq,
|
|
cnt, cnt ? (double)fd->total / cnt : 0.0 } );
|
|
}
|
|
return result;
|
|
} ) ).def( "get_frame_boundaries", locked( +[]( const Worker& w ) {
|
|
auto* fd = w.GetFramesBase();
|
|
if( !fd ) return std::vector<std::pair<int64_t, int64_t>>{};
|
|
const size_t cnt = w.GetFrameCount( *fd );
|
|
std::vector<std::pair<int64_t, int64_t>> result;
|
|
result.reserve( cnt );
|
|
for( size_t i = 0; i < cnt; ++i )
|
|
result.emplace_back( w.GetFrameBegin( *fd, i ), w.GetFrameEnd( *fd, i ) );
|
|
return result;
|
|
} ) ).def( "get_frame_times", locked( +[]( const Worker& w ) {
|
|
auto* fd = w.GetFramesBase();
|
|
if( !fd ) return std::vector<int64_t>{};
|
|
const size_t cnt = w.GetFrameCount( *fd );
|
|
std::vector<int64_t> times;
|
|
times.reserve( cnt );
|
|
for( size_t i = 0; i < cnt; ++i )
|
|
times.push_back( w.GetFrameTime( *fd, i ) );
|
|
return times;
|
|
} ) ).def( "get_frame_times_named", locked( +[]( const Worker& w, const std::string& name ) {
|
|
for( const auto* fd : w.GetFrames() )
|
|
{
|
|
if( !fd ) continue;
|
|
if( w.GetString( fd->name ) == name )
|
|
{
|
|
const size_t cnt = w.GetFrameCount( *fd );
|
|
std::vector<int64_t> times;
|
|
times.reserve( cnt );
|
|
for( size_t i = 0; i < cnt; ++i )
|
|
times.push_back( w.GetFrameTime( *fd, i ) );
|
|
return times;
|
|
}
|
|
}
|
|
return std::vector<int64_t>{};
|
|
} ) ).def( "get_zones_in_frame", locked( +[]( const Worker& w, size_t frameIdx ) {
|
|
// Returns {zone_name: {count, total_ns}} for all CPU zones that STARTED within
|
|
// the specified frame's time window. Uses sorted thread timelines for early exit.
|
|
auto* fd = w.GetFramesBase();
|
|
if( !fd || frameIdx >= (size_t)w.GetFrameCount( *fd ) ) return py::dict{};
|
|
|
|
const int64_t frameStart = w.GetFrameBegin( *fd, (int)frameIdx );
|
|
const int64_t frameEnd = w.GetFrameEnd( *fd, (int)frameIdx );
|
|
|
|
struct Acc
|
|
{
|
|
int64_t total = 0;
|
|
size_t count = 0;
|
|
};
|
|
std::unordered_map<int16_t, Acc> acc;
|
|
|
|
// Returns false when a zone starts at or after frameEnd (prune signal
|
|
// for sorted sibling lists). Uses a local struct instead of std::function
|
|
// to avoid per-call heap allocation on the hot recursive path.
|
|
struct Visitor
|
|
{
|
|
const Worker& w;
|
|
std::unordered_map<int16_t, Acc>& acc;
|
|
int64_t frameStart, frameEnd;
|
|
|
|
bool operator()( const ZoneEvent& z )
|
|
{
|
|
if( !z.IsEndValid() ) return true;
|
|
const int64_t zs = z.Start();
|
|
if( zs >= frameEnd ) return false;
|
|
if( zs >= frameStart )
|
|
{
|
|
auto& s = acc[z.SrcLoc()];
|
|
s.total += z.End() - zs;
|
|
s.count++;
|
|
}
|
|
if( z.HasChildren() && z.End() > frameStart )
|
|
{
|
|
const auto& ch = w.GetZoneChildren( z.Child() );
|
|
if( ch.is_magic() )
|
|
{
|
|
for( const auto& c : *(const Vector<ZoneEvent>*)&ch )
|
|
{
|
|
if( !( *this )( c ) ) break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for( const auto& cptr : ch )
|
|
{
|
|
if( const ZoneEvent* c = cptr.get() )
|
|
{
|
|
if( !( *this )( *c ) ) break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
} visit{ w, acc, frameStart, frameEnd };
|
|
|
|
for( const auto* td : w.GetThreadData() )
|
|
{
|
|
if( !td ) continue;
|
|
if( td->timeline.is_magic() )
|
|
{
|
|
for( const auto& z : *(const Vector<ZoneEvent>*)&td->timeline )
|
|
{
|
|
if( !visit( z ) ) break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for( const auto& zptr : td->timeline )
|
|
{
|
|
const ZoneEvent* z = zptr.get();
|
|
if( z && !visit( *z ) ) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
py::dict result;
|
|
for( const auto& kv : acc )
|
|
{
|
|
py::dict d;
|
|
d["count"] = kv.second.count;
|
|
d["total_ns"] = kv.second.total;
|
|
const char* zname = w.GetZoneName( w.GetSourceLocation( kv.first ) );
|
|
result[zname] = d;
|
|
}
|
|
return result;
|
|
} ), "frame_idx"_a )
|
|
|
|
// --- Messages ---
|
|
.def( "get_messages", locked( +[]( const Worker& w ) {
|
|
const auto& msgs = w.GetMessages();
|
|
std::vector<MessageInfo> result;
|
|
result.reserve( msgs.size() );
|
|
for( const auto& m_ptr : msgs )
|
|
{
|
|
const auto& msg = *m_ptr;
|
|
result.push_back( MessageInfo{
|
|
msg.time,
|
|
std::string( w.GetString( msg.ref ) ),
|
|
msg.color,
|
|
(uint64_t)msg.thread } );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Plots ---
|
|
.def( "get_plots", locked( +[]( const Worker& w ) {
|
|
static const char* plotTypeStr[] = { "User", "Memory", "SysTime", "Power" };
|
|
std::vector<PlotSummary> result;
|
|
for( const auto* pd : w.GetPlots() )
|
|
{
|
|
if( !pd ) continue;
|
|
const size_t cnt = pd->data.size();
|
|
const std::string name = w.GetString( pd->name );
|
|
const char* typeStr = (uint8_t)pd->type < 4 ? plotTypeStr[(uint8_t)pd->type] : "Unknown";
|
|
result.push_back( PlotSummary{
|
|
name, pd->min, pd->max, pd->sum,
|
|
cnt, cnt ? pd->sum / cnt : 0.0,
|
|
std::string( typeStr ) } );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Memory pools ---
|
|
.def( "get_memory_pools", locked( +[]( const Worker& w ) {
|
|
std::vector<MemPoolSummary> result;
|
|
for( const auto& kv : w.GetMemNameMap() )
|
|
{
|
|
const MemData* md = kv.second;
|
|
const std::string name = kv.first == 0 ? "(default)" : std::string( w.GetString( kv.first ) );
|
|
result.push_back( MemPoolSummary{
|
|
name, md->high, md->low, md->usage, md->data.size() } );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Locks ---
|
|
.def( "get_locks", locked( +[]( const Worker& w ) {
|
|
std::vector<LockSummary> result;
|
|
for( const auto& kv : w.GetLockMap() )
|
|
{
|
|
const LockMap* lm = kv.second;
|
|
if( !lm || !lm->valid ) continue;
|
|
std::string name;
|
|
if( lm->customName.Active() )
|
|
name = w.GetString( lm->customName );
|
|
else
|
|
name = w.GetZoneName( w.GetSourceLocation( lm->srcloc ) );
|
|
const char* typeStr = lm->type == LockType::Lockable ? "Lockable" : "SharedLockable";
|
|
result.push_back( LockSummary{
|
|
name,
|
|
lm->isContended,
|
|
std::string( typeStr ),
|
|
lm->timeAnnounce,
|
|
lm->timeTerminate,
|
|
lm->threadList } );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- GPU contexts ---
|
|
.def( "get_gpu_contexts", locked( +[]( const Worker& w ) {
|
|
static const char* gpuTypeStr[] = {
|
|
"Invalid", "OpenGL", "Vulkan", "OpenCL", "Direct3D12", "Direct3D11", "Metal", "Custom", "CUDA", "Rocprof", "WebGPU" };
|
|
static size_t numTypes = sizeof(gpuTypeStr) / sizeof(gpuTypeStr[0]);
|
|
std::vector<GpuContextSummary> result;
|
|
for( const auto* ctx : w.GetGpuData() )
|
|
{
|
|
if( !ctx ) continue;
|
|
const std::string name = ctx->name.Active() ? w.GetString( ctx->name ) : "";
|
|
const uint8_t typeIdx = (uint8_t)ctx->type;
|
|
const char* typeStr = typeIdx < numTypes ? gpuTypeStr[typeIdx] : "Unknown";
|
|
result.push_back( GpuContextSummary{
|
|
name, ctx->count, std::string( typeStr ), ctx->thread } );
|
|
}
|
|
return result;
|
|
} ) )
|
|
|
|
// --- Threads ---
|
|
.def( "get_threads", locked( +[]( const Worker& w ) {
|
|
// Returns list of dicts to avoid raw-pointer pybind11 ownership issues
|
|
py::list result;
|
|
for( const auto& t : w.GetThreadData() )
|
|
{
|
|
if( !t ) continue;
|
|
py::dict d;
|
|
d["id"] = t->id;
|
|
d["count"] = t->count;
|
|
d["is_fiber"] = (bool)t->isFiber;
|
|
d["name"] = std::string( w.GetThreadName( t->id ) );
|
|
result.append( d );
|
|
}
|
|
return result;
|
|
} ) ).def( "get_thread_name", locked( +[]( const Worker& w, uint64_t tid ) {
|
|
return w.GetThreadName( tid );
|
|
} ) )
|
|
|
|
// --- Connection control ---
|
|
.def( "is_connected", &Worker::IsConnected )
|
|
.def( "shutdown", &Worker::Shutdown )
|
|
.def( "disconnect", &Worker::Disconnect );
|
|
|
|
// -------------------------------------------------------------------------
|
|
// FileRead
|
|
// -------------------------------------------------------------------------
|
|
m.def( "open_file", []( const char* path ) -> std::shared_ptr<FileRead> {
|
|
auto f = FileRead::Open( path );
|
|
if( !f ) throw std::runtime_error( "Could not open file" );
|
|
return std::shared_ptr<FileRead>( f );
|
|
} );
|
|
|
|
py::class_<FileRead, std::shared_ptr<FileRead>>( m, "FileRead" );
|
|
|
|
m.def( "create_worker_from_file", []( std::shared_ptr<FileRead> f ) {
|
|
return std::make_unique<Worker>( *f );
|
|
} );
|
|
|
|
// -------------------------------------------------------------------------
|
|
// FileWrite — snapshot a Worker (live or loaded) to a .tracy file.
|
|
//
|
|
// Mirrors capture.cpp and View::Save: open a Zstd FileWrite, call
|
|
// Worker::Write under the main-thread data lock (so a live receive thread
|
|
// yields cooperatively rather than racing with our reads), then Finish to
|
|
// drain the compression streams before returning the size pair.
|
|
//
|
|
// Defaults match the standalone capture tool: Zstd level 3, 4 streams,
|
|
// fiDict=false. Returns (uncompressed_bytes, compressed_bytes).
|
|
// -------------------------------------------------------------------------
|
|
m.def( "save_worker", []( Worker& w, const char* path, int level, int streams, bool fiDict ) {
|
|
auto f = std::unique_ptr<FileWrite>( FileWrite::Open( path, FileCompression::Zstd, level, streams ) );
|
|
if( !f ) throw std::runtime_error( "Could not open output file for writing" );
|
|
std::pair<size_t, size_t> stats;
|
|
{
|
|
py::gil_scoped_release release;
|
|
auto lock = w.ObtainLockForMainThread();
|
|
w.Write( *f, fiDict );
|
|
f->Finish();
|
|
stats = f->GetCompressionStatistics();
|
|
}
|
|
return py::make_tuple( stats.first, stats.second );
|
|
}, py::arg( "worker" ), py::arg( "path" ),
|
|
py::arg( "level" ) = 3, py::arg( "streams" ) = 4, py::arg( "fi_dict" ) = false );
|
|
}
|
|
|
|
} // namespace tracy
|