1use std::collections::BTreeMap;
33use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
34use std::time::Duration;
35
36use serde::{Deserialize, Serialize};
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
41pub struct Reason(pub &'static str);
42
43impl Reason {
44 pub const INPUT: Reason = Reason("input");
46 pub const STARTUP: Reason = Reason("startup");
48 pub const CONTROL: Reason = Reason("control");
50 pub const MENU: Reason = Reason("menu");
52 pub const DOCUMENT: Reason = Reason("document");
54 pub const SCREENSHOT: Reason = Reason("screenshot");
56 pub const JOB: Reason = Reason("job");
58 pub const AUTOSAVE: Reason = Reason("autosave");
60 pub const SETTINGS: Reason = Reason("settings");
62 pub const TIMER: Reason = Reason("timer");
64 pub const ANIMATION: Reason = Reason("animation");
66
67 pub const IDLE: Reason = Reason("idle");
69
70 pub const STANDARD: [Reason; 12] = [
72 Reason::INPUT,
73 Reason::STARTUP,
74 Reason::CONTROL,
75 Reason::MENU,
76 Reason::DOCUMENT,
77 Reason::SCREENSHOT,
78 Reason::JOB,
79 Reason::AUTOSAVE,
80 Reason::SETTINGS,
81 Reason::TIMER,
82 Reason::ANIMATION,
83 Reason::IDLE,
84 ];
85
86 pub fn name(self) -> &'static str {
87 self.0
88 }
89}
90
91impl std::fmt::Display for Reason {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.write_str(self.0)
94 }
95}
96
97const MAX_DUE: usize = 8;
100
101const SLACK_S: f64 = 1e-3;
103
104#[derive(Debug, Default)]
105struct State {
106 due: BTreeMap<&'static str, Vec<f64>>,
110 requests: BTreeMap<&'static str, u64>,
111 frames: BTreeMap<&'static str, u64>,
112 total: u64,
113}
114
115fn bump(m: &mut BTreeMap<&'static str, u64>, k: &'static str) {
116 let n = m.entry(k).or_insert(0);
117 *n = n.saturating_add(1);
118}
119
120#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
124pub struct WakeStats {
125 pub frames: u64,
127 pub frames_by: BTreeMap<String, u64>,
129 pub requests_by: BTreeMap<String, u64>,
131}
132
133impl WakeStats {
134 pub fn frames_for(&self, r: Reason) -> u64 {
135 self.frames_by.get(r.0).copied().unwrap_or(0)
136 }
137
138 pub fn requests_for(&self, r: Reason) -> u64 {
139 self.requests_by.get(r.0).copied().unwrap_or(0)
140 }
141
142 pub fn requests(&self) -> u64 {
144 self.requests_by.values().fold(0, |a, b| a.saturating_add(*b))
145 }
146}
147
148#[derive(Clone, Debug, Default)]
151pub struct Scheduler {
152 state: Arc<Mutex<State>>,
153}
154
155fn id() -> egui::Id {
156 egui::Id::new("ashlar.wake.scheduler")
157}
158
159impl Scheduler {
160 pub fn of(ctx: &egui::Context) -> Scheduler {
162 ctx.data_mut(|d| d.get_temp_mut_or_default::<Scheduler>(id()).clone())
163 }
164
165 pub fn install(&self, ctx: &egui::Context) {
167 ctx.data_mut(|d| d.insert_temp(id(), self.clone()));
168 }
169
170 fn lock(&self) -> MutexGuard<'_, State> {
171 self.state.lock().unwrap_or_else(PoisonError::into_inner)
172 }
173
174 pub fn request(&self, reason: Reason, due: f64) {
176 let mut s = self.lock();
177 bump(&mut s.requests, reason.0);
178 let list = s.due.entry(reason.0).or_default();
179 if list.iter().any(|d| (d - due).abs() <= SLACK_S || (d.is_infinite() && due.is_infinite())) {
181 return;
182 }
183 let at = list.partition_point(|d| *d < due);
184 list.insert(at, due);
185 if list.len() > MAX_DUE {
186 list.remove(MAX_DUE - 1);
189 }
190 }
191
192 pub fn begin_frame(&self, now: f64) {
195 let mut s = self.lock();
196 let due = std::mem::take(&mut s.due);
197 let mut any = false;
198 for (r, mut list) in due {
199 let n = list.partition_point(|d| *d <= now + SLACK_S);
200 if n > 0 {
201 bump(&mut s.frames, r);
202 any = true;
203 list.drain(..n);
204 }
205 if !list.is_empty() {
206 s.due.insert(r, list);
207 }
208 }
209 if !any {
210 bump(&mut s.frames, Reason::INPUT.0);
211 }
212 s.total = s.total.saturating_add(1);
213 }
214
215 pub fn cancel(&self, reason: Reason) {
217 self.lock().due.remove(reason.0);
218 }
219
220 pub fn rearm(&self, ctx: &egui::Context) {
223 let next = self.lock().due.values().filter_map(|l| l.first().copied()).fold(f64::INFINITY, f64::min);
224 if !next.is_finite() {
225 return;
226 }
227 let (now, predicted_dt) = ctx.input(|i| (i.time, i.predicted_dt));
228 let delay = Duration::try_from_secs_f64((next - now).max(0.0)).unwrap_or_default();
229 if delay.is_zero() {
230 repaint_now(ctx);
231 } else {
232 let early = Duration::try_from_secs_f32(predicted_dt).unwrap_or_default();
233 ctx.request_repaint_after(delay.saturating_add(early).saturating_add(Duration::from_millis(1)));
234 }
235 }
236
237 pub fn stats(&self) -> WakeStats {
238 let s = self.lock();
239 let owned = |m: &BTreeMap<&'static str, u64>| m.iter().map(|(k, v)| ((*k).to_string(), *v)).collect();
240 WakeStats { frames: s.total, frames_by: owned(&s.frames), requests_by: owned(&s.requests) }
241 }
242}
243
244pub fn wake(ctx: &egui::Context, reason: Reason) {
247 Scheduler::of(ctx).request(reason, f64::NEG_INFINITY);
248 repaint_now(ctx);
249}
250
251fn repaint_now(ctx: &egui::Context) {
256 ctx.request_repaint_after(Duration::from_nanos(1));
257}
258
259pub fn wake_after(ctx: &egui::Context, reason: Reason, delay: Duration) {
262 request_after(ctx, &Scheduler::of(ctx), reason, delay, false);
263}
264
265pub fn reschedule(ctx: &egui::Context, reason: Reason, delay: Duration) {
269 request_after(ctx, &Scheduler::of(ctx), reason, delay, true);
270}
271
272fn request_after(ctx: &egui::Context, s: &Scheduler, reason: Reason, delay: Duration, replace: bool) {
273 if replace {
274 s.cancel(reason);
275 }
276 if delay.is_zero() {
277 s.request(reason, f64::NEG_INFINITY);
278 repaint_now(ctx);
279 return;
280 }
281 let (now, predicted_dt) = ctx.input(|i| (i.time, i.predicted_dt));
282 s.request(reason, now + delay.as_secs_f64());
283 let early = Duration::try_from_secs_f32(predicted_dt).unwrap_or_default();
287 ctx.request_repaint_after(delay.saturating_add(early).saturating_add(Duration::from_millis(1)));
288}
289
290pub fn waker(ctx: &egui::Context, reason: Reason) -> Waker {
293 Waker { ctx: ctx.clone(), scheduler: Scheduler::of(ctx), reason }
294}
295
296#[derive(Clone, Debug)]
298pub struct Waker {
299 ctx: egui::Context,
300 scheduler: Scheduler,
301 reason: Reason,
302}
303
304impl Waker {
305 pub fn wake(&self) {
307 self.scheduler.request(self.reason, f64::NEG_INFINITY);
308 repaint_now(&self.ctx);
309 }
310
311 pub fn reschedule(&self, delay: Duration) {
313 request_after(&self.ctx, &self.scheduler, self.reason, delay, true);
314 }
315
316 pub fn wake_after(&self, delay: Duration) {
318 request_after(&self.ctx, &self.scheduler, self.reason, delay, false);
319 }
320
321 pub fn reason(&self) -> Reason {
322 self.reason
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 const STATUS: Reason = Reason("status");
331 const PLAYBACK: Reason = Reason("playback");
332
333 #[test]
334 fn frames_are_credited_to_the_reasons_that_were_due() {
335 let s = Scheduler::default();
336 s.request(PLAYBACK, f64::NEG_INFINITY);
337 s.request(STATUS, 1.0);
338 s.begin_frame(0.5);
339 s.begin_frame(1.0);
341 s.request(STATUS, 2.0);
342 s.request(Reason::JOB, 1.15);
343 s.begin_frame(1.15);
344 let st = s.stats();
345 assert_eq!(st.frames, 3);
346 assert_eq!(st.frames_for(PLAYBACK), 1);
347 assert_eq!(st.frames_for(STATUS), 1);
348 assert_eq!(st.frames_for(Reason::JOB), 1);
349 assert_eq!(st.frames_for(Reason::INPUT), 0);
350 assert_eq!(st.requests_for(STATUS), 2);
351 assert_eq!(st.requests(), 4);
352 let v = serde_json::to_value(&st).unwrap_or_default();
353 assert_eq!(v["frames_by"], serde_json::json!({"status": 1, "playback": 1, "job": 1}));
354 assert_eq!(v["requests_by"], serde_json::json!({"playback": 1, "status": 2, "job": 1}));
355 assert_eq!(v["frames"], serde_json::json!(3));
356 let back: WakeStats = serde_json::from_value(v).unwrap_or_default();
357 assert_eq!(back, st);
358 }
359
360 #[test]
361 fn a_frame_counts_once_per_reason_and_the_earliest_request_wins() {
362 let s = Scheduler::default();
363 s.request(Reason::TIMER, 5.0);
364 s.request(Reason::TIMER, 1.0);
365 s.request(Reason::TIMER, 3.0);
366 s.begin_frame(1.0);
367 let st = s.stats();
368 assert_eq!((st.frames_for(Reason::TIMER), st.requests_for(Reason::TIMER), st.frames), (1, 3, 1));
369 }
370
371 #[test]
372 fn deadlines_not_yet_due_stay_pending_each_one_gets_its_frame() {
373 let s = Scheduler::default();
374 s.request(Reason::SETTINGS, 1.0);
375 s.request(Reason::SETTINGS, 1.0); s.request(Reason::SETTINGS, 1.4);
377 s.begin_frame(0.2); s.begin_frame(1.0);
379 s.begin_frame(1.2);
380 s.begin_frame(1.4);
381 let st = s.stats();
382 assert_eq!((st.frames_for(Reason::SETTINGS), st.frames_for(Reason::INPUT), st.requests_for(Reason::SETTINGS)), (2, 2, 3));
383 s.request(Reason::TIMER, 9.0);
384 s.cancel(Reason::TIMER);
385 s.begin_frame(9.0);
386 assert_eq!(s.stats().frames_for(Reason::TIMER), 0);
387 for i in 0..100 {
388 s.request(Reason::JOB, 20.0 + f64::from(i));
389 }
390 s.begin_frame(200.0);
391 assert_eq!(s.stats().frames_for(Reason::JOB), 1, "a capped list still ends with the latest deadline");
392 }
393
394 #[test]
395 fn standard_names_are_unique() {
396 let mut names: Vec<_> = Reason::STANDARD.iter().map(|r| r.name()).collect();
397 names.sort_unstable();
398 names.dedup();
399 assert_eq!(names.len(), Reason::STANDARD.len());
400 }
401
402 #[test]
403 fn wakes_reach_the_context_and_its_scheduler() {
404 let ctx = egui::Context::default();
405 let w = waker(&ctx, Reason::JOB);
406 let t = std::thread::spawn(move || w.wake());
407 assert!(t.join().is_ok());
408 wake_after(&ctx, Reason::TIMER, Duration::from_millis(10));
409 let st = Scheduler::of(&ctx).stats();
410 assert_eq!((st.requests_for(Reason::JOB), st.requests_for(Reason::TIMER)), (1, 1));
411 let s = Scheduler::of(&ctx);
413 ctx.memory_mut(|m| *m = egui::Memory::default());
414 s.install(&ctx);
415 assert_eq!(Scheduler::of(&ctx).stats().requests(), 2);
416 }
417}