use std::collections::BTreeMap;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Reason(pub &'static str);
impl Reason {
pub const INPUT: Reason = Reason("input");
pub const STARTUP: Reason = Reason("startup");
pub const CONTROL: Reason = Reason("control");
pub const MENU: Reason = Reason("menu");
pub const DOCUMENT: Reason = Reason("document");
pub const SCREENSHOT: Reason = Reason("screenshot");
pub const JOB: Reason = Reason("job");
pub const AUTOSAVE: Reason = Reason("autosave");
pub const SETTINGS: Reason = Reason("settings");
pub const TIMER: Reason = Reason("timer");
pub const ANIMATION: Reason = Reason("animation");
pub const STANDARD: [Reason; 11] = [
Reason::INPUT,
Reason::STARTUP,
Reason::CONTROL,
Reason::MENU,
Reason::DOCUMENT,
Reason::SCREENSHOT,
Reason::JOB,
Reason::AUTOSAVE,
Reason::SETTINGS,
Reason::TIMER,
Reason::ANIMATION,
];
pub fn name(self) -> &'static str {
self.0
}
}
impl std::fmt::Display for Reason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
const MAX_DUE: usize = 8;
const SLACK_S: f64 = 1e-3;
#[derive(Debug, Default)]
struct State {
due: BTreeMap<&'static str, Vec<f64>>,
requests: BTreeMap<&'static str, u64>,
frames: BTreeMap<&'static str, u64>,
total: u64,
}
fn bump(m: &mut BTreeMap<&'static str, u64>, k: &'static str) {
let n = m.entry(k).or_insert(0);
*n = n.saturating_add(1);
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WakeStats {
pub frames: u64,
pub frames_by: BTreeMap<String, u64>,
pub requests_by: BTreeMap<String, u64>,
}
impl WakeStats {
pub fn frames_for(&self, r: Reason) -> u64 {
self.frames_by.get(r.0).copied().unwrap_or(0)
}
pub fn requests_for(&self, r: Reason) -> u64 {
self.requests_by.get(r.0).copied().unwrap_or(0)
}
pub fn requests(&self) -> u64 {
self.requests_by.values().fold(0, |a, b| a.saturating_add(*b))
}
}
#[derive(Clone, Debug, Default)]
pub struct Scheduler {
state: Arc<Mutex<State>>,
}
fn id() -> egui::Id {
egui::Id::new("ashlar.wake.scheduler")
}
impl Scheduler {
pub fn of(ctx: &egui::Context) -> Scheduler {
ctx.data_mut(|d| d.get_temp_mut_or_default::<Scheduler>(id()).clone())
}
pub fn install(&self, ctx: &egui::Context) {
ctx.data_mut(|d| d.insert_temp(id(), self.clone()));
}
fn lock(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn request(&self, reason: Reason, due: f64) {
let mut s = self.lock();
bump(&mut s.requests, reason.0);
let list = s.due.entry(reason.0).or_default();
if list.iter().any(|d| (d - due).abs() <= SLACK_S || (d.is_infinite() && due.is_infinite())) {
return;
}
let at = list.partition_point(|d| *d < due);
list.insert(at, due);
if list.len() > MAX_DUE {
list.remove(MAX_DUE - 1);
}
}
pub fn begin_frame(&self, now: f64) {
let mut s = self.lock();
let due = std::mem::take(&mut s.due);
let mut any = false;
for (r, mut list) in due {
let n = list.partition_point(|d| *d <= now + SLACK_S);
if n > 0 {
bump(&mut s.frames, r);
any = true;
list.drain(..n);
}
if !list.is_empty() {
s.due.insert(r, list);
}
}
if !any {
bump(&mut s.frames, Reason::INPUT.0);
}
s.total = s.total.saturating_add(1);
}
pub fn cancel(&self, reason: Reason) {
self.lock().due.remove(reason.0);
}
pub fn rearm(&self, ctx: &egui::Context) {
let next = self.lock().due.values().filter_map(|l| l.first().copied()).fold(f64::INFINITY, f64::min);
if !next.is_finite() {
return;
}
let (now, predicted_dt) = ctx.input(|i| (i.time, i.predicted_dt));
let delay = Duration::try_from_secs_f64((next - now).max(0.0)).unwrap_or_default();
if delay.is_zero() {
ctx.request_repaint();
} else {
let early = Duration::try_from_secs_f32(predicted_dt).unwrap_or_default();
ctx.request_repaint_after(delay.saturating_add(early).saturating_add(Duration::from_millis(1)));
}
}
pub fn stats(&self) -> WakeStats {
let s = self.lock();
let owned = |m: &BTreeMap<&'static str, u64>| m.iter().map(|(k, v)| ((*k).to_string(), *v)).collect();
WakeStats { frames: s.total, frames_by: owned(&s.frames), requests_by: owned(&s.requests) }
}
}
pub fn wake(ctx: &egui::Context, reason: Reason) {
Scheduler::of(ctx).request(reason, f64::NEG_INFINITY);
ctx.request_repaint();
}
pub fn wake_after(ctx: &egui::Context, reason: Reason, delay: Duration) {
request_after(ctx, &Scheduler::of(ctx), reason, delay, false);
}
pub fn reschedule(ctx: &egui::Context, reason: Reason, delay: Duration) {
request_after(ctx, &Scheduler::of(ctx), reason, delay, true);
}
fn request_after(ctx: &egui::Context, s: &Scheduler, reason: Reason, delay: Duration, replace: bool) {
if replace {
s.cancel(reason);
}
if delay.is_zero() {
s.request(reason, f64::NEG_INFINITY);
ctx.request_repaint();
return;
}
let (now, predicted_dt) = ctx.input(|i| (i.time, i.predicted_dt));
s.request(reason, now + delay.as_secs_f64());
let early = Duration::try_from_secs_f32(predicted_dt).unwrap_or_default();
ctx.request_repaint_after(delay.saturating_add(early).saturating_add(Duration::from_millis(1)));
}
pub fn waker(ctx: &egui::Context, reason: Reason) -> Waker {
Waker { ctx: ctx.clone(), scheduler: Scheduler::of(ctx), reason }
}
#[derive(Clone, Debug)]
pub struct Waker {
ctx: egui::Context,
scheduler: Scheduler,
reason: Reason,
}
impl Waker {
pub fn wake(&self) {
self.scheduler.request(self.reason, f64::NEG_INFINITY);
self.ctx.request_repaint();
}
pub fn reschedule(&self, delay: Duration) {
request_after(&self.ctx, &self.scheduler, self.reason, delay, true);
}
pub fn wake_after(&self, delay: Duration) {
request_after(&self.ctx, &self.scheduler, self.reason, delay, false);
}
pub fn reason(&self) -> Reason {
self.reason
}
}
#[cfg(test)]
mod tests {
use super::*;
const STATUS: Reason = Reason("status");
const PLAYBACK: Reason = Reason("playback");
#[test]
fn frames_are_credited_to_the_reasons_that_were_due() {
let s = Scheduler::default();
s.request(PLAYBACK, f64::NEG_INFINITY);
s.request(STATUS, 1.0);
s.begin_frame(0.5);
s.begin_frame(1.0);
s.request(STATUS, 2.0);
s.request(Reason::JOB, 1.15);
s.begin_frame(1.15);
let st = s.stats();
assert_eq!(st.frames, 3);
assert_eq!(st.frames_for(PLAYBACK), 1);
assert_eq!(st.frames_for(STATUS), 1);
assert_eq!(st.frames_for(Reason::JOB), 1);
assert_eq!(st.frames_for(Reason::INPUT), 0);
assert_eq!(st.requests_for(STATUS), 2);
assert_eq!(st.requests(), 4);
let v = serde_json::to_value(&st).unwrap_or_default();
assert_eq!(v["frames_by"], serde_json::json!({"status": 1, "playback": 1, "job": 1}));
assert_eq!(v["requests_by"], serde_json::json!({"playback": 1, "status": 2, "job": 1}));
assert_eq!(v["frames"], serde_json::json!(3));
let back: WakeStats = serde_json::from_value(v).unwrap_or_default();
assert_eq!(back, st);
}
#[test]
fn a_frame_counts_once_per_reason_and_the_earliest_request_wins() {
let s = Scheduler::default();
s.request(Reason::TIMER, 5.0);
s.request(Reason::TIMER, 1.0);
s.request(Reason::TIMER, 3.0);
s.begin_frame(1.0);
let st = s.stats();
assert_eq!((st.frames_for(Reason::TIMER), st.requests_for(Reason::TIMER), st.frames), (1, 3, 1));
}
#[test]
fn deadlines_not_yet_due_stay_pending_each_one_gets_its_frame() {
let s = Scheduler::default();
s.request(Reason::SETTINGS, 1.0);
s.request(Reason::SETTINGS, 1.0); s.request(Reason::SETTINGS, 1.4);
s.begin_frame(0.2); s.begin_frame(1.0);
s.begin_frame(1.2);
s.begin_frame(1.4);
let st = s.stats();
assert_eq!((st.frames_for(Reason::SETTINGS), st.frames_for(Reason::INPUT), st.requests_for(Reason::SETTINGS)), (2, 2, 3));
s.request(Reason::TIMER, 9.0);
s.cancel(Reason::TIMER);
s.begin_frame(9.0);
assert_eq!(s.stats().frames_for(Reason::TIMER), 0);
for i in 0..100 {
s.request(Reason::JOB, 20.0 + f64::from(i));
}
s.begin_frame(200.0);
assert_eq!(s.stats().frames_for(Reason::JOB), 1, "a capped list still ends with the latest deadline");
}
#[test]
fn standard_names_are_unique() {
let mut names: Vec<_> = Reason::STANDARD.iter().map(|r| r.name()).collect();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), Reason::STANDARD.len());
}
#[test]
fn wakes_reach_the_context_and_its_scheduler() {
let ctx = egui::Context::default();
let w = waker(&ctx, Reason::JOB);
let t = std::thread::spawn(move || w.wake());
assert!(t.join().is_ok());
wake_after(&ctx, Reason::TIMER, Duration::from_millis(10));
let st = Scheduler::of(&ctx).stats();
assert_eq!((st.requests_for(Reason::JOB), st.requests_for(Reason::TIMER)), (1, 1));
let s = Scheduler::of(&ctx);
ctx.memory_mut(|m| *m = egui::Memory::default());
s.install(&ctx);
assert_eq!(Scheduler::of(&ctx).stats().requests(), 2);
}
}