1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
11pub struct Frame {
12 pub ip: u64,
14
15 pub function: Option<String>,
17
18 pub file: Option<String>,
20
21 pub line: Option<u32>,
23
24 pub module: Option<String>,
26}
27
28impl Frame {
29 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 pub fn is_symbolized(&self) -> bool {
42 self.function.is_some()
43 }
44}
45
46#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
48pub struct Stack {
49 pub frames: Vec<Frame>,
51}
52
53impl Stack {
54 pub fn from_ips(ips: &[u64]) -> Self {
56 Self {
57 frames: ips.iter().map(|&ip| Frame::new_unresolved(ip)).collect(),
58 }
59 }
60
61 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#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Profile {
86 pub start_time: u64,
88
89 pub end_time: u64,
91
92 pub samples: HashMap<Stack, u64>,
94
95 pub total_samples: u64,
97
98 pub sample_period_ns: u64,
100}
101
102impl Profile {
103 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 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 pub fn duration_ns(&self) -> u64 {
122 self.end_time.saturating_sub(self.start_time)
123 }
124
125 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct LockProfile {
158 pub start_time: u64,
159 pub end_time: u64,
160 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#[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 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#[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 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); 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])); 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}