Skip to main content

clock_costs/
main.rs

1// Copyright (c) 2026 Mike Grier
2//! What each clock costs to read, and how finely it moves, on the machine this runs on (`WT-1.6`).
3//!
4//! ```text
5//! cargo run --release -p win-time-sys --example clock_costs
6//! ```
7//!
8//! Each clock is read in batches of consecutive reads, timed with `Instant`; the cost per read is a
9//! batch's time divided by its reads, reported as the least, the median and the most over the
10//! batches. Its step is the smallest change seen between consecutive readings that differ. Two
11//! readings from `std` are included for reference. The output is what was observed here and makes
12//! no comparison: what a reading costs depends on the processor, the hypervisor and how the
13//! performance counter is backed, and this measures only the machine it runs on.
14
15use std::fmt;
16use std::hint::black_box;
17use std::io::{self, Write as _};
18use std::time::{Duration, Instant, SystemTime};
19
20use win_time_sys::{
21    Clock, CoarseSystemClock, InterruptClock, PerformanceClock, PerformanceCounter,
22    PreciseInterruptClock, PreciseSystemClock, PreciseUnbiasedInterruptClock, TimePoint, Timeline,
23    UnbiasedInterruptClock,
24};
25
26#[cfg(test)]
27mod tests;
28
29/// Reads timed together, so the timing's own cost is spread across them.
30const READS_PER_BATCH: u32 = 10_000;
31/// Batches per clock; odd, so the median is one of them.
32const BATCHES: usize = 31;
33/// Distinct steps to see before reporting the smallest.
34const STEPS: usize = 5;
35/// How long to wait for those steps before reporting what was seen.
36const STEP_BUDGET: Duration = Duration::from_millis(250);
37
38/// One clock's observations.
39struct Row {
40    name: &'static str,
41    /// Least, median and most nanoseconds per read.
42    cost: Summary,
43    /// The smallest step seen, if the clock moved at all within the budget.
44    step: Option<Duration>,
45}
46
47/// Least, median and most of some samples.
48#[derive(Debug, PartialEq)]
49struct Summary {
50    least: f64,
51    median: f64,
52    most: f64,
53}
54
55/// The least, the median -- the lower of the two middle samples, for an even count -- and the most.
56fn summarize(samples: &mut [f64]) -> Option<Summary> {
57    samples.sort_by(f64::total_cmp);
58    Some(Summary {
59        least: *samples.first()?,
60        median: samples[(samples.len() - 1) / 2],
61        most: *samples.last()?,
62    })
63}
64
65/// The smallest positive difference between consecutive readings, in readings' own units.
66fn smallest_step(readings: &[u128]) -> Option<u128> {
67    readings
68        .windows(2)
69        .filter_map(|pair| pair[1].checked_sub(pair[0]).filter(|step| *step > 0))
70        .min()
71}
72
73/// Nanoseconds per read of `read`, over each batch.
74fn cost<R>(read: impl Fn() -> R) -> Summary {
75    for _ in 0..READS_PER_BATCH {
76        black_box(read());
77    }
78    let mut samples: Vec<f64> = (0..BATCHES)
79        .map(|_| {
80            let started = Instant::now();
81            for _ in 0..READS_PER_BATCH {
82                black_box(read());
83            }
84            started.elapsed().as_nanos() as f64 / f64::from(READS_PER_BATCH)
85        })
86        .collect();
87    summarize(&mut samples).expect("at least one batch")
88}
89
90/// The smallest step of `nanos`, a reading in nanoseconds from some fixed origin.
91fn step(nanos: impl Fn() -> u128) -> Option<Duration> {
92    let started = Instant::now();
93    let mut readings = vec![nanos()];
94    while readings.len() <= STEPS && started.elapsed() < STEP_BUDGET {
95        let next = nanos();
96        if next != *readings.last().expect("one reading") {
97            readings.push(next);
98        }
99    }
100    smallest_step(&readings)
101        .map(|step| Duration::from_nanos(u64::try_from(step).unwrap_or(u64::MAX)))
102}
103
104/// A reading of `clock`, in nanoseconds since its timeline's zero.
105fn clock_nanos<C: Clock>(clock: &C) -> u128 {
106    let since_zero = clock.now() - TimePoint::from_ticks(0);
107    since_zero.abs_duration().as_nanos()
108}
109
110fn clock_row<C: Clock>(name: &'static str, clock: C) -> Row {
111    Row {
112        name,
113        cost: cost(|| clock.now()),
114        step: step(|| clock_nanos(&clock)),
115    }
116}
117
118fn rows() -> Vec<Row> {
119    let mut rows = vec![
120        clock_row("InterruptClock", InterruptClock),
121        clock_row("PreciseInterruptClock", PreciseInterruptClock),
122        clock_row("UnbiasedInterruptClock", UnbiasedInterruptClock),
123        clock_row(
124            "PreciseUnbiasedInterruptClock",
125            PreciseUnbiasedInterruptClock,
126        ),
127        clock_row("CoarseSystemClock", CoarseSystemClock),
128        clock_row("PreciseSystemClock", PreciseSystemClock),
129        clock_row("PerformanceClock", PerformanceClock),
130    ];
131    let origin = Instant::now();
132    rows.push(Row {
133        name: "std Instant (reference)",
134        cost: cost(Instant::now),
135        step: step(|| origin.elapsed().as_nanos()),
136    });
137    rows.push(Row {
138        name: "std SystemTime (reference)",
139        cost: cost(SystemTime::now),
140        step: step(|| {
141            SystemTime::now()
142                .duration_since(SystemTime::UNIX_EPOCH)
143                .map_or(0, |since| since.as_nanos())
144        }),
145    });
146    rows
147}
148
149/// The report, written to `out`: the one place this probe produces output.
150fn report(out: &mut impl fmt::Write, frequency: u64, rows: &[Row]) -> fmt::Result {
151    writeln!(out, "win-time-sys clock costs, observed on this machine")?;
152    writeln!(out, "performance counter frequency: {frequency} Hz")?;
153    writeln!(
154        out,
155        "{READS_PER_BATCH} reads per batch, {BATCHES} batches; ns per read as least / median / most"
156    )?;
157    writeln!(out)?;
158    writeln!(
159        out,
160        "{:<32} {:>10} {:>10} {:>10}   {:>14}",
161        "clock", "least", "median", "most", "smallest step"
162    )?;
163    for row in rows {
164        let step = row
165            .step
166            .map_or_else(|| "none seen".to_owned(), |step| format!("{step:?}"));
167        writeln!(
168            out,
169            "{:<32} {:>10.1} {:>10.1} {:>10.1}   {:>14}",
170            row.name, row.cost.least, row.cost.median, row.cost.most, step
171        )?;
172    }
173    Ok(())
174}
175
176fn main() -> io::Result<()> {
177    let frequency = PerformanceCounter::ticks_per_second().get();
178    let rows = rows();
179    let mut text = String::new();
180    report(&mut text, frequency, &rows).expect("writing to a String does not fail");
181    io::stdout().lock().write_all(text.as_bytes())
182}