Skip to content

Commit 7c38837

Browse files
authored
Merge pull request #87 from zz85/group_by_pids
Group by process/PIDs + TUI support
2 parents 3fcfaa0 + 3912417 commit 7c38837

9 files changed

Lines changed: 552 additions & 13 deletions

File tree

profile-bee-tui/src/app.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::state::{FlameGraphState, UpdateMode};
44
use crate::view::FlameGraphView;
55
use std::collections::HashMap;
66
use std::error;
7+
use std::sync::atomic::AtomicBool;
78
use std::sync::{Arc, Mutex};
89
use std::time::Duration;
910

@@ -59,6 +60,8 @@ pub struct App {
5960
next_flamegraph: Arc<Mutex<Option<ParsedFlameGraph>>>,
6061
/// Shared update mode for the profiling thread
6162
update_mode_handle: Arc<Mutex<UpdateMode>>,
63+
/// Shared pid-mode flag for the profiling thread (toggled with 'p')
64+
pid_mode_handle: Arc<AtomicBool>,
6265
/// Stack positions from last render (for mouse click handling)
6366
pub stack_positions: Vec<StackPosition>,
6467
/// Last click for double-click detection
@@ -83,6 +86,7 @@ impl App {
8386
dirty: true,
8487
next_flamegraph: Arc::new(Mutex::new(None)),
8588
update_mode_handle: Arc::new(Mutex::new(UpdateMode::default())),
89+
pid_mode_handle: Arc::new(AtomicBool::new(false)),
8690
stack_positions: Vec::new(),
8791
last_click: None,
8892
process_output: None,
@@ -114,6 +118,7 @@ impl App {
114118
debug: false,
115119
dirty: true,
116120
update_mode_handle,
121+
pid_mode_handle: Arc::new(AtomicBool::new(false)),
117122
stack_positions: Vec::new(),
118123
last_click: None,
119124
process_output: None,
@@ -151,6 +156,10 @@ impl App {
151156
self.update_mode_handle.clone()
152157
}
153158

159+
pub fn get_pid_mode_handle(&self) -> Arc<AtomicBool> {
160+
self.pid_mode_handle.clone()
161+
}
162+
154163
/// Update flamegraph with new data
155164
pub fn update_flamegraph(&self, data: String) {
156165
let tic = std::time::Instant::now();

profile-bee-tui/src/handler.rs

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,21 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
2626
pub fn handle_command(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
2727
let mut key_handled = handle_command_generic(key_event, app)?;
2828
if !key_handled {
29-
match app.flamegraph_state().view_kind {
29+
let view = app.flamegraph_state().view_kind;
30+
let tree_mode = app.flamegraph_view.state.tree_mode;
31+
match view {
3032
ViewKind::FlameGraph => {
3133
key_handled = handle_command_flamegraph(key_event, app)?;
3234
}
35+
ViewKind::Table | ViewKind::ProcessList if tree_mode => {
36+
key_handled = handle_command_tree_view(key_event, app)?;
37+
}
3338
ViewKind::Table => {
3439
key_handled = handle_command_table(key_event, app)?;
3540
}
41+
ViewKind::ProcessList => {
42+
key_handled = handle_command_process_list(key_event, app)?;
43+
}
3644
ViewKind::Output => {
3745
key_handled = handle_command_output(key_event, app)?;
3846
}
@@ -90,6 +98,15 @@ pub fn handle_command_generic(key_event: KeyEvent, app: &mut App) -> AppResult<b
9098
KeyCode::Char('?') => {
9199
app.toggle_debug();
92100
}
101+
KeyCode::Char('t') => {
102+
app.flamegraph_view.state.tree_mode = !app.flamegraph_view.state.tree_mode;
103+
let mode_str = if app.flamegraph_view.state.tree_mode {
104+
"on"
105+
} else {
106+
"off"
107+
};
108+
app.set_transient_message(&format!("Tree mode: {}", mode_str));
109+
}
93110
_ => {
94111
key_handled = false;
95112
}
@@ -142,6 +159,15 @@ fn handle_command_flamegraph(key_event: KeyEvent, app: &mut App) -> AppResult<bo
142159
KeyCode::Char('#') => {
143160
app.search_selected();
144161
}
162+
KeyCode::Char('p') => {
163+
let new_val = !app.flamegraph_view.state.pid_mode;
164+
app.flamegraph_view.state.pid_mode = new_val;
165+
// Sync to shared handle so profiling thread picks it up
166+
app.get_pid_mode_handle()
167+
.store(new_val, std::sync::atomic::Ordering::Relaxed);
168+
let mode_str = if new_val { "on" } else { "off" };
169+
app.set_transient_message(&format!("PID mode: {}", mode_str));
170+
}
145171
_ => {
146172
key_handled = false;
147173
}
@@ -228,6 +254,114 @@ fn handle_command_output(key_event: KeyEvent, app: &mut App) -> AppResult<bool>
228254
Ok(key_handled)
229255
}
230256

257+
fn handle_command_process_list(key_event: KeyEvent, app: &mut App) -> AppResult<bool> {
258+
let process_count = app.flamegraph_view.get_process_list().len();
259+
let mut key_handled = true;
260+
match key_event.code {
261+
KeyCode::Down | KeyCode::Char('j') => {
262+
if process_count > 0 {
263+
let state = &mut app.flamegraph_view.state.process_list_state;
264+
state.selected = (state.selected + 1).min(process_count.saturating_sub(1));
265+
}
266+
}
267+
KeyCode::Up | KeyCode::Char('k') => {
268+
let state = &mut app.flamegraph_view.state.process_list_state;
269+
state.selected = state.selected.saturating_sub(1);
270+
}
271+
KeyCode::Char('G') => {
272+
if process_count > 0 {
273+
app.flamegraph_view.state.process_list_state.selected =
274+
process_count.saturating_sub(1);
275+
}
276+
}
277+
KeyCode::Char('g') => {
278+
app.flamegraph_view.state.process_list_state.selected = 0;
279+
}
280+
KeyCode::Enter => {
281+
let processes = app.flamegraph_view.get_process_list();
282+
let selected = app.flamegraph_view.state.process_list_state.selected;
283+
if let Some(entry) = processes.get(selected) {
284+
app.flamegraph_view.zoom_to_process(entry.stack_id);
285+
}
286+
}
287+
KeyCode::Esc | KeyCode::Char('p') => {
288+
app.flamegraph_view.state.view_kind = ViewKind::FlameGraph;
289+
}
290+
_ => {
291+
key_handled = false;
292+
}
293+
}
294+
Ok(key_handled)
295+
}
296+
297+
fn handle_command_tree_view(key_event: KeyEvent, app: &mut App) -> AppResult<bool> {
298+
let rows = app.flamegraph_view.get_tree_rows();
299+
let row_count = rows.len();
300+
let mut key_handled = true;
301+
match key_event.code {
302+
KeyCode::Down | KeyCode::Char('j') => {
303+
if row_count > 0 {
304+
let state = &mut app.flamegraph_view.state.tree_view_state;
305+
state.selected = (state.selected + 1).min(row_count.saturating_sub(1));
306+
}
307+
}
308+
KeyCode::Up | KeyCode::Char('k') => {
309+
let state = &mut app.flamegraph_view.state.tree_view_state;
310+
state.selected = state.selected.saturating_sub(1);
311+
}
312+
KeyCode::Char('G') => {
313+
if row_count > 0 {
314+
app.flamegraph_view.state.tree_view_state.selected = row_count.saturating_sub(1);
315+
}
316+
}
317+
KeyCode::Char('g') => {
318+
app.flamegraph_view.state.tree_view_state.selected = 0;
319+
}
320+
KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => {
321+
// Expand the selected node (or collapse if already expanded)
322+
let selected = app.flamegraph_view.state.tree_view_state.selected;
323+
if let Some(row) = rows.get(selected) {
324+
if row.has_children {
325+
app.flamegraph_view
326+
.state
327+
.tree_view_state
328+
.toggle_expanded(row.stack_id);
329+
}
330+
}
331+
}
332+
KeyCode::Left | KeyCode::Char('h') => {
333+
// Collapse the selected node, or move to parent
334+
let selected = app.flamegraph_view.state.tree_view_state.selected;
335+
if let Some(row) = rows.get(selected) {
336+
if row.is_expanded {
337+
// Collapse it
338+
app.flamegraph_view
339+
.state
340+
.tree_view_state
341+
.expanded
342+
.remove(&row.stack_id);
343+
} else if row.depth > 0 {
344+
// Move cursor to parent (find the row with depth-1 above us)
345+
for i in (0..selected).rev() {
346+
if rows[i].depth < row.depth {
347+
app.flamegraph_view.state.tree_view_state.selected = i;
348+
break;
349+
}
350+
}
351+
}
352+
}
353+
}
354+
KeyCode::Esc => {
355+
// Collapse all and reset
356+
app.flamegraph_view.state.tree_view_state.expanded.clear();
357+
}
358+
_ => {
359+
key_handled = false;
360+
}
361+
}
362+
Ok(key_handled)
363+
}
364+
231365
pub fn handle_input_buffer(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
232366
if let Some(input) = app.input_buffer.as_mut() {
233367
match key_event.code {

profile-bee-tui/src/state.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub enum ViewKind {
4545
FlameGraph,
4646
Table,
4747
Output,
48+
ProcessList,
4849
}
4950

5051
#[derive(Default, Debug, Clone)]
@@ -69,11 +70,44 @@ pub struct FlameGraphState {
6970
pub zoom: Option<ZoomState>,
7071
pub search_pattern: Option<SearchPattern>,
7172
pub freeze: bool,
73+
/// When true, stacks are prefixed with "process_name (pid)" root frames
74+
/// to split the flamegraph by process. Toggled with 'p'.
75+
pub pid_mode: bool,
76+
/// When true, the Top/Processes views show an expandable tree instead
77+
/// of a flat list. Toggled with 't'.
78+
pub tree_mode: bool,
7279
pub view_kind: ViewKind,
7380
pub table_state: TableState,
81+
pub process_list_state: TableState,
82+
pub tree_view_state: TreeViewState,
7483
pub update_mode: UpdateMode,
7584
}
7685

86+
/// State for the expandable call-tree view (like `perf report`).
87+
#[derive(Debug, Clone, Default)]
88+
pub struct TreeViewState {
89+
/// Currently selected row in the flattened tree.
90+
pub selected: usize,
91+
/// Scroll offset for rendering.
92+
pub offset: usize,
93+
/// Set of expanded node IDs.
94+
pub expanded: std::collections::HashSet<StackIdentifier>,
95+
}
96+
97+
impl TreeViewState {
98+
pub fn reset(&mut self) {
99+
self.selected = 0;
100+
self.offset = 0;
101+
self.expanded.clear();
102+
}
103+
104+
pub fn toggle_expanded(&mut self, id: StackIdentifier) {
105+
if !self.expanded.remove(&id) {
106+
self.expanded.insert(id);
107+
}
108+
}
109+
}
110+
77111
impl Default for FlameGraphState {
78112
fn default() -> Self {
79113
Self {
@@ -84,8 +118,12 @@ impl Default for FlameGraphState {
84118
zoom: None,
85119
search_pattern: None,
86120
freeze: false,
121+
pid_mode: false,
122+
tree_mode: false,
87123
view_kind: ViewKind::FlameGraph,
88124
table_state: TableState::default(),
125+
process_list_state: TableState::default(),
126+
tree_view_state: TreeViewState::default(),
89127
update_mode: UpdateMode::default(),
90128
}
91129
}
@@ -123,7 +161,8 @@ impl FlameGraphState {
123161
pub fn toggle_view_kind(&mut self) {
124162
self.view_kind = match self.view_kind {
125163
ViewKind::FlameGraph => ViewKind::Table,
126-
ViewKind::Table => ViewKind::FlameGraph,
164+
ViewKind::Table => ViewKind::ProcessList,
165+
ViewKind::ProcessList => ViewKind::FlameGraph,
127166
ViewKind::Output => ViewKind::FlameGraph,
128167
};
129168
}
@@ -133,7 +172,8 @@ impl FlameGraphState {
133172
pub fn toggle_view_kind_with_output(&mut self) {
134173
self.view_kind = match self.view_kind {
135174
ViewKind::FlameGraph => ViewKind::Table,
136-
ViewKind::Table => ViewKind::Output,
175+
ViewKind::Table => ViewKind::ProcessList,
176+
ViewKind::ProcessList => ViewKind::Output,
137177
ViewKind::Output => ViewKind::FlameGraph,
138178
};
139179
}
@@ -163,6 +203,15 @@ impl FlameGraphState {
163203
if let Some(p) = &self.search_pattern {
164204
new.set_hits(p);
165205
}
206+
207+
// Remap expanded tree node IDs from old to new flamegraph.
208+
// Nodes are matched by full name so expanded state survives data refreshes.
209+
let old_expanded = std::mem::take(&mut self.tree_view_state.expanded);
210+
for old_id in old_expanded {
211+
if let Some(new_id) = Self::get_new_stack_id(&old_id, old, new) {
212+
self.tree_view_state.expanded.insert(new_id);
213+
}
214+
}
166215
}
167216

168217
fn get_new_stack_id(

0 commit comments

Comments
 (0)