Skip to main content

boa_engine/context/
time.rs

1//! Clock related types and functions.
2
3use crate::sys::time::Instant;
4
5/// A monotonic instant in time, in the Boa engine.
6///
7/// This type is guaranteed to be monotonic, i.e. if two instants
8/// are compared, the later one will always be greater than the
9/// earlier one.
10///
11/// This mirrors the behavior of [`std::time::Instant`] and represents
12/// a measurement of elapsed time relative to an arbitrary starting point.
13/// It is NOT tied to wall-clock time or the Unix epoch, and system clock
14/// adjustments will not affect it.
15///
16/// This should not be used to keep dates or times, but only to
17/// measure monotonic time progression in the engine.
18#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
19pub struct JsInstant {
20    /// The duration of time since an arbitrary starting point.
21    inner: std::time::Duration,
22}
23
24impl JsInstant {
25    /// Creates a new `JsInstant` from the given number of seconds and nanoseconds.
26    #[must_use]
27    pub fn new(secs: u64, nanos: u32) -> Self {
28        let inner = std::time::Duration::new(secs, nanos);
29        Self::new_unchecked(inner)
30    }
31
32    /// Creates a new `JsInstant` from an unchecked duration since the Unix epoch.
33    #[must_use]
34    fn new_unchecked(inner: std::time::Duration) -> Self {
35        Self { inner }
36    }
37
38    /// Returns the number of milliseconds since the clock's starting point.
39    ///
40    /// Note: This is NOT a Unix timestamp. It represents elapsed time
41    /// since an arbitrary starting point and is only meaningful for
42    /// measuring durations and comparing instants.
43    #[must_use]
44    pub fn millis_since_epoch(&self) -> u64 {
45        self.inner.as_millis() as u64
46    }
47
48    /// Returns the number of nanoseconds since the clock's starting point.
49    ///
50    /// Note: This is NOT a Unix timestamp. It represents elapsed time
51    /// since an arbitrary starting point and is only meaningful for
52    /// measuring durations and comparing instants.
53    #[must_use]
54    pub fn nanos_since_epoch(&self) -> u128 {
55        self.inner.as_nanos()
56    }
57}
58
59/// A duration of time, inside the Boa engine.
60#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
61pub struct JsDuration {
62    inner: std::time::Duration,
63}
64
65impl JsDuration {
66    /// Creates a new `JsDuration` from the given number of milliseconds.
67    #[must_use]
68    pub fn from_millis(millis: u64) -> Self {
69        Self {
70            inner: std::time::Duration::from_millis(millis),
71        }
72    }
73
74    /// Returns the number of milliseconds in this duration.
75    #[must_use]
76    pub fn as_millis(&self) -> u64 {
77        self.inner.as_millis() as u64
78    }
79
80    /// Returns the number of seconds in this duration.
81    #[must_use]
82    pub fn as_secs(&self) -> u64 {
83        self.inner.as_secs()
84    }
85
86    /// Returns the number of nanoseconds in this duration.
87    #[must_use]
88    pub fn as_nanos(&self) -> u128 {
89        self.inner.as_nanos()
90    }
91}
92
93impl From<std::time::Duration> for JsDuration {
94    fn from(duration: std::time::Duration) -> Self {
95        Self { inner: duration }
96    }
97}
98
99impl From<JsDuration> for std::time::Duration {
100    fn from(duration: JsDuration) -> Self {
101        duration.inner
102    }
103}
104
105macro_rules! impl_duration_ops {
106    ($($trait:ident $trait_fn:ident),*) => {
107        $(
108            impl std::ops::$trait for JsDuration {
109                type Output = JsDuration;
110
111                #[inline]
112                fn $trait_fn(self, rhs: JsDuration) -> Self::Output {
113                    Self {
114                        inner: std::ops::$trait::$trait_fn(self.inner, rhs.inner)
115                    }
116                }
117            }
118            impl std::ops::$trait<JsDuration> for JsInstant {
119                type Output = JsInstant;
120
121                #[inline]
122                fn $trait_fn(self, rhs: JsDuration) -> Self::Output {
123                    Self {
124                        inner: std::ops::$trait::$trait_fn(self.inner, rhs.inner)
125                    }
126                }
127            }
128        )*
129    };
130}
131
132impl_duration_ops!(Add add, Sub sub);
133
134impl std::ops::Sub for JsInstant {
135    type Output = JsDuration;
136
137    #[inline]
138    fn sub(self, rhs: JsInstant) -> Self::Output {
139        JsDuration {
140            // saturating preserves the behaviour of std's Instant.
141            inner: self.inner.saturating_sub(rhs.inner),
142        }
143    }
144}
145
146/// Implement a clock that can be used to measure time.
147pub trait Clock {
148    /// Returns the current monotonic time.
149    ///
150    /// Implementers must ensure this is monotonic and should be used for measuring
151    /// durations and scheduling timeouts. The engine assumes monotonicity.
152    fn now(&self) -> JsInstant;
153
154    /// Returns the current wall-clock time in milliseconds since the Unix epoch.
155    ///
156    /// This is NOT monotonic and can go backward if the system clock is adjusted.
157    /// It should only be used for `Date` objects and other wall-clock time needs.
158    fn system_time_millis(&self) -> i64;
159}
160
161/// A clock that uses the standard monotonic clock.
162///
163/// This clock is based on the `instant` crate's `Instant` type, which provides
164/// cross-platform monotonic time, including WASM support via `performance.now()`.
165/// Time measurements are relative to an arbitrary starting point
166/// (the first call to `now()`) and are not affected by system clock adjustments.
167///
168/// This ensures that time never goes backward, which is critical for
169/// maintaining the invariants of [`JsInstant`].
170#[derive(Debug, Clone, Copy)]
171pub struct StdClock {
172    /// The base instant from which all measurements are relative.
173    base: Instant,
174}
175
176impl Default for StdClock {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182impl StdClock {
183    /// Creates a new `StdClock` with the current instant as the base.
184    #[must_use]
185    pub fn new() -> Self {
186        Self {
187            base: Instant::now(),
188        }
189    }
190}
191
192impl Clock for StdClock {
193    fn now(&self) -> JsInstant {
194        let elapsed = self.base.elapsed();
195        JsInstant::new_unchecked(elapsed)
196    }
197
198    fn system_time_millis(&self) -> i64 {
199        let now = crate::sys::time::SystemTime::now();
200        let duration = now
201            .duration_since(crate::sys::time::UNIX_EPOCH)
202            .expect("System clock is before Unix epoch");
203        duration.as_millis() as i64
204    }
205}
206
207/// A clock that uses a fixed time, useful for testing. The internal time is in milliseconds.
208///
209/// This clock will always return the same time, unless it is moved forward manually. It cannot
210/// be moved backward or set to a specific time.
211#[derive(Debug, Clone, Default)]
212pub struct FixedClock(std::cell::RefCell<u64>);
213
214impl FixedClock {
215    /// Creates a new `FixedClock` from the given number of milliseconds since the Unix epoch.
216    #[must_use]
217    pub fn from_millis(millis: u64) -> Self {
218        Self(std::cell::RefCell::new(millis))
219    }
220
221    /// Move the clock forward by the given number of milliseconds.
222    pub fn forward(&self, millis: u64) {
223        *self.0.borrow_mut() += millis;
224    }
225}
226
227impl Clock for FixedClock {
228    fn now(&self) -> JsInstant {
229        let millis = *self.0.borrow();
230        JsInstant::new_unchecked(std::time::Duration::new(
231            millis / 1000,
232            ((millis % 1000) * 1_000_000) as u32,
233        ))
234    }
235
236    fn system_time_millis(&self) -> i64 {
237        *self.0.borrow() as i64
238    }
239}
240
241#[test]
242fn basic() {
243    let clock = StdClock::new();
244    let now = clock.now();
245    // Since we're using a relative clock, values are always >= 0 by type
246    let _millis = now.millis_since_epoch();
247    let _nanos = now.nanos_since_epoch();
248
249    let duration = JsDuration::from_millis(1000);
250    let later = now + duration;
251    assert!(later > now);
252
253    // Only subtract if we have enough time elapsed
254    let duration_small = JsDuration::from_millis(100);
255    let later_small = now + duration_small;
256    let earlier = later_small - duration_small;
257    assert_eq!(earlier, now);
258
259    let diff = later - now;
260    assert_eq!(diff.as_millis(), 1000);
261
262    let fixed = FixedClock::from_millis(0);
263    let now2 = fixed.now();
264    assert_eq!(now2.millis_since_epoch(), 0);
265
266    fixed.forward(1000);
267    let now3 = fixed.now();
268    assert_eq!(now3.millis_since_epoch(), 1000);
269    assert!(now3 > now2);
270
271    // End of time.
272    fixed.forward(u64::MAX - 1000);
273    let now4 = fixed.now();
274    assert_eq!(now4.millis_since_epoch(), u64::MAX);
275    assert!(now4 > now3);
276}
277
278#[test]
279fn monotonic_behavior() {
280    let clock = StdClock::new();
281
282    // Verify that time always moves forward
283    let t1 = clock.now();
284    std::thread::sleep(std::time::Duration::from_millis(1));
285    let t2 = clock.now();
286    std::thread::sleep(std::time::Duration::from_millis(1));
287    let t3 = clock.now();
288
289    // Time must always increase
290    assert!(t2 > t1, "Time must move forward");
291    assert!(t3 > t2, "Time must continue moving forward");
292    assert!(t3 > t1, "Time must be transitive");
293
294    // Verify that elapsed time is reasonable
295    let elapsed = t3 - t1;
296    assert!(elapsed.as_millis() >= 2, "At least 2ms should have elapsed");
297}
298
299#[test]
300fn clock_independence() {
301    // Each clock instance has its own base instant
302    let clock1 = StdClock::new();
303    std::thread::sleep(std::time::Duration::from_millis(10));
304    let clock2 = StdClock::new();
305
306    let t1 = clock1.now();
307    let t2 = clock2.now();
308
309    // clock1 started earlier, so it should show more elapsed time
310    assert!(t1.millis_since_epoch() >= t2.millis_since_epoch());
311}