rivet/deadlines.rs
1//! Periods, drift-corrected periodic wake, and CPU-budget enforcement
2//! (plan.md Phase 11).
3//!
4//! Both are configured per task by id via [`set_period_us`]/
5//! [`set_budget_us`] (wired up through [`crate::preempt::TaskHandle`]).
6//! `period_us` is consumed by [`wait_period`], which a periodic task calls
7//! once per iteration instead of `sleep_ms` — the deadline is computed
8//! from the *previous* deadline, not `now`, so per-iteration jitter never
9//! accumulates into long-term drift (the same technique `soak`/`drift_test`
10//! already exercise for the timer queue itself). `budget_us` is checked
11//! every tick against the task's accumulated busy time *within the current
12//! period* (via [`crate::exec_time`]); exceeding it raises
13//! [`crate::fault::FaultKind::BudgetExceeded`] through the normal fault
14//! policy (`Panic` or `IsolateTask` — no new fault-handling path needed).
15//!
16//! Deadline/snapshot storage follows the exact pattern already established
17//! by `crate::timer`'s `PTASK_DEADLINES` (`UnsafeCell` array, `unsafe impl
18//! Sync`, every access under `critical::enter`) since RV32/ARMv7-M have no
19//! native 64-bit atomics.
20
21use core::cell::UnsafeCell;
22
23use crate::preempt::tcb::MAX_PTASKS;
24use crate::sync::atomic::{AtomicU32, Ordering};
25
26struct PeriodSlot {
27 /// Next period deadline, in microseconds since boot (0 = not yet
28 /// anchored — the first `wait_period()` call anchors it to `now`).
29 next_us: UnsafeCell<u64>,
30 /// `exec_time::busy_cycles(id)` snapshot taken at the start of the
31 /// current period, so budget checks compare *this period's* busy
32 /// time, not the task's lifetime total.
33 budget_start_cycles: UnsafeCell<u64>,
34}
35
36// SAFETY: every field access goes through `critical::enter` (interrupts
37// disabled, single-core), so there is no concurrent access.
38unsafe impl Sync for PeriodSlot {}
39
40#[cfg(not(loom))]
41static PERIOD_US: [AtomicU32; MAX_PTASKS] = [const { AtomicU32::new(0) }; MAX_PTASKS];
42#[cfg(loom)]
43loom::lazy_static! {
44 static ref PERIOD_US: [AtomicU32; MAX_PTASKS] = core::array::from_fn(|_| AtomicU32::new(0));
45}
46
47#[cfg(not(loom))]
48static BUDGET_US: [AtomicU32; MAX_PTASKS] = [const { AtomicU32::new(0) }; MAX_PTASKS];
49#[cfg(loom)]
50loom::lazy_static! {
51 static ref BUDGET_US: [AtomicU32; MAX_PTASKS] = core::array::from_fn(|_| AtomicU32::new(0));
52}
53static SLOTS: [PeriodSlot; MAX_PTASKS] = [const {
54 PeriodSlot {
55 next_us: UnsafeCell::new(0),
56 budget_start_cycles: UnsafeCell::new(0),
57 }
58}; MAX_PTASKS];
59
60/// Configure task `id`'s period (microseconds). `0` disables
61/// [`wait_period`] for that task (it returns immediately).
62pub fn set_period_us(id: usize, period_us: u32) {
63 if let Some(slot) = PERIOD_US.get(id) {
64 slot.store(period_us, Ordering::Release);
65 }
66}
67
68/// Configure task `id`'s per-period CPU budget (microseconds, estimated —
69/// see [`crate::exec_time::estimate_us_from_cycles`]). `0` disables budget
70/// enforcement for that task.
71pub fn set_budget_us(id: usize, budget_us: u32) {
72 if let Some(slot) = BUDGET_US.get(id) {
73 slot.store(budget_us, Ordering::Release);
74 }
75}
76
77pub fn period_us(id: usize) -> u32 {
78 PERIOD_US.get(id).map_or(0, |s| s.load(Ordering::Acquire))
79}
80
81pub fn budget_us(id: usize) -> u32 {
82 BUDGET_US.get(id).map_or(0, |s| s.load(Ordering::Acquire))
83}
84
85/// Block the calling preemptive task until its next period boundary.
86/// Drift-corrected: the deadline is `previous_deadline + period`, not
87/// `now + period`, so a task that occasionally runs a bit late never
88/// permanently shifts its schedule. No-op if the calling task has no
89/// period configured ([`set_period_us`] not called, or called with `0`)
90/// or isn't a preemptive task.
91pub fn wait_period() {
92 let Some(me) = crate::preempt::sched::current() else {
93 return;
94 };
95 let period = period_us(me) as u64;
96 if period == 0 {
97 return;
98 }
99 let Some(slot) = SLOTS.get(me) else {
100 return;
101 };
102 let next = crate::critical::enter(|| {
103 // SAFETY: guarded by `critical::enter` (see module docs).
104 unsafe {
105 let prev = *slot.next_us.get();
106 let next = if prev == 0 {
107 crate::port::board::now_us().wrapping_add(period)
108 } else {
109 prev.wrapping_add(period)
110 };
111 *slot.next_us.get() = next;
112 *slot.budget_start_cycles.get() = crate::exec_time::busy_cycles(me);
113 next
114 }
115 });
116 crate::preempt::sleep_until(next);
117}
118
119/// Called from [`crate::preempt::on_tick`] for the currently-running task:
120/// true if it has exceeded its configured budget within the current
121/// period. Always false if no budget is configured for `id`.
122pub(crate) fn check_budget(id: usize) -> bool {
123 let budget = budget_us(id);
124 if budget == 0 {
125 return false;
126 }
127 let Some(slot) = SLOTS.get(id) else {
128 return false;
129 };
130 // SAFETY: guarded by `critical::enter` (see module docs).
131 let start = crate::critical::enter(|| unsafe { *slot.budget_start_cycles.get() });
132 // `busy_cycles_live`, not `busy_cycles`: called from `on_tick` for the
133 // task that's still mid-dispatch right now (see its docs) — a task
134 // that never yields must still be checkable.
135 let used_cycles = crate::exec_time::busy_cycles_live(id).wrapping_sub(start);
136 let used_us = crate::exec_time::estimate_us_from_cycles(used_cycles);
137 used_us > budget as u64
138}
139
140/// Test-only: reset every period/budget slot. Part of the global reset
141/// done by [`crate::kernel_test!`].
142#[cfg(feature = "test-support")]
143pub(crate) fn reset_for_test() {
144 for s in PERIOD_US.iter() {
145 s.store(0, Ordering::Relaxed);
146 }
147 for s in BUDGET_US.iter() {
148 s.store(0, Ordering::Relaxed);
149 }
150 crate::critical::enter(|| {
151 for slot in SLOTS.iter() {
152 // SAFETY: guarded by `critical::enter` (see module docs).
153 unsafe {
154 *slot.next_us.get() = 0;
155 *slot.budget_start_cycles.get() = 0;
156 }
157 }
158 });
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn wait_period_noop_without_config() {
167 crate::kernel_test! {
168 // No current task, no period configured: must not panic.
169 wait_period();
170 }
171 }
172
173 #[test]
174 fn check_budget_false_without_config() {
175 crate::kernel_test! {
176 assert!(!check_budget(0));
177 }
178 }
179
180 #[test]
181 fn set_and_read_period_budget() {
182 crate::kernel_test! {
183 set_period_us(2, 5000);
184 set_budget_us(2, 1000);
185 assert_eq!(period_us(2), 5000);
186 assert_eq!(budget_us(2), 1000);
187 }
188 }
189}