From f78e34ebdb862213b526a2bd78c4effb8a6c3fdd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 01:55:03 +0000 Subject: [PATCH 1/5] Add pipeline trace logging (enable_pipeline_trace setting) Adds two debugging capabilities to the pipeline execution system: 1. Static pipeline graph: after query initialization, prints all pipelines and their dependency relationships to stderr. 2. Runtime timing trace: records wall-clock start/end times for each pipeline and outputs a Chrome Trace JSON report to stderr on query completion. The JSON is loadable in https://ui.perfetto.dev/ or chrome://tracing for an interactive Gantt visualization. Usage: SET enable_pipeline_trace = true; SELECT ...; -- pipeline graph + Chrome trace JSON go to stderr New files: src/include/duckdb/parallel/pipeline_tracer.hpp src/parallel/pipeline_tracer.cpp Key changes: - Pipeline: add pipeline_id, start_time_ns, end_time_ns, MarkStart/MarkEnd - PipelineEvent: call MarkStart in Schedule(), MarkEnd in FinishEvent() - PipelineFinishEvent: call MarkEnd in FinishEvent() (last-call wins, so finalization time is captured for base pipelines) - Executor: assign IDs, save traced_pipelines ref, print graph at init and Chrome trace JSON before pipelines.clear() - Settings: add enable_pipeline_trace (local bool, default false) https://claude.ai/code/session_01UV8WQWq1RyBsyTAxsbu4yG --- src/common/settings.json | 10 ++- src/include/duckdb/execution/executor.hpp | 4 + src/include/duckdb/main/client_config.hpp | 4 + src/include/duckdb/main/settings.hpp | 12 +++ src/include/duckdb/parallel/pipeline.hpp | 22 ++++++ .../duckdb/parallel/pipeline_tracer.hpp | 41 ++++++++++ src/main/settings/custom_settings.cpp | 16 ++++ src/parallel/CMakeLists.txt | 1 + src/parallel/executor.cpp | 20 +++++ src/parallel/pipeline.cpp | 11 +++ src/parallel/pipeline_event.cpp | 2 + src/parallel/pipeline_finish_event.cpp | 1 + src/parallel/pipeline_tracer.cpp | 75 +++++++++++++++++++ 13 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/include/duckdb/parallel/pipeline_tracer.hpp create mode 100644 src/parallel/pipeline_tracer.cpp diff --git a/src/common/settings.json b/src/common/settings.json index 9a4af33d5174..397cdb7d0b1b 100644 --- a/src/common/settings.json +++ b/src/common/settings.json @@ -514,6 +514,14 @@ "default_scope": "global", "default_value": "false" }, + { + "name": "enable_pipeline_trace", + "description": "Enables pipeline trace logging: prints the pipeline graph and outputs a Chrome Trace JSON timing report to stderr on query completion", + "type": "BOOLEAN", + "scope": "local", + "struct": "EnablePipelineTraceSetting", + "custom_implementation": true + }, { "name": "enable_profiling", "description": "Enables profiling, and sets the output format (JSON, QUERY_TREE, QUERY_TREE_OPTIMIZER)", @@ -1119,4 +1127,4 @@ "scope": "global", "default_value": "4096" } -] \ No newline at end of file +] diff --git a/src/include/duckdb/execution/executor.hpp b/src/include/duckdb/execution/executor.hpp index 46315a989828..83486fc74df0 100644 --- a/src/include/duckdb/execution/executor.hpp +++ b/src/include/duckdb/execution/executor.hpp @@ -155,6 +155,10 @@ class Executor { vector> pipelines; //! The root pipelines of the query vector> root_pipelines; + //! Pipelines saved for trace output (persists after pipelines.clear()) + vector> traced_pipelines; + //! Steady-clock nanoseconds at query initialization (base for Chrome trace timestamps) + int64_t pipeline_trace_start_ns = 0; //! The recursive CTE's in this query plan vector> recursive_ctes; //! The pipeline executor for the root pipeline diff --git a/src/include/duckdb/main/client_config.hpp b/src/include/duckdb/main/client_config.hpp index 893ddcb318bf..4eaf0e18d4fc 100644 --- a/src/include/duckdb/main/client_config.hpp +++ b/src/include/duckdb/main/client_config.hpp @@ -101,6 +101,10 @@ struct ClientConfig { //! If HTTP logging is enabled or not. bool enable_http_logging = true; + //! If pipeline trace logging is enabled (SET enable_pipeline_trace = true). + //! Prints the pipeline dependency graph and Chrome Trace JSON timing to stderr. + bool enable_pipeline_trace = false; + //! **DEPRECATED** The file to save query HTTP logging information to, instead of printing it to the console //! (empty = output to the DuckDB logger) string http_logging_output; diff --git a/src/include/duckdb/main/settings.hpp b/src/include/duckdb/main/settings.hpp index cb339df2ea24..6992cb665eba 100644 --- a/src/include/duckdb/main/settings.hpp +++ b/src/include/duckdb/main/settings.hpp @@ -780,6 +780,18 @@ struct EnableObjectCacheSetting { static constexpr idx_t SettingIndex = 42; }; +struct EnablePipelineTraceSetting { + using RETURN_TYPE = bool; + static constexpr const char *Name = "enable_pipeline_trace"; + static constexpr const char *Description = + "Enables pipeline trace logging: prints the pipeline graph and outputs a Chrome Trace JSON " + "timing report to stderr on query completion"; + static constexpr const char *InputType = "BOOLEAN"; + static void SetLocal(ClientContext &context, const Value ¶meter); + static void ResetLocal(ClientContext &context); + static Value GetSetting(const ClientContext &context); +}; + struct EnableProfilingSetting { using RETURN_TYPE = string; static constexpr const char *Name = "enable_profiling"; diff --git a/src/include/duckdb/parallel/pipeline.hpp b/src/include/duckdb/parallel/pipeline.hpp index 0d8a665a5d65..ef2758811a6f 100644 --- a/src/include/duckdb/parallel/pipeline.hpp +++ b/src/include/duckdb/parallel/pipeline.hpp @@ -76,6 +76,7 @@ class Pipeline : public enable_shared_from_this { friend class PipelineFinishEvent; friend class PipelineBuildState; friend class MetaPipeline; + friend class PipelineTracer; public: explicit Pipeline(Executor &execution_context); @@ -100,6 +101,21 @@ class Pipeline : public enable_shared_from_this { void Print() const; void PrintDependencies() const; + //! Record the wall-clock start time of this pipeline's execution. + void MarkStart(); + //! Record the wall-clock end time (last-call wins, so PipelineFinishEvent overwrites PipelineEvent). + void MarkEnd(); + + idx_t GetPipelineId() const { + return pipeline_id; + } + int64_t GetStartTimeNs() const { + return start_time_ns; + } + int64_t GetEndTimeNs() const { + return end_time_ns; + } + //! Returns query progress bool GetProgress(ProgressData &progress_data); @@ -132,6 +148,12 @@ class Pipeline : public enable_shared_from_this { atomic initialized; //! The source of this pipeline optional_ptr source; + + //! Pipeline ID assigned by PipelineTracer::AssignIds (0-based) + idx_t pipeline_id = 0; + //! Wall-clock start/end times in steady_clock nanoseconds since epoch (-1 = unset) + int64_t start_time_ns = -1; + int64_t end_time_ns = -1; //! The chain of intermediate operators vector> operators; //! The sink (i.e. destination) for data; this is e.g. a hash table to-be-built diff --git a/src/include/duckdb/parallel/pipeline_tracer.hpp b/src/include/duckdb/parallel/pipeline_tracer.hpp new file mode 100644 index 000000000000..332c19fd0dcc --- /dev/null +++ b/src/include/duckdb/parallel/pipeline_tracer.hpp @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/parallel/pipeline_tracer.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/common/common.hpp" + +namespace duckdb { + +class Pipeline; + +//! PipelineTracer collects and outputs pipeline structure and execution timing. +//! Enabled by SET enable_pipeline_trace = true. +//! Outputs: +//! 1. A pipeline dependency graph to stderr after initialization. +//! 2. A Chrome Trace JSON timing report to stderr after query completion +//! (loadable in https://ui.perfetto.dev/ or chrome://tracing). +class PipelineTracer { +public: + //! Assign sequential IDs (0, 1, 2, ...) to all pipelines. + //! Must be called before PrintGraph or PrintChromeTrace. + static void AssignIds(vector> &pipelines); + + //! Print the static pipeline structure and dependency graph to stderr. + static void PrintGraph(const vector> &pipelines); + + //! Print Chrome Trace JSON to stderr. + //! query_start_ns: steady_clock nanoseconds since epoch at query start. + static void PrintChromeTrace(const vector> &pipelines, int64_t query_start_ns); + +private: + //! Build a short human-readable description: "TableScan→HashJoinBuild→..." + static string Describe(const Pipeline &pipeline); +}; + +} // namespace duckdb diff --git a/src/main/settings/custom_settings.cpp b/src/main/settings/custom_settings.cpp index ba82c71d5826..adea90ac47de 100644 --- a/src/main/settings/custom_settings.cpp +++ b/src/main/settings/custom_settings.cpp @@ -1205,6 +1205,22 @@ Value EnableHTTPLoggingSetting::GetSetting(const ClientContext &context) { return Value::BOOLEAN(config.enable_http_logging); } +//===----------------------------------------------------------------------===// +// Enable Pipeline Trace +//===----------------------------------------------------------------------===// + +void EnablePipelineTraceSetting::SetLocal(ClientContext &context, const Value &input) { + ClientConfig::GetConfig(context).enable_pipeline_trace = input.GetValue(); +} + +void EnablePipelineTraceSetting::ResetLocal(ClientContext &context) { + ClientConfig::GetConfig(context).enable_pipeline_trace = ClientConfig().enable_pipeline_trace; +} + +Value EnablePipelineTraceSetting::GetSetting(const ClientContext &context) { + return Value::BOOLEAN(ClientConfig::GetConfig(context).enable_pipeline_trace); +} + //===----------------------------------------------------------------------===// // Enable Mbedtls //===----------------------------------------------------------------------===// diff --git a/src/parallel/CMakeLists.txt b/src/parallel/CMakeLists.txt index 307168ff49f8..ae366ef47727 100644 --- a/src/parallel/CMakeLists.txt +++ b/src/parallel/CMakeLists.txt @@ -10,6 +10,7 @@ add_library_unity( interrupt.cpp pipeline.cpp pipeline_complete_event.cpp + pipeline_tracer.cpp pipeline_event.cpp pipeline_executor.cpp pipeline_finish_event.cpp diff --git a/src/parallel/executor.cpp b/src/parallel/executor.cpp index bc8b75723359..8083e0145c6e 100644 --- a/src/parallel/executor.cpp +++ b/src/parallel/executor.cpp @@ -1,5 +1,6 @@ #include "duckdb/execution/executor.hpp" +#include "duckdb/common/chrono.hpp" #include "duckdb/execution/execution_context.hpp" #include "duckdb/execution/operator/helper/physical_result_collector.hpp" #include "duckdb/execution/operator/scan/physical_table_scan.hpp" @@ -16,8 +17,10 @@ #include "duckdb/parallel/pipeline_finish_event.hpp" #include "duckdb/parallel/pipeline_initialize_event.hpp" #include "duckdb/parallel/pipeline_prepare_finish_event.hpp" +#include "duckdb/parallel/pipeline_tracer.hpp" #include "duckdb/parallel/task_scheduler.hpp" #include "duckdb/parallel/thread_context.hpp" +#include "duckdb/main/client_config.hpp" #include #include @@ -424,6 +427,16 @@ void Executor::InitializeInternal(PhysicalOperator &plan) { // finally, verify and schedule VerifyPipelines(); ScheduleEvents(to_schedule); + + // pipeline trace: assign IDs, record query start time, and print the static graph + auto &trace_config = ClientConfig::GetConfig(context); + if (trace_config.enable_pipeline_trace) { + PipelineTracer::AssignIds(pipelines); + pipeline_trace_start_ns = static_cast( + duration_cast(steady_clock::now().time_since_epoch()).count()); + PipelineTracer::PrintGraph(pipelines); + traced_pipelines = pipelines; + } } } @@ -622,6 +635,11 @@ PendingExecutionResult Executor::ExecuteTask(bool dry_run) { D_ASSERT(!task); lock_guard elock(executor_lock); + // emit Chrome Trace JSON before clearing pipelines (timing data lives in Pipeline objects) + if (!traced_pipelines.empty()) { + PipelineTracer::PrintChromeTrace(traced_pipelines, pipeline_trace_start_ns); + traced_pipelines.clear(); + } pipelines.clear(); NextExecutor(); if (HasError()) { // LCOV_EXCL_START @@ -647,6 +665,8 @@ void Executor::Reset() { events.clear(); to_be_rescheduled_tasks.clear(); execution_result = PendingExecutionResult::RESULT_NOT_READY; + traced_pipelines.clear(); + pipeline_trace_start_ns = 0; } shared_ptr Executor::CreateChildPipeline(Pipeline ¤t, PhysicalOperator &op) { diff --git a/src/parallel/pipeline.cpp b/src/parallel/pipeline.cpp index d48cc1241cb7..c029019e2a02 100644 --- a/src/parallel/pipeline.cpp +++ b/src/parallel/pipeline.cpp @@ -1,6 +1,7 @@ #include "duckdb/parallel/pipeline.hpp" #include "duckdb/common/algorithm.hpp" +#include "duckdb/common/chrono.hpp" #include "duckdb/common/printer.hpp" #include "duckdb/common/tree_renderer/text_tree_renderer.hpp" #include "duckdb/execution/executor.hpp" @@ -73,6 +74,16 @@ ClientContext &Pipeline::GetClientContext() { return executor.context; } +void Pipeline::MarkStart() { + start_time_ns = static_cast( + duration_cast(steady_clock::now().time_since_epoch()).count()); +} + +void Pipeline::MarkEnd() { + end_time_ns = static_cast( + duration_cast(steady_clock::now().time_since_epoch()).count()); +} + bool Pipeline::GetProgress(ProgressData &progress) { D_ASSERT(source); idx_t source_cardinality = MinValue(source->estimated_cardinality, 1ULL << 48ULL); diff --git a/src/parallel/pipeline_event.cpp b/src/parallel/pipeline_event.cpp index a7f07e2d9cc3..38b57d8caecd 100644 --- a/src/parallel/pipeline_event.cpp +++ b/src/parallel/pipeline_event.cpp @@ -9,6 +9,7 @@ PipelineEvent::PipelineEvent(shared_ptr pipeline_p) : BasePipelineEven void PipelineEvent::Schedule() { auto event = shared_from_this(); auto &executor = pipeline->executor; + pipeline->MarkStart(); try { pipeline->Schedule(event); D_ASSERT(total_tasks > 0); @@ -20,6 +21,7 @@ void PipelineEvent::Schedule() { } void PipelineEvent::FinishEvent() { + pipeline->MarkEnd(); } } // namespace duckdb diff --git a/src/parallel/pipeline_finish_event.cpp b/src/parallel/pipeline_finish_event.cpp index bf57cda256c4..196329f1c571 100644 --- a/src/parallel/pipeline_finish_event.cpp +++ b/src/parallel/pipeline_finish_event.cpp @@ -84,6 +84,7 @@ void PipelineFinishEvent::Schedule() { } void PipelineFinishEvent::FinishEvent() { + pipeline->MarkEnd(); } } // namespace duckdb diff --git a/src/parallel/pipeline_tracer.cpp b/src/parallel/pipeline_tracer.cpp new file mode 100644 index 000000000000..b040135ae85b --- /dev/null +++ b/src/parallel/pipeline_tracer.cpp @@ -0,0 +1,75 @@ +#include "duckdb/parallel/pipeline_tracer.hpp" + +#include "duckdb/common/printer.hpp" +#include "duckdb/common/string_util.hpp" +#include "duckdb/execution/physical_operator.hpp" +#include "duckdb/parallel/pipeline.hpp" + +namespace duckdb { + +void PipelineTracer::AssignIds(vector> &pipelines) { + for (idx_t i = 0; i < pipelines.size(); i++) { + pipelines[i]->pipeline_id = i; + } +} + +string PipelineTracer::Describe(const Pipeline &pipeline) { + auto ops = pipeline.GetOperators(); + string result; + for (idx_t i = 0; i < ops.size(); i++) { + if (i > 0) { + result += "→"; + } + result += ops[i].get().GetName(); + } + return result; +} + +void PipelineTracer::PrintGraph(const vector> &pipelines) { + string out = "\n=== Pipeline Graph ===\n"; + for (auto &p : pipelines) { + out += "Pipeline #" + to_string(p->pipeline_id) + ": " + Describe(*p) + "\n"; + auto deps = p->GetDependencies(); + if (!deps.empty()) { + out += " depends on:"; + for (auto &dep : deps) { + auto locked = dep.lock(); + if (locked) { + out += " #" + to_string(locked->pipeline_id); + } + } + out += "\n"; + } + } + out += "======================\n"; + Printer::Print(OutputStream::STREAM_STDERR, out); +} + +void PipelineTracer::PrintChromeTrace(const vector> &pipelines, int64_t query_start_ns) { + string events; + bool first = true; + for (auto &p : pipelines) { + if (p->start_time_ns < 0 || p->end_time_ns < 0) { + continue; + } + int64_t start_us = (p->start_time_ns - query_start_ns) / 1000; + int64_t dur_us = (p->end_time_ns - p->start_time_ns) / 1000; + if (dur_us < 0) { + dur_us = 0; + } + if (!first) { + events += ",\n "; + } else { + first = false; + } + string name = "#" + to_string(p->pipeline_id) + ": " + Describe(*p); + // Escape quotes in name for JSON safety + StringUtil::ReplaceAll(name, "\"", "\\\""); + events += "{\"name\":\"" + name + "\",\"ph\":\"X\",\"pid\":0,\"tid\":" + to_string(p->pipeline_id) + + ",\"ts\":" + to_string(start_us) + ",\"dur\":" + to_string(dur_us) + "}"; + } + string json = "\n{\"traceEvents\":[\n " + events + "\n]}\n"; + Printer::Print(OutputStream::STREAM_STDERR, json); +} + +} // namespace duckdb From 9faa2b4aaf6f2a1a1d5bb79206fbad27e5f5e7fd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 02:00:13 +0000 Subject: [PATCH 2/5] Remove redundant client_config.hpp include in executor.cpp client_config.hpp is already included transitively via client_context.hpp. https://claude.ai/code/session_01UV8WQWq1RyBsyTAxsbu4yG --- src/parallel/executor.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parallel/executor.cpp b/src/parallel/executor.cpp index 8083e0145c6e..5d25ed230d0f 100644 --- a/src/parallel/executor.cpp +++ b/src/parallel/executor.cpp @@ -20,7 +20,6 @@ #include "duckdb/parallel/pipeline_tracer.hpp" #include "duckdb/parallel/task_scheduler.hpp" #include "duckdb/parallel/thread_context.hpp" -#include "duckdb/main/client_config.hpp" #include #include From 2ef8634136340e6b0b71693989088ec61366909b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 23:06:39 +0000 Subject: [PATCH 3/5] Add pipeline_graph_output and pipeline_trace_output session settings Allows directing each trace output to a dedicated file: SET pipeline_graph_output = '/tmp/graph.txt'; -- pipeline dependency graph SET pipeline_trace_output = '/tmp/trace.json'; -- Chrome Trace JSON (Perfetto) When a path is empty (default), the respective output falls back to stderr. If the file cannot be opened, a warning is prepended and stderr is used. Changes: - PipelineTracer: add WriteOutput() helper (file vs stderr), update PrintGraph/PrintChromeTrace signatures to accept output_path - ClientConfig: add pipeline_graph_output / pipeline_trace_output fields - Settings: add PipelineGraphOutputSetting / PipelineTraceOutputSetting - settings.json: register both new settings - custom_settings.cpp: implement SetLocal/ResetLocal/GetSetting for both - executor.cpp: thread file paths through to tracer calls https://claude.ai/code/session_01UV8WQWq1RyBsyTAxsbu4yG --- src/common/settings.json | 16 ++++++++++ src/include/duckdb/main/client_config.hpp | 4 +++ src/include/duckdb/main/settings.hpp | 22 +++++++++++++ .../duckdb/parallel/pipeline_tracer.hpp | 22 ++++++++----- src/main/settings/custom_settings.cpp | 32 +++++++++++++++++++ src/parallel/executor.cpp | 5 +-- src/parallel/pipeline_tracer.cpp | 28 +++++++++++++--- 7 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/common/settings.json b/src/common/settings.json index 397cdb7d0b1b..88dc8d8520c8 100644 --- a/src/common/settings.json +++ b/src/common/settings.json @@ -903,6 +903,22 @@ "scope": "global", "default_value": "auto" }, + { + "name": "pipeline_graph_output", + "description": "File path for pipeline graph output when enable_pipeline_trace is true (empty = stderr)", + "type": "VARCHAR", + "scope": "local", + "struct": "PipelineGraphOutputSetting", + "custom_implementation": true + }, + { + "name": "pipeline_trace_output", + "description": "File path for Chrome Trace JSON output when enable_pipeline_trace is true (empty = stderr)", + "type": "VARCHAR", + "scope": "local", + "struct": "PipelineTraceOutputSetting", + "custom_implementation": true + }, { "name": "pivot_filter_threshold", "description": "The threshold to switch from using filtered aggregates to LIST with a dedicated pivot operator", diff --git a/src/include/duckdb/main/client_config.hpp b/src/include/duckdb/main/client_config.hpp index 4eaf0e18d4fc..2528af58021f 100644 --- a/src/include/duckdb/main/client_config.hpp +++ b/src/include/duckdb/main/client_config.hpp @@ -104,6 +104,10 @@ struct ClientConfig { //! If pipeline trace logging is enabled (SET enable_pipeline_trace = true). //! Prints the pipeline dependency graph and Chrome Trace JSON timing to stderr. bool enable_pipeline_trace = false; + //! File path for pipeline graph output (empty = stderr). + string pipeline_graph_output; + //! File path for Chrome Trace JSON output (empty = stderr). + string pipeline_trace_output; //! **DEPRECATED** The file to save query HTTP logging information to, instead of printing it to the console //! (empty = output to the DuckDB logger) diff --git a/src/include/duckdb/main/settings.hpp b/src/include/duckdb/main/settings.hpp index 6992cb665eba..114f93aa5b32 100644 --- a/src/include/duckdb/main/settings.hpp +++ b/src/include/duckdb/main/settings.hpp @@ -792,6 +792,28 @@ struct EnablePipelineTraceSetting { static Value GetSetting(const ClientContext &context); }; +struct PipelineGraphOutputSetting { + using RETURN_TYPE = string; + static constexpr const char *Name = "pipeline_graph_output"; + static constexpr const char *Description = + "File path for pipeline graph output when enable_pipeline_trace is true (empty = stderr)"; + static constexpr const char *InputType = "VARCHAR"; + static void SetLocal(ClientContext &context, const Value ¶meter); + static void ResetLocal(ClientContext &context); + static Value GetSetting(const ClientContext &context); +}; + +struct PipelineTraceOutputSetting { + using RETURN_TYPE = string; + static constexpr const char *Name = "pipeline_trace_output"; + static constexpr const char *Description = + "File path for Chrome Trace JSON output when enable_pipeline_trace is true (empty = stderr)"; + static constexpr const char *InputType = "VARCHAR"; + static void SetLocal(ClientContext &context, const Value ¶meter); + static void ResetLocal(ClientContext &context); + static Value GetSetting(const ClientContext &context); +}; + struct EnableProfilingSetting { using RETURN_TYPE = string; static constexpr const char *Name = "enable_profiling"; diff --git a/src/include/duckdb/parallel/pipeline_tracer.hpp b/src/include/duckdb/parallel/pipeline_tracer.hpp index 332c19fd0dcc..c3dd0837b8f6 100644 --- a/src/include/duckdb/parallel/pipeline_tracer.hpp +++ b/src/include/duckdb/parallel/pipeline_tracer.hpp @@ -16,26 +16,32 @@ class Pipeline; //! PipelineTracer collects and outputs pipeline structure and execution timing. //! Enabled by SET enable_pipeline_trace = true. -//! Outputs: -//! 1. A pipeline dependency graph to stderr after initialization. -//! 2. A Chrome Trace JSON timing report to stderr after query completion -//! (loadable in https://ui.perfetto.dev/ or chrome://tracing). +//! +//! Output destinations (empty string = stderr): +//! SET pipeline_graph_output = '/path/graph.txt' -- pipeline dependency graph +//! SET pipeline_trace_output = '/path/trace.json' -- Chrome Trace JSON (Perfetto) class PipelineTracer { public: //! Assign sequential IDs (0, 1, 2, ...) to all pipelines. //! Must be called before PrintGraph or PrintChromeTrace. static void AssignIds(vector> &pipelines); - //! Print the static pipeline structure and dependency graph to stderr. - static void PrintGraph(const vector> &pipelines); + //! Print the static pipeline structure and dependency graph. + //! output_path: file path to write to, or empty string for stderr. + static void PrintGraph(const vector> &pipelines, const string &output_path); - //! Print Chrome Trace JSON to stderr. + //! Print Chrome Trace JSON (loadable in https://ui.perfetto.dev/). //! query_start_ns: steady_clock nanoseconds since epoch at query start. - static void PrintChromeTrace(const vector> &pipelines, int64_t query_start_ns); + //! output_path: file path to write to, or empty string for stderr. + static void PrintChromeTrace(const vector> &pipelines, int64_t query_start_ns, + const string &output_path); private: //! Build a short human-readable description: "TableScan→HashJoinBuild→..." static string Describe(const Pipeline &pipeline); + + //! Write content to output_path if non-empty, otherwise to stderr. + static void WriteOutput(const string &content, const string &output_path); }; } // namespace duckdb diff --git a/src/main/settings/custom_settings.cpp b/src/main/settings/custom_settings.cpp index adea90ac47de..5e7f394f50da 100644 --- a/src/main/settings/custom_settings.cpp +++ b/src/main/settings/custom_settings.cpp @@ -1221,6 +1221,38 @@ Value EnablePipelineTraceSetting::GetSetting(const ClientContext &context) { return Value::BOOLEAN(ClientConfig::GetConfig(context).enable_pipeline_trace); } +//===----------------------------------------------------------------------===// +// Pipeline Graph Output +//===----------------------------------------------------------------------===// + +void PipelineGraphOutputSetting::SetLocal(ClientContext &context, const Value &input) { + ClientConfig::GetConfig(context).pipeline_graph_output = input.GetValue(); +} + +void PipelineGraphOutputSetting::ResetLocal(ClientContext &context) { + ClientConfig::GetConfig(context).pipeline_graph_output = ClientConfig().pipeline_graph_output; +} + +Value PipelineGraphOutputSetting::GetSetting(const ClientContext &context) { + return Value(ClientConfig::GetConfig(context).pipeline_graph_output); +} + +//===----------------------------------------------------------------------===// +// Pipeline Trace Output +//===----------------------------------------------------------------------===// + +void PipelineTraceOutputSetting::SetLocal(ClientContext &context, const Value &input) { + ClientConfig::GetConfig(context).pipeline_trace_output = input.GetValue(); +} + +void PipelineTraceOutputSetting::ResetLocal(ClientContext &context) { + ClientConfig::GetConfig(context).pipeline_trace_output = ClientConfig().pipeline_trace_output; +} + +Value PipelineTraceOutputSetting::GetSetting(const ClientContext &context) { + return Value(ClientConfig::GetConfig(context).pipeline_trace_output); +} + //===----------------------------------------------------------------------===// // Enable Mbedtls //===----------------------------------------------------------------------===// diff --git a/src/parallel/executor.cpp b/src/parallel/executor.cpp index 5d25ed230d0f..b5a4e4d21638 100644 --- a/src/parallel/executor.cpp +++ b/src/parallel/executor.cpp @@ -433,7 +433,7 @@ void Executor::InitializeInternal(PhysicalOperator &plan) { PipelineTracer::AssignIds(pipelines); pipeline_trace_start_ns = static_cast( duration_cast(steady_clock::now().time_since_epoch()).count()); - PipelineTracer::PrintGraph(pipelines); + PipelineTracer::PrintGraph(pipelines, trace_config.pipeline_graph_output); traced_pipelines = pipelines; } } @@ -636,7 +636,8 @@ PendingExecutionResult Executor::ExecuteTask(bool dry_run) { lock_guard elock(executor_lock); // emit Chrome Trace JSON before clearing pipelines (timing data lives in Pipeline objects) if (!traced_pipelines.empty()) { - PipelineTracer::PrintChromeTrace(traced_pipelines, pipeline_trace_start_ns); + auto &trace_config = ClientConfig::GetConfig(context); + PipelineTracer::PrintChromeTrace(traced_pipelines, pipeline_trace_start_ns, trace_config.pipeline_trace_output); traced_pipelines.clear(); } pipelines.clear(); diff --git a/src/parallel/pipeline_tracer.cpp b/src/parallel/pipeline_tracer.cpp index b040135ae85b..2fe813875cf5 100644 --- a/src/parallel/pipeline_tracer.cpp +++ b/src/parallel/pipeline_tracer.cpp @@ -5,6 +5,8 @@ #include "duckdb/execution/physical_operator.hpp" #include "duckdb/parallel/pipeline.hpp" +#include + namespace duckdb { void PipelineTracer::AssignIds(vector> &pipelines) { @@ -25,7 +27,22 @@ string PipelineTracer::Describe(const Pipeline &pipeline) { return result; } -void PipelineTracer::PrintGraph(const vector> &pipelines) { +void PipelineTracer::WriteOutput(const string &content, const string &output_path) { + if (output_path.empty()) { + Printer::Print(OutputStream::STREAM_STDERR, content); + return; + } + std::ofstream f(output_path, std::ios::out | std::ios::trunc); + if (!f.is_open()) { + // Fall back to stderr if the file cannot be opened + Printer::Print(OutputStream::STREAM_STDERR, + "pipeline_tracer: could not open '" + output_path + "', falling back to stderr\n" + content); + return; + } + f << content; +} + +void PipelineTracer::PrintGraph(const vector> &pipelines, const string &output_path) { string out = "\n=== Pipeline Graph ===\n"; for (auto &p : pipelines) { out += "Pipeline #" + to_string(p->pipeline_id) + ": " + Describe(*p) + "\n"; @@ -42,10 +59,11 @@ void PipelineTracer::PrintGraph(const vector> &pipelines) { } } out += "======================\n"; - Printer::Print(OutputStream::STREAM_STDERR, out); + WriteOutput(out, output_path); } -void PipelineTracer::PrintChromeTrace(const vector> &pipelines, int64_t query_start_ns) { +void PipelineTracer::PrintChromeTrace(const vector> &pipelines, int64_t query_start_ns, + const string &output_path) { string events; bool first = true; for (auto &p : pipelines) { @@ -68,8 +86,8 @@ void PipelineTracer::PrintChromeTrace(const vector> &pipeli events += "{\"name\":\"" + name + "\",\"ph\":\"X\",\"pid\":0,\"tid\":" + to_string(p->pipeline_id) + ",\"ts\":" + to_string(start_us) + ",\"dur\":" + to_string(dur_us) + "}"; } - string json = "\n{\"traceEvents\":[\n " + events + "\n]}\n"; - Printer::Print(OutputStream::STREAM_STDERR, json); + string json = "{\"traceEvents\":[\n " + events + "\n]}\n"; + WriteOutput(json, output_path); } } // namespace duckdb From fbb8a416564f7d9fdf49a0ec8b6c72ed92c33383 Mon Sep 17 00:00:00 2001 From: "bingtao.yin" Date: Sat, 2 May 2026 18:18:12 +0800 Subject: [PATCH 4/5] Address review feedback: codegen registration, atomic timestamps, append/trunc, Replace API - src/main/config.cpp: register 3 new settings in internal_options[] (DUCKDB_LOCAL) and adjust setting_aliases[] indexes for entries shifted by the inserts. Equivalent to running scripts/generate_settings.py; done by hand to keep the diff minimal. - src/include/duckdb/parallel/pipeline.hpp: make start_time_ns/end_time_ns atomic; MarkStart/MarkEnd run on different threads than PipelineTracer::PrintChromeTrace. - src/parallel/pipeline_tracer.cpp: - WriteOutput takes an `append` flag; PrintGraph appends, PrintChromeTrace truncates (Chrome Trace JSON must remain a single valid object). - StringUtil::ReplaceAll -> StringUtil::Replace (the in-place ReplaceAll does not exist; Replace is the global, returning variant). - src/include/duckdb/parallel/pipeline_tracer.hpp: document the append flag. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/include/duckdb/parallel/pipeline.hpp | 8 +++++--- src/include/duckdb/parallel/pipeline_tracer.hpp | 5 ++++- src/main/config.cpp | 11 +++++++---- src/parallel/pipeline_tracer.cpp | 17 +++++++++++------ 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/include/duckdb/parallel/pipeline.hpp b/src/include/duckdb/parallel/pipeline.hpp index ef2758811a6f..e2396dbf0d83 100644 --- a/src/include/duckdb/parallel/pipeline.hpp +++ b/src/include/duckdb/parallel/pipeline.hpp @@ -151,9 +151,11 @@ class Pipeline : public enable_shared_from_this { //! Pipeline ID assigned by PipelineTracer::AssignIds (0-based) idx_t pipeline_id = 0; - //! Wall-clock start/end times in steady_clock nanoseconds since epoch (-1 = unset) - int64_t start_time_ns = -1; - int64_t end_time_ns = -1; + //! Wall-clock start/end times in steady_clock nanoseconds since epoch (-1 = unset). + //! Atomic because MarkStart/MarkEnd may run on different worker threads than the + //! reader (PipelineTracer::PrintChromeTrace). + atomic start_time_ns {-1}; + atomic end_time_ns {-1}; //! The chain of intermediate operators vector> operators; //! The sink (i.e. destination) for data; this is e.g. a hash table to-be-built diff --git a/src/include/duckdb/parallel/pipeline_tracer.hpp b/src/include/duckdb/parallel/pipeline_tracer.hpp index c3dd0837b8f6..96197cdeb01e 100644 --- a/src/include/duckdb/parallel/pipeline_tracer.hpp +++ b/src/include/duckdb/parallel/pipeline_tracer.hpp @@ -41,7 +41,10 @@ class PipelineTracer { static string Describe(const Pipeline &pipeline); //! Write content to output_path if non-empty, otherwise to stderr. - static void WriteOutput(const string &content, const string &output_path); + //! append=true opens the file in append mode (used for the graph, which accumulates + //! across queries); append=false truncates (used for the Chrome Trace JSON, which + //! must remain a single valid JSON object per file). + static void WriteOutput(const string &content, const string &output_path, bool append); }; } // namespace duckdb diff --git a/src/main/config.cpp b/src/main/config.cpp index f5f56660bf65..b2e921cb2f0e 100644 --- a/src/main/config.cpp +++ b/src/main/config.cpp @@ -128,6 +128,7 @@ static const ConfigurationOption internal_options[] = { DUCKDB_GLOBAL(EnableLogging), DUCKDB_SETTING(EnableMacroDependenciesSetting), DUCKDB_SETTING(EnableObjectCacheSetting), + DUCKDB_LOCAL(EnablePipelineTraceSetting), DUCKDB_LOCAL(EnableProfilingSetting), DUCKDB_LOCAL(EnableProgressBarSetting), DUCKDB_LOCAL(EnableProgressBarPrintSetting), @@ -177,6 +178,8 @@ static const ConfigurationOption internal_options[] = { DUCKDB_SETTING(PasswordSetting), DUCKDB_SETTING_CALLBACK(PerfectHtThresholdSetting), DUCKDB_SETTING_CALLBACK(PinThreadsSetting), + DUCKDB_LOCAL(PipelineGraphOutputSetting), + DUCKDB_LOCAL(PipelineTraceOutputSetting), DUCKDB_SETTING(PivotFilterThresholdSetting), DUCKDB_SETTING(PivotLimitSetting), DUCKDB_SETTING(PreferRangeJoinsSetting), @@ -208,12 +211,12 @@ static const ConfigurationOption internal_options[] = { DUCKDB_SETTING(ZstdMinStringLengthSetting), FINAL_SETTING}; -static const ConfigurationAlias setting_aliases[] = {DUCKDB_SETTING_ALIAS("memory_limit", 99), +static const ConfigurationAlias setting_aliases[] = {DUCKDB_SETTING_ALIAS("memory_limit", 100), DUCKDB_SETTING_ALIAS("null_order", 43), - DUCKDB_SETTING_ALIAS("profiling_output", 118), - DUCKDB_SETTING_ALIAS("user", 133), + DUCKDB_SETTING_ALIAS("profiling_output", 121), + DUCKDB_SETTING_ALIAS("user", 136), DUCKDB_SETTING_ALIAS("wal_autocheckpoint", 25), - DUCKDB_SETTING_ALIAS("worker_threads", 132), + DUCKDB_SETTING_ALIAS("worker_threads", 135), FINAL_ALIAS}; vector DBConfig::GetOptions() { diff --git a/src/parallel/pipeline_tracer.cpp b/src/parallel/pipeline_tracer.cpp index 2fe813875cf5..51471eebb32b 100644 --- a/src/parallel/pipeline_tracer.cpp +++ b/src/parallel/pipeline_tracer.cpp @@ -27,12 +27,13 @@ string PipelineTracer::Describe(const Pipeline &pipeline) { return result; } -void PipelineTracer::WriteOutput(const string &content, const string &output_path) { +void PipelineTracer::WriteOutput(const string &content, const string &output_path, bool append) { if (output_path.empty()) { Printer::Print(OutputStream::STREAM_STDERR, content); return; } - std::ofstream f(output_path, std::ios::out | std::ios::trunc); + auto mode = std::ios::out | (append ? std::ios::app : std::ios::trunc); + std::ofstream f(output_path, mode); if (!f.is_open()) { // Fall back to stderr if the file cannot be opened Printer::Print(OutputStream::STREAM_STDERR, @@ -59,7 +60,9 @@ void PipelineTracer::PrintGraph(const vector> &pipelines, c } } out += "======================\n"; - WriteOutput(out, output_path); + // Append: the graph is plain text and the per-query "=== Pipeline Graph ===" header + // already separates entries, so multiple queries in the same session accumulate cleanly. + WriteOutput(out, output_path, /*append=*/true); } void PipelineTracer::PrintChromeTrace(const vector> &pipelines, int64_t query_start_ns, @@ -81,13 +84,15 @@ void PipelineTracer::PrintChromeTrace(const vector> &pipeli first = false; } string name = "#" + to_string(p->pipeline_id) + ": " + Describe(*p); - // Escape quotes in name for JSON safety - StringUtil::ReplaceAll(name, "\"", "\\\""); + // Escape quotes in name for JSON safety (StringUtil::Replace is global, returns new string) + name = StringUtil::Replace(name, "\"", "\\\""); events += "{\"name\":\"" + name + "\",\"ph\":\"X\",\"pid\":0,\"tid\":" + to_string(p->pipeline_id) + ",\"ts\":" + to_string(start_us) + ",\"dur\":" + to_string(dur_us) + "}"; } string json = "{\"traceEvents\":[\n " + events + "\n]}\n"; - WriteOutput(json, output_path); + // Truncate: Chrome Trace JSON must be a single valid JSON object; concatenating + // per-query traces would produce invalid JSON. Each query overwrites the file. + WriteOutput(json, output_path, /*append=*/false); } } // namespace duckdb From 537dc6e433e5fad13a306c270bba3ce6437a2742 Mon Sep 17 00:00:00 2001 From: "bingtao.yin" Date: Sat, 2 May 2026 20:46:35 +0800 Subject: [PATCH 5/5] Per-PipelineTask trace events: emit one event per parallel task Previously the tracer recorded a single (start, end) pair per Pipeline, which collapsed all parallel PipelineTasks (created by LaunchScanTasks according to MaxThreads()) into one logical event. Real parallelism and per-worker timing were invisible. Now each PipelineTask records its own lifetime: - task_start_ns and task_thread_hash are captured on the first ExecuteTask() entry (handles PROCESS_PARTIAL yielding by checking task_start_ns < 0 sentinel, so re-entry doesn't overwrite the start); - on TASK_FINISHED return, the task appends a TaskTiming record to Pipeline::task_timings under task_timings_lock. PrintChromeTrace iterates task_timings and emits one Chrome Trace event per task, with tid = thread_hash so each worker thread occupies its own row in Perfetto, making real parallelism and load distribution directly visible. Pipeline-level MarkStart/MarkEnd and the start_time_ns/end_time_ns atomics are removed; PipelineEvent::Schedule/FinishEvent and PipelineFinishEvent::FinishEvent no longer touch timing. The graph output (PrintGraph) is structural-only and is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/include/duckdb/parallel/pipeline.hpp | 39 +++++++++++++--------- src/parallel/pipeline.cpp | 23 ++++++++----- src/parallel/pipeline_event.cpp | 2 -- src/parallel/pipeline_finish_event.cpp | 1 - src/parallel/pipeline_tracer.cpp | 41 ++++++++++++++---------- 5 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/include/duckdb/parallel/pipeline.hpp b/src/include/duckdb/parallel/pipeline.hpp index e2396dbf0d83..d4a9218410fe 100644 --- a/src/include/duckdb/parallel/pipeline.hpp +++ b/src/include/duckdb/parallel/pipeline.hpp @@ -43,6 +43,15 @@ class PipelineTask : public ExecutorTask { public: TaskExecutionResult ExecuteTask(TaskExecutionMode mode) override; + +private: + //! Wall-clock start time of this task's first ExecuteTask invocation (-1 = not yet started). + //! Captured once even if the task yields and is rescheduled, so the recorded interval covers + //! the whole task lifetime including any blocked periods between Execute calls. + int64_t task_start_ns = -1; + //! std::hash of this->thread::id at the time of first ExecuteTask. Used as Chrome Trace tid + //! so each worker thread gets its own row in Perfetto. + uint64_t task_thread_hash = 0; }; class PipelineBuildState { @@ -101,20 +110,21 @@ class Pipeline : public enable_shared_from_this { void Print() const; void PrintDependencies() const; - //! Record the wall-clock start time of this pipeline's execution. - void MarkStart(); - //! Record the wall-clock end time (last-call wins, so PipelineFinishEvent overwrites PipelineEvent). - void MarkEnd(); + //! Per-PipelineTask timing record. One entry per parallel task that ran this pipeline, + //! capturing first-Execute-entry → TASK_FINISHED return wall-clock interval and the + //! worker thread's hash. + struct TaskTiming { + int64_t start_ns; + int64_t end_ns; + uint64_t thread_hash; + }; + + //! Append a task timing record. Thread-safe; called by PipelineTask on completion. + void RecordTaskTiming(int64_t start_ns, int64_t end_ns, uint64_t thread_hash); idx_t GetPipelineId() const { return pipeline_id; } - int64_t GetStartTimeNs() const { - return start_time_ns; - } - int64_t GetEndTimeNs() const { - return end_time_ns; - } //! Returns query progress bool GetProgress(ProgressData &progress_data); @@ -151,11 +161,10 @@ class Pipeline : public enable_shared_from_this { //! Pipeline ID assigned by PipelineTracer::AssignIds (0-based) idx_t pipeline_id = 0; - //! Wall-clock start/end times in steady_clock nanoseconds since epoch (-1 = unset). - //! Atomic because MarkStart/MarkEnd may run on different worker threads than the - //! reader (PipelineTracer::PrintChromeTrace). - atomic start_time_ns {-1}; - atomic end_time_ns {-1}; + //! Per-task timing records. Appended to from worker threads via RecordTaskTiming under + //! task_timings_lock; read once at query end by PipelineTracer::PrintChromeTrace. + mutable mutex task_timings_lock; + vector task_timings; //! The chain of intermediate operators vector> operators; //! The sink (i.e. destination) for data; this is e.g. a hash table to-be-built diff --git a/src/parallel/pipeline.cpp b/src/parallel/pipeline.cpp index c029019e2a02..8a94d2d517b0 100644 --- a/src/parallel/pipeline.cpp +++ b/src/parallel/pipeline.cpp @@ -1,5 +1,7 @@ #include "duckdb/parallel/pipeline.hpp" +#include + #include "duckdb/common/algorithm.hpp" #include "duckdb/common/chrono.hpp" #include "duckdb/common/printer.hpp" @@ -31,7 +33,16 @@ const PipelineExecutor &PipelineTask::GetPipelineExecutor() const { return *pipeline_executor; } +static int64_t SteadyNowNs() { + return static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()); +} + TaskExecutionResult PipelineTask::ExecuteTask(TaskExecutionMode mode) { + if (task_start_ns < 0) { + task_start_ns = SteadyNowNs(); + task_thread_hash = std::hash {}(std::this_thread::get_id()); + } + if (!pipeline_executor) { pipeline_executor = make_uniq(pipeline.GetClientContext(), pipeline); } @@ -61,6 +72,7 @@ TaskExecutionResult PipelineTask::ExecuteTask(TaskExecutionMode mode) { } } + pipeline.RecordTaskTiming(task_start_ns, SteadyNowNs(), task_thread_hash); event->FinishTask(); pipeline_executor.reset(); return TaskExecutionResult::TASK_FINISHED; @@ -74,14 +86,9 @@ ClientContext &Pipeline::GetClientContext() { return executor.context; } -void Pipeline::MarkStart() { - start_time_ns = static_cast( - duration_cast(steady_clock::now().time_since_epoch()).count()); -} - -void Pipeline::MarkEnd() { - end_time_ns = static_cast( - duration_cast(steady_clock::now().time_since_epoch()).count()); +void Pipeline::RecordTaskTiming(int64_t start_ns, int64_t end_ns, uint64_t thread_hash) { + lock_guard lock(task_timings_lock); + task_timings.push_back({start_ns, end_ns, thread_hash}); } bool Pipeline::GetProgress(ProgressData &progress) { diff --git a/src/parallel/pipeline_event.cpp b/src/parallel/pipeline_event.cpp index 38b57d8caecd..a7f07e2d9cc3 100644 --- a/src/parallel/pipeline_event.cpp +++ b/src/parallel/pipeline_event.cpp @@ -9,7 +9,6 @@ PipelineEvent::PipelineEvent(shared_ptr pipeline_p) : BasePipelineEven void PipelineEvent::Schedule() { auto event = shared_from_this(); auto &executor = pipeline->executor; - pipeline->MarkStart(); try { pipeline->Schedule(event); D_ASSERT(total_tasks > 0); @@ -21,7 +20,6 @@ void PipelineEvent::Schedule() { } void PipelineEvent::FinishEvent() { - pipeline->MarkEnd(); } } // namespace duckdb diff --git a/src/parallel/pipeline_finish_event.cpp b/src/parallel/pipeline_finish_event.cpp index 196329f1c571..bf57cda256c4 100644 --- a/src/parallel/pipeline_finish_event.cpp +++ b/src/parallel/pipeline_finish_event.cpp @@ -84,7 +84,6 @@ void PipelineFinishEvent::Schedule() { } void PipelineFinishEvent::FinishEvent() { - pipeline->MarkEnd(); } } // namespace duckdb diff --git a/src/parallel/pipeline_tracer.cpp b/src/parallel/pipeline_tracer.cpp index 51471eebb32b..3a0d206515c7 100644 --- a/src/parallel/pipeline_tracer.cpp +++ b/src/parallel/pipeline_tracer.cpp @@ -70,24 +70,31 @@ void PipelineTracer::PrintChromeTrace(const vector> &pipeli string events; bool first = true; for (auto &p : pipelines) { - if (p->start_time_ns < 0 || p->end_time_ns < 0) { - continue; - } - int64_t start_us = (p->start_time_ns - query_start_ns) / 1000; - int64_t dur_us = (p->end_time_ns - p->start_time_ns) / 1000; - if (dur_us < 0) { - dur_us = 0; - } - if (!first) { - events += ",\n "; - } else { - first = false; + string desc = Describe(*p); + // Escape quotes for JSON safety; do it once per pipeline, not per task. + desc = StringUtil::Replace(desc, "\"", "\\\""); + // One Chrome Trace event per PipelineTask: each parallel slice of the pipeline gets + // its own bar. tid = thread hash so each worker thread occupies its own row in + // Perfetto, making real parallelism visible. + idx_t task_idx = 0; + for (auto &t : p->task_timings) { + int64_t start_us = (t.start_ns - query_start_ns) / 1000; + int64_t dur_us = (t.end_ns - t.start_ns) / 1000; + if (dur_us < 0) { + dur_us = 0; + } + // Mask to a 31-bit non-negative value for tools that treat tid as signed int32. + uint64_t tid = t.thread_hash & 0x7fffffffULL; + if (!first) { + events += ",\n "; + } else { + first = false; + } + events += "{\"name\":\"#" + to_string(p->pipeline_id) + "[" + to_string(task_idx) + "]: " + desc + + "\",\"ph\":\"X\",\"pid\":0,\"tid\":" + to_string(tid) + + ",\"ts\":" + to_string(start_us) + ",\"dur\":" + to_string(dur_us) + "}"; + task_idx++; } - string name = "#" + to_string(p->pipeline_id) + ": " + Describe(*p); - // Escape quotes in name for JSON safety (StringUtil::Replace is global, returns new string) - name = StringUtil::Replace(name, "\"", "\\\""); - events += "{\"name\":\"" + name + "\",\"ph\":\"X\",\"pid\":0,\"tid\":" + to_string(p->pipeline_id) + - ",\"ts\":" + to_string(start_us) + ",\"dur\":" + to_string(dur_us) + "}"; } string json = "{\"traceEvents\":[\n " + events + "\n]}\n"; // Truncate: Chrome Trace JSON must be a single valid JSON object; concatenating