1#![allow(dead_code)]
2use std::collections::VecDeque;
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone)]
13pub struct TimerRegion {
14 pub label: String,
16 pub start: Instant,
18 pub end: Option<Instant>,
20}
21
22impl TimerRegion {
23 #[must_use]
25 pub fn start(label: &str) -> Self {
26 Self {
27 label: label.to_string(),
28 start: Instant::now(),
29 end: None,
30 }
31 }
32
33 pub fn stop(&mut self) {
35 self.end = Some(Instant::now());
36 }
37
38 #[must_use]
40 pub fn elapsed(&self) -> Duration {
41 match self.end {
42 Some(end) => end.duration_since(self.start),
43 None => self.start.elapsed(),
44 }
45 }
46
47 #[must_use]
49 pub fn is_stopped(&self) -> bool {
50 self.end.is_some()
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct TimingSample {
57 pub label: String,
59 pub duration: Duration,
61 pub frame_number: u64,
63}
64
65#[derive(Debug, Clone)]
67pub struct GpuTimerConfig {
68 pub max_history: usize,
70 pub enabled: bool,
72 pub target_frame_time: Duration,
74}
75
76impl Default for GpuTimerConfig {
77 fn default() -> Self {
78 Self {
79 max_history: 300,
80 enabled: true,
81 target_frame_time: Duration::from_micros(16_667), }
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct TimingStats {
89 pub min: Duration,
91 pub max: Duration,
93 pub mean: Duration,
95 pub median: Duration,
97 pub p95: Duration,
99 pub p99: Duration,
101 pub std_dev_us: f64,
103 pub sample_count: usize,
105}
106
107impl TimingStats {
108 #[allow(clippy::cast_precision_loss)]
110 #[must_use]
111 pub fn from_durations(durations: &[Duration]) -> Option<Self> {
112 if durations.is_empty() {
113 return None;
114 }
115
116 let mut sorted: Vec<Duration> = durations.to_vec();
117 sorted.sort();
118
119 let count = sorted.len();
120 let min = sorted[0];
121 let max = sorted[count - 1];
122 let median = sorted[count / 2];
123
124 let sum_us: f64 = sorted.iter().map(|d| d.as_micros() as f64).sum();
125 let mean_us = sum_us / count as f64;
126 let mean = Duration::from_micros(mean_us as u64);
127
128 let p95_idx = ((count as f64) * 0.95).ceil() as usize;
129 let p95 = sorted[p95_idx.min(count - 1)];
130
131 let p99_idx = ((count as f64) * 0.99).ceil() as usize;
132 let p99 = sorted[p99_idx.min(count - 1)];
133
134 let variance: f64 = sorted
135 .iter()
136 .map(|d| {
137 let diff = d.as_micros() as f64 - mean_us;
138 diff * diff
139 })
140 .sum::<f64>()
141 / count as f64;
142 let std_dev_us = variance.sqrt();
143
144 Some(Self {
145 min,
146 max,
147 mean,
148 median,
149 p95,
150 p99,
151 std_dev_us,
152 sample_count: count,
153 })
154 }
155
156 #[allow(clippy::cast_precision_loss)]
158 #[must_use]
159 pub fn mean_fps(&self) -> f64 {
160 let mean_secs = self.mean.as_secs_f64();
161 if mean_secs > 0.0 {
162 1.0 / mean_secs
163 } else {
164 0.0
165 }
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct FrameTimer {
172 history: VecDeque<Duration>,
174 max_history: usize,
176 frame_start: Option<Instant>,
178 total_frames: u64,
180}
181
182impl FrameTimer {
183 #[must_use]
185 pub fn new(max_history: usize) -> Self {
186 Self {
187 history: VecDeque::with_capacity(max_history),
188 max_history,
189 frame_start: None,
190 total_frames: 0,
191 }
192 }
193
194 pub fn begin_frame(&mut self) {
196 self.frame_start = Some(Instant::now());
197 }
198
199 pub fn end_frame(&mut self) -> Option<Duration> {
201 let start = self.frame_start.take()?;
202 let duration = start.elapsed();
203 if self.history.len() >= self.max_history {
204 self.history.pop_front();
205 }
206 self.history.push_back(duration);
207 self.total_frames += 1;
208 Some(duration)
209 }
210
211 #[must_use]
213 pub fn last_frame_time(&self) -> Option<Duration> {
214 self.history.back().copied()
215 }
216
217 #[allow(clippy::cast_precision_loss)]
219 #[must_use]
220 pub fn average_frame_time(&self) -> Option<Duration> {
221 if self.history.is_empty() {
222 return None;
223 }
224 let sum: Duration = self.history.iter().sum();
225 Some(sum / self.history.len() as u32)
226 }
227
228 #[must_use]
230 pub fn current_fps(&self) -> Option<f64> {
231 self.average_frame_time().map(|avg| 1.0 / avg.as_secs_f64())
232 }
233
234 #[must_use]
236 pub fn total_frames(&self) -> u64 {
237 self.total_frames
238 }
239
240 #[must_use]
242 pub fn stats(&self) -> Option<TimingStats> {
243 let durations: Vec<Duration> = self.history.iter().copied().collect();
244 TimingStats::from_durations(&durations)
245 }
246
247 pub fn clear(&mut self) {
249 self.history.clear();
250 self.frame_start = None;
251 }
252
253 #[must_use]
255 pub fn history_len(&self) -> usize {
256 self.history.len()
257 }
258}
259
260pub struct GpuTimer {
262 active_regions: Vec<TimerRegion>,
264 samples: VecDeque<TimingSample>,
266 frame_timer: FrameTimer,
268 config: GpuTimerConfig,
270 current_frame: u64,
272}
273
274impl GpuTimer {
275 #[must_use]
277 pub fn new() -> Self {
278 Self::with_config(GpuTimerConfig::default())
279 }
280
281 #[must_use]
283 pub fn with_config(config: GpuTimerConfig) -> Self {
284 let max_history = config.max_history;
285 Self {
286 active_regions: Vec::new(),
287 samples: VecDeque::with_capacity(max_history),
288 frame_timer: FrameTimer::new(max_history),
289 config,
290 current_frame: 0,
291 }
292 }
293
294 pub fn begin_region(&mut self, label: &str) -> usize {
296 if !self.config.enabled {
297 return 0;
298 }
299 let region = TimerRegion::start(label);
300 self.active_regions.push(region);
301 self.active_regions.len() - 1
302 }
303
304 pub fn end_region(&mut self, index: usize) -> Option<Duration> {
306 if !self.config.enabled || index >= self.active_regions.len() {
307 return None;
308 }
309 self.active_regions[index].stop();
310 let region = &self.active_regions[index];
311 let duration = region.elapsed();
312 let sample = TimingSample {
313 label: region.label.clone(),
314 duration,
315 frame_number: self.current_frame,
316 };
317 if self.samples.len() >= self.config.max_history {
318 self.samples.pop_front();
319 }
320 self.samples.push_back(sample);
321 Some(duration)
322 }
323
324 pub fn begin_frame(&mut self) {
326 self.current_frame += 1;
327 self.frame_timer.begin_frame();
328 self.active_regions.clear();
329 }
330
331 pub fn end_frame(&mut self) -> Option<Duration> {
333 self.frame_timer.end_frame()
334 }
335
336 #[must_use]
338 pub fn stats_for_label(&self, label: &str) -> Option<TimingStats> {
339 let durations: Vec<Duration> = self
340 .samples
341 .iter()
342 .filter(|s| s.label == label)
343 .map(|s| s.duration)
344 .collect();
345 TimingStats::from_durations(&durations)
346 }
347
348 #[must_use]
350 pub fn frame_stats(&self) -> Option<TimingStats> {
351 self.frame_timer.stats()
352 }
353
354 #[must_use]
356 pub fn current_fps(&self) -> Option<f64> {
357 self.frame_timer.current_fps()
358 }
359
360 #[must_use]
362 pub fn is_over_budget(&self) -> bool {
363 self.frame_timer
364 .average_frame_time()
365 .is_some_and(|avg| avg > self.config.target_frame_time)
366 }
367
368 #[must_use]
370 pub fn labels(&self) -> Vec<String> {
371 let mut labels: Vec<String> = self
372 .samples
373 .iter()
374 .map(|s| s.label.clone())
375 .collect::<std::collections::HashSet<_>>()
376 .into_iter()
377 .collect();
378 labels.sort();
379 labels
380 }
381
382 #[must_use]
384 pub fn sample_count(&self) -> usize {
385 self.samples.len()
386 }
387
388 #[must_use]
390 pub fn current_frame_number(&self) -> u64 {
391 self.current_frame
392 }
393
394 #[must_use]
396 pub fn is_enabled(&self) -> bool {
397 self.config.enabled
398 }
399
400 pub fn set_enabled(&mut self, enabled: bool) {
402 self.config.enabled = enabled;
403 }
404
405 pub fn reset(&mut self) {
407 self.active_regions.clear();
408 self.samples.clear();
409 self.frame_timer.clear();
410 self.current_frame = 0;
411 }
412}
413
414impl Default for GpuTimer {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_timer_region_start_stop() {
426 let mut region = TimerRegion::start("test");
427 assert!(!region.is_stopped());
428 region.stop();
429 assert!(region.is_stopped());
430 assert!(region.elapsed() < Duration::from_secs(1));
431 }
432
433 #[test]
434 fn test_timer_region_label() {
435 let region = TimerRegion::start("my_region");
436 assert_eq!(region.label, "my_region");
437 }
438
439 #[test]
440 fn test_timing_stats_basic() {
441 let durations = vec![
442 Duration::from_micros(100),
443 Duration::from_micros(200),
444 Duration::from_micros(300),
445 Duration::from_micros(400),
446 Duration::from_micros(500),
447 ];
448 let stats = TimingStats::from_durations(&durations).unwrap();
449 assert_eq!(stats.min, Duration::from_micros(100));
450 assert_eq!(stats.max, Duration::from_micros(500));
451 assert_eq!(stats.sample_count, 5);
452 assert_eq!(stats.median, Duration::from_micros(300));
453 }
454
455 #[test]
456 fn test_timing_stats_empty() {
457 let result = TimingStats::from_durations(&[]);
458 assert!(result.is_none());
459 }
460
461 #[test]
462 fn test_timing_stats_single() {
463 let durations = vec![Duration::from_micros(1000)];
464 let stats = TimingStats::from_durations(&durations).unwrap();
465 assert_eq!(stats.min, stats.max);
466 assert_eq!(stats.sample_count, 1);
467 assert!((stats.std_dev_us - 0.0).abs() < 0.001);
468 }
469
470 #[test]
471 fn test_timing_stats_mean_fps() {
472 let durations = vec![Duration::from_millis(16), Duration::from_millis(17)];
473 let stats = TimingStats::from_durations(&durations).unwrap();
474 let fps = stats.mean_fps();
475 assert!(fps > 50.0 && fps < 70.0);
476 }
477
478 #[test]
479 fn test_frame_timer_basic() {
480 let mut timer = FrameTimer::new(100);
481 timer.begin_frame();
482 let dur = timer.end_frame();
483 assert!(dur.is_some());
484 assert_eq!(timer.total_frames(), 1);
485 }
486
487 #[test]
488 fn test_frame_timer_history_limit() {
489 let mut timer = FrameTimer::new(3);
490 for _ in 0..5 {
491 timer.begin_frame();
492 timer.end_frame();
493 }
494 assert_eq!(timer.history_len(), 3);
495 assert_eq!(timer.total_frames(), 5);
496 }
497
498 #[test]
499 fn test_frame_timer_clear() {
500 let mut timer = FrameTimer::new(100);
501 timer.begin_frame();
502 timer.end_frame();
503 timer.clear();
504 assert_eq!(timer.history_len(), 0);
505 assert!(timer.last_frame_time().is_none());
506 }
507
508 #[test]
509 fn test_frame_timer_no_begin() {
510 let mut timer = FrameTimer::new(100);
511 let dur = timer.end_frame();
512 assert!(dur.is_none());
513 }
514
515 #[test]
516 fn test_gpu_timer_create() {
517 let timer = GpuTimer::new();
518 assert!(timer.is_enabled());
519 assert_eq!(timer.sample_count(), 0);
520 }
521
522 #[test]
523 fn test_gpu_timer_region() {
524 let mut timer = GpuTimer::new();
525 let idx = timer.begin_region("vertex_shader");
526 let dur = timer.end_region(idx);
527 assert!(dur.is_some());
528 assert_eq!(timer.sample_count(), 1);
529 }
530
531 #[test]
532 fn test_gpu_timer_frame_cycle() {
533 let mut timer = GpuTimer::new();
534 timer.begin_frame();
535 let _idx = timer.begin_region("pass1");
536 timer.end_region(0);
537 let frame_dur = timer.end_frame();
538 assert!(frame_dur.is_some());
539 assert_eq!(timer.current_frame_number(), 1);
540 }
541
542 #[test]
543 fn test_gpu_timer_labels() {
544 let mut timer = GpuTimer::new();
545 let i1 = timer.begin_region("alpha");
546 timer.end_region(i1);
547 let i2 = timer.begin_region("beta");
548 timer.end_region(i2);
549 let labels = timer.labels();
550 assert_eq!(labels.len(), 2);
551 assert!(labels.contains(&"alpha".to_string()));
552 assert!(labels.contains(&"beta".to_string()));
553 }
554
555 #[test]
556 fn test_gpu_timer_disabled() {
557 let config = GpuTimerConfig {
558 enabled: false,
559 ..Default::default()
560 };
561 let mut timer = GpuTimer::with_config(config);
562 assert!(!timer.is_enabled());
563 let idx = timer.begin_region("test");
564 assert_eq!(idx, 0);
565 let dur = timer.end_region(idx);
566 assert!(dur.is_none());
567 }
568
569 #[test]
570 fn test_gpu_timer_reset() {
571 let mut timer = GpuTimer::new();
572 timer.begin_frame();
573 let idx = timer.begin_region("test");
574 timer.end_region(idx);
575 timer.end_frame();
576 timer.reset();
577 assert_eq!(timer.sample_count(), 0);
578 assert_eq!(timer.current_frame_number(), 0);
579 }
580
581 #[test]
582 fn test_gpu_timer_set_enabled() {
583 let mut timer = GpuTimer::new();
584 assert!(timer.is_enabled());
585 timer.set_enabled(false);
586 assert!(!timer.is_enabled());
587 }
588
589 #[test]
590 fn test_gpu_timer_stats_for_label() {
591 let mut timer = GpuTimer::new();
592 for _ in 0..5 {
593 let idx = timer.begin_region("compute");
594 timer.end_region(idx);
595 }
596 let stats = timer.stats_for_label("compute");
597 assert!(stats.is_some());
598 assert_eq!(stats.unwrap().sample_count, 5);
599 }
600
601 #[test]
602 fn test_gpu_timer_over_budget() {
603 let config = GpuTimerConfig {
604 target_frame_time: Duration::from_nanos(1), ..Default::default()
606 };
607 let mut timer = GpuTimer::with_config(config);
608 timer.begin_frame();
609 let _x: u64 = (0..1000).sum();
611 timer.end_frame();
612 assert!(timer.is_over_budget());
613 }
614}