use crate::platform::time::Instant;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
pub const DEFAULT_INTERVAL: Duration = Duration::from_nanos(16_666_667);
#[derive(Clone)]
struct Source {
arm: Arc<dyn Fn() + Send + Sync>,
disarm: Option<Arc<dyn Fn() + Send + Sync>>,
}
struct Clock {
wanted: Option<Instant>,
claimed: bool,
last: Option<Instant>,
interval: Duration,
idle: bool,
}
impl Clock {
const fn new() -> Self {
Self {
wanted: None,
claimed: false,
last: None,
interval: DEFAULT_INTERVAL,
idle: false,
}
}
fn request(&mut self, now: Instant) {
self.wanted.get_or_insert(now);
self.idle = false;
}
fn deadline(&self) -> Option<Instant> {
let asked = self.wanted?;
Some(if self.claimed {
asked + self.interval * 2
} else {
match self.last {
Some(last) => asked.max(last + self.interval),
None => asked,
}
})
}
fn tick(&mut self, now: Instant) -> bool {
if self.wanted.is_none() {
self.claimed = false;
return false;
}
self.deliver(now);
true
}
fn deliver(&mut self, now: Instant) {
self.wanted = None;
self.claimed = false;
self.last = Some(now);
}
fn take_due(&mut self, now: Instant) -> bool {
match self.deadline() {
Some(at) if at <= now => {
self.deliver(now);
true
}
_ => false,
}
}
}
static CLOCK: Mutex<Clock> = Mutex::new(Clock::new());
static SOURCE: Mutex<Option<Source>> = Mutex::new(None);
fn clock<R>(f: impl FnOnce(&mut Clock) -> R) -> R {
let mut c = CLOCK.lock().unwrap_or_else(|e| e.into_inner());
f(&mut c)
}
fn source() -> Option<Source> {
SOURCE.lock().ok().and_then(|s| s.clone())
}
pub fn request() {
let Some(now) = now() else {
return; };
let source = source();
let deadline = clock(|c| {
c.request(now);
if source.is_some() {
c.claimed = true;
}
c.deadline()
});
if let Some(source) = source {
(source.arm)();
}
if let Some(at) = deadline {
super::wake::schedule(at);
}
}
pub fn wanted() -> bool {
clock(|c| c.wanted.is_some())
}
pub(crate) fn tick() -> bool {
let Some(now) = now() else {
return false;
};
let wanted = clock(|c| c.tick(now));
if wanted {
super::wake::wake();
}
wanted
}
#[cfg_attr(not(any(target_os = "macos", target_os = "ios")), allow(dead_code))]
pub(crate) fn idle() {
clock(|c| c.idle = true);
super::wake::wake();
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(crate) fn claim() {
clock(|c| {
if c.wanted.is_some() {
c.claimed = true;
}
});
}
#[cfg_attr(
not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android",
target_arch = "wasm32"
)),
allow(dead_code)
)]
pub(crate) fn install(
arm: Arc<dyn Fn() + Send + Sync>,
disarm: Option<Arc<dyn Fn() + Send + Sync>>,
) {
if let Ok(mut s) = SOURCE.lock() {
*s = Some(Source { arm, disarm });
}
}
#[cfg_attr(
not(any(target_os = "linux", target_os = "macos", target_os = "ios")),
allow(dead_code)
)]
pub(crate) fn set_interval(interval: Duration) {
if (Duration::from_millis(2)..=Duration::from_millis(100)).contains(&interval) {
clock(|c| c.interval = interval);
}
}
pub fn interval() -> Duration {
clock(|c| c.interval)
}
pub(crate) fn deadline() -> Option<Instant> {
clock(|c| c.deadline())
}
pub(crate) fn take_due(now: Instant) -> bool {
let (due, stop) = clock(|c| {
let due = c.take_due(now);
let stop = std::mem::take(&mut c.idle) && c.wanted.is_none();
(due, stop)
});
if stop && let Some(disarm) = source().and_then(|s| s.disarm) {
disarm();
}
due
}
pub(crate) fn now() -> Option<Instant> {
Some(Instant::now())
}
pub fn elapsed() -> Option<Duration> {
static EPOCH: OnceLock<Instant> = OnceLock::new();
let now = now()?;
Some(now.saturating_duration_since(*EPOCH.get_or_init(|| now)))
}
#[cfg(test)]
mod tests {
use super::*;
const MS: Duration = Duration::from_millis(1);
#[test]
fn timer_paces_frames_at_the_interval() {
let mut c = Clock::new();
let t0 = Instant::now();
assert_eq!(c.deadline(), None, "idle: no frames");
c.request(t0);
assert_eq!(c.deadline(), Some(t0), "the first frame comes at once");
assert!(c.take_due(t0));
assert_eq!(c.deadline(), None, "delivered, and nothing asked again");
c.request(t0 + 5 * MS);
assert_eq!(
c.deadline(),
Some(t0 + c.interval),
"paced from the last frame"
);
assert!(!c.take_due(t0 + 10 * MS));
assert!(c.take_due(t0 + 17 * MS));
}
#[test]
fn a_source_delivers_and_the_timer_backs_it_up() {
let mut c = Clock::new();
let t0 = Instant::now();
c.request(t0);
c.claimed = true;
assert_eq!(c.deadline(), Some(t0 + c.interval * 2), "fallback only");
assert!(c.tick(t0 + 16 * MS));
assert_eq!(c.deadline(), None);
assert!(!c.tick(t0 + 33 * MS), "no frame wanted: an idle tick");
c.request(t0 + 40 * MS);
c.claimed = true;
assert!(!c.take_due(t0 + 50 * MS));
assert!(
c.take_due(t0 + 80 * MS),
"the source never came: the timer did"
);
}
#[test]
fn repeated_requests_ask_for_one_frame() {
let mut c = Clock::new();
let t0 = Instant::now();
c.request(t0);
c.request(t0 + 3 * MS);
assert_eq!(c.wanted, Some(t0), "the first request's time stands");
assert!(c.take_due(t0 + 3 * MS));
assert!(!c.take_due(t0 + 50 * MS), "one frame");
}
}