1use oriera_common::{FrameIndex, Timestamp};
10use std::collections::VecDeque;
11
12pub const FRAME_BUDGET_NS: u64 = 16_666_666;
14
15const HISTORY: usize = 60;
18
19pub struct FrameCtx {
21 pub frame: FrameIndex,
22 pub dt: f32,
23 pub now: Timestamp,
24}
25
26pub trait System<W> {
28 fn name(&self) -> &str;
29 fn run(&mut self, ctx: &FrameCtx, world: &mut W);
30}
31
32pub struct Scheduler<W> {
34 systems: Vec<Box<dyn System<W>>>,
35 frame: FrameIndex,
36 last: Timestamp,
37 frame_times: VecDeque<f32>,
38 consecutive_overruns: u32,
41 pub budget_warnings: u64,
44 budget_ns: u64,
49 system_ms: Vec<(String, f32)>,
54}
55
56const OVERRUN_STREAK: u32 = 60;
58const OVERRUN_RATIO: f32 = 1.2;
59
60impl<W> Scheduler<W> {
61 pub fn new() -> Self {
62 Self::with_budget_ns(FRAME_BUDGET_NS)
63 }
64
65 pub fn with_budget_ns(budget_ns: u64) -> Self {
67 Self {
68 systems: Vec::new(),
69 frame: FrameIndex(0),
70 last: Timestamp::now(),
71 frame_times: VecDeque::with_capacity(HISTORY + 1),
72 consecutive_overruns: 0,
73 budget_warnings: 0,
74 budget_ns: budget_ns.max(1),
75 system_ms: Vec::new(),
76 }
77 }
78
79 pub fn add_system(&mut self, s: Box<dyn System<W>>) {
80 self.system_ms.push((s.name().to_string(), 0.0));
81 self.systems.push(s);
82 }
83
84 pub fn budget_ms(&self) -> f32 {
86 self.budget_ns as f32 / 1_000_000.0
87 }
88
89 pub fn system_timings(&self) -> &[(String, f32)] {
92 &self.system_ms
93 }
94
95 pub fn tick(&mut self, world: &mut W) -> f32 {
97 let start = Timestamp::now();
98 let dt = start.elapsed(self.last);
99 self.last = start;
100
101 let ctx = FrameCtx {
102 frame: self.frame,
103 dt,
104 now: start,
105 };
106
107 for (s, timing) in self.systems.iter_mut().zip(self.system_ms.iter_mut()) {
108 let t0 = Timestamp::now();
109 s.run(&ctx, world);
110 timing.1 = Timestamp::now().elapsed(t0) * 1000.0;
111 }
112
113 let frame_ms = Timestamp::now().elapsed(start) * 1000.0;
114 self.frame.0 += 1;
115 self.frame_times.push_back(frame_ms);
116 if self.frame_times.len() > HISTORY {
117 self.frame_times.pop_front();
118 }
119 self.record_budget(frame_ms);
120 frame_ms
121 }
122
123 fn record_budget(&mut self, frame_ms: f32) {
128 let budget_ms = self.budget_ms();
129 if frame_ms > budget_ms * OVERRUN_RATIO {
130 self.consecutive_overruns += 1;
131 if self.consecutive_overruns >= OVERRUN_STREAK {
132 let worst = self
133 .system_ms
134 .iter()
135 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
136 .map(|(n, _)| n.as_str())
137 .unwrap_or("");
138 tracing::warn!(
139 frame = self.frame.0,
140 avg_frame_ms = self.avg_frame_ms(),
141 budget_ms,
142 worst_system = worst,
143 "L2 scheduler over budget for {OVERRUN_STREAK} consecutive frames"
144 );
145 self.budget_warnings += 1;
146 self.consecutive_overruns = 0;
147 }
148 } else {
149 self.consecutive_overruns = 0;
150 }
151 }
152
153 pub fn avg_frame_ms(&self) -> f32 {
155 if self.frame_times.is_empty() {
156 return 0.0;
157 }
158 self.frame_times.iter().sum::<f32>() / self.frame_times.len() as f32
159 }
160}
161
162impl<W> Default for Scheduler<W> {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 struct IncSystem;
173
174 impl System<u32> for IncSystem {
175 fn name(&self) -> &str {
176 "inc"
177 }
178 fn run(&mut self, _ctx: &FrameCtx, world: &mut u32) {
179 *world += 1;
180 }
181 }
182
183 #[test]
184 fn scheduler_passes_world_to_systems() {
185 let mut sched: Scheduler<u32> = Scheduler::new();
186 sched.add_system(Box::new(IncSystem));
187 let mut world = 0u32;
188 sched.tick(&mut world);
189 sched.tick(&mut world);
190 assert_eq!(world, 2);
191 }
192
193 #[test]
194 fn custom_budget_changes_the_grading_curve() {
195 let mut sched: Scheduler<()> = Scheduler::with_budget_ns(11_111_000);
198 let over_ms = sched.budget_ms() * 1.5;
199 for _ in 0..60 {
200 sched.record_budget(over_ms);
201 }
202 assert_eq!(sched.budget_warnings, 1);
203
204 let mut desktop: Scheduler<()> = Scheduler::new();
205 for _ in 0..120 {
206 desktop.record_budget(over_ms);
207 }
208 assert_eq!(desktop.budget_warnings, 0);
209 assert!((desktop.budget_ms() - 16.6666).abs() < 0.01);
210 }
211
212 #[test]
213 fn tick_records_per_system_timings_in_run_order() {
214 let mut sched: Scheduler<u32> = Scheduler::new();
215 sched.add_system(Box::new(IncSystem));
216 sched.add_system(Box::new(IncSystem));
217 let mut world = 0u32;
218 sched.tick(&mut world);
219 let t = sched.system_timings();
220 assert_eq!(t.len(), 2);
221 assert_eq!(t[0].0, "inc");
222 assert!(t.iter().all(|(_, ms)| *ms >= 0.0));
223 }
224
225 #[test]
226 fn sustained_overruns_fire_one_warning_per_streak() {
227 let mut sched: Scheduler<()> = Scheduler::new();
228 let over_ms = (FRAME_BUDGET_NS as f32 / 1_000_000.0) * 1.5;
229 for _ in 0..60 {
230 sched.record_budget(over_ms);
231 }
232 assert_eq!(sched.budget_warnings, 1);
233
234 for _ in 0..60 {
236 sched.record_budget(over_ms);
237 }
238 assert_eq!(sched.budget_warnings, 2);
239 }
240
241 #[test]
242 fn single_in_budget_frame_resets_the_streak() {
243 let mut sched: Scheduler<()> = Scheduler::new();
244 let over_ms = (FRAME_BUDGET_NS as f32 / 1_000_000.0) * 1.5;
245 for _ in 0..59 {
246 sched.record_budget(over_ms);
247 }
248 sched.record_budget(1.0); for _ in 0..59 {
250 sched.record_budget(over_ms);
251 }
252 assert_eq!(sched.budget_warnings, 0);
253 }
254
255 #[test]
256 fn exactly_at_ratio_threshold_is_not_an_overrun() {
257 let mut sched: Scheduler<()> = Scheduler::new();
258 let at_threshold = (FRAME_BUDGET_NS as f32 / 1_000_000.0) * 1.2;
259 for _ in 0..120 {
260 sched.record_budget(at_threshold);
261 }
262 assert_eq!(sched.budget_warnings, 0);
263 assert_eq!(sched.consecutive_overruns, 0);
264 }
265}