use std::collections::HashMap;
const IO_SYSCALLS: &[&str] = &[
"read",
"write",
"readv",
"writev",
"pread64",
"pwrite64",
"openat",
"open",
"close",
"fsync",
"fdatasync",
"sync",
"sendfile",
"splice",
"tee",
"vmsplice",
];
const SLOW_IO_THRESHOLD_US: u64 = 1000;
#[derive(Debug, Clone, Default)]
pub struct FunctionStats {
pub syscall_count: u64,
pub total_time_us: u64,
pub callees: HashMap<String, u64>,
pub io_syscalls: u64,
pub slow_io_count: u64,
}
#[derive(Debug, Default)]
pub struct FunctionProfiler {
stats: HashMap<String, FunctionStats>,
}
impl FunctionProfiler {
pub fn new() -> Self {
Self::default()
}
pub fn record(
&mut self,
function_name: &str,
syscall_name: &str,
duration_us: u64,
caller_name: Option<&str>,
) {
let entry = self.stats.entry(function_name.to_string()).or_default();
entry.syscall_count += 1;
entry.total_time_us += duration_us;
if IO_SYSCALLS.contains(&syscall_name) {
entry.io_syscalls += 1;
if duration_us > SLOW_IO_THRESHOLD_US {
entry.slow_io_count += 1;
}
}
if let Some(caller) = caller_name {
let caller_entry = self.stats.entry(caller.to_string()).or_default();
*caller_entry.callees.entry(function_name.to_string()).or_insert(0) += 1;
}
}
pub fn export_flamegraph<W: std::io::Write>(&self, mut writer: W) -> std::io::Result<()> {
for (function, stats) in &self.stats {
if !self.has_caller(function) {
writeln!(writer, "{} {}", function, stats.syscall_count)?;
}
for (callee, count) in &stats.callees {
writeln!(writer, "{function};{callee} {count}")?;
}
}
Ok(())
}
fn has_caller(&self, function: &str) -> bool {
self.stats.values().any(|stats| stats.callees.contains_key(function))
}
pub fn print_summary(&self) {
if self.stats.is_empty() {
eprintln!("\nNo function profiling data collected.");
return;
}
let mut sorted: Vec<_> = self.stats.iter().collect();
sorted.sort_by(|a, b| b.1.total_time_us.cmp(&a.1.total_time_us));
eprintln!("\n╔════════════════════════════════════════════════════════════════════════════════════════════════════╗");
eprintln!("║ Function Timing Summary (sorted by total time) ║");
eprintln!("╚════════════════════════════════════════════════════════════════════════════════════════════════════╝");
eprintln!();
eprintln!(
"{:<40} {:>10} {:>12} {:>12} {:>10} {:>10}",
"Function", "Calls", "Total Time", "Avg Time", "I/O Ops", "Slow I/O"
);
eprintln!("{}", "─".repeat(104));
for (function, stats) in &sorted {
let total_seconds = stats.total_time_us as f64 / 1_000_000.0;
let avg_us =
if stats.syscall_count > 0 { stats.total_time_us / stats.syscall_count } else { 0 };
let avg_seconds = avg_us as f64 / 1_000_000.0;
let marker = if stats.slow_io_count > 0 { "⚠️ " } else { " " };
eprintln!(
"{}{:<37} {:>10} {:>11.6}s {:>11.6}s {:>10} {:>10}",
marker,
function,
stats.syscall_count,
total_seconds,
avg_seconds,
stats.io_syscalls,
stats.slow_io_count
);
}
eprintln!("{}", "─".repeat(104));
if sorted.len() > 1 {
eprintln!();
eprintln!("╔════════════════════════════════════════════════════════════════════════════════════════════════════╗");
eprintln!("║ Hot Path Analysis (top 10 most active functions) ║");
eprintln!("╚════════════════════════════════════════════════════════════════════════════════════════════════════╝");
eprintln!();
let hot_functions = sorted.iter().take(10);
for (rank, (function, stats)) in hot_functions.enumerate() {
let total_seconds = stats.total_time_us as f64 / 1_000_000.0;
let percent = if self.stats.values().map(|s| s.total_time_us).sum::<u64>() > 0 {
(stats.total_time_us as f64
/ self.stats.values().map(|s| s.total_time_us).sum::<u64>() as f64)
* 100.0
} else {
0.0
};
eprintln!(
"{}. {} - {:.2}% of total time ({:.6}s, {} syscalls)",
rank + 1,
function,
percent,
total_seconds,
stats.syscall_count
);
if !stats.callees.is_empty() {
let mut callees: Vec<_> = stats.callees.iter().collect();
callees.sort_by(|a, b| b.1.cmp(a.1));
for (callee, count) in callees.iter().take(5) {
eprintln!(
" └─> {} ({} call{})",
callee,
count,
if **count == 1 { "" } else { "s" }
);
}
}
eprintln!();
}
}
let has_call_graph = sorted.iter().any(|(_, stats)| !stats.callees.is_empty());
if has_call_graph {
eprintln!("╔════════════════════════════════════════════════════════════════════════════════════════════════════╗");
eprintln!("║ Call Graph (parent → child relationships) ║");
eprintln!("╚════════════════════════════════════════════════════════════════════════════════════════════════════╝");
eprintln!();
for (function, stats) in &sorted {
if !stats.callees.is_empty() {
eprintln!("{function} calls:");
let mut callees: Vec<_> = stats.callees.iter().collect();
callees.sort_by(|a, b| b.1.cmp(a.1));
for (callee, count) in callees {
eprintln!(
" └─> {} ({} call{})",
callee,
count,
if *count == 1 { "" } else { "s" }
);
}
eprintln!();
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_function_profiler_new() {
let profiler = FunctionProfiler::new();
assert!(profiler.stats.is_empty());
}
#[test]
fn test_function_profiler_record() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("main", "read", 2000, None);
profiler.record("helper", "open", 500, None);
assert_eq!(profiler.stats.len(), 2);
assert_eq!(profiler.stats.get("main").expect("test").syscall_count, 2);
assert_eq!(profiler.stats.get("main").expect("test").total_time_us, 3000);
assert_eq!(profiler.stats.get("helper").expect("test").syscall_count, 1);
assert_eq!(profiler.stats.get("helper").expect("test").total_time_us, 500);
}
#[test]
fn test_function_profiler_empty_summary() {
let profiler = FunctionProfiler::new();
profiler.print_summary();
}
#[test]
fn test_function_stats_default() {
let stats = FunctionStats::default();
assert_eq!(stats.syscall_count, 0);
assert_eq!(stats.total_time_us, 0);
}
#[test]
fn test_function_profiler_print_summary_with_data() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000000, None); profiler.record("main", "read", 500000, None); profiler.record("helper", "open", 250000, None); profiler.record("foo", "close", 100000, None);
profiler.print_summary();
assert_eq!(profiler.stats.len(), 3);
}
#[test]
fn test_function_profiler_sorting_by_total_time() {
let mut profiler = FunctionProfiler::new();
profiler.record("slow_func", "write", 5000000, None); profiler.record("fast_func", "brk", 100000, None); profiler.record("medium_func", "read", 1000000, None);
profiler.print_summary();
assert_eq!(profiler.stats.get("slow_func").expect("test").total_time_us, 5000000);
assert_eq!(profiler.stats.get("fast_func").expect("test").total_time_us, 100000);
assert_eq!(profiler.stats.get("medium_func").expect("test").total_time_us, 1000000);
}
#[test]
fn test_function_profiler_average_calculation() {
let mut profiler = FunctionProfiler::new();
profiler.record("test_func", "write", 1000, None);
profiler.record("test_func", "read", 2000, None);
profiler.record("test_func", "open", 3000, None);
let stats = profiler.stats.get("test_func").expect("test");
assert_eq!(stats.syscall_count, 3);
assert_eq!(stats.total_time_us, 6000);
let avg = stats.total_time_us / stats.syscall_count;
assert_eq!(avg, 2000);
profiler.print_summary();
}
#[test]
fn test_function_profiler_zero_syscalls_edge_case() {
let mut profiler = FunctionProfiler::new();
profiler.stats.insert(
"never_called".to_string(),
FunctionStats {
syscall_count: 0,
total_time_us: 0,
callees: HashMap::new(),
io_syscalls: 0,
slow_io_count: 0,
},
);
profiler.print_summary();
let stats = profiler.stats.get("never_called").expect("test");
assert_eq!(stats.syscall_count, 0);
}
#[test]
fn test_io_syscall_tracking() {
let mut profiler = FunctionProfiler::new();
profiler.record("io_func", "read", 500, None);
profiler.record("io_func", "write", 600, None);
profiler.record("io_func", "open", 700, None);
profiler.record("io_func", "close", 400, None);
let stats = profiler.stats.get("io_func").expect("test");
assert_eq!(stats.io_syscalls, 4);
assert_eq!(stats.syscall_count, 4);
assert_eq!(stats.slow_io_count, 0); }
#[test]
fn test_non_io_syscalls_not_tracked() {
let mut profiler = FunctionProfiler::new();
profiler.record("compute_func", "brk", 500, None);
profiler.record("compute_func", "mmap", 600, None);
profiler.record("compute_func", "getpid", 100, None);
let stats = profiler.stats.get("compute_func").expect("test");
assert_eq!(stats.io_syscalls, 0);
assert_eq!(stats.syscall_count, 3);
assert_eq!(stats.slow_io_count, 0);
}
#[test]
fn test_slow_io_detection() {
let mut profiler = FunctionProfiler::new();
profiler.record("slow_io_func", "read", 2000, None); profiler.record("slow_io_func", "write", 5000, None); profiler.record("slow_io_func", "fsync", 10000, None);
let stats = profiler.stats.get("slow_io_func").expect("test");
assert_eq!(stats.io_syscalls, 3);
assert_eq!(stats.slow_io_count, 3);
assert_eq!(stats.syscall_count, 3);
}
#[test]
fn test_fast_io_not_marked_slow() {
let mut profiler = FunctionProfiler::new();
profiler.record("fast_io_func", "read", 100, None); profiler.record("fast_io_func", "write", 500, None); profiler.record("fast_io_func", "close", 50, None);
let stats = profiler.stats.get("fast_io_func").expect("test");
assert_eq!(stats.io_syscalls, 3);
assert_eq!(stats.slow_io_count, 0); assert_eq!(stats.syscall_count, 3);
}
#[test]
fn test_mixed_io_and_non_io_operations() {
let mut profiler = FunctionProfiler::new();
profiler.record("mixed_func", "read", 1500, None); profiler.record("mixed_func", "brk", 100, None); profiler.record("mixed_func", "write", 500, None); profiler.record("mixed_func", "mmap", 200, None); profiler.record("mixed_func", "fsync", 3000, None);
let stats = profiler.stats.get("mixed_func").expect("test");
assert_eq!(stats.syscall_count, 5);
assert_eq!(stats.io_syscalls, 3); assert_eq!(stats.slow_io_count, 2); assert_eq!(stats.total_time_us, 5300);
}
#[test]
fn test_slow_io_threshold_boundary() {
let mut profiler = FunctionProfiler::new();
profiler.record("boundary_func", "read", 1000, None); profiler.record("boundary_func", "write", 999, None); profiler.record("boundary_func", "open", 1001, None);
let stats = profiler.stats.get("boundary_func").expect("test");
assert_eq!(stats.io_syscalls, 3);
assert_eq!(stats.slow_io_count, 1); }
#[test]
fn test_all_io_syscall_types() {
let mut profiler = FunctionProfiler::new();
profiler.record("io_types", "read", 100, None);
profiler.record("io_types", "write", 100, None);
profiler.record("io_types", "readv", 100, None);
profiler.record("io_types", "writev", 100, None);
profiler.record("io_types", "pread64", 100, None);
profiler.record("io_types", "pwrite64", 100, None);
profiler.record("io_types", "openat", 100, None);
profiler.record("io_types", "open", 100, None);
profiler.record("io_types", "close", 100, None);
profiler.record("io_types", "fsync", 100, None);
profiler.record("io_types", "fdatasync", 100, None);
profiler.record("io_types", "sync", 100, None);
profiler.record("io_types", "sendfile", 100, None);
profiler.record("io_types", "splice", 100, None);
profiler.record("io_types", "tee", 100, None);
profiler.record("io_types", "vmsplice", 100, None);
let stats = profiler.stats.get("io_types").expect("test");
assert_eq!(stats.io_syscalls, 16); assert_eq!(stats.syscall_count, 16);
assert_eq!(stats.slow_io_count, 0); }
#[test]
fn test_call_graph_single_relationship() {
let mut profiler = FunctionProfiler::new();
profiler.record("helper", "write", 1000, Some("main"));
let main_stats = profiler.stats.get("main").expect("test");
assert_eq!(main_stats.callees.len(), 1);
assert_eq!(*main_stats.callees.get("helper").expect("test"), 1);
}
#[test]
fn test_call_graph_multiple_calls() {
let mut profiler = FunctionProfiler::new();
profiler.record("helper", "write", 1000, Some("main"));
profiler.record("helper", "read", 2000, Some("main"));
profiler.record("helper", "open", 500, Some("main"));
let main_stats = profiler.stats.get("main").expect("test");
assert_eq!(main_stats.callees.len(), 1);
assert_eq!(*main_stats.callees.get("helper").expect("test"), 3);
}
#[test]
fn test_call_graph_multiple_callees() {
let mut profiler = FunctionProfiler::new();
profiler.record("helper_a", "write", 1000, Some("main"));
profiler.record("helper_b", "read", 2000, Some("main"));
profiler.record("helper_c", "open", 500, Some("main"));
let main_stats = profiler.stats.get("main").expect("test");
assert_eq!(main_stats.callees.len(), 3);
assert_eq!(*main_stats.callees.get("helper_a").expect("test"), 1);
assert_eq!(*main_stats.callees.get("helper_b").expect("test"), 1);
assert_eq!(*main_stats.callees.get("helper_c").expect("test"), 1);
}
#[test]
fn test_call_graph_nested_calls() {
let mut profiler = FunctionProfiler::new();
profiler.record("helper_a", "write", 1000, Some("main"));
profiler.record("helper_b", "read", 2000, Some("helper_a"));
let main_stats = profiler.stats.get("main").expect("test");
assert_eq!(main_stats.callees.len(), 1);
assert_eq!(*main_stats.callees.get("helper_a").expect("test"), 1);
let helper_a_stats = profiler.stats.get("helper_a").expect("test");
assert_eq!(helper_a_stats.callees.len(), 1);
assert_eq!(*helper_a_stats.callees.get("helper_b").expect("test"), 1);
}
#[test]
fn test_call_graph_with_no_caller() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
let main_stats = profiler.stats.get("main").expect("test");
assert_eq!(main_stats.callees.len(), 0);
assert_eq!(main_stats.syscall_count, 1);
}
#[test]
fn test_call_graph_mixed_with_without_caller() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("helper", "read", 2000, Some("main"));
profiler.record("helper", "open", 500, None);
let main_stats = profiler.stats.get("main").expect("test");
assert_eq!(main_stats.syscall_count, 1);
assert_eq!(main_stats.callees.len(), 1);
assert_eq!(*main_stats.callees.get("helper").expect("test"), 1);
let helper_stats = profiler.stats.get("helper").expect("test");
assert_eq!(helper_stats.syscall_count, 2);
assert_eq!(helper_stats.callees.len(), 0);
}
#[test]
fn test_hot_path_analysis_sorting() {
let mut profiler = FunctionProfiler::new();
profiler.record("func_slow", "write", 5000000, None); profiler.record("func_medium", "read", 1000000, None); profiler.record("func_fast", "open", 100000, None); profiler.record("func_very_slow", "fsync", 10000000, None); profiler.record("func_quick", "close", 50000, None);
profiler.print_summary();
let mut sorted: Vec<_> = profiler.stats.iter().collect();
sorted.sort_by(|a, b| b.1.total_time_us.cmp(&a.1.total_time_us));
assert_eq!(sorted[0].0, "func_very_slow");
assert_eq!(sorted[1].0, "func_slow");
assert_eq!(sorted[2].0, "func_medium");
}
#[test]
fn test_hot_path_analysis_with_few_functions() {
let mut profiler = FunctionProfiler::new();
profiler.record("func_a", "write", 3000000, None);
profiler.record("func_b", "read", 2000000, None);
profiler.record("func_c", "open", 1000000, None);
profiler.print_summary();
assert_eq!(profiler.stats.len(), 3);
}
#[test]
fn test_hot_path_analysis_with_call_graph() {
let mut profiler = FunctionProfiler::new();
profiler.record("hot_main", "write", 5000000, None);
profiler.record("helper_a", "read", 1000000, Some("hot_main"));
profiler.record("helper_b", "open", 500000, Some("hot_main"));
profiler.record("helper_c", "close", 250000, Some("hot_main"));
profiler.print_summary();
let hot_main_stats = profiler.stats.get("hot_main").expect("test");
assert_eq!(hot_main_stats.callees.len(), 3);
assert_eq!(hot_main_stats.total_time_us, 5000000);
}
#[test]
fn test_hot_path_analysis_percentage_calculation() {
let mut profiler = FunctionProfiler::new();
profiler.record("func_50", "write", 5000000, None); profiler.record("func_30", "read", 3000000, None); profiler.record("func_20", "open", 2000000, None);
let total: u64 = profiler.stats.values().map(|s| s.total_time_us).sum();
assert_eq!(total, 10000000);
let func_50_stats = profiler.stats.get("func_50").expect("test");
let percent_50 = (func_50_stats.total_time_us as f64 / total as f64) * 100.0;
assert!((percent_50 - 50.0).abs() < 0.01);
profiler.print_summary();
}
#[test]
fn test_hot_path_analysis_more_than_10_functions() {
let mut profiler = FunctionProfiler::new();
for i in 0..15 {
let time = (15 - i) * 100000; profiler.record(&format!("func_{}", i), "write", time, None);
}
assert_eq!(profiler.stats.len(), 15);
profiler.print_summary();
}
#[test]
fn test_flamegraph_export_simple() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
let mut output = Vec::new();
profiler.export_flamegraph(&mut output).expect("test");
let flamegraph = String::from_utf8(output).expect("test");
assert!(flamegraph.contains("main 1"));
}
#[test]
fn test_flamegraph_export_with_call_graph() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("helper", "read", 2000, Some("main"));
let mut output = Vec::new();
profiler.export_flamegraph(&mut output).expect("test");
let flamegraph = String::from_utf8(output).expect("test");
assert!(flamegraph.contains("main 1"));
assert!(flamegraph.contains("main;helper 1"));
}
#[test]
fn test_flamegraph_export_multiple_callees() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("helper_a", "read", 2000, Some("main"));
profiler.record("helper_b", "open", 3000, Some("main"));
profiler.record("helper_c", "close", 4000, Some("main"));
let mut output = Vec::new();
profiler.export_flamegraph(&mut output).expect("test");
let flamegraph = String::from_utf8(output).expect("test");
assert!(flamegraph.contains("main;helper_a 1"));
assert!(flamegraph.contains("main;helper_b 1"));
assert!(flamegraph.contains("main;helper_c 1"));
}
#[test]
fn test_flamegraph_export_nested_calls() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("helper_a", "read", 2000, Some("main"));
profiler.record("helper_b", "open", 3000, Some("helper_a"));
let mut output = Vec::new();
profiler.export_flamegraph(&mut output).expect("test");
let flamegraph = String::from_utf8(output).expect("test");
assert!(flamegraph.contains("main 1"));
assert!(flamegraph.contains("main;helper_a 1"));
assert!(flamegraph.contains("helper_a;helper_b 1"));
}
#[test]
fn test_flamegraph_export_multiple_calls_same_function() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("helper", "read", 1000, Some("main"));
profiler.record("helper", "read", 2000, Some("main"));
profiler.record("helper", "open", 3000, Some("main"));
let mut output = Vec::new();
profiler.export_flamegraph(&mut output).expect("test");
let flamegraph = String::from_utf8(output).expect("test");
assert!(flamegraph.contains("main;helper 3"));
}
#[test]
fn test_flamegraph_export_empty() {
let profiler = FunctionProfiler::new();
let mut output = Vec::new();
profiler.export_flamegraph(&mut output).expect("test");
let flamegraph = String::from_utf8(output).expect("test");
assert!(flamegraph.is_empty());
}
#[test]
fn test_flamegraph_export_multiple_roots() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("worker_thread", "read", 2000, None);
profiler.record("signal_handler", "open", 3000, None);
let mut output = Vec::new();
profiler.export_flamegraph(&mut output).expect("test");
let flamegraph = String::from_utf8(output).expect("test");
assert!(flamegraph.contains("main 1"));
assert!(flamegraph.contains("worker_thread 1"));
assert!(flamegraph.contains("signal_handler 1"));
}
#[test]
fn test_has_caller_helper() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("helper", "read", 2000, Some("main"));
assert!(!profiler.has_caller("main"));
assert!(profiler.has_caller("helper"));
}
#[test]
fn test_flamegraph_format_correctness() {
let mut profiler = FunctionProfiler::new();
profiler.record("main", "write", 1000, None);
profiler.record("helper", "read", 2000, Some("main"));
let mut output = Vec::new();
profiler.export_flamegraph(&mut output).expect("test");
let flamegraph = String::from_utf8(output).expect("test");
let lines: Vec<&str> = flamegraph.lines().collect();
for line in lines {
let parts: Vec<&str> = line.split_whitespace().collect();
assert!(parts.len() >= 2, "Line should have stack and count: {}", line);
let count_str = parts.last().expect("test");
assert!(count_str.parse::<u64>().is_ok(), "Count should be a number: {}", count_str);
}
}
}