use crate::EngineResult;
use std::collections::VecDeque;
use std::time::{Duration, Instant};
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub fps: f32,
pub frame_time_ms: f32,
pub avg_frame_time_ms: f32,
pub min_frame_time_ms: f32,
pub max_frame_time_ms: f32,
pub memory_usage_mb: f32,
pub draw_calls: u32,
pub entities_rendered: u32,
pub physics_time_ms: f32,
pub render_time_ms: f32,
pub update_time_ms: f32,
pub cpu_usage_percent: f32,
}
impl Default for PerformanceMetrics {
fn default() -> Self {
Self {
fps: 0.0,
frame_time_ms: 0.0,
avg_frame_time_ms: 0.0,
min_frame_time_ms: 0.0,
max_frame_time_ms: 0.0,
memory_usage_mb: 0.0,
draw_calls: 0,
entities_rendered: 0,
physics_time_ms: 0.0,
render_time_ms: 0.0,
update_time_ms: 0.0,
cpu_usage_percent: 0.0,
}
}
}
#[derive(Debug)]
struct FrameData {
frame_time: Duration,
draw_calls: u32,
entities_rendered: u32,
physics_time: Duration,
render_time: Duration,
update_time: Duration,
}
pub struct PerformanceProfiler {
enabled: bool,
frame_history: VecDeque<FrameData>,
max_history_size: usize,
frame_start_time: Instant,
current_frame: FrameData,
section_start_time: Option<Instant>,
current_section: ProfileSection,
initial_memory: Option<usize>,
fps_timer: Instant,
fps_frame_count: u32,
current_fps: f32,
show_overlay: bool,
overlay_position: OverlayPosition,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProfileSection {
Update,
Physics,
Render,
None,
}
#[derive(Debug, Clone, Copy)]
pub enum OverlayPosition {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl Default for PerformanceProfiler {
fn default() -> Self {
Self::new()
}
}
impl PerformanceProfiler {
pub fn new() -> Self {
let now = Instant::now();
Self {
enabled: true,
frame_history: VecDeque::new(),
max_history_size: 60,
frame_start_time: now,
current_frame: FrameData {
frame_time: Duration::ZERO,
draw_calls: 0,
entities_rendered: 0,
physics_time: Duration::ZERO,
render_time: Duration::ZERO,
update_time: Duration::ZERO,
},
section_start_time: None,
current_section: ProfileSection::None,
initial_memory: None,
fps_timer: now,
fps_frame_count: 0,
current_fps: 0.0,
show_overlay: false,
overlay_position: OverlayPosition::TopLeft,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
if enabled {
self.reset();
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_show_overlay(&mut self, show: bool) {
self.show_overlay = show;
}
pub fn is_overlay_visible(&self) -> bool {
self.show_overlay && self.enabled
}
pub fn set_overlay_position(&mut self, position: OverlayPosition) {
self.overlay_position = position;
}
pub fn get_overlay_position(&self) -> OverlayPosition {
self.overlay_position
}
pub fn reset(&mut self) {
let now = Instant::now();
self.frame_history.clear();
self.frame_start_time = now;
self.fps_timer = now;
self.fps_frame_count = 0;
self.current_fps = 0.0;
self.initial_memory = None;
self.end_section();
}
pub fn begin_frame(&mut self) {
if !self.enabled {
return;
}
self.frame_start_time = Instant::now();
self.current_frame = FrameData {
frame_time: Duration::ZERO,
draw_calls: 0,
entities_rendered: 0,
physics_time: Duration::ZERO,
render_time: Duration::ZERO,
update_time: Duration::ZERO,
};
self.current_section = ProfileSection::None;
self.section_start_time = None;
}
pub fn end_frame(&mut self) {
if !self.enabled {
return;
}
self.end_section();
let frame_time = self.frame_start_time.elapsed();
self.current_frame.frame_time = frame_time;
self.frame_history.push_back(FrameData {
frame_time: self.current_frame.frame_time,
draw_calls: self.current_frame.draw_calls,
entities_rendered: self.current_frame.entities_rendered,
physics_time: self.current_frame.physics_time,
render_time: self.current_frame.render_time,
update_time: self.current_frame.update_time,
});
while self.frame_history.len() > self.max_history_size {
self.frame_history.pop_front();
}
self.fps_frame_count += 1;
let fps_elapsed = self.fps_timer.elapsed();
if fps_elapsed >= Duration::from_millis(500) {
self.current_fps = self.fps_frame_count as f32 / fps_elapsed.as_secs_f32();
self.fps_frame_count = 0;
self.fps_timer = Instant::now();
}
}
pub fn begin_section(&mut self, section: ProfileSection) {
if !self.enabled {
return;
}
self.end_section();
self.current_section = section;
self.section_start_time = Some(Instant::now());
}
pub fn end_section(&mut self) {
if !self.enabled {
return;
}
if let Some(start_time) = self.section_start_time.take() {
let elapsed = start_time.elapsed();
match self.current_section {
ProfileSection::Update => {
self.current_frame.update_time += elapsed;
}
ProfileSection::Physics => {
self.current_frame.physics_time += elapsed;
}
ProfileSection::Render => {
self.current_frame.render_time += elapsed;
}
ProfileSection::None => {}
}
}
self.current_section = ProfileSection::None;
}
pub fn add_draw_call(&mut self) {
if self.enabled {
self.current_frame.draw_calls += 1;
}
}
pub fn add_entities_rendered(&mut self, count: u32) {
if self.enabled {
self.current_frame.entities_rendered += count;
}
}
pub fn get_current_metrics(&self) -> PerformanceMetrics {
if !self.enabled || self.frame_history.is_empty() {
return PerformanceMetrics::default();
}
let frame_times: Vec<f32> = self.frame_history
.iter()
.map(|frame| frame.frame_time.as_secs_f32() * 1000.0)
.collect();
let current_frame_time = self.current_frame.frame_time.as_secs_f32() * 1000.0;
let avg_frame_time = if !frame_times.is_empty() {
frame_times.iter().sum::<f32>() / frame_times.len() as f32
} else {
current_frame_time
};
let min_frame_time = frame_times
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.copied()
.unwrap_or(current_frame_time);
let max_frame_time = frame_times
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.copied()
.unwrap_or(current_frame_time);
let memory_usage = self.get_memory_usage_mb();
PerformanceMetrics {
fps: self.current_fps,
frame_time_ms: current_frame_time,
avg_frame_time_ms: avg_frame_time,
min_frame_time_ms: min_frame_time,
max_frame_time_ms: max_frame_time,
memory_usage_mb: memory_usage,
draw_calls: self.current_frame.draw_calls,
entities_rendered: self.current_frame.entities_rendered,
physics_time_ms: self.current_frame.physics_time.as_secs_f32() * 1000.0,
render_time_ms: self.current_frame.render_time.as_secs_f32() * 1000.0,
update_time_ms: self.current_frame.update_time.as_secs_f32() * 1000.0,
cpu_usage_percent: self.get_cpu_usage_percent(),
}
}
pub fn get_frame_history(&self) -> Vec<f32> {
self.frame_history
.iter()
.map(|frame| frame.frame_time.as_secs_f32() * 1000.0)
.collect()
}
pub fn get_fps_history(&self) -> Vec<f32> {
self.frame_history
.iter()
.map(|frame| {
let frame_time_secs = frame.frame_time.as_secs_f32();
if frame_time_secs > 0.0 {
1.0 / frame_time_secs
} else {
0.0
}
})
.collect()
}
fn get_memory_usage_mb(&self) -> f32 {
#[cfg(target_os = "windows")]
{
self.get_memory_usage_windows()
}
#[cfg(not(target_os = "windows"))]
{
0.0
}
}
#[cfg(target_os = "windows")]
fn get_memory_usage_windows(&self) -> f32 {
0.0
}
fn get_cpu_usage_percent(&self) -> f32 {
0.0
}
pub fn export_metrics_csv(&self, duration_seconds: f32) -> EngineResult<String> {
let mut csv = String::new();
csv.push_str("timestamp_ms,fps,frame_time_ms,draw_calls,entities_rendered,physics_time_ms,render_time_ms,update_time_ms\n");
let frames_to_export = (duration_seconds * 60.0) as usize;
let start_index = if self.frame_history.len() > frames_to_export {
self.frame_history.len() - frames_to_export
} else {
0
};
for (i, frame) in self.frame_history.iter().enumerate().skip(start_index) {
let timestamp_ms = i as f32 * (1000.0 / 60.0);
let fps = if frame.frame_time.as_secs_f32() > 0.0 {
1.0 / frame.frame_time.as_secs_f32()
} else {
0.0
};
csv.push_str(&format!(
"{},{:.2},{:.2},{},{},{:.2},{:.2},{:.2}\n",
timestamp_ms,
fps,
frame.frame_time.as_secs_f32() * 1000.0,
frame.draw_calls,
frame.entities_rendered,
frame.physics_time.as_secs_f32() * 1000.0,
frame.render_time.as_secs_f32() * 1000.0,
frame.update_time.as_secs_f32() * 1000.0,
));
}
Ok(csv)
}
pub fn print_summary(&self) {
if !self.enabled {
println!("Profiler is disabled");
return;
}
let metrics = self.get_current_metrics();
println!("=== Performance Summary ===");
println!("FPS: {:.1}", metrics.fps);
println!("Frame Time: {:.2}ms (avg: {:.2}ms)", metrics.frame_time_ms, metrics.avg_frame_time_ms);
println!("Min/Max Frame Time: {:.2}ms / {:.2}ms", metrics.min_frame_time_ms, metrics.max_frame_time_ms);
println!("Memory Usage: {:.1}MB", metrics.memory_usage_mb);
println!("Draw Calls: {}", metrics.draw_calls);
println!("Entities Rendered: {}", metrics.entities_rendered);
println!("Timing Breakdown:");
println!(" Update: {:.2}ms", metrics.update_time_ms);
println!(" Physics: {:.2}ms", metrics.physics_time_ms);
println!(" Render: {:.2}ms", metrics.render_time_ms);
println!("CPU Usage: {:.1}%", metrics.cpu_usage_percent);
}
pub fn get_overlay_text(&self) -> String {
if !self.is_overlay_visible() {
return String::new();
}
let metrics = self.get_current_metrics();
format!(
"FPS: {:.1}\nFrame: {:.1}ms\nDraw Calls: {}\nMemory: {:.1}MB",
metrics.fps,
metrics.frame_time_ms,
metrics.draw_calls,
metrics.memory_usage_mb
)
}
pub fn toggle_overlay(&mut self) {
self.show_overlay = !self.show_overlay;
}
}
#[macro_export]
macro_rules! profile_section {
($profiler:expr, $section:expr, $code:block) => {
$profiler.begin_section($section);
let result = $code;
$profiler.end_section();
result
};
}
#[macro_export]
macro_rules! profile_scope {
($profiler:expr, $section:expr) => {
$profiler.begin_section($section);
defer! { $profiler.end_section(); }
};
}
pub struct ProfileGuard<'a> {
profiler: &'a mut PerformanceProfiler,
}
impl<'a> ProfileGuard<'a> {
pub fn new(profiler: &'a mut PerformanceProfiler, section: ProfileSection) -> Self {
profiler.begin_section(section);
Self { profiler }
}
}
impl<'a> Drop for ProfileGuard<'a> {
fn drop(&mut self) {
self.profiler.end_section();
}
}