Skip to main content

qframe/
uptime.rs

1//! Two monotonic clocks read together, so an application can tell time spent working from time
2//! the machine spent asleep.
3//!
4//! A wall clock cannot answer this: a time-synchronisation step moves it while nobody slept, and
5//! on the platforms where the monotonic clock keeps running through a suspend the wall clock
6//! moves by exactly the same amount, so their difference is zero. The answer is the difference
7//! between two *monotonic* clocks, one that stops while the machine sleeps and one that does not.
8//!
9//! | Platform | Stops while asleep | Keeps counting |
10//! |---|---|---|
11//! | Linux, Android | `CLOCK_MONOTONIC` | `CLOCK_BOOTTIME` |
12//!
13//! Only that pair is read here. On every other platform there is one clock, [`Uptime::elapsed`]
14//! equals [`Uptime::awake`], [`Uptime::suspended_since`] is always zero and
15//! [`Uptime::detects_suspend`] returns `false`, so an application can say "I cannot tell sleep
16//! apart on this system" instead of showing a number that is made up:
17//!
18//! - **macOS** has `CLOCK_UPTIME_RAW` and `CLOCK_MONOTONIC_RAW`, the pair that would answer this,
19//!   but reading them needs a foreign function call, and the framework forbids `unsafe`. The
20//!   dependency that provides the clocks safely ([`rustix`](https://docs.rs/rustix)) exposes no
21//!   Apple-only clock id, so the pair cannot be read from safe code today.
22//! - **Windows** has `QueryUnbiasedInterruptTime` and `QueryInterruptTime`, which again need a
23//!   foreign function call, and `rustix` is Unix only. There, `awake` is measured from the first
24//!   reading in the process rather than from boot.
25
26use std::time::Duration;
27
28/// A moment read on both monotonic clocks at once.
29///
30/// The fields are counted from a fixed point in the past — the machine's boot where the platform
31/// offers it, otherwise the first reading in this process — so a single `Uptime` is only
32/// meaningful next to another one. Keep the reading you started from and compare.
33///
34/// ```
35/// use qframe::uptime::Uptime;
36///
37/// let started = Uptime::now();
38/// // ... the application runs, and the machine may be suspended in between ...
39/// let now = Uptime::now();
40/// let worked = now.awake.saturating_sub(started.awake);
41/// let asleep = now.suspended_since(&started);
42/// assert_eq!(asleep, std::time::Duration::ZERO, "nothing slept in a test");
43/// assert!(worked < std::time::Duration::from_secs(1));
44/// ```
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub struct Uptime {
47    /// Time counted by the clock that stops while the machine is asleep.
48    pub awake: Duration,
49    /// Time counted by the clock that keeps going while the machine is asleep. Equal to `awake`
50    /// on a platform with no second clock; see [`Uptime::detects_suspend`].
51    pub elapsed: Duration,
52}
53
54impl Uptime {
55    /// Reads both clocks now.
56    #[must_use]
57    pub fn now() -> Self {
58        let (awake, elapsed) = platform::read();
59        Self { awake, elapsed }
60    }
61
62    /// How long the machine was suspended between `earlier` and this reading.
63    ///
64    /// Zero when the readings are in the wrong order, and zero on a platform that cannot tell
65    /// sleep apart.
66    #[must_use]
67    pub fn suspended_since(&self, earlier: &Self) -> Duration {
68        let elapsed = self.elapsed.saturating_sub(earlier.elapsed);
69        let awake = self.awake.saturating_sub(earlier.awake);
70        elapsed.saturating_sub(awake)
71    }
72
73    /// Whether this platform has a second clock, so [`Uptime::suspended_since`] can report sleep
74    /// at all. `true` on Linux and Android, `false` everywhere else.
75    #[must_use]
76    pub fn detects_suspend() -> bool {
77        platform::DETECTS_SUSPEND
78    }
79}
80
81/// The clocks Linux and Android offer: `CLOCK_MONOTONIC` stops while the machine sleeps,
82/// `CLOCK_BOOTTIME` does not. Both are read with `clock_gettime`, which the kernel always
83/// supports for these two ids.
84#[cfg(any(target_os = "linux", target_os = "android"))]
85mod platform {
86    use std::time::Duration;
87
88    use rustix::time::{ClockId, Timespec, clock_gettime};
89
90    pub const DETECTS_SUSPEND: bool = true;
91
92    pub fn read() -> (Duration, Duration) {
93        (duration(clock_gettime(ClockId::Monotonic)), duration(clock_gettime(ClockId::Boottime)))
94    }
95
96    /// A clock reading as a `Duration`; a negative reading, which these clocks never give, counts
97    /// as zero rather than wrapping around.
98    fn duration(time: Timespec) -> Duration {
99        let seconds = u64::try_from(time.tv_sec).unwrap_or(0);
100        let nanoseconds = u32::try_from(time.tv_nsec).unwrap_or(0).min(999_999_999);
101        Duration::new(seconds, nanoseconds)
102    }
103}
104
105/// Other Unix systems: `CLOCK_MONOTONIC` alone. macOS's `CLOCK_UPTIME_RAW` and the BSD uptime
106/// clocks are not reachable from safe code with the dependency in hand, so sleep is not reported.
107#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
108mod platform {
109    use std::time::Duration;
110
111    use rustix::time::{ClockId, clock_gettime};
112
113    pub const DETECTS_SUSPEND: bool = false;
114
115    pub fn read() -> (Duration, Duration) {
116        let time = clock_gettime(ClockId::Monotonic);
117        let seconds = u64::try_from(time.tv_sec).unwrap_or(0);
118        let nanoseconds = u32::try_from(time.tv_nsec).unwrap_or(0).min(999_999_999);
119        let awake = Duration::new(seconds, nanoseconds);
120        (awake, awake)
121    }
122}
123
124/// Everything else, Windows among it: `Instant` measured from the first reading in this process.
125/// Its behaviour across a suspend is not specified, so no sleep is reported.
126#[cfg(not(unix))]
127mod platform {
128    use std::sync::OnceLock;
129    use std::time::{Duration, Instant};
130
131    pub const DETECTS_SUSPEND: bool = false;
132
133    pub fn read() -> (Duration, Duration) {
134        static START: OnceLock<Instant> = OnceLock::new();
135        let awake = START.get_or_init(Instant::now).elapsed();
136        (awake, awake)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    fn reading(awake: u64, elapsed: u64) -> Uptime {
145        Uptime { awake: Duration::from_secs(awake), elapsed: Duration::from_secs(elapsed) }
146    }
147
148    #[test]
149    fn suspended_time_is_what_one_clock_counted_and_the_other_did_not() {
150        let started = reading(100, 100);
151        // Ten seconds of work around an hour of sleep.
152        let now = reading(110, 3_710);
153        assert_eq!(now.suspended_since(&started), Duration::from_secs(3_600));
154        assert_eq!(now.awake - started.awake, Duration::from_secs(10));
155    }
156
157    #[test]
158    fn a_platform_with_one_clock_reports_no_sleep() {
159        let started = reading(100, 100);
160        let now = reading(3_700, 3_700);
161        assert_eq!(now.suspended_since(&started), Duration::ZERO);
162    }
163
164    #[test]
165    fn readings_in_the_wrong_order_report_no_sleep() {
166        let started = reading(3_700, 3_700);
167        assert_eq!(reading(100, 100).suspended_since(&started), Duration::ZERO);
168        // A second clock that fell behind the first cannot make the sleep negative either.
169        assert_eq!(reading(200, 150).suspended_since(&reading(100, 100)), Duration::ZERO);
170    }
171
172    #[test]
173    fn the_clocks_move_forward_together_and_report_the_platform_honestly() {
174        let started = Uptime::now();
175        let mut later = Uptime::now();
176        for _ in 0..2_000 {
177            later = Uptime::now();
178        }
179        assert!(later.awake >= started.awake, "{started:?} {later:?}");
180        assert!(later.elapsed >= started.elapsed, "{started:?} {later:?}");
181        assert!(later.elapsed >= later.awake, "the clock that counts sleep cannot be behind: {later:?}");
182        assert_eq!(later.suspended_since(&started), Duration::ZERO, "no test sleeps the machine");
183        assert!(later.awake > Duration::ZERO, "the clock is running: {later:?}");
184        let linux = cfg!(any(target_os = "linux", target_os = "android"));
185        assert_eq!(Uptime::detects_suspend(), linux);
186        if !linux {
187            assert_eq!(later.elapsed, later.awake, "one clock means the two readings are the same");
188        }
189    }
190}