use clap::{Parser, Subcommand};
use colored::Colorize;
#[cfg(feature = "ebpf")]
use denet::ebpf::debug;
use denet::error::Result;
use denet::monitor::{AggregatedMetrics, Metrics, Summary, SummaryGenerator};
use denet::ProcessMonitor;
use std::fs::File;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::exit;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use tabled::{builder::Builder, settings::Style};
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long)]
json: bool,
#[clap(short, long, value_name = "FILE")]
out: Option<PathBuf>,
#[clap(short, long, default_value = "100")]
interval: u64,
#[clap(short, long, default_value = "1000")]
max_interval: u64,
#[clap(short, long)]
no_update: bool,
#[clap(short, long, default_value = "0")]
duration: u64,
#[clap(long)]
since_process_start: bool,
#[clap(long)]
exclude_children: bool,
#[clap(short, long)]
quiet: bool,
#[clap(long, value_name = "FILE")]
stats: Option<PathBuf>,
#[clap(long)]
enable_ebpf: bool,
#[clap(long)]
debug: bool,
#[clap(long)]
no_polling: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Run {
#[clap(required = true)]
command: Vec<String>,
},
Attach {
#[clap(required = true)]
pid: usize,
},
Stats {
#[clap(required = true)]
file: PathBuf,
},
Summary {
#[clap(required = true)]
file: PathBuf,
},
}
fn main() -> Result<()> {
let args = Args::parse();
if let Commands::Stats { file } | Commands::Summary { file } = &args.command {
return handle_stats_command(file, &args);
}
handle_monitoring_commands(&args)
}
fn handle_stats_command(file: &PathBuf, args: &Args) -> Result<()> {
if let Some(old_cmd) = std::env::args().nth(1) {
if old_cmd == "summary" && !args.quiet {
eprintln!("Note: Using 'stats' is recommended over 'summary'");
}
}
generate_summary_from_file(file, args.json, args.out.as_ref())
}
fn handle_monitoring_commands(args: &Args) -> Result<()> {
let file_handles = setup_output_files(args)?;
let monitor = create_monitor_from_args(args)?;
execute_monitoring_with_output(monitor, file_handles, args)
}
struct OutputHandles {
out_file: Option<File>,
_stats_file: Option<File>,
}
fn setup_output_files(args: &Args) -> Result<OutputHandles> {
let out_file = args.out.as_ref().map(|path| {
File::create(path).unwrap_or_else(|err| {
eprintln!("Error creating output file: {err}");
exit(1);
})
});
let stats_file = args.stats.as_ref().map(|path| {
File::create(path).unwrap_or_else(|err| {
eprintln!("Error creating stats output file: {err}");
exit(1);
})
});
Ok(OutputHandles {
out_file,
_stats_file: stats_file,
})
}
fn create_monitor_from_args(args: &Args) -> Result<ProcessMonitor> {
match &args.command {
Commands::Run { command } => create_monitor_for_command(command, args),
Commands::Attach { pid } => create_monitor_for_pid(*pid, args),
Commands::Stats { .. } | Commands::Summary { .. } => unreachable!(),
}
}
fn create_monitor_for_command(command: &[String], args: &Args) -> Result<ProcessMonitor> {
if command.is_empty() {
eprintln!("Error: Empty command");
exit(1);
}
match ProcessMonitor::new_with_options(
command.to_vec(),
Duration::from_millis(args.interval),
Duration::from_millis(args.max_interval),
args.since_process_start,
) {
Ok(monitor) => {
if args.debug && !args.quiet && !args.json {
println!("Monitoring process: {}", command.join(" ").cyan());
}
Ok(monitor)
}
Err(err) => {
eprintln!("Error starting command: {err}");
exit(1);
}
}
}
fn create_monitor_for_pid(pid: usize, args: &Args) -> Result<ProcessMonitor> {
match ProcessMonitor::from_pid_with_options(
pid,
Duration::from_millis(args.interval),
Duration::from_millis(args.max_interval),
args.since_process_start,
) {
Ok(monitor) => {
if !args.quiet && !args.json {
println!(
"Monitoring existing process with PID: {}",
pid.to_string().cyan()
);
}
Ok(monitor)
}
Err(err) => {
eprintln!("Error attaching to process {pid}: {err}");
exit(1);
}
}
}
fn execute_monitoring_with_output(
mut monitor: ProcessMonitor,
mut file_handles: OutputHandles,
args: &Args,
) -> Result<()> {
let ui_quiet = args.quiet || args.json;
if args.debug {
monitor.set_debug_mode(true);
if !ui_quiet {
println!("Debug mode enabled - verbose output will be shown");
}
}
#[cfg(feature = "ebpf")]
{
if args.debug && !ui_quiet {
println!("Debug mode enabled for eBPF profiling - verbose output will be shown");
unsafe {
debug::set_debug_mode(args.debug);
}
} else {
unsafe {
debug::set_debug_mode(args.debug);
}
}
}
if args.enable_ebpf {
if let Err(e) = monitor.enable_ebpf() {
if !args.quiet {
eprintln!("Warning: Failed to enable eBPF profiling: {e}");
eprintln!("Hint: Try running with sudo or setting CAP_BPF capability:");
eprintln!(" sudo setcap cap_bpf+ep target/release/denet");
if args.debug {
eprintln!("\nFor detailed diagnostics, run: cargo run --bin ebpf_diag --features ebpf");
eprintln!("(Add --debug flag for even more verbose output)");
} else {
eprintln!("Run with --debug flag for more detailed error information");
}
eprintln!("Continuing without eBPF profiling...");
}
} else if !ui_quiet {
println!("eBPF profiling enabled");
}
}
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
if !ui_quiet {
println!("\nReceived Ctrl-C, finishing...");
}
})
.expect("Error setting Ctrl-C handler");
if !ui_quiet {
println!("Press Ctrl+C to stop monitoring");
println!();
}
let mut terminal_width = 80; if let Ok((w, _)) = crossterm::terminal::size() {
terminal_width = w as usize;
}
let update_in_place = !args.no_update && !args.json;
let mut needs_newline_on_exit = false;
let progress_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let mut progress_index = 0;
let start_time = Instant::now();
let mut metrics_count = 0;
let mut results = Vec::new();
let mut aggregated_metrics: Vec<AggregatedMetrics> = Vec::new();
let timeout = if args.duration > 0 {
Some(Duration::from_secs(args.duration))
} else {
None
};
let metadata = monitor.get_metadata();
if let Some(metadata_ref) = &metadata {
let metadata_json = serde_json::to_string(&metadata_ref).unwrap();
if let Some(file) = &mut file_handles.out_file {
writeln!(file, "{metadata_json}")?;
}
if args.json && !args.quiet {
println!("{metadata_json}");
}
}
if args.no_polling {
if !args.quiet {
println!("🚀 Pure event-driven mode: eBPF collecting syscalls until completion...");
}
while monitor.is_running() && running.load(Ordering::SeqCst) {
if let Some(timeout_duration) = timeout {
if start_time.elapsed() >= timeout_duration {
if !args.quiet {
println!("\nTimeout reached after {} seconds", args.duration);
}
break;
}
}
std::thread::sleep(Duration::from_millis(100));
}
if !args.quiet {
println!("✅ Process completed. Generating comprehensive summary...");
}
let final_tree_metrics = monitor.sample_tree_metrics();
if args.json {
let json = serde_json::to_string(&final_tree_metrics).unwrap();
println!("{json}");
} else if let Some(agg) = final_tree_metrics.aggregated {
results.push(convert_aggregated_to_metrics(&agg));
metrics_count = 1;
}
} else {
while monitor.is_running() && running.load(Ordering::SeqCst) {
if let Some(timeout_duration) = timeout {
if start_time.elapsed() >= timeout_duration {
if !args.quiet {
println!("\nTimeout reached after {} seconds", args.duration);
}
break;
}
}
if args.exclude_children {
if let Some(metrics) = monitor.sample_metrics() {
metrics_count += 1;
results.push(metrics.clone());
if args.json {
let json = serde_json::to_string(&metrics).unwrap();
if let Some(file) = &mut file_handles.out_file {
writeln!(file, "{json}")?;
}
if !args.quiet {
if update_in_place {
let spinner = progress_chars[progress_index % progress_chars.len()];
let elapsed = start_time.elapsed().as_secs();
print!(
"\r{}\r{} [{}s] {}",
" ".repeat(terminal_width.saturating_sub(1)),
spinner.to_string().cyan(),
elapsed.to_string().bright_black(),
json
);
io::stdout().flush()?;
needs_newline_on_exit = true;
progress_index += 1;
} else {
println!("{json}");
}
}
} else {
let formatted = format_metrics(&metrics);
if let Some(file) = &mut file_handles.out_file {
writeln!(file, "{}", serde_json::to_string(&metrics).unwrap())?;
}
if !args.quiet {
if update_in_place {
let formatted_compact = format_metrics_compact(&metrics);
let spinner = progress_chars[progress_index % progress_chars.len()];
let elapsed = start_time.elapsed().as_secs();
print!(
"\r{}\r{} [{}s] {}",
" ".repeat(terminal_width.saturating_sub(1)),
spinner.to_string().cyan(),
elapsed.to_string().bright_black(),
formatted_compact
);
io::stdout().flush()?;
needs_newline_on_exit = true;
progress_index += 1;
} else {
println!("{formatted}");
}
}
}
}
} else {
let tree_metrics = monitor.sample_tree_metrics();
if let Some(agg_metrics) = tree_metrics.aggregated.as_ref() {
metrics_count += 1;
let storage_metrics = convert_aggregated_to_metrics(agg_metrics);
results.push(storage_metrics);
aggregated_metrics.push(agg_metrics.clone());
if args.json {
let json = serde_json::to_string(&tree_metrics).unwrap();
if let Some(file) = &mut file_handles.out_file {
writeln!(file, "{json}")?;
}
if !args.quiet {
if update_in_place {
let agg_json = serde_json::to_string(&agg_metrics).unwrap();
let spinner = progress_chars[progress_index % progress_chars.len()];
let elapsed = start_time.elapsed().as_secs();
print!(
"\r{}\r{} [{}s] {}",
" ".repeat(terminal_width.saturating_sub(1)),
spinner.to_string().cyan(),
elapsed.to_string().bright_black(),
agg_json
);
io::stdout().flush()?;
needs_newline_on_exit = true;
progress_index += 1;
} else {
println!("{json}");
}
}
} else {
let formatted = format_aggregated_metrics(agg_metrics);
if let Some(file) = &mut file_handles.out_file {
writeln!(file, "{}", serde_json::to_string(&tree_metrics).unwrap())?;
}
if !args.quiet {
if update_in_place {
let formatted_compact =
format_aggregated_metrics_compact(agg_metrics);
let spinner = progress_chars[progress_index % progress_chars.len()];
let elapsed = start_time.elapsed().as_secs();
print!(
"\r{}\r{} [{}s] {}",
" ".repeat(terminal_width.saturating_sub(1)),
spinner.to_string().cyan(),
elapsed.to_string().bright_black(),
formatted_compact
);
io::stdout().flush()?;
needs_newline_on_exit = true;
progress_index += 1;
} else {
println!("{formatted}");
}
}
}
}
}
std::thread::sleep(monitor.adaptive_interval());
}
}
let runtime = start_time.elapsed();
if !args.quiet && !args.json {
if needs_newline_on_exit {
println!();
}
println!(
"\n✅ {} {}",
"Monitoring complete after".green(),
format!("{:.1} seconds", runtime.as_secs_f64())
.cyan()
.bold()
);
println!(
"📊 {} {}",
"Collected".green(),
format!("{metrics_count} metric samples").cyan().bold()
);
if let Some(path) = &args.out {
println!("Results written to {}", path.display().to_string().green());
}
if !results.is_empty() {
print_summary(&results, runtime.as_secs_f64());
}
}
Ok(())
}
fn color_for_cpu(cpu: f32) -> &'static str {
if cpu < 10.0 {
"green"
} else if cpu < 50.0 {
"yellow"
} else {
"red"
}
}
fn color_for_mem(mem_mb: f64) -> &'static str {
if mem_mb < 100.0 {
"green"
} else if mem_mb < 500.0 {
"yellow"
} else {
"red"
}
}
#[cfg(feature = "gpu")]
fn color_for_util(util: u32) -> &'static str {
if util < 30 {
"green"
} else if util < 70 {
"yellow"
} else {
"red"
}
}
#[allow(clippy::too_many_arguments)]
fn format_base(
cpu_usage: f32,
mem_rss_kb: u64,
thread_count: usize,
disk_read_bytes: u64,
disk_write_bytes: u64,
sys_net_rx_bytes: u64,
sys_net_tx_bytes: u64,
uptime_secs: u64,
process_count: Option<usize>,
compact: bool,
) -> String {
let cpu_color = color_for_cpu(cpu_usage);
let mem_mb = mem_rss_kb as f64 / 1024.0;
let mem_color = color_for_mem(mem_mb);
let prefix = match (process_count, compact) {
(Some(n), false) => format!("Tree ({n} procs): "),
(Some(n), true) => format!("Tree({n}): "),
(None, _) => String::new(),
};
if compact {
format!(
"{prefix}CPU {} | Mem {} | Threads {} | Disk {} rd, {} wr | Sys Net {} rx, {} tx",
format!("{:.1}%", cpu_usage).color(cpu_color),
format!("{mem_mb:.0}M").color(mem_color),
thread_count,
format_bytes(disk_read_bytes).cyan(),
format_bytes(disk_write_bytes).cyan(),
format_bytes(sys_net_rx_bytes).green(),
format_bytes(sys_net_tx_bytes).green(),
)
} else {
format!(
"{prefix}CPU: {} | Memory: {} | Threads: {} | Disk: {} rd, {} wr | Sys Net: {} rx, {} tx | Uptime: {}s",
format!("{:.1}%", cpu_usage).color(cpu_color),
format!("{mem_mb:.1} MB").color(mem_color),
thread_count,
format_bytes(disk_read_bytes).cyan(),
format_bytes(disk_write_bytes).cyan(),
format_bytes(sys_net_rx_bytes).green(),
format_bytes(sys_net_tx_bytes).green(),
uptime_secs,
)
}
}
#[cfg(feature = "gpu")]
fn append_gpu_suffix(base: String, gpu: Option<&denet::gpu::GpuMetrics>, compact: bool) -> String {
let Some(gpu_metrics) = gpu else { return base };
if gpu_metrics.has_process_data {
if let Some(util) = gpu_metrics.max_process_utilization() {
let mem = gpu_metrics.total_process_memory_usage();
let color = color_for_util(util);
if compact {
return format!("{base} | GPU {}%", format!("{util}").color(color));
} else {
let gb = mem as f64 / (1024.0 * 1024.0 * 1024.0);
return format!(
"{base} | GPU: {}%, {gb:.1}GB",
format!("{util}").color(color)
);
}
} else if gpu_metrics.total_process_memory_usage() > 0 {
let mem = gpu_metrics.total_process_memory_usage();
if compact {
let mb = mem as f64 / (1024.0 * 1024.0);
return format!("{base} | GPU {mb:.0}M");
} else {
let gb = mem as f64 / (1024.0 * 1024.0 * 1024.0);
return format!("{base} | GPU: {gb:.1}GB");
}
}
} else if let Some(util) = gpu_metrics.max_system_utilization() {
let color = color_for_util(util);
let sep = if compact { "" } else { ":" };
return format!(
"{base} | GPU{sep} {}% (sys)",
format!("{util}").color(color)
);
}
base
}
fn format_metrics(metrics: &Metrics) -> String {
let base = format_base(
metrics.cpu_usage,
metrics.mem_rss_kb,
metrics.thread_count,
metrics.disk_read_bytes,
metrics.disk_write_bytes,
metrics.sys_net_rx_bytes,
metrics.sys_net_tx_bytes,
metrics.uptime_secs,
None,
false,
);
#[cfg(feature = "gpu")]
return append_gpu_suffix(base, metrics.gpu.as_ref(), false);
#[cfg(not(feature = "gpu"))]
base
}
fn format_metrics_compact(metrics: &Metrics) -> String {
let base = format_base(
metrics.cpu_usage,
metrics.mem_rss_kb,
metrics.thread_count,
metrics.disk_read_bytes,
metrics.disk_write_bytes,
metrics.sys_net_rx_bytes,
metrics.sys_net_tx_bytes,
metrics.uptime_secs,
None,
true,
);
#[cfg(feature = "gpu")]
return append_gpu_suffix(base, metrics.gpu.as_ref(), true);
#[cfg(not(feature = "gpu"))]
base
}
fn format_aggregated_metrics(metrics: &AggregatedMetrics) -> String {
let base = format_base(
metrics.cpu_usage,
metrics.mem_rss_kb,
metrics.thread_count,
metrics.disk_read_bytes,
metrics.disk_write_bytes,
metrics.sys_net_rx_bytes,
metrics.sys_net_tx_bytes,
metrics.uptime_secs,
Some(metrics.process_count),
false,
);
#[cfg(feature = "gpu")]
return append_gpu_suffix(base, metrics.gpu.as_ref(), false);
#[cfg(not(feature = "gpu"))]
base
}
fn format_aggregated_metrics_compact(metrics: &AggregatedMetrics) -> String {
let base = format_base(
metrics.cpu_usage,
metrics.mem_rss_kb,
metrics.thread_count,
metrics.disk_read_bytes,
metrics.disk_write_bytes,
metrics.sys_net_rx_bytes,
metrics.sys_net_tx_bytes,
metrics.uptime_secs,
Some(metrics.process_count),
true,
);
#[cfg(feature = "gpu")]
return append_gpu_suffix(base, metrics.gpu.as_ref(), true);
#[cfg(not(feature = "gpu"))]
base
}
fn convert_aggregated_to_metrics(agg: &AggregatedMetrics) -> Metrics {
Metrics {
ts_ms: agg.ts_ms,
cpu_usage: agg.cpu_usage,
mem_rss_kb: agg.mem_rss_kb,
mem_vms_kb: agg.mem_vms_kb,
disk_read_bytes: agg.disk_read_bytes,
disk_write_bytes: agg.disk_write_bytes,
syscall_read_bytes: agg.syscall_read_bytes,
syscall_write_bytes: agg.syscall_write_bytes,
page_faults_cached: agg.page_faults_cached,
page_faults_disk: agg.page_faults_disk,
sys_net_rx_bytes: agg.sys_net_rx_bytes,
sys_net_tx_bytes: agg.sys_net_tx_bytes,
thread_count: agg.thread_count,
uptime_secs: agg.uptime_secs,
cpu_core: None,
gpu: agg.gpu.clone(),
}
}
fn format_bytes(bytes: u64) -> String {
if bytes < 1024 {
format!("{bytes}B")
} else if bytes < 1024 * 1024 {
format!("{:.1}KB", bytes as f64 / 1024.0)
} else if bytes < 1024 * 1024 * 1024 {
format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
} else {
format!("{:.1}GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
}
}
fn summary_rows(summary: &Summary) -> Vec<(&'static str, String)> {
#[allow(unused_mut)]
let mut rows = vec![
(
"Duration",
format!("{:.2} seconds", summary.total_time_secs),
),
("Samples", format!("{}", summary.sample_count)),
("Max Processes", format!("{}", summary.max_processes)),
("Max Threads", format!("{}", summary.max_threads)),
(
"Peak Memory",
format!("{} MB", (summary.peak_mem_rss_kb as f64 / 1024.0).round()),
),
("Avg CPU Usage", format!("{:.1}%", summary.avg_cpu_usage)),
("Disk Read", format_bytes(summary.total_disk_read_bytes)),
("Disk Write", format_bytes(summary.total_disk_write_bytes)),
(
"Network Received",
format_bytes(summary.total_sys_net_rx_bytes),
),
("Network Sent", format_bytes(summary.total_sys_net_tx_bytes)),
];
#[cfg(feature = "gpu")]
if let Some(ref gpu) = summary.gpu {
if gpu.enabled {
rows.push(("GPU Devices", format!("{}", gpu.device_count)));
rows.push((
"Peak GPU VRAM Used",
format!(
"{:.2} GB / {:.1} GB",
gpu.peak_used_memory_gb, gpu.total_memory_gb
),
));
rows.push((
"Peak GPU Utilization",
format!("{}%", gpu.max_system_gpu_utilization),
));
if let Some(proc_util) = gpu.max_process_gpu_utilization {
rows.push(("Peak Process GPU", format!("{}%", proc_util)));
}
if gpu.process_memory_usage_gb > 0.0 {
rows.push((
"Process GPU Memory",
format!("{:.2} GB", gpu.process_memory_usage_gb),
));
}
}
}
rows
}
fn print_summary_rows(summary: &Summary) {
let rows = summary_rows(summary);
let mut builder = Builder::default();
for (key, val) in &rows {
builder.push_record([key, val.as_str()]);
}
let mut table = builder.build();
table.with(Style::blank());
println!("{table}");
}
fn print_summary(metrics: &[Metrics], duration: f64) {
let summary = Summary::from_metrics(metrics, duration);
println!("\n{}", "EXECUTION SUMMARY".cyan().bold());
print_summary_rows(&summary);
}
fn generate_summary_from_file(
file_path: &PathBuf,
json_output: bool,
out_file: Option<&PathBuf>,
) -> Result<()> {
if !json_output {
println!("Generating statistics from file: {}", file_path.display());
}
match SummaryGenerator::from_json_file(file_path) {
Ok(summary) => {
if json_output {
let json = serde_json::to_string_pretty(&summary)?;
if let Some(out_path) = out_file {
let mut file = File::create(out_path)?;
writeln!(file, "{json}")?;
} else {
println!("{json}");
}
} else {
if let Some(out_path) = out_file {
let mut file = File::create(out_path)?;
writeln!(file, "\n{}", "FILE STATISTICS".bold())?;
writeln!(file, "{}", "===============".bold())?;
writeln!(file, "Duration: {:.2} seconds", summary.total_time_secs)?;
writeln!(file, "Samples: {}", summary.sample_count)?;
writeln!(file, "Max processes: {}", summary.max_processes)?;
writeln!(file, "Max threads: {}", summary.max_threads)?;
writeln!(
file,
"Peak memory usage: {} MB",
(summary.peak_mem_rss_kb as f64 / 1024.0).round()
)?;
writeln!(file, "Average CPU usage: {:.1}%", summary.avg_cpu_usage)?;
writeln!(
file,
"Total disk read: {}",
format_bytes(summary.total_disk_read_bytes)
)?;
writeln!(
file,
"Total disk write: {}",
format_bytes(summary.total_disk_write_bytes)
)?;
writeln!(
file,
"Total network received: {}",
format_bytes(summary.total_sys_net_rx_bytes)
)?;
writeln!(
file,
"Total network sent: {}",
format_bytes(summary.total_sys_net_tx_bytes)
)?;
} else {
println!("\n{}", "FILE STATISTICS".cyan().bold());
print_summary_rows(&summary);
}
}
Ok(())
}
Err(e) => {
eprintln!("Error processing metrics file: {e}");
Err(e)
}
}
}