use std::fmt;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::identity::TimelineId;
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[error("robot instants belong to different timelines ({left} vs {right})")]
pub struct TimelineMismatch {
pub left: TimelineId,
pub right: TimelineId,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LocalInstant {
boot_ns: u64,
}
static CLOCK_FAULTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
impl LocalInstant {
pub fn clock_faulted() -> bool {
CLOCK_FAULTED.load(std::sync::atomic::Ordering::Acquire)
}
pub fn try_now() -> Option<Self> {
match read_boot_clock_ns() {
Some(boot_ns) => Some(LocalInstant { boot_ns }),
None => {
CLOCK_FAULTED.store(true, std::sync::atomic::Ordering::Release);
None
}
}
}
pub const fn boot_ns(self) -> u64 {
self.boot_ns
}
#[doc(hidden)]
pub const fn from_boot_ns(boot_ns: u64) -> Self {
LocalInstant { boot_ns }
}
pub fn saturating_duration_since(self, earlier: LocalInstant) -> Duration {
Duration::from_nanos(self.boot_ns.saturating_sub(earlier.boot_ns))
}
pub fn saturating_add(self, delta: Duration) -> Self {
LocalInstant {
boot_ns: self
.boot_ns
.saturating_add(u64::try_from(delta.as_nanos()).unwrap_or(u64::MAX)),
}
}
pub fn reached(self, deadline: LocalInstant) -> bool {
self.boot_ns >= deadline.boot_ns
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RobotInstant {
timeline: TimelineId,
ticks: u64,
}
impl RobotInstant {
#[doc(hidden)]
pub const fn new(timeline: TimelineId, ticks: u64) -> Self {
RobotInstant { timeline, ticks }
}
pub const fn timeline(self) -> TimelineId {
self.timeline
}
pub const fn ticks(self) -> u64 {
self.ticks
}
pub fn checked_cmp(self, other: RobotInstant) -> Result<std::cmp::Ordering, TimelineMismatch> {
self.same_timeline(other)?;
Ok(self.ticks.cmp(&other.ticks))
}
pub fn duration_since(self, earlier: RobotInstant) -> Result<Duration, TimelineMismatch> {
self.same_timeline(earlier)?;
Ok(Duration::from_nanos(
self.ticks.saturating_sub(earlier.ticks),
))
}
#[must_use]
pub fn saturating_add(self, delta: Duration) -> Self {
RobotInstant {
timeline: self.timeline,
ticks: self
.ticks
.saturating_add(u64::try_from(delta.as_nanos()).unwrap_or(u64::MAX)),
}
}
fn same_timeline(self, other: RobotInstant) -> Result<(), TimelineMismatch> {
if self.timeline == other.timeline {
Ok(())
} else {
Err(TimelineMismatch {
left: self.timeline,
right: other.timeline,
})
}
}
}
impl fmt::Display for RobotInstant {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}@{}", self.timeline, self.ticks)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TimeWindow {
timeline: TimelineId,
earliest_ticks: u64,
latest_ticks: u64,
}
impl TimeWindow {
pub const fn exact(instant: RobotInstant) -> Self {
TimeWindow {
timeline: instant.timeline(),
earliest_ticks: instant.ticks(),
latest_ticks: instant.ticks(),
}
}
pub fn bounded(earliest: RobotInstant, latest: RobotInstant) -> Result<Self, TimelineMismatch> {
earliest.same_timeline(latest)?;
Ok(TimeWindow {
timeline: earliest.timeline(),
earliest_ticks: earliest.ticks().min(latest.ticks()),
latest_ticks: earliest.ticks().max(latest.ticks()),
})
}
pub const fn timeline(self) -> TimelineId {
self.timeline
}
pub const fn earliest(self) -> RobotInstant {
RobotInstant::new(self.timeline, self.earliest_ticks)
}
pub const fn latest(self) -> RobotInstant {
RobotInstant::new(self.timeline, self.latest_ticks)
}
pub const fn as_exact(self) -> Option<RobotInstant> {
if self.earliest_ticks == self.latest_ticks {
Some(RobotInstant::new(self.timeline, self.earliest_ticks))
} else {
None
}
}
pub const fn uncertainty(self) -> Duration {
Duration::from_nanos(self.latest_ticks - self.earliest_ticks)
}
pub fn uncertainty_within(self, bound: Duration) -> bool {
self.uncertainty() <= bound
}
pub fn definitely_older_than(
self,
reference: RobotInstant,
bound: Duration,
) -> Result<bool, TimelineMismatch> {
Ok(reference.duration_since(self.latest())? > bound)
}
pub fn possibly_fresh_within(
self,
reference: RobotInstant,
bound: Duration,
) -> Result<bool, TimelineMismatch> {
if self.latest().checked_cmp(reference)? == std::cmp::Ordering::Greater {
return Ok(false);
}
Ok(reference.duration_since(self.latest())? <= bound)
}
}
impl fmt::Display for TimeWindow {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.as_exact() {
Some(exact) => write!(formatter, "{exact}"),
None => write!(
formatter,
"{}@[{}..{}]",
self.timeline, self.earliest_ticks, self.latest_ticks
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CaptureStamp {
Translated(TimeWindow),
Untranslated,
}
impl CaptureStamp {
pub const fn exact(instant: RobotInstant) -> Self {
CaptureStamp::Translated(TimeWindow::exact(instant))
}
pub(crate) const fn into_window(self) -> Option<TimeWindow> {
match self {
CaptureStamp::Translated(window) => Some(window),
CaptureStamp::Untranslated => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WallTimestamp {
unix_ns: u64,
}
impl WallTimestamp {
pub fn now() -> Self {
let unix_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| u64::try_from(since.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0);
WallTimestamp { unix_ns }
}
pub const fn unix_ns(self) -> u64 {
self.unix_ns
}
}
impl fmt::Display for WallTimestamp {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}ns since the UNIX epoch", self.unix_ns)
}
}
fn read_boot_clock_ns() -> Option<u64> {
#[cfg(target_os = "linux")]
const CLOCK: libc::clockid_t = libc::CLOCK_BOOTTIME;
#[cfg(not(target_os = "linux"))]
const CLOCK: libc::clockid_t = libc::CLOCK_MONOTONIC;
let mut timespec = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
let outcome = unsafe { libc::clock_gettime(CLOCK, &raw mut timespec) };
if outcome != 0 {
return None;
}
Some(
u64::try_from(timespec.tv_sec)
.ok()?
.saturating_mul(1_000_000_000)
.saturating_add(u64::try_from(timespec.tv_nsec).ok()?),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn timeline(value: u64) -> TimelineId {
TimelineId::from_raw(value).expect("test timeline id must be nonzero")
}
fn instant(line: u64, ticks: u64) -> RobotInstant {
RobotInstant::new(timeline(line), ticks)
}
#[test]
fn local_instant_reads_a_monotonic_host_wide_domain() {
let first = LocalInstant::try_now().expect("test host clock");
let second = LocalInstant::try_now().expect("test host clock");
assert!(second >= first);
assert!(first.boot_ns() > 0, "the boot clock must be readable");
assert!(second.saturating_duration_since(first) < Duration::from_secs(1));
assert_eq!(
first.saturating_duration_since(second),
Duration::ZERO,
"a future reference saturates instead of wrapping"
);
}
#[test]
fn cross_timeline_comparison_is_a_checked_error_in_both_directions() {
let left = instant(11, 100);
let right = instant(12, 100);
assert_eq!(
left.checked_cmp(right),
Err(TimelineMismatch {
left: timeline(11),
right: timeline(12)
})
);
assert_eq!(
right.checked_cmp(left),
Err(TimelineMismatch {
left: timeline(12),
right: timeline(11)
})
);
assert!(left.duration_since(right).is_err());
assert!(right.duration_since(left).is_err());
}
#[test]
fn same_timeline_age_is_exact_and_saturates_on_a_future_reference() {
let earlier = instant(7, 100);
let later = instant(7, 350);
assert_eq!(later.duration_since(earlier), Ok(Duration::from_nanos(250)));
assert_eq!(earlier.duration_since(later), Ok(Duration::ZERO));
assert_eq!(later.checked_cmp(earlier), Ok(std::cmp::Ordering::Greater));
}
#[test]
fn an_exact_window_round_trips_and_a_bounded_one_does_not_collapse() {
let exact = TimeWindow::exact(instant(3, 500));
assert_eq!(exact.as_exact(), Some(instant(3, 500)));
assert_eq!(exact.uncertainty(), Duration::ZERO);
let bounded = TimeWindow::bounded(instant(3, 400), instant(3, 600)).unwrap();
assert_eq!(bounded.as_exact(), None);
assert_eq!(bounded.uncertainty(), Duration::from_nanos(200));
assert!(bounded.uncertainty_within(Duration::from_nanos(200)));
assert!(!bounded.uncertainty_within(Duration::from_nanos(199)));
}
#[test]
fn bounded_normalizes_reversed_bounds_and_rejects_mixed_timelines() {
let reversed = TimeWindow::bounded(instant(3, 600), instant(3, 400)).unwrap();
assert_eq!(reversed.earliest(), instant(3, 400));
assert_eq!(reversed.latest(), instant(3, 600));
assert!(TimeWindow::bounded(instant(3, 400), instant(4, 600)).is_err());
}
#[test]
fn freshness_predicates_are_conservative_at_both_ends() {
let reference = instant(1, 1_000);
let window = TimeWindow::bounded(instant(1, 400), instant(1, 600)).unwrap();
assert!(
window
.definitely_older_than(reference, Duration::from_nanos(399))
.unwrap()
);
assert!(
!window
.definitely_older_than(reference, Duration::from_nanos(400))
.unwrap()
);
assert!(
window
.possibly_fresh_within(reference, Duration::from_nanos(400))
.unwrap()
);
assert!(
!window
.possibly_fresh_within(reference, Duration::from_nanos(399))
.unwrap()
);
let wide = TimeWindow::bounded(instant(1, 0), instant(1, 950)).unwrap();
assert!(
wide.possibly_fresh_within(reference, Duration::from_nanos(100))
.unwrap()
);
let straddling = TimeWindow::bounded(instant(1, 900), instant(1, 1_100)).unwrap();
assert!(
!straddling
.possibly_fresh_within(reference, Duration::from_secs(1))
.unwrap()
);
let wholly_future = TimeWindow::bounded(instant(1, 1_001), instant(1, 1_100)).unwrap();
assert!(
!wholly_future
.possibly_fresh_within(reference, Duration::from_secs(1))
.unwrap()
);
let foreign = TimeWindow::exact(instant(2, 400));
assert!(
foreign
.definitely_older_than(reference, Duration::ZERO)
.is_err()
);
assert!(
foreign
.possibly_fresh_within(reference, Duration::ZERO)
.is_err()
);
}
}