ADR-368: A Program Can Measure Time and What Another Program Used: time::now(), a Duration’s Readers, and What process::run Reports Beyond the Output

Status: Accepted Date: October 19, 2026 Answers: what a Nikaia program needs to measure how long something takes, and what a program it ran cost, so that a benchmark harness can be written in Nikaia (CLAUDE.md: helper tools are written in Nikaia) Related: ADR-150 (a Duration is the language below’s, re-exported, written 5.seconds()), ADR-243 (process::run), #672 (calendar time, not decided here); Part III’s std::process and std::time

1. Question

let started = time::now()                  // no such call today
let out = process::run("./brc", [input]; dir: ".")
let took = started.elapsed()
println(f"{took.in_nanos()} ns, {out.max_rss_kib ?? 0} KiB")   // none of it exists

std::time holds Duration, its constructors (5.seconds()) and sleep. A program cannot read a clock, reads a Duration no finer than microseconds, and learns from process::run only the exit code and the two streams. A benchmark harness needs all three: the time a run took, the processor time it used, and the most memory it held.

2. How others do it

3. Decision

D1 - time::now() reads a monotonic clock as a time::Instant.

time::now() returns a time::Instant; i.elapsed() is the Duration since it, and b.since(a) the Duration from a to b, zero where b is earlier. An Instant is monotonic: it never goes back. It says nothing about the calendar (#672 decides that) and means something only within the run that read it. It is the language below’s std::time::Instant, re-exported as ADR-150 re-exports Duration.

D2 - A Duration reads in nanoseconds.

d.in_nanos() is the count of whole nanoseconds, an i64 saturated at i64::MAX (292 years), the sixth of ADR-150 D5’s readers (in_hours to in_micros) and under its rules. No floating-point reader: a program that wants seconds divides, and the language below’s as_* names stay out of the ledger, as ADR-150 D5 keeps them.

D3 - process::run reports what the run cost.

process::Output gains:

The figures are the operating system’s for that child alone (wait4 on Unix), never a maximum over every child the program ran. max_rss_kib is the kernel’s figure as it stands, not a cleaned one: Linux counts in it the high-water mark of the process image that exec replaced, which is the caller’s, so on Linux it is never below the most memory the caller had held resident before it started the program. A harness that compares programs keeps its own memory small and reports the figure for a program that does nothing beside its table. The unsafe the call takes lives in a crate of its own under crates/unsafe/, as every unsafe std needs does.

4. Rejected

5. Consequences

6. Amendment (#922, 2026-10-31): the wall clock, time::since_epoch()

time::since_epoch() reads the wall clock as the time::Duration since the Unix epoch, zero before it, with no touches (as time::now). This narrows the wall clock’s rejection in section 4 to measuring: the monotonic clock (time::now, an Instant) measures spans within a run and never goes back; the wall clock is what a file’s time is stamped in (fs::Metadata::modified, ADR-319 section 7), so only it can be compared with that. It can go back, and a program that measures a span with it is wrong. Both exist because neither does the other’s job; calendar time stays #672’s.

This site is open source. Improve this page.