use crate::app::{App, RendererFactory};
use crate::ecs::World;
#[cfg(feature = "std")]
use crate::perf::PerfEvent;
use crate::perf::StageStat;
use crate::plugin::Plugin;
use crate::surface::Surface;
#[derive(Default, Clone)]
pub struct SystemPerfSnapshot {
pub entries: alloc::vec::Vec<SystemStat>,
}
#[derive(Clone, Copy)]
pub struct SystemStat {
pub name: &'static str,
pub priority: i32,
pub last_us: u32,
pub avg_us: u32,
pub call_count: u32,
}
#[derive(Default, Clone, Copy)]
pub struct PerfResetFlag(pub bool);
#[derive(Clone)]
pub struct PerfReport {
pub frames: u32,
pub stage_stats: alloc::vec::Vec<StageStat>,
pub systems: alloc::vec::Vec<SystemStat>,
}
pub struct PerfReportPlugin {
frames_per_report: u32,
frame_count: u32,
on_report: fn(report: &PerfReport),
#[cfg(feature = "std")]
perfetto_writer: Option<std::sync::Mutex<std::fs::File>>,
}
impl PerfReportPlugin {
pub fn new(frames_per_report: u32) -> Self {
Self {
frames_per_report,
frame_count: 0,
on_report: default_sink,
#[cfg(feature = "std")]
perfetto_writer: None,
}
}
pub fn with_sink(mut self, sink: fn(&PerfReport)) -> Self {
self.on_report = sink;
self
}
#[cfg(feature = "std")]
pub fn with_perfetto_writer(mut self, path: impl AsRef<std::path::Path>) -> Self {
let f = std::fs::File::create(path).expect("failed to create perfetto trace file");
self.perfetto_writer = Some(std::sync::Mutex::new(f));
self
}
}
impl Default for PerfReportPlugin {
fn default() -> Self {
Self::new(60)
}
}
impl<B, F> Plugin<B, F> for PerfReportPlugin
where
B: Surface,
F: RendererFactory<B>,
{
fn build(&mut self, app: &mut App<B, F>) {
app.world.insert_resource(PerfAccum::default());
}
fn post_render(&mut self, world: &mut World, _render_nanos: u64) {
let events = crate::perf::drain_events();
#[cfg(feature = "std")]
if let Some(w) = self.perfetto_writer.as_ref() {
write_perfetto_ndjson(w, &events);
}
if let Some(acc) = world.resource_mut::<PerfAccum>() {
for ev in &events {
let dur = ev.end_ns.saturating_sub(ev.start_ns);
if let Some(s) = acc.stage_stats.iter_mut().find(|s| s.name == ev.name) {
s.count += 1;
s.total_ns += dur;
s.last_ns = dur;
if dur < s.min_ns {
s.min_ns = dur;
}
if dur > s.max_ns {
s.max_ns = dur;
}
} else {
acc.stage_stats.push(StageStat {
name: ev.name,
count: 1,
total_ns: dur,
last_ns: dur,
min_ns: dur,
max_ns: dur,
});
}
}
}
self.frame_count += 1;
if self.frame_count < self.frames_per_report {
return;
}
let stage_stats = world
.resource::<PerfAccum>()
.map(|a| a.stage_stats.clone())
.unwrap_or_default();
let report = PerfReport {
frames: self.frame_count,
stage_stats,
systems: collect_system_stats(world),
};
(self.on_report)(&report);
self.frame_count = 0;
if let Some(a) = world.resource_mut::<PerfAccum>() {
a.stage_stats.clear();
}
if let Some(snap) = world.resource_mut::<SystemPerfSnapshot>() {
snap.entries.clear();
}
world.insert_resource(PerfResetFlag(true));
}
}
#[derive(Default, Clone)]
struct PerfAccum {
stage_stats: alloc::vec::Vec<StageStat>,
}
fn collect_system_stats(world: &World) -> alloc::vec::Vec<SystemStat> {
world
.resource::<SystemPerfSnapshot>()
.map(|s| s.entries.clone())
.unwrap_or_default()
}
#[cfg(feature = "std")]
fn write_perfetto_ndjson(w: &std::sync::Mutex<std::fs::File>, events: &[PerfEvent]) {
use std::io::Write;
let Ok(mut f) = w.lock() else { return };
for ev in events {
let dur_us = ev.end_ns.saturating_sub(ev.start_ns) / 1_000;
let ts_us = ev.start_ns / 1_000;
let _ = writeln!(
f,
r#"{{"name":"{}","cat":"mirui","ph":"X","pid":1,"tid":1,"ts":{},"dur":{}}}"#,
ev.name, ts_us, dur_us,
);
}
}
#[cfg(feature = "std")]
fn default_sink(report: &PerfReport) {
eprintln!("[perf] {} frames", report.frames);
let mut sorted: alloc::vec::Vec<&StageStat> = report.stage_stats.iter().collect();
sorted.sort_by_key(|s| core::cmp::Reverse(s.total_ns));
for s in &sorted {
let avg = if s.count == 0 {
0
} else {
s.total_ns / s.count as u64
};
eprintln!(
"[perf] {:>26} n={:<4} total {:>7}µs avg {:>5}µs min {:>5}µs max {:>5}µs",
s.name,
s.count,
s.total_ns / 1_000,
avg / 1_000,
s.min_ns / 1_000,
s.max_ns / 1_000,
);
}
for s in &report.systems {
if s.avg_us == 0 && s.last_us == 0 {
continue;
}
eprintln!(
"[perf] {:>26} last {:>5}µs avg {:>5}µs n={}",
s.name, s.last_us, s.avg_us, s.call_count
);
}
}
#[cfg(not(feature = "std"))]
fn default_sink(_report: &PerfReport) {}