web_time/time/
instant.rs

1//! Re-implementation of [`std::time::Instant`].
2
3use std::ops::{Add, AddAssign, Sub, SubAssign};
4use std::time::Duration;
5
6use super::js::PERFORMANCE;
7
8#[cfg(target_feature = "atomics")]
9thread_local! {
10	static ORIGIN: f64 = PERFORMANCE.with(super::js::Performance::time_origin);
11}
12
13/// See [`std::time::Instant`].
14#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub struct Instant(Duration);
16
17impl Instant {
18	/// See [`std::time::Instant::now()`].
19	///
20	/// # Panics
21	///
22	/// This call will panic if the [`Performance` object] was not found, e.g.
23	/// calling from a [worklet].
24	///
25	/// [`Performance` object]: https://developer.mozilla.org/en-US/docs/Web/API/performance_property
26	/// [worklet]: https://developer.mozilla.org/en-US/docs/Web/API/Worklet
27	#[must_use]
28	pub fn now() -> Self {
29		let now = PERFORMANCE.with(|performance| {
30			#[cfg(target_feature = "atomics")]
31			return ORIGIN.with(|origin| performance.now() + origin);
32
33			#[cfg(not(target_feature = "atomics"))]
34			performance.now()
35		});
36
37		Self(time_stamp_to_duration(now))
38	}
39
40	/// See [`std::time::Instant::duration_since()`].
41	#[must_use]
42	pub fn duration_since(&self, earlier: Self) -> Duration {
43		self.checked_duration_since(earlier).unwrap_or_default()
44	}
45
46	/// See [`std::time::Instant::checked_duration_since()`].
47	#[must_use]
48	#[allow(clippy::missing_const_for_fn)]
49	pub fn checked_duration_since(&self, earlier: Self) -> Option<Duration> {
50		self.0.checked_sub(earlier.0)
51	}
52
53	/// See [`std::time::Instant::saturating_duration_since()`].
54	#[must_use]
55	pub fn saturating_duration_since(&self, earlier: Self) -> Duration {
56		self.checked_duration_since(earlier).unwrap_or_default()
57	}
58
59	/// See [`std::time::Instant::elapsed()`].
60	#[must_use]
61	pub fn elapsed(&self) -> Duration {
62		Self::now() - *self
63	}
64
65	/// See [`std::time::Instant::checked_add()`].
66	pub fn checked_add(&self, duration: Duration) -> Option<Self> {
67		self.0.checked_add(duration).map(Instant)
68	}
69
70	/// See [`std::time::Instant::checked_sub()`].
71	pub fn checked_sub(&self, duration: Duration) -> Option<Self> {
72		self.0.checked_sub(duration).map(Instant)
73	}
74}
75
76impl Add<Duration> for Instant {
77	type Output = Self;
78
79	/// # Panics
80	///
81	/// This function may panic if the resulting point in time cannot be
82	/// represented by the underlying data structure. See
83	/// [`Instant::checked_add`] for a version without panic.
84	fn add(self, other: Duration) -> Self {
85		self.checked_add(other)
86			.expect("overflow when adding duration to instant")
87	}
88}
89
90impl AddAssign<Duration> for Instant {
91	fn add_assign(&mut self, other: Duration) {
92		*self = *self + other;
93	}
94}
95
96impl Sub<Duration> for Instant {
97	type Output = Self;
98
99	fn sub(self, other: Duration) -> Self {
100		self.checked_sub(other)
101			.expect("overflow when subtracting duration from instant")
102	}
103}
104
105impl Sub<Self> for Instant {
106	type Output = Duration;
107
108	/// Returns the amount of time elapsed from another instant to this one,
109	/// or zero duration if that instant is later than this one.
110	fn sub(self, other: Self) -> Duration {
111		self.duration_since(other)
112	}
113}
114
115impl SubAssign<Duration> for Instant {
116	fn sub_assign(&mut self, other: Duration) {
117		*self = *self - other;
118	}
119}
120
121/// Converts a `DOMHighResTimeStamp` to a [`Duration`].
122///
123/// # Note
124///
125/// Keep in mind that like [`Duration::from_secs_f64()`] this doesn't do perfect
126/// rounding.
127#[allow(
128	clippy::as_conversions,
129	clippy::cast_possible_truncation,
130	clippy::cast_sign_loss
131)]
132fn time_stamp_to_duration(time_stamp: f64) -> Duration {
133	Duration::from_millis(time_stamp.trunc() as u64)
134		+ Duration::from_nanos((time_stamp.fract() * 1.0e6).round() as u64)
135}
136
137#[cfg(test)]
138mod test {
139	use std::time::Duration;
140
141	use rand::distributions::Uniform;
142	use rand::Rng;
143	use wasm_bindgen_test::wasm_bindgen_test;
144
145	wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
146
147	// According to <https://www.w3.org/TR/2023/WD-hr-time-3-20230719/#introduction>.
148	const MAXIMUM_ACCURATE_SECS: u64 = 285_616 * 365 * 24 * 60 * 60;
149	#[allow(clippy::as_conversions, clippy::cast_precision_loss)]
150	const MAXIMUM_ACCURATE_MILLIS: f64 = MAXIMUM_ACCURATE_SECS as f64 * 1_000.;
151
152	#[derive(Debug)]
153	struct ControlDuration(Duration);
154
155	impl ControlDuration {
156		fn new(time_stamp: f64) -> Self {
157			// See <https://doc.rust-lang.org/1.73.0/src/core/time.rs.html#657-668>.
158			let time_stamp = Duration::from_secs_f64(time_stamp);
159			let secs = time_stamp.as_secs() / 1000;
160			let carry = time_stamp.as_secs() - secs * 1000;
161			#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
162			let extra_nanos = (carry * 1_000_000_000 / 1000) as u32;
163			// CHANGED: Added rounding.
164			let nanos = time_stamp.subsec_micros()
165				+ u32::from(time_stamp.subsec_nanos() % 1000 > 499)
166				+ extra_nanos;
167			// CHANGED: Removed check that would fail because of the additional time added
168			// by rounding.
169			Self(Duration::new(secs, nanos))
170		}
171	}
172
173	impl PartialEq<Duration> for ControlDuration {
174		fn eq(&self, duration: &Duration) -> bool {
175			// Our control `Duration` has perfect accuracy, unlike
176			// [`super::time_stamp_to_duration()`].
177			if self.0 == *duration {
178				true
179			} else if let Some(diff) = self.0.checked_sub(*duration) {
180				diff == Duration::from_nanos(1)
181			} else {
182				false
183			}
184		}
185	}
186
187	#[wasm_bindgen_test]
188	fn sanity() {
189		#[track_caller]
190		fn assert(time_stamp: f64, result: Duration) {
191			let control = ControlDuration::new(time_stamp);
192			let duration = super::time_stamp_to_duration(time_stamp);
193
194			assert_eq!(control, result, "control and expected result are different");
195			assert_eq!(control, duration);
196		}
197
198		assert(0.000_000, Duration::ZERO);
199		assert(0.000_000_4, Duration::ZERO);
200		assert(0.000_000_5, Duration::from_nanos(1));
201		assert(0.000_001, Duration::from_nanos(1));
202		assert(0.000_001_4, Duration::from_nanos(1));
203		assert(0.000_001_5, Duration::from_nanos(2));
204		assert(0.999_999, Duration::from_nanos(999_999));
205		assert(0.999_999_4, Duration::from_nanos(999_999));
206		assert(0.999_999_5, Duration::from_millis(1));
207		assert(1., Duration::from_millis(1));
208		assert(1.000_000_4, Duration::from_millis(1));
209		assert(1.000_000_5, Duration::from_nanos(1_000_001));
210		assert(1.000_001, Duration::from_nanos(1_000_001));
211		assert(1.000_001_4, Duration::from_nanos(1_000_001));
212		assert(1.000_001_5, Duration::from_nanos(1_000_002));
213		assert(999.999_999, Duration::from_nanos(999_999_999));
214		assert(999.999_999_4, Duration::from_nanos(999_999_999));
215		assert(999.999_999_5, Duration::from_secs(1));
216		assert(1000., Duration::from_secs(1));
217		assert(1_000.000_000_4, Duration::from_secs(1));
218		assert(1_000.000_000_5, Duration::from_nanos(1_000_000_001));
219		assert(1_000.000_001, Duration::from_nanos(1_000_000_001));
220		assert(1_000.000_001_4, Duration::from_nanos(1_000_000_001));
221		assert(1_000.000_001_5, Duration::from_nanos(1_000_000_002));
222		assert(
223			MAXIMUM_ACCURATE_MILLIS,
224			Duration::from_secs(MAXIMUM_ACCURATE_SECS),
225		);
226	}
227
228	#[wasm_bindgen_test]
229	fn fuzzing() {
230		let mut random =
231			rand::thread_rng().sample_iter(Uniform::new_inclusive(0., MAXIMUM_ACCURATE_MILLIS));
232
233		for _ in 0..10_000_000 {
234			let time_stamp = random.next().unwrap();
235
236			let control = ControlDuration::new(time_stamp);
237			let duration = super::time_stamp_to_duration(time_stamp);
238
239			assert_eq!(control, duration);
240		}
241	}
242}