profile_inspect/analysis/
diff.rs1use std::collections::HashMap;
2
3use crate::ir::ProfileIR;
4
5use super::{CpuAnalyzer, FunctionStats};
6
7#[derive(Debug, Clone)]
9pub struct FunctionDelta {
10 pub name: String,
12 pub location: String,
14 pub before_time: u64,
16 pub after_time: u64,
18 pub delta_time: i64,
20 pub delta_percent: f64,
22}
23
24impl FunctionDelta {
25 pub fn is_regression(&self) -> bool {
27 self.delta_time > 0
28 }
29
30 pub fn is_improvement(&self) -> bool {
32 self.delta_time < 0
33 }
34}
35
36#[derive(Debug)]
38pub struct ProfileDiff {
39 pub before_total: u64,
41 pub after_total: u64,
43 pub overall_delta_percent: f64,
45 pub regressions: Vec<FunctionDelta>,
47 pub improvements: Vec<FunctionDelta>,
49 pub new_functions: Vec<FunctionStats>,
51 pub removed_functions: Vec<FunctionStats>,
53}
54
55pub struct ProfileDiffer {
57 min_delta_percent: f64,
59 top_n: usize,
61}
62
63impl ProfileDiffer {
64 pub fn new() -> Self {
66 Self {
67 min_delta_percent: 1.0, top_n: 20,
69 }
70 }
71
72 pub fn min_delta_percent(mut self, percent: f64) -> Self {
74 self.min_delta_percent = percent;
75 self
76 }
77
78 pub fn top_n(mut self, n: usize) -> Self {
80 self.top_n = n;
81 self
82 }
83
84 #[expect(clippy::cast_precision_loss)]
86 pub fn diff(&self, before: &ProfileIR, after: &ProfileIR) -> ProfileDiff {
87 let analyzer = CpuAnalyzer::new().include_internals(true).top_n(1000);
88
89 let before_analysis = analyzer.analyze(before);
90 let after_analysis = analyzer.analyze(after);
91
92 let before_total = before_analysis.total_time;
93 let after_total = after_analysis.total_time;
94
95 let overall_delta_percent = if before_total > 0 {
96 ((after_total as f64 - before_total as f64) / before_total as f64) * 100.0
97 } else {
98 0.0
99 };
100
101 let before_map: HashMap<String, &FunctionStats> = before_analysis
103 .functions
104 .iter()
105 .map(|f| (format!("{}@{}", f.name, f.location), f))
106 .collect();
107
108 let after_map: HashMap<String, &FunctionStats> = after_analysis
109 .functions
110 .iter()
111 .map(|f| (format!("{}@{}", f.name, f.location), f))
112 .collect();
113
114 let mut regressions = Vec::new();
115 let mut improvements = Vec::new();
116 let mut new_functions = Vec::new();
117 let mut removed_functions = Vec::new();
118
119 for (key, after_stats) in &after_map {
121 if let Some(before_stats) = before_map.get(key) {
122 let delta_time = after_stats.self_time as i64 - before_stats.self_time as i64;
124 let delta_percent = if before_stats.self_time > 0 {
125 (delta_time as f64 / before_stats.self_time as f64) * 100.0
126 } else if after_stats.self_time > 0 {
127 100.0 } else {
129 0.0
130 };
131
132 if delta_percent.abs() >= self.min_delta_percent {
133 let delta = FunctionDelta {
134 name: after_stats.name.clone(),
135 location: after_stats.location.clone(),
136 before_time: before_stats.self_time,
137 after_time: after_stats.self_time,
138 delta_time,
139 delta_percent,
140 };
141
142 if delta.is_regression() {
143 regressions.push(delta);
144 } else if delta.is_improvement() {
145 improvements.push(delta);
146 }
147 }
148 } else {
149 new_functions.push((*after_stats).clone());
151 }
152 }
153
154 for (key, before_stats) in &before_map {
156 if !after_map.contains_key(key) {
157 removed_functions.push((*before_stats).clone());
158 }
159 }
160
161 regressions.sort_by(|a, b| {
163 b.delta_time
164 .abs()
165 .partial_cmp(&a.delta_time.abs())
166 .unwrap_or(std::cmp::Ordering::Equal)
167 });
168 improvements.sort_by(|a, b| {
169 b.delta_time
170 .abs()
171 .partial_cmp(&a.delta_time.abs())
172 .unwrap_or(std::cmp::Ordering::Equal)
173 });
174 new_functions.sort_by(|a, b| b.self_time.cmp(&a.self_time));
175 removed_functions.sort_by(|a, b| b.self_time.cmp(&a.self_time));
176
177 regressions.truncate(self.top_n);
178 improvements.truncate(self.top_n);
179 new_functions.truncate(self.top_n);
180 removed_functions.truncate(self.top_n);
181
182 ProfileDiff {
183 before_total,
184 after_total,
185 overall_delta_percent,
186 regressions,
187 improvements,
188 new_functions,
189 removed_functions,
190 }
191 }
192}
193
194impl Default for ProfileDiffer {
195 fn default() -> Self {
196 Self::new()
197 }
198}