Skip to main content

nv_core/
timestamp.rs

1//! Timestamp types for the NextVision runtime.
2//!
3//! Two timestamp types serve distinct purposes:
4//!
5//! - [`MonotonicTs`] — monotonic nanoseconds since feed start. Used for all
6//!   internal ordering and duration calculations. Never wall-clock.
7//! - [`WallTs`] — wall-clock microseconds since Unix epoch. Used only in
8//!   output and provenance for correlation with external systems.
9
10use std::fmt;
11use std::ops;
12use std::time;
13
14/// Monotonic timestamp — nanoseconds since feed start.
15///
16/// Used for all internal ordering, duration calculations, and temporal logic.
17/// Never derived from wall-clock time. Compare freely for ordering.
18#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct MonotonicTs(u64);
20
21impl MonotonicTs {
22    /// The zero timestamp (feed start).
23    pub const ZERO: Self = Self(0);
24
25    /// Create a monotonic timestamp from nanoseconds since feed start.
26    #[must_use]
27    pub fn from_nanos(nanos: u64) -> Self {
28        Self(nanos)
29    }
30
31    /// Returns the timestamp as nanoseconds.
32    #[must_use]
33    pub fn as_nanos(self) -> u64 {
34        self.0
35    }
36
37    /// Returns the timestamp as fractional seconds.
38    #[must_use]
39    pub fn as_secs_f64(self) -> f64 {
40        self.0 as f64 / 1_000_000_000.0
41    }
42
43    /// Compute the duration between two timestamps.
44    ///
45    /// Returns `None` if `other` is later than `self`.
46    #[must_use]
47    pub fn checked_duration_since(self, other: Self) -> Option<Duration> {
48        self.0.checked_sub(other.0).map(Duration::from_nanos)
49    }
50
51    /// Saturating subtraction — returns zero duration if `other > self`.
52    #[must_use]
53    pub fn saturating_duration_since(self, other: Self) -> Duration {
54        Duration::from_nanos(self.0.saturating_sub(other.0))
55    }
56}
57
58impl fmt::Display for MonotonicTs {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "{}ns", self.0)
61    }
62}
63
64impl fmt::Debug for MonotonicTs {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(f, "MonotonicTs({}ns)", self.0)
67    }
68}
69
70impl ops::Add<Duration> for MonotonicTs {
71    type Output = Self;
72    fn add(self, rhs: Duration) -> Self {
73        Self(self.0 + rhs.as_nanos())
74    }
75}
76
77/// Wall-clock timestamp — microseconds since Unix epoch.
78///
79/// Used only in output and provenance for external correlation.
80/// **Never use for ordering or duration calculations within the pipeline.**
81#[derive(Clone, Copy, PartialEq, Eq)]
82pub struct WallTs(i64);
83
84impl WallTs {
85    /// Create a wall-clock timestamp from microseconds since Unix epoch.
86    #[must_use]
87    pub fn from_micros(micros: i64) -> Self {
88        Self(micros)
89    }
90
91    /// Capture the current wall-clock time.
92    #[must_use]
93    pub fn now() -> Self {
94        let d = time::SystemTime::now()
95            .duration_since(time::UNIX_EPOCH)
96            .unwrap_or_default();
97        Self(d.as_micros() as i64)
98    }
99
100    /// Returns the timestamp as microseconds since Unix epoch.
101    #[must_use]
102    pub fn as_micros(self) -> i64 {
103        self.0
104    }
105}
106
107impl fmt::Display for WallTs {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{}μs", self.0)
110    }
111}
112
113impl fmt::Debug for WallTs {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(f, "WallTs({}μs)", self.0)
116    }
117}
118
119/// A duration in nanoseconds.
120///
121/// Library-defined duration type for consistency across the crate boundary.
122/// Convertible to/from [`std::time::Duration`].
123#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
124pub struct Duration(u64);
125
126impl Duration {
127    /// Zero duration.
128    pub const ZERO: Self = Self(0);
129
130    /// Create a duration from nanoseconds.
131    #[must_use]
132    pub fn from_nanos(nanos: u64) -> Self {
133        Self(nanos)
134    }
135
136    /// Create a duration from microseconds.
137    #[must_use]
138    pub fn from_micros(micros: u64) -> Self {
139        Self(micros * 1_000)
140    }
141
142    /// Create a duration from milliseconds.
143    #[must_use]
144    pub fn from_millis(millis: u64) -> Self {
145        Self(millis * 1_000_000)
146    }
147
148    /// Create a duration from seconds.
149    #[must_use]
150    pub fn from_secs(secs: u64) -> Self {
151        Self(secs * 1_000_000_000)
152    }
153
154    /// Returns the duration in nanoseconds.
155    #[must_use]
156    pub fn as_nanos(self) -> u64 {
157        self.0
158    }
159
160    /// Returns the duration as fractional seconds.
161    #[must_use]
162    pub fn as_secs_f64(self) -> f64 {
163        self.0 as f64 / 1_000_000_000.0
164    }
165}
166
167impl fmt::Display for Duration {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        if self.0 >= 1_000_000_000 {
170            write!(f, "{:.3}s", self.as_secs_f64())
171        } else if self.0 >= 1_000_000 {
172            write!(f, "{:.3}ms", self.0 as f64 / 1_000_000.0)
173        } else if self.0 >= 1_000 {
174            write!(f, "{:.1}μs", self.0 as f64 / 1_000.0)
175        } else {
176            write!(f, "{}ns", self.0)
177        }
178    }
179}
180
181impl fmt::Debug for Duration {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "Duration({}ns)", self.0)
184    }
185}
186
187impl From<std::time::Duration> for Duration {
188    fn from(d: std::time::Duration) -> Self {
189        Self(d.as_nanos() as u64)
190    }
191}
192
193impl From<Duration> for std::time::Duration {
194    fn from(d: Duration) -> Self {
195        std::time::Duration::from_nanos(d.0)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn monotonic_ordering() {
205        let a = MonotonicTs::from_nanos(100);
206        let b = MonotonicTs::from_nanos(200);
207        assert!(a < b);
208    }
209
210    #[test]
211    fn duration_conversion() {
212        let d = Duration::from_millis(42);
213        let std_d: std::time::Duration = d.into();
214        assert_eq!(std_d.as_millis(), 42);
215    }
216
217    #[test]
218    fn checked_duration_since() {
219        let a = MonotonicTs::from_nanos(300);
220        let b = MonotonicTs::from_nanos(100);
221        let d = a.checked_duration_since(b).unwrap();
222        assert_eq!(d.as_nanos(), 200);
223        assert!(b.checked_duration_since(a).is_none());
224    }
225}