Skip to main content

ax_task/thread/
tick_work.rs

1//! Scheduler-tick-gated extension work executed in ordinary task context.
2
3use alloc::sync::Arc;
4use core::sync::atomic::{AtomicU64, Ordering};
5
6use super::ThreadId;
7
8/// Execution mode sampled by the periodic scheduler tick.
9///
10/// This is the OS-independent equivalent of Linux's `user_mode(regs)` result.
11/// The IRQ entry passes the saved-context classification into the scheduler
12/// tick so syscall boundaries do not need to publish a mirrored mode.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum SchedulerTickMode {
15    /// The thread is executing userspace.
16    User,
17    /// The thread is executing kernel code.
18    System,
19}
20
21/// IRQ-safe tick-sampled user/system CPU time for one scheduler thread.
22///
23/// The object is retained by both the OS task and [`super::ThreadExtension`].
24/// Only the current CPU samples it from the periodic tick's saved trap origin.
25#[derive(Debug)]
26pub struct SchedulerTickCpuTime {
27    user_ns: AtomicU64,
28    system_ns: AtomicU64,
29    realtime: Option<RealtimeTickAccounting>,
30}
31
32#[derive(Debug)]
33struct RealtimeTickAccounting {
34    gate: Arc<SchedulerTickGate>,
35    period_ns: AtomicU64,
36    last_period: AtomicU64,
37    ticks: AtomicU64,
38}
39
40impl SchedulerTickCpuTime {
41    /// Creates an empty accounting stream.
42    pub const fn new() -> Self {
43        Self {
44            user_ns: AtomicU64::new(0),
45            system_ns: AtomicU64::new(0),
46            realtime: None,
47        }
48    }
49
50    /// Creates tick accounting with optional continuous real-time accounting.
51    ///
52    /// The OS owns the shared gate. While enabled, actual FIFO/RR class ticks
53    /// count at most once per common monotonic-clock period, including across
54    /// migration. A real-time wake or PI deboost to Fair resets the count but
55    /// preserves deduplication.
56    /// Disabling the gate preserves previously accumulated ticks.
57    pub fn with_realtime_gate(gate: Arc<SchedulerTickGate>) -> Self {
58        Self {
59            realtime: Some(RealtimeTickAccounting {
60                gate,
61                period_ns: AtomicU64::new(0),
62                last_period: AtomicU64::new(u64::MAX),
63                ticks: AtomicU64::new(0),
64            }),
65            ..Self::new()
66        }
67    }
68
69    /// Returns continuous real-time ticks and their fixed period in nanoseconds.
70    ///
71    /// Returns `None` until the first enabled real-time tick, or when this
72    /// stream has no real-time accounting. The period cannot change during
73    /// the stream's lifetime. A wake or PI deboost may concurrently reset the count.
74    pub fn realtime_ticks(&self) -> Option<(u64, core::num::NonZeroU64)> {
75        let realtime = self.realtime.as_ref()?;
76        let period = core::num::NonZeroU64::new(realtime.period_ns.load(Ordering::Acquire))?;
77        Some((realtime.ticks.load(Ordering::Acquire), period))
78    }
79
80    pub(crate) fn sample_realtime(&self, wall_ns: u64, tick_ns: u64) {
81        let Some(realtime) = &self.realtime else {
82            return;
83        };
84        if realtime.gate.enabled_generation().is_none() {
85            return;
86        }
87        assert_ne!(tick_ns, 0, "real-time tick period must be nonzero");
88        let period = realtime.period_ns.load(Ordering::Relaxed);
89        if period == 0 {
90            realtime.period_ns.store(tick_ns, Ordering::Release);
91        } else {
92            assert_eq!(period, tick_ns, "real-time tick period changed");
93        }
94        // Owner-rq exclusion and migration handoff serialize every writer.
95        // Only the count is consumed remotely; last_period is writer-local
96        // bookkeeping represented atomically to keep the carrier safely shared.
97        let current_period = wall_ns / tick_ns;
98        if realtime.last_period.load(Ordering::Relaxed) == current_period {
99            return;
100        }
101        realtime
102            .last_period
103            .store(current_period, Ordering::Relaxed);
104        let ticks = realtime.ticks.load(Ordering::Relaxed);
105        realtime
106            .ticks
107            .store(ticks.saturating_add(1), Ordering::Release);
108    }
109
110    pub(crate) fn reset_realtime(&self) {
111        if let Some(realtime) = &self.realtime {
112            // Resetting the count does not permit a second charge in this period.
113            realtime.ticks.store(0, Ordering::Release);
114        }
115    }
116
117    /// Returns the raw tick-accounted totals.
118    pub fn snapshot(&self) -> SchedulerTickCpuTimeSnapshot {
119        SchedulerTickCpuTimeSnapshot {
120            user_ns: self.user_ns.load(Ordering::Acquire),
121            system_ns: self.system_ns.load(Ordering::Acquire),
122        }
123    }
124
125    pub(crate) fn sample(&self, mode: SchedulerTickMode, tick_ns: u64) {
126        let total = match mode {
127            SchedulerTickMode::User => &self.user_ns,
128            SchedulerTickMode::System => &self.system_ns,
129        };
130        total
131            .try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
132                Some(current.saturating_add(tick_ns))
133            })
134            .expect("infallible scheduler-tick CPU-time update failed");
135    }
136}
137
138impl Default for SchedulerTickCpuTime {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144/// Coherent-enough raw totals from independent monotonic tick counters.
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub struct SchedulerTickCpuTimeSnapshot {
147    user_ns: u64,
148    system_ns: u64,
149}
150
151impl SchedulerTickCpuTimeSnapshot {
152    /// Returns tick-sampled userspace CPU time.
153    pub const fn user_ns(self) -> u64 {
154        self.user_ns
155    }
156
157    /// Returns tick-sampled kernel CPU time.
158    pub const fn system_ns(self) -> u64 {
159        self.system_ns
160    }
161}
162
163/// Shared process or subsystem interest in scheduler-tick task work.
164///
165/// An operating system may share one gate across every scheduler thread that
166/// belongs to the same higher-level accounting domain. The hard-IRQ path only
167/// observes this atomic gate; it never invokes the associated callback.
168#[derive(Debug)]
169pub struct SchedulerTickGate {
170    state: AtomicU64,
171}
172
173impl SchedulerTickGate {
174    const ENABLED: u64 = 1;
175    const GENERATION_STEP: u64 = 2;
176
177    /// Creates a disabled scheduler-tick gate.
178    pub const fn new() -> Self {
179        Self {
180            state: AtomicU64::new(0),
181        }
182    }
183
184    /// Publishes whether scheduler ticks should enqueue deferred extension work.
185    ///
186    /// A disable transition invalidates every queued publication from the
187    /// previous enabled generation. It does not wait for a callback that an
188    /// ordinary-context consumer already claimed.
189    pub fn set_enabled(&self, enabled: bool) {
190        let mut observed = self.state.load(Ordering::Acquire);
191        loop {
192            if (observed & Self::ENABLED != 0) == enabled {
193                return;
194            }
195            let generation = observed
196                .checked_add(Self::GENERATION_STEP)
197                .expect("scheduler tick gate generation overflow");
198            let updated = (generation & !Self::ENABLED) | u64::from(enabled);
199            match self.state.compare_exchange_weak(
200                observed,
201                updated,
202                Ordering::AcqRel,
203                Ordering::Acquire,
204            ) {
205                Ok(_) => return,
206                Err(current) => observed = current,
207            }
208        }
209    }
210
211    fn enabled_generation(&self) -> Option<u64> {
212        let state = self.state.load(Ordering::Acquire);
213        (state & Self::ENABLED != 0).then_some(state)
214    }
215
216    fn generation_is_enabled(&self, generation: u64) -> bool {
217        self.state.load(Ordering::Acquire) == generation
218    }
219}
220
221impl Default for SchedulerTickGate {
222    fn default() -> Self {
223        Self::new()
224    }
225}
226
227/// Task-context callback selected by one scheduler-tick publication.
228///
229/// `observed_ns` is the latest monotonic scheduler-tick timestamp coalesced
230/// into this publication. It lets the callback account the carrier thread up
231/// to the IRQ observation boundary without running OS code in hard IRQ.
232///
233/// The callback returns [`SchedulerTickWorkDisposition::Retry`] only when a
234/// transient task-context serialization boundary prevented it from consuming
235/// the publication. The task system then republishes the same generation
236/// instead of spinning in one worker pass.
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
238pub enum SchedulerTickWorkDisposition {
239    /// The callback consumed the publication.
240    Complete,
241    /// The callback made no state change and needs a later task-context retry.
242    Retry,
243}
244
245/// Scheduler-tick task-work callback.
246pub type SchedulerTickTaskWork = unsafe extern "Rust" fn(
247    data: usize,
248    thread: ThreadId,
249    observed_ns: u64,
250) -> SchedulerTickWorkDisposition;
251
252#[derive(Clone, Debug)]
253pub(crate) struct SchedulerTickWork {
254    gate: Arc<SchedulerTickGate>,
255    callback: SchedulerTickTaskWork,
256}
257
258impl SchedulerTickWork {
259    pub(crate) const fn new(gate: Arc<SchedulerTickGate>, callback: SchedulerTickTaskWork) -> Self {
260        Self { gate, callback }
261    }
262
263    pub(crate) fn enabled_generation(&self) -> Option<u64> {
264        self.gate.enabled_generation()
265    }
266
267    pub(crate) fn generation_is_enabled(&self, generation: u64) -> bool {
268        self.gate.generation_is_enabled(generation)
269    }
270
271    pub(crate) unsafe fn invoke(
272        &self,
273        data: usize,
274        thread: ThreadId,
275        observed_ns: u64,
276    ) -> SchedulerTickWorkDisposition {
277        unsafe { (self.callback)(data, thread, observed_ns) }
278    }
279}
280
281/// One detached scheduler-tick publication owned by the task-work consumer.
282#[derive(Debug)]
283pub(crate) struct SchedulerTickWorkClaim {
284    work: SchedulerTickWork,
285    generation: u64,
286    observed_ns: u64,
287}
288
289impl SchedulerTickWorkClaim {
290    pub(crate) const fn new(work: SchedulerTickWork, generation: u64, observed_ns: u64) -> Self {
291        Self {
292            work,
293            generation,
294            observed_ns,
295        }
296    }
297
298    pub(crate) const fn generation(&self) -> u64 {
299        self.generation
300    }
301
302    pub(crate) fn generation_is_enabled(&self) -> bool {
303        self.work.generation_is_enabled(self.generation)
304    }
305
306    pub(crate) unsafe fn invoke(
307        &self,
308        data: usize,
309        thread: ThreadId,
310    ) -> SchedulerTickWorkDisposition {
311        unsafe { self.work.invoke(data, thread, self.observed_ns) }
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn periodic_tick_samples_only_the_published_execution_mode() {
321        let accounting = SchedulerTickCpuTime::new();
322
323        accounting.sample(SchedulerTickMode::User, 10);
324        accounting.sample(SchedulerTickMode::System, 10);
325
326        assert_eq!(
327            accounting.snapshot(),
328            SchedulerTickCpuTimeSnapshot {
329                user_ns: 10,
330                system_ns: 10,
331            }
332        );
333    }
334}