Skip to main content

rama_utils/
time.rs

1//! Time utilities providing a high-performance, cached wall clock.
2
3use std::sync::LazyLock;
4use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use tokio::time::Instant;
8
9/// Frequency at which we resync the cached wall clock with the system clock.
10const RESYNC_EVERY_MS: u64 = 60 * 60 * 1000;
11
12struct State {
13    start_instant: Instant,
14
15    /// Difference between wall clock unix ms and monotonic elapsed ms.
16    ///
17    /// Stored in a single atomic so readers observe a consistent snapshot.
18    skew_ms: AtomicI64,
19
20    /// Next eligible resync time expressed as elapsed ms since `start_instant`.
21    next_resync_elapsed_ms: AtomicU64,
22}
23
24static STATE: LazyLock<State> = LazyLock::new(State::init);
25
26impl State {
27    fn init() -> Self {
28        let start_instant = Instant::now();
29        let unix_ms = unix_timestamp_millis_slow();
30        Self {
31            start_instant,
32            skew_ms: AtomicI64::new(unix_ms),
33            next_resync_elapsed_ms: AtomicU64::new(RESYNC_EVERY_MS),
34        }
35    }
36
37    #[inline]
38    fn elapsed_ms_now(&self) -> u64 {
39        self.start_instant.elapsed().as_millis() as u64
40    }
41
42    #[inline]
43    fn elapsed_nanos(&self) -> u64 {
44        self.start_instant.elapsed().as_nanos() as u64
45    }
46
47    #[inline]
48    fn instant_from_nanos(&self, nanos: u64) -> Instant {
49        self.start_instant + Duration::from_nanos(nanos)
50    }
51
52    fn maybe_resync(&self, elapsed_now_ms: u64) {
53        let next = self.next_resync_elapsed_ms.load(Ordering::Relaxed);
54        if elapsed_now_ms < next {
55            return;
56        }
57
58        let new_next = elapsed_now_ms.saturating_add(RESYNC_EVERY_MS);
59
60        // Best effort. If multiple threads race, only one wins the right to resync.
61        if self
62            .next_resync_elapsed_ms
63            .compare_exchange(next, new_next, Ordering::Relaxed, Ordering::Relaxed)
64            .is_err()
65        {
66            return;
67        }
68
69        let unix_now_ms = unix_timestamp_millis_slow();
70        let new_skew = unix_now_ms - elapsed_now_ms as i64;
71        self.skew_ms.store(new_skew, Ordering::Relaxed);
72    }
73
74    fn now_unix_ms(&self) -> i64 {
75        let elapsed_now_ms = self.elapsed_ms_now();
76        self.maybe_resync(elapsed_now_ms);
77
78        let skew = self.skew_ms.load(Ordering::Relaxed);
79        elapsed_now_ms as i64 + skew
80    }
81}
82
83#[inline(always)]
84/// Returns the current unix timestamp in seconds by reading the system clock.
85pub fn unix_timestamp_secs() -> i64 {
86    unix_timestamp_secs_from_millis(unix_timestamp_millis())
87}
88
89#[inline(always)]
90const fn unix_timestamp_secs_from_millis(ms: i64) -> i64 {
91    ms.div_euclid(1000)
92}
93
94#[inline(always)]
95/// Returns the current unix timestamp in milliseconds by reading the system clock.
96pub fn unix_timestamp_millis() -> i64 {
97    unix_timestamp_millis_slow()
98}
99
100fn unix_timestamp_millis_slow() -> i64 {
101    match SystemTime::now().duration_since(UNIX_EPOCH) {
102        Ok(d) => d.as_millis().try_into().unwrap_or(i64::MAX),
103        Err(e) => {
104            let ms: i64 = e.duration().as_millis().try_into().unwrap_or(i64::MAX);
105            -ms
106        }
107    }
108}
109
110/// Returns an approximate unix timestamp in milliseconds.
111///
112/// Optimized for high-frequency calls. The underlying wall clock skew is refreshed
113/// roughly once per hour.
114pub fn now_unix_ms() -> i64 {
115    STATE.now_unix_ms()
116}
117
118/// Returns monotonic nanoseconds elapsed since a fixed process epoch.
119///
120/// Suitable for cheap, lock-free recency/ordering comparisons (e.g. via
121/// [`AtomicInstant`]). This is not wall-clock time; use [`now_unix_ms`] for that.
122#[must_use]
123pub fn now_monotonic_nanos() -> u64 {
124    STATE.elapsed_nanos()
125}
126
127#[inline]
128/// Returns an approximate unix timestamp in seconds.
129///
130/// Optimized for high-frequency calls. The underlying wall clock skew is refreshed
131/// roughly once per hour.
132pub fn now_unix() -> i64 {
133    // floor-division so that 999 ms -> 0 s, -1 ms -> -1 s; matches the unix-seconds
134    // contract `floor(ms/1000)` even for pre-epoch (negative) timestamps.
135    now_unix_ms().div_euclid(1000)
136}
137
138#[inline]
139/// Returns an approximate system time using approximate unix timestamp.
140///
141/// Optimized for high-frequency calls. The underlying wall clock skew is refreshed
142/// roughly once per hour.
143pub fn now_system_time() -> SystemTime {
144    let ms = now_unix_ms();
145    let epoch = SystemTime::UNIX_EPOCH;
146    let elapsed_since_epoch = Duration::from_millis(ms.wrapping_abs() as u64);
147    if ms >= 0 {
148        epoch + elapsed_since_epoch
149    } else {
150        epoch - elapsed_since_epoch
151    }
152}
153
154/// Returns a rotating index in range `0..len` derived from current unix ms time.
155pub fn time_modulo_index(len: usize) -> usize {
156    if len == 0 {
157        return 0;
158    }
159
160    // Cast to u64 makes pre-epoch values wrap in a stable way for modulo.
161    (now_unix_ms() as u64 % len as u64) as usize
162}
163
164/// A monotonic instant stored in a single [`AtomicU64`] as nanoseconds since a
165/// shared process epoch (see [`now_monotonic_nanos`]).
166///
167/// Lock-free to read and update, useful for recency tracking (e.g. "least
168/// recently used") without a `Mutex<Instant>`. Reads/writes use relaxed
169/// ordering, so it is a heuristic timestamp, not a synchronization point.
170#[derive(Debug)]
171pub struct AtomicInstant(AtomicU64);
172
173impl AtomicInstant {
174    /// Create an [`AtomicInstant`] set to now.
175    #[must_use]
176    pub fn now() -> Self {
177        Self(AtomicU64::new(now_monotonic_nanos()))
178    }
179
180    /// Set this instant to now.
181    pub fn set_now(&self) {
182        self.0.store(now_monotonic_nanos(), Ordering::Relaxed);
183    }
184
185    /// Nanoseconds since the shared epoch, for cheap ordering comparisons.
186    #[must_use]
187    pub fn as_nanos(&self) -> u64 {
188        self.0.load(Ordering::Relaxed)
189    }
190
191    /// Duration elapsed since this instant (zero if it is in the future).
192    #[must_use]
193    pub fn elapsed(&self) -> Duration {
194        Duration::from_nanos(now_monotonic_nanos().saturating_sub(self.as_nanos()))
195    }
196
197    /// Convert back to a real [`Instant`].
198    #[must_use]
199    pub fn to_instant(&self) -> Instant {
200        STATE.instant_from_nanos(self.as_nanos())
201    }
202}
203
204impl Default for AtomicInstant {
205    fn default() -> Self {
206        Self::now()
207    }
208}
209
210impl From<&AtomicInstant> for Instant {
211    fn from(value: &AtomicInstant) -> Self {
212        value.to_instant()
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use std::{thread, time::Duration};
220
221    #[test]
222    fn test_progression_non_decreasing() {
223        let a = now_unix_ms();
224        thread::sleep(Duration::from_millis(2));
225        let b = now_unix_ms();
226        assert!(b >= a);
227    }
228
229    #[test]
230    fn now_unix_ms_is_close_to_system_clock() {
231        let sys = unix_timestamp_millis();
232        let approx = now_unix_ms();
233        let diff = (sys - approx).abs();
234        assert!(diff <= 1_000);
235    }
236
237    #[test]
238    fn unix_timestamp_secs_matches_system_clock() {
239        let before = unix_timestamp_millis().div_euclid(1000);
240        let actual = unix_timestamp_secs();
241        let after = unix_timestamp_millis().div_euclid(1000);
242
243        assert!((before..=after).contains(&actual));
244    }
245
246    #[test]
247    fn unix_timestamp_secs_floors_milliseconds() {
248        for (millis, seconds) in [
249            (-1_001, -2),
250            (-1_000, -1),
251            (-999, -1),
252            (-1, -1),
253            (0, 0),
254            (1, 0),
255            (999, 0),
256            (1_000, 1),
257            (1_001, 1),
258        ] {
259            assert_eq!(unix_timestamp_secs_from_millis(millis), seconds);
260        }
261    }
262
263    #[test]
264    fn test_skew_math() {
265        let state = State::init();
266        let elapsed = state.elapsed_ms_now();
267
268        // Force an artificial skew and verify the formula.
269        let target_unix = unix_timestamp_millis_slow() + 1234;
270        state
271            .skew_ms
272            .store(target_unix - elapsed as i64, Ordering::Relaxed);
273
274        let now = state.now_unix_ms();
275        assert!(now >= target_unix);
276    }
277
278    #[test]
279    fn modulo_index_zero_len() {
280        assert_eq!(time_modulo_index(0), 0);
281    }
282
283    #[test]
284    fn atomic_instant_roundtrip_and_elapsed() {
285        let t = AtomicInstant::now();
286        thread::sleep(Duration::from_millis(5));
287        assert!(t.elapsed() >= Duration::from_millis(5));
288
289        // ordering: a later instant has a larger nanos value
290        let later = AtomicInstant::now();
291        assert!(later.as_nanos() > t.as_nanos());
292
293        // converting back to an Instant agrees with the original epoch math
294        let recovered = t.to_instant();
295        assert!(recovered.elapsed() >= Duration::from_millis(5));
296
297        // set_now advances it
298        let before = t.as_nanos();
299        thread::sleep(Duration::from_millis(1));
300        t.set_now();
301        assert!(t.as_nanos() > before);
302    }
303
304    #[test]
305    fn now_unix_floor_division() {
306        // Sanity: now_unix must equal floor(now_unix_ms / 1000) and must not exceed
307        // the system clock's seconds value at the call site.
308        let sys_s = unix_timestamp_millis() / 1000;
309        let approx_s = now_unix();
310        let ms = now_unix_ms();
311        assert_eq!(approx_s, ms.div_euclid(1000));
312        // Allow a small drift either way (cached clock).
313        assert!((sys_s - approx_s).abs() <= 1);
314    }
315
316    #[test]
317    fn test_modulo_bounds() {
318        for len in 1..50 {
319            assert!(time_modulo_index(len) < len);
320        }
321    }
322}