Skip to content

Conversation

@UnamedRus
Copy link

@UnamedRus UnamedRus commented Aug 3, 2025

Perfetto is an open-source suite of SDKs, daemons and tools which use tracing to help developers understand the behaviour of the complex systems and root-cause functional and performance issues on client / embedded systems.

It was suggested by good people, that it's one of very few decent trace viewers.
https://ui.perfetto.dev/

2 commands added:

  1. Generate Perfetto .pb file
  2. Open in Perfetto, (start http server which serve simple page and trace.pb file, which implement deeplinking to ui.perfetto.dev)

Doesn't work

  1. Seems, events are sync? so, no other events are running during trace generation. (And it's takes a while)
  2. Big traces (>200MB-400MB) better to be processed by TraceProcessor server, not using WASM in ui.perfetto.dev https://perfetto.dev/docs/visualization/large-traces

Problems of generated trace/Perfetto:

  1. Cant filter counters by categories
  2. Particular remote processor event doesn't know from which node it's consumed data opentelemetry_span_log propagate original trace_id (and initial_query_id) for Distributed queries ClickHouse/ClickHouse#77375 opentelemetry_span_log connect to processors_profile_log ClickHouse/ClickHouse#77395
  3. clock_sync_failure during import of stack_traces Clock synchronization column in system.query_log ClickHouse/ClickHouse#78234
  4. No memory tracing
  5. No flows out of system.processors_profile_log, but flows are not gonna work at amount of spans if we use opentelemetry_trace_processors=1
  6. Use of threads vs processors for query threads.
  7. Better deduplication for internedData

@azat
Copy link
Owner

azat commented Aug 4, 2025

@UnamedRus This looks very promising! Please ping me once you will finish and I will start review

Couple of thought son the current draft after brief look:

  • It is OK for now to execute the query again, but I am not sure that I like this (since any other actions do not do this, but this is another story, since it requires lots of info), let's at least underline this in the actions name
  • I see the reason for having separate action to store the profile data on disk, let's keep it for now, but not sure that this is a way to go
  • you may rely on symbols/lines as we do for system.trace_log over using trace
  • Can we render stacktraces for ProfileEvents changes in UI?
  • I guess we will also need to tune the UI to make it even better!

Seems, events are sync? so, no other events are running during trace generation. (And it's takes a while)

Right now it is true

@UnamedRus
Copy link
Author

pull_request / Spell Check with Typos (pull_request)Failing after 8s

typo in proto definition.

But, it's kinda should work now.
Trace size still an issue, as traces over 500MB doesn't work in browser and they need to be processed using extra tool - trace_processor which run as server docs and UI connects to it.

Can we render stacktraces for ProfileEvents changes in UI?

Does it have much value?
StreamingStackTraces belongs to particular track or thread_id even, and i didn't figure out nice way to show multiple types of them, like Real/CPU yet per thread.

@azat
Copy link
Owner

azat commented Sep 1, 2025

typo in proto definition.

Let's add them into ignore list

Does it have much value?

It depends on the happened events, I guess it can be useful, but I am not sure, I need to play with it.
One it will be ready form your side ping me and I will start looking into it.

@azat azat requested a review from Copilot November 30, 2025 14:40
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Perfetto trace generation and visualization capabilities to chdig, enabling users to analyze ClickHouse query performance using the Perfetto UI. The implementation includes generating protobuf-formatted traces from ClickHouse system logs and serving them via a local HTTP server with deep linking to the Perfetto web interface.

Key Changes:

  • Added Perfetto trace generation from ClickHouse profiling data including CPU sampling, memory allocation, processor events, and system metrics
  • Implemented local HTTP server to serve trace files with automatic browser opening
  • Added two new commands: generate trace to .pb file and open trace in browser

Reviewed changes

Copilot reviewed 11 out of 15 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/view/processes_view.rs Added two new view actions for Perfetto trace generation and browser viewing
src/utils.rs Implemented HTTP server for serving Perfetto traces with deep linking support
src/lib.rs Added generated module import for protobuf types
src/interpreter/worker.rs Added event handlers for GeneratePerfettoTrace and OpenPerfettoTrace
src/interpreter/options.rs Changed private fields to public for service options
src/interpreter/mod.rs Added clickhouse_perfetto module
src/interpreter/clickhouse_perfetto.rs Core implementation of Perfetto trace generation with interned data management
src/interpreter/clickhouse.rs Added helper functions and made execute_simple public
src/bin.rs Whitespace change only
build.rs Added build script for protobuf compilation
Cargo.toml Added prost, base64, and tonic-build dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

};

cmd.stderr(Stdio::null()).stdout(Stdio::null());
cmd.stdout(Stdio::null());
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed stderr suppression from open_url_command, but this could cause error messages to appear in the terminal when opening URLs. This change appears unrelated to the PR's main purpose and may have been made by mistake. Consider restoring cmd.stderr(Stdio::null()) to maintain consistent behavior with the previous implementation.

Suggested change
cmd.stdout(Stdio::null());
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());

