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}