pub mod app;
pub mod event;
pub mod ui;
pub mod tui;
pub mod handler;
pub mod flame;
pub mod state;
pub mod view;
#[cfg(feature = "python")]
pub mod py_spy;
#[cfg(feature = "python")]
pub mod py_spy_flamegraph;
use app::{App, AppResult, ParsedFlameGraph};
use event::{Event, EventHandler};
use flame::FlameGraph;
use handler::handle_key_events;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io;
use std::sync::{Arc, Mutex};
pub fn run_from_collapsed_stacks(data: String, title: &str, sorted: bool) -> AppResult<()> {
let flamegraph = FlameGraph::from_string(data, sorted);
let mut app = App::with_flamegraph(title, flamegraph);
run_tui_loop(&mut app)
}
pub fn run_from_live_stream(rx: std::sync::mpsc::Receiver<String>, title: &str) -> AppResult<()> {
let flamegraph = FlameGraph::from_string(String::new(), true);
let mut app = App::with_flamegraph(title, flamegraph);
let (parse_tx, parse_rx) = std::sync::mpsc::channel::<String>();
let next_flamegraph: Arc<Mutex<Option<ParsedFlameGraph>>> = Arc::new(Mutex::new(None));
let next_fg_clone = next_flamegraph.clone();
std::thread::spawn(move || {
while let Ok(combined) = parse_rx.recv() {
let mut latest = combined;
while let Ok(newer) = parse_rx.try_recv() {
latest = newer;
}
let tic = std::time::Instant::now();
let flamegraph = FlameGraph::from_string(latest, true);
let parsed = ParsedFlameGraph {
flamegraph,
elapsed: tic.elapsed(),
};
*next_fg_clone.lock().unwrap() = Some(parsed);
}
});
let mut accumulated_stacks: Vec<String> = Vec::new();
let backend = CrosstermBackend::new(io::stderr());
let terminal = Terminal::new(backend)?;
let events = EventHandler::new(250);
let mut tui = tui::Tui::new(terminal, events);
tui.init()?;
while app.running {
let mut has_new_data = false;
while let Ok(new_data) = rx.try_recv() {
accumulated_stacks = merge_collapsed_stacks(&accumulated_stacks, &new_data);
has_new_data = true;
}
if has_new_data {
let combined = accumulated_stacks.join("\n");
let _ = parse_tx.send(combined);
}
if let Some(parsed) = next_flamegraph.lock().unwrap().take() {
app.flamegraph_view.replace_flamegraph(parsed.flamegraph);
}
tui.draw(&mut app)?;
match tui.events.next()? {
Event::Tick => app.tick(),
Event::Key(key_event) => handle_key_events(key_event, &mut app)?,
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
}
}
tui.exit()?;
Ok(())
}
fn run_tui_loop(app: &mut App) -> AppResult<()> {
let backend = CrosstermBackend::new(io::stderr());
let terminal = Terminal::new(backend)?;
let events = EventHandler::new(250);
let mut tui = tui::Tui::new(terminal, events);
tui.init()?;
while app.running {
tui.draw(app)?;
match tui.events.next()? {
Event::Tick => app.tick(),
Event::Key(key_event) => handle_key_events(key_event, app)?,
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
}
}
tui.exit()?;
Ok(())
}
fn merge_collapsed_stacks(existing: &[String], new_data: &str) -> Vec<String> {
use std::collections::HashMap;
let mut counts: HashMap<String, u64> = HashMap::new();
for line in existing {
if let Some((stack, count_str)) = line.rsplit_once(' ') {
if let Ok(count) = count_str.parse::<u64>() {
*counts.entry(stack.to_string()).or_insert(0) += count;
}
}
}
for line in new_data.lines() {
if let Some((stack, count_str)) = line.rsplit_once(' ') {
if let Ok(count) = count_str.parse::<u64>() {
*counts.entry(stack.to_string()).or_insert(0) += count;
}
}
}
let mut result: Vec<String> = counts
.into_iter()
.map(|(stack, count)| format!("{} {}", stack, count))
.collect();
result.sort();
result
}