Skip to main content

taktora_executor/
clock.rs

1//! Monotonic time source seam for cycle telemetry (`REQ_0101`, `REQ_0105`,
2//! `REQ_0106`).
3//!
4//! `std::time::Instant` is **opaque** — it cannot be constructed from a
5//! duration — so it is impossible to feed scripted instants to the telemetry
6//! math in a test. The timing path that produces `took` / jitter / lateness
7//! therefore reads time through this `u64`-nanosecond abstraction instead of
8//! calling [`Instant::now`](std::time::Instant::now) directly.
9//!
10//! * Production wires in [`SystemClock`] (a thin wrapper over a monotonic
11//!   [`Instant`](std::time::Instant) epoch), so behaviour is unchanged.
12//! * Tests wire in [`MockClock`] via [`ExecutorBuilder::clock`] and advance it
13//!   by hand — typically from inside a task body — so jitter, lateness and
14//!   min/max can be asserted to the *exact* nanosecond with no real sleeps and
15//!   no dependence on the CI scheduler.
16//!
17//! Only the telemetry path uses this clock. Scheduling (the iceoryx2 `WaitSet`
18//! interval triggers), run-mode deadlines, fault `since_ms`, and the
19//! [`ExecutionMonitor`](crate::ExecutionMonitor) callbacks continue to use the
20//! real [`Instant`](std::time::Instant) clock, so a mock clock can never alter
21//! dispatch or fault behaviour.
22//!
23//! [`ExecutorBuilder::clock`]: crate::ExecutorBuilder::clock
24
25use std::sync::Arc;
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::time::Instant;
28
29/// A monotonic nanosecond time source for cycle telemetry.
30///
31/// Implementations return nanoseconds elapsed since an arbitrary fixed epoch.
32/// The value must be monotonic non-decreasing within one clock instance; the
33/// epoch itself is unspecified (telemetry only ever takes differences).
34pub trait MonotonicClock: Send + Sync + 'static {
35    /// Nanoseconds since this clock's epoch. Monotonic non-decreasing.
36    fn now_nanos(&self) -> u64;
37}
38
39/// Production clock: monotonic nanoseconds since the clock was constructed
40/// (typically executor `build()` time).
41#[derive(Debug)]
42pub struct SystemClock {
43    epoch: Instant,
44}
45
46impl SystemClock {
47    /// Construct a clock whose epoch is the current instant.
48    #[must_use]
49    pub fn new() -> Self {
50        Self {
51            epoch: Instant::now(),
52        }
53    }
54}
55
56impl Default for SystemClock {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl MonotonicClock for SystemClock {
63    fn now_nanos(&self) -> u64 {
64        u64::try_from(self.epoch.elapsed().as_nanos()).unwrap_or(u64::MAX)
65    }
66}
67
68/// Test clock: a manually-advanced nanosecond counter.
69///
70/// `MockClock` is a cloneable handle over a shared counter — clone it, hand one
71/// clone to [`ExecutorBuilder::clock`](crate::ExecutorBuilder::clock) and keep
72/// the other to drive time from the test (or from inside a task body). Both
73/// clones observe the same counter.
74///
75/// Advancing the clock from inside a cyclic task body is the idiomatic pattern:
76/// the body "spends" a precise number of nanoseconds, which the telemetry fold
77/// then reads back as the cycle's `took` (and, across cycles, as the measured
78/// period feeding jitter and lateness).
79#[derive(Clone, Debug, Default)]
80pub struct MockClock {
81    nanos: Arc<AtomicU64>,
82}
83
84impl MockClock {
85    /// Construct a mock clock reading `0`.
86    #[must_use]
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Advance the clock by `delta_ns`, returning the new value.
92    pub fn advance(&self, delta_ns: u64) -> u64 {
93        self.nanos.fetch_add(delta_ns, Ordering::Release) + delta_ns
94    }
95
96    /// Set the clock to the absolute value `ns`.
97    pub fn set(&self, ns: u64) {
98        self.nanos.store(ns, Ordering::Release);
99    }
100
101    /// Read the current value.
102    #[must_use]
103    pub fn now(&self) -> u64 {
104        self.nanos.load(Ordering::Acquire)
105    }
106}
107
108impl MonotonicClock for MockClock {
109    fn now_nanos(&self) -> u64 {
110        self.nanos.load(Ordering::Acquire)
111    }
112}