Introduction
t2t is a set of Rust crates for time: the points a program reads off its clocks, the spans between them, and the clocks themselves. Each timeline has a point type of its own, so a reading of the wall clock is never subtracted from a reading of the monotonic clock: the compiler refuses it.
It is for any program that needs time values or clocks: a server’s deadlines, a
log’s stamps, a benchmark’s spans, a test that moves time by hand. It is built
for code where a nanosecond counts: every point and span is one i64, the CPU’s
counter is read in one instruction, and no conversion on a reading’s or a
spelling’s path divides by a number known only at run time.
What It Holds
- Points, one per timeline:
Timestampon the wall clock,TaiTimestampon International Atomic Time,Uptime,RawUptimeandBootUptimeon the clocks that count from boot, andTickstampon the CPU’s counter. - Spans, one per kind of count:
Timedeltacounts nanoseconds andTickdeltaa counter’s ticks; aTickRateturns one into the other. - Two views:
UtcDateTimereads a timestamp as a date and a time of day, andTimedholds a value with the stamp it was captured with. - Clocks, each of which implements
Clock, whose one verb,now, gives a point on the clock’s timeline, or a span for a clock of CPU time.
A point minus a point is a span, a point plus a span is a point, and every
operator saturates at the ends of the range, with a checked_* twin. Every
value has one spelling, which Display writes and FromStr reads back.
The Crates
| Crate | What it holds |
|---|---|
t2t | everything, the values at its root and the clocks under t2t::clock |
t2t-core | the values; no_std, and never reaches the operating system |
t2t-clock | the clocks: the operating system’s, the CPU’s counter, manual clocks |
A program needs t2t alone; the other two are for a crate that wants the
values without the clocks, or the clocks without the facade.
What It Leaves Out
- Time zones beyond UTC and TAI. A timestamp is written and read in UTC, and a spelling with an offset is refused. A zone or an offset from chrono, jiff or time converts as the instant it names.
- Calendars beyond the proleptic Gregorian. A date is a day of that calendar in UTC, from 1677-09-21 to 2262-04-11, with no leap seconds, as Unix time has none.
- Formatting beyond RFC 3339 and its own spellings. There are no format patterns and no names of months or days.
How t2t Compares names the crates that do these, and Working with Other Crates shows the conversions to and from them.
How to Read This Book
The guide teaches the pieces in the order a program meets them, from a first program to testing with a manual clock; the reference holds what each path costs, what builds where, and how t2t compares with its neighbours. The API documentation, under API Reference in the menu bar, holds each item’s detail.
Every listing on these pages is part of a test that cargo builds and runs, so
the code shown compiles against the crates as they are. Most are under
crates/t2t/tests/book/; those that need serde_json or another crate’s types
are under crates/t2t-core/tests/book/, and name the crate t2t_core, whose
items the facade re-exports under the same names.
Getting Started
Add the crate, with the operating system’s clocks:
cargo add t2t --features std
The values, the CPU’s counter and the manual clocks need no feature, and build
without std; the operating system’s clocks need std, on 64-bit Linux or
macOS. t2t builds on stable Rust 1.98 or later.
Platforms and Features lists every feature and what
it adds.
A First Program
The wall clock stamps the moment, since its reading names a moment another machine can name too; the monotonic clock times the pause, since no correction to the wall clock moves it.
fn the_wall_clock_stamps_and_the_monotonic_clock_times() {
use core::time::Duration;
use std::thread;
use t2t::Timedelta;
use t2t::clock::{Clock, MonotonicClock, SystemClock};
// A stamp another machine can read: the wall clock's.
let captured = SystemClock.now();
// A start that no correction to the wall clock moves: the monotonic clock's.
let start = MonotonicClock.now();
let pause = Timedelta::from_millis(20);
thread::sleep(Duration::try_from(pause).expect("a span forwards"));
let took = MonotonicClock.now() - start;
println!("captured at {captured:.6}, then slept {took}");
assert!(took >= pause, "at least the pause: {took}");
}
Timestamp’s {:.6} writes RFC 3339 to the microsecond, and a span is written
coarsest unit first, as 20ms83us. Choosing a Clock says
which clock to read for what.
Two Tours
In a checkout of the repository, two examples walk through the rest. The first prints the values: points and spans, the calendar, a stamped value, and a counter’s rate.
cargo run -p t2t --example time-tour
The second prints every clock’s reading, and what one costs on the machine it runs on.
cargo run --release -p t2t --features std --example clock-tour
Every listing in this book runs as a test too:
cargo test -p t2t --all-features --test book
cargo test -p t2t-core --all-features --test book
From there, clock lists every clock, and the crate’s page,
t2t, says which to choose for what.
Points and Spans
A point is a moment on one timeline, and a span is how far apart two points are. Each timeline has a point type of its own, and each kind of count a span type of its own, so a sum or a difference that mixes two timelines does not compile.
A Type per Timeline
| Point | Timeline | Origin | Span |
|---|---|---|---|
Timestamp | the wall clock, in UTC | 1970-01-01T00:00:00Z | Timedelta |
TaiTimestamp | International Atomic Time | 1970-01-01T00:00:00 TAI | Timedelta |
Uptime | the monotonic clock | near boot | Timedelta |
RawUptime | the monotonic clock at the hardware’s rate | near boot | Timedelta |
BootUptime | the boot clock, suspensions counted | boot | Timedelta |
Tickstamp | a hardware counter | the hardware’s own | Tickdelta |
Every point and span is an i64 and nothing else: nanoseconds for all but the
counter’s, whose unit is its tick. Each is Copy, ordered and hashable, and its
Default is zero. A count of nanoseconds holds about 292 years either side of
its origin, so a Timestamp holds 1677-09-21 to 2262-04-11.
Choosing a Clock says which clock reads which point.
Arithmetic
A point minus a point is a span, a point plus or minus a span is a point, and
spans add, subtract, negate, scale by an i64 and sum. Two points do not add.
fn a_point_minus_a_point_is_a_span() {
use t2t::{Timedelta, Timestamp};
let published = Timestamp::from_secs(1_700_000_000);
let captured = published + Timedelta::from_micros(850);
let latency = captured - published;
assert_eq!(latency, Timedelta::from_micros(850), "a point minus a point is a span");
assert_eq!(captured - latency, published, "and a point minus a span is a point");
assert_eq!(latency * 3, Timedelta::from_micros(2_550), "a span scales by a count");
assert!(latency < Timedelta::MILLISECOND, "and compares with another span");
}
Two Timelines Never Mix
Two timelines differ by an amount that only the machine knows, and that moves: a time service steps the wall clock and never the monotonic clock; TAI runs ahead of UTC by the offset the kernel holds; the raw monotonic clock drifts from the monotonic clock by whatever slewing a time service applies; the boot clock gains every suspension; and a counter’s origin is the hardware’s own. A difference taken across two of them would be a number with no meaning, so the types refuse it. A wall-clock instant minus an uptime does not compile:
//! Points of two timelines never mix: a wall-clock instant minus an uptime does not compile.
use t2t_core::{Timestamp, Uptime};
fn main() {
let _span = Timestamp::from_secs(10) - Uptime::from_secs(10);
}
error[E0277]: cannot subtract `Uptime` from `Timestamp`
--> tests/compile_fail/a_timestamp_minus_an_uptime.rs:6:42
|
6 | let _span = Timestamp::from_secs(10) - Uptime::from_secs(10);
| ^ no implementation for `Timestamp - Uptime`
|
= help: the trait `Sub<Uptime>` is not implemented for `Timestamp`
Nor does a counter’s reading moved by nanoseconds, since a tick’s length is the counter’s rate, which the type does not know:
//! Spans of two kinds never mix: a counter's reading moves by its ticks, not by nanoseconds.
use t2t_core::{Tickstamp, Timedelta};
fn main() {
let _later = Tickstamp::from_ticks(10) + Timedelta::SECOND;
}
error[E0308]: mismatched types
--> tests/compile_fail/a_tickstamp_plus_a_timedelta.rs:6:46
|
6 | let _later = Tickstamp::from_ticks(10) + Timedelta::SECOND;
| ^^^^^^^^^^^^^^^^^ expected `Tickdelta`, found `Timedelta`
Each of these is a fixture that t2t’s tests compile, and that must fail to
compile. A program that does need to compare two timelines says how: a counter’s
span crosses to nanoseconds through its rate, as
The CPU Counter shows, and two clocks’ counts compare as
plain i64s, as the TAI clock’s listing in
Choosing a Clock does.
Units
Each nanosecond point has from_nanos, from_micros, from_millis and
from_secs, and an as_* for each; a Timedelta adds from_mins,
from_hours and from_days, and a constant for each unit from NANOSECOND to
DAY, a day being 24 hours, with no calendar consulted. A counter’s point and
span have from_ticks and as_ticks. A coarser unit’s constructor saturates
where its count passes the range.
A Timedelta’s accessor counts whole units toward zero, as std’s Duration
does, and subsec_nanos, subsec_micros and subsec_millis give the rest,
signed as the span is. A point’s accessor counts the whole units since its
origin, so a point before the origin rounds down, to the unit it falls in.
fn each_unit_has_a_constructor_and_an_accessor() {
use t2t::{Timedelta, Timestamp, Uptime};
assert_eq!(Timedelta::from_mins(90), Timedelta::HOUR + Timedelta::from_mins(30), "a span");
assert_eq!(Uptime::from_secs(90).as_millis(), 90_000, "a point, counted in a finer unit");
// A span counts whole units toward zero, as std's `Duration` does.
assert_eq!(Timedelta::from_millis(1_500).as_secs(), 1, "forwards");
assert_eq!(Timedelta::from_millis(-1_500).as_secs(), -1, "and backwards");
assert_eq!(Timedelta::from_millis(-1_500).subsec_millis(), -500, "the rest has its sign");
// A point counts the whole units since its origin, so one before it rounds down.
assert_eq!(Timestamp::from_millis(-1_500).as_secs(), -2, "the second it falls in");
}
Saturation and the Checked Twins
Every operator saturates at the ends of the range, so none overflows or panics,
and each has a checked_* twin that returns None where the operator would
saturate:
| Operator | Checked twin |
|---|---|
point + span | checked_add |
point - span | checked_sub |
point - point | checked_since |
span + span | checked_add |
span - span | checked_sub |
span * count | checked_mul |
-span | checked_neg |
span.abs() | checked_abs |
none: a span has no / | checked_div |
The operator suits code where the end of the range is out of reach, or where
stopping there is right; the twin suits code to which reaching it is a bug. A
span has no /: checked_div divides it, refusing zero parts, and MIN in
-1.
fn an_operator_saturates_and_its_checked_twin_refuses() {
use t2t::{Timedelta, Timestamp};
let latest = Timestamp::MAX;
assert_eq!(latest + Timedelta::SECOND, Timestamp::MAX, "the sum stops at the end");
assert_eq!(latest.checked_add(Timedelta::SECOND), None, "where its twin refuses");
let widest = Timestamp::MAX - Timestamp::MIN;
assert_eq!(widest, Timedelta::MAX, "a span past its range stops at its end too");
assert_eq!(Timestamp::MAX.checked_since(Timestamp::MIN), None, "or is refused");
assert_eq!(Timedelta::from_secs(i64::MAX), Timedelta::MAX, "a constructor saturates");
assert_eq!(-Timedelta::MIN, Timedelta::MAX, "and so does a negation");
assert_eq!(Timedelta::MIN.checked_neg(), None, "which its twin refuses");
// A span has no `/`; `checked_div` divides it, and refuses zero parts.
let third = Timedelta::SECOND.checked_div(3);
assert_eq!(third, Some(Timedelta::from_nanos(333_333_333)), "truncated toward zero");
assert_eq!(Timedelta::SECOND.checked_div(0), None, "and refused for no parts");
}
A workspace that denies clippy’s arithmetic_side_effects lint names each point
and span in its clippy configuration’s arithmetic-side-effects-allowed, since
the lint cannot see that they saturate; t2t’s own
clippy.toml shows
the list.
Floor and Ceil
floor and ceil take a point or a span to the nearest multiple of a unit at
or below it, or at or above it: the minute an event falls in, or the end of the
bucket a histogram counts it in. They work on both sides of the origin, take the
unit’s magnitude, and leave the value as it is for a zero unit.
fn floor_and_ceil_find_the_bucket_a_point_falls_in() {
use t2t::{Timedelta, Timestamp};
let event: Timestamp = "2026-09-16T07:45:35.123Z".parse().expect("an instant in UTC");
let start = event.floor(Timedelta::MINUTE);
let end = event.ceil(Timedelta::MINUTE);
assert_eq!(format!("{start:.0}"), "2026-09-16T07:45:00Z", "the minute it falls in");
assert_eq!(format!("{end:.0}"), "2026-09-16T07:46:00Z", "and the next");
assert_eq!(start.floor(Timedelta::MINUTE), start, "a multiple stays where it is");
assert_eq!(event.floor(Timedelta::ZERO), event, "and a zero unit leaves the point");
let budget = Timedelta::from_micros(1_250);
assert_eq!(budget.ceil(Timedelta::MILLISECOND), Timedelta::from_millis(2), "a span too");
}
Code Generic Over Points
TimePoint is what every point shares: its span type, Span,
the operators between the two, and from_count and count, the point as its
i64. Code generic over it works on every timeline, as the manual clocks and
Timed::elapsed do, and a type of your own that wraps a point may implement it
to join them.
fn code_generic_over_points_takes_every_timeline() {
use t2t::{Tickdelta, Tickstamp, TimePoint, Timedelta, Uptime};
/// How long ago `stamp` is, at `now`, on whichever timeline the two are on.
fn age<P: TimePoint>(stamp: P, now: P) -> P::Span {
now - stamp
}
let span = age(Uptime::from_secs(5), Uptime::from_secs(7));
assert_eq!(span, Timedelta::from_secs(2), "a span of nanoseconds on the monotonic clock");
let ticks = age(Tickstamp::from_ticks(100), Tickstamp::from_ticks(124));
assert_eq!(ticks, Tickdelta::from_ticks(24), "and a span of ticks on a counter");
assert_eq!(Uptime::from_count(span.as_nanos()), Uptime::from_secs(2), "from a count");
}
Choosing a Clock
Every clock implements Clock, whose one verb, now,
gives the clock’s Reading: a point on its timeline, or a span for a clock of
CPU time. Choosing a clock is choosing the timeline its readings are on.
The Clocks
| Clock | Reads | Linux | macOS | Steps |
|---|---|---|---|---|
SystemClock | Timestamp | CLOCK_REALTIME | CLOCK_REALTIME | yes |
CoarseSystemClock | Timestamp | CLOCK_REALTIME_COARSE | CLOCK_REALTIME | yes |
TaiClock | TaiTimestamp | CLOCK_TAI | none | with the wall clock |
MonotonicClock | Uptime | CLOCK_MONOTONIC | CLOCK_UPTIME_RAW | never |
CoarseMonotonicClock | Uptime | CLOCK_MONOTONIC_COARSE | CLOCK_UPTIME_RAW_APPROX | never |
RawMonotonicClock | RawUptime | CLOCK_MONOTONIC_RAW | CLOCK_UPTIME_RAW | never |
BootClock | BootUptime | CLOCK_BOOTTIME | CLOCK_MONOTONIC | never |
ProcessCpuClock | Timedelta | CLOCK_PROCESS_CPUTIME_ID | CLOCK_PROCESS_CPUTIME_ID | never |
ThreadCpuClock | Timedelta | CLOCK_THREAD_CPUTIME_ID | CLOCK_THREAD_CPUTIME_ID | never |
Counter | Tickstamp | cntvct_el0 or rdtsc | cntvct_el0 or rdtsc | never |
The operating system’s clocks read clock_gettime with the id the table gives,
and need the std feature, on 64-bit Linux or macOS; TaiClock is Linux’s
alone. Each id is one its system has, so a reading never fails. The counter
reads cntvct_el0 on aarch64 and rdtsc on x86_64, on any system, with or
without std; The CPU Counter has the rest of it.
Each clock is a unit struct, so SystemClock.now() reads one. A reference to a
clock is a clock too, so code takes &C where C: Clock.
The Wall Clock
SystemClock is the clock whose readings mean something off this machine, so it
stamps a capture another machine will compare. A time service disciplines it,
and may step it backwards across a correction: a span that must never run
backwards is measured on MonotonicClock or the counter instead.
CoarseSystemClock hands back the value the kernel’s timer last wrote, and
reads no counter: the same moment, to the timer’s period, a few milliseconds. It
answers whether a heartbeat or an expiry is due; it never stamps a capture.
macOS has no coarse wall clock, so there it reads CLOCK_REALTIME.
fn the_wall_clock_stamps_a_capture() {
use t2t::clock::{Clock, CoarseSystemClock, SystemClock};
use t2t::{Timedelta, Timestamp};
let captured = SystemClock.now();
assert!(captured > Timestamp::from_secs(1_700_000_000), "a moment past November 2023");
println!("captured at {captured}");
// The coarse clock names the same moment, to the kernel's timer period.
let gap = SystemClock.now() - CoarseSystemClock.now();
assert!(gap.abs() < Timedelta::from_millis(100), "within a period or two: {gap}");
}
TAI
TaiClock reads International Atomic Time: the wall clock plus the kernel’s TAI
offset, which a time service such as chrony or ptp4l sets to the leap
seconds since 1972. Until one does, the offset is zero, and the clock reads the
wall clock’s time. A leap second does not step it, but it steps with the wall
clock when the wall clock is set, and when a time service sets the offset. Linux
alone has the clock.
fn the_tai_clock_runs_ahead_of_the_wall_clock_by_the_kernels_offset() {
use t2t::Timedelta;
use t2t::clock::{Clock, SystemClock, TaiClock};
// The two are different timelines, so their counts are compared, not the points.
let wall = SystemClock.now().as_nanos();
let tai = TaiClock.now().as_nanos();
let offset = Timedelta::from_nanos(tai.saturating_sub(wall)).floor(Timedelta::SECOND);
println!("TAI runs {offset} ahead of UTC here");
let leap_seconds = Timedelta::ZERO..Timedelta::from_secs(60);
assert!(leap_seconds.contains(&offset), "zero, or the leap seconds since 1972: {offset}");
}
The Monotonic Clocks
MonotonicClock never steps, and every process on the machine reads the same
clock, so a deadline is MonotonicClock.now() plus a timeout. It stops while
the machine sleeps. Linux lets a time service slew its rate; macOS does not.
CoarseMonotonicClock reads the same timeline to the timer’s period, for a
deadline that is far off, or polled often; both read an Uptime, so their
readings compare.
fn the_monotonic_clock_keeps_a_deadline() {
use core::time::Duration;
use std::thread;
use t2t::Timedelta;
use t2t::clock::{Clock, CoarseMonotonicClock, MonotonicClock};
// Sleep for what is left until the deadline, however early a sleep returns.
let deadline = MonotonicClock.now() + Timedelta::from_millis(5);
let mut left = deadline - MonotonicClock.now();
while left.is_positive() {
thread::sleep(Duration::try_from(left).expect("a span forwards is a duration"));
left = deadline - MonotonicClock.now();
}
// A deadline far off, checked often, is cheaper on the coarse clock: both read `Uptime`.
let expiry = CoarseMonotonicClock.now() + Timedelta::from_secs(30);
assert!(MonotonicClock.now() < expiry, "the two read one timeline, so they compare");
}
RawMonotonicClock runs at the hardware’s own rate, which no time service
adjusts: the clock to measure a span free of a time service’s corrections, and
the one t2t measures an x86_64 counter’s rate against. It stops while the
machine sleeps too. Its readings are a RawUptime, so they never mix with the
monotonic clock’s. On macOS it reads the same clock as MonotonicClock, which
no time service slews there.
BootClock counts the time the machine was suspended, which MonotonicClock
does not: the clock for a lease or a timeout that must run down across a
suspension.
fn the_raw_and_boot_clocks_read_timelines_of_their_own() {
use t2t::Timedelta;
use t2t::clock::{BootClock, Clock, RawMonotonicClock};
// A span no time service's slewing bends.
let start = RawMonotonicClock.now();
let span = RawMonotonicClock.now() - start;
assert!(span >= Timedelta::ZERO, "it never runs backwards");
// A lease that a suspension of the machine runs down too.
let lease = BootClock.now() + Timedelta::from_mins(5);
assert!(BootClock.now() < lease, "the lease lies ahead");
}
CPU Time
ProcessCpuClock reads the CPU time the process has used, on every thread, and
ThreadCpuClock the time the calling thread has used, so two threads’ readings
count two different times. Each reads a Timedelta, the CPU time used so far,
and each takes a system call, not the fast path the clocks above take.
fn the_cpu_clocks_count_the_work_done() {
use core::hint::black_box;
use t2t::Timedelta;
use t2t::clock::{Clock, ProcessCpuClock, ThreadCpuClock};
// The process's clock is read first and last, so its span holds the thread's.
let (process_start, thread_start) = (ProcessCpuClock.now(), ThreadCpuClock.now());
for value in 0_u64..1_000_000 {
black_box(value);
}
let thread_time = ThreadCpuClock.now() - thread_start;
let process_time = ProcessCpuClock.now() - process_start;
assert!(thread_time > Timedelta::ZERO, "the loop took this thread's time: {thread_time}");
assert!(process_time >= thread_time, "and the process counts every thread's: {process_time}");
}
What a Reading Costs
One reading of each, on an AWS Graviton4 under Linux 6.12, pinned to an isolated core, the median of each of two runs:
| Clock | One reading |
|---|---|
CoarseSystemClock, CoarseMonotonicClock | 9.8 to 9.9 ns |
Counter | 11.8 ns |
SystemClock, TaiClock, MonotonicClock, RawMonotonicClock, BootClock | 33.2 ns |
ThreadCpuClock | 269 to 273 ns |
ProcessCpuClock | 388 to 394 ns |
The run is under
crates/t2t-clock/benches/results/2026-10-03T10-21Z-8aeae1f-clock-reads/;
Performance has the rest, and how to measure on your own
machine. The coarse clocks cost least because they read no counter, and for the
same reason are right only to the timer’s period.
Which to Read
- To stamp a capture, read
SystemClock: its reading names a moment another machine names too. - To wait, read
MonotonicClock: it never steps, so a deadline holds. - To measure, read the
Counter: one instruction, and one counter for every core and process on the machine. - To poll, read
CoarseSystemClockorCoarseMonotonicClock: either costs least, and is right to the timer’s period. - To run a test or a replay, read a
ManualClock, or anAtomicManualClockacross threads, as Testing with Manual Clocks shows.
The CPU Counter
Counter reads the CPU’s free-running counter in one
instruction that touches no memory: cntvct_el0 on aarch64, the time-stamp
counter on x86_64. A reading is a Tickstamp, the
difference of two a Tickdelta, and the counter’s
TickRate turns that into a Timedelta.
Ticks and Rates
A tick’s length is the counter’s: Apple silicon’s counter runs at 24 MHz, an Arm
core’s from Armv8.6 on at 1 GHz, which is TickRate::GIGAHERTZ, and an x86_64
core’s at the rate its time-stamp counter is rated for. A reading’s origin is
the hardware’s own and means nothing, so only a span converts:
Tickdelta::to_timedelta and Timedelta::to_tickdelta, each at a rate.
fn a_rate_turns_ticks_into_a_span_and_back() {
use t2t::{TickRate, Tickdelta, Tickstamp, Timedelta};
// Apple silicon's counter runs at 24 MHz.
let rate = TickRate::from_hertz(24_000_000).expect("a rate above zero");
let (start, end) = (Tickstamp::from_ticks(1_000_000), Tickstamp::from_ticks(1_000_024));
let ticks = end - start;
assert_eq!(ticks, Tickdelta::from_ticks(24), "a reading minus a reading is a span of ticks");
assert_eq!(ticks.to_timedelta(rate), Timedelta::MICROSECOND, "worth a microsecond at 24 MHz");
assert_eq!(Timedelta::SECOND.to_tickdelta(rate), Tickdelta::from_ticks(24_000_000), "and back");
assert_eq!(TickRate::from_hertz(0), None, "a counter that never ticks has no rate");
}
Discovering the Counter
Counter::discover finds the counter’s rate, or refuses with a
CounterError that says why there is none to read
as time:
- On
aarch64it readscntfrq_el0, the rate the firmware wrote. - On
x86_64it refuses a time-stamp counter that is not invariant, by CPUID leaf0x8000_0007. It takes the rate from CPUID leaf0x15, a crystal’s frequency times a ratio; where that has none, from the hypervisor’s leaf0x4000_0010; and where neither has one, withstdon 64-bit Linux or macOS, it measures the rate over 10 ms againstRawMonotonicClock. - On either, a rate outside 1 MHz to 10 GHz is refused.
| Refusal | When | Why |
|---|---|---|
NotInvariant | the time-stamp counter’s rate follows the core’s | its ticks have no fixed length, so no rate makes them time |
NoRate | the CPU reports no rate, and none could be measured | nothing says what a tick is worth |
ImplausibleRate | the rate read or measured is outside 1 MHz to 10 GHz | below a tick a microsecond a counter measures nothing worth measuring, and none runs past 10 GHz, so a rate outside is taken for wrong |
NoRate comes only on x86_64, where CPUID reports no rate and none could be
measured, as without std, or off 64-bit Linux and macOS. Each refusal’s
message says which it is: counter error: a rate of 42 Hz is outside 1 MHz to 10 GHz.
fn the_counter_is_discovered_or_refused_with_its_reason() {
use t2t::Timedelta;
use t2t::clock::{Clock, Counter};
match Counter::discover() {
Ok(counter) => {
let start = counter.now();
let ticks = counter.now() - start;
let took = ticks.to_timedelta(counter.rate());
println!("two reads {ticks} apart at {}: {took}", counter.rate());
assert!(took < Timedelta::from_millis(1), "two reads back to back");
},
// A virtual machine may promise no invariant counter, and a CPU may report no rate.
Err(refusal) => println!("no counter to time with: {refusal}"),
}
}
A measurement takes 10 ms, so a program discovers its counter once, at start-up,
and keeps it: a Counter is Copy, and holds only its rate.
Virtual Machines
A hypervisor chooses the CPUID bits its guest sees. One that does not promise an
invariant counter makes discover refuse with NotInvariant, even where the
host’s counter is invariant: the Intel macOS virtual machines t2t’s CI runs on
are such guests, and refuse. One that reports the counter’s rate in leaf
0x4000_0010 spares the guest the measurement.
Where the rate is known, from a config or from a discovery made elsewhere,
Counter::new takes it and checks nothing: neither that the counter is
invariant, nor that the rate is plausible. The caller vouches for both. Where
discover refuses and no rate is known, read MonotonicClock instead.
fn a_counter_at_a_known_rate_needs_no_discovery() {
use t2t::TickRate;
use t2t::clock::{Clock, Counter};
// A rate taken from elsewhere: a config, or a discovery another process made.
let rate: TickRate = "1000000000 Hz".parse().expect("a rate above zero");
let counter = Counter::new(rate);
let start = counter.now();
assert!(counter.now() >= start, "the counter never runs backwards");
assert_eq!(counter.rate(), TickRate::GIGAHERTZ, "and runs at the rate it was given");
}
One Counter for the Machine
Every core and every process on a machine reads the same counter, so a
Tickstamp one process takes, another may subtract from. t2t-clock’s test
counter_cross_process
starts a child process, and checks that the reading the child prints lies
between two the parent took around it. A reading crosses as its spelling, 1000 ticks, or as its count. Since only spans convert, two processes that measured
slightly different rates still agree on every reading; they differ only in what
a span is worth.
The read is not ordered against the instructions around it, which is right for a
stamp, and for timing anything much longer than the read itself. Clock::now on
a Counter is #[inline(always)], since a call around the read would move it.
The Conversion
TickRate::from_hertz works out, once, a factor and a shift for each
direction, so a conversion is a 128-bit multiply and a shift on the count’s
magnitude, with its sign put back, and no division. For a ratio of to ,
to the rate for ticks to nanoseconds and the rate to the other
way:
The shift is the largest whose factor still fits 64 bits, so the factor carries
every bit of precision it can. Rounding it up makes an exact multiple of the
ratio convert exactly, and any other count land within one unit of its exact
quotient, which a property test checks against an i128 division, on counts
drawn from every i64 and rates from every u64 above zero. A result past the
range saturates.
fn a_conversion_is_within_one_unit_of_the_exact_quotient() {
use t2t::{TickRate, Tickdelta, Timedelta};
// 3 GHz: a nanosecond is three ticks, and a tick a third of a nanosecond.
let rate = TickRate::from_hertz(3_000_000_000).expect("a rate above zero");
let exact = Tickdelta::from_ticks(3_000).to_timedelta(rate);
assert_eq!(exact, Timedelta::MICROSECOND, "a multiple of the ratio converts exactly");
// 1,000 ticks are 333.33 ns: the result is 333 or 334, never further off.
let nanos = Tickdelta::from_ticks(1_000).to_timedelta(rate).as_nanos();
assert!((333..=334).contains(&nanos), "within a nanosecond of 333.33: {nanos}");
// A count whose span is past what nanoseconds hold saturates, as every operator does.
let slow = TickRate::from_hertz(1).expect("a rate above zero");
assert_eq!(Tickdelta::MAX.to_timedelta(slow), Timedelta::MAX, "a tick a second, for ever");
}
A conversion takes about 1.2 ns from ticks to nanoseconds and 1.0 ns the other
way, at 24 MHz on a Graviton4: 1,024 of them in 1.206 µs and 1.034 µs, in the
run under
crates/t2t-core/benches/results/2026-10-02T23-14Z-9776c95-span-divisors/.
Dates and Spellings
Every value has one spelling: Display writes it, FromStr reads it back, and
Serialization takes the same text. A timestamp’s spelling is
RFC 3339 in UTC, read off the calendar view this chapter starts with.
A Timestamp as a Date
Timestamp::to_utc reads a point as a UtcDateTime: its
year, month and day in the proleptic Gregorian calendar, its hour, minute and
second in UTC, and the nanoseconds past the second. Unix time has no leap
seconds, so the second is never 60.
A UtcDateTime is a view: arithmetic stays on the Timestamp it came from. Its
fields are public, so one may be built by hand; is_valid checks that every
field names a real date and time, and to_timestamp gives the point back, or
None for one that is not valid or is outside 1677-09-21 to 2262-04-11.
fn a_timestamp_reads_as_a_date_and_back() {
use t2t::{Timestamp, UtcDateTime};
let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
let date_time = instant.to_utc();
assert_eq!((date_time.year, date_time.month, date_time.day), (2026, 9, 16), "the date");
assert_eq!((date_time.hour, date_time.minute, date_time.second), (7, 45, 35), "the time");
assert_eq!(date_time.nanosecond, 123_456_789, "to the nanosecond");
assert_eq!(date_time.to_timestamp(), Some(instant), "and back to the instant");
// The fields are public, so a date and time can be built by hand, and checked.
let leap_day = UtcDateTime { year: 2028, month: 2, day: 29, ..date_time };
assert!(leap_day.is_valid(), "2028 is a leap year");
let no_such_day = UtcDateTime { day: 30, ..leap_day };
assert!(!no_such_day.is_valid(), "February has no 30th");
assert_eq!(no_such_day.to_timestamp(), None, "so it names no instant");
let too_late = UtcDateTime { year: 2263, ..date_time };
assert_eq!(too_late.to_timestamp(), None, "nor does a date past 2262");
}
RFC 3339
A Timestamp is written as RFC 3339 in UTC, with Z, and nine fraction digits:
2026-09-16T07:45:35.123456789Z. A precision chooses the digits: {:.3} writes
milliseconds, {:.0} none, and nine is the most. A width pads the whole
spelling, with the fill and alignment given, and never cuts it. Debug writes
what Display does.
fn an_instant_is_written_as_rfc_3339() {
use t2t::Timestamp;
let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
assert_eq!(instant.to_string(), "2026-09-16T07:45:35.123456789Z", "nine digits");
assert_eq!(format!("{instant:.3}"), "2026-09-16T07:45:35.123Z", "to the millisecond");
assert_eq!(format!("{instant:.0}"), "2026-09-16T07:45:35Z", "to the second");
assert_eq!(format!("{instant:.12}"), instant.to_string(), "never past the nanosecond");
assert_eq!(format!("[{instant:>24.0}]"), "[ 2026-09-16T07:45:35Z]", "padded to a width");
assert_eq!(format!("{instant:?}"), instant.to_string(), "and debugged as written");
}
FromStr reads what Display writes: T and Z in either case, and a
fraction of one to nine digits, or none. It refuses an offset, a 60th second, a
date that does not exist, an instant outside the range, and any text after the
Z.
fn an_instant_is_read_from_rfc_3339_in_utc() {
use t2t::{ParseTimestampError, Timestamp};
let read = "2026-09-16T07:45:35.5Z".parse::<Timestamp>();
assert_eq!(read, Ok(Timestamp::from_millis(1_789_544_735_500)), "a fraction of any length");
let lower = "1970-01-01t00:00:00z".parse::<Timestamp>();
assert_eq!(lower, Ok(Timestamp::UNIX_EPOCH), "and either case");
for (refused, reason) in [
("2026-09-16T09:45:35+02:00", "an offset: an instant is read in UTC alone"),
("2026-09-16T07:45:60Z", "a leap second, which Unix time has none of"),
("2026-02-30T07:45:35Z", "a date that does not exist"),
("2263-01-01T00:00:00Z", "a date past 2262"),
("2026-09-16T07:45:35.1234567890Z", "a tenth fraction digit"),
("2026-09-16 07:45:35Z", "a space for the `T`"),
] {
assert_eq!(refused.parse::<Timestamp>(), Err(ParseTimestampError), "{reason} is refused");
}
}
TAI
A TaiTimestamp is written as RFC 3339’s date and time,
then the zone TAI: 2026-09-16T07:46:12.123456789 TAI. The precision and the
width work as they do for a Timestamp, and FromStr reads the zone back, and
refuses a Z, since an instant in UTC is not one in TAI.
fn a_tai_instant_is_written_in_its_own_zone() {
use t2t::{ParseTaiTimestampError, TaiTimestamp, Timedelta};
let synced: TaiTimestamp = "2026-09-16T07:46:12.5 TAI".parse().expect("an instant in TAI");
let next = synced + Timedelta::from_millis(500);
assert_eq!(next.to_string(), "2026-09-16T07:46:13.000000000 TAI", "written in TAI");
assert_eq!(format!("{next:.0}"), "2026-09-16T07:46:13 TAI", "to the precision asked");
let utc = "2026-09-16T07:46:12Z".parse::<TaiTimestamp>();
assert_eq!(utc, Err(ParseTaiTimestampError), "an instant in UTC is not one in TAI");
}
Spans
A Timedelta is written the way a config spells a span:
counts with units, coarsest first, from d, h, m, s, ms, us and ns.
Once a unit is written, every finer one with a remainder is too, a zero among
them, so a minute and 123 ms is 1m0s123ms. Zero is 0s, and a span backwards
starts with -. The longest spelling is Timedelta::MIN’s,
-106751d23h47m16s854ms775us808ns.
FromStr reads that, the bare 0, and any count in each unit, so 48h reads
as the span 2d spells. It takes each unit at most once and coarsest first, and
refuses a span past the range.
An Uptime, a RawUptime and a BootUptime are written as the span since
their origin, and read back from it.
fn a_span_is_spelled_coarsest_unit_first() {
use t2t::{ParseTimedeltaError, Timedelta, Uptime};
assert_eq!(Timedelta::from_secs(90).to_string(), "1m30s", "coarsest first");
assert_eq!(Timedelta::from_millis(60_123).to_string(), "1m0s123ms", "a zero inside is written");
assert_eq!(Timedelta::from_micros(-3).to_string(), "-3us", "a sign for a span backwards");
assert_eq!(Timedelta::ZERO.to_string(), "0s", "and zero in seconds");
assert_eq!(Uptime::from_millis(42_050).to_string(), "42s50ms", "a point from boot alike");
assert_eq!("48h".parse(), Ok(Timedelta::from_days(2)), "a count may pass the next unit");
assert_eq!("0".parse(), Ok(Timedelta::ZERO), "and zero may be bare");
for refused in ["90 seconds", "1s1m", "1m1m", "+1s", "-0", "1"] {
assert_eq!(refused.parse::<Timedelta>(), Err(ParseTimedeltaError), "{refused} refused");
}
}
Counts
A Tickstamp and a Tickdelta are written
as their count, then ticks: 24 ticks, -24 ticks. A
TickRate is written as its count of hertz, then Hz:
24000000 Hz. A width pads the count and its unit together. FromStr reads
what Display writes, and nothing else: no +, no 1 tick, no kHz, and no
rate of zero.
fn a_count_is_spelled_with_its_unit() {
use t2t::{ParseTickRateError, ParseTickdeltaError, TickRate, Tickdelta, Tickstamp};
assert_eq!(Tickdelta::from_ticks(-24).to_string(), "-24 ticks", "a span of ticks");
assert_eq!(Tickstamp::from_ticks(1_000).to_string(), "1000 ticks", "a counter's reading");
assert_eq!(TickRate::GIGAHERTZ.to_string(), "1000000000 Hz", "and a rate");
assert_eq!(format!("[{:>10}]", Tickdelta::from_ticks(24)), "[ 24 ticks]", "padded whole");
assert_eq!("24 ticks".parse(), Ok(Tickdelta::from_ticks(24)), "read back");
assert_eq!("24".parse::<Tickdelta>(), Err(ParseTickdeltaError), "a count names its unit");
assert_eq!("0 Hz".parse::<TickRate>(), Err(ParseTickRateError), "and a rate is above zero");
}
Refusals
Each spelling has its refusal, and values that share a spelling share it:
| Refusal | Refuses the text of |
|---|---|
ParseTimestampError | a Timestamp |
ParseTaiTimestampError | a TaiTimestamp |
ParseTimedeltaError | a Timedelta, an Uptime, a RawUptime, a BootUptime |
ParseTickdeltaError | a Tickdelta, a Tickstamp |
ParseTickRateError | a TickRate |
Each is a unit struct, whose message names the spelling it expected, with an example of it.
fn values_that_share_a_spelling_share_its_refusal() {
use t2t::{BootUptime, ParseTickdeltaError, ParseTimedeltaError, Tickstamp, Uptime};
assert_eq!(
"soon".parse::<Uptime>(),
Err(ParseTimedeltaError),
"an uptime is a span's spelling"
);
assert_eq!("soon".parse::<BootUptime>(), Err(ParseTimedeltaError), "and so is a boot uptime");
assert_eq!("soon".parse::<Tickstamp>(), Err(ParseTickdeltaError), "a reading is a count's");
assert_eq!(
ParseTimedeltaError.to_string(),
"parse timedelta error: expected `0`, or counts with units from coarsest to finest, as \
`1m30s`",
"each refusal names the spelling it expected"
);
}
Serialization
With the serde feature, every value serializes as its spelling where a person
reads the format, and as its count where none does. The modules under
t2t::serde read and write a count in a named unit, for a field that holds
one. The schemars feature adds each spelling’s JSON Schema, and the
zerocopy-08 feature reads and writes values as bytes.
This chapter’s listings are t2t-core’s tests, since they depend on serde_json,
serde_test and the other crates’ types; they name the crate t2t_core, and
the facade has the same items under t2t.
Two Forms
A serializer that says it is human-readable, as serde_json and TOML’s do,
takes each value’s spelling; one that does not, as bincode and postcard do,
takes its count:
| Value | Where a person reads the format | Where none does |
|---|---|---|
Timestamp | "2026-09-16T07:45:35.123456789Z" | i64 |
TaiTimestamp | "2026-09-16T07:46:12.123456789 TAI" | i64 |
Timedelta, Uptime, RawUptime, BootUptime | "1m30s" | i64 |
Tickstamp, Tickdelta | "24 ticks" | i64 |
TickRate | "24000000 Hz" | u64 |
Each is read from the form it is written in. A rate of zero is refused, as serde
refuses a zero for a NonZeroU64.
fn a_value_is_written_as_its_spelling_where_a_person_reads_the_format() {
use serde::{Deserialize, Serialize};
use serde_json::{from_str, to_string};
use t2t_core::{Timedelta, Timestamp};
/// A job's settings, as a config file holds them.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Job {
/// When the job first runs.
start: Timestamp,
/// How often it runs after that.
every: Timedelta,
}
let job: Job = from_str(r#"{"start":"2026-09-16T07:45:00Z","every":"1m30s"}"#).expect("a job");
assert_eq!(job.every, Timedelta::from_secs(90), "a span, from its spelling");
let written = to_string(&job).expect("a timestamp and a span always serialize");
let expected = r#"{"start":"2026-09-16T07:45:00.000000000Z","every":"1m30s"}"#;
assert_eq!(written, expected, "and written back as the spellings `Display` writes");
}
serde_test shows both forms, its readable and compact standing for the two
kinds of format:
fn a_format_no_person_reads_takes_the_count() {
use serde_test::{Configure as _, Token, assert_tokens};
use t2t_core::{TickRate, Timedelta, Timestamp};
let span = Timedelta::from_secs(90);
assert_tokens(&span.readable(), &[Token::Str("1m30s")]);
assert_tokens(&span.compact(), &[Token::I64(90_000_000_000)]);
assert_tokens(&Timestamp::UNIX_EPOCH.compact(), &[Token::I64(0)]);
let rate = TickRate::from_hertz(24_000_000).expect("a rate above zero");
assert_tokens(&rate.compact(), &[Token::U64(24_000_000)]);
}
Counts in a Named Unit
A service or a peer that sends a count names its unit at the field, and the
field’s #[serde(with = …)] names the module:
| Module | Reads and writes |
|---|---|
timestamp::secs, millis, micros | whole units since the Unix epoch |
timestamp::nanos | nanoseconds since the Unix epoch |
timedelta::secs, millis, micros, nanos | a span’s count in the unit |
Each has an option module, for an Option field, as
timedelta::secs::option. Where a person reads the format, a module reads a
count as a number or as a decimal string; where none does, as an i64. It
writes a number, but for nanoseconds since the epoch where a person reads the
format: those pass what an f64 holds exactly, so a JSON reader backed by one
would round them, and they are written as a string.
fn a_count_in_a_named_unit_takes_a_module() {
use serde::{Deserialize, Serialize};
use serde_json::{from_str, to_string};
use t2t_core::serde::{timedelta, timestamp};
use t2t_core::{Timedelta, Timestamp};
/// A job, as a peer's API sends it.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Job {
/// When the peer received it, in milliseconds since the epoch.
#[serde(with = "timestamp::millis")]
received: Timestamp,
/// When the peer started it, in nanoseconds since the epoch.
#[serde(with = "timestamp::nanos")]
started: Timestamp,
/// How long it may run, in seconds, where it has a limit at all.
#[serde(with = "timedelta::secs::option")]
timeout: Option<Timedelta>,
}
let sent = r#"{"received":1789544735123,"started":"1789544735123456789","timeout":"30"}"#;
let job: Job = from_str(sent).expect("a job");
assert_eq!(job.received, Timestamp::from_millis(1_789_544_735_123), "a number");
assert_eq!(job.timeout, Some(Timedelta::from_secs(30)), "or a decimal string");
let written = to_string(&job).expect("each field serializes as a number or a string");
let expected = r#"{"received":1789544735123,"started":"1789544735123456789","timeout":30}"#;
assert_eq!(written, expected, "nanoseconds since the epoch in a string, the rest as numbers");
}
A count past what the type holds is refused, with the reason
OutOfRangeError gives. Writing a coarser unit drops
what is finer: a timestamp is written as the unit it falls in, rounded down, and
a span as its whole units toward zero.
fn a_count_past_the_range_is_refused_and_a_finer_one_rounded_down() {
use serde::{Deserialize, Serialize};
use serde_json::{from_str, to_string};
use t2t_core::Timestamp;
use t2t_core::serde::timestamp;
/// A stamp, in whole seconds since the epoch.
#[derive(Debug, Serialize, Deserialize)]
struct Stamp {
/// When the stamped thing was published.
#[serde(with = "timestamp::secs")]
published: Timestamp,
}
let refused = from_str::<Stamp>(r#"{"published":253402300799}"#).expect_err("the year 9999");
let reason = refused.to_string();
assert!(reason.starts_with("out of range error"), "past 2262: {reason}");
let stamp = Stamp { published: Timestamp::from_millis(1_789_544_735_999) };
let written = to_string(&stamp).expect("a count of seconds always serializes");
assert_eq!(written, r#"{"published":1789544735}"#, "the second the moment falls in");
}
JSON Schema
With schemars, which turns serde on, every value with a spelling implements
JsonSchema: an inlined string schema with a description and a pattern, and for
a Timestamp the format date-time. Each pattern takes what its parser reads,
which t2t’s tests check on spellings the parser takes and on ones it refuses; a
date that does not exist, or a count past the range, matches the pattern and is
the parser’s alone to refuse.
fn an_instant_and_a_span_have_the_json_schemas_of_their_spellings() {
use schemars::schema_for;
use t2t_core::{Timedelta, Timestamp};
let instant = schema_for!(Timestamp);
let field = |name: &str| instant.get(name).and_then(|value| value.as_str());
assert_eq!(field("type"), Some("string"), "an instant is a string");
assert_eq!(field("format"), Some("date-time"), "in RFC 3339's format");
let span = schema_for!(Timedelta);
let pattern = span.get("pattern").and_then(|value| value.as_str());
let expected = "^(0|-?(?=[0-9])([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s)?([0-9]+ms)?([0-9]+us)?([0-9]+ns)?)$";
assert_eq!(pattern, Some(expected), "a span's pattern takes what its parser reads");
}
A span’s pattern holds a lookahead, which JSON Schema’s ECMA-262 regular
expressions read; Rust’s regex crate refuses a lookahead, so a validator built
on it cannot use the pattern.
Bytes
With zerocopy-08, each type derives the zerocopy traits it can honour. A
type’s bytes are native-endian, and with the feature on, its layout is part of
its API.
| Type | Traits | Left out, and why |
|---|---|---|
every point, Timedelta, Tickdelta | FromBytes, IntoBytes, Immutable, KnownLayout | Unaligned: each is an 8-byte-aligned i64 |
Timed<T, S> | FromBytes, Immutable, KnownLayout | IntoBytes: zerocopy’s derive takes a generic struct only where every field is Unaligned, and no stamp is |
UtcDateTime | FromBytes, Immutable, KnownLayout | IntoBytes: three bytes of padding sit between its seconds and its nanoseconds |
TickRate | Immutable, KnownLayout | FromBytes and TryFromBytes: a derived check would test that the rate is not zero, and take factors that do not match it; a rate crosses as its hertz |
A Timed<u64> is still read from bytes, its stamp’s then its value’s, and any
sixteen bytes make a UtcDateTime, so one read from bytes is checked with
is_valid, or by to_timestamp, before it is used.
fn a_value_is_read_from_bytes_and_a_point_written_to_them() {
use t2t_core::{Timed, Timestamp, UtcDateTime};
use zerocopy::{FromBytes, IntoBytes};
// A point is its count, native-endian, both ways.
let instant = Timestamp::from_secs(1_700_000_000);
let bytes = instant.as_bytes();
assert_eq!(bytes, 1_700_000_000_000_000_000_i64.to_ne_bytes(), "the count's bytes");
assert_eq!(Timestamp::read_from_bytes(bytes), Ok(instant), "and back");
// A stamped value is read from bytes: the stamp's, then the value's.
let mut record = [0_u8; 16];
record[..8].copy_from_slice(bytes);
record[8..].copy_from_slice(&101_u64.to_ne_bytes());
let sample = Timed::<u64>::read_from_bytes(&record).expect("sixteen bytes");
assert_eq!((sample.stamp, sample.value), (instant, 101), "a stamped word");
// Any bytes make a date and time, so one read from them is checked before it is used.
let date_time = UtcDateTime::read_from_bytes(&[0xFF; 16]).expect("sixteen bytes");
assert!(!date_time.is_valid(), "month 255 is no month");
assert_eq!(date_time.to_timestamp(), None, "so it names no instant");
}
Working with Other Crates
A Timestamp and a Timedelta convert to and from std’s time types and those
of chrono, jiff and time. Where every t2t value fits the other type, the
conversion is a From; the way back is a TryFrom, refusing with
OutOfRangeError what t2t’s range does not hold. The
other points, a TaiTimestamp and those counted from boot or by a counter, have
no counterpart in these crates, and no conversion.
| Other type | Feature | Into it | From it |
|---|---|---|---|
core::time::Duration | none | a Timedelta forwards | a Duration up to Timedelta::MAX |
std::time::SystemTime | std | every Timestamp | a moment from 1677-09-21 to 2262-04-11 |
chrono 0.4’s DateTime<Utc> | chrono-04 | every Timestamp | a DateTime in any zone, within the range |
chrono 0.4’s TimeDelta | chrono-04 | every Timedelta | a TimeDelta within the range |
jiff 0.2’s Timestamp | jiff-02 | every Timestamp | a Timestamp within the range |
jiff 0.2’s SignedDuration | jiff-02 | every Timedelta | a SignedDuration within the range |
time 0.3’s OffsetDateTime | time-03 | every Timestamp, in UTC | an OffsetDateTime at any offset, within the range |
time 0.3’s Duration | time-03 | every Timedelta | a Duration within the range |
A zone or an offset converts as the instant it names, read in UTC. Each feature
is named for the version of the crate it converts with, and with std on, it
turns on that crate’s own std too. The listings are t2t-core’s tests, which
have each crate as a dependency, so they name the crate t2t_core.
std
A Timedelta crosses to std’s Duration and back by TryFrom both ways: a
Duration never runs backwards, so a negative span is refused, and a
Timedelta holds about 292 years, so a longer Duration is refused. The
conversion is core’s, and needs no feature.
fn a_span_crosses_to_a_duration_where_it_runs_forwards() {
use core::time::Duration;
use t2t_core::{OutOfRangeError, Timedelta};
let span = Timedelta::from_millis(5);
assert_eq!(Duration::try_from(span), Ok(Duration::from_millis(5)), "a span forwards");
assert_eq!(Timedelta::try_from(Duration::from_millis(5)), Ok(span), "and back");
assert_eq!(Duration::try_from(-span), Err(OutOfRangeError), "no duration runs backwards");
assert_eq!(Timedelta::try_from(Duration::MAX), Err(OutOfRangeError), "nor past 292 years");
}
A Timestamp becomes a SystemTime with From, since a SystemTime on 64-bit
Linux and macOS reaches past both ends of a timestamp’s range, and comes back
with TryFrom, refusing a moment outside it. These need std, on 64-bit Linux
or macOS.
fn a_timestamp_crosses_to_a_system_time_and_back_within_its_range() {
use core::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
use t2t_core::{OutOfRangeError, Timestamp};
let instant = Timestamp::from_secs(1_700_000_000);
let system_time = SystemTime::from(instant);
let since_epoch = system_time.duration_since(UNIX_EPOCH).ok();
assert_eq!(since_epoch, Some(Duration::from_secs(1_700_000_000)), "the same moment");
assert_eq!(Timestamp::try_from(system_time), Ok(instant), "and back");
let far_future = UNIX_EPOCH
.checked_add(Duration::from_secs(1 << 40))
.expect("a 64-bit SystemTime reaches 2^40 seconds past 1970");
assert_eq!(Timestamp::try_from(far_future), Err(OutOfRangeError), "past 2262 is refused");
}
std’s Instant has no conversion: it is opaque, and std gives no way to read
its count or build one from a count. On Linux and macOS it reads
CLOCK_MONOTONIC and CLOCK_UPTIME_RAW, the clocks MonotonicClock reads, so
code that would convert an Instant reads MonotonicClock in its place.
chrono
chrono’s years reach far past 1677 and 2262, so a DateTime<Utc> holds every
timestamp, and a TimeDelta every span; a DateTime in another zone comes back
as the instant it names.
fn a_timestamp_and_a_span_cross_to_chrono() {
use chrono::{DateTime, FixedOffset, TimeDelta, Utc};
use t2t_core::{OutOfRangeError, Timedelta, Timestamp};
let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
let theirs = DateTime::<Utc>::from(instant);
assert_eq!(theirs.timestamp_nanos_opt(), Some(instant.as_nanos()), "the same instant");
let paris = theirs.with_timezone(&FixedOffset::east_opt(7_200).expect("two hours east"));
assert_eq!(Timestamp::try_from(paris), Ok(instant), "in any zone, read as its instant");
let latest = DateTime::<Utc>::MAX_UTC;
assert_eq!(Timestamp::try_from(latest), Err(OutOfRangeError), "past 2262 is refused");
let span = Timedelta::from_millis(-250);
assert_eq!(TimeDelta::from(span), TimeDelta::milliseconds(-250), "every span crosses");
assert_eq!(Timedelta::try_from(TimeDelta::MAX), Err(OutOfRangeError), "but not every back");
}
jiff
jiff’s Timestamp reaches the years -9999 to 9999, so it holds every timestamp,
and its SignedDuration every span.
fn a_timestamp_and_a_span_cross_to_jiff() {
use jiff::{SignedDuration, Timestamp as JiffTimestamp};
use t2t_core::{OutOfRangeError, Timedelta, Timestamp};
let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
let theirs = JiffTimestamp::from(instant);
assert_eq!(theirs.to_string(), "2026-09-16T07:45:35.123456789Z", "the same instant");
assert_eq!(Timestamp::try_from(theirs), Ok(instant), "and back");
let latest = JiffTimestamp::MAX;
assert_eq!(Timestamp::try_from(latest), Err(OutOfRangeError), "past 2262 is refused");
let span = Timedelta::from_millis(-250);
assert_eq!(SignedDuration::from(span), SignedDuration::from_millis(-250), "a span");
let longest = SignedDuration::MAX;
assert_eq!(Timedelta::try_from(longest), Err(OutOfRangeError), "and one too long for it");
}
time
time’s OffsetDateTime takes every timestamp, at the UTC offset; one at another
offset comes back as the instant it names. Its Duration holds every span.
fn a_timestamp_and_a_span_cross_to_time() {
use t2t_core::{OutOfRangeError, Timedelta, Timestamp};
use time::{Duration, OffsetDateTime, UtcOffset};
let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
let theirs = OffsetDateTime::from(instant);
assert_eq!(theirs.offset(), UtcOffset::UTC, "in UTC");
let east = theirs.to_offset(UtcOffset::from_hms(2, 0, 0).expect("two hours east"));
assert_eq!(Timestamp::try_from(east), Ok(instant), "at any offset, read as its instant");
let span = Timedelta::from_millis(-250);
assert_eq!(Duration::from(span), Duration::milliseconds(-250), "a span");
assert_eq!(Timedelta::try_from(Duration::MAX), Err(OutOfRangeError), "and one too long");
}
Stamped Values
Timed<T, S> holds a value T and the stamp S its writer
captured it with, a Timestamp unless a stamp of another timeline is named. It
says how old the value is at any later point, and reaches the value through
Deref, so a stamped sample’s fields read as the sample’s own.
fn a_stamped_value_says_how_old_it_is() {
use t2t::{Timed, Timedelta, Timestamp};
/// What a weather station measured.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Weather {
/// The temperature, in degrees Celsius.
celsius: i16,
/// The relative humidity, in percent.
humidity: u8,
}
let captured = Timestamp::from_secs(1_700_000_000);
let sample = Timed::new(captured, Weather { celsius: 21, humidity: 40 });
assert_eq!(sample.celsius, 21, "the value's fields, through `Deref`");
let now = captured + Timedelta::from_millis(1_500);
assert_eq!(sample.elapsed(now), Timedelta::from_millis(1_500), "its age at `now`");
assert!(sample.elapsed(now) > Timedelta::SECOND, "so it is stale after a second");
let early = captured - Timedelta::MILLISECOND;
assert!(sample.elapsed(early).is_negative(), "and its age is negative before its stamp");
}
elapsed is the span from the stamp to the point it is given, on the stamp’s
own timeline, and negative for a stamp still ahead of it.
The Writer Stamps
The code that captures a value stamps it, and whatever reads it keeps that
stamp: by the time a reader holds the value, the moment it was captured has
gone, and a clock read on the reading side would time the reader. map makes a
new value under the same stamp, and map_stamp a new stamp over the same value.
fn the_writer_stamps_and_the_reader_keeps_the_stamp() {
use t2t::clock::{Clock, ManualClock};
use t2t::{Timed, Timedelta, Timestamp};
/// A value, stamped by `clock` as it is written.
fn publish<C: Clock<Reading = Timestamp>, T>(clock: &C, value: T) -> Timed<T> {
Timed::new(clock.now(), value)
}
let clock = ManualClock::new(Timestamp::from_secs(1_700_000_000));
let sample = publish(&clock, 101_u64);
clock.advance(Timedelta::from_millis(3));
// Whatever the reader makes of the value, the stamp stays the writer's.
let doubled = sample.map(|count| count.saturating_mul(2));
assert_eq!((doubled.stamp, doubled.value), (sample.stamp, 202), "a new value, the same stamp");
let in_seconds = sample.map_stamp(Timestamp::as_secs);
assert_eq!(in_seconds.stamp, 1_700_000_000, "or a new stamp over the same value");
assert_eq!(sample.elapsed(clock.now()), Timedelta::from_millis(3), "and its age, read late");
assert_eq!(sample.into_parts(), (sample.stamp, 101), "and its parts, the stamp first");
}
as_ref and as_mut borrow the value under the same stamp; into_inner gives
the value, into_parts the stamp and the value, and a Timed converts to and
from the pair (S, T).
Order
The stamp is the first field, so the order a Timed derives is chronological,
and the value decides only between equal stamps.
fn stamped_values_sort_by_their_stamps() {
use t2t::{Timed, Uptime};
let mut events = [
Timed::new(Uptime::from_secs(3), "third"),
Timed::new(Uptime::from_secs(1), "first"),
Timed::new(Uptime::from_secs(2), "second"),
];
events.sort();
let order = events.map(Timed::into_inner);
assert_eq!(order, ["first", "second", "third"], "the stamp decides the order");
}
Layout
A Timed is #[repr(C)]: the stamp at offset zero, then the value. A
Timed<u64> is 16 bytes, with no padding, on any timeline, since every stamp is
an i64. With zerocopy-08, a Timed is read from bytes, as
Serialization shows.
fn a_stamped_word_is_two_words() {
use core::mem::offset_of;
use t2t::{Tickstamp, Timed};
assert_eq!(size_of::<Timed<u64>>(), 16, "a stamp and a word, no padding");
assert_eq!(offset_of!(Timed<u64>, stamp), 0, "the stamp first");
assert_eq!(size_of::<Timed<u64, Tickstamp>>(), 16, "on any timeline");
}
Testing with Manual Clocks
Code that takes its clock as a parameter, generic over
Clock, reads SystemClock in production and a manual
clock in a test. A manual clock reads whatever point it was last set or moved
to, so a test decides what time it is.
fn code_generic_over_its_clock_is_tested_on_a_manual_one() {
use t2t::clock::{Clock, ManualClock};
use t2t::{Timed, Timedelta, Timestamp};
/// Whether `sample` is older than a second by `clock`: `SystemClock` in production.
fn is_stale<C: Clock<Reading = Timestamp>>(clock: &C, sample: Timed<u64>) -> bool {
sample.elapsed(clock.now()) > Timedelta::SECOND
}
let clock = ManualClock::new(Timestamp::from_secs(1_700_000_000));
let sample = Timed::new(clock.now(), 101);
assert!(!is_stale(&clock, sample), "fresh when captured");
clock.advance(Timedelta::from_millis(1_500));
assert!(is_stale(&clock, sample), "stale a second and a half on");
}
ManualClock
ManualClock<P> holds a point of any timeline, a
Timestamp unless another is named. set moves it to a point, earlier or
later, and advance moves it by a span, back for a negative one, saturating as
the point’s own addition does. Between moves it stands still, and reading it
costs a load.
fn a_manual_clock_moves_only_when_moved() {
use t2t::clock::{Clock, ManualClock};
use t2t::{Timedelta, Uptime};
let clock = ManualClock::new(Uptime::from_secs(10));
assert_eq!(clock.now(), clock.now(), "it stands still between moves");
clock.advance(Timedelta::from_millis(250));
assert_eq!(clock.now(), Uptime::from_millis(10_250), "forwards by a span");
clock.advance(-Timedelta::SECOND);
assert_eq!(clock.now(), Uptime::from_millis(9_250), "backwards by a negative one");
clock.set(Uptime::MAX);
clock.advance(Timedelta::SECOND);
assert_eq!(clock.now(), Uptime::MAX, "saturating at the end, as the point's addition does");
}
A manual clock is not Clone: a copy would fork time, the copy and the original
each moving alone. It is shared by reference, and the code under test takes
&C.
Replays
A replay drives the clock from what a capture recorded: the clock is set to each stamp in turn, and the code under test reads it as it would read the wall clock.
fn a_replay_sets_the_clock_to_each_recorded_stamp() {
use t2t::clock::{Clock, ManualClock};
use t2t::{Timedelta, Timestamp};
/// Whether a heartbeat last seen at `last` has lapsed by `clock`, past `limit`.
fn has_lapsed<C: Clock<Reading = Timestamp>>(
clock: &C, last: Timestamp, limit: Timedelta,
) -> bool {
clock.now() - last > limit
}
// The stamps a capture recorded, one a heartbeat.
let recorded = ["2026-09-16T07:45:35Z", "2026-09-16T07:45:36.5Z", "2026-09-16T07:45:40Z"];
let beats: Vec<Timestamp> =
recorded.iter().map(|text| text.parse().expect("an instant")).collect();
let clock = ManualClock::new(beats[0]);
let mut lapses = Vec::new();
for pair in beats.windows(2) {
clock.set(pair[1]);
lapses.push(has_lapsed(&clock, pair[0], Timedelta::from_secs(2)));
}
assert_eq!(lapses, [false, true], "the second gap, of three and a half seconds, lapsed");
}
Threads
A ManualClock keeps its point in a Cell, so it moves to another thread but
two threads never share one: the compiler refuses it.
//! A manual clock may move to another thread, but two threads never share one.
use std::thread;
use t2t_clock::{Clock, ManualClock};
use t2t_core::Timestamp;
fn main() {
let clock = ManualClock::new(Timestamp::UNIX_EPOCH);
thread::scope(|scope| {
scope.spawn(|| clock.now());
});
}
error[E0277]: `Cell<Timestamp>` cannot be shared between threads safely
--> tests/compile_fail/a_manual_clock_is_not_shared_across_threads.rs:11:21
|
11 | scope.spawn(|| clock.now());
| ----- ^^^^^^^^^^^^^^ `Cell<Timestamp>` cannot be shared between threads safely
| |
| required by a bound introduced by this call
|
= help: within `ManualClock`, the trait `Sync` is not implemented for `Cell<Timestamp>`
AtomicManualClock<P> is the one to share,
across scoped threads by reference or across any by Arc. It keeps its point in
an AtomicI64, and a thread that reads a point another thread set or moved the
clock to also sees what that thread wrote before it moved the clock. Two threads
that advance it at once both count. It builds where the target has 64-bit
atomics.
fn an_atomic_manual_clock_is_shared_across_threads() {
use std::thread;
use t2t::clock::{AtomicManualClock, Clock};
use t2t::{Timedelta, Timestamp};
let clock = AtomicManualClock::new(Timestamp::UNIX_EPOCH);
thread::scope(|scope| {
for _ in 0..4 {
scope.spawn(|| {
for _ in 0..1_000 {
clock.advance(Timedelta::MICROSECOND);
}
});
}
});
assert_eq!(clock.now(), Timestamp::from_millis(4), "every thread's moves count");
}
Loom models check those promises, trying every interleaving of two threads that the memory model allows; Platforms and Features says where they run.
A Clock of Your Own
A manual clock holds a point, so a clock of CPU time, which reads a span, has no
manual twin. Clock is one associated type and one method, so a test writes the
clock it needs:
fn a_test_writes_the_clock_it_needs() {
use core::cell::Cell;
use t2t::Timedelta;
use t2t::clock::Clock;
/// A CPU-time clock for a test: a span, which no manual clock reads, set by hand.
#[derive(Debug, Default)]
struct TestCpuClock(Cell<Timedelta>);
impl Clock for TestCpuClock {
type Reading = Timedelta;
fn now(&self) -> Timedelta {
self.0.get()
}
}
let clock = TestCpuClock::default();
clock.0.set(Timedelta::from_millis(3));
assert_eq!(clock.now(), Timedelta::from_millis(3), "what the test set");
}
Performance
Every figure on this page is from a run committed under a crate’s
benches/results/: a directory of two runs’ transcripts and a manifest.toml
that says how they were taken. Each run was taken on an AWS Graviton4 (Arm
Neoverse V2, a c8g.8xlarge) under Linux 6.12.53, built by rustc 1.101.0-nightly
of 2026-09-27 in the bench profile (release’s settings, fat LTO, one codegen
unit) unless a row says it ran without LTO, pinned with taskset to a core the
kernel keeps other work off. Each figure is a median of 100 samples, and each
pair a run’s two medians.
| Run | Crate | What it compares |
|---|---|---|
2026-10-02T23-14Z-9776c95-span-divisors | t2t-core | a span’s spelling with each unit divided by a constant, against dividing by a unit read from a table |
2026-10-03T08-54Z-635b659-calendar-and-digit-pairs | t2t-core | #[inline] on the calendar’s conversions with and without LTO, and the digit-pair table against write! |
2026-10-03T10-21Z-8aeae1f-clock-reads | t2t-clock | one reading of each clock |
The name is the time the run was taken, in UTC, the commit it measured, and what it measured.
Reading a Clock
One reading, on one thread, in the clock-reads run:
| Clock | Nanoseconds |
|---|---|
CoarseSystemClock | 9.849, 9.836 |
CoarseMonotonicClock | 9.897, 9.891 |
Counter | 11.82, 11.82 |
RawMonotonicClock | 33.16, 33.16 |
MonotonicClock | 33.18, 33.18 |
BootClock | 33.19, 33.19 |
TaiClock | 33.19, 33.19 |
SystemClock | 33.21, 33.19 |
ThreadCpuClock | 272.8, 269.3 |
ProcessCpuClock | 394.2, 387.7 |
The bench reads each clock on 1, 2, 4 and 8 threads at once, so that a clock whose reading shares a cache line with another core’s would show the rise. This run pinned every thread to one core, so its rows for more threads time the threads taking turns, and show nothing of what cores cost each other; the manifest has them.
The counter’s read is one instruction, mrs of cntvct_el0 on aarch64 and
rdtsc on x86_64, in inline assembly that tells the compiler it touches no
memory, no stack and no flags. now is #[inline(always)], since a call around
the read would move it. The coarse clocks hand back a value the kernel’s timer
last wrote, without reading the counter, and cost the least; the CPU-time clocks
take a system call.
Converting a Value
Per 1,024 values, varied over the range, in microseconds:
| Conversion | Microseconds per 1,024 | Run |
|---|---|---|
| ticks to nanoseconds, at 24 MHz | 1.206, 1.207 | span divisors, after |
| nanoseconds to ticks, at 24 MHz | 1.034, 1.034 | span divisors, after |
Timestamp::to_utc | 0.9816, 0.9818 | calendar, fat LTO, #[inline] |
UtcDateTime::to_timestamp | 5.184, 5.184 | calendar, fat LTO, #[inline] |
writing a Timestamp | 24.22, 24.24 | calendar, fat LTO, #[inline] |
reading a Timestamp | 20.46, 20.44 | calendar, fat LTO, #[inline] |
writing a Timedelta | 46.27, 46.86 | span divisors, after |
reading a Timedelta | 31.33, 30.74 | span divisors, after |
Each row is measured at the commit its run names; the code of each path is the variant the row names, the one the crates have.
How Each Path Was Chosen
No Division by a Number Known at Run Time
On every path a reading or a spelling takes, a division divides by a constant,
which the compiler turns into a multiply. A TickRate works
out its factors once, so a conversion is a multiply and a shift, as
The CPU Counter shows. Timestamp::to_utc
counts the seconds from the first day a timestamp reaches, so every split of
them is an unsigned division by a constant, the time of day’s in 32 bits.
A span’s spelling writes each unit through a function generic over the unit,
write_unit::<UNIT>, so each unit’s division is by a constant: 46.27 and 46.86
µs per 1,024 spans, against 51.59 and 51.83 µs dividing by a unit read from a
table of the seven, a tenth faster.
The Digit-Pair Table
A timestamp’s date and time of day are written two digits at a time, from a
table of the hundred numbers below 100 as ASCII. Writing 1,024 instants takes
24.22 and 24.23 µs through the table, and 108.8 and 108.5 µs through write!’s
{:02}, four and a half times as long, under fat LTO; without LTO, 29.15 and
29.15 µs against 114.4 and 114.4.
Inlining Across Crates
The calendar’s conversions are #[inline], so a build without link-time
optimization inlines them across crates. That is the build a dependency gets in
a default release profile, which the run stood in for with
CARGO_PROFILE_BENCH_LTO=false and 16 codegen units. The run timed the
conversions with #[inline] on the calendar’s five functions and without it,
each with fat LTO and without:
| Per 1,024, µs | fat LTO, plain | fat LTO, #[inline] | no LTO, plain | no LTO, #[inline] |
|---|---|---|---|---|
Timestamp::to_utc | 0.9875, 0.9851 | 0.9816, 0.9818 | 8.009, 8.002 | 0.9788, 0.9786 |
UtcDateTime::to_timestamp | 4.559, 4.568 | 5.184, 5.184 | 6.912, 6.912 | 5.091, 5.087 |
reading a Timestamp | 20.58, 20.60 | 20.46, 20.44 | 29.03, 29.03 | 19.51, 19.50 |
Without LTO, #[inline] makes to_utc eight times as fast, and reading a
timestamp half as fast again. Under fat LTO, which inlines across crates anyway,
to_utc gains nothing, and to_timestamp is 13% slower for it, 5.18 µs against
4.56. Per conversion, a fat-LTO build pays 0.6 ns in to_timestamp for it, and
a build without LTO saves 6.9 ns in to_utc, 1.8 ns in to_timestamp and 9.3
ns in reading a timestamp. The crates keep #[inline], for the build without
LTO.
Measuring on Your Own Machine
The benches are divan’s, one for t2t-core’s conversions and one for the clocks’ reads:
cargo bench -p t2t-core --bench convert
cargo bench -p t2t-clock --features std --bench read
A name after -- runs the benches it matches, as -- timestamp. For figures
that hold still, build first, so the run times no compilation, then pin the run
to a core the kernel keeps other work off, one that isolcpus and nohz_full
name on the kernel’s command line, and run it twice:
cargo bench -p t2t-clock --features std --bench read --no-run
taskset -c 10 cargo bench -p t2t-clock --features std --bench read
The clock tour prints a rough figure for every clock in one command, timing 100,000 readings of each on the counter:
cargo run --release -p t2t --features std --example clock-tour
A run worth keeping goes under the crate’s benches/results/, in a directory
named for its time in UTC, its commit and what it measures, with its transcripts
and a manifest.toml in the shape of those there: the hardware, the
environment, the toolchain, the parameters, and the results.
Platforms and Features
The values build without std, bare metal included; the CPU’s counter builds on
aarch64 and x86_64; and the operating system’s clocks build with std, on
64-bit Linux and macOS.
What Builds Where
| Part | Builds |
|---|---|
| the values, t2t-core | without std, bare metal included |
ManualClock | without std, bare metal included |
AtomicManualClock | on a target with 64-bit atomics |
Counter | on aarch64 and x86_64, on any system, with or without std |
| the operating system’s clocks | with std, on 64-bit Linux and macOS |
TaiClock | with std, on 64-bit Linux |
the SystemTime conversions | with std, on 64-bit Linux and macOS |
an x86_64 counter’s measured rate | with std, on 64-bit Linux and macOS |
On another target the parts it cannot build are left out, and the rest builds.
The operating system’s clocks read a 64-bit timespec, which is why they need
64-bit pointers. On aarch64, Linux and macOS let a program read the counter
and its rate; a system that does not traps the read, which stops the process. On
x86_64 without std, or off 64-bit Linux and macOS, a counter whose rate
CPUID does not report is refused with NoRate, since there is no clock to
measure it against.
Without std
Every crate is #![no_std], and nothing reaches the operating system unless
std is on. t2t-core never reaches it at all: its std adds the SystemTime
conversions, and turns on its dependencies’ own std. The schemars feature
needs alloc, for the schemas it builds. CI builds all three crates for
aarch64-unknown-none and x86_64-unknown-none with no feature, and the facade
there with every feature that needs no std: chrono-04, jiff-02,
schemars, serde, time-03 and zerocopy-08.
32-Bit Linux
On 32-bit Linux the operating system’s clocks and the SystemTime conversions
are left out. On x32, x86_64 with 32-bit pointers, the counter builds, its
rate from CPUID alone; on i686 there is no counter. CI checks every crate with
every feature on both.
Crate Features
None is on by default, and nothing reaches the operating system unless std is
named; what std adds is on 64-bit Linux and macOS.
| Feature | Adds |
|---|---|
std | the OS clocks, an x86_64 counter’s measured rate, SystemTime conversions |
serde | every value’s spelling, and the serde modules for counts in a named unit |
schemars | JsonSchema for every value with a spelling; turns serde on |
zerocopy-08 | the zerocopy traits each type can honour, native-endian |
chrono-04 | conversions to and from chrono 0.4’s DateTime and TimeDelta |
jiff-02 | conversions to and from jiff 0.2’s Timestamp and SignedDuration |
time-03 | conversions to and from time 0.3’s OffsetDateTime and Duration |
That is the facade’s table. t2t-core has every feature but its std adds the
SystemTime conversions alone, and t2t-clock has std alone, which adds the OS
clocks and an x86_64 counter’s measured rate. Features are named with cargo add, as cargo add t2t --features std,serde.
Rust Version
Every crate builds on stable Rust 1.98 or later, the workspace’s rust-version,
and uses no nightly feature. The repository pins a nightly toolchain for
rustfmt’s and the lints’ nightly options alone, and just check-rust-msrv
builds every crate on 1.98.
What CI Checks
check.yml runs every just check recipe on x86_64 Linux. platforms.yml runs
what it cannot:
- the tests on arm64 Linux, and on macOS on arm64 and on x86_64;
- the bare-metal builds, and the 32-bit Linux checks, above;
- every combination of every crate’s features, each crate built alone;
- the loom models of
AtomicManualClock, which try every interleaving of two threads its orderings allow; - the two parsers fuzzed, from the corpus committed under
crates/t2t-core/fuzz/.
How t2t Compares
t2t sits beside std’s time types, two crates that read the CPU’s counter, quanta and minstant, and three calendar crates, chrono, jiff and time. Each section says what the other does, from its own documentation, where it and t2t differ, and where it is the better choice. The versions read are those docs.rs served on 2026-10-03: std 1.99, quanta 0.13.0, minstant 0.1.7, chrono 0.4.45, jiff 0.2.37 and time 0.3.55. Where a crate’s documentation and its source disagree, the section says so.
| t2t | std | quanta | minstant | chrono | jiff | time | |
|---|---|---|---|---|---|---|---|
| Points | one type per timeline, six | Instant, SystemTime | one Instant | one Instant | date and time types | date and time types | date and time types |
| A signed span | Timedelta | none | none | none | TimeDelta | SignedDuration, Span | Duration |
| Reads the CPU’s counter | on aarch64 and x86_64 | no | on aarch64 and x86_64 | on x86 Linux | no | no | no |
| Time zones | UTC and TAI | none | none | none | Utc, Local, fixed offsets; IANA zones through chrono-tz or tzfile | IANA zones | UTC offsets |
| Years | 1677 to 2262 | not a calendar | not a calendar | not a calendar | about ±262,000 | -9999 to 9999 | ±9999, or ±999,999 with large-dates |
no_std | yes | Duration alone, in core | no | no | yes | yes, with fixed zones alone without alloc | mostly |
std::time
std has Instant, a
monotonic clock’s reading, which reads CLOCK_MONOTONIC on Unix and
CLOCK_UPTIME_RAW on Darwin;
SystemTime, the
wall clock’s, which is not monotonic; and
Duration, an
unsigned span of up to about 584 billion years, with no Display. An Instant
is opaque: it compares with another and subtracts from one, but has no count to
read and no constructor from one. std has no signed span, no calendar and no
RFC 3339.
t2t’s MonotonicClock reads the clocks Instant reads, and its SystemClock
the one SystemTime reads; beside them it has TAI, the raw and boot clocks, and
the counter, each a type, with signed spans, counts to read, spellings and
manual clocks.
std is the better choice where a program times spans inside one process and
hands them to std’s own APIs, which take a Duration: it needs no dependency.
t2t’s Timedelta converts to and from a Duration.
quanta
quanta reads the time-stamp counter on
x86_64 and the system counter on aarch64, and falls back to the operating
system’s clock where the counter is missing or unreliable. Its documentation
asks for a constant or an invariant time-stamp counter; its source requires an
invariant one, and rdtscp. It calibrates the counter against the reference
clock once a process, lazily, at the first Clock::new or Instant::now, in up
to 200 ms. It has one Instant, a count of nanoseconds; a cached
Clock::recent that an upkeep thread keeps current; and a mock clock,
Clock::mock. It converts a reading with an integer multiply and a shift, as
t2t does. It builds on Windows, the BSDs and WebAssembly as well as Linux and
macOS, and its source needs std.
t2t reads the counter’s rate where the CPU reports one, and measures it only on
x86_64 where it does not; it refuses a counter it cannot read as time where
quanta falls back. Its readings are the raw register, so two processes agree on
every one, and it builds without std.
quanta is the better choice for a program that runs on Windows, a BSD or WebAssembly; that wants the operating system’s clock taken in silently where the counter cannot serve; or that reads the time often enough to want a reading an upkeep thread caches.
minstant
minstant reads the time-stamp
counter on Linux on x86 and x86_64, calibrated as the process starts. Its
documentation says that elsewhere it falls back to coarse time; its source falls
back to SystemTime, the wall clock, unless its fallback-coarse feature is
on. It has one Instant, and an Anchor that turns a reading into nanoseconds
since the Unix epoch; is_tsc_available says which it reads. Its source
converts a reading with floating point, and needs std.
t2t reads the counter on aarch64 too, and on macOS; it never falls back to
another clock, and converts with integers. It has no anchor from a counter’s
reading to a Timestamp: a program that needs one takes both readings itself.
minstant is the better choice for a program on Linux on x86 that wants one
Instant that builds everywhere, falling back off x86 Linux, and readings
turned into Unix time by its Anchor.
chrono
chrono is a calendar crate: a
DateTime in a time zone, Utc, Local or a fixed offset, with the IANA zones
through chrono-tz or tzfile; naive dates and times without a zone; and a signed
TimeDelta of seconds and nanoseconds. It formats with strftime-style patterns,
RFC 3339 and RFC 2822, reaches about 262,000 years either side of the common
era, and has no monotonic clock of its own: its documentation points to std’s
Instant.
chrono is the better choice for local time, time zones, dates far outside 1677
to 2262, and formatting to a pattern. t2t converts a Timestamp and a
Timedelta to and from chrono’s DateTime and TimeDelta.
jiff
jiff is a calendar crate built around the
IANA time zone database, the system’s copy or one it embeds where the system has
none. Its Zoned does arithmetic that follows daylight saving time, its Span
holds calendar units such as months, and its Timestamp and SignedDuration
are 96-bit counts of nanoseconds over the years -9999 to 9999. It reads and
writes RFC 3339, RFC 9557 and RFC 2822, and strftime-style patterns; it has
nearly all of this without std.
jiff is the better choice for anything that follows a time zone: local
deadlines, daylight saving time, calendar arithmetic in months and days. t2t
converts a Timestamp and a Timedelta to and from jiff’s Timestamp and
SignedDuration.
time
time is a calendar crate of dates and
times, at a UTC offset or in UTC, with a signed Duration of seconds and
nanoseconds. It formats and parses with format descriptions of any layout, and
with RFC 3339, RFC 2822 and ISO 8601; it holds years ±9999, or ±999,999 with its
large-dates feature; and it is mostly no_std.
time is the better choice for dates and times at an offset, and for formats
beyond RFC 3339. t2t converts a Timestamp and a Timedelta to and from time’s
OffsetDateTime and Duration.
Where t2t Fits
t2t is for the time a program reads off its clocks and carries: every value one
i64, every timeline a type, every operator saturating, and the counter read in
one instruction. It has no time zones but UTC and TAI, no calendar arithmetic,
and no formatting beyond its spellings; a program that needs those reads its
clocks with t2t and converts to chrono, jiff or time at the edge.