use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Uptime {
pub awake: Duration,
pub elapsed: Duration,
}
impl Uptime {
#[must_use]
pub fn now() -> Self {
let (awake, elapsed) = platform::read();
Self { awake, elapsed }
}
#[must_use]
pub fn suspended_since(&self, earlier: &Self) -> Duration {
let elapsed = self.elapsed.saturating_sub(earlier.elapsed);
let awake = self.awake.saturating_sub(earlier.awake);
elapsed.saturating_sub(awake)
}
#[must_use]
pub fn detects_suspend() -> bool {
platform::DETECTS_SUSPEND
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
mod platform {
use std::time::Duration;
use rustix::time::{ClockId, Timespec, clock_gettime};
pub const DETECTS_SUSPEND: bool = true;
pub fn read() -> (Duration, Duration) {
(duration(clock_gettime(ClockId::Monotonic)), duration(clock_gettime(ClockId::Boottime)))
}
fn duration(time: Timespec) -> Duration {
let seconds = u64::try_from(time.tv_sec).unwrap_or(0);
let nanoseconds = u32::try_from(time.tv_nsec).unwrap_or(0).min(999_999_999);
Duration::new(seconds, nanoseconds)
}
}
#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
mod platform {
use std::time::Duration;
use rustix::time::{ClockId, clock_gettime};
pub const DETECTS_SUSPEND: bool = false;
pub fn read() -> (Duration, Duration) {
let time = clock_gettime(ClockId::Monotonic);
let seconds = u64::try_from(time.tv_sec).unwrap_or(0);
let nanoseconds = u32::try_from(time.tv_nsec).unwrap_or(0).min(999_999_999);
let awake = Duration::new(seconds, nanoseconds);
(awake, awake)
}
}
#[cfg(not(unix))]
mod platform {
use std::sync::OnceLock;
use std::time::{Duration, Instant};
pub const DETECTS_SUSPEND: bool = false;
pub fn read() -> (Duration, Duration) {
static START: OnceLock<Instant> = OnceLock::new();
let awake = START.get_or_init(Instant::now).elapsed();
(awake, awake)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reading(awake: u64, elapsed: u64) -> Uptime {
Uptime { awake: Duration::from_secs(awake), elapsed: Duration::from_secs(elapsed) }
}
#[test]
fn suspended_time_is_what_one_clock_counted_and_the_other_did_not() {
let started = reading(100, 100);
let now = reading(110, 3_710);
assert_eq!(now.suspended_since(&started), Duration::from_secs(3_600));
assert_eq!(now.awake - started.awake, Duration::from_secs(10));
}
#[test]
fn a_platform_with_one_clock_reports_no_sleep() {
let started = reading(100, 100);
let now = reading(3_700, 3_700);
assert_eq!(now.suspended_since(&started), Duration::ZERO);
}
#[test]
fn readings_in_the_wrong_order_report_no_sleep() {
let started = reading(3_700, 3_700);
assert_eq!(reading(100, 100).suspended_since(&started), Duration::ZERO);
assert_eq!(reading(200, 150).suspended_since(&reading(100, 100)), Duration::ZERO);
}
#[test]
fn the_clocks_move_forward_together_and_report_the_platform_honestly() {
let started = Uptime::now();
let mut later = Uptime::now();
for _ in 0..2_000 {
later = Uptime::now();
}
assert!(later.awake >= started.awake, "{started:?} {later:?}");
assert!(later.elapsed >= started.elapsed, "{started:?} {later:?}");
assert!(later.elapsed >= later.awake, "the clock that counts sleep cannot be behind: {later:?}");
assert_eq!(later.suspended_since(&started), Duration::ZERO, "no test sleeps the machine");
assert!(later.awake > Duration::ZERO, "the clock is running: {later:?}");
let linux = cfg!(any(target_os = "linux", target_os = "android"));
assert_eq!(Uptime::detects_suspend(), linux);
if !linux {
assert_eq!(later.elapsed, later.awake, "one clock means the two readings are the same");
}
}
}