Skip to main content

graphrefly_core/
clock.rs

1//! Centralised timestamp utilities.
2//!
3//! Mirrors `~/src/graphrefly-ts/src/core/clock.ts` and the Python
4//! `core/clock.py`. Convention: nanoseconds, `_ns` suffix, two clock domains
5//! (monotonic for ordering, wall-clock for attribution) per CLAUDE.md
6//! "Time utility rule."
7//!
8//! # Precision
9//!
10//! - [`monotonic_ns`] — `std::time::Instant` resolves to true nanoseconds on
11//!   most modern platforms. Captured against a process-static origin (lazy via
12//!   `OnceLock`) so values fit in `u64` for ~584 years of process uptime.
13//! - [`wall_clock_ns`] — `SystemTime::now() - UNIX_EPOCH` truncated to `u64`
14//!   nanoseconds; valid until ~2554. Beyond that, returns `u64::MAX`. Pre-epoch
15//!   system clocks (rare misconfiguration) return 0.
16//!
17//! Mixing the two clocks is a bug — durations belong in monotonic; user-visible
18//! timestamps belong in wall-clock. The Core dispatcher uses monotonic
19//! exclusively.
20
21use std::sync::OnceLock;
22use std::time::{Instant, SystemTime, UNIX_EPOCH};
23
24/// Process-static monotonic origin, captured lazily on first call. All
25/// monotonic timestamps are deltas from this origin so the result fits in
26/// `u64` ns.
27static MONOTONIC_ORIGIN: OnceLock<Instant> = OnceLock::new();
28
29/// Monotonic nanosecond timestamp.
30///
31/// Use for: event ordering, durations, version counters, timeline events.
32/// Never use for: user-visible timestamps (wall-clock attribution), since
33/// monotonic time has no relation to civil time.
34///
35/// Saturates at `u64::MAX` after ~584 years of process uptime — a non-issue
36/// in practice, and avoids unwrap on the `u128 -> u64` narrowing.
37#[must_use]
38pub fn monotonic_ns() -> u64 {
39    let origin = MONOTONIC_ORIGIN.get_or_init(Instant::now);
40    let elapsed = origin.elapsed().as_nanos();
41    u64::try_from(elapsed).unwrap_or(u64::MAX)
42}
43
44/// Wall-clock nanosecond timestamp (Unix epoch).
45///
46/// Use for: user-visible timestamps, mutation attribution, cron emission.
47/// Never use for: ordering or durations within the dispatcher (use
48/// [`monotonic_ns`] — wall-clock can jump backwards on NTP correction).
49///
50/// Returns 0 for pre-epoch system clocks (misconfiguration). Saturates at
51/// `u64::MAX` past ~2554 — same reasoning as [`monotonic_ns`].
52#[must_use]
53pub fn wall_clock_ns() -> u64 {
54    SystemTime::now()
55        .duration_since(UNIX_EPOCH)
56        .map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use std::thread::sleep;
63    use std::time::Duration;
64
65    #[test]
66    fn monotonic_ns_is_nondecreasing() {
67        let a = monotonic_ns();
68        let b = monotonic_ns();
69        assert!(b >= a, "monotonic clock went backwards: {a} -> {b}");
70    }
71
72    #[test]
73    fn monotonic_ns_advances() {
74        let a = monotonic_ns();
75        sleep(Duration::from_millis(2));
76        let b = monotonic_ns();
77        // 2ms = 2_000_000ns; allow generous slack for low-resolution clocks.
78        assert!(
79            b - a >= 1_000_000,
80            "monotonic clock advanced too little: {a} -> {b}"
81        );
82    }
83
84    #[test]
85    fn wall_clock_ns_is_in_recent_epoch() {
86        // Sanity: wall clock is past 2020 (1.577e18 ns) and before 2554 (saturate).
87        let t = wall_clock_ns();
88        assert!(t > 1_577_000_000_000_000_000, "wall clock too low: {t}");
89        assert!(t < u64::MAX, "wall clock saturated: {t}");
90    }
91
92    #[test]
93    fn monotonic_and_wall_clock_are_independent() {
94        // Different clocks; values should differ in any sane environment.
95        // The point of having two functions is that they can't be conflated.
96        let m = monotonic_ns();
97        let w = wall_clock_ns();
98        assert_ne!(m, w);
99    }
100}