Skip to main content

recon_core/
time.rs

1//! Virtual monotonic time.
2//!
3//! Deliberately not `std::time::Instant` or any runtime's instant type: neither can be
4//! constructed at an arbitrary value, which makes a seeded, replayable run impossible.
5//!
6//! `std::time::Duration` has no such problem — it is a pure value type with no clock behind it —
7//! so it is what [`Time`] is built from. The newtype is still worth having: a `Time` is a
8//! *point* and a `Duration` is a *span*, and keeping them distinct is what stops one being
9//! passed where the other belongs.
10
11use core::ops::{Add, AddAssign, Sub};
12use core::time::Duration;
13
14/// A point in time, measured as an offset from the start of a run.
15///
16/// Monotonic by construction: the simulator only ever advances it, and a real driver derives it
17/// from a base instant captured at startup. Arithmetic saturates rather than wrapping, so
18/// monotonicity cannot be broken by overflow.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
20pub struct Time(Duration);
21
22impl Time {
23    /// The start of a run.
24    pub const ZERO: Time = Time(Duration::ZERO);
25
26    /// The latest representable time — around 584 billion years in, so useful as a
27    /// "no deadline" sentinel and unreachable in practice.
28    pub const MAX: Time = Time(Duration::MAX);
29
30    /// A time `offset` after the start of the run.
31    pub const fn from_offset(offset: Duration) -> Self {
32        Time(offset)
33    }
34
35    pub const fn from_nanos(nanos: u64) -> Self {
36        Time(Duration::from_nanos(nanos))
37    }
38
39    pub const fn from_micros(micros: u64) -> Self {
40        Time(Duration::from_micros(micros))
41    }
42
43    pub const fn from_millis(millis: u64) -> Self {
44        Time(Duration::from_millis(millis))
45    }
46
47    pub const fn from_secs(secs: u64) -> Self {
48        Time(Duration::from_secs(secs))
49    }
50
51    /// The offset from the start of the run.
52    pub const fn as_offset(self) -> Duration {
53        self.0
54    }
55
56    /// Nanoseconds since the start of the run. `u128`, because the range exceeds `u64`.
57    pub const fn as_nanos(self) -> u128 {
58        self.0.as_nanos()
59    }
60
61    /// Milliseconds since the start of the run, truncated.
62    pub const fn as_millis(self) -> u128 {
63        self.0.as_millis()
64    }
65
66    /// Time elapsed from `earlier` to `self`, saturating at zero if `earlier` is later.
67    pub fn saturating_since(self, earlier: Time) -> Duration {
68        self.0.saturating_sub(earlier.0)
69    }
70
71    /// `self` advanced by `d`, saturating at [`Time::MAX`].
72    pub fn saturating_add(self, d: Duration) -> Time {
73        Time(self.0.saturating_add(d))
74    }
75}
76
77impl Add<Duration> for Time {
78    type Output = Time;
79    fn add(self, d: Duration) -> Time {
80        self.saturating_add(d)
81    }
82}
83
84impl AddAssign<Duration> for Time {
85    fn add_assign(&mut self, d: Duration) {
86        *self = self.saturating_add(d);
87    }
88}
89
90impl Sub<Time> for Time {
91    type Output = Duration;
92    fn sub(self, earlier: Time) -> Duration {
93        self.saturating_since(earlier)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn constructed_at_an_arbitrary_value() {
103        // The property std::time::Instant cannot offer, and the reason this type exists.
104        assert_eq!(Time::from_nanos(42).as_nanos(), 42);
105        assert_eq!(Time::from_millis(3).as_nanos(), 3_000_000);
106        assert_eq!(Time::from_nanos(5_500_000).as_millis(), 5);
107        assert_eq!(Time::from_secs(2), Time::from_millis(2_000));
108        assert_eq!(Time::from_offset(Duration::from_micros(7)).as_nanos(), 7_000);
109    }
110
111    #[test]
112    fn orders_by_offset() {
113        assert!(Time::ZERO < Time::from_nanos(1));
114        assert!(Time::from_millis(2) > Time::from_millis(1));
115        assert_eq!(Time::ZERO, Time::from_nanos(0));
116        assert!(Time::MAX > Time::from_secs(u32::MAX as u64));
117
118        let mut v = [Time::from_nanos(3), Time::from_nanos(1), Time::from_nanos(2)];
119        v.sort();
120        assert_eq!(v, [Time::from_nanos(1), Time::from_nanos(2), Time::from_nanos(3)]);
121    }
122
123    #[test]
124    fn adds_a_duration() {
125        assert_eq!(Time::ZERO + Duration::from_millis(5), Time::from_millis(5));
126        assert_eq!(Time::from_nanos(10) + Duration::from_nanos(7), Time::from_nanos(17));
127
128        let mut t = Time::ZERO;
129        t += Duration::from_millis(4);
130        t += Duration::from_millis(6);
131        assert_eq!(t, Time::from_millis(10));
132    }
133
134    #[test]
135    fn subtracts_to_a_duration() {
136        assert_eq!(Time::from_millis(9) - Time::from_millis(4), Duration::from_millis(5));
137    }
138
139    #[test]
140    fn saturates_rather_than_wrapping() {
141        // Monotonicity must not be breakable by arithmetic.
142        assert_eq!(Time::MAX + Duration::from_secs(1), Time::MAX);
143        assert_eq!(Time::from_millis(1) - Time::from_millis(9), Duration::ZERO);
144        assert_eq!(Time::ZERO.saturating_add(Duration::MAX), Time::MAX);
145    }
146
147    #[test]
148    fn the_range_exceeds_a_u64_of_nanoseconds() {
149        // The ceiling a u64-of-nanos representation would have imposed was ~584 years.
150        let beyond_u64_nanos = Time::from_secs(600 * 365 * 24 * 60 * 60);
151        assert!(beyond_u64_nanos.as_nanos() > u64::MAX as u128);
152        assert!(beyond_u64_nanos < Time::MAX);
153    }
154}