1use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Statistics {
11 pub min: f64,
13 pub max: f64,
15 pub mean: f64,
17 pub median: f64,
19 pub std_dev: f64,
21 pub p95: f64,
23 pub p99: f64,
25 pub count: usize,
27}
28
29impl Statistics {
30 #[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 #[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 #[must_use]
87 pub fn within_budget(&self, budget_ms: f64) -> bool {
88 self.p99 <= budget_ms
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct FrameMetrics {
95 pub frame_time_ms: f64,
97 pub frame_number: u64,
99 pub timestamp_ms: f64,
101}
102
103impl FrameMetrics {
104 #[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 #[must_use]
116 pub fn with_frame_number(mut self, number: u64) -> Self {
117 self.frame_number = number;
118 self
119 }
120
121 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct MemoryMetrics {
142 pub heap_used: u64,
144 pub heap_total: u64,
146 pub peak_usage: u64,
148}
149
150impl MemoryMetrics {
151 #[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 #[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 #[must_use]
173 pub fn heap_used_formatted(&self) -> String {
174 format_bytes(self.heap_used)
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct PerformanceMetrics {
181 pub frame_times: Statistics,
183 pub memory: Option<MemoryMetrics>,
185 pub function_times: std::collections::HashMap<String, Statistics>,
187 pub duration: Duration,
189}
190
191impl PerformanceMetrics {
192 #[must_use]
194 pub fn from_trace(trace: &super::trace::Trace) -> Self {
195 let mut function_times = std::collections::HashMap::new();
196
197 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 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 #[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#[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 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 #[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 let stats = Statistics::from_values(&[1.0, 2.0, 3.0, 4.0]);
337 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 assert!((stats.median - 15.0).abs() < f64::EPSILON);
347 }
348
349 #[test]
350 fn test_statistics_percentiles() {
351 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 assert!(stats.p95 >= 94.0 && stats.p95 <= 96.0);
360 assert!(stats.p99 >= 98.0 && stats.p99 <= 100.0);
362 }
363
364 #[test]
365 fn test_statistics_std_dev() {
366 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 let stats = Statistics::from_values(&[1.0, f64::NAN, 3.0]);
378 assert_eq!(stats.count, 3);
380 }
381
382 #[test]
383 fn test_statistics_unsorted_input() {
384 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 let metrics = FrameMetrics::new(16.666666666666668); 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); }
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 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 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 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 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 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 let unclosed_span = Span::new("unclosed", 1000);
562 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 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 let mut span1 = Span::new("test_fn", 0);
583 span1.close(1_000_000); let mut span2 = Span::new("test_fn", 2_000_000);
586 span2.close(3_000_000); 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 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}