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}