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        crate::waker::broadcast_reschedule();
156    }
157    poll_ptask_deadlines(now_us);
158}
159
160// ── Preemptive-task block-with-timeout deadlines ────────────────────
161//
162// Backs `PriorityMutex::lock_timeout` (and later `Semaphore`/`Channel`
163// timeouts): one deadline slot per preemptive task id (indexed by id), so
164// a blocked task is unblocked by the tick when its deadline passes. The
165// cooperative tier has its own wake mechanism (the waker bitmap); this is
166// the preemptive-tier analog, keyed by task id and unblocking via
167// `sched::unblock`.
168
169/// Deadline slots, indexed by task id (0 = no deadline registered).
170struct PtaskDeadline {
171    deadline: UnsafeCell<u64>,
172}
173
174// SAFETY: all access goes through `critical::enter` (interrupts disabled,
175// single-core), so there is no concurrent access.
176unsafe impl Sync for PtaskDeadline {}
177
178// Inline const avoids a named `const` item with interior mutability
179// (clippy::declare_interior_mutable_const).
180static PTASK_DEADLINES: [PtaskDeadline; crate::preempt::tcb::MAX_PTASKS] = [const {
181    PtaskDeadline {
182        deadline: UnsafeCell::new(0),
183    }
184};
185    crate::preempt::tcb::MAX_PTASKS];
186
187/// Register a wake-up deadline for a blocked preemptive task. Replaces any
188/// previous registration for the same task.
189pub(crate) fn register_ptask_deadline(deadline_us: u64, task: usize) -> Result<(), TimerQueueFull> {
190    let Some(slot) = PTASK_DEADLINES.get(task) else {
191        return Err(TimerQueueFull);
192    };
193    crate::critical::enter(|| {
194        // SAFETY: guarded by critical::enter; single writer per task slot
195        // (the blocking task), reader is the tick ISR.
196        unsafe {
197            *slot.deadline.get() = deadline_us;
198        }
199    });
200    Ok(())
201}
202
203/// Cancel a preemptive task's block deadline (e.g. it acquired the
204/// resource before the deadline). No-op if none registered.
205pub(crate) fn cancel_ptask_deadline(task: usize) {
206    if let Some(slot) = PTASK_DEADLINES.get(task) {
207        crate::critical::enter(|| {
208            // SAFETY: guarded by critical::enter (see register).
209            unsafe {
210                *slot.deadline.get() = 0;
211            }
212        });
213    }
214}
215
216/// Wake preemptive tasks whose block deadline has passed.
217fn poll_ptask_deadlines(now_us: u64) {
218    let mut woke_any = false;
219    crate::critical::enter(|| {
220        for (task, slot) in PTASK_DEADLINES.iter().enumerate() {
221            // SAFETY: guarded by critical::enter (see register).
222            unsafe {
223                let d = *slot.deadline.get();
224                if d != 0 && now_us >= d {
225                    *slot.deadline.get() = 0;
226                    crate::preempt::sched::unblock(task);
227                    woke_any = true;
228                }
229            }
230        }
231    });
232    // Same reasoning, same fix as `poll_timers`'s identical broadcast
233    // above (plan.md Phase 24) — a preemptive task's own blocking
234    // timeout (`PriorityMutex::lock_timeout` etc.) can unblock a task
235    // that's not "current" on this hart at all.
236    if woke_any {
237        let hart = crate::port::arch::hart_id();
238        for other in 0..crate::config::MAX_HARTS {
239            if other != hart {
240                crate::port::arch::request_reschedule_on(other);
241            }
242        }
243    }
244}
245
246/// Queue-full error returned by timer registration APIs (plan.md §4.3).
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub struct TimerQueueFull;
249
250/// Test-only: clear every timer slot. Part of the global reset done by
251/// [`crate::kernel_test!`].
252#[cfg(feature = "test-support")]
253pub(crate) fn reset_for_test() {
254    crate::critical::enter(|| {
255        for slot in &TIMER_SLOTS {
256            // SAFETY: all access to TIMER_SLOTS goes through
257            // `critical::enter` (interrupts disabled, single-core), so no
258            // concurrent access is possible here.
259            unsafe {
260                *slot.deadline.get() = 0;
261            }
262        }
263        for slot in &PTASK_DEADLINES {
264            // SAFETY: same guard as above.
265            unsafe {
266                *slot.deadline.get() = 0;
267            }
268        }
269    });
270}
271
272/// Count of timer slots currently in use. Used by host-test reset/
273/// inspection helpers and by [`crate::report`].
274pub fn slots_in_use() -> usize {
275    crate::critical::enter(|| {
276        TIMER_SLOTS
277            .iter()
278            // SAFETY: all access to TIMER_SLOTS goes through
279            // `critical::enter` (interrupts disabled), so reads are
280            // exclusive.
281            .filter(|slot| unsafe { *slot.deadline.get() != 0 })
282            .count()
283    })
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn register_and_expire() {
292        crate::kernel_test! {
293            register_deadline(1000, crate::task::TaskId::new(3, 2)).unwrap();
294            poll_timers(500); // not yet
295            assert_eq!(crate::waker::next_ready(), None);
296
297            poll_timers(1000); // now expired
298            assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(3, 2)));
299        }
300    }
301
302    #[test]
303    fn multiple_timers_independent() {
304        crate::kernel_test! {
305            register_deadline(100, crate::task::TaskId::new(1, 0)).unwrap();
306            register_deadline(200, crate::task::TaskId::new(2, 0)).unwrap();
307
308            poll_timers(150);
309            assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(1, 0)));
310            assert_eq!(crate::waker::next_ready(), None);
311
312            poll_timers(250);
313            assert_eq!(crate::waker::next_ready(), Some(crate::task::TaskId::new(2, 0)));
314        }
315    }
316}