Skip to main content

epics_libcom_rs/runtime/
time.rs

1use std::time::{Duration, Instant, SystemTime};
2
3use crate::walltime::WallTime;
4
5/// Current wall-clock time as a [`WallTime`].
6///
7/// Returns [`WallTime`] rather than [`SystemTime`] so a snapshot built from
8/// "now" shares one timestamp type with snapshots built from exact wire
9/// integers. The OS clock is still read via [`SystemTime::now`]; on Windows
10/// that clock is itself 100 ns-granular, which `WallTime` does not change.
11pub fn now_wall() -> WallTime {
12    SystemTime::now().into()
13}
14
15pub fn now_mono() -> Instant {
16    Instant::now()
17}
18
19/// The instant `base + d`, saturating where `Instant + Duration` would
20/// panic — the single owner of "turn a delay into a deadline".
21///
22/// [`duration_from_secs`] deliberately maps `+inf`, `NaN` and any
23/// magnitude past `Duration`'s range to [`Duration::MAX`], so a deadline
24/// computed from a record delay field can be exactly that sum and
25/// `Instant`'s `Add` panics on it. tokio's `sleep()` does not: it takes
26/// `Instant::now().checked_add(dur)` and falls back to
27/// `Instant::far_future()`, about thirty years out. The hosted build
28/// therefore slept forever where the exec build unwound the task, and it
29/// is that disagreement — not the arithmetic — that is the defect, so
30/// the fallback here is tokio's, to the same constant.
31///
32/// Measured on `armv7-rtems-eabihf` (QEMU `xilinx-zynq-a9`,
33/// `realtime-ca-ioc`), which is where the panic was filed: `caput
34/// RTEMS:BO.HIGH 1e300` then `caput RTEMS:BO 1` makes `bo::process` arm
35/// `DelayedCallbackAfter(Duration::MAX)`. With this fallback the guest
36/// takes it silently and the one-shot simply never fires — the record
37/// still reads `On` eight seconds later, and a 0.5 s one-shot armed
38/// afterwards still reverts it. Rebuilding the same image with `base + d`
39/// in place of the `checked_add` gives *"panic on thread `cbMedium` at
40/// runtime/time.rs: overflow when adding duration to instant"*, so the
41/// embedded timer path really does consume this sum and the guard is what
42/// keeps it off the console. VxWorks is still arithmetic-only.
43pub fn deadline_after(base: Instant, d: Duration) -> Instant {
44    base.checked_add(d).unwrap_or_else(far_future)
45}
46
47/// `base` = now. See [`deadline_after`].
48pub fn deadline_from_now(d: Duration) -> Instant {
49    deadline_after(Instant::now(), d)
50}
51
52/// Roughly thirty years — the offset in tokio's `Instant::far_future()`,
53/// the "never fires" deadline an unrepresentable one collapses to.
54/// Shared with [`crate::runtime::task::deadline_after`], which applies
55/// the same rule to the runtime's own `Instant` alias.
56pub(crate) const FAR_FUTURE: Duration = Duration::from_secs(86_400 * 365 * 30);
57
58/// See [`FAR_FUTURE`].
59fn far_future() -> Instant {
60    Instant::now() + FAR_FUTURE
61}
62
63/// The OS clock-tick period, in seconds — C `epicsThreadSleepQuantum()`.
64///
65/// posix (`libcom/src/osi/os/posix/osdThread.c:1108-1116`):
66///
67/// ```c
68/// double epicsThreadSleepQuantum(void)
69/// {
70///     double hz = sysconf(_SC_CLK_TCK);
71///     if (hz <= 0) return 0.0;
72///     return 1.0 / hz;
73/// }
74/// ```
75///
76/// Records use it to round a delay field to a whole number of ticks — e.g.
77/// `sseqRecord.c:198-200` quantizes every `DLYn` at init. Returns 0.0 when the
78/// tick rate is unavailable, exactly as C does; callers must treat that as "no
79/// quantization" rather than dividing by it.
80///
81/// # Why this asks instead of stating
82///
83/// The tick rate is not ours to declare. On RTEMS it is set by
84/// `CONFIGURE_MICROSECONDS_PER_TICK` in `epics-rtems-boot`'s
85/// `csrc/rtems_config.c`, which is deliberately `#ifndef`-overridable from the
86/// build so a timing experiment needs no source edit. A Rust constant here
87/// would be a second copy of that number, silently wrong the first time
88/// anyone overrides it — with nothing checking the two agree.
89///
90/// So the unix arm asks, and the answer comes from the same define:
91///
92/// ```text
93/// sysconf(_SC_CLK_TCK)
94///   -> rtems_clock_get_ticks_per_second()      cpukit/posix/src/sysconf.c:60-61 (rtems_6)
95///   -> _Watchdog_Ticks_per_second              rtems/rtems/clock.h:871 (both rtems pins)
96///    = 1000000 / CONFIGURE_MICROSECONDS_PER_TICK   confdefs/clock.h:100-101 (rtems_6)
97/// ```
98///
99/// This is also what C itself does on RTEMS — `RTEMS-score/osdThread.c:860-865`
100/// returns `1.0 / rtemsTicksPerSecond_double` rather than a constant — so the
101/// port matches C's behaviour on the target, not just on posix.
102///
103/// The non-unix (Windows) arm keeps a constant. `_SC_CLK_TCK` is 100 on Linux
104/// and macOS, and 100 Hz is the historical default this port shipped; it
105/// restates no `#define` of ours. It is *not* C parity: `WIN32/osdThread.c:906-932`
106/// asks `GetSystemTimeAdjustment` and returns 0.0 on failure. Closing that gap
107/// needs a Windows syscall dependency this crate does not have, and is a
108/// separate change from the RTEMS one.
109pub fn thread_sleep_quantum() -> f64 {
110    #[cfg(unix)]
111    {
112        // SAFETY: `sysconf` is a pure query with no preconditions.
113        let hz = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
114        if hz <= 0.0 { 0.0 } else { 1.0 / hz }
115    }
116    #[cfg(not(unix))]
117    {
118        0.01
119    }
120}
121
122/// Round `seconds` to the nearest whole [`thread_sleep_quantum`] tick, the way
123/// C records do it:
124///
125/// ```c
126/// #define NINT(f) (long)((f)>0 ? (f)+0.5 : (f)-0.5)
127/// plinkGroup->dly = epicsThreadSleepQuantum() *
128///                   NINT(plinkGroup->dly / epicsThreadSleepQuantum());
129/// ```
130///
131/// (`sseqRecord.c:67`, `:198-199`.) The `NINT` cast is to a C `long` (i64),
132/// NOT an f64 round, and the served DLY must reproduce that cast byte-for-byte
133/// — see `c_long_cast`. Two boundaries C's cast owns that an `f64::trunc`
134/// port gets wrong:
135///
136///   * **Overflow.** A `dly` large enough that `ticks` rounds past 2^63
137///     overflows the `(long)` cast. On x86-64 (the target the oracle runs on)
138///     `cvttsd2si` maps every out-of-range value — and NaN/±inf — to
139///     i64::MIN = `0x8000_0000_0000_0000`, so the field becomes `quantum *
140///     i64::MIN` ≈ -9.22e16, exactly what C serves for a huge `caput`. An
141///     `f64::trunc` port instead keeps the huge value (or `inf`).
142///   * **Negative zero.** A `dly` that rounds to zero yields the *integer* 0,
143///     and `quantum * 0` is `+0.0`. An `f64::trunc` port produces `-0.0` for
144///     the `0.0` default (`(-0.0 - 0.5).trunc()` = `-0.0`) and for any tiny or
145///     negative input that rounds to zero, which renders as `-0` where C
146///     renders `0`.
147///
148/// With a zero quantum (C's `hz <= 0` path) the value is returned unchanged
149/// rather than dividing by zero.
150pub fn quantize_to_sleep_quantum(seconds: f64) -> f64 {
151    let quantum = thread_sleep_quantum();
152    if quantum <= 0.0 {
153        return seconds;
154    }
155    let ticks = seconds / quantum;
156    // C `NINT(f) = (long)((f) > 0 ? (f) + 0.5 : (f) - 0.5)`.
157    let rounded = if ticks > 0.0 {
158        ticks + 0.5
159    } else {
160        ticks - 0.5
161    };
162    quantum * c_long_cast(rounded) as f64
163}
164
165/// Reproduce C's `(long)` cast of a `double` with x86-64 `cvttsd2si`
166/// semantics: an in-range finite value truncates toward zero (as Rust's
167/// `as i64` already does); every out-of-range value and NaN/±inf yields
168/// i64::MIN, the "integer indefinite" the instruction returns.
169///
170/// Rust's own `as i64` *saturates* out-of-range inputs instead (2^63 →
171/// i64::MAX, -inf → i64::MIN, NaN → 0), so the explicit range check is what
172/// makes the port match C on the overflow boundary.
173fn c_long_cast(f: f64) -> i64 {
174    // i64::MIN is exactly -2^63 and representable as f64; i64::MAX rounds up to
175    // 2^63 as f64 (out of range), so the upper bound is a strict `< 2^63`.
176    const MIN: f64 = -9_223_372_036_854_775_808.0; // -2^63
177    const LIMIT: f64 = 9_223_372_036_854_775_808.0; //  2^63
178    if f.is_nan() || f < MIN || f >= LIMIT {
179        i64::MIN
180    } else {
181        f as i64
182    }
183}
184
185/// Seconds as an `f64` → [`Duration`], without the panic
186/// `Duration::from_secs_f64` raises.
187///
188/// This is the libcom time seam's single converter, and every caller
189/// that turns a *record field*, an *environment variable* or any other
190/// externally supplied `double` into a delay must come through it.
191/// `Duration::from_secs_f64` panics on NaN, on either infinity, on a
192/// negative, and on a finite value past `u64::MAX` seconds — and an
193/// `is_finite()` test at the call site is not the rule, because `1e300`
194/// is finite and still panics. `Duration::try_from_secs_f64` is the one
195/// rule that covers all four in a single test.
196///
197/// C never aborts on any of them: `epicsTimeAddSeconds`
198/// (`epicsTime.cpp`) does `nsec += epicsInt64(seconds*1e9 + ...)`, an
199/// out-of-range float→integer conversion, so the deadline is garbage and
200/// the callback fires at the wrong time while the IOC keeps serving
201/// every other PV. The mapping here keeps that "IOC survives" property
202/// and gives the garbage a defined shape:
203///
204/// * negative, including `-inf` → [`Duration::ZERO`] — C's
205///   already-expired deadline, which fires at once.
206/// * `+inf`, `NaN`, or a magnitude beyond `Duration` → [`Duration::MAX`]
207///   — a deadline no comparison ever reaches, i.e. it never fires.
208///   NaN lands here because in C every `now < expire` test against NaN
209///   is false, which is the same "never fires".
210pub fn duration_from_secs(secs: f64) -> Duration {
211    Duration::try_from_secs_f64(secs).unwrap_or(if secs < 0.0 {
212        Duration::ZERO
213    } else {
214        Duration::MAX
215    })
216}
217
218/// Block the calling thread for `secs` — C `epicsThreadSleep`, and the
219/// single owner of "turn a caller-supplied delay into a sleep".
220///
221/// C (`libcom/src/osi/os/posix/osdThread.c:916-934` @R7.0.10) truncates
222/// `seconds` into `timespec.tv_sec`, zeroes the delay when
223/// `seconds <= 0`, and lets `nanosleep` reject whatever will not fit.
224/// Measured against `bin/linux-x86_64/softIoc` driving
225/// `epicsThreadSleep`, `1e300`, `inf`, `nan` and `-5` each return inside
226/// the process's own 0.33 s startup while `0.25` and `1.5` sleep their
227/// full delay: a delay that is not a representable positive `Duration`
228/// is not slept at all.
229///
230/// Deliberately NOT [`duration_from_secs`]. That owner saturates to
231/// [`Duration::MAX`] because a *deadline* built from an absurd delay
232/// must never fire; sleeping on [`Duration::MAX`] would park the caller
233/// for 584 billion years exactly where C returns at once, and
234/// `Duration::from_secs_f64` would panic. The two meanings need two
235/// owners, not one conversion reused on both sides.
236pub fn sleep_secs(secs: f64) {
237    if let Ok(d) = Duration::try_from_secs_f64(secs) {
238        std::thread::sleep(d);
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use source_guard::{Comments, production};
246
247    /// The sleep owner's boundaries are the *opposite* of the deadline
248    /// owner's, which is the whole reason it exists: everything
249    /// `duration_from_secs` saturates to [`Duration::MAX`] is a delay
250    /// `nanosleep` refuses, so C returns from it at once and so must we.
251    ///
252    /// The pairs measured against `softIoc`: `1e300`, `inf`, `nan`, `-5`
253    /// returned in C's 0.33 s startup baseline; `0.25` and `1.5` slept.
254    #[test]
255    fn sleep_secs_returns_at_once_on_every_delay_nanosleep_refuses() {
256        for refused in [
257            f64::INFINITY,
258            f64::NEG_INFINITY,
259            f64::NAN,
260            1e300,
261            -5.0,
262            u64::MAX as f64,
263        ] {
264            let t = Instant::now();
265            sleep_secs(refused);
266            assert!(
267                t.elapsed() < Duration::from_millis(50),
268                "sleep_secs({refused}) must return at once, as C's nanosleep does"
269            );
270        }
271        // Zero is representable, and C's `nanosleep(0,0)` also returns
272        // at once — it must not be confused with the refused set.
273        let t = Instant::now();
274        sleep_secs(0.0);
275        assert!(t.elapsed() < Duration::from_millis(50));
276        // A representable positive delay is slept in full.
277        let t = Instant::now();
278        sleep_secs(0.05);
279        assert!(t.elapsed() >= Duration::from_millis(50));
280    }
281
282    /// Boundaries of the one rule, not scenarios: every input
283    /// `Duration::from_secs_f64` would panic on has a defined answer
284    /// here, and the representable ones convert unchanged.
285    #[test]
286    fn duration_from_secs_covers_every_panic_boundary() {
287        assert_eq!(duration_from_secs(f64::INFINITY), Duration::MAX);
288        assert_eq!(duration_from_secs(f64::NEG_INFINITY), Duration::ZERO);
289        assert_eq!(duration_from_secs(f64::NAN), Duration::MAX);
290        // Finite and far too large — the case an `is_finite()` guard
291        // lets through and `from_secs_f64` still panics on.
292        assert_eq!(duration_from_secs(1e300), Duration::MAX);
293        // The representable edge: `u64::MAX` seconds is out of range,
294        // one below the power of two above it is not.
295        assert_eq!(duration_from_secs(u64::MAX as f64), Duration::MAX);
296        assert_eq!(
297            duration_from_secs(9.0e18),
298            Duration::try_from_secs_f64(9.0e18).unwrap()
299        );
300        assert_eq!(duration_from_secs(-1.0), Duration::ZERO);
301        assert_eq!(duration_from_secs(-0.0), Duration::ZERO);
302        assert_eq!(duration_from_secs(0.0), Duration::ZERO);
303        assert_eq!(duration_from_secs(0.25), Duration::from_millis(250));
304        assert_eq!(duration_from_secs(2.5), Duration::from_millis(2500));
305    }
306
307    /// The tick rate must be ASKED for, never restated.
308    ///
309    /// `CONFIGURE_MICROSECONDS_PER_TICK` in `epics-rtems-boot`'s
310    /// `csrc/rtems_config.c` owns the number, and it is `#ifndef`-overridable
311    /// from the build. A Rust constant restating it is a second source of
312    /// truth that goes silently wrong the first time anyone overrides it.
313    ///
314    /// Fails today, on Linux, with no cross toolchain.
315    #[test]
316    fn the_tick_rate_is_asked_for_not_restated() {
317        // `Strip`: the doc above `thread_sleep_quantum` spells out
318        // `1000000 / CONFIGURE_MICROSECONDS_PER_TICK` to explain the chain,
319        // and that text must not read as a restatement.
320        let src = production(include_str!("time.rs"), Comments::Strip);
321
322        assert!(
323            src.contains("libc::sysconf(libc::_SC_CLK_TCK)"),
324            "thread_sleep_quantum must ask the OS for the tick rate"
325        );
326
327        // The defect this replaced: asking only on Linux, and handing every
328        // other unix — RTEMS included — a constant.
329        assert!(
330            !src.contains("#[cfg(target_os = \"linux\")]"),
331            "the tick-rate arm must select on `unix`, not on `linux`: RTEMS is \
332             a unix that answers _SC_CLK_TCK from the boot crate's define"
333        );
334        assert_eq!(
335            src.matches("#[cfg(unix)]").count(),
336            1,
337            "exactly one arm asks; if a second appears, this guard needs updating"
338        );
339
340        // 10 ms expressed any of the ways someone would naturally write it.
341        // The sole surviving literal is the documented Windows arm.
342        assert_eq!(
343            src.matches("0.01").count(),
344            1,
345            "0.01 may appear only once, in the non-unix arm"
346        );
347        for restatement in ["10000", "10_000", "0.010", "1e-2"] {
348            assert!(
349                !src.contains(restatement),
350                "`{restatement}` restates CONFIGURE_MICROSECONDS_PER_TICK; \
351                 read it back through sysconf instead"
352            );
353        }
354    }
355
356    #[test]
357    fn test_now_wall() {
358        let t = now_wall();
359        assert!(t.since_unix_epoch().as_secs() > 0);
360    }
361
362    #[test]
363    fn test_now_mono() {
364        let t1 = now_mono();
365        let t2 = now_mono();
366        assert!(t2 >= t1);
367    }
368
369    /// Boundaries of the deadline owner: a representable delay converts
370    /// unchanged, and the one `Instant + Duration` panics on —
371    /// `Duration::MAX`, which is exactly what `duration_from_secs`
372    /// returns for `+inf`, `NaN` and `1e300` — saturates instead.
373    #[test]
374    fn deadline_saturates_where_instant_add_would_panic() {
375        let base = Instant::now();
376        assert_eq!(
377            deadline_after(base, Duration::from_secs(10)),
378            base + Duration::from_secs(10)
379        );
380        let never = deadline_after(base, Duration::MAX);
381        assert!(never > base + Duration::from_secs(86_400 * 365));
382        assert!(deadline_from_now(duration_from_secs(f64::INFINITY)) > Instant::now());
383        assert!(deadline_from_now(duration_from_secs(1e300)) > Instant::now());
384        // Saturating twice still saturates.
385        assert!(deadline_after(never, Duration::MAX) > base);
386    }
387
388    #[test]
389    fn test_deadline_from_now() {
390        let before = Instant::now();
391        let deadline = deadline_from_now(Duration::from_secs(10));
392        assert!(deadline > before);
393        assert!(deadline <= before + Duration::from_secs(11));
394    }
395
396    /// C's `(long)` cast, boundary by boundary: in-range truncation toward
397    /// zero, and i64::MIN for everything x86-64 `cvttsd2si` cannot represent.
398    #[test]
399    fn c_long_cast_matches_cvttsd2si() {
400        // In range: plain truncation toward zero, both signs.
401        assert_eq!(c_long_cast(0.0), 0);
402        assert_eq!(c_long_cast(-0.0), 0);
403        assert_eq!(c_long_cast(0.5), 0);
404        assert_eq!(c_long_cast(-0.5), 0); // toward zero, NOT -1
405        assert_eq!(c_long_cast(1.9), 1);
406        assert_eq!(c_long_cast(-1.9), -1);
407        // The representable extremes.
408        assert_eq!(c_long_cast(-9_223_372_036_854_775_808.0), i64::MIN);
409        // Out of range and non-finite all collapse to the "integer indefinite".
410        assert_eq!(c_long_cast(9_223_372_036_854_775_808.0), i64::MIN); // +2^63
411        assert_eq!(c_long_cast(1e300), i64::MIN);
412        assert_eq!(c_long_cast(-1e300), i64::MIN);
413        assert_eq!(c_long_cast(f64::INFINITY), i64::MIN);
414        assert_eq!(c_long_cast(f64::NEG_INFINITY), i64::MIN);
415        assert_eq!(c_long_cast(f64::NAN), i64::MIN);
416    }
417
418    /// The DLY quantization the served value must match, by invariant boundary.
419    #[test]
420    fn quantize_dly_boundaries_match_c() {
421        let q = thread_sleep_quantum();
422        assert!(q > 0.0, "test assumes a positive clock quantum, got {q}");
423
424        // The default DLY is 0.0; it must serve as +0.0, never -0.0.
425        let zero = quantize_to_sleep_quantum(0.0);
426        assert_eq!(zero, 0.0);
427        assert!(
428            zero.is_sign_positive(),
429            "DLY=0.0 must round to +0.0 (renders \"0\"), got a negative zero"
430        );
431
432        // A -0.0 input must also normalize to +0.0 (C's `NINT` yields integer 0).
433        let neg_zero = quantize_to_sleep_quantum(-0.0);
434        assert_eq!(neg_zero, 0.0);
435        assert!(
436            neg_zero.is_sign_positive(),
437            "DLY=-0.0 must round to +0.0, got a negative zero"
438        );
439
440        // A tiny positive below half a tick rounds to +0.0.
441        let tiny = quantize_to_sleep_quantum(q / 4.0);
442        assert_eq!(tiny, 0.0);
443        assert!(tiny.is_sign_positive(), "tiny +dly rounds to +0.0");
444
445        // A small negative that rounds to zero: C truncates `-0.x` to 0 → +0.0.
446        let tiny_neg = quantize_to_sleep_quantum(-q / 4.0);
447        assert_eq!(tiny_neg, 0.0);
448        assert!(
449            tiny_neg.is_sign_positive(),
450            "a -dly rounding to zero must serve +0.0, not -0.0"
451        );
452
453        // An exact quantum multiple is preserved exactly.
454        assert_eq!(quantize_to_sleep_quantum(3.0 * q), 3.0 * q);
455        assert_eq!(quantize_to_sleep_quantum(-3.0 * q), -3.0 * q);
456
457        // Round-half-away-from-zero at the tick boundary.
458        assert_eq!(quantize_to_sleep_quantum(1.5 * q), 2.0 * q);
459        assert_eq!(quantize_to_sleep_quantum(-1.5 * q), -2.0 * q);
460
461        // A huge finite `dly` overflows the `(long)` cast to i64::MIN, so the
462        // served value is `quantum * i64::MIN` — negative, and NOT `inf`.
463        let huge = quantize_to_sleep_quantum(1e300);
464        assert_eq!(huge, q * (i64::MIN as f64));
465        assert!(
466            huge.is_finite() && huge < 0.0,
467            "a huge +dly must serve C's ~-9.22e16, not inf, got {huge}"
468        );
469        // A huge negative `dly` overflows the same way.
470        let huge_neg = quantize_to_sleep_quantum(-1e300);
471        assert_eq!(huge_neg, q * (i64::MIN as f64));
472
473        // ±inf also collapses to i64::MIN (an `f64::trunc` port kept inf).
474        assert_eq!(
475            quantize_to_sleep_quantum(f64::INFINITY),
476            q * (i64::MIN as f64)
477        );
478        assert_eq!(
479            quantize_to_sleep_quantum(f64::NEG_INFINITY),
480            q * (i64::MIN as f64)
481        );
482    }
483}