#![allow(dead_code)]
use std::collections::VecDeque;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct TimerRegion {
pub label: String,
pub start: Instant,
pub end: Option<Instant>,
}
impl TimerRegion {
#[must_use]
pub fn start(label: &str) -> Self {
Self {
label: label.to_string(),
start: Instant::now(),
end: None,
}
}
pub fn stop(&mut self) {
self.end = Some(Instant::now());
}
#[must_use]
pub fn elapsed(&self) -> Duration {
match self.end {
Some(end) => end.duration_since(self.start),
None => self.start.elapsed(),
}
}
#[must_use]
pub fn is_stopped(&self) -> bool {
self.end.is_some()
}
}
#[derive(Debug, Clone)]
pub struct TimingSample {
pub label: String,
pub duration: Duration,
pub frame_number: u64,
}
#[derive(Debug, Clone)]
pub struct GpuTimerConfig {
pub max_history: usize,
pub enabled: bool,
pub target_frame_time: Duration,
}
impl Default for GpuTimerConfig {
fn default() -> Self {
Self {
max_history: 300,
enabled: true,
target_frame_time: Duration::from_micros(16_667), }
}
}
#[derive(Debug, Clone)]
pub struct TimingStats {
pub min: Duration,
pub max: Duration,
pub mean: Duration,
pub median: Duration,
pub p95: Duration,
pub p99: Duration,
pub std_dev_us: f64,
pub sample_count: usize,
}
impl TimingStats {
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn from_durations(durations: &[Duration]) -> Option<Self> {
if durations.is_empty() {
return None;
}
let mut sorted: Vec<Duration> = durations.to_vec();
sorted.sort();
let count = sorted.len();
let min = sorted[0];
let max = sorted[count - 1];
let median = sorted[count / 2];
let sum_us: f64 = sorted.iter().map(|d| d.as_micros() as f64).sum();
let mean_us = sum_us / count as f64;
let mean = Duration::from_micros(mean_us as u64);
let p95_idx = ((count as f64) * 0.95).ceil() as usize;
let p95 = sorted[p95_idx.min(count - 1)];
let p99_idx = ((count as f64) * 0.99).ceil() as usize;
let p99 = sorted[p99_idx.min(count - 1)];
let variance: f64 = sorted
.iter()
.map(|d| {
let diff = d.as_micros() as f64 - mean_us;
diff * diff
})
.sum::<f64>()
/ count as f64;
let std_dev_us = variance.sqrt();
Some(Self {
min,
max,
mean,
median,
p95,
p99,
std_dev_us,
sample_count: count,
})
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn mean_fps(&self) -> f64 {
let mean_secs = self.mean.as_secs_f64();
if mean_secs > 0.0 {
1.0 / mean_secs
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct FrameTimer {
history: VecDeque<Duration>,
max_history: usize,
frame_start: Option<Instant>,
total_frames: u64,
}
impl FrameTimer {
#[must_use]
pub fn new(max_history: usize) -> Self {
Self {
history: VecDeque::with_capacity(max_history),
max_history,
frame_start: None,
total_frames: 0,
}
}
pub fn begin_frame(&mut self) {
self.frame_start = Some(Instant::now());
}
pub fn end_frame(&mut self) -> Option<Duration> {
let start = self.frame_start.take()?;
let duration = start.elapsed();
if self.history.len() >= self.max_history {
self.history.pop_front();
}
self.history.push_back(duration);
self.total_frames += 1;
Some(duration)
}
#[must_use]
pub fn last_frame_time(&self) -> Option<Duration> {
self.history.back().copied()
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn average_frame_time(&self) -> Option<Duration> {
if self.history.is_empty() {
return None;
}
let sum: Duration = self.history.iter().sum();
Some(sum / self.history.len() as u32)
}
#[must_use]
pub fn current_fps(&self) -> Option<f64> {
self.average_frame_time().map(|avg| 1.0 / avg.as_secs_f64())
}
#[must_use]
pub fn total_frames(&self) -> u64 {
self.total_frames
}
#[must_use]
pub fn stats(&self) -> Option<TimingStats> {
let durations: Vec<Duration> = self.history.iter().copied().collect();
TimingStats::from_durations(&durations)
}
pub fn clear(&mut self) {
self.history.clear();
self.frame_start = None;
}
#[must_use]
pub fn history_len(&self) -> usize {
self.history.len()
}
}
pub struct GpuTimer {
active_regions: Vec<TimerRegion>,
samples: VecDeque<TimingSample>,
frame_timer: FrameTimer,
config: GpuTimerConfig,
current_frame: u64,
}
impl GpuTimer {
#[must_use]
pub fn new() -> Self {
Self::with_config(GpuTimerConfig::default())
}
#[must_use]
pub fn with_config(config: GpuTimerConfig) -> Self {
let max_history = config.max_history;
Self {
active_regions: Vec::new(),
samples: VecDeque::with_capacity(max_history),
frame_timer: FrameTimer::new(max_history),
config,
current_frame: 0,
}
}
pub fn begin_region(&mut self, label: &str) -> usize {
if !self.config.enabled {
return 0;
}
let region = TimerRegion::start(label);
self.active_regions.push(region);
self.active_regions.len() - 1
}
pub fn end_region(&mut self, index: usize) -> Option<Duration> {
if !self.config.enabled || index >= self.active_regions.len() {
return None;
}
self.active_regions[index].stop();
let region = &self.active_regions[index];
let duration = region.elapsed();
let sample = TimingSample {
label: region.label.clone(),
duration,
frame_number: self.current_frame,
};
if self.samples.len() >= self.config.max_history {
self.samples.pop_front();
}
self.samples.push_back(sample);
Some(duration)
}
pub fn begin_frame(&mut self) {
self.current_frame += 1;
self.frame_timer.begin_frame();
self.active_regions.clear();
}
pub fn end_frame(&mut self) -> Option<Duration> {
self.frame_timer.end_frame()
}
#[must_use]
pub fn stats_for_label(&self, label: &str) -> Option<TimingStats> {
let durations: Vec<Duration> = self
.samples
.iter()
.filter(|s| s.label == label)
.map(|s| s.duration)
.collect();
TimingStats::from_durations(&durations)
}
#[must_use]
pub fn frame_stats(&self) -> Option<TimingStats> {
self.frame_timer.stats()
}
#[must_use]
pub fn current_fps(&self) -> Option<f64> {
self.frame_timer.current_fps()
}
#[must_use]
pub fn is_over_budget(&self) -> bool {
self.frame_timer
.average_frame_time()
.is_some_and(|avg| avg > self.config.target_frame_time)
}
#[must_use]
pub fn labels(&self) -> Vec<String> {
let mut labels: Vec<String> = self
.samples
.iter()
.map(|s| s.label.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
labels.sort();
labels
}
#[must_use]
pub fn sample_count(&self) -> usize {
self.samples.len()
}
#[must_use]
pub fn current_frame_number(&self) -> u64 {
self.current_frame
}
#[must_use]
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
self.config.enabled = enabled;
}
pub fn reset(&mut self) {
self.active_regions.clear();
self.samples.clear();
self.frame_timer.clear();
self.current_frame = 0;
}
}
impl Default for GpuTimer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timer_region_start_stop() {
let mut region = TimerRegion::start("test");
assert!(!region.is_stopped());
region.stop();
assert!(region.is_stopped());
assert!(region.elapsed() < Duration::from_secs(1));
}
#[test]
fn test_timer_region_label() {
let region = TimerRegion::start("my_region");
assert_eq!(region.label, "my_region");
}
#[test]
fn test_timing_stats_basic() {
let durations = vec![
Duration::from_micros(100),
Duration::from_micros(200),
Duration::from_micros(300),
Duration::from_micros(400),
Duration::from_micros(500),
];
let stats = TimingStats::from_durations(&durations)
.expect("from_durations should succeed with valid durations");
assert_eq!(stats.min, Duration::from_micros(100));
assert_eq!(stats.max, Duration::from_micros(500));
assert_eq!(stats.sample_count, 5);
assert_eq!(stats.median, Duration::from_micros(300));
}
#[test]
fn test_timing_stats_empty() {
let result = TimingStats::from_durations(&[]);
assert!(result.is_none());
}
#[test]
fn test_timing_stats_single() {
let durations = vec![Duration::from_millis(1)];
let stats = TimingStats::from_durations(&durations)
.expect("from_durations should succeed with valid durations");
assert_eq!(stats.min, stats.max);
assert_eq!(stats.sample_count, 1);
assert!((stats.std_dev_us - 0.0).abs() < 0.001);
}
#[test]
fn test_timing_stats_mean_fps() {
let durations = vec![Duration::from_millis(16), Duration::from_millis(17)];
let stats = TimingStats::from_durations(&durations)
.expect("from_durations should succeed with valid durations");
let fps = stats.mean_fps();
assert!(fps > 50.0 && fps < 70.0);
}
#[test]
fn test_frame_timer_basic() {
let mut timer = FrameTimer::new(100);
timer.begin_frame();
let dur = timer.end_frame();
assert!(dur.is_some());
assert_eq!(timer.total_frames(), 1);
}
#[test]
fn test_frame_timer_history_limit() {
let mut timer = FrameTimer::new(3);
for _ in 0..5 {
timer.begin_frame();
timer.end_frame();
}
assert_eq!(timer.history_len(), 3);
assert_eq!(timer.total_frames(), 5);
}
#[test]
fn test_frame_timer_clear() {
let mut timer = FrameTimer::new(100);
timer.begin_frame();
timer.end_frame();
timer.clear();
assert_eq!(timer.history_len(), 0);
assert!(timer.last_frame_time().is_none());
}
#[test]
fn test_frame_timer_no_begin() {
let mut timer = FrameTimer::new(100);
let dur = timer.end_frame();
assert!(dur.is_none());
}
#[test]
fn test_gpu_timer_create() {
let timer = GpuTimer::new();
assert!(timer.is_enabled());
assert_eq!(timer.sample_count(), 0);
}
#[test]
fn test_gpu_timer_region() {
let mut timer = GpuTimer::new();
let idx = timer.begin_region("vertex_shader");
let dur = timer.end_region(idx);
assert!(dur.is_some());
assert_eq!(timer.sample_count(), 1);
}
#[test]
fn test_gpu_timer_frame_cycle() {
let mut timer = GpuTimer::new();
timer.begin_frame();
let _idx = timer.begin_region("pass1");
timer.end_region(0);
let frame_dur = timer.end_frame();
assert!(frame_dur.is_some());
assert_eq!(timer.current_frame_number(), 1);
}
#[test]
fn test_gpu_timer_labels() {
let mut timer = GpuTimer::new();
let i1 = timer.begin_region("alpha");
timer.end_region(i1);
let i2 = timer.begin_region("beta");
timer.end_region(i2);
let labels = timer.labels();
assert_eq!(labels.len(), 2);
assert!(labels.contains(&"alpha".to_string()));
assert!(labels.contains(&"beta".to_string()));
}
#[test]
fn test_gpu_timer_disabled() {
let config = GpuTimerConfig {
enabled: false,
..Default::default()
};
let mut timer = GpuTimer::with_config(config);
assert!(!timer.is_enabled());
let idx = timer.begin_region("test");
assert_eq!(idx, 0);
let dur = timer.end_region(idx);
assert!(dur.is_none());
}
#[test]
fn test_gpu_timer_reset() {
let mut timer = GpuTimer::new();
timer.begin_frame();
let idx = timer.begin_region("test");
timer.end_region(idx);
timer.end_frame();
timer.reset();
assert_eq!(timer.sample_count(), 0);
assert_eq!(timer.current_frame_number(), 0);
}
#[test]
fn test_gpu_timer_set_enabled() {
let mut timer = GpuTimer::new();
assert!(timer.is_enabled());
timer.set_enabled(false);
assert!(!timer.is_enabled());
}
#[test]
fn test_gpu_timer_stats_for_label() {
let mut timer = GpuTimer::new();
for _ in 0..5 {
let idx = timer.begin_region("compute");
timer.end_region(idx);
}
let stats = timer.stats_for_label("compute");
assert!(stats.is_some());
assert_eq!(stats.expect("stats should be available").sample_count, 5);
}
#[test]
fn test_gpu_timer_over_budget() {
let config = GpuTimerConfig {
target_frame_time: Duration::from_nanos(1), ..Default::default()
};
let mut timer = GpuTimer::with_config(config);
timer.begin_frame();
let _x: u64 = (0..1000).sum();
timer.end_frame();
assert!(timer.is_over_budget());
}
}