#![expect(dead_code)]
pub mod common;
mod reader;
mod shm_header;
mod tsc;
mod writer;
use common::{CLOCK_MONOTONIC, CLOCK_REALTIME, clock_gettime_safe};
pub use reader::ShmReader;
use tsc::read_timestamp_counter_begin;
pub use writer::{ShmWrite, ShmWriter};
use bon::Builder;
use errno::Errno;
use nix::sys::time::{TimeSpec, TimeValLike};
use std::error::Error;
use std::fmt;
pub const CLOCKBOUND_SHM_DEFAULT_PATH_V0: &str = "/var/run/clockbound/shm0";
pub const CLOCKBOUND_SHM_DEFAULT_PATH_V1: &str = "/var/run/clockbound/shm1";
pub const CLOCKBOUND_SHM_CLIENT_DEFAULT_PATH: &str = CLOCKBOUND_SHM_DEFAULT_PATH_V1;
const FREE_RUNNING_GRACE_PERIOD: TimeSpec = TimeSpec::new(60, 0);
const NANOS_PER_SECOND: f64 = 1_000_000_000.0;
#[macro_export]
macro_rules! syserror {
($msg:expr) => {
Err($crate::shm::ShmError::SyscallError($msg, ::errno::errno()))
};
}
pub trait ClockBoundSnapshot {
#[expect(clippy::missing_errors_doc, reason = "todo")]
fn now(&self) -> Result<ClockBoundNowResult, ShmError>;
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ClockErrorBound {
V2(ClockErrorBoundV2),
V3(ClockErrorBoundV3),
}
impl ClockErrorBound {
pub fn as_of(&self) -> TimeSpec {
match self {
ClockErrorBound::V2(ceb) => ceb.as_of,
ClockErrorBound::V3(ceb) => ceb.as_of,
}
}
pub fn void_after(&self) -> TimeSpec {
match self {
ClockErrorBound::V2(ceb) => ceb.void_after,
ClockErrorBound::V3(ceb) => ceb.void_after,
}
}
pub fn bound_nsec(&self) -> i64 {
match self {
ClockErrorBound::V2(ceb) => ceb.bound_nsec,
ClockErrorBound::V3(ceb) => ceb.bound_nsec,
}
}
pub fn max_drift_ppb(&self) -> u32 {
match self {
ClockErrorBound::V2(ceb) => ceb.max_drift_ppb,
ClockErrorBound::V3(ceb) => ceb.max_drift_ppb,
}
}
pub fn clock_status(&self) -> ClockStatus {
match self {
ClockErrorBound::V2(ceb) => ceb.clock_status,
ClockErrorBound::V3(ceb) => ceb.clock_status,
}
}
pub fn disruption_marker(&self) -> u64 {
match self {
ClockErrorBound::V2(ceb) => ceb.disruption_marker,
ClockErrorBound::V3(ceb) => ceb.disruption_marker,
}
}
pub fn clock_disruption_support_enabled(&self) -> bool {
match self {
ClockErrorBound::V2(ceb) => ceb.clock_disruption_support_enabled,
ClockErrorBound::V3(ceb) => ceb.clock_disruption_support_enabled,
}
}
}
impl ClockBoundSnapshot for ClockErrorBound {
fn now(&self) -> Result<ClockBoundNowResult, ShmError> {
match self {
ClockErrorBound::V2(ceb) => ceb.now(),
ClockErrorBound::V3(ceb) => ceb.now(),
}
}
}
#[derive(Builder)]
#[builder(finish_fn(vis = "", name = build_internal))]
pub struct ClockErrorBoundGeneric {
#[builder(default)]
as_of_tsc: u64,
#[builder(default = TimeSpec::new(0, 0))]
as_of: TimeSpec,
#[builder(default = TimeSpec::new(0, 0))]
void_after: TimeSpec,
#[builder(default)]
bound_nsec: i64,
#[builder(default)]
period: f64,
#[builder(default)]
period_err: f64,
#[builder(default)]
disruption_marker: u64,
#[builder(default)]
max_drift_ppb: u32,
#[builder(default = ClockStatus::Unknown)]
clock_status: ClockStatus,
#[builder(default)]
clock_disruption_support_enabled: bool,
}
impl<S: clock_error_bound_generic_builder::IsComplete> ClockErrorBoundGenericBuilder<S> {
pub fn build(self, layout_version: ClockErrorBoundLayoutVersion) -> ClockErrorBound {
let ceb = self.build_internal();
match layout_version {
ClockErrorBoundLayoutVersion::V2 => ClockErrorBound::V2(ClockErrorBoundV2::new(
ceb.as_of,
ceb.void_after,
ceb.bound_nsec,
ceb.disruption_marker,
ceb.max_drift_ppb,
ceb.clock_status,
ceb.clock_disruption_support_enabled,
)),
ClockErrorBoundLayoutVersion::V3 => ClockErrorBound::V3(ClockErrorBoundV3::new(
ceb.as_of_tsc,
ceb.as_of,
ceb.void_after,
ceb.period,
ceb.period_err,
ceb.bound_nsec,
ceb.disruption_marker,
ceb.max_drift_ppb,
ceb.clock_status,
ceb.clock_disruption_support_enabled,
)),
}
}
}
#[derive(Copy, Clone)]
pub enum ClockErrorBoundLayoutVersion {
V2,
V3,
}
impl TryFrom<u8> for ClockErrorBoundLayoutVersion {
type Error = ShmError;
fn try_from(value: u8) -> Result<Self, ShmError> {
match value {
2 => Ok(ClockErrorBoundLayoutVersion::V2),
3 => Ok(ClockErrorBoundLayoutVersion::V3),
_ => Err(ShmError::SegmentVersionNotSupported(format!(
"Found version {value}",
))),
}
}
}
impl TryFrom<u16> for ClockErrorBoundLayoutVersion {
type Error = ShmError;
fn try_from(value: u16) -> Result<Self, ShmError> {
match value {
2 => Ok(ClockErrorBoundLayoutVersion::V2),
3 => Ok(ClockErrorBoundLayoutVersion::V3),
_ => Err(ShmError::SegmentVersionNotSupported(format!(
"Found version {value}",
))),
}
}
}
impl From<ClockErrorBoundLayoutVersion> for u16 {
fn from(value: ClockErrorBoundLayoutVersion) -> Self {
match value {
ClockErrorBoundLayoutVersion::V2 => 2,
ClockErrorBoundLayoutVersion::V3 => 3,
}
}
}
#[derive(PartialEq, Clone, Debug)]
pub struct ClockBoundNowResult {
pub earliest: TimeSpec,
pub latest: TimeSpec,
pub clock_status: ClockStatus,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ShmError {
SyscallError(String, Errno),
SegmentNotInitialized(String),
SegmentMalformed(String),
CausalityBreach(String),
SegmentVersionNotSupported(String),
}
impl fmt::Display for ShmError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ShmError::SyscallError(msg, errno) => {
write!(f, "Errno: {errno:?} Details: {msg}")
}
ShmError::SegmentNotInitialized(msg) => {
write!(f, "The shared memory segment is not initialized [{msg}].")
}
ShmError::SegmentMalformed(msg) => {
write!(
f,
"The shared memory segment is initialized but malformed [{msg}]."
)
}
ShmError::CausalityBreach(msg) => {
write!(
f,
"Failed causality check when comparing timestamps [{msg}]."
)
}
ShmError::SegmentVersionNotSupported(msg) => {
write!(
f,
"The shared memory segment version is not supported [{msg}]."
)
}
}
}
}
impl Error for ShmError {}
#[repr(i32)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ClockStatus {
Unknown = 0,
Synchronized = 1,
FreeRunning = 2,
Disrupted = 3,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct ClockErrorBoundV2 {
as_of: TimeSpec,
void_after: TimeSpec,
bound_nsec: i64,
disruption_marker: u64,
max_drift_ppb: u32,
clock_status: ClockStatus,
clock_disruption_support_enabled: bool,
_padding: [u8; 7],
}
impl ClockErrorBoundV2 {
pub fn new(
as_of: TimeSpec,
void_after: TimeSpec,
bound_nsec: i64,
disruption_marker: u64,
max_drift_ppb: u32,
clock_status: ClockStatus,
clock_disruption_support_enabled: bool,
) -> ClockErrorBoundV2 {
ClockErrorBoundV2 {
as_of,
void_after,
bound_nsec,
disruption_marker,
max_drift_ppb,
clock_status,
clock_disruption_support_enabled,
_padding: [0u8; 7],
}
}
#[expect(clippy::missing_errors_doc, reason = "todo")]
pub fn now(&self) -> Result<ClockBoundNowResult, ShmError> {
let real = clock_gettime_safe(CLOCK_REALTIME)?;
let mono = clock_gettime_safe(CLOCK_MONOTONIC)?;
self.compute_bound_at(real, mono)
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "todo, come back and evaluate impact"
)]
fn compute_bound_at(
&self,
real: TimeSpec,
mono: TimeSpec,
) -> Result<ClockBoundNowResult, ShmError> {
if self.max_drift_ppb >= 1_000_000_000 {
return Err(ShmError::SegmentMalformed(format!(
"max_drift_ppb too large [{}]",
self.max_drift_ppb,
)));
}
let clock_status = match self.clock_status {
ClockStatus::Unknown | ClockStatus::Disrupted => self.clock_status,
ClockStatus::Synchronized | ClockStatus::FreeRunning => {
if mono > self.void_after {
ClockStatus::Unknown
} else if mono > self.as_of + FREE_RUNNING_GRACE_PERIOD {
ClockStatus::FreeRunning
} else {
self.clock_status
}
}
};
let causality_blur = self.as_of - TimeSpec::new(0, 1000);
let duration = if mono >= self.as_of {
mono - self.as_of
} else if mono > causality_blur {
TimeSpec::new(0, 0)
} else {
return Err(ShmError::CausalityBreach(format!(
"as_of ({:?}) more recent than {:?}",
self.as_of, mono
)));
};
let duration_sec = duration.num_nanoseconds() as f64 / 1_000_000_000_f64;
let updated_bound = TimeSpec::nanoseconds(
self.bound_nsec + (duration_sec * f64::from(self.max_drift_ppb)) as i64,
);
let earliest = real - updated_bound;
let latest = real + updated_bound;
Ok(ClockBoundNowResult {
earliest,
latest,
clock_status,
})
}
}
impl ClockBoundSnapshot for ClockErrorBoundV2 {
fn now(&self) -> Result<ClockBoundNowResult, ShmError> {
let real = clock_gettime_safe(CLOCK_REALTIME)?;
let mono = clock_gettime_safe(CLOCK_MONOTONIC)?;
self.compute_bound_at(real, mono)
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct ClockErrorBoundV3 {
as_of_tsc: u64,
as_of: TimeSpec,
void_after: TimeSpec,
period_frac: u64,
period_err_frac: u64,
bound_nsec: i64,
disruption_marker: u64,
max_drift_ppb: u32,
clock_status: ClockStatus,
clock_disruption_support_enabled: bool,
period_shift: u8,
period_err_shift: u8,
_padding: [u8; 5],
}
impl ClockErrorBoundV3 {
#[allow(clippy::too_many_arguments)]
pub fn new(
as_of_tsc: u64,
as_of: TimeSpec,
void_after: TimeSpec,
period: f64,
period_err: f64,
bound_nsec: i64,
disruption_marker: u64,
max_drift_ppb: u32,
clock_status: ClockStatus,
clock_disruption_support_enabled: bool,
) -> ClockErrorBoundV3 {
let p_frac = PeriodFrac::from(period);
let p_err_frac = PeriodFrac::from(period_err);
ClockErrorBoundV3 {
as_of_tsc,
as_of,
void_after,
period_frac: p_frac.frac,
period_err_frac: p_err_frac.frac,
bound_nsec,
disruption_marker,
max_drift_ppb,
clock_status,
clock_disruption_support_enabled,
period_shift: p_frac.shift,
period_err_shift: p_err_frac.shift,
_padding: [0u8; 5],
}
}
fn period(&self) -> f64 {
f64::from(PeriodFrac {
frac: self.period_frac,
shift: self.period_shift,
})
}
fn period_err(&self) -> f64 {
f64::from(PeriodFrac {
frac: self.period_err_frac,
shift: self.period_err_shift,
})
}
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
fn compute_bound_at_tsc(&self, now_tsc: u64) -> Result<ClockBoundNowResult, ShmError> {
if self.max_drift_ppb >= 1_000_000_000 {
return Err(ShmError::SegmentMalformed(format!(
"max_drift_ppb too large: [{}]",
self.max_drift_ppb,
)));
}
let duration_tsc = now_tsc.saturating_sub(self.as_of_tsc);
let duration = duration_tsc as f64 * self.period();
let duration_nsec = duration_tsc as f64 * self.period() * NANOS_PER_SECOND;
let now = TimeSpec::nanoseconds(
self.as_of.tv_nsec()
+ NANOS_PER_SECOND as i64 * self.as_of.tv_sec()
+ duration_nsec as i64,
);
let oscillator_err_nsec = duration * f64::from(self.max_drift_ppb);
let p_estimate_err_nsec = duration_nsec * self.period_err();
let updated_bound = TimeSpec::nanoseconds(
(self.bound_nsec as f64 + oscillator_err_nsec + p_estimate_err_nsec) as i64,
);
let earliest = now - updated_bound;
let latest = now + updated_bound;
let clock_status = match self.clock_status {
ClockStatus::Unknown | ClockStatus::Disrupted => self.clock_status,
ClockStatus::Synchronized | ClockStatus::FreeRunning => {
if now > self.void_after {
ClockStatus::Unknown
} else if now > self.as_of + FREE_RUNNING_GRACE_PERIOD {
ClockStatus::FreeRunning
} else {
self.clock_status
}
}
};
Ok(ClockBoundNowResult {
earliest,
latest,
clock_status,
})
}
}
impl ClockBoundSnapshot for ClockErrorBoundV3 {
fn now(&self) -> Result<ClockBoundNowResult, ShmError> {
let now_tsc = read_timestamp_counter_begin();
self.compute_bound_at_tsc(now_tsc)
}
}
struct PeriodFrac {
frac: u64,
shift: u8,
}
impl PeriodFrac {
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
fn calculate_frac_shift(period: f64) -> u8 {
assert!(
period < 1.0,
"Cannot convert period larger than 1 second: {period}"
);
if period == 0_f64 {
return 0_u8;
}
let freq: u64 = (1.0 / period) as u64;
(64 - freq.leading_zeros() - 1) as u8
}
}
impl From<f64> for PeriodFrac {
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
fn from(value: f64) -> Self {
let shift = PeriodFrac::calculate_frac_shift(value);
let scale = 64 + i32::from(shift);
let frac = (value * 2_f64.powi(scale)) as u64;
PeriodFrac { frac, shift }
}
}
impl From<PeriodFrac> for f64 {
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
fn from(value: PeriodFrac) -> Self {
let denominator = 2_f64.powi(64 + i32::from(value.shift));
(value.frac as f64) / denominator
}
}
#[cfg(test)]
mod t_lib {
use super::*;
macro_rules! clockbound_v2 {
(($asof_tv_sec:literal, $asof_tv_nsec:literal), ($after_tv_sec:literal, $after_tv_nsec:literal)) => {
ClockErrorBoundV2::new(
TimeSpec::new($asof_tv_sec, $asof_tv_nsec), TimeSpec::new($after_tv_sec, $after_tv_nsec), 10000, 0, 1000, ClockStatus::Synchronized, true, )
};
}
#[test]
fn compute_bound_ok() {
let ceb = clockbound_v2!((0, 0), (10, 0));
let real = TimeSpec::new(2, 0);
let mono = TimeSpec::new(2, 0);
let ClockBoundNowResult {
earliest,
latest,
clock_status,
} = ceb
.compute_bound_at(real, mono)
.expect("Failed to compute bound");
assert_eq!(earliest.tv_sec(), 1);
assert_eq!(earliest.tv_nsec(), 1_000_000_000 - 12_000);
assert_eq!(latest.tv_sec(), 2);
assert_eq!(latest.tv_nsec(), 12_000);
assert_eq!(clock_status, ClockStatus::Synchronized);
}
#[test]
fn compute_bound_ok_when_real_ahead() {
let ceb = clockbound_v2!((0, 0), (10, 0));
let real = TimeSpec::new(20, 0); let mono = TimeSpec::new(4, 0);
let ClockBoundNowResult {
earliest,
latest,
clock_status,
} = ceb
.compute_bound_at(real, mono)
.expect("Failed to compute bound");
assert_eq!(earliest.tv_sec(), 19);
assert_eq!(earliest.tv_nsec(), 1_000_000_000 - 14_000);
assert_eq!(latest.tv_sec(), 20);
assert_eq!(latest.tv_nsec(), 14_000);
assert_eq!(clock_status, ClockStatus::Synchronized);
}
#[test]
fn compute_bound_force_free_running_status() {
let ceb = clockbound_v2!((0, 0), (100, 0));
let real = TimeSpec::new(61, 0);
let mono = TimeSpec::new(61, 0);
let ClockBoundNowResult {
earliest,
latest,
clock_status,
} = ceb
.compute_bound_at(real, mono)
.expect("Failed to compute bound");
assert_eq!(earliest.tv_sec(), 60);
assert_eq!(earliest.tv_nsec(), 1_000_000_000 - 71_000);
assert_eq!(latest.tv_sec(), 61);
assert_eq!(latest.tv_nsec(), 71_000);
assert_eq!(clock_status, ClockStatus::FreeRunning);
}
#[test]
fn compute_bound_unknown_status_if_expired() {
let ceb = clockbound_v2!((0, 0), (5, 0));
let real = TimeSpec::new(10, 0);
let mono = TimeSpec::new(10, 0);
let ClockBoundNowResult {
earliest,
latest,
clock_status,
} = ceb
.compute_bound_at(real, mono)
.expect("Failed to compute bound");
assert_eq!(earliest.tv_sec(), 9);
assert_eq!(earliest.tv_nsec(), 1_000_000_000 - 20_000);
assert_eq!(latest.tv_sec(), 10);
assert_eq!(latest.tv_nsec(), 20_000);
assert_eq!(clock_status, ClockStatus::Unknown);
}
#[test]
fn compute_bound_bad_drift() {
let mut ceb = clockbound_v2!((0, 0), (10, 0));
let real = TimeSpec::new(5, 0);
let mono = TimeSpec::new(5, 0);
ceb.max_drift_ppb = 2_000_000_000;
assert!(ceb.compute_bound_at(real, mono).is_err());
}
#[test]
fn compute_bound_causality_break() {
let ceb = clockbound_v2!((5, 0), (10, 0));
let real = TimeSpec::new(1, 0);
let mono = TimeSpec::new(1, 0);
let res = ceb.compute_bound_at(real, mono);
assert!(res.is_err());
}
#[test]
fn test_ceb_v3_new() {
let ceb = ClockErrorBoundV3::new(
1000, TimeSpec::new(1, 0), TimeSpec::new(10, 0), 1e-9, 1e-12, 5000, 42, 1000, ClockStatus::Synchronized, true, );
assert_eq!(ceb.as_of_tsc, 1000);
assert_eq!(ceb.as_of, TimeSpec::new(1, 0));
assert_eq!(ceb.void_after, TimeSpec::new(10, 0));
assert_eq!(ceb.bound_nsec, 5000);
assert_eq!(ceb.disruption_marker, 42);
assert_eq!(ceb.max_drift_ppb, 1000);
assert_eq!(ceb.clock_status, ClockStatus::Synchronized);
assert_eq!(ceb.clock_disruption_support_enabled, true);
let period = ceb.period();
assert!((period - 1e-9).abs() < 1e-15);
let period_err = ceb.period_err();
assert!((period_err - 1e-12).abs() < 1e-18);
}
#[test]
fn test_ceb_v3_period_conversion() {
let ceb = ClockErrorBoundV3::new(
0,
TimeSpec::new(0, 0),
TimeSpec::new(10, 0),
2.5e-9, 1e-11,
1000,
0,
1000,
ClockStatus::Synchronized,
true,
);
let period = ceb.period();
let relative_error = (period - 2.5e-9).abs() / 2.5e-9;
assert!(relative_error < 1e-10);
let period_err = ceb.period_err();
let relative_error = (period_err - 1e-11).abs() / 1e-11;
assert!(relative_error < 1e-10);
}
#[test]
fn test_v3_compute_bound_at_tsc_synchronized_status() {
let ceb = ClockErrorBoundV3::new(
1_000_000_000, TimeSpec::new(1, 0), TimeSpec::new(100, 0), 1e-9, 1e-12, 10_000, 0, 1000, ClockStatus::Synchronized,
true,
);
let now_tsc = 3_500_000_000;
let result = ceb.compute_bound_at_tsc(now_tsc).expect("Should succeed");
assert_eq!(result.earliest.tv_sec(), 3); assert_eq!(result.latest.tv_sec(), 3);
assert_eq!(result.clock_status, ClockStatus::Synchronized);
}
#[test]
fn test_period_frac_conversion_typical_periods() {
let periods = [1e-3, 1e-6, 1e-7, 1e-8, 1e-9, 2e-9, 5e-9, 1e-10];
for &period in &periods {
let frac = PeriodFrac::from(period);
let result: f64 = f64::from(frac);
assert!(result == period);
}
}
#[test]
fn test_period_frac_conversion_edge_cases() {
let small_period = 1e-25;
let frac = PeriodFrac::from(small_period);
let result: f64 = f64::from(frac);
let relative_error = (result - small_period).abs() / small_period;
assert!(relative_error < 1e-10);
let large_period = 0.1;
let frac = PeriodFrac::from(large_period);
let result: f64 = f64::from(frac);
let relative_error = (result - large_period).abs() / large_period;
assert!(relative_error < 1e-10);
}
#[test]
#[should_panic(expected = "Cannot convert period larger than 1 second")]
fn test_period_frac_conversion_panci() {
let large_period = 1.0;
let _ = PeriodFrac::from(large_period);
}
#[test]
fn test_calculate_frac_shift_typical() {
let period = 1e-9;
let shift = PeriodFrac::calculate_frac_shift(period);
assert!(shift >= 29 && shift <= 30, "shift = {}", shift);
let period = 4e-10;
let shift = PeriodFrac::calculate_frac_shift(period);
assert!(shift >= 30 && shift <= 32, "shift = {}", shift);
}
#[test]
fn test_calculate_frac_shift_zero() {
let period = 0.0;
let shift = PeriodFrac::calculate_frac_shift(period);
assert_eq!(shift, 0);
}
#[test]
fn test_zero_period_frac_conversion() {
let period = 0.0;
let frac = PeriodFrac::from(period);
assert_eq!(frac.shift, 0);
assert_eq!(frac.frac, 0);
let result: f64 = f64::from(frac);
assert_eq!(result, 0.0);
}
#[test]
fn test_precision_maintained() {
let period = 2.718281828e-9; let frac = PeriodFrac::from(period);
let result: f64 = f64::from(frac);
let relative_error = (result - period).abs() / period;
assert!(relative_error < 1e-10);
}
#[test]
fn test_frac_representation_property() {
let period = 1e-9;
let frac = PeriodFrac::from(period);
assert!(frac.frac > 0);
assert!(frac.shift > 0 && frac.shift < 64);
}
}