suspend_time/lib.rs
1//! Suspend-time is a cross-platform monotonic clock that is suspend-unaware, written in Rust!
2//! It allows system suspension (e.g. when a user closes their laptop on windows) to not affect
3//! `Instant` durations and timeouts!
4//!
5//! Example of using [`SuspendUnawareInstant`]:
6//! ```
7//! use std::{thread, time};
8//! use suspend_time::{SuspendUnawareInstant};
9//!
10//! fn main() {
11//! // If you used std::time::Instant here and you suspend the system on windows,
12//! // it will print more than 3 seconds (circa July 2024).
13//! // With SuspendUnawareInstant this has no effect.
14//! let instant = SuspendUnawareInstant::now();
15//! let three_secs = time::Duration::from_secs(3);
16//! thread::sleep(three_secs);
17//! println!("{:#?}", instant.elapsed());
18//! }
19//! ```
20//!
21//! Example of using `suspend_time::`[`timeout`]
22//!
23//! ```
24//! use std::time::Duration;
25//!
26//! #[tokio::main]
27//! async fn main() {
28//! // If you suspend the system during main's execution, Tokio will time
29//! // out even though it only slept for 1 second. suspend_time::timeout does not.
30//! let _ = suspend_time::timeout(
31//! Duration::from_secs(2),
32//! suspend_time::sleep(Duration::from_secs(1)),
33//! ).await;
34//! }
35//! ```
36//!
37use std::{
38 error::Error,
39 fmt,
40 future::Future,
41 ops::{Add, Sub},
42 time::Duration,
43};
44
45mod platform;
46#[cfg(test)]
47mod tests;
48
49const NANOS_PER_SECOND: u32 = 1_000_000_000;
50
51/// Similar to the standard library's implementation of
52/// [`Instant`](https://doc.rust-lang.org/1.78.0/std/time/struct.Instant.html),
53/// except it is consistently unaware of system suspends across all platforms
54/// supported by this library.
55///
56/// Historically, this has been inconsistent in the standard library, with
57/// windows allowing time to pass when the system is suspended/hibernating,
58/// however unix systems do not "pass time" during system suspension. In this
59/// library, time **never passes** when the system is suspended on **any
60/// platform**.
61///
62/// This instant implementation is:
63/// - Opaque (you cannot manually create an Instant. You must call ::now())
64/// - Cross platform (windows, macOS)
65/// - Monotonic (time never goes backwards)
66/// - Suspend-unaware (when you put your computer to sleep, "time" does not pass.)
67///
68/// # Undefined behavior / Invariants
69/// 1. When polling the system clock, nanoseconds should never exceed 10^9 (the number of nanoseconds in 1 second).
70/// If this happens, we simply return zero. The standard library has a similar invariant (0 <= nanos <= 10^9), but handles it differently.
71/// 2. If an instant in the future is subtracted from an instant in the past, we return a Duration of 0.
72/// 3. If a duration is subtracted that would cause an instant to be negative, we return an instant set at 0.
73/// 4. If a duration is added to an instant that would cause the instant to exceed 2^64 seconds, we return an instant set to 0.
74///
75/// # Underlying System calls
76///
77/// The following system calls are currently being used by `now()` to find out
78/// the current time:
79///
80/// | Platform | System call |
81/// |-----------|---------------------------------------------------------|
82/// | UNIX | [clock_gettime] (CLOCK_UPTIME_RAW) |
83/// | Darwin | [clock_gettime] (CLOCK_UPTIME_RAW) |
84/// | VXWorks | [clock_gettime] (CLOCK_UPTIME_RAW) |
85/// | Windows | [QueryUnbiasedInterruptTimePrecise] |
86///
87/// [clock_gettime]: https://www.manpagez.com/man/3/clock_gettime/
88/// [QueryUnbiasedInterruptTimePrecise]:
89/// https://learn.microsoft.com/en-us/windows/win32/api/realtimeapiset/nf-realtimeapiset-queryunbiasedinterrupttimeprecise
90///
91/// Certain overflows are dependent on how the standard library implements
92/// Duration. For example, right now it is implemented as a u64 counting
93/// seconds. As such, to prevent overflow we must check if the number of seconds
94/// in two Durations exceeds the bounds of a u64. To avoid being dependent on
95/// the standard library for cases like this, we choose our own representation
96/// of time which matches the "apple" libc platform implementation.
97#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
98pub struct SuspendUnawareInstant {
99 secs: u64,
100 nanos: u32, // invariant: 0 <= self.nanos <= NANOS_PER_SECOND
101}
102
103impl SuspendUnawareInstant {
104 /// Returns an instant corresponding to "now".
105 ///
106 /// # Examples
107 ///
108 /// ```
109 /// use suspend_time::SuspendUnawareInstant;
110 ///
111 /// let now = SuspendUnawareInstant::now();
112 /// ```
113 pub fn now() -> SuspendUnawareInstant {
114 platform::now()
115 }
116
117 /// Returns the amount of system unsuspended time elapsed since this suspend
118 /// unaware instant was created, or zero duration if that this instant is in
119 /// the future.
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// use std::{thread, time};
125 /// use suspend_time::{SuspendUnawareInstant};
126 ///
127 /// let instant = SuspendUnawareInstant::now();
128 /// let one_sec = time::Duration::from_secs(1);
129 /// thread::sleep(one_sec);
130 /// assert!(instant.elapsed() >= one_sec);
131 /// ```
132 pub fn elapsed(&self) -> Duration {
133 Self::now() - *self
134 }
135}
136
137impl Sub<SuspendUnawareInstant> for SuspendUnawareInstant {
138 type Output = Duration;
139
140 fn sub(self, rhs: SuspendUnawareInstant) -> Duration {
141 if rhs > self {
142 Duration::new(0, 0)
143 } else {
144 // The following operations are guaranteed to be valid, since we confirmed self >= rhs
145 let diff_secs = self.secs - rhs.secs;
146 if rhs.nanos > self.nanos {
147 Duration::new(diff_secs - 1, NANOS_PER_SECOND + self.nanos - rhs.nanos)
148 } else {
149 Duration::new(diff_secs, self.nanos - rhs.nanos)
150 }
151 }
152 }
153}
154
155// When adding/subtracting a `Duration` to/from a SuspendUnawareInstant, we want
156// the result to be a new instant (point in time)
157
158impl Sub<Duration> for SuspendUnawareInstant {
159 type Output = SuspendUnawareInstant;
160
161 fn sub(self, rhs: Duration) -> SuspendUnawareInstant {
162 let rhs_secs = rhs.as_secs();
163 let rhs_nanos = rhs.subsec_nanos();
164
165 if self.secs.checked_sub(rhs_secs).is_none() {
166 SuspendUnawareInstant { secs: 0, nanos: 0 }
167 } else if rhs_nanos > self.nanos {
168 // Since (self.secs - rhs_secs) passed, we know that self.secs >= rhs_secs.
169 // The only case in which rhs_nanos > self.nanos is a problem is
170 // when self.secs == rhs_secs, since this will cause the instant
171 // to be "negative".
172 if self.secs == rhs_secs {
173 SuspendUnawareInstant { secs: 0, nanos: 0 }
174 } else {
175 SuspendUnawareInstant {
176 secs: self.secs - rhs_secs - 1,
177 nanos: (NANOS_PER_SECOND + self.nanos) - rhs_nanos,
178 }
179 }
180 } else {
181 SuspendUnawareInstant {
182 secs: self.secs - rhs_secs,
183 nanos: self.nanos - rhs_nanos,
184 }
185 }
186 }
187}
188
189impl Add<Duration> for SuspendUnawareInstant {
190 type Output = SuspendUnawareInstant;
191
192 #[allow(clippy::suspicious_arithmetic_impl)]
193 fn add(self, rhs: Duration) -> SuspendUnawareInstant {
194 let rhs_secs = rhs.as_secs();
195 let rhs_nanos = rhs.subsec_nanos();
196
197 if self.secs.checked_add(rhs_secs).is_none() {
198 // undefined behavior, return 0
199 SuspendUnawareInstant { secs: 0, nanos: 0 }
200 } else {
201 let nanos_carry = (self.nanos + rhs_nanos) / NANOS_PER_SECOND;
202 // very pedantic edge case where the nanos pushed us over the
203 // overflow limit. Nevertheless, we handle it.
204 if (self.secs + rhs_secs)
205 .checked_add(nanos_carry as u64)
206 .is_none()
207 {
208 SuspendUnawareInstant { secs: 0, nanos: 0 }
209 } else {
210 SuspendUnawareInstant {
211 secs: self.secs + rhs_secs + (nanos_carry as u64),
212 nanos: (self.nanos + rhs_nanos) % NANOS_PER_SECOND,
213 }
214 }
215 }
216 }
217}
218
219/// Suspend-time's equivalent of tokio's `tokio::time::error::Elapsed`.
220/// Constructing the `Elapsed` struct is impossible due to its private construct
221/// and private members. As such, we must create our own struct
222#[derive(Clone, Debug, PartialEq)]
223pub struct TimedOutError;
224
225impl fmt::Display for TimedOutError {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 write!(f, "Timed out")
228 }
229}
230
231impl Error for TimedOutError {}
232
233/// The same API as tokio::time::timeout, except it is uses on SuspendUnawareInstant for measuring time.
234pub async fn timeout<'a, F>(duration: Duration, future: F) -> Result<F::Output, TimedOutError>
235where
236 F: Future + 'a,
237{
238 tokio::select! {
239 _ = sleep(duration) => {
240 Err(TimedOutError)
241 }
242 output = future => {
243 Ok(output)
244 }
245 }
246}
247
248/// The same API as tokio::time::sleep, except it is uses on SuspendUnawareInstant for measuring time.
249pub async fn sleep(duration: Duration) {
250 let deadline = SuspendUnawareInstant::now() + duration;
251 let mut now = SuspendUnawareInstant::now();
252 while now < deadline {
253 tokio::time::sleep(deadline - now).await;
254
255 now = SuspendUnawareInstant::now();
256 }
257}