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)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = VisualizeConfig::default();
assert_eq!(config.tick_rate_ms, 50);
assert_eq!(config.history_size, 300);
assert!(config.enable_anomaly);
assert!(!config.enable_ml);
}
#[test]
fn test_config_clone() {
let config = VisualizeConfig {
tick_rate_ms: 100,
enable_ml: true,
ml_clusters: 5,
..Default::default()
};
let cloned = config.clone();
assert_eq!(cloned.tick_rate_ms, 100);
assert!(cloned.enable_ml);
assert_eq!(cloned.ml_clusters, 5);
}
#[test]
fn test_config_all_fields() {
let config = VisualizeConfig {
tick_rate_ms: 100,
enable_anomaly: false,
enable_ml: true,
ml_clusters: 5,
anomaly_threshold: 2.5,
history_size: 500,
deterministic: true,
show_fps: true,
pid: Some(1234),
enable_source: true,
filter: Some("read|write".to_string()),
otlp_endpoint: Some("http://localhost:4317".to_string()),
enable_metrics: true,
enable_alerts: true,
alert_latency_threshold_us: 5000,
alert_error_rate_percent: 2.5,
};
assert_eq!(config.tick_rate_ms, 100);
assert!(!config.enable_anomaly);
assert!(config.enable_ml);
assert_eq!(config.ml_clusters, 5);
assert!((config.anomaly_threshold - 2.5).abs() < f32::EPSILON);
assert_eq!(config.history_size, 500);
assert!(config.deterministic);
assert!(config.show_fps);
assert_eq!(config.pid, Some(1234));
assert!(config.enable_source);
assert_eq!(config.filter, Some("read|write".to_string()));
assert_eq!(config.otlp_endpoint, Some("http://localhost:4317".to_string()));
assert!(config.enable_metrics);
assert!(config.enable_alerts);
assert_eq!(config.alert_latency_threshold_us, 5000);
assert!((config.alert_error_rate_percent - 2.5).abs() < f32::EPSILON);
}
#[test]
fn test_inject_demo_data() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
assert_eq!(app.total_syscalls, 0);
for tick in 0..10 {
inject_demo_data(&mut app, tick);
}
assert!(app.total_syscalls > 0);
assert!(!app.latency_history.is_empty());
}
#[test]
fn test_inject_demo_data_deterministic() {
let config = VisualizeConfig::default();
let mut app1 = app::VisualizeApp::new(config.clone());
let mut app2 = app::VisualizeApp::new(config);
inject_demo_data(&mut app1, 42);
inject_demo_data(&mut app2, 42);
assert_eq!(app1.total_syscalls, app2.total_syscalls);
}
#[test]
fn test_inject_demo_data_distribution() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
for tick in 0..100 {
inject_demo_data(&mut app, tick);
}
assert!(app.total_syscalls > 500);
assert!(app.total_errors > 0);
assert!(app.anomaly_count > 0);
}
#[test]
fn test_config_debug() {
let config = VisualizeConfig::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("tick_rate_ms"));
assert!(debug_str.contains("enable_anomaly"));
}
#[test]
fn test_sprint56_config_defaults() {
let config = VisualizeConfig::default();
assert!(!config.enable_metrics);
assert!(!config.enable_alerts);
assert_eq!(config.alert_latency_threshold_us, 10_000);
assert!((config.alert_error_rate_percent - 5.0).abs() < f32::EPSILON);
}
#[test]
fn test_config_with_sprint56_features() {
let config = VisualizeConfig {
enable_metrics: true,
enable_alerts: true,
alert_latency_threshold_us: 20_000,
alert_error_rate_percent: 10.0,
..Default::default()
};
assert!(config.enable_metrics);
assert!(config.enable_alerts);
assert_eq!(config.alert_latency_threshold_us, 20_000);
assert!((config.alert_error_rate_percent - 10.0).abs() < f32::EPSILON);
}
#[test]
fn test_inject_demo_data_syscall_categories() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
for tick in 0..200 {
inject_demo_data(&mut app, tick);
}
assert!(app.total_syscalls > 1000);
}
#[test]
fn test_inject_demo_data_anomaly_detection() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
for tick in 0..500 {
inject_demo_data(&mut app, tick);
}
assert!(app.anomaly_count > 0);
}
#[test]
fn test_inject_demo_data_error_rate() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
for tick in 0..1000 {
inject_demo_data(&mut app, tick);
}
let error_rate = app.total_errors as f64 / app.total_syscalls as f64;
assert!(error_rate > 0.01 && error_rate < 0.05);
}
#[test]
fn test_config_with_pid() {
let config = VisualizeConfig { pid: Some(12345), ..Default::default() };
assert_eq!(config.pid, Some(12345));
}
#[test]
fn test_config_with_filter() {
let config =
VisualizeConfig { filter: Some("mmap|munmap".to_string()), ..Default::default() };
assert_eq!(config.filter, Some("mmap|munmap".to_string()));
}
#[test]
fn test_config_with_otlp() {
let config = VisualizeConfig {
otlp_endpoint: Some("http://jaeger:4317".to_string()),
..Default::default()
};
assert_eq!(config.otlp_endpoint, Some("http://jaeger:4317".to_string()));
}
#[test]
fn test_visualize_config_deterministic_mode() {
let config = VisualizeConfig { deterministic: true, ..Default::default() };
assert!(config.deterministic);
}
#[test]
fn test_update_frame_stats() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
let mut frame_times: Vec<u64> = Vec::with_capacity(60);
update_frame_stats(&mut app, &mut frame_times, 1000);
assert_eq!(app.frame_id, 1);
assert_eq!(app.avg_frame_time_us, 1000);
assert_eq!(app.max_frame_time_us, 1000);
assert_eq!(frame_times.len(), 1);
update_frame_stats(&mut app, &mut frame_times, 2000);
assert_eq!(app.frame_id, 2);
assert_eq!(app.avg_frame_time_us, 1500); assert_eq!(app.max_frame_time_us, 2000);
assert_eq!(frame_times.len(), 2);
for i in 0..65 {
update_frame_stats(&mut app, &mut frame_times, 100 + i as u64);
}
assert_eq!(frame_times.len(), 60); }
#[test]
fn test_handle_key_input_quit() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
let key_q = event::KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
assert!(handle_key_input(&mut app, key_q));
let key_ctrl_c = event::KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
assert!(handle_key_input(&mut app, key_ctrl_c));
}
#[test]
fn test_handle_key_input_other_keys() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
let key_a = event::KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
assert!(!handle_key_input(&mut app, key_a));
let key_enter = event::KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
assert!(!handle_key_input(&mut app, key_enter));
let key_tab = event::KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
assert!(!handle_key_input(&mut app, key_tab));
}
#[test]
fn test_create_tracer_handle_demo_mode() {
let (tx, _rx) = mpsc::channel::<VisualizerEvent>();
let setup = create_tracer_handle(None, None, tx);
assert!(setup.demo_mode);
assert!(setup.handle.is_none());
}
#[test]
fn test_terminal_guard_drop() {
{
let _guard = TerminalGuard;
}
}
#[test]
fn test_suppress_stdio_safe() {
suppress_stdio();
}
#[test]
fn test_handle_tracer_finish_none() {
let config = VisualizeConfig::default();
let mut app = app::VisualizeApp::new(config);
let mut handle: Option<thread::JoinHandle<Result<i32>>> = None;
handle_tracer_finish(&mut app, &mut handle);
assert!(!app.trace_complete); }
#[test]
fn test_open_tty_fallback() {
let _file = open_tty();
}
}