3 releases
Uses new Rust 2024
| 0.1.2 | May 17, 2026 |
|---|---|
| 0.1.1 | May 17, 2026 |
| 0.1.0 | May 14, 2026 |
#638 in GUI
39KB
446 lines
Understory Timing: host-agnostic timer queue primitives.
This crate provides a small, deterministic core for ordering timers by monotonic deadlines. It is intended for UI toolkits, event loops, and other host runtimes that want timer bookkeeping without taking on clocks, threads, async reactors, callbacks, or platform wakeups.
The core concepts are:
TimerInstantandTimerDuration: host-provided integer ticks. Most hosts use nanoseconds, but the queue treats them as opaque monotonic labels.TimerQueue: a deadline-ordered queue of pending timers.TimerId: the queue-assigned id used for queue-local cancellation and delivery recognition.TimerRepeat: the policy for calculating a repeating timer's next deadline.ExpiredTimer: an owned record returned to the host once a timer is due.
This crate deliberately does not know about wall-clock time, sleeping, wakeup registration, async tasks, widgets, rendering, or redraw policy. Host runtimes are responsible for:
- converting their clock into
TimerInstantandTimerDurationvalues, - calling
TimerQueue::schedule/TimerQueue::schedule_oncefor relative delays, andTimerQueue::schedule_at/TimerQueue::schedule_once_atfor host-computed absolute deadlines, - using
TimerQueue::next_deadlineto arm the host wakeup mechanism, - storing callback or action state in or alongside the timer target payload,
- calling
TimerQueue::pop_expiredwith the current monotonic time when the host wakes, - dispatching each owned expired record to the owner identified by
ExpiredTimer::target, - calling
TimerQueue::rearmafter dispatch if a repeating timer should keep running, - using
TimerQueue::retain_pendingwhen an owner is removed and all of its pending timers should be purged.
The queue is backed by a sorted alloc::collections::VecDeque. It favors a
small dependency-free core and explicit ordering rules over high-volume timer
scheduling machinery. It is a good fit for the modest timer counts common in
UI toolkits and small runtime loops; hosts with very large timer sets can
layer a heap or timing wheel above a different scheduling core.
Invariants
- Pending timers are stored in deadline order.
- Timers with equal deadlines fire in scheduling order.
- Timer ids are stable for the timer record, including after expiration.
- Cancellation is idempotent, applies to pending timers, and reports whether a pending timer was removed.
- Expired timers are removed before they are returned to the host.
- Relative deadline arithmetic saturates at
u64::MAX.
Cancellation and delivery
TimerQueue::cancel removes pending timers only. Once a timer has been
returned by TimerQueue::pop_expired, it is no longer pending; hosts that
batch expired timers before dispatch may still deliver that already-drained
record. Owners should compare the delivered TimerId with their current
stored id or token and ignore stale deliveries.
Repeating timers make rearm explicit for the same reason. To cancel a
repeating timer that has already expired, drop the ExpiredTimer instead
of passing it to TimerQueue::rearm.
Target payloads
Timer targets are host-defined owner handles. Use ids such as element, widget, task, connection, or request ids when possible. Expired timers own their target handle so the queue is not borrowed while the host dispatches the timer.
Minimal example
use understory_timing::TimerQueue;
let mut timers = TimerQueue::new();
let button = 42_u32;
let id = timers.schedule_once(button, 1_000, 250);
assert_eq!(timers.next_deadline(), Some(1_250));
let timer = timers.pop_expired(1_250).expect("timer is due");
assert_eq!(timer.id(), id);
assert_eq!(*timer.target(), button);
assert!(timers.is_empty());
External timer tokens
Host runtimes that already expose their own timer token type can store that
token in the target payload. The queue's TimerId remains useful for
diagnostics or queue-local cancellation, while the host token remains the
value returned from higher-level APIs:
use understory_timing::TimerQueue;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct AppTimerToken(u64);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct TimerTarget {
token: AppTimerToken,
window: u32,
}
let mut timers = TimerQueue::new();
let delivered = AppTimerToken(7);
let cancelled = AppTimerToken(8);
timers.schedule_once(
TimerTarget {
token: delivered,
window: 1,
},
1_000,
250,
);
timers.schedule_once(
TimerTarget {
token: cancelled,
window: 1,
},
1_000,
500,
);
let removed = timers.retain_pending(|timer| timer.target().token != cancelled);
assert_eq!(removed, 1);
let expired = timers.pop_expired(1_250).expect("timer is due");
let target = expired.into_target();
assert_eq!(target.token, delivered);
assert_eq!(target.window, 1);
Minimum supported Rust Version (MSRV)
This crate has been verified to compile with Rust 1.88 and later.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0), or
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT),
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Contribution
Contributions are welcome by pull request. The Rust code of conduct applies. Please feel free to add your name to the AUTHORS file in any substantive pull request.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.