use std::time::Instant;
#[derive(Debug, Clone)]
pub struct ProfileScope {
pub name: &'static str,
pub start_ns: u64,
pub end_ns: u64,
pub depth: u32,
}
impl ProfileScope {
#[inline]
pub fn duration_ms(&self) -> f64 {
(self.end_ns - self.start_ns) as f64 / 1_000_000.0
}
#[inline]
pub fn duration_us(&self) -> f64 {
(self.end_ns - self.start_ns) as f64 / 1_000.0
}
}
#[derive(Debug, Clone, Default)]
pub struct FrameProfile {
pub scopes: Vec<ProfileScope>,
pub frame_number: u64,
pub total_ms: f64,
}
pub struct FrameProfiler {
history: Vec<FrameProfile>,
write_idx: usize,
frame_count: u64,
active_scopes: Vec<(&'static str, u64, u32)>, current_scopes: Vec<ProfileScope>,
current_depth: u32,
frame_start: Instant,
epoch: Instant,
pub enabled: bool,
}
const HISTORY_SIZE: usize = 300;
impl FrameProfiler {
pub fn new() -> Self {
let now = Instant::now();
Self {
history: Vec::with_capacity(HISTORY_SIZE),
write_idx: 0,
frame_count: 0,
active_scopes: Vec::with_capacity(16),
current_scopes: Vec::with_capacity(32),
current_depth: 0,
frame_start: now,
epoch: now,
enabled: true,
}
}
#[inline]
pub fn begin_scope(&mut self, name: &'static str) {
if !self.enabled {
return;
}
let now_ns = self.epoch.elapsed().as_nanos() as u64;
self.active_scopes.push((name, now_ns, self.current_depth));
self.current_depth += 1;
}
#[inline]
pub fn end_scope(&mut self, name: &'static str) {
if !self.enabled {
return;
}
let now_ns = self.epoch.elapsed().as_nanos() as u64;
if let Some(pos) = self.active_scopes.iter().rposition(|(n, _, _)| *n == name) {
let (_, start_ns, depth) = self.active_scopes.remove(pos);
self.current_depth = self.current_depth.saturating_sub(1);
self.current_scopes.push(ProfileScope {
name,
start_ns,
end_ns: now_ns,
depth,
});
}
}
pub fn end_frame(&mut self) {
if !self.enabled {
return;
}
let total_ms = self.frame_start.elapsed().as_secs_f64() * 1000.0;
let profile = FrameProfile {
scopes: std::mem::take(&mut self.current_scopes),
frame_number: self.frame_count,
total_ms,
};
if self.history.len() < HISTORY_SIZE {
self.history.push(profile);
} else {
self.history[self.write_idx] = profile;
}
self.write_idx = (self.write_idx + 1) % HISTORY_SIZE;
self.frame_count += 1;
self.frame_start = Instant::now();
self.active_scopes.clear();
self.current_depth = 0;
}
pub fn last_frame(&self) -> Option<&FrameProfile> {
if self.history.is_empty() {
return None;
}
let idx = if self.write_idx == 0 {
self.history.len() - 1
} else {
self.write_idx - 1
};
self.history.get(idx)
}
pub fn avg_frame_ms(&self, n: usize) -> f64 {
let count = n.min(self.history.len());
if count == 0 {
return 0.0;
}
let sum: f64 = self
.history
.iter()
.rev()
.take(count)
.map(|p| p.total_ms)
.sum();
sum / count as f64
}
pub fn avg_scope_ms(&self, name: &str, n: usize) -> f64 {
let count = n.min(self.history.len());
if count == 0 {
return 0.0;
}
let mut total = 0.0;
let mut found = 0;
for profile in self.history.iter().rev().take(count) {
for scope in &profile.scopes {
if scope.name == name {
total += scope.duration_ms();
found += 1;
}
}
}
if found == 0 {
0.0
} else {
total / found as f64
}
}
pub fn history(&self) -> &[FrameProfile] {
&self.history
}
pub fn frame_count(&self) -> u64 {
self.frame_count
}
pub fn estimated_fps(&self) -> f64 {
let avg = self.avg_frame_ms(60);
if avg > 0.0 {
1000.0 / avg
} else {
0.0
}
}
}
impl Default for FrameProfiler {
fn default() -> Self {
Self::new()
}
}
pub struct ProfileGuard<'a> {
profiler: &'a mut FrameProfiler,
name: &'static str,
}
impl<'a> Drop for ProfileGuard<'a> {
fn drop(&mut self) {
self.profiler.end_scope(self.name);
}
}
impl FrameProfiler {
pub fn scope_guard(&mut self, name: &'static str) -> ProfileGuard<'_> {
self.begin_scope(name);
ProfileGuard {
profiler: self,
name,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_profiling() {
let mut profiler = FrameProfiler::new();
profiler.begin_scope("test_scope");
std::thread::sleep(std::time::Duration::from_millis(1));
profiler.end_scope("test_scope");
profiler.end_frame();
let last = profiler.last_frame().unwrap();
assert_eq!(last.scopes.len(), 1);
assert_eq!(last.scopes[0].name, "test_scope");
assert!(last.scopes[0].duration_ms() > 0.5);
assert_eq!(last.scopes[0].depth, 0);
}
#[test]
fn test_nested_scopes() {
let mut profiler = FrameProfiler::new();
profiler.begin_scope("outer");
profiler.begin_scope("inner");
profiler.end_scope("inner");
profiler.end_scope("outer");
profiler.end_frame();
let last = profiler.last_frame().unwrap();
assert_eq!(last.scopes.len(), 2);
assert_eq!(last.scopes[0].name, "inner");
assert_eq!(last.scopes[0].depth, 1);
assert_eq!(last.scopes[1].name, "outer");
assert_eq!(last.scopes[1].depth, 0);
}
#[test]
fn test_ring_buffer() {
let mut profiler = FrameProfiler::new();
for _i in 0..350 {
profiler.begin_scope("frame_scope");
profiler.end_scope("frame_scope");
profiler.end_frame();
}
assert_eq!(profiler.history().len(), HISTORY_SIZE);
assert_eq!(profiler.frame_count(), 350);
}
#[test]
fn test_avg_fps() {
let mut profiler = FrameProfiler::new();
for _ in 0..10 {
profiler.end_frame();
}
assert!(profiler.estimated_fps() > 100.0);
}
#[test]
fn test_disabled_profiler() {
let mut profiler = FrameProfiler::new();
profiler.enabled = false;
profiler.begin_scope("disabled_scope");
profiler.end_scope("disabled_scope");
profiler.end_frame();
assert!(profiler.history().is_empty());
}
}