use crate::error::{EcsError, Result};
use crate::schedule::Schedule;
use crate::system::{System, SystemId};
use crate::World;
use rustc_hash::FxHashMap;
use std::time::{Duration, Instant};
#[cfg(feature = "profiling")]
use tracing::info_span;
#[derive(Debug, Clone)]
pub struct SystemStats {
pub min: Duration,
pub max: Duration,
pub avg: Duration,
pub call_count: u64,
}
pub struct SystemProfiler {
timings: FxHashMap<SystemId, Vec<Duration>>,
call_counts: FxHashMap<SystemId, u64>,
}
impl SystemProfiler {
pub fn new() -> Self {
Self {
timings: FxHashMap::default(),
call_counts: FxHashMap::default(),
}
}
pub fn record_execution(&mut self, id: SystemId, duration: Duration) {
self.timings.entry(id).or_default().push(duration);
self.call_counts
.entry(id)
.and_modify(|c| *c += 1)
.or_insert(1);
}
pub fn get_stats(&self, id: SystemId) -> Option<SystemStats> {
let timings = self.timings.get(&id)?;
if timings.is_empty() {
return None;
}
let min = *timings.iter().min().unwrap_or(&Duration::ZERO);
let max = *timings.iter().max().unwrap_or(&Duration::ZERO);
let avg = timings.iter().sum::<Duration>() / timings.len() as u32;
Some(SystemStats {
min,
max,
avg,
call_count: *self.call_counts.get(&id).unwrap_or(&0),
})
}
pub fn clear(&mut self) {
self.timings.clear();
self.call_counts.clear();
}
}
impl Default for SystemProfiler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct SystemTiming {
pub name: String,
pub duration: Duration,
}
#[derive(Debug, Clone)]
pub struct ExecutionProfile {
pub total_frame_time: Duration,
pub system_timings: Vec<SystemTiming>,
}
#[derive(Debug, Clone)]
pub struct ProfilingStats {
pub total_frame_time: Duration,
pub system_timings: Vec<SystemTiming>,
pub time_per_entity: f32,
pub memory_usage: usize,
pub entity_count: usize,
pub systems_per_second: f32,
}
impl Default for ProfilingStats {
fn default() -> Self {
Self {
total_frame_time: Duration::ZERO,
system_timings: Vec::new(),
time_per_entity: 0.0,
memory_usage: 0,
entity_count: 0,
systems_per_second: 0.0,
}
}
}
pub struct Executor<'s> {
schedule: &'s mut Schedule,
pub profiler: SystemProfiler,
pub last_profile: Option<ExecutionProfile>,
}
impl<'s> Executor<'s> {
pub fn new(schedule: &'s mut Schedule) -> Self {
Self {
schedule,
profiler: SystemProfiler::new(),
last_profile: None,
}
}
pub fn execute_frame(&mut self, world: &mut World) -> Result<()> {
world.increment_tick();
self.schedule.ensure_built()?;
let has_named_stages = self.schedule.stages.iter().any(|s| s.name != "default");
if has_named_stages {
let frame_start = Instant::now();
let mut system_timings = Vec::with_capacity(self.schedule.systems.len());
let mut commands = CommandBuffer::new();
let parallel_plan = self.schedule.parallel_plan.clone();
for stage_plan in parallel_plan {
for group in stage_plan.parallel_groups {
for &system_id_idx in &group.system_indices {
let system_id = SystemId(system_id_idx as u32);
let system = self
.schedule
.system_mut_by_id(system_id)
.ok_or(EcsError::SystemNotFound)?;
let system_name = system.name();
let start = Instant::now();
system.run(world, &mut commands)?;
let duration = start.elapsed();
self.profiler.record_execution(system_id, duration);
system_timings.push(SystemTiming {
name: system_name.to_string(),
duration,
});
}
}
commands.apply(world)?;
}
let frame_duration = frame_start.elapsed();
self.last_profile = Some(ExecutionProfile {
total_frame_time: frame_duration,
system_timings,
});
} else {
let stage_plan: Vec<Vec<SystemId>> = self
.schedule
.stage_plan()
.iter()
.map(|stage| stage.to_vec())
.collect();
let frame_start = Instant::now();
let mut system_timings = Vec::with_capacity(self.schedule.systems.len());
let mut commands = CommandBuffer::new();
for stage in stage_plan {
for system_id in stage {
let system = self
.schedule
.system_mut_by_id(system_id)
.ok_or(EcsError::SystemNotFound)?;
let system_name = system.name();
let start = Instant::now();
system.run(world, &mut commands)?;
let duration = start.elapsed();
self.profiler.record_execution(system_id, duration);
system_timings.push(SystemTiming {
name: system_name.to_string(),
duration,
});
}
commands.apply(world)?;
}
let frame_duration = frame_start.elapsed();
self.last_profile = Some(ExecutionProfile {
total_frame_time: frame_duration,
system_timings,
});
}
Ok(())
}
pub fn execute_frame_parallel(&mut self, world: &mut World) -> Result<()> {
world.increment_tick();
self.schedule.ensure_built()?;
let systems_ptr = self.schedule.systems.as_mut_ptr() as usize;
let systems_len = self.schedule.systems.len();
let parallel_plan = self.schedule.parallel_plan.clone();
for stage_plan in parallel_plan {
#[cfg(feature = "profiling")]
let _stage_span = info_span!("stage", name = %stage_plan.name).entered();
for group in stage_plan.parallel_groups {
use rayon::prelude::*;
let world_cell = unsafe { world.as_unsafe_world_cell() };
let results: Vec<(Result<()>, CommandBuffer)> = group
.system_indices
.par_iter()
.map(move |&sys_idx| {
let mut commands = CommandBuffer::new();
if sys_idx >= systems_len {
return (Err(EcsError::SystemNotFound), commands);
}
let system =
unsafe { &mut *(systems_ptr as *mut Box<dyn System>).add(sys_idx) };
let res = unsafe { system.run_parallel(world_cell, &mut commands) };
(res, commands)
})
.collect();
for (result, mut commands) in results {
result?;
commands.apply(world)?;
}
self.barrier(world)?;
}
}
Ok(())
}
pub fn execute_frame_auto(&mut self, world: &mut World) -> Result<()> {
if self.schedule.systems.len() > 1 {
self.execute_frame_parallel(world)
} else {
self.execute_frame(world)
}
}
pub fn execute_frame_with_events(&mut self, world: &mut World) -> Result<()> {
let mut commands = CommandBuffer::new();
for system in &mut self.schedule.systems {
system.run(world, &mut commands)?;
}
commands.apply(world)?;
world.process_events()?;
Ok(())
}
pub fn execute_frame_full(&mut self, world: &mut World) -> Result<()> {
#[cfg(feature = "profiling")]
let _span = info_span!("execute_frame_full");
let mut commands = CommandBuffer::new();
for system in &mut self.schedule.systems {
system.run(world, &mut commands)?;
}
commands.apply(world)?;
world.process_events()?;
Ok(())
}
pub fn execute_with_hierarchy(&mut self, world: &mut World) -> Result<()> {
use crate::hierarchy_system::HierarchyUpdateSystem;
let mut commands = CommandBuffer::new();
let mut hierarchy_system = HierarchyUpdateSystem::new();
hierarchy_system.run(world, &mut commands)?;
for system in &mut self.schedule.systems {
system.run(world, &mut commands)?;
}
commands.apply(world)?;
world.process_events()?;
Ok(())
}
pub fn execute_full(&mut self, world: &mut World) -> Result<()> {
use crate::hierarchy_system::HierarchyUpdateSystem;
let mut commands = CommandBuffer::new();
let mut hierarchy_system = HierarchyUpdateSystem::new();
hierarchy_system.run(world, &mut commands)?;
for system in &mut self.schedule.systems {
system.run(world, &mut commands)?;
}
commands.apply(world)?;
world.process_events()?;
Ok(())
}
pub fn execute_with_global_events(&mut self, world: &mut World) -> Result<()> {
let mut commands = CommandBuffer::new();
for system in &mut self.schedule.systems {
system.run(world, &mut commands)?;
}
commands.apply(world)?;
world.process_global_events()?;
Ok(())
}
pub fn execute_complete_frame(&mut self, world: &mut World) -> Result<()> {
use crate::hierarchy_system::HierarchyUpdateSystem;
let mut commands = CommandBuffer::new();
let mut hierarchy_system = HierarchyUpdateSystem::new();
hierarchy_system.run(world, &mut commands)?;
for system in &mut self.schedule.systems {
system.run(world, &mut commands)?;
}
commands.apply(world)?;
world.process_global_events()?;
world.process_events()?;
Ok(())
}
fn barrier(&mut self, _world: &mut World) -> Result<()> {
Ok(())
}
pub fn profile(&self) -> Option<&ExecutionProfile> {
self.last_profile.as_ref()
}
pub fn print_profile(&self) {
if let Some(profile) = &self.last_profile {
println!(
"Frame time: {:.3?} ({} systems)",
profile.total_frame_time,
profile.system_timings.len()
);
for (index, timing) in profile.system_timings.iter().enumerate() {
println!(" {:02}: {:<24} {:?}", index, timing.name, timing.duration);
}
} else {
println!("No profiling data collected yet.");
}
}
pub fn profiling_stats(&self, world: &World) -> ProfilingStats {
let mut stats = ProfilingStats::default();
if let Some(profile) = &self.last_profile {
stats.total_frame_time = profile.total_frame_time;
stats.system_timings = profile.system_timings.clone();
stats.entity_count = world.entity_count() as usize;
if stats.entity_count > 0 {
stats.time_per_entity =
stats.total_frame_time.as_secs_f32() / stats.entity_count as f32;
}
if !stats.total_frame_time.is_zero() {
stats.systems_per_second =
stats.system_timings.len() as f32 / stats.total_frame_time.as_secs_f32();
}
stats.memory_usage = world.memory_stats().total_memory;
}
stats
}
pub fn export_profiling_csv(&self, world: &World, path: &str) -> Result<()> {
use std::fs::File;
use std::io::Write;
if let Some(profile) = &self.last_profile {
let mut file = File::create(path)?;
writeln!(file, "system_name,duration_ms,duration_us,percentage")?;
for timing in &profile.system_timings {
let duration_ms = timing.duration.as_millis();
let duration_us = timing.duration.as_micros();
let percentage = (timing.duration.as_secs_f32()
/ profile.total_frame_time.as_secs_f32())
* 100.0;
writeln!(
file,
"{},{},{},{:.2}",
timing.name, duration_ms, duration_us, percentage
)?;
}
writeln!(file)?;
writeln!(
file,
"Total Frame Time,{} ms",
profile.total_frame_time.as_millis()
)?;
writeln!(file, "Entity Count,{}", world.entity_count())?;
writeln!(
file,
"Time Per Entity,{:.6} ms",
(profile.total_frame_time.as_secs_f32() / world.entity_count() as f32) * 1000.0
)?;
writeln!(
file,
"Memory Usage,{} bytes",
world.memory_stats().total_memory
)?;
}
Ok(())
}
pub fn print_profiling_summary(&self, world: &World) {
let stats = self.profiling_stats(world);
println!("=== Enhanced Profiling Summary ===");
println!(
"Frame Time: {:.3?} ({:.1} FPS)",
stats.total_frame_time,
1.0 / stats.total_frame_time.as_secs_f32()
);
println!(
"Systems: {} ({:.1} systems/sec)",
stats.system_timings.len(),
stats.systems_per_second
);
println!("Entities: {}", stats.entity_count);
println!("Time/Entity: {:.6} ms", stats.time_per_entity * 1000.0);
println!(
"Memory: {} bytes ({:.1} MB)",
stats.memory_usage,
stats.memory_usage as f64 / 1_048_576.0
);
if !stats.system_timings.is_empty() {
println!("\nSlowest Systems:");
let mut sorted_timings = stats.system_timings.clone();
sorted_timings.sort_by(|a, b| b.duration.cmp(&a.duration));
for (i, timing) in sorted_timings.iter().take(5).enumerate() {
let percentage =
(timing.duration.as_secs_f32() / stats.total_frame_time.as_secs_f32()) * 100.0;
println!(
" {}. {} - {:.3?} ({:.1}%)",
i + 1,
timing.name,
timing.duration,
percentage
);
}
}
println!("===================================");
}
}
use crate::command::CommandBuffer;
use crate::entity::EntityId;
pub struct SyncPoint {
pub command_buffers: Vec<CommandBuffer>,
pub despawn_queue: Vec<EntityId>,
}
impl SyncPoint {
pub fn new() -> Self {
Self {
command_buffers: Vec::new(),
despawn_queue: Vec::new(),
}
}
pub fn add_command_buffer(&mut self, buffer: CommandBuffer) {
self.command_buffers.push(buffer);
}
pub fn queue_despawn(&mut self, entity: EntityId) {
self.despawn_queue.push(entity);
}
pub fn flush(&mut self, world: &mut World) -> Result<()> {
for &entity in self.despawn_queue.iter().rev() {
world.despawn(entity).ok();
}
self.despawn_queue.clear();
for buffer in self.command_buffers.drain(..) {
world.flush_commands(buffer)?;
}
Ok(())
}
}
impl Default for SyncPoint {
fn default() -> Self {
Self::new()
}
}
use std::fs::File;
use std::io::Write;
#[derive(Debug, Clone)]
pub struct ScheduleDebugInfo {
pub stage_count: usize,
pub total_systems: usize,
pub systems_per_stage: Vec<usize>,
}
impl ScheduleDebugInfo {
pub fn from_schedule(schedule: &Schedule) -> Self {
let stage_count = schedule.stage_count();
let total_systems = schedule.system_count();
let systems_per_stage = (0..stage_count)
.map(|i| schedule.stage_system_count(i))
.collect();
Self {
stage_count,
total_systems,
systems_per_stage,
}
}
pub fn print_debug(&self) {
println!("Schedule Debug Info:");
println!(" Total systems: {}", self.total_systems);
println!(" Stages: {}", self.stage_count);
for (i, &count) in self.systems_per_stage.iter().enumerate() {
println!(" Stage {i}: {count} systems");
}
}
pub fn export_json(&self, filename: &str) -> std::io::Result<()> {
let mut file = File::create(filename)?;
write!(file, "{{")?;
write!(file, "\"stage_count\":{},", self.stage_count)?;
write!(file, "\"total_systems\":{},", self.total_systems)?;
write!(file, "\"systems_per_stage\":[")?;
for (i, &count) in self.systems_per_stage.iter().enumerate() {
if i > 0 {
write!(file, ",")?;
}
write!(file, "{count}")?;
}
write!(file, "]")?;
write!(file, "}}")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_point_creation() {
let sp = SyncPoint::new();
assert!(sp.command_buffers.is_empty());
assert!(sp.despawn_queue.is_empty());
}
#[test]
fn test_profiler_creation() {
let profiler = SystemProfiler::new();
assert!(profiler.timings.is_empty());
}
}