Copilot uses AI. Check for mistakes.


let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let addr = listener.local_addr().unwrap(); // <-- actual assigned port here
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using unwrap() on TcpListener::bind() will panic if port binding fails. Since this is in a spawned thread, the panic won't be caught gracefully and the user won't see a helpful error message. Consider using proper error handling with ? operator and returning a Result, or at minimum use expect() with a descriptive message like expect(\"Failed to bind to local address for Perfetto server\").

Suggested change
let addr = listener.local_addr().unwrap(); // <-- actual assigned port here
let addr = listener.local_addr().expect("Failed to get local address for Perfetto server"); // <-- actual assigned port here

Copilot uses AI. Check for mistakes.
let addr = listener.local_addr().unwrap(); // <-- actual assigned port here

// Use dynamic port allocation
let server = warp::serve(routes).incoming(listener);
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incoming() method expects a stream that implements TryStream<Ok = impl Into<AddrStream>>, but TcpListener from tokio doesn't directly satisfy this. This code will likely fail to compile. Use warp::serve(routes).bind_with_graceful_shutdown() or convert the tokio listener appropriately using tokio_stream::wrappers::TcpListenerStream.

Copilot uses AI. Check for mistakes.
Comment on lines +432 to +434
_ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => {
log::info!("Perfetto HTTP server shutting down after 5 minutes");
}
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 5-minute (300 seconds) timeout is a magic number with no explanation. Consider extracting this as a named constant at the module or function level (e.g., const PERFETTO_SERVER_TIMEOUT_SECS: u64 = 300;) and documenting why this specific duration was chosen.

Copilot uses AI. Check for mistakes.
let timestamp_start_ns = block.get::<i64, _>(row, "timestamp_start_ns")?;

// Extract actual data from ClickHouse
let mut timestamp_delta_us = block.get::<Vec<i64>, _>(row, "timestamp_delta_us")?;
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying timestamp_delta_us[0] after retrieving it from the database assumes the array has at least one element. If the array is empty, this will panic. Add a bounds check or handle the empty case explicitly before accessing index 0.

Suggested change
let mut timestamp_delta_us = block.get::<Vec<i64>, _>(row, "timestamp_delta_us")?;
let mut timestamp_delta_us = block.get::<Vec<i64>, _>(row, "timestamp_delta_us")?;
if timestamp_delta_us.is_empty() {
anyhow::bail!("timestamp_delta_us vector is empty for row {}", row);
}

Copilot uses AI. Check for mistakes.
Comment on lines +455 to +461
source_locations: source_locations,
log_message_body: log_message_body,
callstacks: callstacks,
frames: frames,
function_names: function_names,
event_categories: event_categories,
event_names: event_names,
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Rust, when a struct field and variable have the same name, you can use field init shorthand. Replace these lines with just source_locations,, log_message_body,, callstacks,, frames,, function_names,, event_categories,, and event_names, without the repetition.

Suggested change
source_locations: source_locations,
log_message_body: log_message_body,
callstacks: callstacks,
frames: frames,
function_names: function_names,
event_categories: event_categories,
event_names: event_names,
source_locations,
log_message_body,
callstacks,
frames,
function_names,
event_categories,
event_names,

Copilot uses AI. Check for mistakes.
Comment on lines +713 to +716
let clock_monotonic_coarse_timestamp = block.get::<Vec<u64>, _>(row, "clock_monotonic_coarse_timestamp")?;
let address = block.get::<Vec<u64>, _>(row, "address")?;
let size = block.get::<Vec<u64>, _>(row, "size_arr")?;
let sequence_number = block.get::<Vec<u64>, _>(row, "sequence_number")?;
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Inconsistent spacing: some variables have extra spaces after the equals sign (lines 713, 714, 716) while size on line 715 has normal spacing. Standardize the spacing for consistency, preferably using single spaces as is conventional in Rust.

Suggested change
let clock_monotonic_coarse_timestamp = block.get::<Vec<u64>, _>(row, "clock_monotonic_coarse_timestamp")?;
let address = block.get::<Vec<u64>, _>(row, "address")?;
let size = block.get::<Vec<u64>, _>(row, "size_arr")?;
let sequence_number = block.get::<Vec<u64>, _>(row, "sequence_number")?;
let clock_monotonic_coarse_timestamp = block.get::<Vec<u64>, _>(row, "clock_monotonic_coarse_timestamp")?;
let address = block.get::<Vec<u64>, _>(row, "address")?;
let size = block.get::<Vec<u64>, _>(row, "size_arr")?;
let sequence_number = block.get::<Vec<u64>, _>(row, "sequence_number")?;

Copilot uses AI. Check for mistakes.
let io_wait_ns = block.get::<f64, _>(row, "io_wait_ns").unwrap_or(0.0) as u64;
let irq_ns = block.get::<f64, _>(row, "irq_ns").unwrap_or(0.0) as u64;
let softirq_ns: u64 = block.get::<f64, _>(row, "softirq_ns").unwrap_or(0.0) as u64;
let steal_ns: u64 = block.get::<f64, _>(row, "steal_ns").unwrap_or(0.0) as u64;
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Type annotations are redundant here since the type is already explicitly cast with as u64 on the right-hand side. Remove : u64 type annotations for cleaner code, as they don't add value when the type is immediately coerced.

Suggested change
let steal_ns: u64 = block.get::<f64, _>(row, "steal_ns").unwrap_or(0.0) as u64;
let steal_ns = block.get::<f64, _>(row, "steal_ns").unwrap_or(0.0) as u64;

Copilot uses AI. Check for mistakes.
&self,
database: &str,
query: &str,
_output: &str,
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _output parameter is prefixed with underscore indicating it's intentionally unused, but it's included in the function signature. Since this parameter is not used in the function body, consider removing it entirely or document why it's kept for future use.

Suggested change
_output: &str,

Copilot uses AI. Check for mistakes.
urlencoding = { version = "*", default-features = false }
percent-encoding = { version = "*", default-features = false }
warp = { version = "*", default-features = false, features = ["server"] }
base64 = { version = "*", default-features = false}
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base64 dependency is added but never imported or used in any of the changed files. Remove this unused dependency from Cargo.toml to avoid unnecessary bloat.

Suggested change
base64 = { version = "*", default-features = false}

Copilot uses AI. Check for mistakes.
@azat
Copy link
Owner

azat commented Dec 21, 2025

@UnamedRus are you going to finish this? Or should I pick it?

I want to reimplement it w/o using protobuf, to simplify code, since shipping 50K lines just for format - does not worth it

@UnamedRus
Copy link
Author

Which format you are intended to use?

@azat
Copy link
Owner

azat commented Dec 22, 2025

Starts with regular JSON, if it will has too much overhead - something more optimal. Even ProtoBuf can be OK, but w/o shipping these 50K lines (*.rs can be compiled I guess, as for *.proto not sure need to look)

@UnamedRus
Copy link
Author

Starts with regular JSON, if it will has too much overhead - something more optimal.

Well, even optimized protobuf easily explode over hundreds of MBs, so my last attempts were to optimize it even further.
Problem is that processors tracing happen on blocks, and if your query is somewhat big (read billions of rows), it create quite a few events. Also, AFAIK support for json format was added in CH (Chrome trace format) itself, but amount of event types you can share in that way is limited.

Even ProtoBuf can be OK, but w/o shipping these 50K lines (*.rs can be compiled I guess, as for *.proto not sure need to look)

I think, it's just possible to cut it and remove fields which we are not going to use, it should drastically reduce protobuf size.

@azat
Copy link
Owner

azat commented Dec 22, 2025

I also had an idea to run custom server that serves SQL queries for perfetto, that way we can query ClickHouse directly, but maybe perfertto will query all at once, and then, it does not make any sense, though you can still defer at least some stuff from the browser AFAIU (do not use builtin webassembly to convert data from one format to another)

But this is the next steps, for now I think need to focus on basics

UPD: you already mentioned it in the PR description

@azat
Copy link
Owner

azat commented Dec 22, 2025

So @UnamedRus you are going to continue working on this?

@azat
Copy link
Owner

azat commented Dec 22, 2025

About *.proto, it is OK to bundle it as part of chdig, but we need to exclude 100%-kernel only stuff, i.e. ftrace, generic_kernel, and lots of other things, and provide a script to generate .trace file for chdig as well for periodic updates (if any) and for transparency

@UnamedRus
Copy link
Author

I also had an idea to run custom server that serves SQL queries for perfetto, that way we can query ClickHouse directly, but maybe perfetto will query all at once, and then, it does not make any sense, though you can still defer at least some stuff from the

Perfetto store data in it's own (columnar it seems?) in memory heavily normalized dbms, with even more strange layout/data optimizations. So, i don't think it would be simple to hijack queries and redirect to CH.

BTW, there is fork of perfetto by jane-street https://github.com/janestreet/magic-trace didn't looked much at it.

My last attempt was to optimize/reduce trace size, as on semi production (and heavy) queries i tested, trace file became very big.
For that you need to use some quirky features of Perfetto proto format, but i ends up stuck with trying to fix issues with clocks.
Or, it's possible to remove "features" (which is less desirable) or may be adjust max_block_size for bigger value.

So @UnamedRus you are going to continue working on this?

My ideal scenario with that PR/task was to explore/define set of rules, ie how to export data from CH tables & convert it to what perfetto expect.
So, there are basically few topics, which i may/willing to spend more time on:

  1. Export/map new data/events.
  2. Optimize trace files size
  3. Improve trace files for better UI/UX experience in Perfetto
  4. Distributed tracing?

None of them is actually about making it into prod state. Still, i'm unlikely to have time to work on those points until end of Jan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants