pub mod app;
pub mod collectors;
pub mod panels;
pub mod ring_buffer;
pub mod theme;
pub mod ui;
pub mod widgets;
pub use app::VisualizeApp;
pub use collectors::{AnomalyCollector, Collector, SpanReceiver, SyscallCollector};
pub use ring_buffer::HistoryBuffer;
pub use theme::{percent_color, severity_color};
use crate::tracer::{self, TracerConfig, VisualizerEvent};
use anyhow::Result;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::fs::{File, OpenOptions};
use std::os::unix::io::AsRawFd;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct VisualizeConfig {
pub tick_rate_ms: u64,
pub enable_anomaly: bool,
pub enable_ml: bool,
pub ml_clusters: usize,
pub anomaly_threshold: f32,
pub history_size: usize,
pub deterministic: bool,
pub show_fps: bool,
pub pid: Option<i32>,
pub enable_source: bool,
pub filter: Option<String>,
pub otlp_endpoint: Option<String>,
pub enable_metrics: bool,
pub enable_alerts: bool,
pub alert_latency_threshold_us: u64,
pub alert_error_rate_percent: f32,
}
impl Default for VisualizeConfig {
fn default() -> Self {
Self {
tick_rate_ms: 50,
enable_anomaly: true,
enable_ml: false,
ml_clusters: 3,
anomaly_threshold: 3.0,
history_size: 300,
deterministic: false,
show_fps: false,
pid: None,
enable_source: false,
filter: None,
otlp_endpoint: None,
enable_metrics: false,
enable_alerts: false,
alert_latency_threshold_us: 10_000,
alert_error_rate_percent: 5.0,
}
}
}
fn inject_demo_data(app: &mut app::VisualizeApp, tick: u64) {
let mut seed = tick.wrapping_mul(6364136223846793005).wrapping_add(1);
let mut next_rand = || {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
seed
};
const FILE_SYSCALLS: &[&str] = &["read", "write", "open", "close", "stat", "fstat", "lseek"];
const NET_SYSCALLS: &[&str] = &["socket", "connect", "sendto", "recvfrom", "bind", "listen"];
const MEM_SYSCALLS: &[&str] = &["mmap", "munmap", "brk", "mprotect"];
const PROC_SYSCALLS: &[&str] = &["fork", "clone", "execve", "wait4", "getpid"];
let count = 5 + (next_rand() % 16) as usize;
for _ in 0..count {
let r = next_rand();
let (syscalls, base_duration) = match r % 100 {
0..=49 => (FILE_SYSCALLS, 50), 50..=74 => (NET_SYSCALLS, 200), 75..=89 => (MEM_SYSCALLS, 20), _ => (PROC_SYSCALLS, 500), };
let name = syscalls[(next_rand() as usize) % syscalls.len()];
let duration = if next_rand() % 100 < 3 {
base_duration * (10 + (next_rand() % 90))
} else {
base_duration + (next_rand() % (base_duration * 2))
};
let result = if next_rand() % 100 < 2 { -1 } else { 0 };
app.record_syscall(name, duration, result);
if duration > base_duration * 5 {
let z_score = (duration as f32 / base_duration as f32).min(10.0);
app.record_anomaly(
name.to_string(),
duration,
z_score,
Some("demo.rs".to_string()),
Some((tick % 500) as u32 + 1),
);
}
}
}
#[allow(unsafe_code)]
fn suppress_stdio() {
if let Ok(devnull) = File::open("/dev/null") {
let devnull_fd = devnull.as_raw_fd();
unsafe {
libc::dup2(devnull_fd, libc::STDOUT_FILENO);
libc::dup2(devnull_fd, libc::STDERR_FILENO);
}
}
}
struct TracerSetup {
handle: Option<thread::JoinHandle<Result<i32>>>,
demo_mode: bool,
}
fn create_tracer_handle(
command: Option<&[String]>,
pid: Option<i32>,
tx: mpsc::Sender<VisualizerEvent>,
) -> TracerSetup {
if let Some(cmd) = command {
let cmd_owned: Vec<String> = cmd.to_vec();
let tracer_config = TracerConfig { visualizer_sink: Some(tx), ..Default::default() };
let handle = thread::spawn(move || {
suppress_stdio();
tracer::trace_command(&cmd_owned, tracer_config)
});
return TracerSetup { handle: Some(handle), demo_mode: false };
}
if let Some(pid) = pid {
let tracer_config = TracerConfig { visualizer_sink: Some(tx), ..Default::default() };
let handle = thread::spawn(move || {
suppress_stdio();
tracer::attach_to_pid(pid, tracer_config)
});
return TracerSetup { handle: Some(handle), demo_mode: false };
}
drop(tx);
TracerSetup { handle: None, demo_mode: true }
}
fn open_tty() -> File {
OpenOptions::new().read(true).write(true).open("/dev/tty").unwrap_or_else(|_| {
OpenOptions::new().write(true).open("/proc/self/fd/1").expect("Failed to open terminal")
})
}
struct TerminalGuard;
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
if let Ok(mut tty) = OpenOptions::new().write(true).open("/dev/tty") {
let _ = execute!(tty, LeaveAlternateScreen, DisableMouseCapture);
}
}
}
fn update_frame_stats(app: &mut app::VisualizeApp, frame_times: &mut Vec<u64>, frame_time: u64) {
frame_times.push(frame_time);
if frame_times.len() > 60 {
frame_times.remove(0);
}
app.avg_frame_time_us = frame_times.iter().sum::<u64>() / frame_times.len().max(1) as u64;
app.max_frame_time_us = frame_times.iter().copied().max().unwrap_or(0);
app.frame_id += 1;
}
fn handle_key_input(app: &mut app::VisualizeApp, key: event::KeyEvent) -> bool {
if key.code == KeyCode::Char('q')
|| (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL))
{
return true;
}
app.handle_key(key.code, key.modifiers);
false
}
fn handle_tracer_finish(
app: &mut app::VisualizeApp,
tracer_handle: &mut Option<thread::JoinHandle<Result<i32>>>,
) {
if tracer_handle.as_ref().is_some_and(std::thread::JoinHandle::is_finished) {
app.collect_metrics();
app.trace_complete = true;
app.event_receiver = None;
if let Some(handle) = tracer_handle.take() {
let _ = handle.join();
}
let _ = enable_raw_mode();
}
}
fn process_tick(
app: &mut app::VisualizeApp,
demo_mode: bool,
demo_tick: &mut u64,
last_tick: &mut Instant,
) {
if demo_mode {
inject_demo_data(app, *demo_tick);
*demo_tick = demo_tick.wrapping_add(1);
}
app.collect_metrics();
*last_tick = Instant::now();
}
fn poll_keyboard(app: &mut app::VisualizeApp, timeout: Duration) -> Result<Option<bool>> {
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
return Ok(Some(handle_key_input(app, key)));
}
}
Ok(None)
}
fn init_terminal() -> Result<(Terminal<CrosstermBackend<File>>, TerminalGuard)> {
enable_raw_mode()?;
let mut tty = open_tty();
let guard = TerminalGuard;
execute!(tty, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(tty);
let terminal = Terminal::new(backend)?;
Ok((terminal, guard))
}
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<File>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
Ok(())
}
enum LoopAction {
Continue,
Exit(i32),
}
fn run_event_loop_iteration(
terminal: &mut Terminal<CrosstermBackend<File>>,
app: &mut app::VisualizeApp,
frame_times: &mut Vec<u64>,
tick_rate: Duration,
demo_mode: bool,
demo_tick: &mut u64,
last_tick: &mut Instant,
tracer_handle: &mut Option<std::thread::JoinHandle<Result<i32>>>,
) -> Result<LoopAction> {
let frame_start = Instant::now();
terminal.draw(|f| ui::draw(f, app))?;
update_frame_stats(app, frame_times, frame_start.elapsed().as_micros() as u64);
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if let Some(true) = poll_keyboard(app, timeout)? {
return Ok(LoopAction::Exit(0));
}
if last_tick.elapsed() >= tick_rate {
process_tick(app, demo_mode, demo_tick, last_tick);
}
if app.should_exit {
return Ok(LoopAction::Exit(app.exit_code));
}
handle_tracer_finish(app, tracer_handle);
Ok(LoopAction::Continue)
}
pub fn run_visualize(command: Option<&[String]>, config: VisualizeConfig) -> Result<i32> {
let (tx, rx) = mpsc::channel::<VisualizerEvent>();
let setup = create_tracer_handle(command, config.pid, tx);
let mut tracer_handle = setup.handle;
let demo_mode = setup.demo_mode;
let (mut terminal, _guard) = init_terminal()?;
let receiver = if tracer_handle.is_some() { Some(rx) } else { None };
let mut app = app::VisualizeApp::with_receiver(config.clone(), receiver);
let mut demo_tick: u64 = 0;
let tick_rate = Duration::from_millis(config.tick_rate_ms);
let mut last_tick = Instant::now();
let mut frame_times: Vec<u64> = Vec::with_capacity(60);
let result = loop {
match run_event_loop_iteration(
&mut terminal,
&mut app,
&mut frame_times,
tick_rate,
demo_mode,
&mut demo_tick,
&mut last_tick,
&mut tracer_handle,
)? {
LoopAction::Continue => {}
LoopAction::Exit(code) => break Ok(code),
}
};
if let Some(handle) = tracer_handle {
let _ = handle.join();
}
restore_terminal(&mut terminal)?;
result
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;