use crate::cluster::ClusterRegistry;
use crate::unified_trace::SyscallSpan;
use std::collections::HashMap;
use std::fmt;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct TimeAttribution {
pub cluster: String,
pub total_time: Duration,
pub call_count: usize,
pub percentage: f64,
pub avg_per_call: Duration,
}
impl fmt::Display for TimeAttribution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {:.2}% ({} calls, avg {:?}/call, total {:?})",
self.cluster, self.percentage, self.call_count, self.avg_per_call, self.total_time
)
}
}
pub fn calculate_time_attribution(
spans: &[SyscallSpan],
registry: &ClusterRegistry,
) -> Vec<TimeAttribution> {
if spans.is_empty() {
return Vec::new();
}
let total_time_nanos: u64 = spans.iter().map(|s| s.duration_nanos).sum();
if total_time_nanos == 0 {
return Vec::new(); }
let mut cluster_time: HashMap<String, u64> = HashMap::new();
let mut cluster_count: HashMap<String, usize> = HashMap::new();
for span in spans {
let args: Vec<String> = span.args.iter().map(|(_, v)| v.clone()).collect();
let cluster_name =
registry.classify_simple(&span.name, &args).unwrap_or("Unclassified".to_string());
*cluster_time.entry(cluster_name.clone()).or_default() += span.duration_nanos;
*cluster_count.entry(cluster_name).or_default() += 1;
}
let mut attributions: Vec<TimeAttribution> = cluster_time
.into_iter()
.map(|(cluster, time_nanos)| {
let count = cluster_count[&cluster];
let total_time_cluster = Duration::from_nanos(time_nanos);
let percentage = (time_nanos as f64 / total_time_nanos as f64) * 100.0;
let avg_per_call = Duration::from_nanos(time_nanos / count as u64);
TimeAttribution {
cluster,
total_time: total_time_cluster,
call_count: count,
percentage,
avg_per_call,
}
})
.collect();
attributions.sort_by(|a, b| b.total_time.cmp(&a.total_time));
attributions
}
#[cfg(test)]
mod tests {
use super::*;
use std::borrow::Cow;
fn make_span(name: &'static str, duration_nanos: u64) -> SyscallSpan {
SyscallSpan {
span_id: 1,
parent_span_id: 0,
name: Cow::Borrowed(name),
args: vec![],
return_value: 0,
timestamp_nanos: 0,
duration_nanos,
errno: None,
}
}
#[test]
fn test_time_attribution_basic() {
let registry = ClusterRegistry::default_transpiler_clusters().expect("test");
let spans = vec![
make_span("mmap", 1000), make_span("read", 9000), make_span("write", 1000), ];
let attributions = calculate_time_attribution(&spans, ®istry);
assert_eq!(attributions.len(), 2);
let file_io = attributions.iter().find(|a| a.cluster == "FileIO").expect("test");
assert!((file_io.percentage - 90.9).abs() < 0.1); assert_eq!(file_io.call_count, 2);
}
#[test]
fn test_time_attribution_sorted() {
let registry = ClusterRegistry::default_transpiler_clusters().expect("test");
let spans = vec![
make_span("mmap", 1000), make_span("read", 9000), make_span("write", 5000), ];
let attributions = calculate_time_attribution(&spans, ®istry);
assert!(attributions[0].total_time >= attributions[1].total_time);
}
#[test]
fn test_time_attribution_empty() {
let registry = ClusterRegistry::default_transpiler_clusters().expect("test");
let spans: Vec<SyscallSpan> = vec![];
let attributions = calculate_time_attribution(&spans, ®istry);
assert!(attributions.is_empty());
}
#[test]
fn test_time_attribution_zero_duration() {
let registry = ClusterRegistry::default_transpiler_clusters().expect("test");
let spans = vec![make_span("mmap", 0), make_span("read", 0)];
let attributions = calculate_time_attribution(&spans, ®istry);
assert!(attributions.is_empty()); }
#[test]
fn test_avg_per_call() {
let registry = ClusterRegistry::default_transpiler_clusters().expect("test");
let spans = vec![
make_span("read", 1000), make_span("read", 3000), make_span("write", 2000), ];
let attributions = calculate_time_attribution(&spans, ®istry);
let file_io = attributions.iter().find(|a| a.cluster == "FileIO").expect("test");
assert_eq!(file_io.avg_per_call, Duration::from_nanos(2000));
}
}