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
19pub fn deadline_from_now(d: Duration) -> Instant {
20 Instant::now() + d
21}
22
23/// The OS clock-tick period, in seconds — C `epicsThreadSleepQuantum()`.
24///
25/// posix (`libcom/src/osi/os/posix/osdThread.c:1108-1116`):
26///
27/// ```c
28/// double epicsThreadSleepQuantum(void)
29/// {
30/// double hz = sysconf(_SC_CLK_TCK);
31/// if (hz <= 0) return 0.0;
32/// return 1.0 / hz;
33/// }
34/// ```
35///
36/// Records use it to round a delay field to a whole number of ticks — e.g.
37/// `sseqRecord.c:197-200` quantizes every `DLYn` at init. Returns 0.0 when the
38/// tick rate is unavailable, exactly as C does; callers must treat that as "no
39/// quantization" rather than dividing by it.
40///
41/// # Why this asks instead of stating
42///
43/// The tick rate is not ours to declare. On RTEMS it is set by
44/// `CONFIGURE_MICROSECONDS_PER_TICK` in `epics-rtems-boot`'s
45/// `csrc/rtems_config.c`, which is deliberately `#ifndef`-overridable from the
46/// build so a timing experiment needs no source edit. A Rust constant here
47/// would be a second copy of that number, silently wrong the first time
48/// anyone overrides it — with nothing checking the two agree.
49///
50/// So the unix arm asks, and the answer comes from the same define:
51///
52/// ```text
53/// sysconf(_SC_CLK_TCK)
54/// -> rtems_clock_get_ticks_per_second() cpukit/posix/src/sysconf.c:60-61
55/// -> _Watchdog_Ticks_per_second rtems/rtems/clock.h:871
56/// = 1000000 / CONFIGURE_MICROSECONDS_PER_TICK confdefs/clock.h:100-101
57/// ```
58///
59/// This is also what C itself does on RTEMS — `RTEMS-score/osdThread.c:860-865`
60/// returns `1.0 / rtemsTicksPerSecond_double` rather than a constant — so the
61/// port matches C's behaviour on the target, not just on posix.
62///
63/// The non-unix (Windows) arm keeps a constant. `_SC_CLK_TCK` is 100 on Linux
64/// and macOS, and 100 Hz is the historical default this port shipped; it
65/// restates no `#define` of ours. It is *not* C parity: `WIN32/osdThread.c:906-932`
66/// asks `GetSystemTimeAdjustment` and returns 0.0 on failure. Closing that gap
67/// needs a Windows syscall dependency this crate does not have, and is a
68/// separate change from the RTEMS one.
69pub fn thread_sleep_quantum() -> f64 {
70 #[cfg(unix)]
71 {
72 // SAFETY: `sysconf` is a pure query with no preconditions.
73 let hz = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
74 if hz <= 0.0 { 0.0 } else { 1.0 / hz }
75 }
76 #[cfg(not(unix))]
77 {
78 0.01
79 }
80}
81
82/// Round `seconds` to the nearest whole [`thread_sleep_quantum`] tick, the way
83/// C records do it:
84///
85/// ```c
86/// #define NINT(f) (long)((f)>0 ? (f)+0.5 : (f)-0.5)
87/// plinkGroup->dly = epicsThreadSleepQuantum() *
88/// NINT(plinkGroup->dly / epicsThreadSleepQuantum());
89/// ```
90///
91/// (`sseqRecord.c:67`, `:197-199`.) The `NINT` cast is to a C `long` (i64),
92/// NOT an f64 round, and the served DLY must reproduce that cast byte-for-byte
93/// — see `c_long_cast`. Two boundaries C's cast owns that an `f64::trunc`
94/// port gets wrong:
95///
96/// * **Overflow.** A `dly` large enough that `ticks` rounds past 2^63
97/// overflows the `(long)` cast. On x86-64 (the target the oracle runs on)
98/// `cvttsd2si` maps every out-of-range value — and NaN/±inf — to
99/// i64::MIN = `0x8000_0000_0000_0000`, so the field becomes `quantum *
100/// i64::MIN` ≈ -9.22e16, exactly what C serves for a huge `caput`. An
101/// `f64::trunc` port instead keeps the huge value (or `inf`).
102/// * **Negative zero.** A `dly` that rounds to zero yields the *integer* 0,
103/// and `quantum * 0` is `+0.0`. An `f64::trunc` port produces `-0.0` for
104/// the `0.0` default (`(-0.0 - 0.5).trunc()` = `-0.0`) and for any tiny or
105/// negative input that rounds to zero, which renders as `-0` where C
106/// renders `0`.
107///
108/// With a zero quantum (C's `hz <= 0` path) the value is returned unchanged
109/// rather than dividing by zero.
110pub fn quantize_to_sleep_quantum(seconds: f64) -> f64 {
111 let quantum = thread_sleep_quantum();
112 if quantum <= 0.0 {
113 return seconds;
114 }
115 let ticks = seconds / quantum;
116 // C `NINT(f) = (long)((f) > 0 ? (f) + 0.5 : (f) - 0.5)`.
117 let rounded = if ticks > 0.0 {
118 ticks + 0.5
119 } else {
120 ticks - 0.5
121 };
122 quantum * c_long_cast(rounded) as f64
123}
124
125/// Reproduce C's `(long)` cast of a `double` with x86-64 `cvttsd2si`
126/// semantics: an in-range finite value truncates toward zero (as Rust's
127/// `as i64` already does); every out-of-range value and NaN/±inf yields
128/// i64::MIN, the "integer indefinite" the instruction returns.
129///
130/// Rust's own `as i64` *saturates* out-of-range inputs instead (2^63 →
131/// i64::MAX, -inf → i64::MIN, NaN → 0), so the explicit range check is what
132/// makes the port match C on the overflow boundary.
133fn c_long_cast(f: f64) -> i64 {
134 // i64::MIN is exactly -2^63 and representable as f64; i64::MAX rounds up to
135 // 2^63 as f64 (out of range), so the upper bound is a strict `< 2^63`.
136 const MIN: f64 = -9_223_372_036_854_775_808.0; // -2^63
137 const LIMIT: f64 = 9_223_372_036_854_775_808.0; // 2^63
138 if f.is_nan() || f < MIN || f >= LIMIT {
139 i64::MIN
140 } else {
141 f as i64
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 /// Everything before the first column-0 `#[cfg(test)]` — the code that
150 /// actually ships. Same helper as `epics-pva-rs`'s source guards.
151 fn production_scope(src: &str) -> &str {
152 match src.find("\n#[cfg(test)]") {
153 Some(i) => &src[..i],
154 None => src,
155 }
156 }
157
158 /// The tick rate must be ASKED for, never restated.
159 ///
160 /// `CONFIGURE_MICROSECONDS_PER_TICK` in `epics-rtems-boot`'s
161 /// `csrc/rtems_config.c` owns the number, and it is `#ifndef`-overridable
162 /// from the build. A Rust constant restating it is a second source of
163 /// truth that goes silently wrong the first time anyone overrides it.
164 ///
165 /// Fails today, on Linux, with no cross toolchain.
166 #[test]
167 fn the_tick_rate_is_asked_for_not_restated() {
168 // Comment lines are stripped: the doc above `thread_sleep_quantum`
169 // spells out `1000000 / CONFIGURE_MICROSECONDS_PER_TICK` to explain
170 // the chain, and that text must not read as a restatement.
171 let src: String = production_scope(include_str!("time.rs"))
172 .lines()
173 .filter(|l| !l.trim_start().starts_with("//"))
174 .collect::<Vec<_>>()
175 .join("\n");
176 let src = src.as_str();
177
178 assert!(
179 src.contains("libc::sysconf(libc::_SC_CLK_TCK)"),
180 "thread_sleep_quantum must ask the OS for the tick rate"
181 );
182
183 // The defect this replaced: asking only on Linux, and handing every
184 // other unix — RTEMS included — a constant.
185 assert!(
186 !src.contains("#[cfg(target_os = \"linux\")]"),
187 "the tick-rate arm must select on `unix`, not on `linux`: RTEMS is \
188 a unix that answers _SC_CLK_TCK from the boot crate's define"
189 );
190 assert_eq!(
191 src.matches("#[cfg(unix)]").count(),
192 1,
193 "exactly one arm asks; if a second appears, this guard needs updating"
194 );
195
196 // 10 ms expressed any of the ways someone would naturally write it.
197 // The sole surviving literal is the documented Windows arm.
198 assert_eq!(
199 src.matches("0.01").count(),
200 1,
201 "0.01 may appear only once, in the non-unix arm"
202 );
203 for restatement in ["10000", "10_000", "0.010", "1e-2"] {
204 assert!(
205 !src.contains(restatement),
206 "`{restatement}` restates CONFIGURE_MICROSECONDS_PER_TICK; \
207 read it back through sysconf instead"
208 );
209 }
210 }
211
212 #[test]
213 fn test_now_wall() {
214 let t = now_wall();
215 assert!(t.since_unix_epoch().as_secs() > 0);
216 }
217
218 #[test]
219 fn test_now_mono() {
220 let t1 = now_mono();
221 let t2 = now_mono();
222 assert!(t2 >= t1);
223 }
224
225 #[test]
226 fn test_deadline_from_now() {
227 let before = Instant::now();
228 let deadline = deadline_from_now(Duration::from_secs(10));
229 assert!(deadline > before);
230 assert!(deadline <= before + Duration::from_secs(11));
231 }
232
233 /// C's `(long)` cast, boundary by boundary: in-range truncation toward
234 /// zero, and i64::MIN for everything x86-64 `cvttsd2si` cannot represent.
235 #[test]
236 fn c_long_cast_matches_cvttsd2si() {
237 // In range: plain truncation toward zero, both signs.
238 assert_eq!(c_long_cast(0.0), 0);
239 assert_eq!(c_long_cast(-0.0), 0);
240 assert_eq!(c_long_cast(0.5), 0);
241 assert_eq!(c_long_cast(-0.5), 0); // toward zero, NOT -1
242 assert_eq!(c_long_cast(1.9), 1);
243 assert_eq!(c_long_cast(-1.9), -1);
244 // The representable extremes.
245 assert_eq!(c_long_cast(-9_223_372_036_854_775_808.0), i64::MIN);
246 // Out of range and non-finite all collapse to the "integer indefinite".
247 assert_eq!(c_long_cast(9_223_372_036_854_775_808.0), i64::MIN); // +2^63
248 assert_eq!(c_long_cast(1e300), i64::MIN);
249 assert_eq!(c_long_cast(-1e300), i64::MIN);
250 assert_eq!(c_long_cast(f64::INFINITY), i64::MIN);
251 assert_eq!(c_long_cast(f64::NEG_INFINITY), i64::MIN);
252 assert_eq!(c_long_cast(f64::NAN), i64::MIN);
253 }
254
255 /// The DLY quantization the served value must match, by invariant boundary.
256 #[test]
257 fn quantize_dly_boundaries_match_c() {
258 let q = thread_sleep_quantum();
259 assert!(q > 0.0, "test assumes a positive clock quantum, got {q}");
260
261 // The default DLY is 0.0; it must serve as +0.0, never -0.0.
262 let zero = quantize_to_sleep_quantum(0.0);
263 assert_eq!(zero, 0.0);
264 assert!(
265 zero.is_sign_positive(),
266 "DLY=0.0 must round to +0.0 (renders \"0\"), got a negative zero"
267 );
268
269 // A -0.0 input must also normalize to +0.0 (C's `NINT` yields integer 0).
270 let neg_zero = quantize_to_sleep_quantum(-0.0);
271 assert_eq!(neg_zero, 0.0);
272 assert!(
273 neg_zero.is_sign_positive(),
274 "DLY=-0.0 must round to +0.0, got a negative zero"
275 );
276
277 // A tiny positive below half a tick rounds to +0.0.
278 let tiny = quantize_to_sleep_quantum(q / 4.0);
279 assert_eq!(tiny, 0.0);
280 assert!(tiny.is_sign_positive(), "tiny +dly rounds to +0.0");
281
282 // A small negative that rounds to zero: C truncates `-0.x` to 0 → +0.0.
283 let tiny_neg = quantize_to_sleep_quantum(-q / 4.0);
284 assert_eq!(tiny_neg, 0.0);
285 assert!(
286 tiny_neg.is_sign_positive(),
287 "a -dly rounding to zero must serve +0.0, not -0.0"
288 );
289
290 // An exact quantum multiple is preserved exactly.
291 assert_eq!(quantize_to_sleep_quantum(3.0 * q), 3.0 * q);
292 assert_eq!(quantize_to_sleep_quantum(-3.0 * q), -3.0 * q);
293
294 // Round-half-away-from-zero at the tick boundary.
295 assert_eq!(quantize_to_sleep_quantum(1.5 * q), 2.0 * q);
296 assert_eq!(quantize_to_sleep_quantum(-1.5 * q), -2.0 * q);
297
298 // A huge finite `dly` overflows the `(long)` cast to i64::MIN, so the
299 // served value is `quantum * i64::MIN` — negative, and NOT `inf`.
300 let huge = quantize_to_sleep_quantum(1e300);
301 assert_eq!(huge, q * (i64::MIN as f64));
302 assert!(
303 huge.is_finite() && huge < 0.0,
304 "a huge +dly must serve C's ~-9.22e16, not inf, got {huge}"
305 );
306 // A huge negative `dly` overflows the same way.
307 let huge_neg = quantize_to_sleep_quantum(-1e300);
308 assert_eq!(huge_neg, q * (i64::MIN as f64));
309
310 // ±inf also collapses to i64::MIN (an `f64::trunc` port kept inf).
311 assert_eq!(
312 quantize_to_sleep_quantum(f64::INFINITY),
313 q * (i64::MIN as f64)
314 );
315 assert_eq!(
316 quantize_to_sleep_quantum(f64::NEG_INFINITY),
317 q * (i64::MIN as f64)
318 );
319 }
320}