Skip to main content

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}