Skip to main content

jugar_probar/perf/
metrics.rs

1//! Performance Metrics
2//!
3//! Statistical analysis of performance data.
4
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// Statistical summary of values
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Statistics {
11    /// Minimum value
12    pub min: f64,
13    /// Maximum value
14    pub max: f64,
15    /// Mean (average)
16    pub mean: f64,
17    /// Median value
18    pub median: f64,
19    /// Standard deviation
20    pub std_dev: f64,
21    /// 95th percentile
22    pub p95: f64,
23    /// 99th percentile
24    pub p99: f64,
25    /// Sample count
26    pub count: usize,
27}
28
29impl Statistics {
30    /// Calculate statistics from a slice of values
31    #[must_use]
32    pub fn from_values(values: &[f64]) -> Self {
33        if values.is_empty() {
34            return Self::empty();
35        }
36
37        let mut sorted = values.to_vec();
38        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
39
40        let count = values.len();
41        let min = sorted[0];
42        let max = sorted[count - 1];
43        let sum: f64 = values.iter().sum();
44        let mean = sum / count as f64;
45
46        let median = if count % 2 == 0 {
47            (sorted[count / 2 - 1] + sorted[count / 2]) / 2.0
48        } else {
49            sorted[count / 2]
50        };
51
52        let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / count as f64;
53        let std_dev = variance.sqrt();
54
55        let p95_idx = ((count as f64 * 0.95) as usize).min(count - 1);
56        let p99_idx = ((count as f64 * 0.99) as usize).min(count - 1);
57
58        Self {
59            min,
60            max,
61            mean,
62            median,
63            std_dev,
64            p95: sorted[p95_idx],
65            p99: sorted[p99_idx],
66            count,
67        }
68    }
69
70    /// Create empty statistics
71    #[must_use]
72    pub fn empty() -> Self {
73        Self {
74            min: 0.0,
75            max: 0.0,
76            mean: 0.0,
77            median: 0.0,
78            std_dev: 0.0,
79            p95: 0.0,
80            p99: 0.0,
81            count: 0,
82        }
83    }
84
85    /// Check if within acceptable range
86    #[must_use]
87    pub fn within_budget(&self, budget_ms: f64) -> bool {
88        self.p99 <= budget_ms
89    }
90}
91
92/// Frame timing metrics
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct FrameMetrics {
95    /// Frame time in milliseconds
96    pub frame_time_ms: f64,
97    /// Frame number
98    pub frame_number: u64,
99    /// Timestamp
100    pub timestamp_ms: f64,
101}
102
103impl FrameMetrics {
104    /// Create new frame metrics
105    #[must_use]
106    pub fn new(frame_time_ms: f64) -> Self {
107        Self {
108            frame_time_ms,
109            frame_number: 0,
110            timestamp_ms: 0.0,
111        }
112    }
113
114    /// Create with frame number
115    #[must_use]
116    pub fn with_frame_number(mut self, number: u64) -> Self {
117        self.frame_number = number;
118        self
119    }
120
121    /// Calculate FPS from frame time
122    #[must_use]
123    pub fn fps(&self) -> f64 {
124        if self.frame_time_ms > 0.0 {
125            1000.0 / self.frame_time_ms
126        } else {
127            0.0
128        }
129    }
130
131    /// Check if frame meets target FPS (with small tolerance for floating-point)
132    #[must_use]
133    pub fn meets_target(&self, target_fps: f64) -> bool {
134        const EPSILON: f64 = 1e-9;
135        self.fps() >= target_fps - EPSILON
136    }
137}
138
139/// Memory usage metrics
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct MemoryMetrics {
142    /// Heap bytes used
143    pub heap_used: u64,
144    /// Total heap size
145    pub heap_total: u64,
146    /// Peak memory usage
147    pub peak_usage: u64,
148}
149
150impl MemoryMetrics {
151    /// Create new memory metrics
152    #[must_use]
153    pub fn new(heap_used: u64, heap_total: u64) -> Self {
154        Self {
155            heap_used,
156            heap_total,
157            peak_usage: heap_used,
158        }
159    }
160
161    /// Calculate usage percentage
162    #[must_use]
163    pub fn usage_percent(&self) -> f64 {
164        if self.heap_total > 0 {
165            (self.heap_used as f64 / self.heap_total as f64) * 100.0
166        } else {
167            0.0
168        }
169    }
170
171    /// Format heap used for display
172    #[must_use]
173    pub fn heap_used_formatted(&self) -> String {
174        format_bytes(self.heap_used)
175    }
176}
177
178/// Aggregate performance metrics
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct PerformanceMetrics {
181    /// Frame time statistics (ms)
182    pub frame_times: Statistics,
183    /// Memory usage snapshots
184    pub memory: Option<MemoryMetrics>,
185    /// Function timing by name
186    pub function_times: std::collections::HashMap<String, Statistics>,
187    /// Total measurement duration
188    pub duration: Duration,
189}
190
191impl PerformanceMetrics {
192    /// Create from a trace
193    #[must_use]
194    pub fn from_trace(trace: &super::trace::Trace) -> Self {
195        let mut function_times = std::collections::HashMap::new();
196
197        // Group spans by name
198        let mut by_name: std::collections::HashMap<&str, Vec<f64>> =
199            std::collections::HashMap::new();
200        for span in &trace.spans {
201            if let Some(dur_ns) = span.duration_ns() {
202                by_name
203                    .entry(&span.name)
204                    .or_default()
205                    .push(dur_ns as f64 / 1_000_000.0);
206            }
207        }
208
209        // Calculate statistics for each
210        for (name, values) in by_name {
211            function_times.insert(name.to_string(), Statistics::from_values(&values));
212        }
213
214        Self {
215            frame_times: Statistics::empty(),
216            memory: None,
217            function_times,
218            duration: trace.duration.unwrap_or_default(),
219        }
220    }
221
222    /// Check if all metrics within budget
223    #[must_use]
224    pub fn within_budget(&self, frame_budget_ms: f64) -> bool {
225        self.frame_times.within_budget(frame_budget_ms)
226    }
227}
228
229/// Format bytes for display
230#[must_use]
231pub fn format_bytes(bytes: u64) -> String {
232    if bytes < 1024 {
233        format!("{} B", bytes)
234    } else if bytes < 1024 * 1024 {
235        format!("{:.1} KB", bytes as f64 / 1024.0)
236    } else if bytes < 1024 * 1024 * 1024 {
237        format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
238    } else {
239        format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
240    }
241}
242
243#[cfg(test)]
244#[allow(clippy::unwrap_used, clippy::expect_used)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_statistics_from_values() {
250        let stats = Statistics::from_values(&[1.0, 2.0, 3.0, 4.0, 5.0]);
251
252        assert!((stats.min - 1.0).abs() < f64::EPSILON);
253        assert!((stats.max - 5.0).abs() < f64::EPSILON);
254        assert!((stats.mean - 3.0).abs() < f64::EPSILON);
255        assert!((stats.median - 3.0).abs() < f64::EPSILON);
256        assert_eq!(stats.count, 5);
257    }
258
259    #[test]
260    fn test_statistics_empty() {
261        let stats = Statistics::from_values(&[]);
262        assert_eq!(stats.count, 0);
263        assert!((stats.mean - 0.0).abs() < f64::EPSILON);
264    }
265
266    #[test]
267    fn test_statistics_single_value() {
268        let stats = Statistics::from_values(&[42.0]);
269        assert!((stats.min - 42.0).abs() < f64::EPSILON);
270        assert!((stats.max - 42.0).abs() < f64::EPSILON);
271        assert!((stats.mean - 42.0).abs() < f64::EPSILON);
272    }
273
274    #[test]
275    fn test_statistics_within_budget() {
276        let stats = Statistics::from_values(&[10.0, 12.0, 14.0, 16.0, 18.0]);
277        assert!(stats.within_budget(20.0));
278        assert!(!stats.within_budget(15.0));
279    }
280
281    #[test]
282    fn test_frame_metrics_fps() {
283        let metrics = FrameMetrics::new(16.67);
284        let fps = metrics.fps();
285        assert!(fps > 59.0 && fps < 61.0);
286    }
287
288    #[test]
289    fn test_frame_metrics_meets_target() {
290        // 16.0ms = 62.5 FPS, clearly above 60
291        let metrics = FrameMetrics::new(16.0);
292        assert!(metrics.meets_target(60.0));
293        assert!(!metrics.meets_target(120.0));
294    }
295
296    #[test]
297    fn test_memory_metrics_usage_percent() {
298        let metrics = MemoryMetrics::new(512, 1024);
299        assert!((metrics.usage_percent() - 50.0).abs() < f64::EPSILON);
300    }
301
302    #[test]
303    fn test_memory_metrics_formatted() {
304        let metrics = MemoryMetrics::new(1024 * 1024, 2 * 1024 * 1024);
305        assert!(metrics.heap_used_formatted().contains("MB"));
306    }
307
308    #[test]
309    fn test_format_bytes() {
310        assert_eq!(format_bytes(500), "500 B");
311        assert_eq!(format_bytes(1024), "1.0 KB");
312        assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
313        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
314    }
315
316    // =========================================================================
317    // Additional tests for 95%+ coverage
318    // =========================================================================
319
320    #[test]
321    fn test_statistics_empty_method() {
322        let stats = Statistics::empty();
323        assert_eq!(stats.count, 0);
324        assert!((stats.min - 0.0).abs() < f64::EPSILON);
325        assert!((stats.max - 0.0).abs() < f64::EPSILON);
326        assert!((stats.mean - 0.0).abs() < f64::EPSILON);
327        assert!((stats.median - 0.0).abs() < f64::EPSILON);
328        assert!((stats.std_dev - 0.0).abs() < f64::EPSILON);
329        assert!((stats.p95 - 0.0).abs() < f64::EPSILON);
330        assert!((stats.p99 - 0.0).abs() < f64::EPSILON);
331    }
332
333    #[test]
334    fn test_statistics_even_count_median() {
335        // Test median calculation for even number of elements
336        let stats = Statistics::from_values(&[1.0, 2.0, 3.0, 4.0]);
337        // Median of [1, 2, 3, 4] = (2 + 3) / 2 = 2.5
338        assert!((stats.median - 2.5).abs() < f64::EPSILON);
339        assert_eq!(stats.count, 4);
340    }
341
342    #[test]
343    fn test_statistics_two_values_median() {
344        let stats = Statistics::from_values(&[10.0, 20.0]);
345        // Median of [10, 20] = (10 + 20) / 2 = 15
346        assert!((stats.median - 15.0).abs() < f64::EPSILON);
347    }
348
349    #[test]
350    fn test_statistics_percentiles() {
351        // Create 100 values for clear percentile testing
352        let values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
353        let stats = Statistics::from_values(&values);
354
355        assert_eq!(stats.count, 100);
356        assert!((stats.min - 1.0).abs() < f64::EPSILON);
357        assert!((stats.max - 100.0).abs() < f64::EPSILON);
358        // p95 should be around 95
359        assert!(stats.p95 >= 94.0 && stats.p95 <= 96.0);
360        // p99 should be around 99
361        assert!(stats.p99 >= 98.0 && stats.p99 <= 100.0);
362    }
363
364    #[test]
365    fn test_statistics_std_dev() {
366        // Known standard deviation case: [2, 4, 4, 4, 5, 5, 7, 9]
367        // Mean = 5, Variance = 4, Std Dev = 2
368        let values = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
369        let stats = Statistics::from_values(&values);
370        assert!((stats.mean - 5.0).abs() < f64::EPSILON);
371        assert!((stats.std_dev - 2.0).abs() < f64::EPSILON);
372    }
373
374    #[test]
375    fn test_statistics_with_nan_values() {
376        // Test handling of NaN values in sorting
377        let stats = Statistics::from_values(&[1.0, f64::NAN, 3.0]);
378        // The function should handle NaN gracefully
379        assert_eq!(stats.count, 3);
380    }
381
382    #[test]
383    fn test_statistics_unsorted_input() {
384        // Ensure sorting works correctly
385        let stats = Statistics::from_values(&[5.0, 1.0, 4.0, 2.0, 3.0]);
386        assert!((stats.min - 1.0).abs() < f64::EPSILON);
387        assert!((stats.max - 5.0).abs() < f64::EPSILON);
388        assert!((stats.median - 3.0).abs() < f64::EPSILON);
389    }
390
391    #[test]
392    fn test_frame_metrics_new() {
393        let metrics = FrameMetrics::new(16.67);
394        assert!((metrics.frame_time_ms - 16.67).abs() < f64::EPSILON);
395        assert_eq!(metrics.frame_number, 0);
396        assert!((metrics.timestamp_ms - 0.0).abs() < f64::EPSILON);
397    }
398
399    #[test]
400    fn test_frame_metrics_with_frame_number() {
401        let metrics = FrameMetrics::new(16.67).with_frame_number(42);
402        assert_eq!(metrics.frame_number, 42);
403        assert!((metrics.frame_time_ms - 16.67).abs() < f64::EPSILON);
404    }
405
406    #[test]
407    fn test_frame_metrics_fps_zero_frame_time() {
408        let metrics = FrameMetrics::new(0.0);
409        assert!((metrics.fps() - 0.0).abs() < f64::EPSILON);
410    }
411
412    #[test]
413    fn test_frame_metrics_fps_negative_frame_time() {
414        let metrics = FrameMetrics::new(-10.0);
415        assert!((metrics.fps() - 0.0).abs() < f64::EPSILON);
416    }
417
418    #[test]
419    fn test_frame_metrics_meets_target_edge_case() {
420        // Test when FPS exactly equals target
421        let metrics = FrameMetrics::new(16.666666666666668); // Exactly 60 FPS
422        let fps = metrics.fps();
423        assert!((fps - 60.0).abs() < 0.001);
424        assert!(metrics.meets_target(60.0));
425    }
426
427    #[test]
428    fn test_memory_metrics_new() {
429        let metrics = MemoryMetrics::new(1024, 4096);
430        assert_eq!(metrics.heap_used, 1024);
431        assert_eq!(metrics.heap_total, 4096);
432        assert_eq!(metrics.peak_usage, 1024); // peak starts as heap_used
433    }
434
435    #[test]
436    fn test_memory_metrics_usage_percent_zero_total() {
437        let metrics = MemoryMetrics::new(1024, 0);
438        assert!((metrics.usage_percent() - 0.0).abs() < f64::EPSILON);
439    }
440
441    #[test]
442    fn test_memory_metrics_usage_percent_full() {
443        let metrics = MemoryMetrics::new(1024, 1024);
444        assert!((metrics.usage_percent() - 100.0).abs() < f64::EPSILON);
445    }
446
447    #[test]
448    fn test_memory_metrics_formatted_bytes() {
449        let metrics = MemoryMetrics::new(500, 1000);
450        assert_eq!(metrics.heap_used_formatted(), "500 B");
451    }
452
453    #[test]
454    fn test_memory_metrics_formatted_kb() {
455        let metrics = MemoryMetrics::new(2048, 4096);
456        assert!(metrics.heap_used_formatted().contains("KB"));
457    }
458
459    #[test]
460    fn test_memory_metrics_formatted_gb() {
461        let metrics = MemoryMetrics::new(2 * 1024 * 1024 * 1024, 4 * 1024 * 1024 * 1024);
462        assert!(metrics.heap_used_formatted().contains("GB"));
463    }
464
465    #[test]
466    fn test_format_bytes_boundaries() {
467        // Test exact boundaries
468        assert_eq!(format_bytes(0), "0 B");
469        assert_eq!(format_bytes(1023), "1023 B");
470        assert_eq!(format_bytes(1024), "1.0 KB");
471        assert_eq!(format_bytes(1024 * 1024 - 1), "1024.0 KB");
472        assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
473        assert_eq!(format_bytes(1024 * 1024 * 1024 - 1), "1024.00 MB");
474        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
475    }
476
477    #[test]
478    fn test_format_bytes_large_values() {
479        assert_eq!(format_bytes(10 * 1024 * 1024 * 1024), "10.00 GB");
480    }
481
482    #[test]
483    fn test_performance_metrics_from_trace() {
484        use super::super::trace::Tracer;
485
486        let mut tracer = Tracer::new();
487        tracer.start();
488
489        // Create spans with known names
490        for _ in 0..3 {
491            let _span = tracer.span("render");
492            std::thread::sleep(std::time::Duration::from_micros(100));
493        }
494        for _ in 0..2 {
495            let _span = tracer.span("update");
496            std::thread::sleep(std::time::Duration::from_micros(50));
497        }
498
499        let trace = tracer.stop();
500        let metrics = PerformanceMetrics::from_trace(&trace);
501
502        // Should have function times for both span types
503        assert!(metrics.function_times.contains_key("render"));
504        assert!(metrics.function_times.contains_key("update"));
505        assert_eq!(metrics.function_times.get("render").unwrap().count, 3);
506        assert_eq!(metrics.function_times.get("update").unwrap().count, 2);
507        assert!(metrics.duration.as_nanos() > 0);
508    }
509
510    #[test]
511    fn test_performance_metrics_from_empty_trace() {
512        use super::super::trace::Tracer;
513
514        let mut tracer = Tracer::new();
515        tracer.start();
516        let trace = tracer.stop();
517
518        let metrics = PerformanceMetrics::from_trace(&trace);
519
520        assert!(metrics.function_times.is_empty());
521        assert!(metrics.memory.is_none());
522        assert_eq!(metrics.frame_times.count, 0);
523    }
524
525    #[test]
526    fn test_performance_metrics_within_budget() {
527        use super::super::trace::Tracer;
528
529        let mut tracer = Tracer::new();
530        tracer.start();
531        let trace = tracer.stop();
532
533        let metrics = PerformanceMetrics::from_trace(&trace);
534
535        // Empty frame_times should be within any budget
536        assert!(metrics.within_budget(16.67));
537        assert!(metrics.within_budget(0.0));
538    }
539
540    #[test]
541    fn test_performance_metrics_duration_none() {
542        use super::super::trace::{Trace, TraceConfig};
543
544        // Create a trace with no duration
545        let trace = Trace {
546            spans: vec![],
547            duration: None,
548            config: TraceConfig::default(),
549        };
550
551        let metrics = PerformanceMetrics::from_trace(&trace);
552        assert_eq!(metrics.duration, Duration::default());
553    }
554
555    #[test]
556    fn test_performance_metrics_with_unclosed_spans() {
557        use super::super::span::Span;
558        use super::super::trace::{Trace, TraceConfig};
559
560        // Create a trace with spans that have no end_ns (unclosed)
561        let unclosed_span = Span::new("unclosed", 1000);
562        // Note: end_ns is None, so duration_ns() returns None
563
564        let trace = Trace {
565            spans: vec![unclosed_span],
566            duration: Some(Duration::from_millis(100)),
567            config: TraceConfig::default(),
568        };
569
570        let metrics = PerformanceMetrics::from_trace(&trace);
571
572        // Unclosed spans should not appear in function_times
573        assert!(!metrics.function_times.contains_key("unclosed"));
574    }
575
576    #[test]
577    fn test_performance_metrics_with_closed_spans() {
578        use super::super::span::Span;
579        use super::super::trace::{Trace, TraceConfig};
580
581        // Create a trace with properly closed spans
582        let mut span1 = Span::new("test_fn", 0);
583        span1.close(1_000_000); // 1ms in ns
584
585        let mut span2 = Span::new("test_fn", 2_000_000);
586        span2.close(3_000_000); // 1ms duration
587
588        let trace = Trace {
589            spans: vec![span1, span2],
590            duration: Some(Duration::from_millis(5)),
591            config: TraceConfig::default(),
592        };
593
594        let metrics = PerformanceMetrics::from_trace(&trace);
595
596        assert!(metrics.function_times.contains_key("test_fn"));
597        let stats = metrics.function_times.get("test_fn").unwrap();
598        assert_eq!(stats.count, 2);
599        // Each span is 1ms = 1.0 in the stats
600        assert!((stats.mean - 1.0).abs() < 0.001);
601    }
602
603    #[test]
604    fn test_statistics_clone_and_debug() {
605        let stats = Statistics::from_values(&[1.0, 2.0, 3.0]);
606        let cloned = stats.clone();
607        assert_eq!(cloned.count, stats.count);
608
609        let debug_str = format!("{:?}", stats);
610        assert!(debug_str.contains("Statistics"));
611    }
612
613    #[test]
614    fn test_frame_metrics_clone_and_debug() {
615        let metrics = FrameMetrics::new(16.67).with_frame_number(10);
616        let cloned = metrics.clone();
617        assert_eq!(cloned.frame_number, 10);
618
619        let debug_str = format!("{:?}", metrics);
620        assert!(debug_str.contains("FrameMetrics"));
621    }
622
623    #[test]
624    fn test_memory_metrics_clone_and_debug() {
625        let metrics = MemoryMetrics::new(1024, 4096);
626        let cloned = metrics.clone();
627        assert_eq!(cloned.heap_used, 1024);
628
629        let debug_str = format!("{:?}", metrics);
630        assert!(debug_str.contains("MemoryMetrics"));
631    }
632
633    #[test]
634    fn test_performance_metrics_clone_and_debug() {
635        use super::super::trace::Tracer;
636
637        let mut tracer = Tracer::new();
638        tracer.start();
639        let trace = tracer.stop();
640        let metrics = PerformanceMetrics::from_trace(&trace);
641
642        let cloned = metrics.clone();
643        assert_eq!(cloned.frame_times.count, metrics.frame_times.count);
644
645        let debug_str = format!("{:?}", metrics);
646        assert!(debug_str.contains("PerformanceMetrics"));
647    }
648
649    #[test]
650    fn test_statistics_serialize_deserialize() {
651        let stats = Statistics::from_values(&[1.0, 2.0, 3.0, 4.0, 5.0]);
652        let json = serde_json::to_string(&stats).unwrap();
653        let deserialized: Statistics = serde_json::from_str(&json).unwrap();
654
655        assert_eq!(deserialized.count, stats.count);
656        assert!((deserialized.mean - stats.mean).abs() < f64::EPSILON);
657    }
658
659    #[test]
660    fn test_frame_metrics_serialize_deserialize() {
661        let metrics = FrameMetrics::new(16.67).with_frame_number(42);
662        let json = serde_json::to_string(&metrics).unwrap();
663        let deserialized: FrameMetrics = serde_json::from_str(&json).unwrap();
664
665        assert_eq!(deserialized.frame_number, 42);
666        assert!((deserialized.frame_time_ms - 16.67).abs() < f64::EPSILON);
667    }
668
669    #[test]
670    fn test_memory_metrics_serialize_deserialize() {
671        let metrics = MemoryMetrics::new(1024, 4096);
672        let json = serde_json::to_string(&metrics).unwrap();
673        let deserialized: MemoryMetrics = serde_json::from_str(&json).unwrap();
674
675        assert_eq!(deserialized.heap_used, 1024);
676        assert_eq!(deserialized.heap_total, 4096);
677    }
678
679    #[test]
680    fn test_performance_metrics_serialize_deserialize() {
681        use super::super::trace::Tracer;
682
683        let mut tracer = Tracer::new();
684        tracer.start();
685        {
686            let _span = tracer.span("test");
687        }
688        let trace = tracer.stop();
689        let metrics = PerformanceMetrics::from_trace(&trace);
690
691        let json = serde_json::to_string(&metrics).unwrap();
692        let deserialized: PerformanceMetrics = serde_json::from_str(&json).unwrap();
693
694        assert_eq!(
695            deserialized.function_times.len(),
696            metrics.function_times.len()
697        );
698    }
699}