rivet/executor.rs
1//! Priority-aware async executor with single-stack task polling.
2//!
3//! All cooperative tasks share one system stack. The executor polls tasks
4//! in priority order. When a task returns `Poll::Pending`, its state lives
5//! in a static `TaskCell` referenced by the task's `TaskReg.user_data` —
6//! no per-task stack allocation is needed.
7//!
8//! When all tasks are pending, the executor calls `port::arch::idle()` to enter
9//! a low-power state.
10
11use core::task::Poll;
12
13use crate::task::{iter_task_regs, TaskReg, TaskRegistry};
14use crate::waker;
15
16/// Identity of the task currently being polled, encoded as
17/// `(priority << 8) | index`. `u32::MAX` means "not currently polling a task".
18/// Set by the executor immediately before calling a task's `poll_fn` and
19/// read by sync primitives (`Semaphore`, `Channel`, `Sleep`) so they know
20/// which task to register as a waiter, without needing the user to pass
21/// priority/index manually.
22#[cfg(not(loom))]
23static CURRENT_TASK: crate::sync::atomic::AtomicU32 = crate::sync::atomic::AtomicU32::new(u32::MAX);
24#[cfg(loom)]
25loom::lazy_static! {
26 static ref CURRENT_TASK: crate::sync::atomic::AtomicU32 = crate::sync::atomic::AtomicU32::new(u32::MAX);
27}
28
29fn set_current(id: crate::task::TaskId) {
30 CURRENT_TASK.store(id.as_u16() as u32, crate::sync::atomic::Ordering::Release);
31}
32
33fn clear_current() {
34 CURRENT_TASK.store(u32::MAX, crate::sync::atomic::Ordering::Release);
35}
36
37/// Test-only: simulate being inside a task's poll, so `Semaphore::acquire()`,
38/// `Channel::send()/recv()`, and `Sleep` can be exercised directly from host
39/// tests without spinning up the full executor loop.
40#[doc(hidden)]
41pub fn set_current_for_test(priority: u8, index: u8) {
42 set_current(crate::task::TaskId::new(priority, index));
43}
44
45/// Test-only: clear the simulated task context set by [`set_current_for_test`].
46#[doc(hidden)]
47pub fn clear_current_for_test() {
48 clear_current();
49}
50
51/// The identity of the task currently being polled. `None` if called
52/// outside of a task's poll (e.g. from an ISR or before the executor starts).
53///
54/// Used by `Semaphore::acquire()`, `Channel::send()/recv()`, and `Sleep`
55/// to register themselves as waiters without requiring the caller to pass
56/// `(priority, index)` explicitly.
57pub fn current_task() -> Option<crate::task::TaskId> {
58 let v = CURRENT_TASK.load(crate::sync::atomic::Ordering::Acquire);
59 if v == u32::MAX {
60 None
61 } else {
62 Some(crate::task::TaskId::from_u16(v as u16))
63 }
64}
65
66/// Number of tasks not yet completed (plan.md [B10]): decremented the
67/// first time a task's poll returns `Ready`. Used to skip re-polling
68/// completed tasks and to know when the cooperative tier is idle.
69///
70/// A standalone static, not an `Executor` field: `EXECUTOR` below is a
71/// `static mut` with a `const fn` initializer (required — it has no other
72/// construction point), and loom's atomics are not const-constructible
73/// (same reason `Semaphore`/`Channel`/`Signal` split their `new()` by
74/// `#[cfg(loom)]`). Keeping this counter out of `Executor` lets
75/// `Executor::new()` stay `const` unconditionally instead of needing that
76/// same split plus a non-`static mut`-compatible loom variant of
77/// `EXECUTOR` itself.
78#[cfg(not(loom))]
79static LIVE_TASKS: crate::sync::atomic::AtomicUsize = crate::sync::atomic::AtomicUsize::new(0);
80#[cfg(loom)]
81loom::lazy_static! {
82 static ref LIVE_TASKS: crate::sync::atomic::AtomicUsize = crate::sync::atomic::AtomicUsize::new(0);
83}
84
85/// The global executor singleton.
86pub struct Executor {
87 registry: TaskRegistry,
88}
89
90impl Executor {
91 pub const fn new() -> Self {
92 Self {
93 registry: TaskRegistry::new(),
94 }
95 }
96
97 /// Discover tasks from the `.rivet_tasks` linker section.
98 /// Assigns per-priority indices. Must be called once before `run()`.
99 pub fn init(&mut self) {
100 let mut counts: [u8; 32] = [0; 32];
101
102 for reg in iter_task_regs() {
103 let prio = reg.priority as usize;
104 if prio > (crate::task::MAX_PRIORITY as usize) {
105 panic!(
106 "rivet: task priority {} exceeds MAX_PRIORITY {} \
107 (check #[rivet::task(priority = ...)])",
108 reg.priority,
109 crate::task::MAX_PRIORITY
110 );
111 }
112 let idx = counts[prio] as usize;
113 if idx >= crate::task::MAX_TASKS {
114 // plan.md [B12]: overflow must be loud, not a silent drop.
115 panic!(
116 "rivet: too many #[rivet::task]s at priority {} \
117 (limit MAX_TASKS = {} per priority)",
118 reg.priority,
119 crate::task::MAX_TASKS
120 );
121 }
122
123 self.registry.tasks[prio][idx] = Some(reg as *const TaskReg);
124 counts[prio] += 1;
125 self.registry.total += 1;
126 LIVE_TASKS.fetch_add(1, crate::sync::atomic::Ordering::Relaxed);
127 }
128
129 self.registry.count_per_priority[..=(crate::task::MAX_PRIORITY as usize)]
130 .copy_from_slice(&counts[..=(crate::task::MAX_PRIORITY as usize)]);
131
132 // Mark all tasks as initially ready so the executor polls them once
133 // to start their async state machines running.
134 for (p, &count) in counts[..=(crate::task::MAX_PRIORITY as usize)]
135 .iter()
136 .enumerate()
137 {
138 for i in 0..(count as usize) {
139 crate::waker::mark_ready(crate::task::TaskId::new(p as u8, i as u8));
140 }
141 }
142 }
143
144 /// Main executor loop. Never returns.
145 pub fn run(&self) -> ! {
146 loop {
147 waker::clear_pend();
148
149 // Poll all ready tasks, highest priority first.
150 while let Some(id) = waker::next_ready() {
151 let reg = match self.lookup_task(id.priority(), id.index()) {
152 Some(r) => r,
153 None => continue,
154 };
155
156 // Skip completed tasks woken by a stale registration
157 // (plan.md [B10]).
158 // SAFETY: `reg.completed_fn` was paired with this task's
159 // `TaskCell` by `#[rivet::task]`/`register_task!`.
160 unsafe {
161 if (reg.completed_fn)(reg.user_data) {
162 continue;
163 }
164 }
165
166 let task_waker = waker::task_waker(id);
167
168 set_current(id);
169 // SAFETY: `reg.poll_fn` is a type-erased poll function
170 // paired with `reg.user_data` (a `TaskCell` pointer) at
171 // registration time by `#[rivet::task]`/`register_task!`.
172 // The executor only ever polls a task that was registered
173 // with a matching (poll_fn, user_data) pair, and never
174 // re-enters a task while it's being polled.
175 let result = unsafe { (reg.poll_fn)(reg.user_data, &task_waker) };
176 clear_current();
177
178 if result == Poll::Ready(()) {
179 LIVE_TASKS.fetch_sub(1, crate::sync::atomic::Ordering::Relaxed);
180 }
181 }
182
183 // All tasks pending — sleep until an interrupt wakes us.
184 if !waker::has_pending() {
185 crate::port::arch::idle();
186 }
187 }
188 }
189
190 fn lookup_task(&self, priority: u8, index: u8) -> Option<&'static TaskReg> {
191 let p = priority as usize;
192 let i = index as usize;
193 if p > (crate::task::MAX_PRIORITY as usize) || i >= crate::task::MAX_TASKS {
194 return None;
195 }
196 // SAFETY: the pointer came from the linker-section walk in `init()`
197 // and points at a `static TaskReg` that lives for the program's
198 // lifetime; the registry is written once at boot and only read
199 // afterwards.
200 self.registry.tasks[p][i].map(|ptr| unsafe { &*ptr })
201 }
202}
203
204/// Test-only: reset the simulated-task-context global. Part of the global
205/// reset done by [`crate::kernel_test!`].
206#[cfg(feature = "test-support")]
207pub(crate) fn reset_for_test() {
208 clear_current();
209}
210
211impl Default for Executor {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217/// Global executor singleton.
218pub static mut EXECUTOR: Executor = Executor::new();