Skip to main content

profile_inspect/analysis/
diff.rs

1use std::collections::HashMap;
2
3use crate::ir::ProfileIR;
4
5use super::{CpuAnalyzer, FunctionStats};
6
7/// Delta between two profile measurements for a function
8#[derive(Debug, Clone)]
9pub struct FunctionDelta {
10    /// Function name
11    pub name: String,
12    /// Source location
13    pub location: String,
14    /// Time in the "before" profile (microseconds)
15    pub before_time: u64,
16    /// Time in the "after" profile (microseconds)
17    pub after_time: u64,
18    /// Absolute change in time
19    pub delta_time: i64,
20    /// Percentage change (positive = regression, negative = improvement)
21    pub delta_percent: f64,
22}
23
24impl FunctionDelta {
25    /// Check if this is a regression (got slower)
26    pub fn is_regression(&self) -> bool {
27        self.delta_time > 0
28    }
29
30    /// Check if this is an improvement (got faster)
31    pub fn is_improvement(&self) -> bool {
32        self.delta_time < 0
33    }
34}
35
36/// Result of comparing two profiles
37#[derive(Debug)]
38pub struct ProfileDiff {
39    /// Total time in "before" profile
40    pub before_total: u64,
41    /// Total time in "after" profile
42    pub after_total: u64,
43    /// Overall change percentage
44    pub overall_delta_percent: f64,
45    /// Functions that got slower
46    pub regressions: Vec<FunctionDelta>,
47    /// Functions that got faster
48    pub improvements: Vec<FunctionDelta>,
49    /// New hotspots (functions that didn't exist in "before")
50    pub new_functions: Vec<FunctionStats>,
51    /// Removed functions (functions that don't exist in "after")
52    pub removed_functions: Vec<FunctionStats>,
53}
54
55/// Compare two CPU profiles to find regressions and improvements
56pub struct ProfileDiffer {
57    /// Minimum percentage change to report
58    min_delta_percent: f64,
59    /// Maximum number of results per category
60    top_n: usize,
61}
62
63impl ProfileDiffer {
64    /// Create a new differ with default settings
65    pub fn new() -> Self {
66        Self {
67            min_delta_percent: 1.0, // 1% minimum change
68            top_n: 20,
69        }
70    }
71
72    /// Set minimum percentage change threshold
73    pub fn min_delta_percent(mut self, percent: f64) -> Self {
74        self.min_delta_percent = percent;
75        self
76    }
77
78    /// Set maximum number of results
79    pub fn top_n(mut self, n: usize) -> Self {
80        self.top_n = n;
81        self
82    }
83
84    /// Compare two profiles
85    #[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        // Build maps by function key (name + location)
102        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        // Find changes and new functions
120        for (key, after_stats) in &after_map {
121            if let Some(before_stats) = before_map.get(key) {
122                // Function exists in both - calculate delta
123                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 // New time from 0
128                } 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 function
150                new_functions.push((*after_stats).clone());
151            }
152        }
153
154        // Find removed functions
155        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        // Sort and truncate
162        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}