use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Calibration {
pub(crate) counter_to_ns: f64,
pub(crate) counter_base: u64,
pub(crate) wall_base_ns: u64,
}
const FALLBACK_COUNTER_HZ: u64 = 1_000_000_000;
pub(crate) fn calibrate() -> Calibration {
#[cfg(target_arch = "x86_64")]
{
check_invariant_tsc();
}
let freq = counter_frequency();
if freq == 0 {
eprintln!(
"ticklog: platform reported a counter frequency of 0; \
falling back to {FALLBACK_COUNTER_HZ} Hz, timestamps may be mis-scaled"
);
}
let counter_to_ns = counter_to_ns_from_freq(freq);
let wall_time = SystemTime::now();
let counter_base = raw_timestamp();
let wall_base_ns = system_time_to_nanos(wall_time);
Calibration {
counter_to_ns,
counter_base,
wall_base_ns,
}
}
fn counter_to_ns_from_freq(freq: u64) -> f64 {
let hz = if freq == 0 { FALLBACK_COUNTER_HZ } else { freq };
1_000_000_000.0 / hz as f64
}
fn system_time_to_nanos(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH)
.expect("invariant: system clock is set before the Unix epoch")
.as_nanos() as u64
}
#[inline]
pub(crate) fn ticks_to_ns(tick: u64, calib: &Calibration) -> u64 {
let delta = tick.wrapping_sub(calib.counter_base) as f64;
let ns = (delta * calib.counter_to_ns) as u64;
ns.wrapping_add(calib.wall_base_ns)
}
#[inline]
pub(crate) fn raw_timestamp() -> u64 {
#[cfg(target_arch = "x86_64")]
{
unsafe { _raw_timestamp_x86() }
}
#[cfg(target_arch = "aarch64")]
{
unsafe { _raw_timestamp_aarch64() }
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
_raw_timestamp_fallback()
}
}
#[cfg(target_arch = "x86_64")]
#[inline]
unsafe fn _raw_timestamp_x86() -> u64 {
let lo: u32;
let hi: u32;
unsafe {
core::arch::asm!(
"rdtsc",
out("eax") lo,
out("edx") hi,
options(nomem, nostack, preserves_flags),
);
}
((hi as u64) << 32) | (lo as u64)
}
#[cfg(target_arch = "x86_64")]
fn check_invariant_tsc() {
let cpuid = core::arch::x86_64::__cpuid(0x80000007);
if cpuid.edx & (1 << 8) == 0 {
eprintln!(
"ticklog: invariant TSC not detected, timestamps may drift under C-state changes"
);
}
}
#[cfg(target_arch = "x86_64")]
fn counter_frequency() -> u64 {
let cpuid = core::arch::x86_64::__cpuid_count(0x15, 0);
if let Some(freq) = tsc_freq_from_cpuid_0x15(cpuid.eax, cpuid.ebx, cpuid.ecx) {
return freq;
}
timing_calibrate_frequency()
}
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
fn tsc_freq_from_cpuid_0x15(eax: u32, ebx: u32, ecx: u32) -> Option<u64> {
let denominator = eax as u64;
let numerator = ebx as u64;
let crystal_hz = ecx as u64;
if denominator == 0 || numerator == 0 || crystal_hz == 0 {
return None;
}
Some(crystal_hz.saturating_mul(numerator) / denominator)
}
#[cfg(target_arch = "aarch64")]
#[inline]
unsafe fn _raw_timestamp_aarch64() -> u64 {
let counter: u64;
unsafe {
core::arch::asm!(
"mrs {0}, cntvct_el0",
out(reg) counter,
options(nomem, nostack, preserves_flags),
);
}
counter
}
#[cfg(target_arch = "aarch64")]
fn counter_frequency() -> u64 {
let freq: u64;
unsafe {
core::arch::asm!(
"mrs {0}, cntfrq_el0",
out(reg) freq,
options(nomem, nostack, preserves_flags),
);
}
freq
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
#[inline]
fn _raw_timestamp_fallback() -> u64 {
system_time_to_nanos(SystemTime::now())
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
fn counter_frequency() -> u64 {
1_000_000_000
}
#[cfg(target_arch = "x86_64")]
fn timing_calibrate_frequency() -> u64 {
let _warm = raw_timestamp();
let t1 = raw_timestamp();
let w1 = SystemTime::now();
std::thread::sleep(std::time::Duration::from_millis(1));
let t2 = raw_timestamp();
let w2 = SystemTime::now();
let tick_delta = t2.wrapping_sub(t1) as f64;
let wall_delta_ns = system_time_to_nanos(w2).saturating_sub(system_time_to_nanos(w1)) as f64;
if wall_delta_ns < 1.0 {
return FALLBACK_COUNTER_HZ;
}
(tick_delta * 1_000_000_000.0 / wall_delta_ns) as u64
}
const DAYS_FROM_CIVIL_EPOCH_TO_UNIX: i64 = 719_468;
fn civil_from_days(days_since_epoch: i64) -> (i32, u32, u32) {
let z = days_since_epoch + DAYS_FROM_CIVIL_EPOCH_TO_UNIX;
let era = if z >= 0 {
z / 146_097
} else {
(z - 146_096) / 146_097
};
let doe = (z - era * 146_097) as u32;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y_adj = y + i64::from(m <= 2);
(y_adj as i32, m, d)
}
fn time_from_ns(ns_since_midnight: u64) -> (u32, u32, u32, u32) {
let secs = (ns_since_midnight / 1_000_000_000) as u32;
let nanos = (ns_since_midnight % 1_000_000_000) as u32;
let hours = secs / 3600;
let mins = (secs % 3600) / 60;
let secs = secs % 60;
(hours, mins, secs, nanos)
}
const NANOS_PER_SEC: i64 = 1_000_000_000;
const SECS_PER_DAY: u64 = 86_400;
pub(crate) fn format_iso8601(ns_since_epoch: u64, timezone_offset_secs: i32, buf: &mut Vec<u8>) {
let offset_ns = (timezone_offset_secs as i64) * NANOS_PER_SEC;
let local_ns = (ns_since_epoch as i64).wrapping_add(offset_ns);
let ns_per_day = NANOS_PER_SEC * SECS_PER_DAY as i64;
let days = local_ns.div_euclid(ns_per_day);
let ns_since_midnight = local_ns.rem_euclid(ns_per_day) as u64;
let (year, month, day) = civil_from_days(days);
let (hour, min, sec, nanos) = time_from_ns(ns_since_midnight);
let mut ts_buf = [0u8; 64];
let mut pos = 0usize;
macro_rules! push_u32 {
($val:expr, $digits:expr) => {
let v = $val;
let d = $digits;
let mut i = pos + d - 1;
let mut remaining = v;
loop {
ts_buf[i] = b'0' + (remaining % 10) as u8;
if i == pos {
break;
}
remaining /= 10;
i -= 1;
}
pos += d;
};
}
macro_rules! push_char {
($ch:expr) => {
ts_buf[pos] = $ch;
pos += 1;
};
}
push_u32!(year as u32, 4);
push_char!(b'-');
push_u32!(month, 2);
push_char!(b'-');
push_u32!(day, 2);
push_char!(b'T');
push_u32!(hour, 2);
push_char!(b':');
push_u32!(min, 2);
push_char!(b':');
push_u32!(sec, 2);
push_char!(b'.');
push_u32!(nanos, 9);
if timezone_offset_secs == 0 {
push_char!(b'Z');
} else {
let (sign, abs_offset) = if timezone_offset_secs < 0 {
(b'-', (-timezone_offset_secs) as u32)
} else {
(b'+', timezone_offset_secs as u32)
};
let off_hours = abs_offset / 3600;
let off_mins = (abs_offset % 3600) / 60;
push_char!(sign);
push_u32!(off_hours, 2);
push_char!(b':');
push_u32!(off_mins, 2);
}
buf.extend_from_slice(&ts_buf[..pos]);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counter_to_ns_guards_zero_frequency() {
let ns = counter_to_ns_from_freq(0);
assert!(ns.is_finite(), "counter_to_ns must stay finite at freq=0");
assert_eq!(ns, 1_000_000_000.0 / FALLBACK_COUNTER_HZ as f64);
}
#[test]
fn counter_to_ns_uses_frequency_when_nonzero() {
assert_eq!(counter_to_ns_from_freq(2_000_000_000), 0.5);
}
#[test]
fn tsc_freq_scales_crystal_by_ratio() {
assert_eq!(
tsc_freq_from_cpuid_0x15(2, 250, 24_000_000),
Some(3_000_000_000)
);
}
#[test]
fn tsc_freq_none_when_any_register_zero() {
assert_eq!(tsc_freq_from_cpuid_0x15(0, 250, 24_000_000), None);
assert_eq!(tsc_freq_from_cpuid_0x15(2, 0, 24_000_000), None);
assert_eq!(tsc_freq_from_cpuid_0x15(2, 250, 0), None);
}
#[test]
fn civil_unix_epoch() {
let (y, m, d) = civil_from_days(0);
assert_eq!((y, m, d), (1970, 1, 1));
}
#[test]
fn civil_known_date() {
let (y, m, d) = civil_from_days(20638);
assert_eq!((y, m, d), (2026, 7, 4));
}
#[test]
fn civil_leap_day_2000() {
let (_y, m, d) = civil_from_days(11016);
assert_eq!((m, d), (2, 29));
}
#[test]
fn civil_year_2100_not_leap() {
let (y, m, d) = civil_from_days(47540);
assert_eq!((y, m, d), (2100, 2, 28));
let (y, m, d) = civil_from_days(47541);
assert_eq!((y, m, d), (2100, 3, 1));
}
#[test]
fn civil_pre_epoch() {
let (y, m, d) = civil_from_days(-1);
assert_eq!((y, m, d), (1969, 12, 31));
}
#[test]
fn time_midnight() {
let (h, m, s, ns) = time_from_ns(0);
assert_eq!((h, m, s, ns), (0, 0, 0, 0));
}
#[test]
fn time_noon() {
let ns = 12 * 3600 * 1_000_000_000u64;
let (h, m, s, ns) = time_from_ns(ns);
assert_eq!((h, m, s, ns), (12, 0, 0, 0));
}
#[test]
fn time_pre_midnight() {
let ns = 24 * 3600 * 1_000_000_000u64 - 1;
let (h, m, s, ns) = time_from_ns(ns);
assert_eq!(h, 23);
assert_eq!(m, 59);
assert_eq!(s, 59);
assert_eq!(ns, 999_999_999);
}
#[test]
fn format_utc_epoch() {
let mut buf = Vec::new();
format_iso8601(0, 0, &mut buf);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"1970-01-01T00:00:00.000000000Z"
);
}
#[test]
fn format_utc_known_timestamp() {
let mut buf = Vec::new();
let day_ns = 20_638u64 * 86_400 * 1_000_000_000;
let time_ns = 51_724u64 * 1_000_000_000 + 656_175_000;
let ts = day_ns + time_ns;
format_iso8601(ts, 0, &mut buf);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"2026-07-04T14:22:04.656175000Z"
);
}
#[test]
fn format_with_positive_offset() {
let mut buf = Vec::new();
format_iso8601(0, 8 * 3600, &mut buf);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"1970-01-01T08:00:00.000000000+08:00"
);
}
#[test]
fn format_with_negative_offset() {
let mut buf = Vec::new();
format_iso8601(0, -5 * 3600, &mut buf);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"1969-12-31T19:00:00.000000000-05:00"
);
}
#[test]
fn format_nanosecond_precision() {
let mut buf = Vec::new();
format_iso8601(1_234_567_890, 0, &mut buf);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"1970-01-01T00:00:01.234567890Z"
);
}
#[test]
fn format_offset_non_multiple_of_hour() {
let mut buf = Vec::new();
format_iso8601(0, 5 * 3600 + 30 * 60, &mut buf);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"1970-01-01T05:30:00.000000000+05:30"
);
}
#[test]
fn format_negative_offset_at_epoch() {
let mut buf = Vec::new();
format_iso8601(0, -1, &mut buf);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"1969-12-31T23:59:59.000000000-00:00"
);
}
#[test]
fn ticks_to_ns_zero_delta() {
let calib = Calibration {
counter_to_ns: 2.5,
counter_base: 100,
wall_base_ns: 1_000_000_000,
};
assert_eq!(ticks_to_ns(100, &calib), 1_000_000_000);
}
#[test]
fn ticks_to_ns_positive_delta() {
let calib = Calibration {
counter_to_ns: 2.5,
counter_base: 100,
wall_base_ns: 0,
};
assert_eq!(ticks_to_ns(104, &calib), 10);
}
#[test]
fn ticks_to_ns_wraparound() {
let calib = Calibration {
counter_to_ns: 1.0,
counter_base: u64::MAX - 10,
wall_base_ns: 0,
};
let tick = 5u64; assert_eq!(ticks_to_ns(tick, &calib), 16);
}
}