Skip to main content

rivet/
exec_time.rs

1//! Per-task execution-time accounting (plan.md Phase 10).
2//!
3//! Built on the Group A cycle counter ([`crate::port::arch::cycle_count`]):
4//! at every *actual* context switch, the cycles since the outgoing task's
5//! last dispatch are added to its running total. No accounting happens on
6//! no-switch ticks (the common case) — this only costs a `cycle_count()`
7//! read plus one add, and only when a switch was already going to happen
8//! anyway.
9//!
10//! RV32 and ARMv7-M both lack native 64-bit atomics (no `AtomicU64` in
11//! `core` for either target — the same gap `rivet-arch-riscv::clint`
12//! documents for `MTIME_HZ`/`TICK_PERIOD`), so the cycle totals below are
13//! plain `static mut u64`s rather than atomics. [`on_switch`] is only ever
14//! called from [`crate::preempt::on_tick`], itself only reached from trap/
15//! exception context — structurally single-writer, since a hart can't take
16//! a second tick trap while already inside one. The one remaining hazard
17//! is a task-context reader (`report()`) observing a torn write if a tick
18//! lands mid-read; every read and write below is wrapped in
19//! [`crate::critical::enter`] to rule that out (a harmless no-op nesting
20//! when called from within `on_tick`, which already runs with interrupts
21//! effectively masked).
22//!
23//! Single global "last dispatch" stamp, matching the kernel's current
24//! single-`CURRENT`-task model (plan.md Phase 19 upgrades both together
25//! for SMP).
26
27use crate::preempt::tcb::MAX_PTASKS;
28use core::sync::atomic::{AtomicBool, Ordering};
29
30static mut BUSY_CYCLES: [u64; MAX_PTASKS] = [0; MAX_PTASKS];
31static mut LAST_DISPATCH: u64 = 0;
32static mut BOOT_CYCLE: u64 = 0;
33static mut WALLCLOCK_BOOT_US: u64 = 0;
34static STARTED: AtomicBool = AtomicBool::new(false);
35
36/// Record the very first dispatch (called once, from [`crate::preempt::start`]).
37pub fn on_first_dispatch() {
38    crate::critical::enter(|| {
39        let now = crate::port::arch::cycle_count();
40        let now_us = crate::port::board::now_us();
41        // SAFETY: guarded by `critical::enter` (see module docs).
42        unsafe {
43            BOOT_CYCLE = now;
44            LAST_DISPATCH = now;
45            WALLCLOCK_BOOT_US = now_us;
46        }
47        STARTED.store(true, Ordering::Release);
48    });
49}
50
51/// Record an actual context switch away from `outgoing` (its id in
52/// `preempt::tcb::TASKS`), crediting it with the cycles since the last
53/// dispatch and resetting the stamp for whichever task runs next. No-op
54/// if accounting hasn't started yet ([`on_first_dispatch`] not called).
55pub fn on_switch(outgoing: usize) {
56    if !STARTED.load(Ordering::Acquire) {
57        return;
58    }
59    crate::critical::enter(|| {
60        let now = crate::port::arch::cycle_count();
61        // SAFETY: guarded by `critical::enter` (see module docs).
62        unsafe {
63            let elapsed = now.wrapping_sub(LAST_DISPATCH);
64            LAST_DISPATCH = now;
65            if outgoing < MAX_PTASKS {
66                BUSY_CYCLES[outgoing] = BUSY_CYCLES[outgoing].wrapping_add(elapsed);
67            }
68        }
69    });
70}
71
72/// Total cycles task `id` has spent running, since boot, **as of its last
73/// completed dispatch** — a task that is still running right now (never
74/// yet switched away from) is not included until its next switch. Budget
75/// enforcement needs the up-to-the-moment figure for whichever task is
76/// *currently* running; see [`busy_cycles_live`].
77pub fn busy_cycles(id: usize) -> u64 {
78    if id >= MAX_PTASKS {
79        return 0;
80    }
81    // SAFETY: guarded by `critical::enter` (see module docs).
82    crate::critical::enter(|| unsafe { BUSY_CYCLES[id] })
83}
84
85/// [`busy_cycles`] plus the in-progress dispatch's elapsed cycles, if `id`
86/// is the currently-running task (checked by the caller — this function
87/// just adds "cycles since the single global `LAST_DISPATCH` stamp",
88/// which is only meaningful for whoever is actually running right now).
89/// Needed by [`crate::deadlines::check_budget`]: a task that never yields
90/// would otherwise never accumulate any *completed*-dispatch cycles at
91/// all, and its budget would never be checked.
92pub fn busy_cycles_live(id: usize) -> u64 {
93    let completed = busy_cycles(id);
94    let in_progress = crate::critical::enter(|| {
95        let now = crate::port::arch::cycle_count();
96        // SAFETY: guarded by `critical::enter` (see module docs).
97        unsafe { now.wrapping_sub(LAST_DISPATCH) }
98    });
99    completed.wrapping_add(in_progress)
100}
101
102/// Cycles elapsed since the scheduler's first dispatch (the denominator
103/// for a `%busy` figure). Zero if the preemptive tier hasn't started.
104pub fn cycles_since_boot() -> u64 {
105    if !STARTED.load(Ordering::Acquire) {
106        return 0;
107    }
108    crate::critical::enter(|| {
109        let now = crate::port::arch::cycle_count();
110        // SAFETY: guarded by `critical::enter` (see module docs).
111        unsafe { now.wrapping_sub(BOOT_CYCLE) }
112    })
113}
114
115/// Wall-clock microseconds elapsed since the scheduler's first dispatch.
116/// Zero if the preemptive tier hasn't started.
117pub fn wallclock_us_since_boot() -> u64 {
118    if !STARTED.load(Ordering::Acquire) {
119        return 0;
120    }
121    crate::critical::enter(|| {
122        let now_us = crate::port::board::now_us();
123        // SAFETY: guarded by `critical::enter` (see module docs).
124        unsafe { now_us.wrapping_sub(WALLCLOCK_BOOT_US) }
125    })
126}
127
128/// Convert a cycle count into an estimated microsecond duration, using the
129/// aggregate cycles-per-microsecond rate measured since boot
130/// (`cycles_since_boot() / wallclock_us_since_boot()`) — no board-declared
131/// clock-rate constant needed, and self-calibrating against whatever the
132/// cycle source actually is (real `mcycle`/DWT counting, or the
133/// microsecond-resolution SysTick fallback, in which case this is
134/// trivially exact). Used by [`crate::deadlines`]'s budget enforcement to
135/// turn a `budget_us` into a comparison against [`busy_cycles`] without
136/// needing a `__rivet_arch_cycle_count` call-rate contract beyond
137/// "monotonic". Zero before the preemptive tier starts, or if no time has
138/// passed yet (avoids a divide-by-zero).
139pub fn estimate_us_from_cycles(cycles: u64) -> u64 {
140    let total_cycles = cycles_since_boot();
141    if total_cycles == 0 {
142        return 0;
143    }
144    let total_us = wallclock_us_since_boot();
145    cycles.saturating_mul(total_us) / total_cycles
146}
147
148/// Integer percentage (0-100) of `cycles_since_boot()` that task `id` has
149/// spent running. `0` before the preemptive tier starts or if the task
150/// hasn't been dispatched yet.
151pub fn busy_percent(id: usize) -> u8 {
152    let total = cycles_since_boot();
153    if total == 0 {
154        return 0;
155    }
156    let busy = busy_cycles(id);
157    (busy.saturating_mul(100) / total).min(100) as u8
158}
159
160#[cfg(feature = "test-support")]
161pub(crate) fn reset_for_test() {
162    crate::critical::enter(|| {
163        // SAFETY: guarded by `critical::enter` (see module docs); test-only
164        // reset runs under `kernel_test!`'s serialization lock too.
165        unsafe {
166            // Raw-pointer writes (not `.iter_mut()`) so this never forms a
167            // `&mut` over the whole static, which `static_mut_refs` (2024
168            // edition lint) flags even though there is no concurrent
169            // access here (guarded by `critical::enter` + the test-only
170            // caller already holding `kernel_test!`'s serialization lock).
171            let base = core::ptr::addr_of_mut!(BUSY_CYCLES) as *mut u64;
172            for i in 0..MAX_PTASKS {
173                base.add(i).write(0);
174            }
175            LAST_DISPATCH = 0;
176            BOOT_CYCLE = 0;
177            WALLCLOCK_BOOT_US = 0;
178        }
179    });
180    STARTED.store(false, Ordering::Relaxed);
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn busy_percent_zero_before_start() {
189        crate::kernel_test! {
190            assert_eq!(busy_percent(0), 0);
191        }
192    }
193
194    #[test]
195    fn accounts_switch_time() {
196        crate::kernel_test! {
197            on_first_dispatch();
198            // Simulate some running time on task 0, then a switch to task 1.
199            for _ in 0..10 {
200                crate::port::arch::cycle_count();
201            }
202            on_switch(0);
203            assert!(busy_cycles(0) > 0);
204            assert_eq!(busy_cycles(1), 0);
205        }
206    }
207}