From cdb27ee9a28aabc281fa8ea878370f3af11f5dea Mon Sep 17 00:00:00 2001 From: Azat Khuzhin Date: Thu, 19 Feb 2026 16:48:05 +0100 Subject: [PATCH] Add system.jemalloc_profile integration --- src/interpreter/clickhouse.rs | 29 +++++++++++++++++++++++++++++ src/interpreter/worker.rs | 17 +++++++++++++++++ src/view/navigation.rs | 10 ++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/interpreter/clickhouse.rs b/src/interpreter/clickhouse.rs index ab9a780..f5de1db 100644 --- a/src/interpreter/clickhouse.rs +++ b/src/interpreter/clickhouse.rs @@ -1038,6 +1038,35 @@ impl ClickHouse { .await; } + /// Return jemalloc flamegraph in pyspy format. + /// It is the same format as TSV, but with ' ' delimiter between symbols and weight. + pub async fn get_jemalloc_flamegraph(&self, selected_host: Option<&String>) -> Result { + let dbtable = self.get_table_name("system", "jemalloc_profile"); + let host_filter = if let Some(host) = selected_host { + if !host.is_empty() && self.options.cluster.is_some() { + format!("AND hostName() = '{}'", host.replace('\'', "''")) + } else { + String::new() + } + } else { + String::new() + }; + return self + .execute(&format!( + r#" + WITH splitByChar(' ', line) AS parts + SELECT + arrayStringConcat(arraySlice(parts, 1, -1), ' ') AS symbols, + parts[-1]::UInt64 AS bytes + FROM {} + WHERE 1 {} + SETTINGS jemalloc_profile_output_format='collapsed' + "#, + dbtable, host_filter, + )) + .await; + } + pub async fn get_live_query_flamegraph( &self, query_ids: &Option>, diff --git a/src/interpreter/worker.rs b/src/interpreter/worker.rs index d7d603d..1fb9840 100644 --- a/src/interpreter/worker.rs +++ b/src/interpreter/worker.rs @@ -33,6 +33,8 @@ pub enum Event { TextLog(&'static str, TextLogArguments), // [bool (true - show in TUI, false - share via pastila), type, start, end] ServerFlameGraph(bool, TraceType, DateTime, DateTime), + // [bool (true - show in TUI, false - share via pastila)] + JemallocFlameGraph(bool), // (type, bool (true - show in TUI, false - open in browser), start time, end time, [query_ids]) QueryFlameGraph( TraceType, @@ -86,6 +88,7 @@ impl Event { Event::LastQueryLog(..) => "LastQueryLog".to_string(), Event::TextLog(..) => "TextLog".to_string(), Event::ServerFlameGraph(..) => "ServerFlameGraph".to_string(), + Event::JemallocFlameGraph(..) => "JemallocFlameGraph".to_string(), Event::QueryFlameGraph(..) => "QueryFlameGraph".to_string(), Event::LiveQueryFlameGraph(..) => "LiveQueryFlameGraph".to_string(), Event::Summary => "Summary".to_string(), @@ -428,6 +431,20 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) .await?; *need_clear = true; } + Event::JemallocFlameGraph(tui) => { + let flamegraph_block = clickhouse + .get_jemalloc_flamegraph(selected_host.as_ref()) + .await?; + render_or_share_flamegraph( + tui, + cb_sink, + flamegraph_block, + pastila_clickhouse_host, + pastila_url, + ) + .await?; + *need_clear = true; + } Event::QueryFlameGraph(trace_type, tui, start, end, query_ids) => { let flamegraph_block = clickhouse .get_flamegraph( diff --git a/src/view/navigation.rs b/src/view/navigation.rs index cedf017..a066829 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -51,6 +51,7 @@ pub trait Navigation { fn show_actions(&mut self); fn show_fuzzy_actions(&mut self); fn show_server_flamegraph(&mut self, tui: bool, trace_type: Option); + fn show_jemalloc_flamegraph(&mut self, tui: bool); fn show_host_filter_dialog(&mut self); fn drop_main_view(&mut self); @@ -270,6 +271,8 @@ impl Navigation for Cursive { context.add_global_action_without_shortcut(self, "Share Server MemoryAllocatedWithoutCheck Flamegraph", |siv| siv.show_server_flamegraph(false, Some(TraceType::MemoryAllocatedWithoutCheck))); context.add_global_action_without_shortcut(self, "Share Server Events Flamegraph", |siv| siv.show_server_flamegraph(false, Some(TraceType::ProfileEvent))); context.add_global_action_without_shortcut(self, "Share Server Live Flamegraph", |siv| siv.show_server_flamegraph(false, None)); + context.add_global_action_without_shortcut(self, "Jemalloc", |siv| siv.show_jemalloc_flamegraph(true)); + context.add_global_action_without_shortcut(self, "Share Jemalloc", |siv| siv.show_jemalloc_flamegraph(false)); // If logging is done to file, console is always empty if context.options.service.log.is_none() { @@ -587,6 +590,13 @@ impl Navigation for Cursive { } } + fn show_jemalloc_flamegraph(&mut self, tui: bool) { + let mut context = self.user_data::().unwrap().lock().unwrap(); + context + .worker + .send(true, WorkerEvent::JemallocFlameGraph(tui)); + } + fn show_host_filter_dialog(&mut self) { let context_arc = self.user_data::().unwrap().clone(); let context = context_arc.lock().unwrap();