Skip to main content

aperture_shared/types/
diff.rs

1//! Differential profiling types and computation
2//!
3//! Compares two profiles (baseline vs comparison) and computes per-entry deltas.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8use super::profile::{LockProfile, Profile, Stack, SyscallProfile};
9
10// ── CPU diff ────────────────────────────────────────────────────────────────
11
12/// Diff of two CPU profiles.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CpuDiff {
15    pub baseline_total: u64,
16    pub comparison_total: u64,
17    /// Per-stack diffs sorted by |delta| descending.
18    pub stacks: Vec<StackDiff>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct StackDiff {
23    pub stack: Stack,
24    pub baseline_count: u64,
25    pub comparison_count: u64,
26    /// comparison - baseline
27    pub delta: i64,
28    /// delta / baseline * 100 (0.0 if baseline is 0)
29    pub delta_pct: f64,
30}
31
32/// Compare two CPU profiles stack-by-stack.
33pub fn diff_cpu(baseline: &Profile, comparison: &Profile) -> CpuDiff {
34    let all_stacks: HashSet<&Stack> = baseline
35        .samples
36        .keys()
37        .chain(comparison.samples.keys())
38        .collect();
39
40    let mut stacks: Vec<StackDiff> = all_stacks
41        .into_iter()
42        .map(|stack| {
43            let b = baseline.samples.get(stack).copied().unwrap_or(0);
44            let c = comparison.samples.get(stack).copied().unwrap_or(0);
45            let delta = c as i64 - b as i64;
46            let delta_pct = if b > 0 {
47                delta as f64 / b as f64 * 100.0
48            } else {
49                0.0
50            };
51            StackDiff {
52                stack: stack.clone(),
53                baseline_count: b,
54                comparison_count: c,
55                delta,
56                delta_pct,
57            }
58        })
59        .collect();
60
61    stacks.sort_by(|a, b| b.delta.unsigned_abs().cmp(&a.delta.unsigned_abs()));
62
63    CpuDiff {
64        baseline_total: baseline.total_samples,
65        comparison_total: comparison.total_samples,
66        stacks,
67    }
68}
69
70// ── Syscall diff ────────────────────────────────────────────────────────────
71
72/// Diff of two syscall profiles.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SyscallDiff {
75    pub baseline_total: u64,
76    pub comparison_total: u64,
77    pub syscalls: Vec<SyscallStatsDiff>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct SyscallStatsDiff {
82    pub syscall_id: u32,
83    pub name: String,
84    pub baseline_count: u64,
85    pub comparison_count: u64,
86    pub delta_count: i64,
87    pub baseline_avg_ns: f64,
88    pub comparison_avg_ns: f64,
89    pub delta_avg_ns: f64,
90}
91
92/// Compare two syscall profiles per-syscall.
93pub fn diff_syscall(baseline: &SyscallProfile, comparison: &SyscallProfile) -> SyscallDiff {
94    let all_ids: HashSet<u32> = baseline
95        .syscalls
96        .keys()
97        .chain(comparison.syscalls.keys())
98        .copied()
99        .collect();
100
101    let mut syscalls: Vec<SyscallStatsDiff> = all_ids
102        .into_iter()
103        .map(|id| {
104            let b = baseline.syscalls.get(&id);
105            let c = comparison.syscalls.get(&id);
106
107            let b_count = b.map_or(0, |s| s.count);
108            let c_count = c.map_or(0, |s| s.count);
109            let b_avg = b.map_or(0.0, |s| {
110                if s.count > 0 {
111                    s.total_duration_ns as f64 / s.count as f64
112                } else {
113                    0.0
114                }
115            });
116            let c_avg = c.map_or(0.0, |s| {
117                if s.count > 0 {
118                    s.total_duration_ns as f64 / s.count as f64
119                } else {
120                    0.0
121                }
122            });
123            let name = b
124                .map(|s| s.name.clone())
125                .or_else(|| c.map(|s| s.name.clone()))
126                .unwrap_or_default();
127
128            SyscallStatsDiff {
129                syscall_id: id,
130                name,
131                baseline_count: b_count,
132                comparison_count: c_count,
133                delta_count: c_count as i64 - b_count as i64,
134                baseline_avg_ns: b_avg,
135                comparison_avg_ns: c_avg,
136                delta_avg_ns: c_avg - b_avg,
137            }
138        })
139        .collect();
140
141    syscalls.sort_by(|a, b| {
142        b.delta_count
143            .unsigned_abs()
144            .cmp(&a.delta_count.unsigned_abs())
145    });
146
147    SyscallDiff {
148        baseline_total: baseline.total_events,
149        comparison_total: comparison.total_events,
150        syscalls,
151    }
152}
153
154// ── Lock diff ───────────────────────────────────────────────────────────────
155
156/// Diff of two lock contention profiles.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct LockDiff {
159    pub baseline_total: u64,
160    pub comparison_total: u64,
161    pub contentions: Vec<LockContentionDiff>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct LockContentionDiff {
166    pub lock_addr: u64,
167    pub stack: Stack,
168    pub baseline_count: u64,
169    pub comparison_count: u64,
170    pub delta_wait_ns: i64,
171}
172
173/// Compare two lock contention profiles per (lock_addr, stack).
174pub fn diff_lock(baseline: &LockProfile, comparison: &LockProfile) -> LockDiff {
175    let all_keys: HashSet<&(u64, Stack)> = baseline
176        .contentions
177        .keys()
178        .chain(comparison.contentions.keys())
179        .collect();
180
181    let mut contentions: Vec<LockContentionDiff> = all_keys
182        .into_iter()
183        .map(|key| {
184            let b = baseline.contentions.get(key);
185            let c = comparison.contentions.get(key);
186            let b_wait = b.map_or(0, |s| s.total_wait_ns);
187            let c_wait = c.map_or(0, |s| s.total_wait_ns);
188            LockContentionDiff {
189                lock_addr: key.0,
190                stack: key.1.clone(),
191                baseline_count: b.map_or(0, |s| s.count),
192                comparison_count: c.map_or(0, |s| s.count),
193                delta_wait_ns: c_wait as i64 - b_wait as i64,
194            }
195        })
196        .collect();
197
198    contentions.sort_by(|a, b| {
199        b.delta_wait_ns
200            .unsigned_abs()
201            .cmp(&a.delta_wait_ns.unsigned_abs())
202    });
203
204    LockDiff {
205        baseline_total: baseline.total_events,
206        comparison_total: comparison.total_events,
207        contentions,
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_diff_cpu_basic() {
217        let mut baseline = Profile::new(0, 1000, 10_000_000);
218        let mut comparison = Profile::new(1000, 2000, 10_000_000);
219
220        let stack_a = Stack::from_ips(&[0x1000, 0x2000]);
221        let stack_b = Stack::from_ips(&[0x3000]);
222
223        // baseline: A=10, B=5
224        for _ in 0..10 {
225            baseline.add_sample(stack_a.clone());
226        }
227        for _ in 0..5 {
228            baseline.add_sample(stack_b.clone());
229        }
230
231        // comparison: A=7, B=8
232        for _ in 0..7 {
233            comparison.add_sample(stack_a.clone());
234        }
235        for _ in 0..8 {
236            comparison.add_sample(stack_b.clone());
237        }
238
239        let diff = diff_cpu(&baseline, &comparison);
240        assert_eq!(diff.baseline_total, 15);
241        assert_eq!(diff.comparison_total, 15);
242        assert_eq!(diff.stacks.len(), 2);
243
244        // Sorted by |delta| descending: A has delta=-3, B has delta=+3 → either first
245        let a_diff = diff.stacks.iter().find(|s| s.stack == stack_a).unwrap();
246        assert_eq!(a_diff.baseline_count, 10);
247        assert_eq!(a_diff.comparison_count, 7);
248        assert_eq!(a_diff.delta, -3);
249        assert!((a_diff.delta_pct - (-30.0)).abs() < 0.01);
250
251        let b_diff = diff.stacks.iter().find(|s| s.stack == stack_b).unwrap();
252        assert_eq!(b_diff.delta, 3);
253        assert!((b_diff.delta_pct - 60.0).abs() < 0.01);
254    }
255
256    #[test]
257    fn test_diff_cpu_new_stack_in_comparison() {
258        let baseline = Profile::new(0, 1000, 10_000_000);
259        let mut comparison = Profile::new(1000, 2000, 10_000_000);
260
261        let stack = Stack::from_ips(&[0x1000]);
262        comparison.add_sample(stack.clone());
263
264        let diff = diff_cpu(&baseline, &comparison);
265        assert_eq!(diff.stacks.len(), 1);
266        assert_eq!(diff.stacks[0].baseline_count, 0);
267        assert_eq!(diff.stacks[0].comparison_count, 1);
268        assert_eq!(diff.stacks[0].delta, 1);
269        // delta_pct is 0 when baseline is 0
270        assert_eq!(diff.stacks[0].delta_pct, 0.0);
271    }
272
273    #[test]
274    fn test_diff_syscall_basic() {
275        let mut baseline = SyscallProfile::new(0);
276        let mut comparison = SyscallProfile::new(1000);
277
278        // baseline: read=10 calls, 1000ns total
279        for _ in 0..10 {
280            baseline.add_syscall(0, "read", 100, 0);
281        }
282        // comparison: read=20 calls, 4000ns total
283        for _ in 0..20 {
284            comparison.add_syscall(0, "read", 200, 0);
285        }
286
287        let diff = diff_syscall(&baseline, &comparison);
288        assert_eq!(diff.syscalls.len(), 1);
289        let s = &diff.syscalls[0];
290        assert_eq!(s.name, "read");
291        assert_eq!(s.baseline_count, 10);
292        assert_eq!(s.comparison_count, 20);
293        assert_eq!(s.delta_count, 10);
294        assert!((s.baseline_avg_ns - 100.0).abs() < 0.01);
295        assert!((s.comparison_avg_ns - 200.0).abs() < 0.01);
296        assert!((s.delta_avg_ns - 100.0).abs() < 0.01);
297    }
298
299    #[test]
300    fn test_diff_lock_basic() {
301        let mut baseline = LockProfile::new(0);
302        let mut comparison = LockProfile::new(1000);
303
304        let stack = Stack::from_ips(&[0x400000]);
305
306        baseline.add_contention(0x1000, stack.clone(), 500);
307        baseline.add_contention(0x1000, stack.clone(), 300);
308
309        comparison.add_contention(0x1000, stack.clone(), 1000);
310
311        let diff = diff_lock(&baseline, &comparison);
312        assert_eq!(diff.contentions.len(), 1);
313        let c = &diff.contentions[0];
314        assert_eq!(c.lock_addr, 0x1000);
315        assert_eq!(c.baseline_count, 2);
316        assert_eq!(c.comparison_count, 1);
317        // baseline total_wait = 800, comparison = 1000
318        assert_eq!(c.delta_wait_ns, 200);
319    }
320}