use oriera_common::{FrameIndex, Timestamp};
use std::collections::VecDeque;
pub const FRAME_BUDGET_NS: u64 = 16_666_666;
const HISTORY: usize = 60;
pub struct FrameCtx {
pub frame: FrameIndex,
pub dt: f32,
pub now: Timestamp,
}
pub trait System<W> {
fn name(&self) -> &str;
fn run(&mut self, ctx: &FrameCtx, world: &mut W);
}
pub struct Scheduler<W> {
systems: Vec<Box<dyn System<W>>>,
frame: FrameIndex,
last: Timestamp,
frame_times: VecDeque<f32>,
consecutive_overruns: u32,
pub budget_warnings: u64,
budget_ns: u64,
system_ms: Vec<(String, f32)>,
}
const OVERRUN_STREAK: u32 = 60;
const OVERRUN_RATIO: f32 = 1.2;
impl<W> Scheduler<W> {
pub fn new() -> Self {
Self::with_budget_ns(FRAME_BUDGET_NS)
}
pub fn with_budget_ns(budget_ns: u64) -> Self {
Self {
systems: Vec::new(),
frame: FrameIndex(0),
last: Timestamp::now(),
frame_times: VecDeque::with_capacity(HISTORY + 1),
consecutive_overruns: 0,
budget_warnings: 0,
budget_ns: budget_ns.max(1),
system_ms: Vec::new(),
}
}
pub fn add_system(&mut self, s: Box<dyn System<W>>) {
self.system_ms.push((s.name().to_string(), 0.0));
self.systems.push(s);
}
pub fn budget_ms(&self) -> f32 {
self.budget_ns as f32 / 1_000_000.0
}
pub fn system_timings(&self) -> &[(String, f32)] {
&self.system_ms
}
pub fn tick(&mut self, world: &mut W) -> f32 {
let start = Timestamp::now();
let dt = start.elapsed(self.last);
self.last = start;
let ctx = FrameCtx {
frame: self.frame,
dt,
now: start,
};
for (s, timing) in self.systems.iter_mut().zip(self.system_ms.iter_mut()) {
let t0 = Timestamp::now();
s.run(&ctx, world);
timing.1 = Timestamp::now().elapsed(t0) * 1000.0;
}
let frame_ms = Timestamp::now().elapsed(start) * 1000.0;
self.frame.0 += 1;
self.frame_times.push_back(frame_ms);
if self.frame_times.len() > HISTORY {
self.frame_times.pop_front();
}
self.record_budget(frame_ms);
frame_ms
}
fn record_budget(&mut self, frame_ms: f32) {
let budget_ms = self.budget_ms();
if frame_ms > budget_ms * OVERRUN_RATIO {
self.consecutive_overruns += 1;
if self.consecutive_overruns >= OVERRUN_STREAK {
let worst = self
.system_ms
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.map(|(n, _)| n.as_str())
.unwrap_or("");
tracing::warn!(
frame = self.frame.0,
avg_frame_ms = self.avg_frame_ms(),
budget_ms,
worst_system = worst,
"L2 scheduler over budget for {OVERRUN_STREAK} consecutive frames"
);
self.budget_warnings += 1;
self.consecutive_overruns = 0;
}
} else {
self.consecutive_overruns = 0;
}
}
pub fn avg_frame_ms(&self) -> f32 {
if self.frame_times.is_empty() {
return 0.0;
}
self.frame_times.iter().sum::<f32>() / self.frame_times.len() as f32
}
}
impl<W> Default for Scheduler<W> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct IncSystem;
impl System<u32> for IncSystem {
fn name(&self) -> &str {
"inc"
}
fn run(&mut self, _ctx: &FrameCtx, world: &mut u32) {
*world += 1;
}
}
#[test]
fn scheduler_passes_world_to_systems() {
let mut sched: Scheduler<u32> = Scheduler::new();
sched.add_system(Box::new(IncSystem));
let mut world = 0u32;
sched.tick(&mut world);
sched.tick(&mut world);
assert_eq!(world, 2);
}
#[test]
fn custom_budget_changes_the_grading_curve() {
let mut sched: Scheduler<()> = Scheduler::with_budget_ns(11_111_000);
let over_ms = sched.budget_ms() * 1.5;
for _ in 0..60 {
sched.record_budget(over_ms);
}
assert_eq!(sched.budget_warnings, 1);
let mut desktop: Scheduler<()> = Scheduler::new();
for _ in 0..120 {
desktop.record_budget(over_ms);
}
assert_eq!(desktop.budget_warnings, 0);
assert!((desktop.budget_ms() - 16.6666).abs() < 0.01);
}
#[test]
fn tick_records_per_system_timings_in_run_order() {
let mut sched: Scheduler<u32> = Scheduler::new();
sched.add_system(Box::new(IncSystem));
sched.add_system(Box::new(IncSystem));
let mut world = 0u32;
sched.tick(&mut world);
let t = sched.system_timings();
assert_eq!(t.len(), 2);
assert_eq!(t[0].0, "inc");
assert!(t.iter().all(|(_, ms)| *ms >= 0.0));
}
#[test]
fn sustained_overruns_fire_one_warning_per_streak() {
let mut sched: Scheduler<()> = Scheduler::new();
let over_ms = (FRAME_BUDGET_NS as f32 / 1_000_000.0) * 1.5;
for _ in 0..60 {
sched.record_budget(over_ms);
}
assert_eq!(sched.budget_warnings, 1);
for _ in 0..60 {
sched.record_budget(over_ms);
}
assert_eq!(sched.budget_warnings, 2);
}
#[test]
fn single_in_budget_frame_resets_the_streak() {
let mut sched: Scheduler<()> = Scheduler::new();
let over_ms = (FRAME_BUDGET_NS as f32 / 1_000_000.0) * 1.5;
for _ in 0..59 {
sched.record_budget(over_ms);
}
sched.record_budget(1.0); for _ in 0..59 {
sched.record_budget(over_ms);
}
assert_eq!(sched.budget_warnings, 0);
}
#[test]
fn exactly_at_ratio_threshold_is_not_an_overrun() {
let mut sched: Scheduler<()> = Scheduler::new();
let at_threshold = (FRAME_BUDGET_NS as f32 / 1_000_000.0) * 1.2;
for _ in 0..120 {
sched.record_budget(at_threshold);
}
assert_eq!(sched.budget_warnings, 0);
assert_eq!(sched.consecutive_overruns, 0);
}
}