use alloc::sync::Arc;
use core::time::Duration;
use std::{
sync::{LazyLock, OnceLock},
thread::{self, JoinHandle},
time::{Instant, SystemTime},
};
use portable_atomic::{AtomicU64, Ordering};
use crate::time::{TimeSource, UNIX_EPOCH};
static GLOBAL_TICKER: LazyLock<Arc<SharedTickerInner>> = LazyLock::new(|| {
let start = Instant::now();
let system_now = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::ZERO);
#[allow(clippy::cast_possible_truncation)]
let base_system_now = system_now.as_millis() as u64;
let inner = Arc::new(SharedTickerInner {
current: AtomicU64::new(0),
handle: OnceLock::new(),
base_system_now,
});
let inner_ref = Arc::clone(&inner);
let handle = thread::spawn(move || {
let mut tick = 0;
loop {
let target = start + Duration::from_millis(tick);
let now = Instant::now();
if now < target {
thread::sleep(target - now);
}
#[allow(clippy::cast_possible_truncation)]
let now_ms = start.elapsed().as_millis() as u64;
inner_ref.current.store(now_ms, Ordering::Relaxed);
tick = now_ms + 1;
}
});
let _ = inner.handle.set(handle);
inner
});
#[derive(Debug)]
struct SharedTickerInner {
current: AtomicU64,
handle: OnceLock<JoinHandle<()>>,
base_system_now: u64,
}
#[derive(Clone, Debug)]
pub struct MonotonicClock<const N: u64 = 1> {
inner: Arc<SharedTickerInner>,
epoch_offset: u64, }
impl Default for MonotonicClock<1> {
fn default() -> Self {
Self::with_epoch(UNIX_EPOCH)
}
}
impl<const N: u64> MonotonicClock<N> {
const ASSERT_VALID_GRANULARITY: () = assert!(
N > 0,
"MonotonicClock granularity must be greater than zero"
);
pub const GRANULARITY_MILLIS: u64 = N;
#[must_use]
pub fn with_epoch(epoch: Duration) -> Self {
let () = Self::ASSERT_VALID_GRANULARITY;
let inner = Arc::clone(&GLOBAL_TICKER);
#[allow(clippy::cast_possible_truncation)]
let offset = inner
.base_system_now
.saturating_sub(epoch.as_millis() as u64);
Self {
inner,
epoch_offset: offset,
}
}
}
impl<const N: u64> TimeSource<u64> for MonotonicClock<N> {
const GRANULARITY_MILLIS: u64 = Self::GRANULARITY_MILLIS;
fn current_millis(&self) -> u64 {
let () = Self::ASSERT_VALID_GRANULARITY;
(self.epoch_offset + self.inner.current.load(Ordering::Relaxed)) / N
}
}
impl<const N: u64> TimeSource<u128> for MonotonicClock<N> {
const GRANULARITY_MILLIS: u64 = Self::GRANULARITY_MILLIS;
fn current_millis(&self) -> u128 {
u128::from(<Self as TimeSource<u64>>::current_millis(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_granularity_stays_in_milliseconds() {
let clock = MonotonicClock::default();
let _ts: u64 = <MonotonicClock as TimeSource<u64>>::current_millis(&clock);
assert_eq!(MonotonicClock::<1>::GRANULARITY_MILLIS, 1);
assert_eq!(<MonotonicClock as TimeSource<u64>>::GRANULARITY_MILLIS, 1);
}
#[test]
fn quantized_granularity_scales_current_millis() {
let millis_clock = MonotonicClock::<1>::with_epoch(UNIX_EPOCH);
let quantized_clock = MonotonicClock::<8>::with_epoch(UNIX_EPOCH);
let lower = <MonotonicClock<1> as TimeSource<u64>>::current_millis(&millis_clock);
let quantized = <MonotonicClock<8> as TimeSource<u64>>::current_millis(&quantized_clock);
let upper = <MonotonicClock<1> as TimeSource<u64>>::current_millis(&millis_clock);
assert_eq!(MonotonicClock::<8>::GRANULARITY_MILLIS, 8);
assert_eq!(
<MonotonicClock<8> as TimeSource<u64>>::GRANULARITY_MILLIS,
8
);
assert!(lower / 8 <= quantized);
assert!(quantized <= upper / 8);
}
}