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 STANDARD: [Reason; 11] = [
69 Reason::INPUT,
70 Reason::STARTUP,
71 Reason::CONTROL,
72 Reason::MENU,
73 Reason::DOCUMENT,
74 Reason::SCREENSHOT,
75 Reason::JOB,
76 Reason::AUTOSAVE,
77 Reason::SETTINGS,
78 Reason::TIMER,
79 Reason::ANIMATION,
80 ];
81
82 pub fn name(self) -> &'static str {
83 self.0
84 }
85}
86
87impl std::fmt::Display for Reason {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.write_str(self.0)
90 }
91}
92
93const MAX_DUE: usize = 8;
96
97const SLACK_S: f64 = 1e-3;
99
100#[derive(Debug, Default)]
101struct State {
102 due: BTreeMap<&'static str, Vec<f64>>,
106 requests: BTreeMap<&'static str, u64>,
107 frames: BTreeMap<&'static str, u64>,
108 total: u64,
109}
110
111fn bump(m: &mut BTreeMap<&'static str, u64>, k: &'static str) {
112 let n = m.entry(k).or_insert(0);
113 *n = n.saturating_add(1);
114}
115
116#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
120pub struct WakeStats {
121 pub frames: u64,
123 pub frames_by: BTreeMap<String, u64>,
125 pub requests_by: BTreeMap<String, u64>,
127}
128
129impl WakeStats {
130 pub fn frames_for(&self, r: Reason) -> u64 {
131 self.frames_by.get(r.0).copied().unwrap_or(0)
132 }
133
134 pub fn requests_for(&self, r: Reason) -> u64 {
135 self.requests_by.get(r.0).copied().unwrap_or(0)
136 }
137
138 pub fn requests(&self) -> u64 {
140 self.requests_by.values().fold(0, |a, b| a.saturating_add(*b))
141 }
142}
143
144#[derive(Clone, Debug, Default)]
147pub struct Scheduler {
148 state: Arc<Mutex<State>>,
149}
150
151fn id() -> egui::Id {
152 egui::Id::new("ashlar.wake.scheduler")
153}
154
155impl Scheduler {
156 pub fn of(ctx: &egui::Context) -> Scheduler {
158 ctx.data_mut(|d| d.get_temp_mut_or_default::<Scheduler>(id()).clone())
159 }
160
161 pub fn install(&self, ctx: &egui::Context) {
163 ctx.data_mut(|d| d.insert_temp(id(), self.clone()));
164 }
165
166 fn lock(&self) -> MutexGuard<'_, State> {
167 self.state.lock().unwrap_or_else(PoisonError::into_inner)
168 }
169
170 pub fn request(&self, reason: Reason, due: f64) {
172 let mut s = self.lock();
173 bump(&mut s.requests, reason.0);
174 let list = s.due.entry(reason.0).or_default();
175 if list.iter().any(|d| (d - due).abs() <= SLACK_S || (d.is_infinite() && due.is_infinite())) {
177 return;
178 }
179 let at = list.partition_point(|d| *d < due);
180 list.insert(at, due);
181 if list.len() > MAX_DUE {
182 list.remove(MAX_DUE - 1);
185 }
186 }
187
188 pub fn begin_frame(&self, now: f64) {
191 let mut s = self.lock();
192 let due = std::mem::take(&mut s.due);
193 let mut any = false;
194 for (r, mut list) in due {
195 let n = list.partition_point(|d| *d <= now + SLACK_S);
196 if n > 0 {
197 bump(&mut s.frames, r);
198 any = true;
199 list.drain(..n);
200 }
201 if !list.is_empty() {
202 s.due.insert(r, list);
203 }
204 }
205 if !any {
206 bump(&mut s.frames, Reason::INPUT.0);
207 }
208 s.total = s.total.saturating_add(1);
209 }
210
211 pub fn cancel(&self, reason: Reason) {
213 self.lock().due.remove(reason.0);
214 }
215
216 pub fn rearm(&self, ctx: &egui::Context) {
219 let next = self.lock().due.values().filter_map(|l| l.first().copied()).fold(f64::INFINITY, f64::min);
220 if !next.is_finite() {
221 return;
222 }
223 let (now, predicted_dt) = ctx.input(|i| (i.time, i.predicted_dt));
224 let delay = Duration::try_from_secs_f64((next - now).max(0.0)).unwrap_or_default();
225 if delay.is_zero() {
226 ctx.request_repaint();
227 } else {
228 let early = Duration::try_from_secs_f32(predicted_dt).unwrap_or_default();
229 ctx.request_repaint_after(delay.saturating_add(early).saturating_add(Duration::from_millis(1)));
230 }
231 }
232
233 pub fn stats(&self) -> WakeStats {
234 let s = self.lock();
235 let owned = |m: &BTreeMap<&'static str, u64>| m.iter().map(|(k, v)| ((*k).to_string(), *v)).collect();
236 WakeStats { frames: s.total, frames_by: owned(&s.frames), requests_by: owned(&s.requests) }
237 }
238}
239
240pub fn wake(ctx: &egui::Context, reason: Reason) {
243 Scheduler::of(ctx).request(reason, f64::NEG_INFINITY);
244 ctx.request_repaint();
245}
246
247pub fn wake_after(ctx: &egui::Context, reason: Reason, delay: Duration) {
250 request_after(ctx, &Scheduler::of(ctx), reason, delay, false);
251}
252
253pub fn reschedule(ctx: &egui::Context, reason: Reason, delay: Duration) {
257 request_after(ctx, &Scheduler::of(ctx), reason, delay, true);
258}
259
260fn request_after(ctx: &egui::Context, s: &Scheduler, reason: Reason, delay: Duration, replace: bool) {
261 if replace {
262 s.cancel(reason);
263 }
264 if delay.is_zero() {
265 s.request(reason, f64::NEG_INFINITY);
266 ctx.request_repaint();
267 return;
268 }
269 let (now, predicted_dt) = ctx.input(|i| (i.time, i.predicted_dt));
270 s.request(reason, now + delay.as_secs_f64());
271 let early = Duration::try_from_secs_f32(predicted_dt).unwrap_or_default();
275 ctx.request_repaint_after(delay.saturating_add(early).saturating_add(Duration::from_millis(1)));
276}
277
278pub fn waker(ctx: &egui::Context, reason: Reason) -> Waker {
281 Waker { ctx: ctx.clone(), scheduler: Scheduler::of(ctx), reason }
282}
283
284#[derive(Clone, Debug)]
286pub struct Waker {
287 ctx: egui::Context,
288 scheduler: Scheduler,
289 reason: Reason,
290}
291
292impl Waker {
293 pub fn wake(&self) {
295 self.scheduler.request(self.reason, f64::NEG_INFINITY);
296 self.ctx.request_repaint();
297 }
298
299 pub fn reschedule(&self, delay: Duration) {
301 request_after(&self.ctx, &self.scheduler, self.reason, delay, true);
302 }
303
304 pub fn wake_after(&self, delay: Duration) {
306 request_after(&self.ctx, &self.scheduler, self.reason, delay, false);
307 }
308
309 pub fn reason(&self) -> Reason {
310 self.reason
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 const STATUS: Reason = Reason("status");
319 const PLAYBACK: Reason = Reason("playback");
320
321 #[test]
322 fn frames_are_credited_to_the_reasons_that_were_due() {
323 let s = Scheduler::default();
324 s.request(PLAYBACK, f64::NEG_INFINITY);
325 s.request(STATUS, 1.0);
326 s.begin_frame(0.5);
327 s.begin_frame(1.0);
329 s.request(STATUS, 2.0);
330 s.request(Reason::JOB, 1.15);
331 s.begin_frame(1.15);
332 let st = s.stats();
333 assert_eq!(st.frames, 3);
334 assert_eq!(st.frames_for(PLAYBACK), 1);
335 assert_eq!(st.frames_for(STATUS), 1);
336 assert_eq!(st.frames_for(Reason::JOB), 1);
337 assert_eq!(st.frames_for(Reason::INPUT), 0);
338 assert_eq!(st.requests_for(STATUS), 2);
339 assert_eq!(st.requests(), 4);
340 let v = serde_json::to_value(&st).unwrap_or_default();
341 assert_eq!(v["frames_by"], serde_json::json!({"status": 1, "playback": 1, "job": 1}));
342 assert_eq!(v["requests_by"], serde_json::json!({"playback": 1, "status": 2, "job": 1}));
343 assert_eq!(v["frames"], serde_json::json!(3));
344 let back: WakeStats = serde_json::from_value(v).unwrap_or_default();
345 assert_eq!(back, st);
346 }
347
348 #[test]
349 fn a_frame_counts_once_per_reason_and_the_earliest_request_wins() {
350 let s = Scheduler::default();
351 s.request(Reason::TIMER, 5.0);
352 s.request(Reason::TIMER, 1.0);
353 s.request(Reason::TIMER, 3.0);
354 s.begin_frame(1.0);
355 let st = s.stats();
356 assert_eq!((st.frames_for(Reason::TIMER), st.requests_for(Reason::TIMER), st.frames), (1, 3, 1));
357 }
358
359 #[test]
360 fn deadlines_not_yet_due_stay_pending_each_one_gets_its_frame() {
361 let s = Scheduler::default();
362 s.request(Reason::SETTINGS, 1.0);
363 s.request(Reason::SETTINGS, 1.0); s.request(Reason::SETTINGS, 1.4);
365 s.begin_frame(0.2); s.begin_frame(1.0);
367 s.begin_frame(1.2);
368 s.begin_frame(1.4);
369 let st = s.stats();
370 assert_eq!((st.frames_for(Reason::SETTINGS), st.frames_for(Reason::INPUT), st.requests_for(Reason::SETTINGS)), (2, 2, 3));
371 s.request(Reason::TIMER, 9.0);
372 s.cancel(Reason::TIMER);
373 s.begin_frame(9.0);
374 assert_eq!(s.stats().frames_for(Reason::TIMER), 0);
375 for i in 0..100 {
376 s.request(Reason::JOB, 20.0 + f64::from(i));
377 }
378 s.begin_frame(200.0);
379 assert_eq!(s.stats().frames_for(Reason::JOB), 1, "a capped list still ends with the latest deadline");
380 }
381
382 #[test]
383 fn standard_names_are_unique() {
384 let mut names: Vec<_> = Reason::STANDARD.iter().map(|r| r.name()).collect();
385 names.sort_unstable();
386 names.dedup();
387 assert_eq!(names.len(), Reason::STANDARD.len());
388 }
389
390 #[test]
391 fn wakes_reach_the_context_and_its_scheduler() {
392 let ctx = egui::Context::default();
393 let w = waker(&ctx, Reason::JOB);
394 let t = std::thread::spawn(move || w.wake());
395 assert!(t.join().is_ok());
396 wake_after(&ctx, Reason::TIMER, Duration::from_millis(10));
397 let st = Scheduler::of(&ctx).stats();
398 assert_eq!((st.requests_for(Reason::JOB), st.requests_for(Reason::TIMER)), (1, 1));
399 let s = Scheduler::of(&ctx);
401 ctx.memory_mut(|m| *m = egui::Memory::default());
402 s.install(&ctx);
403 assert_eq!(Scheduler::of(&ctx).stats().requests(), 2);
404 }
405}