From ecb6ec7574f9fba38bd486f5d68a33a1c4f837c6 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Fri, 13 Mar 2026 12:09:44 -0400 Subject: [PATCH 01/17] Add GPU async timing instrumentation via MX_TRACE_ASYNC New MX_TRACE_ASYNC macro and AsyncTrack infrastructure for emitting GPU timer query results on a dedicated Perfetto track, enabling per-material render time analysis. - AsyncTrack enum and Sink::asyncEvent() in Tracing.h - PerfettoSink implementation using custom track descriptors - GpuTimerQuery helper class using GL_TIME_ELAPSED - Single-frame GPU timing around _renderer->render() --- .../MaterialXRenderGlsl/RenderGlsl.cpp | 64 +++++++++++++++++++ source/MaterialXTrace/PerfettoSink.cpp | 42 ++++++++++++ source/MaterialXTrace/PerfettoSink.h | 2 + source/MaterialXTrace/Tracing.h | 34 ++++++++++ 4 files changed, 142 insertions(+) diff --git a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp index 197c43c93e..70d6269b62 100644 --- a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp +++ b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp @@ -19,8 +19,59 @@ #include +#ifdef MATERIALX_BUILD_PERFETTO_TRACING +#include +#include +#endif + namespace mx = MaterialX; +#ifdef MATERIALX_BUILD_PERFETTO_TRACING +namespace { + +uint64_t getCurrentTimeNs() +{ + using namespace std::chrono; + return duration_cast(steady_clock::now().time_since_epoch()).count(); +} + +class GpuTimerQuery +{ + public: + GpuTimerQuery() + { + glGenQueries(1, &_query); + } + + ~GpuTimerQuery() + { + glDeleteQueries(1, &_query); + } + + void begin() + { + glBeginQuery(GL_TIME_ELAPSED, _query); + } + + void end() + { + glEndQuery(GL_TIME_ELAPSED); + } + + uint64_t getDurationNs() + { + GLuint64 elapsedTime; + glGetQueryObjectui64v(_query, GL_QUERY_RESULT, &elapsedTime); + return elapsedTime; + } + + private: + GLuint _query; +}; + +} // anonymous namespace +#endif // MATERIALX_BUILD_PERFETTO_TRACING + // // Render validation tester for the GLSL shading language // @@ -356,7 +407,20 @@ bool GlslShaderRenderTester::runRenderer(const std::string& shaderName, unsigned int width = (unsigned int) testOptions.renderSize[0] * supersampleFactor; unsigned int height = (unsigned int) testOptions.renderSize[1] * supersampleFactor; _renderer->setSize(width, height); + +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + uint64_t cpuStartNs = getCurrentTimeNs(); + GpuTimerQuery gpuTimer; + gpuTimer.begin(); +#endif _renderer->render(); + +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + gpuTimer.end(); + glFinish(); + uint64_t gpuDurationNs = gpuTimer.getDurationNs(); + MX_TRACE_ASYNC(mx::Tracing::AsyncTrack::GPU, mx::Tracing::Category::Render, shaderName.c_str(), cpuStartNs, gpuDurationNs); +#endif } { diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index 691c73c08f..d7ffc6667b 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -7,7 +7,9 @@ #ifdef MATERIALX_BUILD_PERFETTO_TRACING +#include #include +#include #include // Define Perfetto trace categories for MaterialX @@ -31,6 +33,10 @@ MATERIALX_NAMESPACE_BEGIN namespace Tracing { +// Stable Perfetto track IDs for async tracks (must not collide with thread IDs). +// Use max uint64_t minus small offsets -- no OS will assign these as thread IDs. +constexpr uint64_t GPU_TRACK_ID = std::numeric_limits::max(); + PerfettoSink::PerfettoSink(std::string outputPath, size_t bufferSizeKb) : _outputPath(std::move(outputPath)) { @@ -41,6 +47,14 @@ PerfettoSink::PerfettoSink(std::string outputPath, size_t bufferSizeKb) args.backends |= perfetto::kInProcessBackend; perfetto::Tracing::Initialize(args); perfetto::TrackEvent::Register(); + + // Initialize async track descriptors with stable IDs and names + { + perfetto::Track gpuTrack(GPU_TRACK_ID); + auto desc = gpuTrack.Serialize(); + desc.set_name("GPU"); + perfetto::TrackEvent::SetTrackDescriptor(gpuTrack, desc); + } }); // Create and start a tracing session @@ -155,6 +169,34 @@ void PerfettoSink::counter(Category category, const char* name, double value) } } +void PerfettoSink::asyncEvent(AsyncTrack track, Category category, + const char* eventName, uint64_t startNs, uint64_t durationNs) +{ + // Currently only GPU track is supported + assert(track == AsyncTrack::GPU && "Only AsyncTrack::GPU is currently supported"); + perfetto::Track perfTrack(GPU_TRACK_ID); + + // Emit begin and end events with explicit timestamps + switch (category) + { + case Category::Render: + TRACE_EVENT_BEGIN("mx.render", nullptr, perfTrack, startNs, + [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); + TRACE_EVENT_END("mx.render", perfTrack, startNs + durationNs); + break; + case Category::ShaderGen: + TRACE_EVENT_BEGIN("mx.shadergen", nullptr, perfTrack, startNs, + [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); + TRACE_EVENT_END("mx.shadergen", perfTrack, startNs + durationNs); + break; + default: + TRACE_EVENT_BEGIN("mx.render", nullptr, perfTrack, startNs, + [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); + TRACE_EVENT_END("mx.render", perfTrack, startNs + durationNs); + break; + } +} + void PerfettoSink::setThreadName(const char* name) { // Set thread name for trace visualization diff --git a/source/MaterialXTrace/PerfettoSink.h b/source/MaterialXTrace/PerfettoSink.h index b0012e86ec..87d5a0d7e7 100644 --- a/source/MaterialXTrace/PerfettoSink.h +++ b/source/MaterialXTrace/PerfettoSink.h @@ -70,6 +70,8 @@ class PerfettoSink : public Sink void beginEvent(Category category, const char* name) override; void endEvent(Category category) override; void counter(Category category, const char* name, double value) override; + void asyncEvent(AsyncTrack track, Category category, + const char* eventName, uint64_t startNs, uint64_t durationNs) override; void setThreadName(const char* name) override; private: diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index 283fd33dca..c89dedb061 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -55,6 +56,15 @@ enum class Category Count }; +/// @enum AsyncTrack +/// Async track identifiers for operations with explicit timing (e.g., GPU work). +enum class AsyncTrack +{ + /// GPU render operations (measured via GL timer queries) + GPU = 0 + // Add more tracks here as needed (e.g., Compile, Transfer) +}; + /// @class Sink /// Abstract tracing sink interface. /// @@ -74,6 +84,17 @@ class MX_TRACE_API Sink /// Record a counter value (e.g., GPU time, memory usage). virtual void counter(Category category, const char* name, double value) = 0; + /// Record an async event with explicit timing (e.g., GPU operations). + /// This creates a slice on a separate track, useful for GPU work that + /// runs asynchronously from CPU traces. + /// @param track The async track to record on (e.g., AsyncTrack::GPU) + /// @param category The trace category for filtering + /// @param eventName Name of the event (e.g., material name) + /// @param startNs Start timestamp in nanoseconds (can be approximate) + /// @param durationNs Duration in nanoseconds (should be accurate) + virtual void asyncEvent(AsyncTrack track, Category category, + const char* eventName, uint64_t startNs, uint64_t durationNs) = 0; + /// Set the current thread's name for trace visualization. virtual void setThreadName(const char* name) = 0; }; @@ -142,6 +163,14 @@ class MX_TRACE_API Dispatcher _sink->counter(category, name, value); } + /// Record an async event with explicit timing. + void asyncEvent(AsyncTrack track, Category category, + const char* eventName, uint64_t startNs, uint64_t durationNs) + { + if (_sink) + _sink->asyncEvent(track, category, eventName, startNs, durationNs); + } + private: Dispatcher() = default; Dispatcher(const Dispatcher&) = delete; @@ -234,6 +263,10 @@ MATERIALX_NAMESPACE_END #define MX_TRACE_COUNTER(category, name, value) \ MaterialX::Tracing::Dispatcher::getInstance().counter(category, name, value) +/// Record an async event with explicit timing (e.g., GPU operations). +#define MX_TRACE_ASYNC(track, category, eventName, startNs, durationNs) \ + MaterialX::Tracing::Dispatcher::getInstance().asyncEvent(track, category, eventName, startNs, durationNs) + /// Begin a trace event (must be paired with MX_TRACE_END). #define MX_TRACE_BEGIN(category, name) \ MaterialX::Tracing::Dispatcher::getInstance().beginEvent(category, name) @@ -247,6 +280,7 @@ MATERIALX_NAMESPACE_END #define MX_TRACE_SCOPE(category, name) #define MX_TRACE_FUNCTION(category) #define MX_TRACE_COUNTER(category, name, value) +#define MX_TRACE_ASYNC(track, category, eventName, startNs, durationNs) #define MX_TRACE_BEGIN(category, name) #define MX_TRACE_END(category) From b279a6f65a57fadf81510c643eeb8dc865ae5412 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Wed, 10 Jun 2026 17:48:06 -0400 Subject: [PATCH 02/17] Fix unused parameter warning in PerfettoSink::asyncEvent (Release builds) The `track` parameter is only used inside an assert(), which is compiled out in Release builds. GCC 14 with -Werror rejects this as an unused parameter, breaking the Linux_GCC_14_Python313 CI job. Add `(void) track` to suppress the warning. Also move -DMATERIALX_BUILD_PERFETTO_TRACING=ON from extended_cmake_config (only applied on schedule/dispatch) to cmake_config (always applied), so that Perfetto compilation errors are caught on every PR, not just nightly builds. Trace artifact upload remains gated on IS_EXTENDED_BUILD. Fixes autodesk-forks/MaterialX#1481 --- .github/workflows/main.yml | 2 +- source/MaterialXTrace/PerfettoSink.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 90c096f336..77512f49a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,8 +38,8 @@ jobs: compiler: gcc compiler_version: "14" python: 3.13 + cmake_config: -DMATERIALX_BUILD_PERFETTO_TRACING=ON extended_build_perfetto: ON - extended_cmake_config: -DMATERIALX_BUILD_PERFETTO_TRACING=ON - name: Linux_GCC_14_Python314 os: ubuntu-24.04 diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index d7ffc6667b..905bf1027c 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -172,8 +172,8 @@ void PerfettoSink::counter(Category category, const char* name, double value) void PerfettoSink::asyncEvent(AsyncTrack track, Category category, const char* eventName, uint64_t startNs, uint64_t durationNs) { - // Currently only GPU track is supported assert(track == AsyncTrack::GPU && "Only AsyncTrack::GPU is currently supported"); + (void) track; perfetto::Track perfTrack(GPU_TRACK_ID); // Emit begin and end events with explicit timestamps From 56a732e5feab11eca7930a035687028d3bfdbf22 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Wed, 7 Oct 2026 16:07:32 -0400 Subject: [PATCH 03/17] Give asyncEvent a default no-op body Potentially existing Sink implementations will continue to compile without changes after this addition to the interface. Addresses review feedback on #2824. --- source/MaterialXTrace/Tracing.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index c89dedb061..56c0ca7f79 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -93,7 +93,7 @@ class MX_TRACE_API Sink /// @param startNs Start timestamp in nanoseconds (can be approximate) /// @param durationNs Duration in nanoseconds (should be accurate) virtual void asyncEvent(AsyncTrack track, Category category, - const char* eventName, uint64_t startNs, uint64_t durationNs) = 0; + const char* eventName, uint64_t startNs, uint64_t durationNs) { } /// Set the current thread's name for trace visualization. virtual void setThreadName(const char* name) = 0; From 4db469604cb68e91f453591268d9b89efb38654c Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Wed, 7 Oct 2026 16:34:05 -0400 Subject: [PATCH 04/17] Generalize async tracks via caller-defined {id -> name} map Replace the AsyncTrack enum with AsyncTrackId (uint64_t) and an AsyncTrackMap passed to createPerfettoSink(). The sink creates Perfetto track descriptors from the map and routes asyncEvent calls by ID, without needing to know the semantics of each track. Also handles all four trace categories in asyncEvent (previously only Render and ShaderGen were covered). Addresses review feedback on #2824. --- .../MaterialXRender/RenderUtil.cpp | 2 +- .../MaterialXRenderGlsl/RenderGlsl.cpp | 4 +- source/MaterialXTrace/PerfettoSink.cpp | 62 +++++++++++-------- source/MaterialXTrace/PerfettoSink.h | 9 ++- source/MaterialXTrace/Tracing.h | 38 +++++++----- 5 files changed, 69 insertions(+), 46 deletions(-) diff --git a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp index ea81d0489b..41b89d132c 100644 --- a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp +++ b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp @@ -710,7 +710,7 @@ void TestRunTracer::start(const std::string& target, const GenShaderUtil::TestSu { mx::FilePath tracePath = options.resolveOutputPath(target + "_render_trace.perfetto-trace"); mx::Tracing::Dispatcher::getInstance().setSink( - mx::Tracing::createPerfettoSink(tracePath.asString())); + mx::Tracing::createPerfettoSink(tracePath.asString(), {{0, "GPU"}})); // Scope guard ensures tracing is shut down on any exit path (return, exception, etc.) _state->guard.emplace(); } diff --git a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp index ac62939f79..ce0874d05e 100644 --- a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp +++ b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp @@ -29,6 +29,8 @@ namespace mx = MaterialX; #ifdef MATERIALX_BUILD_PERFETTO_TRACING namespace { +constexpr mx::Tracing::AsyncTrackId GPU_TRACK_ID = 0; + uint64_t getCurrentTimeNs() { using namespace std::chrono; @@ -394,7 +396,7 @@ RenderUtil::RenderProfileResult GlslShaderRenderTester::runRenderer( gpuTimer.end(); glFinish(); uint64_t gpuDurationNs = gpuTimer.getDurationNs(); - MX_TRACE_ASYNC(mx::Tracing::AsyncTrack::GPU, mx::Tracing::Category::Render, shaderName.c_str(), cpuStartNs, gpuDurationNs); + MX_TRACE_ASYNC(GPU_TRACK_ID, mx::Tracing::Category::Render, shaderName.c_str(), cpuStartNs, gpuDurationNs); #endif } diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index 905bf1027c..9931d4f015 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -9,7 +9,6 @@ #include #include -#include #include // Define Perfetto trace categories for MaterialX @@ -33,11 +32,9 @@ MATERIALX_NAMESPACE_BEGIN namespace Tracing { -// Stable Perfetto track IDs for async tracks (must not collide with thread IDs). -// Use max uint64_t minus small offsets -- no OS will assign these as thread IDs. -constexpr uint64_t GPU_TRACK_ID = std::numeric_limits::max(); - -PerfettoSink::PerfettoSink(std::string outputPath, size_t bufferSizeKb) +PerfettoSink::PerfettoSink(std::string outputPath, + const AsyncTrackMap& asyncTracks, + size_t bufferSizeKb) : _outputPath(std::move(outputPath)) { // One-time global Perfetto initialization @@ -47,16 +44,18 @@ PerfettoSink::PerfettoSink(std::string outputPath, size_t bufferSizeKb) args.backends |= perfetto::kInProcessBackend; perfetto::Tracing::Initialize(args); perfetto::TrackEvent::Register(); - - // Initialize async track descriptors with stable IDs and names - { - perfetto::Track gpuTrack(GPU_TRACK_ID); - auto desc = gpuTrack.Serialize(); - desc.set_name("GPU"); - perfetto::TrackEvent::SetTrackDescriptor(gpuTrack, desc); - } }); + // Register async track descriptors from the caller-provided map + for (const auto& [id, name] : asyncTracks) + { + perfetto::Track perfTrack(id); + auto desc = perfTrack.Serialize(); + desc.set_name(name); + perfetto::TrackEvent::SetTrackDescriptor(perfTrack, desc); + _asyncTracks.emplace(id, perfTrack); + } + // Create and start a tracing session perfetto::TraceConfig cfg; cfg.add_buffers()->set_size_kb(static_cast(bufferSizeKb)); @@ -169,30 +168,39 @@ void PerfettoSink::counter(Category category, const char* name, double value) } } -void PerfettoSink::asyncEvent(AsyncTrack track, Category category, +void PerfettoSink::asyncEvent(AsyncTrackId track, Category category, const char* eventName, uint64_t startNs, uint64_t durationNs) { - assert(track == AsyncTrack::GPU && "Only AsyncTrack::GPU is currently supported"); - (void) track; - perfetto::Track perfTrack(GPU_TRACK_ID); + auto it = _asyncTracks.find(track); + if (it == _asyncTracks.end()) + return; + + const auto& perfTrack = it->second; + uint64_t endNs = startNs + durationNs; - // Emit begin and end events with explicit timestamps switch (category) { case Category::Render: TRACE_EVENT_BEGIN("mx.render", nullptr, perfTrack, startNs, [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); - TRACE_EVENT_END("mx.render", perfTrack, startNs + durationNs); + TRACE_EVENT_END("mx.render", perfTrack, endNs); break; case Category::ShaderGen: TRACE_EVENT_BEGIN("mx.shadergen", nullptr, perfTrack, startNs, [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); - TRACE_EVENT_END("mx.shadergen", perfTrack, startNs + durationNs); + TRACE_EVENT_END("mx.shadergen", perfTrack, endNs); break; - default: - TRACE_EVENT_BEGIN("mx.render", nullptr, perfTrack, startNs, + case Category::Optimize: + TRACE_EVENT_BEGIN("mx.optimize", nullptr, perfTrack, startNs, [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); - TRACE_EVENT_END("mx.render", perfTrack, startNs + durationNs); + TRACE_EVENT_END("mx.optimize", perfTrack, endNs); + break; + case Category::Material: + TRACE_EVENT_BEGIN("mx.material", nullptr, perfTrack, startNs, + [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); + TRACE_EVENT_END("mx.material", perfTrack, endNs); + break; + default: break; } } @@ -207,9 +215,11 @@ void PerfettoSink::setThreadName(const char* name) } // Factory function - the exported entry point -std::unique_ptr createPerfettoSink(const std::string& outputPath, size_t bufferSizeKb) +std::unique_ptr createPerfettoSink(const std::string& outputPath, + const AsyncTrackMap& asyncTracks, + size_t bufferSizeKb) { - return std::make_unique(outputPath, bufferSizeKb); + return std::make_unique(outputPath, asyncTracks, bufferSizeKb); } } // namespace Tracing diff --git a/source/MaterialXTrace/PerfettoSink.h b/source/MaterialXTrace/PerfettoSink.h index 87d5a0d7e7..8ddf5c5316 100644 --- a/source/MaterialXTrace/PerfettoSink.h +++ b/source/MaterialXTrace/PerfettoSink.h @@ -35,6 +35,7 @@ #include #include +#include MATERIALX_NAMESPACE_BEGIN @@ -54,8 +55,11 @@ class PerfettoSink : public Sink public: /// Construct and start a Perfetto tracing session. /// @param outputPath Path to write the trace file when destroyed + /// @param asyncTracks Map of async track definitions {id -> display name} /// @param bufferSizeKb Size of the trace buffer in KB (default 32MB) - explicit PerfettoSink(std::string outputPath, size_t bufferSizeKb = 32768); + explicit PerfettoSink(std::string outputPath, + const AsyncTrackMap& asyncTracks = {}, + size_t bufferSizeKb = 32768); /// Stop tracing and write the trace to the output path. ~PerfettoSink() override; @@ -70,13 +74,14 @@ class PerfettoSink : public Sink void beginEvent(Category category, const char* name) override; void endEvent(Category category) override; void counter(Category category, const char* name, double value) override; - void asyncEvent(AsyncTrack track, Category category, + void asyncEvent(AsyncTrackId track, Category category, const char* eventName, uint64_t startNs, uint64_t durationNs) override; void setThreadName(const char* name) override; private: const std::string _outputPath; std::unique_ptr _session; + std::unordered_map _asyncTracks; }; } // namespace Tracing diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index 56c0ca7f79..c4ff697be6 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -25,6 +25,7 @@ #include #include #include +#include MATERIALX_NAMESPACE_BEGIN @@ -56,14 +57,14 @@ enum class Category Count }; -/// @enum AsyncTrack -/// Async track identifiers for operations with explicit timing (e.g., GPU work). -enum class AsyncTrack -{ - /// GPU render operations (measured via GL timer queries) - GPU = 0 - // Add more tracks here as needed (e.g., Compile, Transfer) -}; +/// Numeric identifier for an async track (e.g., GPU, Compile, Transfer). +/// Callers define their own IDs and pass an {id -> name} map when creating +/// a sink. The sink uses the map to set up track descriptors; asyncEvent +/// then routes events by ID without the sink needing to know the semantics. +using AsyncTrackId = uint64_t; + +/// Map of async track definitions: {id -> display name}. +using AsyncTrackMap = std::unordered_map; /// @class Sink /// Abstract tracing sink interface. @@ -85,15 +86,15 @@ class MX_TRACE_API Sink virtual void counter(Category category, const char* name, double value) = 0; /// Record an async event with explicit timing (e.g., GPU operations). - /// This creates a slice on a separate track, useful for GPU work that - /// runs asynchronously from CPU traces. - /// @param track The async track to record on (e.g., AsyncTrack::GPU) + /// The sink places the slice on the track identified by @p track. + /// @param track The async track ID (must have been registered at sink creation) /// @param category The trace category for filtering /// @param eventName Name of the event (e.g., material name) /// @param startNs Start timestamp in nanoseconds (can be approximate) /// @param durationNs Duration in nanoseconds (should be accurate) - virtual void asyncEvent(AsyncTrack track, Category category, - const char* eventName, uint64_t startNs, uint64_t durationNs) { } + virtual void asyncEvent(AsyncTrackId /*track*/, Category /*category*/, + const char* /*eventName*/, uint64_t /*startNs*/, + uint64_t /*durationNs*/) { } /// Set the current thread's name for trace visualization. virtual void setThreadName(const char* name) = 0; @@ -164,7 +165,7 @@ class MX_TRACE_API Dispatcher } /// Record an async event with explicit timing. - void asyncEvent(AsyncTrack track, Category category, + void asyncEvent(AsyncTrackId track, Category category, const char* eventName, uint64_t startNs, uint64_t durationNs) { if (_sink) @@ -221,16 +222,21 @@ class Scope /// visualized at https://ui.perfetto.dev /// /// @param outputPath Path to write the trace file when the sink is destroyed +/// @param asyncTracks Map of async track definitions {id -> display name}. +/// Each entry creates a named track in the trace for asyncEvent calls. /// @param bufferSizeKb Size of the trace buffer in KB (default 32MB) /// @return A unique_ptr to the Perfetto sink /// /// Usage: -/// Dispatcher::getInstance().setSink(createPerfettoSink("trace.perfetto-trace")); +/// Dispatcher::getInstance().setSink( +/// createPerfettoSink("trace.perfetto-trace", {{0, "GPU"}})); /// Dispatcher::ShutdownGuard guard; /// // ... traced work ... /// // guard destructor writes the trace file MX_TRACE_API std::unique_ptr createPerfettoSink( - const std::string& outputPath, size_t bufferSizeKb = 32768); + const std::string& outputPath, + const AsyncTrackMap& asyncTracks = {}, + size_t bufferSizeKb = 32768); #endif // MATERIALX_BUILD_PERFETTO_TRACING From 71a3692d0d14a151b2bd3eb8587d2c9a41440cc6 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Wed, 7 Oct 2026 16:37:58 -0400 Subject: [PATCH 05/17] Revert CI change (split out to #3105) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e49a4bb3ff..5111c15bb6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,8 +38,8 @@ jobs: compiler: gcc compiler_version: "14" python: 3.13 - cmake_config: -DMATERIALX_BUILD_PERFETTO_TRACING=ON extended_build_perfetto: ON + extended_cmake_config: -DMATERIALX_BUILD_PERFETTO_TRACING=ON - name: Linux_GCC_14_Python314 os: ubuntu-24.04 From 0d9e263e023ca39f27a2fa6d5231849e51d96aa8 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Wed, 7 Oct 2026 16:49:40 -0400 Subject: [PATCH 06/17] Use sink trace clock for async event timestamps Add Sink::getTraceTimeNs() so callers can obtain timestamps in the sink's native clock domain. The Perfetto sink delegates to perfetto::TrackEvent::GetTraceTimeNs(), which uses the platform's trace clock (CLOCK_BOOTTIME on Linux, mach_absolute_time on macOS, QueryPerformanceCounter on Windows). The GPU timer call site now uses getTraceTimeNs() instead of std::steady_clock, which only happened to align with the trace clock on Windows. Also removes the redundant glFinish() -- reading GL_QUERY_RESULT already blocks until the result is available. Addresses review feedback on #2824. --- .../MaterialXRenderGlsl/RenderGlsl.cpp | 14 ++++---------- source/MaterialXTrace/PerfettoSink.cpp | 5 +++++ source/MaterialXTrace/PerfettoSink.h | 1 + source/MaterialXTrace/Tracing.h | 16 ++++++++++++++-- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp index ce0874d05e..11382552a1 100644 --- a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp +++ b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp @@ -21,7 +21,6 @@ #ifdef MATERIALX_BUILD_PERFETTO_TRACING #include -#include #endif namespace mx = MaterialX; @@ -31,12 +30,6 @@ namespace { constexpr mx::Tracing::AsyncTrackId GPU_TRACK_ID = 0; -uint64_t getCurrentTimeNs() -{ - using namespace std::chrono; - return duration_cast(steady_clock::now().time_since_epoch()).count(); -} - class GpuTimerQuery { public: @@ -386,7 +379,6 @@ RenderUtil::RenderProfileResult GlslShaderRenderTester::runRenderer( _renderer->setSize(width, height); #ifdef MATERIALX_BUILD_PERFETTO_TRACING - uint64_t cpuStartNs = getCurrentTimeNs(); GpuTimerQuery gpuTimer; gpuTimer.begin(); #endif @@ -394,9 +386,11 @@ RenderUtil::RenderProfileResult GlslShaderRenderTester::runRenderer( #ifdef MATERIALX_BUILD_PERFETTO_TRACING gpuTimer.end(); - glFinish(); + // Reading GL_QUERY_RESULT blocks until the GPU work is done, + // so "now" in the trace clock is a safe end-of-slice marker. uint64_t gpuDurationNs = gpuTimer.getDurationNs(); - MX_TRACE_ASYNC(GPU_TRACK_ID, mx::Tracing::Category::Render, shaderName.c_str(), cpuStartNs, gpuDurationNs); + uint64_t nowNs = mx::Tracing::Dispatcher::getInstance().getTraceTimeNs(); + MX_TRACE_ASYNC(GPU_TRACK_ID, mx::Tracing::Category::Render, shaderName.c_str(), nowNs - gpuDurationNs, gpuDurationNs); #endif } diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index 9931d4f015..ea86aa3886 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -205,6 +205,11 @@ void PerfettoSink::asyncEvent(AsyncTrackId track, Category category, } } +uint64_t PerfettoSink::getTraceTimeNs() +{ + return perfetto::TrackEvent::GetTraceTimeNs(); +} + void PerfettoSink::setThreadName(const char* name) { // Set thread name for trace visualization diff --git a/source/MaterialXTrace/PerfettoSink.h b/source/MaterialXTrace/PerfettoSink.h index 8ddf5c5316..0ae59a4ca6 100644 --- a/source/MaterialXTrace/PerfettoSink.h +++ b/source/MaterialXTrace/PerfettoSink.h @@ -76,6 +76,7 @@ class PerfettoSink : public Sink void counter(Category category, const char* name, double value) override; void asyncEvent(AsyncTrackId track, Category category, const char* eventName, uint64_t startNs, uint64_t durationNs) override; + uint64_t getTraceTimeNs() override; void setThreadName(const char* name) override; private: diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index c4ff697be6..90b114f247 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -90,12 +90,18 @@ class MX_TRACE_API Sink /// @param track The async track ID (must have been registered at sink creation) /// @param category The trace category for filtering /// @param eventName Name of the event (e.g., material name) - /// @param startNs Start timestamp in nanoseconds (can be approximate) - /// @param durationNs Duration in nanoseconds (should be accurate) + /// @param startNs Start timestamp in nanoseconds (in the sink's trace clock; + /// use getTraceTimeNs() to obtain a compatible value) + /// @param durationNs Duration in nanoseconds virtual void asyncEvent(AsyncTrackId /*track*/, Category /*category*/, const char* /*eventName*/, uint64_t /*startNs*/, uint64_t /*durationNs*/) { } + /// Return the current time in the sink's trace clock (nanoseconds). + /// Use this to obtain timestamps compatible with asyncEvent's startNs. + /// Returns 0 if the sink has no notion of a trace clock. + virtual uint64_t getTraceTimeNs() { return 0; } + /// Set the current thread's name for trace visualization. virtual void setThreadName(const char* name) = 0; }; @@ -172,6 +178,12 @@ class MX_TRACE_API Dispatcher _sink->asyncEvent(track, category, eventName, startNs, durationNs); } + /// Return the current time in the sink's trace clock (nanoseconds). + uint64_t getTraceTimeNs() + { + return _sink ? _sink->getTraceTimeNs() : 0; + } + private: Dispatcher() = default; Dispatcher(const Dispatcher&) = delete; From 3ef452467721d3be2d26dbdb62068c3b58c2bffe Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Wed, 7 Oct 2026 17:04:52 -0400 Subject: [PATCH 07/17] Move GPU timer query from test into GlslRenderer::render() Move the GL_TIME_ELAPSED instrumentation out of the test suite and into GlslRenderer::render(), where the GL context is guaranteed current. Use an RAII GpuTimerScope class so that the query is correctly ended and read back on both the normal path and exception unwind, eliminating manual cleanup in the catch block. --- source/MaterialXRenderGlsl/CMakeLists.txt | 1 + source/MaterialXRenderGlsl/GlslRenderer.cpp | 49 ++++++++++++++++ .../MaterialXRenderGlsl/RenderGlsl.cpp | 58 +------------------ 3 files changed, 51 insertions(+), 57 deletions(-) diff --git a/source/MaterialXRenderGlsl/CMakeLists.txt b/source/MaterialXRenderGlsl/CMakeLists.txt index 2ae516e690..9f018f6536 100644 --- a/source/MaterialXRenderGlsl/CMakeLists.txt +++ b/source/MaterialXRenderGlsl/CMakeLists.txt @@ -26,6 +26,7 @@ mx_add_library(MaterialXRenderGlsl MTLX_MODULES MaterialXRenderHw MaterialXGenGlsl + MaterialXTrace EXPORT_DEFINE MATERIALX_RENDERGLSL_EXPORTS ADD_OBJECTIVE_C_CODE) diff --git a/source/MaterialXRenderGlsl/GlslRenderer.cpp b/source/MaterialXRenderGlsl/GlslRenderer.cpp index 4105b3aaf2..18be261a63 100644 --- a/source/MaterialXRenderGlsl/GlslRenderer.cpp +++ b/source/MaterialXRenderGlsl/GlslRenderer.cpp @@ -12,9 +12,54 @@ #include #include #include +#include MATERIALX_NAMESPACE_BEGIN +#ifdef MATERIALX_BUILD_PERFETTO_TRACING +namespace +{ + +/// RAII wrapper around a GL_TIME_ELAPSED query. On destruction, reads the +/// result (blocking until the GPU is done) and emits an MX_TRACE_ASYNC event. +class GpuTimerScope +{ + public: + GpuTimerScope() + { + if (Tracing::Dispatcher::getInstance().isEnabled()) + { + glGenQueries(1, &_query); + glBeginQuery(GL_TIME_ELAPSED, _query); + } + } + + ~GpuTimerScope() + { + if (!_query) + return; + + glEndQuery(GL_TIME_ELAPSED); + // Reading GL_QUERY_RESULT blocks until the GPU work is done, + // so "now" in the trace clock is a safe end-of-slice marker. + GLuint64 gpuDurationNs = 0; + glGetQueryObjectui64v(_query, GL_QUERY_RESULT, &gpuDurationNs); + glDeleteQueries(1, &_query); + uint64_t nowNs = Tracing::Dispatcher::getInstance().getTraceTimeNs(); + MX_TRACE_ASYNC(0, Tracing::Category::Render, "GPU Frame", + nowNs - gpuDurationNs, gpuDurationNs); + } + + GpuTimerScope(const GpuTimerScope&) = delete; + GpuTimerScope& operator=(const GpuTimerScope&) = delete; + + private: + GLuint _query = 0; +}; + +} // anonymous namespace +#endif + // // GlslRenderer methods // @@ -161,6 +206,10 @@ void GlslRenderer::render() glDepthFunc(GL_LESS); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + GpuTimerScope gpuTimer; +#endif + try { // Bind program and input parameters diff --git a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp index 11382552a1..1e74a47b07 100644 --- a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp +++ b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp @@ -19,53 +19,10 @@ #include -#ifdef MATERIALX_BUILD_PERFETTO_TRACING -#include -#endif namespace mx = MaterialX; -#ifdef MATERIALX_BUILD_PERFETTO_TRACING -namespace { - -constexpr mx::Tracing::AsyncTrackId GPU_TRACK_ID = 0; - -class GpuTimerQuery -{ - public: - GpuTimerQuery() - { - glGenQueries(1, &_query); - } - - ~GpuTimerQuery() - { - glDeleteQueries(1, &_query); - } - - void begin() - { - glBeginQuery(GL_TIME_ELAPSED, _query); - } - - void end() - { - glEndQuery(GL_TIME_ELAPSED); - } - - uint64_t getDurationNs() - { - GLuint64 elapsedTime; - glGetQueryObjectui64v(_query, GL_QUERY_RESULT, &elapsedTime); - return elapsedTime; - } - - private: - GLuint _query; -}; - -} // anonymous namespace -#endif // MATERIALX_BUILD_PERFETTO_TRACING + // MATERIALX_BUILD_PERFETTO_TRACING // // Render validation tester for the GLSL shading language @@ -378,20 +335,7 @@ RenderUtil::RenderProfileResult GlslShaderRenderTester::runRenderer( unsigned int height = (unsigned int) testOptions.renderSize[1] * supersampleFactor; _renderer->setSize(width, height); -#ifdef MATERIALX_BUILD_PERFETTO_TRACING - GpuTimerQuery gpuTimer; - gpuTimer.begin(); -#endif _renderer->render(); - -#ifdef MATERIALX_BUILD_PERFETTO_TRACING - gpuTimer.end(); - // Reading GL_QUERY_RESULT blocks until the GPU work is done, - // so "now" in the trace clock is a safe end-of-slice marker. - uint64_t gpuDurationNs = gpuTimer.getDurationNs(); - uint64_t nowNs = mx::Tracing::Dispatcher::getInstance().getTraceTimeNs(); - MX_TRACE_ASYNC(GPU_TRACK_ID, mx::Tracing::Category::Render, shaderName.c_str(), nowNs - gpuDurationNs, gpuDurationNs); -#endif } { From b30ced75acdedbd5c61e019bfb77ea4829be642e Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Wed, 7 Oct 2026 17:07:50 -0400 Subject: [PATCH 08/17] Make MaterialXTrace dependency conditional on MATERIALX_BUILD_PERFETTO_TRACING --- source/MaterialXRenderGlsl/CMakeLists.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/source/MaterialXRenderGlsl/CMakeLists.txt b/source/MaterialXRenderGlsl/CMakeLists.txt index 9f018f6536..0f9ba55f44 100644 --- a/source/MaterialXRenderGlsl/CMakeLists.txt +++ b/source/MaterialXRenderGlsl/CMakeLists.txt @@ -18,15 +18,18 @@ elseif(UNIX) find_package(OpenGL REQUIRED) endif() +set(RENDERGLSL_MTLX_MODULES MaterialXRenderHw MaterialXGenGlsl) +if(MATERIALX_BUILD_PERFETTO_TRACING) + list(APPEND RENDERGLSL_MTLX_MODULES MaterialXTrace) +endif() + mx_add_library(MaterialXRenderGlsl SOURCE_FILES ${materialx_source} HEADER_FILES ${materialx_headers} MTLX_MODULES - MaterialXRenderHw - MaterialXGenGlsl - MaterialXTrace + ${RENDERGLSL_MTLX_MODULES} EXPORT_DEFINE MATERIALX_RENDERGLSL_EXPORTS ADD_OBJECTIVE_C_CODE) From ca6ec6587172edb7c52347fc63d36de12c838b50 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Wed, 7 Oct 2026 17:09:38 -0400 Subject: [PATCH 09/17] Clean up leftover blank line and comment in RenderGlsl.cpp --- source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp index 1e74a47b07..10d78e97a5 100644 --- a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp +++ b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp @@ -19,11 +19,8 @@ #include - namespace mx = MaterialX; - // MATERIALX_BUILD_PERFETTO_TRACING - // // Render validation tester for the GLSL shading language // From 9326e8a70308e47c4d5e022ca002d0f077bdc121 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Thu, 8 Oct 2026 09:29:37 -0400 Subject: [PATCH 10/17] Refactor PerfettoSink: DRY category dispatch, fix async track hierarchy loop Replace per-category switch-case repetition in PerfettoSink with a macro- generated ops struct + generic-lambda dispatch pattern. Adding a new Category now requires one DEFINE line and one switch case instead of touching four method bodies. Fix Perfetto track_event_track_hierarchy_loop import errors by explicitly parenting async tracks to ProcessTrack::Current() and using non-zero track IDs (0 is reserved as 'no track' in Perfetto). --- source/MaterialXRenderGlsl/GlslRenderer.cpp | 2 +- .../MaterialXRender/RenderUtil.cpp | 2 +- source/MaterialXTrace/PerfettoSink.cpp | 174 ++++++++---------- 3 files changed, 81 insertions(+), 97 deletions(-) diff --git a/source/MaterialXRenderGlsl/GlslRenderer.cpp b/source/MaterialXRenderGlsl/GlslRenderer.cpp index 18be261a63..a7f61f164a 100644 --- a/source/MaterialXRenderGlsl/GlslRenderer.cpp +++ b/source/MaterialXRenderGlsl/GlslRenderer.cpp @@ -46,7 +46,7 @@ class GpuTimerScope glGetQueryObjectui64v(_query, GL_QUERY_RESULT, &gpuDurationNs); glDeleteQueries(1, &_query); uint64_t nowNs = Tracing::Dispatcher::getInstance().getTraceTimeNs(); - MX_TRACE_ASYNC(0, Tracing::Category::Render, "GPU Frame", + MX_TRACE_ASYNC(1, Tracing::Category::Render, "GPU Frame", nowNs - gpuDurationNs, gpuDurationNs); } diff --git a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp index 41b89d132c..cb0aaab8c8 100644 --- a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp +++ b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp @@ -710,7 +710,7 @@ void TestRunTracer::start(const std::string& target, const GenShaderUtil::TestSu { mx::FilePath tracePath = options.resolveOutputPath(target + "_render_trace.perfetto-trace"); mx::Tracing::Dispatcher::getInstance().setSink( - mx::Tracing::createPerfettoSink(tracePath.asString(), {{0, "GPU"}})); + mx::Tracing::createPerfettoSink(tracePath.asString(), {{1, "GPU"}})); // Scope guard ensures tracing is shut down on any exit path (return, exception, etc.) _state->guard.emplace(); } diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index ea86aa3886..2c62bc8d49 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -32,6 +32,69 @@ MATERIALX_NAMESPACE_BEGIN namespace Tracing { +// --------------------------------------------------------------------------- +// Per-category Perfetto operations. +// +// Perfetto trace macros require compile-time category string literals, so +// we stamp out a thin ops struct per category and dispatch through a generic +// lambda. Adding a new Category means adding one DEFINE line and one switch +// case in withCategory() below. +// --------------------------------------------------------------------------- +namespace +{ + +#define MX_DEFINE_PERFETTO_OPS(Name, perfettoCategory) \ +struct Name \ +{ \ + static void beginEvent(const char* name) \ + { \ + TRACE_EVENT_BEGIN(perfettoCategory, nullptr, \ + [&](perfetto::EventContext ctx) { ctx.event()->set_name(name); }); \ + } \ + static void endEvent() \ + { \ + TRACE_EVENT_END(perfettoCategory); \ + } \ + static void counter(perfetto::CounterTrack track, double value) \ + { \ + TRACE_COUNTER(perfettoCategory, track, value); \ + } \ + static void asyncBegin(const perfetto::Track& track, uint64_t startNs, \ + const char* name) \ + { \ + TRACE_EVENT_BEGIN(perfettoCategory, nullptr, track, startNs, \ + [&](perfetto::EventContext ctx) { ctx.event()->set_name(name); }); \ + } \ + static void asyncEnd(const perfetto::Track& track, uint64_t endNs) \ + { \ + TRACE_EVENT_END(perfettoCategory, track, endNs); \ + } \ +}; + +MX_DEFINE_PERFETTO_OPS(RenderOps, "mx.render") +MX_DEFINE_PERFETTO_OPS(ShaderGenOps, "mx.shadergen") +MX_DEFINE_PERFETTO_OPS(OptimizeOps, "mx.optimize") +MX_DEFINE_PERFETTO_OPS(MaterialOps, "mx.material") + +#undef MX_DEFINE_PERFETTO_OPS + +/// Dispatch a generic callable by category. The callable receives a +/// category-ops tag whose static methods wrap the Perfetto trace macros. +template +void withCategory(Category category, Fn&& fn) +{ + switch (category) + { + case Category::Render: fn(RenderOps{}); break; + case Category::ShaderGen: fn(ShaderGenOps{}); break; + case Category::Optimize: fn(OptimizeOps{}); break; + case Category::Material: fn(MaterialOps{}); break; + default: break; + } +} + +} // anonymous namespace + PerfettoSink::PerfettoSink(std::string outputPath, const AsyncTrackMap& asyncTracks, size_t bufferSizeKb) @@ -46,10 +109,11 @@ PerfettoSink::PerfettoSink(std::string outputPath, perfetto::TrackEvent::Register(); }); - // Register async track descriptors from the caller-provided map + // Register async track descriptors from the caller-provided map. + // Explicitly parent to the process track to avoid hierarchy loops. for (const auto& [id, name] : asyncTracks) { - perfetto::Track perfTrack(id); + perfetto::Track perfTrack(id, perfetto::ProcessTrack::Current()); auto desc = perfTrack.Serialize(); desc.set_name(name); perfetto::TrackEvent::SetTrackDescriptor(perfTrack, desc); @@ -90,82 +154,23 @@ PerfettoSink::~PerfettoSink() void PerfettoSink::beginEvent(Category category, const char* name) { - // Perfetto requires compile-time category names for TRACE_EVENT macros. - // Switch on the enum lets the compiler optimize to a jump table. - switch (category) - { - case Category::Render: - TRACE_EVENT_BEGIN("mx.render", nullptr, [&](perfetto::EventContext ctx) { - ctx.event()->set_name(name); - }); - break; - case Category::ShaderGen: - TRACE_EVENT_BEGIN("mx.shadergen", nullptr, [&](perfetto::EventContext ctx) { - ctx.event()->set_name(name); - }); - break; - case Category::Optimize: - TRACE_EVENT_BEGIN("mx.optimize", nullptr, [&](perfetto::EventContext ctx) { - ctx.event()->set_name(name); - }); - break; - case Category::Material: - TRACE_EVENT_BEGIN("mx.material", nullptr, [&](perfetto::EventContext ctx) { - ctx.event()->set_name(name); - }); - break; - default: - // Fallback for any future categories - TRACE_EVENT_BEGIN("mx.render", nullptr, [&](perfetto::EventContext ctx) { - ctx.event()->set_name(name); - }); - break; - } + withCategory(category, [name](auto ops) { + decltype(ops)::beginEvent(name); + }); } void PerfettoSink::endEvent(Category category) { - switch (category) - { - case Category::Render: - TRACE_EVENT_END("mx.render"); - break; - case Category::ShaderGen: - TRACE_EVENT_END("mx.shadergen"); - break; - case Category::Optimize: - TRACE_EVENT_END("mx.optimize"); - break; - case Category::Material: - TRACE_EVENT_END("mx.material"); - break; - default: - TRACE_EVENT_END("mx.render"); - break; - } + withCategory(category, [](auto ops) { + decltype(ops)::endEvent(); + }); } void PerfettoSink::counter(Category category, const char* name, double value) { - auto track = perfetto::CounterTrack(name); - switch (category) - { - case Category::Render: - TRACE_COUNTER("mx.render", track, value); - break; - case Category::ShaderGen: - TRACE_COUNTER("mx.shadergen", track, value); - break; - case Category::Optimize: - TRACE_COUNTER("mx.optimize", track, value); - break; - case Category::Material: - TRACE_COUNTER("mx.material", track, value); - break; - default: - TRACE_COUNTER("mx.render", track, value); - break; - } + withCategory(category, [name, value](auto ops) { + decltype(ops)::counter(perfetto::CounterTrack(name), value); + }); } void PerfettoSink::asyncEvent(AsyncTrackId track, Category category, @@ -178,31 +183,10 @@ void PerfettoSink::asyncEvent(AsyncTrackId track, Category category, const auto& perfTrack = it->second; uint64_t endNs = startNs + durationNs; - switch (category) - { - case Category::Render: - TRACE_EVENT_BEGIN("mx.render", nullptr, perfTrack, startNs, - [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); - TRACE_EVENT_END("mx.render", perfTrack, endNs); - break; - case Category::ShaderGen: - TRACE_EVENT_BEGIN("mx.shadergen", nullptr, perfTrack, startNs, - [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); - TRACE_EVENT_END("mx.shadergen", perfTrack, endNs); - break; - case Category::Optimize: - TRACE_EVENT_BEGIN("mx.optimize", nullptr, perfTrack, startNs, - [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); - TRACE_EVENT_END("mx.optimize", perfTrack, endNs); - break; - case Category::Material: - TRACE_EVENT_BEGIN("mx.material", nullptr, perfTrack, startNs, - [&](perfetto::EventContext ctx) { ctx.event()->set_name(eventName); }); - TRACE_EVENT_END("mx.material", perfTrack, endNs); - break; - default: - break; - } + withCategory(category, [&](auto ops) { + decltype(ops)::asyncBegin(perfTrack, startNs, eventName); + decltype(ops)::asyncEnd(perfTrack, endNs); + }); } uint64_t PerfettoSink::getTraceTimeNs() From 2508ef24de908212302325401939c51930e3b62e Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Thu, 8 Oct 2026 09:35:52 -0400 Subject: [PATCH 11/17] Define GPU_ASYNC_TRACK constant for the well-known GPU track ID --- source/MaterialXRenderGlsl/GlslRenderer.cpp | 2 +- source/MaterialXTest/MaterialXRender/RenderUtil.cpp | 2 +- source/MaterialXTrace/Tracing.h | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/source/MaterialXRenderGlsl/GlslRenderer.cpp b/source/MaterialXRenderGlsl/GlslRenderer.cpp index a7f61f164a..8e1ccb15d6 100644 --- a/source/MaterialXRenderGlsl/GlslRenderer.cpp +++ b/source/MaterialXRenderGlsl/GlslRenderer.cpp @@ -46,7 +46,7 @@ class GpuTimerScope glGetQueryObjectui64v(_query, GL_QUERY_RESULT, &gpuDurationNs); glDeleteQueries(1, &_query); uint64_t nowNs = Tracing::Dispatcher::getInstance().getTraceTimeNs(); - MX_TRACE_ASYNC(1, Tracing::Category::Render, "GPU Frame", + MX_TRACE_ASYNC(Tracing::GPU_ASYNC_TRACK, Tracing::Category::Render, "GPU Frame", nowNs - gpuDurationNs, gpuDurationNs); } diff --git a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp index cb0aaab8c8..8fe59e14e1 100644 --- a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp +++ b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp @@ -710,7 +710,7 @@ void TestRunTracer::start(const std::string& target, const GenShaderUtil::TestSu { mx::FilePath tracePath = options.resolveOutputPath(target + "_render_trace.perfetto-trace"); mx::Tracing::Dispatcher::getInstance().setSink( - mx::Tracing::createPerfettoSink(tracePath.asString(), {{1, "GPU"}})); + mx::Tracing::createPerfettoSink(tracePath.asString(), {{mx::Tracing::GPU_ASYNC_TRACK, "GPU"}})); // Scope guard ensures tracing is shut down on any exit path (return, exception, etc.) _state->guard.emplace(); } diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index 90b114f247..b0f698e9dd 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -61,8 +61,12 @@ enum class Category /// Callers define their own IDs and pass an {id -> name} map when creating /// a sink. The sink uses the map to set up track descriptors; asyncEvent /// then routes events by ID without the sink needing to know the semantics. +/// Note: ID 0 is reserved (Perfetto treats uuid 0 as "no track"). using AsyncTrackId = uint64_t; +/// Well-known async track ID for GPU timing events. +constexpr AsyncTrackId GPU_ASYNC_TRACK = 1; + /// Map of async track definitions: {id -> display name}. using AsyncTrackMap = std::unordered_map; From 51b04d7d22151de996ed9f4259b1248a91402f4b Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Tue, 13 Oct 2026 16:31:16 -0400 Subject: [PATCH 12/17] Derive async track IDs from a compile-time name hash Perfetto computes a custom track's uuid as (id ^ parent uuid), and thread tracks share the same process parent keyed by thread ID. An ID of 0 thus makes the track its own parent, and small IDs can alias a live thread's track (e.g. tid 1 in a Linux PID namespace). Derive IDs with a constexpr 64-bit FNV-1a hash of the track name instead, and fix the usage example that still passed ID 0. --- source/MaterialXTrace/Tracing.h | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index b0f698e9dd..8d34e79901 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -61,11 +61,26 @@ enum class Category /// Callers define their own IDs and pass an {id -> name} map when creating /// a sink. The sink uses the map to set up track descriptors; asyncEvent /// then routes events by ID without the sink needing to know the semantics. -/// Note: ID 0 is reserved (Perfetto treats uuid 0 as "no track"). +/// +/// IDs should be derived with trackIdFromName() rather than chosen by hand. +/// Perfetto computes a track's uuid as (id ^ parent uuid), and thread tracks +/// share the same process parent keyed by thread ID, so an ID of 0 makes the +/// track its own parent, and a small ID can alias a live thread's track. using AsyncTrackId = uint64_t; +/// Derive an async track ID from a name at compile time (64-bit FNV-1a). +constexpr AsyncTrackId trackIdFromName(const char* name) +{ + uint64_t hash = 14695981039346656037ull; + for (; *name; ++name) + { + hash = (hash ^ static_cast(*name)) * 1099511628211ull; + } + return hash; +} + /// Well-known async track ID for GPU timing events. -constexpr AsyncTrackId GPU_ASYNC_TRACK = 1; +constexpr AsyncTrackId GPU_ASYNC_TRACK = trackIdFromName("GPU"); /// Map of async track definitions: {id -> display name}. using AsyncTrackMap = std::unordered_map; @@ -245,7 +260,7 @@ class Scope /// /// Usage: /// Dispatcher::getInstance().setSink( -/// createPerfettoSink("trace.perfetto-trace", {{0, "GPU"}})); +/// createPerfettoSink("trace.perfetto-trace", {{GPU_ASYNC_TRACK, "GPU"}})); /// Dispatcher::ShutdownGuard guard; /// // ... traced work ... /// // guard destructor writes the trace file From 0b4f38ef6d514ec375e786e7aeda0ab034f7abcf Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Tue, 13 Oct 2026 16:31:17 -0400 Subject: [PATCH 13/17] Drop redundant ProcessTrack parent argument The process track is already the default parent of perfetto::Track. --- source/MaterialXTrace/PerfettoSink.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index 2c62bc8d49..060ea7a4d8 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -109,11 +109,10 @@ PerfettoSink::PerfettoSink(std::string outputPath, perfetto::TrackEvent::Register(); }); - // Register async track descriptors from the caller-provided map. - // Explicitly parent to the process track to avoid hierarchy loops. + // Register async track descriptors from the caller-provided map for (const auto& [id, name] : asyncTracks) { - perfetto::Track perfTrack(id, perfetto::ProcessTrack::Current()); + perfetto::Track perfTrack(id); auto desc = perfTrack.Serialize(); desc.set_name(name); perfetto::TrackEvent::SetTrackDescriptor(perfTrack, desc); From fee36b8f016839a09ea7b281221712ace83b25d0 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Tue, 13 Oct 2026 16:31:47 -0400 Subject: [PATCH 14/17] Restore the 1.39.5 createPerfettoSink(path, bufferSizeKb) signature Add the async track map as a separate overload so that existing callers passing a buffer size continue to compile. --- source/MaterialXTrace/PerfettoSink.cpp | 6 ++++++ source/MaterialXTrace/Tracing.h | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index 060ea7a4d8..a2a8226107 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -210,6 +210,12 @@ std::unique_ptr createPerfettoSink(const std::string& outputPath, return std::make_unique(outputPath, asyncTracks, bufferSizeKb); } +std::unique_ptr createPerfettoSink(const std::string& outputPath, + size_t bufferSizeKb) +{ + return std::make_unique(outputPath, AsyncTrackMap{}, bufferSizeKb); +} + } // namespace Tracing MATERIALX_NAMESPACE_END diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index 8d34e79901..cbe3d52e0c 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -266,7 +266,12 @@ class Scope /// // guard destructor writes the trace file MX_TRACE_API std::unique_ptr createPerfettoSink( const std::string& outputPath, - const AsyncTrackMap& asyncTracks = {}, + const AsyncTrackMap& asyncTracks, + size_t bufferSizeKb = 32768); + +/// Create a Perfetto-based tracing sink without async tracks. +MX_TRACE_API std::unique_ptr createPerfettoSink( + const std::string& outputPath, size_t bufferSizeKb = 32768); #endif // MATERIALX_BUILD_PERFETTO_TRACING From bfd1724fb1f694778d344efdc7e80ca38a131c87 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Tue, 13 Oct 2026 16:32:44 -0400 Subject: [PATCH 15/17] Replace asyncEvent with duration-only asyncEventEndingNow The sink now places the slice at its own trace clock's current time, so getTraceTimeNs is removed from Sink and Dispatcher. This avoids a start time of (0 - duration) for third-party sinks that override the event method but not the clock. The method is named for its semantics, so a future explicit-timestamp variant can be added under its own name rather than as an overload of a virtual function. --- source/MaterialXRenderGlsl/GlslRenderer.cpp | 9 ++-- source/MaterialXTrace/PerfettoSink.cpp | 12 ++---- source/MaterialXTrace/PerfettoSink.h | 5 +-- source/MaterialXTrace/Tracing.h | 47 ++++++++------------- 4 files changed, 27 insertions(+), 46 deletions(-) diff --git a/source/MaterialXRenderGlsl/GlslRenderer.cpp b/source/MaterialXRenderGlsl/GlslRenderer.cpp index 8e1ccb15d6..5ba344271b 100644 --- a/source/MaterialXRenderGlsl/GlslRenderer.cpp +++ b/source/MaterialXRenderGlsl/GlslRenderer.cpp @@ -21,7 +21,7 @@ namespace { /// RAII wrapper around a GL_TIME_ELAPSED query. On destruction, reads the -/// result (blocking until the GPU is done) and emits an MX_TRACE_ASYNC event. +/// result (blocking until the GPU is done) and emits an async trace event. class GpuTimerScope { public: @@ -41,13 +41,12 @@ class GpuTimerScope glEndQuery(GL_TIME_ELAPSED); // Reading GL_QUERY_RESULT blocks until the GPU work is done, - // so "now" in the trace clock is a safe end-of-slice marker. + // so the GPU slice can safely end "now". GLuint64 gpuDurationNs = 0; glGetQueryObjectui64v(_query, GL_QUERY_RESULT, &gpuDurationNs); glDeleteQueries(1, &_query); - uint64_t nowNs = Tracing::Dispatcher::getInstance().getTraceTimeNs(); - MX_TRACE_ASYNC(Tracing::GPU_ASYNC_TRACK, Tracing::Category::Render, "GPU Frame", - nowNs - gpuDurationNs, gpuDurationNs); + MX_TRACE_ASYNC_ENDING_NOW(Tracing::GPU_ASYNC_TRACK, Tracing::Category::Render, + "GPU Frame", gpuDurationNs); } GpuTimerScope(const GpuTimerScope&) = delete; diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index a2a8226107..e8b8c50f5f 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -172,15 +172,16 @@ void PerfettoSink::counter(Category category, const char* name, double value) }); } -void PerfettoSink::asyncEvent(AsyncTrackId track, Category category, - const char* eventName, uint64_t startNs, uint64_t durationNs) +void PerfettoSink::asyncEventEndingNow(AsyncTrackId track, Category category, + const char* eventName, uint64_t durationNs) { auto it = _asyncTracks.find(track); if (it == _asyncTracks.end()) return; const auto& perfTrack = it->second; - uint64_t endNs = startNs + durationNs; + uint64_t endNs = perfetto::TrackEvent::GetTraceTimeNs(); + uint64_t startNs = endNs - durationNs; withCategory(category, [&](auto ops) { decltype(ops)::asyncBegin(perfTrack, startNs, eventName); @@ -188,11 +189,6 @@ void PerfettoSink::asyncEvent(AsyncTrackId track, Category category, }); } -uint64_t PerfettoSink::getTraceTimeNs() -{ - return perfetto::TrackEvent::GetTraceTimeNs(); -} - void PerfettoSink::setThreadName(const char* name) { // Set thread name for trace visualization diff --git a/source/MaterialXTrace/PerfettoSink.h b/source/MaterialXTrace/PerfettoSink.h index 0ae59a4ca6..904eb218ce 100644 --- a/source/MaterialXTrace/PerfettoSink.h +++ b/source/MaterialXTrace/PerfettoSink.h @@ -74,9 +74,8 @@ class PerfettoSink : public Sink void beginEvent(Category category, const char* name) override; void endEvent(Category category) override; void counter(Category category, const char* name, double value) override; - void asyncEvent(AsyncTrackId track, Category category, - const char* eventName, uint64_t startNs, uint64_t durationNs) override; - uint64_t getTraceTimeNs() override; + void asyncEventEndingNow(AsyncTrackId track, Category category, + const char* eventName, uint64_t durationNs) override; void setThreadName(const char* name) override; private: diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index cbe3d52e0c..1ac8a03ace 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -59,8 +59,8 @@ enum class Category /// Numeric identifier for an async track (e.g., GPU, Compile, Transfer). /// Callers define their own IDs and pass an {id -> name} map when creating -/// a sink. The sink uses the map to set up track descriptors; asyncEvent -/// then routes events by ID without the sink needing to know the semantics. +/// a sink. The sink uses the map to set up track descriptors; async events +/// are then routed by ID without the sink needing to know the semantics. /// /// IDs should be derived with trackIdFromName() rather than chosen by hand. /// Perfetto computes a track's uuid as (id ^ parent uuid), and thread tracks @@ -104,22 +104,15 @@ class MX_TRACE_API Sink /// Record a counter value (e.g., GPU time, memory usage). virtual void counter(Category category, const char* name, double value) = 0; - /// Record an async event with explicit timing (e.g., GPU operations). - /// The sink places the slice on the track identified by @p track. + /// Record a completed async event (e.g., GPU work whose timer query has + /// just been read back). The sink places the slice on the track identified + /// by @p track, ending at the current time in its own trace clock. /// @param track The async track ID (must have been registered at sink creation) /// @param category The trace category for filtering - /// @param eventName Name of the event (e.g., material name) - /// @param startNs Start timestamp in nanoseconds (in the sink's trace clock; - /// use getTraceTimeNs() to obtain a compatible value) + /// @param eventName Name of the event /// @param durationNs Duration in nanoseconds - virtual void asyncEvent(AsyncTrackId /*track*/, Category /*category*/, - const char* /*eventName*/, uint64_t /*startNs*/, - uint64_t /*durationNs*/) { } - - /// Return the current time in the sink's trace clock (nanoseconds). - /// Use this to obtain timestamps compatible with asyncEvent's startNs. - /// Returns 0 if the sink has no notion of a trace clock. - virtual uint64_t getTraceTimeNs() { return 0; } + virtual void asyncEventEndingNow(AsyncTrackId /*track*/, Category /*category*/, + const char* /*eventName*/, uint64_t /*durationNs*/) { } /// Set the current thread's name for trace visualization. virtual void setThreadName(const char* name) = 0; @@ -189,18 +182,12 @@ class MX_TRACE_API Dispatcher _sink->counter(category, name, value); } - /// Record an async event with explicit timing. - void asyncEvent(AsyncTrackId track, Category category, - const char* eventName, uint64_t startNs, uint64_t durationNs) + /// Record a completed async event ending at the current time. + void asyncEventEndingNow(AsyncTrackId track, Category category, + const char* eventName, uint64_t durationNs) { if (_sink) - _sink->asyncEvent(track, category, eventName, startNs, durationNs); - } - - /// Return the current time in the sink's trace clock (nanoseconds). - uint64_t getTraceTimeNs() - { - return _sink ? _sink->getTraceTimeNs() : 0; + _sink->asyncEventEndingNow(track, category, eventName, durationNs); } private: @@ -254,7 +241,7 @@ class Scope /// /// @param outputPath Path to write the trace file when the sink is destroyed /// @param asyncTracks Map of async track definitions {id -> display name}. -/// Each entry creates a named track in the trace for asyncEvent calls. +/// Each entry creates a named track in the trace for async events. /// @param bufferSizeKb Size of the trace buffer in KB (default 32MB) /// @return A unique_ptr to the Perfetto sink /// @@ -305,9 +292,9 @@ MATERIALX_NAMESPACE_END #define MX_TRACE_COUNTER(category, name, value) \ MaterialX::Tracing::Dispatcher::getInstance().counter(category, name, value) -/// Record an async event with explicit timing (e.g., GPU operations). -#define MX_TRACE_ASYNC(track, category, eventName, startNs, durationNs) \ - MaterialX::Tracing::Dispatcher::getInstance().asyncEvent(track, category, eventName, startNs, durationNs) +/// Record a completed async event ending at the current time (e.g., GPU operations). +#define MX_TRACE_ASYNC_ENDING_NOW(track, category, eventName, durationNs) \ + MaterialX::Tracing::Dispatcher::getInstance().asyncEventEndingNow(track, category, eventName, durationNs) /// Begin a trace event (must be paired with MX_TRACE_END). #define MX_TRACE_BEGIN(category, name) \ @@ -322,7 +309,7 @@ MATERIALX_NAMESPACE_END #define MX_TRACE_SCOPE(category, name) #define MX_TRACE_FUNCTION(category) #define MX_TRACE_COUNTER(category, name, value) -#define MX_TRACE_ASYNC(track, category, eventName, startNs, durationNs) +#define MX_TRACE_ASYNC_ENDING_NOW(track, category, eventName, durationNs) #define MX_TRACE_BEGIN(category, name) #define MX_TRACE_END(category) From 6bdde7ea347d558c2ccb83f97e9fd9471596f5cb Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Tue, 13 Oct 2026 16:33:04 -0400 Subject: [PATCH 16/17] Skip GPU timer queries on contexts below OpenGL 3.3 --- source/MaterialXRenderGlsl/GlslRenderer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/MaterialXRenderGlsl/GlslRenderer.cpp b/source/MaterialXRenderGlsl/GlslRenderer.cpp index 5ba344271b..def659c80d 100644 --- a/source/MaterialXRenderGlsl/GlslRenderer.cpp +++ b/source/MaterialXRenderGlsl/GlslRenderer.cpp @@ -27,7 +27,8 @@ class GpuTimerScope public: GpuTimerScope() { - if (Tracing::Dispatcher::getInstance().isEnabled()) + // Timer queries require OpenGL 3.3, while macOS may fall back to a 3.2 core profile. + if (GLAD_GL_VERSION_3_3 && Tracing::Dispatcher::getInstance().isEnabled()) { glGenQueries(1, &_query); glBeginQuery(GL_TIME_ELAPSED, _query); From e29b97ffdc42ec0e7ec77461ace7e259a5ae7579 Mon Sep 17 00:00:00 2001 From: Pavlo Penenko Date: Tue, 13 Oct 2026 16:54:07 -0400 Subject: [PATCH 17/17] Replace the async track registration map with self-describing tracks An AsyncTrack now carries its display name together with an ID hashed from that name at compile time. PerfettoSink wraps it in a NamedTrack whose Serialize() includes the name, like CounterTrack, so Perfetto writes the descriptor on first use per trace sequence. This removes AsyncTrackMap and the extra constructor and factory arguments, and restores the 1.39.5 createPerfettoSink and PerfettoSink signatures. --- .../MaterialXRender/RenderUtil.cpp | 2 +- source/MaterialXTrace/PerfettoSink.cpp | 68 ++++++++-------- source/MaterialXTrace/PerfettoSink.h | 9 +-- source/MaterialXTrace/Tracing.h | 78 +++++++++---------- 4 files changed, 79 insertions(+), 78 deletions(-) diff --git a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp index 8fe59e14e1..ea81d0489b 100644 --- a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp +++ b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp @@ -710,7 +710,7 @@ void TestRunTracer::start(const std::string& target, const GenShaderUtil::TestSu { mx::FilePath tracePath = options.resolveOutputPath(target + "_render_trace.perfetto-trace"); mx::Tracing::Dispatcher::getInstance().setSink( - mx::Tracing::createPerfettoSink(tracePath.asString(), {{mx::Tracing::GPU_ASYNC_TRACK, "GPU"}})); + mx::Tracing::createPerfettoSink(tracePath.asString())); // Scope guard ensures tracing is shut down on any exit path (return, exception, etc.) _state->guard.emplace(); } diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp index e8b8c50f5f..bd306aa48a 100644 --- a/source/MaterialXTrace/PerfettoSink.cpp +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -43,6 +43,36 @@ namespace Tracing namespace { +// A Perfetto track that carries its display name, in the spirit of +// perfetto::CounterTrack. Perfetto writes a track's descriptor on its first +// use per trace sequence by calling Serialize() on the static track type, so +// no up-front registration via SetTrackDescriptor is needed. +class NamedTrack : public perfetto::Track +{ + public: + explicit NamedTrack(const AsyncTrack& track) : + perfetto::Track(track.id), + _name(track.name) + { + } + + perfetto::protos::gen::TrackDescriptor Serialize() const + { + auto desc = perfetto::Track::Serialize(); + desc.set_name(_name); + return desc; + } + + void Serialize(perfetto::protos::pbzero::TrackDescriptor* desc) const + { + auto bytes = Serialize().SerializeAsString(); + desc->AppendRawProtoBytes(bytes.data(), bytes.size()); + } + + private: + const char* _name; +}; + #define MX_DEFINE_PERFETTO_OPS(Name, perfettoCategory) \ struct Name \ { \ @@ -59,13 +89,13 @@ struct Name { \ TRACE_COUNTER(perfettoCategory, track, value); \ } \ - static void asyncBegin(const perfetto::Track& track, uint64_t startNs, \ + static void asyncBegin(const NamedTrack& track, uint64_t startNs, \ const char* name) \ { \ TRACE_EVENT_BEGIN(perfettoCategory, nullptr, track, startNs, \ [&](perfetto::EventContext ctx) { ctx.event()->set_name(name); }); \ } \ - static void asyncEnd(const perfetto::Track& track, uint64_t endNs) \ + static void asyncEnd(const NamedTrack& track, uint64_t endNs) \ { \ TRACE_EVENT_END(perfettoCategory, track, endNs); \ } \ @@ -95,9 +125,7 @@ void withCategory(Category category, Fn&& fn) } // anonymous namespace -PerfettoSink::PerfettoSink(std::string outputPath, - const AsyncTrackMap& asyncTracks, - size_t bufferSizeKb) +PerfettoSink::PerfettoSink(std::string outputPath, size_t bufferSizeKb) : _outputPath(std::move(outputPath)) { // One-time global Perfetto initialization @@ -109,16 +137,6 @@ PerfettoSink::PerfettoSink(std::string outputPath, perfetto::TrackEvent::Register(); }); - // Register async track descriptors from the caller-provided map - for (const auto& [id, name] : asyncTracks) - { - perfetto::Track perfTrack(id); - auto desc = perfTrack.Serialize(); - desc.set_name(name); - perfetto::TrackEvent::SetTrackDescriptor(perfTrack, desc); - _asyncTracks.emplace(id, perfTrack); - } - // Create and start a tracing session perfetto::TraceConfig cfg; cfg.add_buffers()->set_size_kb(static_cast(bufferSizeKb)); @@ -172,14 +190,10 @@ void PerfettoSink::counter(Category category, const char* name, double value) }); } -void PerfettoSink::asyncEventEndingNow(AsyncTrackId track, Category category, +void PerfettoSink::asyncEventEndingNow(const AsyncTrack& track, Category category, const char* eventName, uint64_t durationNs) { - auto it = _asyncTracks.find(track); - if (it == _asyncTracks.end()) - return; - - const auto& perfTrack = it->second; + NamedTrack perfTrack(track); uint64_t endNs = perfetto::TrackEvent::GetTraceTimeNs(); uint64_t startNs = endNs - durationNs; @@ -199,17 +213,9 @@ void PerfettoSink::setThreadName(const char* name) } // Factory function - the exported entry point -std::unique_ptr createPerfettoSink(const std::string& outputPath, - const AsyncTrackMap& asyncTracks, - size_t bufferSizeKb) -{ - return std::make_unique(outputPath, asyncTracks, bufferSizeKb); -} - -std::unique_ptr createPerfettoSink(const std::string& outputPath, - size_t bufferSizeKb) +std::unique_ptr createPerfettoSink(const std::string& outputPath, size_t bufferSizeKb) { - return std::make_unique(outputPath, AsyncTrackMap{}, bufferSizeKb); + return std::make_unique(outputPath, bufferSizeKb); } } // namespace Tracing diff --git a/source/MaterialXTrace/PerfettoSink.h b/source/MaterialXTrace/PerfettoSink.h index 904eb218ce..876342644b 100644 --- a/source/MaterialXTrace/PerfettoSink.h +++ b/source/MaterialXTrace/PerfettoSink.h @@ -35,7 +35,6 @@ #include #include -#include MATERIALX_NAMESPACE_BEGIN @@ -55,11 +54,8 @@ class PerfettoSink : public Sink public: /// Construct and start a Perfetto tracing session. /// @param outputPath Path to write the trace file when destroyed - /// @param asyncTracks Map of async track definitions {id -> display name} /// @param bufferSizeKb Size of the trace buffer in KB (default 32MB) - explicit PerfettoSink(std::string outputPath, - const AsyncTrackMap& asyncTracks = {}, - size_t bufferSizeKb = 32768); + explicit PerfettoSink(std::string outputPath, size_t bufferSizeKb = 32768); /// Stop tracing and write the trace to the output path. ~PerfettoSink() override; @@ -74,14 +70,13 @@ class PerfettoSink : public Sink void beginEvent(Category category, const char* name) override; void endEvent(Category category) override; void counter(Category category, const char* name, double value) override; - void asyncEventEndingNow(AsyncTrackId track, Category category, + void asyncEventEndingNow(const AsyncTrack& track, Category category, const char* eventName, uint64_t durationNs) override; void setThreadName(const char* name) override; private: const std::string _outputPath; std::unique_ptr _session; - std::unordered_map _asyncTracks; }; } // namespace Tracing diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h index 1ac8a03ace..069488f9af 100644 --- a/source/MaterialXTrace/Tracing.h +++ b/source/MaterialXTrace/Tracing.h @@ -25,7 +25,6 @@ #include #include #include -#include MATERIALX_NAMESPACE_BEGIN @@ -57,33 +56,44 @@ enum class Category Count }; -/// Numeric identifier for an async track (e.g., GPU, Compile, Transfer). -/// Callers define their own IDs and pass an {id -> name} map when creating -/// a sink. The sink uses the map to set up track descriptors; async events -/// are then routed by ID without the sink needing to know the semantics. +/// @struct AsyncTrack +/// A named track for async events (e.g., GPU, Compile, Transfer). /// -/// IDs should be derived with trackIdFromName() rather than chosen by hand. -/// Perfetto computes a track's uuid as (id ^ parent uuid), and thread tracks -/// share the same process parent keyed by thread ID, so an ID of 0 makes the -/// track its own parent, and a small ID can alias a live thread's track. -using AsyncTrackId = uint64_t; - -/// Derive an async track ID from a name at compile time (64-bit FNV-1a). -constexpr AsyncTrackId trackIdFromName(const char* name) +/// The ID is a hash of the name, computed at compile time when the track is +/// declared constexpr, so sinks need no registration step. Perfetto computes +/// a track's uuid as (id ^ parent uuid), and thread tracks share the same +/// process parent keyed by thread ID, so hand-picked small IDs could alias a +/// live thread's track, and an ID of 0 would make the track its own parent. +/// +/// Usage: +/// constexpr AsyncTrack MY_TRACK("My Track"); +struct AsyncTrack { - uint64_t hash = 14695981039346656037ull; - for (; *name; ++name) + /// @param trackName Display name of the track. Must have static lifetime. + constexpr explicit AsyncTrack(const char* trackName) : + id(hashName(trackName)), + name(trackName) { - hash = (hash ^ static_cast(*name)) * 1099511628211ull; } - return hash; -} -/// Well-known async track ID for GPU timing events. -constexpr AsyncTrackId GPU_ASYNC_TRACK = trackIdFromName("GPU"); + uint64_t id; + const char* name; -/// Map of async track definitions: {id -> display name}. -using AsyncTrackMap = std::unordered_map; + private: + // 64-bit FNV-1a + static constexpr uint64_t hashName(const char* str) + { + uint64_t hash = 14695981039346656037ull; + for (; *str; ++str) + { + hash = (hash ^ static_cast(*str)) * 1099511628211ull; + } + return hash; + } +}; + +/// Well-known async track for GPU timing events. +constexpr AsyncTrack GPU_ASYNC_TRACK("GPU"); /// @class Sink /// Abstract tracing sink interface. @@ -105,13 +115,13 @@ class MX_TRACE_API Sink virtual void counter(Category category, const char* name, double value) = 0; /// Record a completed async event (e.g., GPU work whose timer query has - /// just been read back). The sink places the slice on the track identified - /// by @p track, ending at the current time in its own trace clock. - /// @param track The async track ID (must have been registered at sink creation) + /// just been read back). The sink places the slice on @p track, ending at + /// the current time in its own trace clock. + /// @param track The async track to record on /// @param category The trace category for filtering /// @param eventName Name of the event /// @param durationNs Duration in nanoseconds - virtual void asyncEventEndingNow(AsyncTrackId /*track*/, Category /*category*/, + virtual void asyncEventEndingNow(const AsyncTrack& /*track*/, Category /*category*/, const char* /*eventName*/, uint64_t /*durationNs*/) { } /// Set the current thread's name for trace visualization. @@ -183,7 +193,7 @@ class MX_TRACE_API Dispatcher } /// Record a completed async event ending at the current time. - void asyncEventEndingNow(AsyncTrackId track, Category category, + void asyncEventEndingNow(const AsyncTrack& track, Category category, const char* eventName, uint64_t durationNs) { if (_sink) @@ -240,26 +250,16 @@ class Scope /// visualized at https://ui.perfetto.dev /// /// @param outputPath Path to write the trace file when the sink is destroyed -/// @param asyncTracks Map of async track definitions {id -> display name}. -/// Each entry creates a named track in the trace for async events. /// @param bufferSizeKb Size of the trace buffer in KB (default 32MB) /// @return A unique_ptr to the Perfetto sink /// /// Usage: -/// Dispatcher::getInstance().setSink( -/// createPerfettoSink("trace.perfetto-trace", {{GPU_ASYNC_TRACK, "GPU"}})); +/// Dispatcher::getInstance().setSink(createPerfettoSink("trace.perfetto-trace")); /// Dispatcher::ShutdownGuard guard; /// // ... traced work ... /// // guard destructor writes the trace file MX_TRACE_API std::unique_ptr createPerfettoSink( - const std::string& outputPath, - const AsyncTrackMap& asyncTracks, - size_t bufferSizeKb = 32768); - -/// Create a Perfetto-based tracing sink without async tracks. -MX_TRACE_API std::unique_ptr createPerfettoSink( - const std::string& outputPath, - size_t bufferSizeKb = 32768); + const std::string& outputPath, size_t bufferSizeKb = 32768); #endif // MATERIALX_BUILD_PERFETTO_TRACING