use std::collections::HashMap;
use crate::ir::ProfileIR;
use super::{CpuAnalyzer, FunctionStats};
#[derive(Debug, Clone)]
pub struct FunctionDelta {
pub name: String,
pub location: String,
pub before_time: u64,
pub after_time: u64,
pub delta_time: i64,
pub delta_percent: f64,
}
impl FunctionDelta {
pub fn is_regression(&self) -> bool {
self.delta_time > 0
}
pub fn is_improvement(&self) -> bool {
self.delta_time < 0
}
}
#[derive(Debug)]
pub struct ProfileDiff {
pub before_total: u64,
pub after_total: u64,
pub overall_delta_percent: f64,
pub regressions: Vec<FunctionDelta>,
pub improvements: Vec<FunctionDelta>,
pub new_functions: Vec<FunctionStats>,
pub removed_functions: Vec<FunctionStats>,
}
pub struct ProfileDiffer {
min_delta_percent: f64,
top_n: usize,
}
impl ProfileDiffer {
pub fn new() -> Self {
Self {
min_delta_percent: 1.0, top_n: 20,
}
}
pub fn min_delta_percent(mut self, percent: f64) -> Self {
self.min_delta_percent = percent;
self
}
pub fn top_n(mut self, n: usize) -> Self {
self.top_n = n;
self
}
#[expect(clippy::cast_precision_loss)]
pub fn diff(&self, before: &ProfileIR, after: &ProfileIR) -> ProfileDiff {
let analyzer = CpuAnalyzer::new().include_internals(true).top_n(1000);
let before_analysis = analyzer.analyze(before);
let after_analysis = analyzer.analyze(after);
let before_total = before_analysis.total_time;
let after_total = after_analysis.total_time;
let overall_delta_percent = if before_total > 0 {
((after_total as f64 - before_total as f64) / before_total as f64) * 100.0
} else {
0.0
};
let before_map: HashMap<String, &FunctionStats> = before_analysis
.functions
.iter()
.map(|f| (format!("{}@{}", f.name, f.location), f))
.collect();
let after_map: HashMap<String, &FunctionStats> = after_analysis
.functions
.iter()
.map(|f| (format!("{}@{}", f.name, f.location), f))
.collect();
let mut regressions = Vec::new();
let mut improvements = Vec::new();
let mut new_functions = Vec::new();
let mut removed_functions = Vec::new();
for (key, after_stats) in &after_map {
if let Some(before_stats) = before_map.get(key) {
let delta_time = after_stats.self_time as i64 - before_stats.self_time as i64;
let delta_percent = if before_stats.self_time > 0 {
(delta_time as f64 / before_stats.self_time as f64) * 100.0
} else if after_stats.self_time > 0 {
100.0 } else {
0.0
};
if delta_percent.abs() >= self.min_delta_percent {
let delta = FunctionDelta {
name: after_stats.name.clone(),
location: after_stats.location.clone(),
before_time: before_stats.self_time,
after_time: after_stats.self_time,
delta_time,
delta_percent,
};
if delta.is_regression() {
regressions.push(delta);
} else if delta.is_improvement() {
improvements.push(delta);
}
}
} else {
new_functions.push((*after_stats).clone());
}
}
for (key, before_stats) in &before_map {
if !after_map.contains_key(key) {
removed_functions.push((*before_stats).clone());
}
}
regressions.sort_by(|a, b| {
b.delta_time
.abs()
.partial_cmp(&a.delta_time.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
improvements.sort_by(|a, b| {
b.delta_time
.abs()
.partial_cmp(&a.delta_time.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
new_functions.sort_by(|a, b| b.self_time.cmp(&a.self_time));
removed_functions.sort_by(|a, b| b.self_time.cmp(&a.self_time));
regressions.truncate(self.top_n);
improvements.truncate(self.top_n);
new_functions.truncate(self.top_n);
removed_functions.truncate(self.top_n);
ProfileDiff {
before_total,
after_total,
overall_delta_percent,
regressions,
improvements,
new_functions,
removed_functions,
}
}
}
impl Default for ProfileDiffer {
fn default() -> Self {
Self::new()
}
}