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}