Skip to main content

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}