Skip to main content

rivet/
timer.rs

1//! Fixed-size timer queue backing [`crate::time::Sleep`].
2//!
3//! A task calling `Sleep::<MICROS>::new().await` registers a deadline here
4//! instead of busy-polling — the arch timer ISR (`riscv::timer_tick` /
5//! `cortex_m::systick_handler`) calls [`poll_timers`] on every tick, which
6//! wakes any task whose deadline has passed. This is what makes
7//! `port::arch::idle()` (WFI) a real power-saving wait instead of a spin loop:
8//! between ticks, no task is marked ready, so the executor actually sleeps.
9//!
10//! Slots are `u64`-deadline `UnsafeCell`s guarded by [`crate::critical`]
11//! rather than atomics, since RV32 has no native 64-bit atomic ops (even
12//! with the `A` extension, which is 32-bit/pointer-width only).
13
14use core::cell::UnsafeCell;
15
16/// Maximum number of outstanding timers (RIVET_MAX_TIMERS; one per task
17/// blocked in `Sleep`, so this should be >= `MAX_TASKS` if every task might
18/// sleep concurrently).
19pub const MAX_TIMERS: usize = crate::config::MAX_TIMERS;
20
21struct TimerSlot {
22    /// Deadline in microseconds. 0 = slot unused.
23    deadline: UnsafeCell<u64>,
24    task: UnsafeCell<crate::task::TaskId>,
25}
26
27// Safety: all access goes through `critical::enter`, which disables
28// interrupts (single-core), so there is no concurrent access.
29unsafe impl Sync for TimerSlot {}
30
31// Inline const avoids a named `const` item with interior mutability
32// (clippy::declare_interior_mutable_const).
33static TIMER_SLOTS: [TimerSlot; MAX_TIMERS] = [const {
34    TimerSlot {
35        deadline: UnsafeCell::new(0),
36        task: UnsafeCell::new(crate::task::TaskId::new(0, 0)),
37    }
38}; MAX_TIMERS];
39
40/// Handle to a registered timer slot; carries the slot index and the
41/// registered deadline so a stale [`cancel_deadline`] (after the slot was
42/// already freed and reused) is a harmless no-op (plan.md [B7]).
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct TimerHandle {
45    slot: u8,
46    deadline: u64,
47}
48
49/// Register a wake-up for `(priority, index)` at `deadline_us`.
50/// Called by `Sleep::poll` on first poll.
51///
52/// Returns a handle that can be used to cancel the registration (plan.md
53/// [B7]: a dropped `Sleep` must not leak its slot), or
54/// [`TimerQueueFull`] when every slot is in use.
55///
56/// Exposed beyond `pub(crate)` only under `feature = "test-support"`, so
57/// the property tests (plan.md §1.4) can drive the queue directly.
58#[cfg(not(feature = "test-support"))]
59pub(crate) fn register_deadline(
60    deadline_us: u64,
61    task: crate::task::TaskId,
62) -> Result<TimerHandle, TimerQueueFull> {
63    register_deadline_impl(deadline_us, task)
64}
65
66/// See [`register_deadline`].
67#[cfg(feature = "test-support")]
68pub fn register_deadline(
69    deadline_us: u64,
70    task: crate::task::TaskId,
71) -> Result<TimerHandle, TimerQueueFull> {
72    register_deadline_impl(deadline_us, task)
73}
74
75fn register_deadline_impl(
76    deadline_us: u64,
77    task: crate::task::TaskId,
78) -> Result<TimerHandle, TimerQueueFull> {
79    crate::critical::enter(|| {
80        for (i, slot) in TIMER_SLOTS.iter().enumerate() {
81            // SAFETY: all access to TIMER_SLOTS goes through
82            // `critical::enter` (interrupts disabled on single-core
83            // targets), so no concurrent access is possible.
84            unsafe {
85                if *slot.deadline.get() == 0 {
86                    *slot.task.get() = task;
87                    *slot.deadline.get() = deadline_us;
88                    return Ok(TimerHandle {
89                        slot: i as u8,
90                        deadline: deadline_us,
91                    });
92                }
93            }
94        }
95        Err(TimerQueueFull)
96    })
97}
98
99/// Cancel a registered deadline. Safe to call with a stale handle: the
100/// slot is only cleared if its deadline still matches the handle's
101/// (plan.md [B7] — a cancelled `Sleep` must not free a *new* registration
102/// that reused its slot).
103pub(crate) fn cancel_deadline(handle: TimerHandle) {
104    if let Some(slot) = TIMER_SLOTS.get(handle.slot as usize) {
105        crate::critical::enter(|| {
106            // SAFETY: guarded by critical::enter (see register).
107            unsafe {
108                if *slot.deadline.get() == handle.deadline {
109                    *slot.deadline.get() = 0;
110                }
111            }
112        });
113    }
114}
115
116/// Scan for expired timers and wake their tasks. Call from the platform
117/// timer ISR on every tick.
118pub fn poll_timers(now_us: u64) {
119    let mut woke_any = false;
120    crate::critical::enter(|| {
121        for slot in &TIMER_SLOTS {
122            // SAFETY: all access to TIMER_SLOTS goes through
123            // `critical::enter` (interrupts disabled on single-core
124            // targets), so no concurrent access is possible.
125            unsafe {
126                let d = *slot.deadline.get();
127                if d != 0 && now_us >= d {
128                    *slot.deadline.get() = 0;
129                    crate::waker::mark_ready(*slot.task.get());
130                    woke_any = true;
131                }
132            }
133        }
134    });
135    // plan.md Phase 24, found on real dual-core hardware: only one hart
136    // ever calls this function (whichever owns the periodic tick — see
137    // every board's own `tick_start` docs), but the task a cooperative
138    // waker just marked ready could be hosted by the async executor
139    // running on any hart, including one that's currently idling
140    // (`waiti`/`wfi`) waiting for exactly this kind of news. `mark_ready`
141    // itself only flips bitmap flags; nothing else here was telling that
142    // *other* hart to wake up and look — on real dual-core hardware, an
143    // executor task idling on the non-tick-owning core would never
144    // notice its `Sleep` had expired, waiting in `waiti` forever even
145    // though the work was genuinely ready (confirmed: `smp_test.rs`'s
146    // monitor task hung exactly this way; a single-core `Sleep` test,
147    // where the tick-owning hart and the only hart are trivially the
148    // same one, passed cleanly, isolating this as *specifically* the
149    // missing piece). No per-task hart affinity is tracked, so this
150    // broadcasts to every other hart rather than targeting one — a
151    // spurious wake on a hart with nothing to do is a cheap, harmless
152    // no-op (it just re-checks and goes back to idling); a real wake
153    // that's never delivered is not.
154    if woke_any {
155        let hart = crate::port::arch::hart_id();
156        for other in 0..crate::config::MAX_HARTS {
157            if other != hart {
158                crate::port::arch::request_reschedule_on(other);
159            }
160        }
161    }
162    poll_ptask_deadlines(now_us);
163}
164
165// ── Preemptive-task block-with-timeout deadlines ────────────────────
166//
167// Backs `PriorityMutex::lock_timeout` (and later `Semaphore`/`Channel`
168// timeouts): one deadline slot per preemptive task id (indexed by id), so
169// a blocked task is unblocked by the tick when its deadline passes. The
170// cooperative tier has its own wake mechanism (the waker bitmap); this is
171// the preemptive-tier analog, keyed by task id and unblocking via
172// `sched::unblock`.
173
174/// Deadline slots, indexed by task id (0 = no deadline registered).
175struct PtaskDeadline {
176    deadline: UnsafeCell<u64>,
177}
178
179// SAFETY: all access goes through `critical::enter` (interrupts disabled,
180// single-core), so there is no concurrent access.
181unsafe impl Sync for PtaskDeadline {}
182
183// Inline const avoids a named `const` item with interior mutability
184// (clippy::declare_interior_mutable_const).
185static PTASK_DEADLINES: [PtaskDeadline; crate::preempt::tcb::MAX_PTASKS] = [const {
186    PtaskDeadline {
187        deadline: UnsafeCell::new(0),
188    }
189};
190    crate::preempt::tcb::MAX_PTASKS];
191
192/// Register a wake-up deadline for a blocked preemptive task. Replaces any
193/// previous registration for the same task.
194pub(crate) fn register_ptask_deadline(deadline_us: u64, task: usize) -> Result<(), TimerQueueFull> {
195    let Some(slot) = PTASK_DEADLINES.get(task) else {
196        return Err(TimerQueueFull);
197    };
198    crate::critical::enter(|| {
199        // SAFETY: guarded by critical::enter; single writer per task slot
200        // (the blocking task), reader is the tick ISR.
201        unsafe {
202            *slot.deadline.get() = deadline_us;
203        }
204    });
205    Ok(())
206}
207
208/// Cancel a preemptive task's block deadline (e.g. it acquired the
209/// resource before the deadline). No-op if none registered.
210pub(crate) fn cancel_ptask_deadline(task: usize) {
211    if let Some(slot) = PTASK_DEADLINES.get(task) {
212        crate::critical::enter(|| {
213            // SAFETY: guarded by critical::enter (see register).
214            unsafe {
215                *slot.deadline.get() = 0;
216            }
217        });
218    }
219}
220
221/// Wake preemptive tasks whose block deadline has passed.
222fn poll_ptask_deadlines(now_us: u64) {
223    let mut woke_any = false;
224    crate::critical::enter(|| {
225        for (task, slot) in PTASK_DEADLINES.iter().enumerate() {
226            // SAFETY: guarded by critical::enter (see register).
227            unsafe {
228                let d = *slot.deadline.get();
229                if d != 0 && now_us >= d {
230                    *slot.deadline.get() = 0;
231                    crate::preempt::sched::unblock(task);
232                    woke_any = true;
233                }
234            }
235        }
236    });
237    // Same reasoning, same fix as `poll_timers`'s identical broadcast
238    // above (plan.md Phase 24) — a preemptive task's own blocking
239    // timeout (`PriorityMutex::lock_timeout` etc.) can unblock a task
240    // that's not "current" on this hart at all.
241    if woke_any {
242        let hart = crate::port::arch::hart_id();
243        for other in 0..crate::config::MAX_HARTS {
244            if other != hart {
245                crate::port::arch::request_reschedule_on(other);
246            }
247        }
248    }
249}
250
251/// Queue-full error returned by timer registration APIs (plan.md §4.3).
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub struct TimerQueueFull;
254
255/// Test-only: clear every timer slot. Part of the global reset done by
256/// [`crate::kernel_test!`].
257#[cfg(feature = "test-support")]
258pub(crate) fn reset_for_test() {
259    crate::critical::enter(|| {
260        for slot in &TIMER_SLOTS {
261            // SAFETY: all access to TIMER_SLOTS goes through
262            // `critical::enter` (interrupts disabled, single-core), so no
263            // concurrent access is possible here.
264            unsafe {
265                *slot.deadline.get() = 0;
266            }
267        }
268        for slot in &PTASK_DEADLINES {
269            // SAFETY: same guard as above.
270            unsafe {
271                *slot.deadline.get() = 0;
272            }
273        }
274    });
275}
276
277/// Count of timer slots currently in use. Used by host-test reset/
278/// inspection helpers and by [`crate::report`].
279pub fn slots_in_use() -> usize {
280    crate::critical::enter(|| {
281        TIMER_SLOTS
282            .iter()
283            // SAFETY: all access to TIMER_SLOTS goes through
284            // `critical::enter` (interrupts disabled), so reads are
285            // exclusive.
286            .filter(|slot| unsafe { *slot.deadline.get() != 0 })
287            .count()
288    })
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn register_and_expire() {
297        crate::kernel_test! {
298            register_deadline(1000, crate::task::TaskId::new(3, 2)).unwrap();
299            poll_timers(500); // not yet
300            assert_eq!(crate::waker::next_ready(), None);
301
302            poll_timers(1000); // now expired
303            assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(3, 2)));
304        }
305    }
306
307    #[test]
308    fn multiple_timers_independent() {
309        crate::kernel_test! {
310            register_deadline(100, crate::task::TaskId::new(1, 0)).unwrap();
311            register_deadline(200, crate::task::TaskId::new(2, 0)).unwrap();
312
313            poll_timers(150);
314            assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(1, 0)));
315            assert_eq!(crate::waker::next_ready(), None);
316
317            poll_timers(250);
318            assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(2, 0)));
319        }
320    }
321}