nros_node/executor/scheduler.rs
1// Copyright (c) 2026, NEWSLab NTU.
2// SPDX-License-Identifier: Apache-2.0
3
4//! phase-509 P6 (RFC-0108 D3, D7; amended RFC-0107 step 4) -- the scheduler /
5//! worker split.
6//!
7//! RFC-0107's per-tier dispatcher was one value that both ORDERED ready work
8//! and was the unit of PREEMPTION. RFC-0108 splits it in two:
9//!
10//! - a per-core [`Scheduler`]: the ordered ready set over the core's callback
11//! slots, each slot's dispatch level, and the band table that says which
12//! worker owns which levels;
13//! - a per-level [`Worker`]: one contiguous [`Band`] of dispatch levels, its
14//! park (RFC-0106: the deadline sources, the primitive that blocks, and
15//! what the last park did) and its stack record (the headroom rule's
16//! bound and state). A worker is the thread that runs its band.
17//!
18//! **One level is today's executor.** [`Executor`](super::Executor) stays the
19//! one-level facade: it holds one [`Worker`] whose band is every level
20//! ([`Band::ALL`]), so its dispatch order, its park and its stack rule are the
21//! ones it always had. The worker's fields are the park and stack fields the
22//! executor used to hold inline, moved and not changed.
23//!
24//! **Preemption between bands is the kernel's** (RFC-0108 D6). Each worker
25//! runs at its band's kernel priority and picks only its own band, most
26//! urgent level first, registration order within a level. On one core a less
27//! urgent worker runs only while every more urgent one is parked, so an
28//! arrival in a more urgent band preempts a running callback of a less urgent
29//! one, and the members of one band never preempt each other: an arrival in
30//! the band of the running worker waits for its current callback to return.
31//!
32//! **Waking (D7).** An arrival the transport attributes to its slot
33//! (RFC-0084) wakes only the worker of that slot's band
34//! ([`Scheduler::arrive`]). An unattributed arrival can only say "something
35//! on this session", so it wakes the most urgent parked worker, which
36//! re-picks ([`Scheduler::arrive_unattributed`]).
37//!
38//! The scheduler's run-time half takes `&self` and is lock-free (one atomic
39//! word of ready bits, one parked flag per worker), so the workers of one core
40//! and the transport's arrival path may all reach it at once. Its set-up half
41//! ([`Scheduler::add_worker`], [`Scheduler::bind_slot`]) takes `&mut self`
42//! and runs before any worker starts.
43//!
44//! Crate-private until the level runners (phase-509 P9) open a scheduler and
45//! its levels through the FFI; until then the executor uses only its one
46//! [`Worker`], and the [`Scheduler`] is driven by its own tests.
47
48// The multi-level half has no in-crate caller before P9.
49#![cfg_attr(not(test), allow(dead_code))]
50
51use core::ffi::c_void;
52
53use portable_atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering::SeqCst};
54
55use super::spin::{MAX_WAKE_SOURCES, ParkUntilFn, WakeSourceId, WakeSourceSlot};
56
57const _: () = assert!(MAX_WAKE_SOURCES < 256, "Worker::wake_source_count is a u8");
58
59/// The least urgent dispatch level ([`Priority::dispatch_level`]: `Critical`
60/// 0, `Normal` 1 + rank, `BestEffort` 31).
61///
62/// [`Priority::dispatch_level`]: super::sched_context::Priority::dispatch_level
63pub const MAX_DISPATCH_LEVEL: u8 = 31;
64
65/// Callback slots one scheduler orders: one machine word of ready bits, as
66/// the entity wake's ready word (`entity_wake::SLOTS`).
67pub const SCHEDULER_SLOTS: usize = usize::BITS as usize;
68
69/// Workers (preemption levels) one scheduler can host: the
70/// `CONFIG_NROS_EXECUTOR_MAX_LEVELS` range's upper end.
71pub const MAX_WORKERS: usize = 32;
72
73/// A slot bound to no level yet.
74const UNBOUND: u8 = u8::MAX;
75
76/// A contiguous range of dispatch levels, inclusive at both ends; level 0 is
77/// the most urgent.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub struct Band {
80 /// The most urgent level in the band: its threshold (RFC-0108 D3).
81 pub most_urgent: u8,
82 /// The least urgent level in the band: its floor.
83 pub least_urgent: u8,
84}
85
86impl Band {
87 /// Every level: the one-level executor.
88 pub const ALL: Band = Band {
89 most_urgent: 0,
90 least_urgent: MAX_DISPATCH_LEVEL,
91 };
92
93 /// The band `[most_urgent, least_urgent]`, or `None` when it is empty or
94 /// reaches past the least urgent dispatch level (31).
95 pub const fn new(most_urgent: u8, least_urgent: u8) -> Option<Band> {
96 if most_urgent > least_urgent || least_urgent > MAX_DISPATCH_LEVEL {
97 None
98 } else {
99 Some(Band {
100 most_urgent,
101 least_urgent,
102 })
103 }
104 }
105
106 /// Whether `level` is in the band.
107 pub const fn contains(&self, level: u8) -> bool {
108 level >= self.most_urgent && level <= self.least_urgent
109 }
110}
111
112/// One preemption level: its band, its park and its stack record.
113///
114/// The park (RFC-0106: the platform deadline sources, the one primitive that
115/// blocks, and what the last park asked for and got) and the stack record
116/// (the headroom rule's declared bound and state, phase-436 E3/E4) moved here
117/// from [`Executor`](super::Executor) unchanged; the executor's park and
118/// stack methods read and write its worker's. The fields stay flat, not in a
119/// park and a stack sub-struct, so their small members share padding as they
120/// did inline in the executor.
121pub(crate) struct Worker {
122 /// The level's index in the image, 0 the most urgent (phase-509 P7):
123 /// what [`Executor::level`](super::Executor::level) answers and the
124 /// level its spins check in as.
125 pub(crate) level: u8,
126 /// The dispatch levels this worker runs.
127 pub(crate) band: Band,
128 // ---- the park (RFC-0106) ----
129 /// phase-436 -- the bound the last park was computed with, in us, and the
130 /// source that won it. See `WakeSourceId`.
131 pub(crate) last_park_bound_us: u64,
132 pub(crate) last_park_source: WakeSourceId,
133 /// phase-436 W2 -- the park bound after rounding UP to what the
134 /// platform's wake primitive can express. Kept beside the request rather
135 /// than replacing it: reporting both is what makes the rounding auditable
136 /// instead of silent (issue 1194).
137 pub(crate) last_park_achieved_us: u64,
138 /// phase-436 W6 -- registered platform deadline sources, and how many are
139 /// live. Inline table: no allocator on the targets this exists for.
140 pub(crate) wake_sources: [Option<WakeSourceSlot>; MAX_WAKE_SOURCES],
141 /// A `u8` (it was a `usize` inline in the executor) so it shares the
142 /// worker's small fields' padding: `MAX_WAKE_SOURCES` is far below 256.
143 pub(crate) wake_source_count: u8,
144 /// The single thing that actually blocks. Many sources say WHEN; one
145 /// primitive does the waiting.
146 pub(crate) park_primitive: Option<(ParkUntilFn, *mut c_void)>,
147 /// phase-521 W8 (c) (RFC-0113 D9) -- the last park answered
148 /// [`PARK_YIELDED`](super::spin::PARK_YIELDED) with no wake pending: the
149 /// park slot handed the wait to its framework's scheduler, so the pass
150 /// that ran the park returns without scanning. Cleared by that pass.
151 pub(crate) park_yielded: bool,
152 /// phase-436 W7.b (issue 1242) -- the finest park the INSTALLED primitive
153 /// can actually express, in microseconds. `0` = none installed, fall back
154 /// to the `wake_wait_ms` ABI floor.
155 pub(crate) park_granularity_declared_us: u64,
156 // ---- the stack record (phase-436 E3/E4) ----
157 /// Declared minimum stack headroom in bytes for the thread this worker
158 /// runs on; `0` (default) disables the rule. The entry that spawned the
159 /// thread is the one that knows what it gave it.
160 pub(crate) min_stack_headroom_bytes: usize,
161 /// Lowest headroom already reported, so the rule fires on new lows only.
162 /// `usize::MAX` = nothing reported yet.
163 pub(crate) stack_headroom_reported: usize,
164 /// phase-436 E3 -- a minimum is set but the port reports no stack data,
165 /// so the rule will skip every tick.
166 pub(crate) stack_headroom_blind: bool,
167 /// phase-436 E4 -- when the headroom query last ran (`None` = never; a
168 /// clockless build stores `Some(0)` to mean "has run"), spins since, and
169 /// how many queries actually reached the port.
170 pub(crate) stack_headroom_last_check_us: Option<u64>,
171 pub(crate) stack_headroom_spins_since_check: u32,
172 pub(crate) stack_headroom_checks: u32,
173 /// phase-509 P9b item 4 -- the level that owns callbacks registered now
174 /// (`Executor::set_registering_level`); [`NO_OWNING_LEVEL`] until a level
175 /// runner first sets one, which is every image's band-of-levels mode. An
176 /// executor-wide fact, kept here among the worker's small fields so it
177 /// shares their padding.
178 pub(crate) registering_level: u8,
179 /// Issue 2018 -- the scheduling context the callbacks registered now
180 /// bind to when no node or group binding names one: the registering
181 /// level's own (`Executor::set_level_sched_context`), else 0, the
182 /// default. Reset to 0 whenever the registering level changes.
183 pub(crate) registering_sched: u8,
184}
185
186/// phase-509 P9b item 4 -- no level owns slots by registration.
187pub(crate) const NO_OWNING_LEVEL: u8 = u8::MAX;
188
189impl Worker {
190 /// The one-level executor's worker: every dispatch level, nothing parked
191 /// or recorded yet.
192 pub(crate) const fn one_level() -> Self {
193 Self {
194 level: 0,
195 band: Band::ALL,
196 last_park_bound_us: 0,
197 last_park_source: WakeSourceId::CallerBudget,
198 last_park_achieved_us: 0,
199 wake_sources: [None; MAX_WAKE_SOURCES],
200 wake_source_count: 0,
201 park_primitive: None,
202 park_yielded: false,
203 park_granularity_declared_us: 0,
204 min_stack_headroom_bytes: 0,
205 stack_headroom_reported: usize::MAX,
206 stack_headroom_blind: false,
207 stack_headroom_last_check_us: None,
208 stack_headroom_spins_since_check: 0,
209 stack_headroom_checks: 0,
210 registering_level: NO_OWNING_LEVEL,
211 registering_sched: 0,
212 }
213 }
214}
215
216/// phase-509 P9b item 3 -- how a band worker waits on its own wake object:
217/// at most `us` microseconds, returning early when the object is signalled
218/// (`WorkerWakeFn`). The answer is ignored: either way the worker re-scans.
219pub type BandWaitFn = unsafe extern "C" fn(ctx: *mut c_void, us: u64) -> i32;
220
221/// phase-509 P9b item 3 (RFC-0108 D7, "As built in P9b") -- a band worker's
222/// wake object and the slots it answers for. The transport's arrival path
223/// reads it without the executor's lock, so it is `'static` and its fields
224/// are atomics or fixed at construction; it is linked into the executor's
225/// arrival routing by [`Executor::attach_band_worker`](super::Executor::attach_band_worker)
226/// and never unlinked.
227pub struct BandWaiter {
228 /// The band's slots, as the worker's last pass saw them. An arrival on
229 /// one of them wakes this worker only.
230 pub(crate) slots: AtomicUsize,
231 /// Signals the wake object (`k_sem_give`, a futex post).
232 #[cfg_attr(not(all(feature = "alloc", feature = "rmw-cffi")), allow(dead_code))]
233 // the arrival path
234 pub(crate) wake: WorkerWakeFn,
235 /// Waits on it, bounded.
236 pub(crate) wait: BandWaitFn,
237 pub(crate) ctx: *mut c_void,
238 /// The next waiter of the same executor.
239 #[cfg_attr(not(all(feature = "alloc", feature = "rmw-cffi")), allow(dead_code))]
240 // the arrival path
241 pub(crate) next: portable_atomic::AtomicPtr<BandWaiter>,
242}
243
244// SAFETY: `ctx` is the port's wake object, handed in with the two functions
245// that use it, callable from any thread (as `Scheduler::add_worker`'s wake);
246// everything else is atomic.
247unsafe impl Sync for BandWaiter {}
248// SAFETY: as above.
249unsafe impl Send for BandWaiter {}
250
251impl BandWaiter {
252 /// A waiter over the port's wake object at `ctx`.
253 ///
254 /// # Safety
255 /// `wake` and `wait` are callable from any thread with `ctx`, which
256 /// outlives every executor the waiter is attached to.
257 pub const unsafe fn new(wake: WorkerWakeFn, wait: BandWaitFn, ctx: *mut c_void) -> Self {
258 Self {
259 slots: AtomicUsize::new(0),
260 wake,
261 wait,
262 ctx,
263 next: portable_atomic::AtomicPtr::new(core::ptr::null_mut()),
264 }
265 }
266
267 /// Signal the worker.
268 #[cfg_attr(not(all(feature = "alloc", feature = "rmw-cffi")), allow(dead_code))] // the arrival path
269 pub(crate) fn signal(&self) {
270 // SAFETY: `new`'s contract.
271 unsafe { (self.wake)(self.ctx) };
272 }
273
274 /// Wait at most `us` microseconds, or until signalled.
275 pub(crate) fn wait_us(&self, us: u64) {
276 // SAFETY: `new`'s contract.
277 let _ = unsafe { (self.wait)(self.ctx, us) };
278 }
279}
280
281/// phase-509 P9b item 2 (RFC-0108 "As built in P9b") -- a worker of one band
282/// over an executor it shares with other workers: the record its own thread
283/// owns and hands to [`Executor::spin_band_once`](super::Executor::spin_band_once).
284///
285/// A band pass runs only the band's callbacks, and credits the band's timers
286/// with the time since this worker's own previous pass, so two workers never
287/// split one timer's elapsed time between them. With a [`BandWaiter`]
288/// (item 3) it parks on its own wake object until its band's next timer is
289/// due or an arrival in its band signals it.
290#[derive(Clone, Copy, Debug)]
291pub struct BandWorker {
292 /// The level's index in the image, 0 the most urgent: what its passes
293 /// check in as (`level_liveness`).
294 pub(crate) level: u8,
295 /// The dispatch levels this worker runs.
296 pub(crate) band: Band,
297 /// When this worker's previous pass read the clock; `None` before its
298 /// first.
299 pub(crate) last_spin_end_us: Option<u64>,
300 /// phase-509 P9b item 3 -- its wake object, if it parks.
301 pub(crate) waiter: Option<&'static BandWaiter>,
302}
303
304impl core::fmt::Debug for BandWaiter {
305 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
306 f.debug_struct("BandWaiter")
307 .field("slots", &self.slots.load(SeqCst))
308 .finish_non_exhaustive()
309 }
310}
311
312impl BandWorker {
313 /// The worker of `band` at image level `level`, polling (no park).
314 pub const fn new(level: u8, band: Band) -> Self {
315 Self {
316 level,
317 band,
318 last_spin_end_us: None,
319 waiter: None,
320 }
321 }
322
323 /// The same worker, parking on `waiter` (phase-509 P9b item 3). Attach
324 /// it to the executor ([`Executor::attach_band_worker`](super::Executor::attach_band_worker))
325 /// so its band's arrivals signal it.
326 pub const fn with_waiter(self, waiter: &'static BandWaiter) -> Self {
327 Self {
328 waiter: Some(waiter),
329 ..self
330 }
331 }
332
333 /// The band this worker runs.
334 pub const fn band(&self) -> Band {
335 self.band
336 }
337
338 /// Issue 2020 -- the image level this worker checks in as, 0 the most
339 /// urgent: whose components' ticks its passes run.
340 pub const fn level(&self) -> u8 {
341 self.level
342 }
343}
344
345/// How a scheduler signals a worker's wake object (`NodeWake` on a real
346/// port; a mock kernel in the tests).
347pub type WorkerWakeFn = unsafe extern "C" fn(ctx: *mut c_void);
348
349/// A worker's index in its scheduler, 0 the most urgent.
350#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
351pub struct WorkerId(pub u8);
352
353/// Why a scheduler refused a set-up step.
354#[derive(Clone, Copy, Debug, PartialEq, Eq)]
355pub enum LevelSetupError {
356 /// [`MAX_WORKERS`] workers are already added.
357 TooManyWorkers,
358 /// The band does not start right after the previous worker's (the first
359 /// at level 0): bands partition the levels, most urgent first.
360 NotContiguous { expected_most_urgent: u8, got: u8 },
361 /// The slot is past [`SCHEDULER_SLOTS`], or the level past
362 /// [`MAX_DISPATCH_LEVEL`].
363 OutOfRange,
364}
365
366struct Seat {
367 band: Band,
368 wake: Option<(WorkerWakeFn, *mut c_void)>,
369 parked: AtomicBool,
370}
371
372/// The per-core half: the ordered ready set and the band table.
373pub struct Scheduler {
374 /// Each slot's dispatch level; [`UNBOUND`] when no callback is bound.
375 slot_level: [AtomicU8; SCHEDULER_SLOTS],
376 /// One bit per slot with work queued.
377 ready: AtomicUsize,
378 seats: [Option<Seat>; MAX_WORKERS],
379 workers: u8,
380}
381
382// SAFETY: the raw wake contexts are handed in by the port together with the
383// function that reads them, and must be callable from any thread (the same
384// contract as a backend's wake callback). Everything else is atomic or set up
385// through `&mut self` before the workers start.
386unsafe impl Sync for Scheduler {}
387// SAFETY: as above; nothing in it is tied to the creating thread.
388unsafe impl Send for Scheduler {}
389
390impl Default for Scheduler {
391 fn default() -> Self {
392 Self::new()
393 }
394}
395
396impl Scheduler {
397 pub const fn new() -> Self {
398 Self {
399 slot_level: [const { AtomicU8::new(UNBOUND) }; SCHEDULER_SLOTS],
400 ready: AtomicUsize::new(0),
401 seats: [const { None }; MAX_WORKERS],
402 workers: 0,
403 }
404 }
405
406 /// Add the next worker, less urgent than every one added before it. Its
407 /// band must start right after the previous band's floor (the first at
408 /// level 0): the bands partition the levels and are contiguous (RFC-0108
409 /// D3). `wake` signals its wake object; `None` for a worker nothing has
410 /// to wake (a test, or a worker that polls).
411 pub fn add_worker(
412 &mut self,
413 band: Band,
414 wake: Option<(WorkerWakeFn, *mut c_void)>,
415 ) -> Result<WorkerId, LevelSetupError> {
416 let idx = self.workers as usize;
417 if idx >= MAX_WORKERS {
418 return Err(LevelSetupError::TooManyWorkers);
419 }
420 let expected = match idx {
421 0 => 0,
422 _ => match &self.seats[idx - 1] {
423 Some(prev) => prev.band.least_urgent + 1,
424 None => 0,
425 },
426 };
427 if band.most_urgent != expected {
428 return Err(LevelSetupError::NotContiguous {
429 expected_most_urgent: expected,
430 got: band.most_urgent,
431 });
432 }
433 self.seats[idx] = Some(Seat {
434 band,
435 wake,
436 parked: AtomicBool::new(false),
437 });
438 self.workers += 1;
439 Ok(WorkerId(idx as u8))
440 }
441
442 /// Workers added.
443 pub fn worker_count(&self) -> usize {
444 self.workers as usize
445 }
446
447 /// Bind callback `slot` to dispatch `level`.
448 pub fn bind_slot(&mut self, slot: usize, level: u8) -> Result<(), LevelSetupError> {
449 if slot >= SCHEDULER_SLOTS || level > MAX_DISPATCH_LEVEL {
450 return Err(LevelSetupError::OutOfRange);
451 }
452 self.slot_level[slot].store(level, SeqCst);
453 Ok(())
454 }
455
456 /// The worker whose band holds `level`; `None` when no band does.
457 pub fn worker_of(&self, level: u8) -> Option<WorkerId> {
458 self.seats[..self.workers as usize]
459 .iter()
460 .position(|s| s.as_ref().is_some_and(|s| s.band.contains(level)))
461 .map(|i| WorkerId(i as u8))
462 }
463
464 /// The band `worker` runs.
465 pub fn band(&self, worker: WorkerId) -> Option<Band> {
466 self.seat(worker).map(|s| s.band)
467 }
468
469 fn seat(&self, worker: WorkerId) -> Option<&Seat> {
470 self.seats
471 .get(worker.0 as usize)
472 .and_then(Option::as_ref)
473 .filter(|_| (worker.0) < self.workers)
474 }
475
476 fn wake(&self, worker: WorkerId) {
477 if let Some(seat) = self.seat(worker)
478 && let Some((wake, ctx)) = seat.wake
479 {
480 // SAFETY: the port handed `wake` and `ctx` in together, callable
481 // from any thread (see `add_worker`).
482 unsafe { wake(ctx) };
483 }
484 }
485
486 /// RFC-0108 D7: an arrival attributed to `slot` queues it and wakes only
487 /// the worker of its band. Returns that worker; `None` when the slot is
488 /// bound to no level a band holds, which is then woken as an
489 /// unattributed arrival would be.
490 pub fn arrive(&self, slot: usize) -> Option<WorkerId> {
491 let Some(level) = self.slot_level.get(slot).map(|l| l.load(SeqCst)) else {
492 return self.arrive_unattributed();
493 };
494 // The bit first, so a worker the wake reaches sees it.
495 self.ready.fetch_or(1usize << slot, SeqCst);
496 match self.worker_of(level) {
497 Some(w) => {
498 self.wake(w);
499 Some(w)
500 }
501 None => self.arrive_unattributed(),
502 }
503 }
504
505 /// RFC-0108 D7: an arrival nothing attributes wakes the most urgent
506 /// parked worker, which re-picks. `None` when no worker is parked: a
507 /// running worker re-picks after its current callback anyway.
508 pub fn arrive_unattributed(&self) -> Option<WorkerId> {
509 let w = self.seats[..self.workers as usize]
510 .iter()
511 .position(|s| s.as_ref().is_some_and(|s| s.parked.load(SeqCst)))
512 .map(|i| WorkerId(i as u8))?;
513 self.wake(w);
514 Some(w)
515 }
516
517 /// Record that `worker` is parked (`true`) or running (`false`).
518 pub fn set_parked(&self, worker: WorkerId, parked: bool) {
519 if let Some(seat) = self.seat(worker) {
520 seat.parked.store(parked, SeqCst);
521 }
522 }
523
524 /// Whether `worker` is parked.
525 pub fn is_parked(&self, worker: WorkerId) -> bool {
526 self.seat(worker).is_some_and(|s| s.parked.load(SeqCst))
527 }
528
529 /// Whether any slot in `worker`'s band has work queued.
530 pub fn has_work(&self, worker: WorkerId) -> bool {
531 let Some(band) = self.band(worker) else {
532 return false;
533 };
534 let ready = self.ready.load(SeqCst);
535 (0..SCHEDULER_SLOTS)
536 .any(|s| ready & (1usize << s) != 0 && band.contains(self.slot_level[s].load(SeqCst)))
537 }
538
539 /// Take the most urgent queued slot of `worker`'s band: the lowest
540 /// dispatch level, registration (slot) order within a level. `None`
541 /// when its band has nothing queued.
542 pub fn pick(&self, worker: WorkerId) -> Option<usize> {
543 let band = self.band(worker)?;
544 loop {
545 let ready = self.ready.load(SeqCst);
546 let mut best: Option<(u8, usize)> = None;
547 let mut bits = ready;
548 while bits != 0 {
549 let s = bits.trailing_zeros() as usize;
550 bits &= bits - 1;
551 let level = self.slot_level[s].load(SeqCst);
552 if band.contains(level) && best.is_none_or(|(l, _)| level < l) {
553 best = Some((level, s));
554 }
555 }
556 let (_, slot) = best?;
557 let bit = 1usize << slot;
558 if self
559 .ready
560 .compare_exchange(ready, ready & !bit, SeqCst, SeqCst)
561 .is_ok()
562 {
563 return Some(slot);
564 }
565 }
566 }
567
568 /// Run `worker`'s band until it has nothing queued: pick, dispatch, pick
569 /// again from the ready set as it is now (RFC-0108 D2). Returns how many
570 /// callbacks ran.
571 pub fn run(&self, worker: WorkerId, mut dispatch: impl FnMut(usize)) -> usize {
572 let mut ran = 0;
573 while let Some(slot) = self.pick(worker) {
574 dispatch(slot);
575 ran += 1;
576 }
577 ran
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use std::{boxed::Box, cell::RefCell, format, string::String, vec, vec::Vec};
585
586 /// A worker index carried as a wake context.
587 fn ctx(i: usize) -> *mut c_void {
588 core::ptr::without_provenance_mut(i)
589 }
590
591 #[test]
592 fn bands_partition_the_levels_most_urgent_first() {
593 let mut s = Scheduler::new();
594 assert_eq!(
595 s.add_worker(Band::new(1, 3).unwrap(), None),
596 Err(LevelSetupError::NotContiguous {
597 expected_most_urgent: 0,
598 got: 1
599 })
600 );
601 assert_eq!(
602 s.add_worker(Band::new(0, 3).unwrap(), None),
603 Ok(WorkerId(0))
604 );
605 assert!(matches!(
606 s.add_worker(Band::new(5, 9).unwrap(), None),
607 Err(LevelSetupError::NotContiguous { .. })
608 ));
609 assert_eq!(
610 s.add_worker(Band::new(4, 31).unwrap(), None),
611 Ok(WorkerId(1))
612 );
613 assert_eq!(s.worker_count(), 2);
614 assert_eq!(s.worker_of(2), Some(WorkerId(0)));
615 assert_eq!(s.worker_of(4), Some(WorkerId(1)));
616 assert_eq!(Band::new(3, 2), None);
617 assert_eq!(Band::new(0, 32), None);
618 }
619
620 /// One band is today's order: most urgent level first, registration
621 /// order within a level.
622 #[test]
623 fn one_band_picks_by_level_then_registration() {
624 let mut s = Scheduler::new();
625 s.add_worker(Band::ALL, None).unwrap();
626 for (slot, level) in [(0, 3), (1, 1), (2, 3), (3, 1)] {
627 s.bind_slot(slot, level).unwrap();
628 }
629 for slot in [2, 0, 3, 1] {
630 s.arrive(slot);
631 }
632 let mut order = vec![];
633 s.run(WorkerId(0), |slot| order.push(slot));
634 assert_eq!(order, [1, 3, 0, 2]);
635 }
636
637 /// D7: an attributed arrival wakes only its band's worker; an
638 /// unattributed one the most urgent parked worker.
639 #[test]
640 fn an_arrival_wakes_only_its_bands_worker() {
641 static WOKEN: [AtomicU8; 2] = [AtomicU8::new(0), AtomicU8::new(0)];
642 unsafe extern "C" fn wake(ctx: *mut c_void) {
643 WOKEN[ctx as usize].fetch_add(1, SeqCst);
644 }
645 let mut s = Scheduler::new();
646 s.add_worker(Band::new(0, 1).unwrap(), Some((wake, ctx(0))))
647 .unwrap();
648 s.add_worker(Band::new(2, 31).unwrap(), Some((wake, ctx(1))))
649 .unwrap();
650 s.bind_slot(0, 1).unwrap();
651 s.bind_slot(1, 5).unwrap();
652 assert_eq!(s.arrive(1), Some(WorkerId(1)));
653 assert_eq!(
654 (WOKEN[0].load(SeqCst), WOKEN[1].load(SeqCst)),
655 (0, 1),
656 "a band-1 arrival does not wake band 0"
657 );
658 assert_eq!(s.arrive(0), Some(WorkerId(0)));
659 assert_eq!((WOKEN[0].load(SeqCst), WOKEN[1].load(SeqCst)), (1, 1));
660 // Nobody parked: an unattributed arrival wakes nobody (each running
661 // worker re-picks after its callback).
662 assert_eq!(s.arrive_unattributed(), None);
663 s.set_parked(WorkerId(1), true);
664 assert!(s.is_parked(WorkerId(1)) && !s.is_parked(WorkerId(0)));
665 assert_eq!(s.arrive_unattributed(), Some(WorkerId(1)));
666 s.set_parked(WorkerId(0), true);
667 assert_eq!(s.arrive_unattributed(), Some(WorkerId(0)));
668 // Each worker picks only its own band.
669 assert_eq!(s.pick(WorkerId(0)), Some(0));
670 assert_eq!(s.pick(WorkerId(0)), None);
671 assert!(s.has_work(WorkerId(1)));
672 assert_eq!(s.pick(WorkerId(1)), Some(1));
673 }
674
675 // ---- a deterministic one-core kernel -------------------------------
676 //
677 // Fixed-priority preemptive, one core, no time: a woken worker more
678 // urgent than the one running runs at once (nested inside the running
679 // callback, which is where a real kernel would preempt it); any other
680 // wake is pending until the running worker returns. Each callback is a
681 // script of arrivals it raises mid-run.
682
683 struct Kernel {
684 sched: &'static Scheduler,
685 running: Vec<WorkerId>,
686 pending: Vec<WorkerId>,
687 scripts: Vec<Vec<usize>>,
688 trace: Vec<String>,
689 }
690
691 std::thread_local! {
692 static KERNEL: RefCell<Option<Kernel>> = const { RefCell::new(None) };
693 }
694
695 fn with<R>(f: impl FnOnce(&mut Kernel) -> R) -> R {
696 KERNEL.with(|k| f(k.borrow_mut().as_mut().expect("kernel installed")))
697 }
698
699 unsafe extern "C" fn kernel_wake(ctx: *mut c_void) {
700 let w = WorkerId(ctx as usize as u8);
701 let preempts = with(|k| {
702 let current = k.running.last().copied();
703 if current.is_none_or(|c| w < c) {
704 true
705 } else {
706 if !k.pending.contains(&w) && current != Some(w) {
707 k.pending.push(w);
708 }
709 false
710 }
711 });
712 if preempts {
713 run_worker(w);
714 }
715 }
716
717 fn run_worker(w: WorkerId) {
718 let sched = with(|k| {
719 k.running.push(w);
720 k.sched
721 });
722 sched.set_parked(w, false);
723 sched.run(w, |slot| {
724 let script = with(|k| {
725 k.trace.push(format!("start {slot}"));
726 k.scripts[slot].clone()
727 });
728 for raised in script {
729 sched.arrive(raised);
730 }
731 with(|k| k.trace.push(format!("end {slot}")));
732 });
733 sched.set_parked(w, true);
734 with(|k| k.running.pop());
735 // The kernel's next pick: the most urgent pending worker.
736 loop {
737 let next = with(|k| {
738 k.pending.sort();
739 if k.pending.is_empty() {
740 None
741 } else {
742 Some(k.pending.remove(0))
743 }
744 });
745 match next {
746 Some(n) => run_worker(n),
747 None => break,
748 }
749 }
750 }
751
752 /// Band 0 (levels 0-1) holds `H`; band 1 (levels 2-31) holds `A` and
753 /// `B`, `B` the more urgent of the two. Slots: H 0, A 1, B 2.
754 fn two_bands(scripts: Vec<Vec<usize>>) -> &'static Scheduler {
755 let mut s = Scheduler::new();
756 s.add_worker(Band::new(0, 1).unwrap(), Some((kernel_wake, ctx(0))))
757 .unwrap();
758 s.add_worker(Band::new(2, 31).unwrap(), Some((kernel_wake, ctx(1))))
759 .unwrap();
760 s.bind_slot(0, 1).unwrap(); // H
761 s.bind_slot(1, 6).unwrap(); // A
762 s.bind_slot(2, 4).unwrap(); // B
763 let s: &'static Scheduler = Box::leak(Box::new(s));
764 s.set_parked(WorkerId(0), true);
765 s.set_parked(WorkerId(1), true);
766 KERNEL.with(|k| {
767 *k.borrow_mut() = Some(Kernel {
768 sched: s,
769 running: vec![],
770 pending: vec![],
771 scripts,
772 trace: vec![],
773 })
774 });
775 s
776 }
777
778 fn trace() -> Vec<String> {
779 with(|k| k.trace.clone())
780 }
781
782 /// RFC-0108 P6 acceptance: an arrival in the more urgent band preempts a
783 /// running callback of the less urgent band.
784 #[test]
785 fn a_more_urgent_bands_arrival_preempts_a_running_callback() {
786 // A raises H mid-run.
787 let s = two_bands(vec![vec![], vec![0], vec![]]);
788 s.arrive(1);
789 assert_eq!(trace(), ["start 1", "start 0", "end 0", "end 1"]);
790 }
791
792 /// RFC-0108 P6 acceptance: members of one band never preempt each other.
793 /// `B` is more urgent than `A` but in its band, so it waits for `A`.
794 #[test]
795 fn members_of_one_band_never_preempt_each_other() {
796 // A raises B mid-run.
797 let s = two_bands(vec![vec![], vec![2], vec![]]);
798 s.arrive(1);
799 assert_eq!(trace(), ["start 1", "end 1", "start 2", "end 2"]);
800 }
801
802 /// A less urgent band's arrival raised by the more urgent band waits
803 /// for it, and the band's own order holds when both of its members are
804 /// queued: `B` before `A`.
805 #[test]
806 fn a_less_urgent_arrival_waits_and_the_band_keeps_its_order() {
807 // H raises A and B mid-run.
808 let s = two_bands(vec![vec![1, 2], vec![], vec![]]);
809 s.arrive(0);
810 assert_eq!(
811 trace(),
812 ["start 0", "end 0", "start 2", "end 2", "start 1", "end 1"]
813 );
814 }
815}