Skip to main content

aperture_shared/types/
profile.rs

1//! Profile data structures
2//!
3//! These types represent aggregated profiling data, suitable for storage
4//! and visualization.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// A single frame in a stack trace
10#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
11pub struct Frame {
12    /// Instruction pointer
13    pub ip: u64,
14
15    /// Function name (if resolved)
16    pub function: Option<String>,
17
18    /// File name (if available)
19    pub file: Option<String>,
20
21    /// Line number (if available)
22    pub line: Option<u32>,
23
24    /// Module/library name
25    pub module: Option<String>,
26}
27
28impl Frame {
29    /// Create a new unresolved frame
30    pub fn new_unresolved(ip: u64) -> Self {
31        Self {
32            ip,
33            function: None,
34            file: None,
35            line: None,
36            module: None,
37        }
38    }
39
40    /// Check if the frame has been symbolized
41    pub fn is_symbolized(&self) -> bool {
42        self.function.is_some()
43    }
44}
45
46/// A complete stack trace with symbol information
47#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
48pub struct Stack {
49    /// Frames from innermost (top) to outermost (bottom)
50    pub frames: Vec<Frame>,
51}
52
53impl Stack {
54    /// Create a new stack from instruction pointers
55    pub fn from_ips(ips: &[u64]) -> Self {
56        Self {
57            frames: ips.iter().map(|&ip| Frame::new_unresolved(ip)).collect(),
58        }
59    }
60
61    /// Create a stack from IPs with optional pre-resolved symbol names.
62    /// `symbols` is a parallel array to `ips`; entries with `Some(name)` populate the frame's function field.
63    pub fn from_ips_with_symbols(ips: &[u64], symbols: &[Option<String>]) -> Self {
64        Self {
65            frames: ips
66                .iter()
67                .enumerate()
68                .map(|(i, &ip)| {
69                    let function = symbols.get(i).and_then(|s| s.clone());
70                    Frame {
71                        ip,
72                        function,
73                        file: None,
74                        line: None,
75                        module: None,
76                    }
77                })
78                .collect(),
79        }
80    }
81}
82
83/// Aggregated profile data for a time period
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Profile {
86    /// Start timestamp of the profile period
87    pub start_time: u64,
88
89    /// End timestamp of the profile period
90    pub end_time: u64,
91
92    /// Sample counts per stack trace
93    pub samples: HashMap<Stack, u64>,
94
95    /// Total number of samples
96    pub total_samples: u64,
97
98    /// Sampling period in nanoseconds
99    pub sample_period_ns: u64,
100}
101
102impl Profile {
103    /// Create a new empty profile
104    pub fn new(start_time: u64, end_time: u64, sample_period_ns: u64) -> Self {
105        Self {
106            start_time,
107            end_time,
108            samples: HashMap::new(),
109            total_samples: 0,
110            sample_period_ns,
111        }
112    }
113
114    /// Add a sample to the profile
115    pub fn add_sample(&mut self, stack: Stack) {
116        *self.samples.entry(stack).or_insert(0) += 1;
117        self.total_samples += 1;
118    }
119
120    /// Get the duration of the profile in nanoseconds
121    pub fn duration_ns(&self) -> u64 {
122        self.end_time.saturating_sub(self.start_time)
123    }
124
125    /// Get the sampling rate in Hz
126    pub fn sampling_rate_hz(&self) -> f64 {
127        if self.sample_period_ns == 0 {
128            0.0
129        } else {
130            1_000_000_000.0 / self.sample_period_ns as f64
131        }
132    }
133}
134
135/// Statistics for lock contention
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct LockContentionStats {
138    pub count: u64,
139    pub total_wait_ns: u64,
140    pub max_wait_ns: u64,
141    pub min_wait_ns: u64,
142}
143
144impl Default for LockContentionStats {
145    fn default() -> Self {
146        Self {
147            count: 0,
148            total_wait_ns: 0,
149            max_wait_ns: 0,
150            min_wait_ns: u64::MAX,
151        }
152    }
153}
154
155/// Profile of lock contention events
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct LockProfile {
158    pub start_time: u64,
159    pub end_time: u64,
160    // (lock_addr, stack) -> stats
161    pub contentions: HashMap<(u64, Stack), LockContentionStats>,
162    pub total_events: u64,
163}
164
165impl LockProfile {
166    pub fn new(start_time: u64) -> Self {
167        Self {
168            start_time,
169            end_time: 0,
170            contentions: HashMap::new(),
171            total_events: 0,
172        }
173    }
174
175    pub fn add_contention(&mut self, lock_addr: u64, stack: Stack, wait_ns: u64) {
176        let stats = self.contentions.entry((lock_addr, stack)).or_default();
177
178        stats.count += 1;
179        stats.total_wait_ns += wait_ns;
180        stats.max_wait_ns = stats.max_wait_ns.max(wait_ns);
181        stats.min_wait_ns = stats.min_wait_ns.min(wait_ns);
182        self.total_events += 1;
183    }
184
185    pub fn as_weighted_stacks(&self) -> HashMap<Stack, u64> {
186        let mut stacks = HashMap::new();
187        for ((_, stack), stats) in &self.contentions {
188            *stacks.entry(stack.clone()).or_insert(0) += stats.total_wait_ns;
189        }
190        stacks
191    }
192}
193
194/// Statistics for system calls
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct SyscallStats {
197    pub syscall_id: u32,
198    pub name: String,
199    pub count: u64,
200    pub total_duration_ns: u64,
201    pub max_duration_ns: u64,
202    pub min_duration_ns: u64,
203    pub error_count: u64,
204    // Power-of-2 buckets from 1ns to ~1s (30 buckets)
205    pub latency_histogram: Vec<u64>,
206}
207
208impl SyscallStats {
209    pub fn new(id: u32, name: String) -> Self {
210        Self {
211            syscall_id: id,
212            name,
213            count: 0,
214            total_duration_ns: 0,
215            max_duration_ns: 0,
216            min_duration_ns: u64::MAX,
217            error_count: 0,
218            latency_histogram: vec![0; 30],
219        }
220    }
221}
222
223/// Profile of system calls
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct SyscallProfile {
226    pub start_time: u64,
227    pub end_time: u64,
228    pub syscalls: HashMap<u32, SyscallStats>,
229    pub total_events: u64,
230}
231
232impl SyscallProfile {
233    pub fn new(start_time: u64) -> Self {
234        Self {
235            start_time,
236            end_time: 0,
237            syscalls: HashMap::new(),
238            total_events: 0,
239        }
240    }
241
242    pub fn add_syscall(&mut self, id: u32, name: &str, duration_ns: u64, return_value: i64) {
243        let stats = self
244            .syscalls
245            .entry(id)
246            .or_insert_with(|| SyscallStats::new(id, name.to_string()));
247
248        stats.count += 1;
249        stats.total_duration_ns += duration_ns;
250        stats.max_duration_ns = stats.max_duration_ns.max(duration_ns);
251        stats.min_duration_ns = stats.min_duration_ns.min(duration_ns);
252
253        if return_value < 0 {
254            stats.error_count += 1;
255        }
256
257        // Calculate bucket: log2(duration_ns)
258        // 0..1ns -> 0
259        // 2..3ns -> 1
260        // ...
261        let bucket = if duration_ns == 0 {
262            0
263        } else {
264            (63 - duration_ns.leading_zeros()).min(29) as usize
265        };
266        stats.latency_histogram[bucket] += 1;
267
268        self.total_events += 1;
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_profile_add_sample() {
278        let mut profile = Profile::new(0, 1000, 10_000_000);
279
280        let stack = Stack::from_ips(&[0x400000, 0x400100]);
281        profile.add_sample(stack.clone());
282        profile.add_sample(stack.clone());
283
284        assert_eq!(profile.total_samples, 2);
285        assert_eq!(*profile.samples.get(&stack).unwrap(), 2);
286    }
287
288    #[test]
289    fn test_sampling_rate_calculation() {
290        let profile = Profile::new(0, 1000, 10_000_000); // 10ms period
291        assert!((profile.sampling_rate_hz() - 100.0).abs() < 0.01);
292    }
293
294    #[test]
295    fn test_sampling_rate_zero_period() {
296        let profile = Profile::new(0, 1000, 0);
297        assert_eq!(profile.sampling_rate_hz(), 0.0);
298    }
299
300    #[test]
301    fn test_duration_ns() {
302        let profile = Profile::new(1000, 5000, 0);
303        assert_eq!(profile.duration_ns(), 4000);
304    }
305
306    #[test]
307    fn test_frame_new_unresolved() {
308        let frame = Frame::new_unresolved(0xdeadbeef);
309        assert_eq!(frame.ip, 0xdeadbeef);
310        assert!(!frame.is_symbolized());
311    }
312
313    #[test]
314    fn test_frame_is_symbolized() {
315        let mut frame = Frame::new_unresolved(0x1000);
316        assert!(!frame.is_symbolized());
317        frame.function = Some("main".to_string());
318        assert!(frame.is_symbolized());
319    }
320
321    #[test]
322    fn test_stack_from_ips() {
323        let stack = Stack::from_ips(&[0x1000, 0x2000, 0x3000]);
324        assert_eq!(stack.frames.len(), 3);
325        assert_eq!(stack.frames[0].ip, 0x1000);
326        assert_eq!(stack.frames[2].ip, 0x3000);
327        assert!(stack.frames.iter().all(|f| !f.is_symbolized()));
328    }
329
330    #[test]
331    fn test_profile_multiple_unique_stacks() {
332        let mut profile = Profile::new(0, 1000, 10_000_000);
333
334        profile.add_sample(Stack::from_ips(&[0x1000]));
335        profile.add_sample(Stack::from_ips(&[0x2000]));
336        profile.add_sample(Stack::from_ips(&[0x1000])); // duplicate
337
338        assert_eq!(profile.total_samples, 3);
339        assert_eq!(profile.samples.len(), 2);
340        assert_eq!(
341            *profile.samples.get(&Stack::from_ips(&[0x1000])).unwrap(),
342            2
343        );
344        assert_eq!(
345            *profile.samples.get(&Stack::from_ips(&[0x2000])).unwrap(),
346            1
347        );
348    }
349}