rivet/time.rs
1//! Static timing: const-generic durations and async sleep futures.
2//!
3//! `Sleep` registers its deadline with [`crate::timer`] on first poll and
4//! then returns `Pending` without re-arming its own waker — the platform
5//! timer ISR wakes it when the deadline passes. This means a sleeping task
6//! does not busy-poll: between ticks the executor has nothing ready and
7//! genuinely enters `port::arch::idle()` (WFI), which is what makes tickless
8//! idle actually save power instead of spinning.
9//!
10//! ```ignore
11//! use rivet::time::Sleep;
12//!
13//! #[rivet::task(priority = 0)]
14//! async fn blink() {
15//! loop {
16//! toggle_led();
17//! Sleep::<500_000>::new().await; // 500ms
18//! }
19//! }
20//! ```
21
22use core::future::Future;
23use core::pin::Pin;
24use core::task::{Context, Poll};
25
26/// A duration in microseconds, known at compile time.
27#[derive(Clone, Copy, Debug)]
28pub struct Duration {
29 micros: u64,
30}
31
32impl Duration {
33 pub const fn from_micros(micros: u64) -> Self {
34 Self { micros }
35 }
36
37 pub const fn from_millis(ms: u64) -> Self {
38 Self { micros: ms * 1000 }
39 }
40
41 pub const fn as_micros(&self) -> u64 {
42 self.micros
43 }
44
45 pub const fn as_millis(&self) -> u64 {
46 self.micros / 1000
47 }
48}
49
50/// A future that resolves after a compile-time-known duration has elapsed.
51///
52/// The const generic `MICROS` encodes the sleep duration. Must be polled
53/// from within a `#[rivet::task]` (needs [`crate::executor::current_task`]
54/// to register the wake-up).
55pub struct Sleep<const MICROS: u64> {
56 deadline: u64,
57 /// Outstanding timer-slot registration, if any. Cleared on completion;
58 /// cancels in [`Drop`] so a dropped sleep never leaks a slot or fires
59 /// a spurious wake (plan.md [B7]).
60 slot: Option<crate::timer::TimerHandle>,
61}
62
63impl<const MICROS: u64> Sleep<MICROS> {
64 /// Create a new sleep future. The deadline is computed on first poll.
65 pub const fn new() -> Self {
66 Self {
67 deadline: 0,
68 slot: None,
69 }
70 }
71}
72
73impl<const MICROS: u64> Default for Sleep<MICROS> {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl<const MICROS: u64> Drop for Sleep<MICROS> {
80 fn drop(&mut self) {
81 if let Some(handle) = self.slot.take() {
82 crate::timer::cancel_deadline(handle);
83 }
84 }
85}
86
87impl<const MICROS: u64> Future for Sleep<MICROS> {
88 type Output = ();
89
90 fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
91 // SAFETY: `Sleep` is a plain struct with no `!Unpin` fields;
92 // projecting through the pinned reference is sound.
93 let this = unsafe { self.get_unchecked_mut() };
94 let now = crate::port::board::now_us();
95
96 if this.deadline == 0 {
97 let deadline = now.wrapping_add(MICROS).max(1); // avoid the 0 "unset" sentinel
98 this.deadline = deadline;
99
100 let id = crate::executor::current_task()
101 .expect("Sleep::poll() called outside of a task context");
102 // Queue full surfaces as a documented panic at the call site —
103 // a sleep that cannot register would silently never fire.
104 let handle = crate::timer::register_deadline(deadline, id).unwrap_or_else(|_| {
105 panic!(
106 "rivet: Sleep timer queue full ({} concurrent sleeps supported)",
107 crate::timer::MAX_TIMERS
108 )
109 });
110 this.slot = Some(handle);
111
112 if now >= deadline {
113 // MICROS == 0 or wrapped: already elapsed.
114 return Poll::Ready(());
115 }
116 return Poll::Pending;
117 }
118
119 if now >= this.deadline {
120 // The timer ISR already cleared the slot when it fired; the
121 // handle is now stale and its eventual cancel is a no-op.
122 this.slot = None;
123 Poll::Ready(())
124 } else {
125 // Not yet elapsed. Do NOT re-wake ourselves — the timer ISR
126 // (registered above) will call waker::mark_ready when the
127 // deadline passes. Busy-waking here would defeat tickless idle.
128 Poll::Pending
129 }
130 }
131}