use std::sync::OnceLock;
#[cfg(feature = "test-util")]
pub mod clock_reads {
use core::cell::Cell;
std::thread_local! {
static WALL: Cell<u64> = const { Cell::new(0) };
static MONOTONIC: Cell<u64> = const { Cell::new(0) };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Reads {
pub wall: u64,
pub monotonic: u64,
}
impl Reads {
pub fn total(&self) -> u64 {
self.wall + self.monotonic
}
}
#[inline]
pub(super) fn bump_wall() {
WALL.with(|c| c.set(c.get().saturating_add(1)));
}
#[inline]
pub(super) fn bump_monotonic() {
MONOTONIC.with(|c| c.set(c.get().saturating_add(1)));
}
pub fn snapshot() -> Reads {
Reads {
wall: WALL.with(Cell::get),
monotonic: MONOTONIC.with(Cell::get),
}
}
pub fn since(base: Reads) -> Reads {
let now = snapshot();
Reads {
wall: now.wall.saturating_sub(base.wall),
monotonic: now.monotonic.saturating_sub(base.monotonic),
}
}
}
pub trait TimeProvider: Send + Sync + 'static {
fn now_millis(&self) -> i64;
}
#[cfg(not(target_arch = "wasm32"))]
struct ChronoTimeProvider;
#[cfg(not(target_arch = "wasm32"))]
impl TimeProvider for ChronoTimeProvider {
#[allow(clippy::disallowed_methods)]
fn now_millis(&self) -> i64 {
chrono::Utc::now().timestamp_millis()
}
}
#[cfg(target_arch = "wasm32")]
struct UnsetWasmTimeProvider;
#[cfg(target_arch = "wasm32")]
impl TimeProvider for UnsetWasmTimeProvider {
fn now_millis(&self) -> i64 {
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if !WARNED.swap(true, Ordering::Relaxed) {
log::warn!(
"wacore::time: no wall-clock provider set on wasm32; returning epoch. \
Call set_time_provider() before the first timestamp."
);
}
0
}
}
static TIME_PROVIDER: OnceLock<Box<dyn TimeProvider>> = OnceLock::new();
pub fn set_time_provider(provider: impl TimeProvider) -> Result<(), &'static str> {
TIME_PROVIDER
.set(Box::new(provider))
.map_err(|_| "time provider already set")
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
pub fn now_millis() -> i64 {
#[cfg(feature = "test-util")]
clock_reads::bump_wall();
TIME_PROVIDER
.get_or_init(default_time_provider)
.now_millis()
}
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn now_millis() -> i64 {
#[cfg(feature = "test-util")]
clock_reads::bump_wall();
match TIME_PROVIDER.get() {
Some(provider) => provider.now_millis(),
None => UnsetWasmTimeProvider.now_millis(),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn default_time_provider() -> Box<dyn TimeProvider> {
Box::new(ChronoTimeProvider)
}
#[inline]
pub fn now_secs() -> i64 {
now_millis() / 1000
}
#[inline]
pub fn now_secs_u64() -> u64 {
now_secs().max(0) as u64
}
#[inline]
pub fn now_utc() -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::from_timestamp_millis(now_millis())
.expect("time provider returned out-of-range millisecond timestamp")
}
#[inline]
pub fn from_secs(ts: i64) -> Option<chrono::DateTime<chrono::Utc>> {
chrono::DateTime::from_timestamp(ts, 0)
}
#[inline]
pub fn from_secs_or_now(ts: i64) -> chrono::DateTime<chrono::Utc> {
from_secs(ts).unwrap_or_else(now_utc)
}
#[inline]
pub fn from_millis(ts: i64) -> Option<chrono::DateTime<chrono::Utc>> {
chrono::DateTime::from_timestamp_millis(ts)
}
#[inline]
pub fn from_millis_or_now(ts: i64) -> chrono::DateTime<chrono::Utc> {
from_millis(ts).unwrap_or_else(now_utc)
}
pub trait MonotonicProvider: Send + Sync + 'static {
fn now_nanos(&self) -> u64;
}
#[cfg(not(target_arch = "wasm32"))]
struct StdMonotonicProvider {
epoch: std::time::Instant,
}
#[cfg(not(target_arch = "wasm32"))]
impl StdMonotonicProvider {
#[allow(clippy::disallowed_methods)]
fn new() -> Self {
Self {
epoch: std::time::Instant::now(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl MonotonicProvider for StdMonotonicProvider {
fn now_nanos(&self) -> u64 {
self.epoch.elapsed().as_nanos().min(u64::MAX as u128) as u64
}
}
#[cfg(target_arch = "wasm32")]
struct WallDerivedMonotonicProvider {
last: portable_atomic::AtomicU64,
}
#[cfg(target_arch = "wasm32")]
impl WallDerivedMonotonicProvider {
const fn new() -> Self {
Self {
last: portable_atomic::AtomicU64::new(0),
}
}
}
#[cfg(target_arch = "wasm32")]
impl MonotonicProvider for WallDerivedMonotonicProvider {
fn now_nanos(&self) -> u64 {
use std::sync::atomic::Ordering;
let raw = (now_millis().max(0) as u64).saturating_mul(1_000_000);
let mut last = self.last.load(Ordering::Relaxed);
loop {
let next = raw.max(last);
match self
.last
.compare_exchange_weak(last, next, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => return next,
Err(observed) => last = observed,
}
}
}
}
static MONOTONIC_PROVIDER: OnceLock<Box<dyn MonotonicProvider>> = OnceLock::new();
pub fn set_monotonic_provider(provider: impl MonotonicProvider) -> Result<(), &'static str> {
MONOTONIC_PROVIDER
.set(Box::new(provider))
.map_err(|_| "monotonic provider already set")
}
#[inline]
fn now_nanos() -> u64 {
#[cfg(feature = "test-util")]
clock_reads::bump_monotonic();
MONOTONIC_PROVIDER
.get_or_init(default_monotonic_provider)
.now_nanos()
}
#[cfg(not(target_arch = "wasm32"))]
fn default_monotonic_provider() -> Box<dyn MonotonicProvider> {
Box::new(StdMonotonicProvider::new())
}
#[cfg(target_arch = "wasm32")]
fn default_monotonic_provider() -> Box<dyn MonotonicProvider> {
Box::new(WallDerivedMonotonicProvider::new())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Instant(u64);
impl Instant {
pub const ZERO: Self = Self(0);
#[inline]
pub fn now() -> Self {
Self(now_nanos())
}
#[inline]
pub fn elapsed(&self) -> std::time::Duration {
let now = now_nanos();
std::time::Duration::from_nanos(now.saturating_sub(self.0))
}
#[inline]
pub fn saturating_duration_since(&self, earlier: Instant) -> std::time::Duration {
std::time::Duration::from_nanos(self.0.saturating_sub(earlier.0))
}
}
impl std::ops::Add<std::time::Duration> for Instant {
type Output = Instant;
fn add(self, rhs: std::time::Duration) -> Self {
let rhs_nanos: u64 = rhs.as_nanos().min(u64::MAX as u128) as u64;
Self(self.0.saturating_add(rhs_nanos))
}
}
impl std::ops::Sub<std::time::Duration> for Instant {
type Output = Instant;
fn sub(self, rhs: std::time::Duration) -> Self {
let rhs_nanos: u64 = rhs.as_nanos().min(u64::MAX as u128) as u64;
Self(self.0.saturating_sub(rhs_nanos))
}
}
impl std::ops::Sub<Instant> for Instant {
type Output = std::time::Duration;
fn sub(self, rhs: Instant) -> std::time::Duration {
self.saturating_duration_since(rhs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_default_time_provider_returns_real_time() {
let ms = default_time_provider().now_millis();
assert!(
ms > 1_600_000_000_000,
"expected a post-2020 timestamp, got {ms}"
);
}
}