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)
449 .expect("from_durations should succeed with valid durations");
450 assert_eq!(stats.min, Duration::from_micros(100));
451 assert_eq!(stats.max, Duration::from_micros(500));
452 assert_eq!(stats.sample_count, 5);
453 assert_eq!(stats.median, Duration::from_micros(300));
454 }
455
456 #[test]
457 fn test_timing_stats_empty() {
458 let result = TimingStats::from_durations(&[]);
459 assert!(result.is_none());
460 }
461
462 #[test]
463 fn test_timing_stats_single() {
464 let durations = vec![Duration::from_millis(1)];
465 let stats = TimingStats::from_durations(&durations)
466 .expect("from_durations should succeed with valid durations");
467 assert_eq!(stats.min, stats.max);
468 assert_eq!(stats.sample_count, 1);
469 assert!((stats.std_dev_us - 0.0).abs() < 0.001);
470 }
471
472 #[test]
473 fn test_timing_stats_mean_fps() {
474 let durations = vec![Duration::from_millis(16), Duration::from_millis(17)];
475 let stats = TimingStats::from_durations(&durations)
476 .expect("from_durations should succeed with valid durations");
477 let fps = stats.mean_fps();
478 assert!(fps > 50.0 && fps < 70.0);
479 }
480
481 #[test]
482 fn test_frame_timer_basic() {
483 let mut timer = FrameTimer::new(100);
484 timer.begin_frame();
485 let dur = timer.end_frame();
486 assert!(dur.is_some());
487 assert_eq!(timer.total_frames(), 1);
488 }
489
490 #[test]
491 fn test_frame_timer_history_limit() {
492 let mut timer = FrameTimer::new(3);
493 for _ in 0..5 {
494 timer.begin_frame();
495 timer.end_frame();
496 }
497 assert_eq!(timer.history_len(), 3);
498 assert_eq!(timer.total_frames(), 5);
499 }
500
501 #[test]
502 fn test_frame_timer_clear() {
503 let mut timer = FrameTimer::new(100);
504 timer.begin_frame();
505 timer.end_frame();
506 timer.clear();
507 assert_eq!(timer.history_len(), 0);
508 assert!(timer.last_frame_time().is_none());
509 }
510
511 #[test]
512 fn test_frame_timer_no_begin() {
513 let mut timer = FrameTimer::new(100);
514 let dur = timer.end_frame();
515 assert!(dur.is_none());
516 }
517
518 #[test]
519 fn test_gpu_timer_create() {
520 let timer = GpuTimer::new();
521 assert!(timer.is_enabled());
522 assert_eq!(timer.sample_count(), 0);
523 }
524
525 #[test]
526 fn test_gpu_timer_region() {
527 let mut timer = GpuTimer::new();
528 let idx = timer.begin_region("vertex_shader");
529 let dur = timer.end_region(idx);
530 assert!(dur.is_some());
531 assert_eq!(timer.sample_count(), 1);
532 }
533
534 #[test]
535 fn test_gpu_timer_frame_cycle() {
536 let mut timer = GpuTimer::new();
537 timer.begin_frame();
538 let _idx = timer.begin_region("pass1");
539 timer.end_region(0);
540 let frame_dur = timer.end_frame();
541 assert!(frame_dur.is_some());
542 assert_eq!(timer.current_frame_number(), 1);
543 }
544
545 #[test]
546 fn test_gpu_timer_labels() {
547 let mut timer = GpuTimer::new();
548 let i1 = timer.begin_region("alpha");
549 timer.end_region(i1);
550 let i2 = timer.begin_region("beta");
551 timer.end_region(i2);
552 let labels = timer.labels();
553 assert_eq!(labels.len(), 2);
554 assert!(labels.contains(&"alpha".to_string()));
555 assert!(labels.contains(&"beta".to_string()));
556 }
557
558 #[test]
559 fn test_gpu_timer_disabled() {
560 let config = GpuTimerConfig {
561 enabled: false,
562 ..Default::default()
563 };
564 let mut timer = GpuTimer::with_config(config);
565 assert!(!timer.is_enabled());
566 let idx = timer.begin_region("test");
567 assert_eq!(idx, 0);
568 let dur = timer.end_region(idx);
569 assert!(dur.is_none());
570 }
571
572 #[test]
573 fn test_gpu_timer_reset() {
574 let mut timer = GpuTimer::new();
575 timer.begin_frame();
576 let idx = timer.begin_region("test");
577 timer.end_region(idx);
578 timer.end_frame();
579 timer.reset();
580 assert_eq!(timer.sample_count(), 0);
581 assert_eq!(timer.current_frame_number(), 0);
582 }
583
584 #[test]
585 fn test_gpu_timer_set_enabled() {
586 let mut timer = GpuTimer::new();
587 assert!(timer.is_enabled());
588 timer.set_enabled(false);
589 assert!(!timer.is_enabled());
590 }
591
592 #[test]
593 fn test_gpu_timer_stats_for_label() {
594 let mut timer = GpuTimer::new();
595 for _ in 0..5 {
596 let idx = timer.begin_region("compute");
597 timer.end_region(idx);
598 }
599 let stats = timer.stats_for_label("compute");
600 assert!(stats.is_some());
601 assert_eq!(stats.expect("stats should be available").sample_count, 5);
602 }
603
604 #[test]
605 fn test_gpu_timer_over_budget() {
606 let config = GpuTimerConfig {
607 target_frame_time: Duration::from_nanos(1), ..Default::default()
609 };
610 let mut timer = GpuTimer::with_config(config);
611 timer.begin_frame();
612 let _x: u64 = (0..1000).sum();
614 timer.end_frame();
615 assert!(timer.is_over_budget());
616 }
617}