use crate::clock::Clock;
use crate::monotonic::mono_floor;
use crate::quality::{ClockSource, SyncQuality};
use crate::system::{monotonic_nanos, wall_nanos};
use crate::timestamp::{Timestamp, WallTime};
#[cfg(all(feature = "tsc", any(target_arch = "x86_64", target_arch = "aarch64")))]
use crate::low;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CalMethod {
X86TscInvariant,
ArmCntvct,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Calibration {
pub ns_per_tick: u64,
pub frac: u32,
pub method: CalMethod,
}
impl Calibration {
#[must_use]
pub fn ticks_to_ns(&self, ticks: u64) -> u64 {
let integral = ticks.wrapping_mul(self.ns_per_tick);
#[allow(clippy::cast_possible_truncation)] let frac = ((u128::from(ticks) * u128::from(self.frac)) >> 32) as u64;
integral.wrapping_add(frac)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CalError {
NoReferenceClock,
UnsupportedCounter,
UnstableCounter,
NoProgress,
}
pub fn calibrate(reps: u32) -> Result<Calibration, CalError> {
#[cfg(all(feature = "tsc", any(target_arch = "x86_64", target_arch = "aarch64")))]
{
#[cfg(target_arch = "x86_64")]
let method = {
if !low::counter::invariant_counter_supported() {
return Err(CalError::UnsupportedCounter);
}
CalMethod::X86TscInvariant
};
#[cfg(target_arch = "aarch64")]
let method = {
match low::counter::counter_frequency_hz() {
Some(f) if (1_000..=10_000_000_000).contains(&f) => CalMethod::ArmCntvct,
_ => return Err(CalError::UnsupportedCounter),
}
};
let a = measure_once(reps)?;
let b = measure_once(reps)?;
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
if (hi - lo) * 100 > lo {
return Err(CalError::UnstableCounter);
}
Ok(split_factor(u128::midpoint(a, b), method))
}
#[cfg(not(all(feature = "tsc", any(target_arch = "x86_64", target_arch = "aarch64"))))]
{
let _ = reps;
Err(CalError::UnsupportedCounter)
}
}
#[cfg(all(feature = "tsc", any(target_arch = "x86_64", target_arch = "aarch64")))]
fn measure_once(reps: u32) -> Result<u128, CalError> {
let r0 = monotonic_nanos();
let t0 = low::counter::read();
for _ in 0..reps {
core::hint::black_box(low::counter::read());
}
let t1 = low::counter::read();
let r1 = monotonic_nanos();
let dns = u128::from(r1) - u128::from(r0);
let dticks = u128::from(t1) - u128::from(t0);
if dticks == 0 {
return Err(CalError::NoProgress);
}
if dns == 0 {
return Err(CalError::UnstableCounter);
}
Ok((dns << 32) / dticks)
}
#[cfg(all(feature = "tsc", any(target_arch = "x86_64", target_arch = "aarch64")))]
fn split_factor(factor: u128, method: CalMethod) -> Calibration {
Calibration {
#[allow(clippy::cast_possible_truncation)]
ns_per_tick: (factor >> 32) as u64,
#[allow(clippy::cast_possible_truncation)]
frac: factor as u32,
method,
}
}
#[derive(Clone, Copy, Debug)]
pub struct CalibratedClock {
cal: Option<Calibration>,
fallback_source: ClockSource,
}
impl CalibratedClock {
#[must_use]
pub fn new(reps: u32) -> Self {
match Self::try_new(reps) {
Ok(clock) => clock,
Err(_) => Self {
cal: None,
fallback_source: fallback_source(),
},
}
}
pub fn try_new(reps: u32) -> Result<Self, CalError> {
Ok(Self {
cal: Some(calibrate(reps)?),
fallback_source: fallback_source(),
})
}
#[must_use]
pub fn with_fallback() -> Self {
Self {
cal: None,
fallback_source: fallback_source(),
}
}
#[must_use]
pub fn with_calibration(cal: Calibration) -> Self {
Self {
cal: Some(cal),
fallback_source: fallback_source(),
}
}
#[must_use]
pub fn calibration(&self) -> Option<Calibration> {
self.cal
}
}
impl Clock for CalibratedClock {
fn now(&self) -> Timestamp {
let mono = match self.cal {
#[cfg(all(feature = "tsc", any(target_arch = "x86_64", target_arch = "aarch64")))]
Some(cal) => mono_floor(cal.ticks_to_ns(low::counter::read())),
_ => mono_floor(monotonic_nanos()),
};
Timestamp {
wall: WallTime::from_unix_nanos(wall_nanos()),
mono,
}
}
fn quality(&self) -> SyncQuality {
#[cfg(feature = "ptp")]
{
crate::system::cached_quality()
}
#[cfg(not(feature = "ptp"))]
{
SyncQuality::Fallback
}
}
fn source(&self) -> ClockSource {
match self.cal {
Some(Calibration {
method: CalMethod::X86TscInvariant,
..
}) => ClockSource::Tsc,
Some(Calibration {
method: CalMethod::ArmCntvct,
..
}) => ClockSource::Cntvct,
None => self.fallback_source,
}
}
}
fn fallback_source() -> ClockSource {
#[cfg(feature = "std")]
{
ClockSource::Instant
}
#[cfg(not(feature = "std"))]
{
ClockSource::LibcMono
}
}
#[cfg(test)]
mod tests {
#[cfg(not(miri))]
use super::calibrate;
use super::{CalMethod, Calibration};
#[cfg(all(feature = "tsc", any(target_arch = "x86_64", target_arch = "aarch64")))]
use super::split_factor;
#[test]
fn ticks_to_ns_is_linear_and_monotone() {
let cal = Calibration {
ns_per_tick: 0,
frac: 0x5555_5555,
method: CalMethod::X86TscInvariant,
};
let ns1 = cal.ticks_to_ns(1);
let ns9 = cal.ticks_to_ns(9);
#[allow(clippy::cast_possible_truncation)] {
assert_eq!(ns1, (0x5555_5555_u128 >> 32) as u64);
assert_eq!(ns9, ((9_u128 * 0x5555_5555) >> 32) as u64);
}
let cal10 = Calibration {
ns_per_tick: 10,
frac: 0,
method: CalMethod::ArmCntvct,
};
assert_eq!(cal10.ticks_to_ns(0), 0);
assert_eq!(cal10.ticks_to_ns(1_000), 10_000);
let mut prev = 0;
for t in [0_u64, 1, 2, 100, u64::MAX / 8] {
let ns = cal10.ticks_to_ns(t);
assert!(ns >= prev);
prev = ns;
}
}
#[cfg(all(feature = "tsc", any(target_arch = "x86_64", target_arch = "aarch64")))]
#[test]
fn split_factor_round_trips() {
let cal = split_factor((3_u128 << 32) / 10, CalMethod::X86TscInvariant);
assert_eq!(cal.ns_per_tick, 0);
assert_eq!(cal.frac, 0x4CCC_CCCC);
}
#[cfg(not(miri))]
#[test]
fn calibrate_is_repeatable_or_cleanly_refused() {
if let Ok(a) = calibrate(1_000) {
let b = calibrate(1_000).unwrap_or(a);
assert!(b.ns_per_tick > 0 || b.frac > 0);
assert!(a.ticks_to_ns(u64::MAX / 8) > 0);
}
}
#[test]
fn source_is_observable_per_construction() {
use crate::clock::Clock;
use crate::quality::ClockSource;
let fallback = super::CalibratedClock::with_fallback();
assert!(fallback.calibration().is_none());
#[cfg(feature = "std")]
assert_eq!(fallback.source(), ClockSource::Instant);
#[cfg(not(feature = "std"))]
assert_eq!(fallback.source(), ClockSource::LibcMono);
let _ = fallback.quality();
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
{
let tsc = super::CalibratedClock::with_calibration(Calibration {
ns_per_tick: 1,
frac: 0,
method: CalMethod::X86TscInvariant,
});
assert_eq!(tsc.source(), ClockSource::Tsc);
assert!(tsc.calibration().is_some());
let cnt = super::CalibratedClock::with_calibration(Calibration {
ns_per_tick: 0,
frac: 40,
method: CalMethod::ArmCntvct,
});
assert_eq!(cnt.source(), ClockSource::Cntvct);
}
}
}