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}