zena:bench
import {…} from 'zena:bench';
zena:bench — a Tachometer-style benchmark runner (CLI-only).
Architecture copied from Google's Tachometer (https://github.com/google/tachometer), minus the browsers:
- Variants are sampled in ROUND-ROBIN order, one sample per variant per round, so drifting CPU load or thermal throttling biases every variant equally instead of penalizing whichever ran last.
- Each variant's samples form a distribution, summarized with a 95% confidence interval of the mean (./stats.zena, re-exported here).
- Variants are compared via the CI of the DIFFERENCE of means: if that interval excludes zero the conclusion "A is faster than B" is robust; if it straddles zero the honest answer is "unsure" — and the runner keeps sampling until every comparison is resolved against the configured horizons or the time budget is exhausted.
This module measures in-process variants (closures in one binary). Cross-binary comparison — the same workload compiled by two compiler versions, or against a pinned .wat milestone — needs a host-side orchestrator; see docs/design/benchmarking.md.
WASI-only, like zena:fs: it imports the WASI monotonic clock directly.
Classes
BenchContext
class BenchContext
Passed to each benchmark function. By default the full invocation is timed; call start()/stop() (possibly repeatedly) to measure only the region of interest and exclude setup/teardown.
BenchOptions
class BenchOptions
TODO: replace with a record type when records get optional fields.
new()
var minSamples: i32
Samples collected per variant before the first conclusiveness check.
var extraSamples: i32
Additional samples per variant per auto-sample round.
var warmupRuns: i32
Untimed invocations per variant before sampling begins.
var timeBudgetMs: f64
Give up on resolving comparisons after this long (sampling still stops only at a round boundary, so every variant has equal counts).
var horizons: FixedArray<String>
Resolution horizons; see ./stats.zena. Default: 0% only.
PairwiseDifference
class PairwiseDifference
BenchResult
class BenchResult
BenchReport
class BenchReport
new()
suiteName: String
results: Array<BenchResult>
machine: MachineInfo
timestampMs: f64
Wall-clock timestamp (ms since epoch) when the run finished.
durationMs: f64
Total sampling duration, monotonic ms.
resolved: boolean
True when every pairwise comparison resolved within budget.
options: BenchOptions
BenchRunner
class BenchRunner
new()
MachineInfo
class MachineInfo
Best-effort host description recorded with each run. Fields are null / negative when the source (/proc, /etc/hostname) is not readable — e.g. when the runner's preopens don't include the host root. Load averages matter most: a loaded machine widens CIs, and recording load explains why after the fact.
NamedSamples
class NamedSamples
A variant's name and collected samples (milliseconds), for analyzing measurements gathered outside BenchRunner — e.g. by a host-side orchestrator round-robining across separate binaries or processes.
SummaryStats
class SummaryStats
Summary of one benchmark variant's sample distribution. Times are in milliseconds. The confidence interval is a 95% t-interval of the mean.
Difference
class Difference
95% confidence interval of (a.mean - b.mean), Welch's construction. Positive values mean a is slower than b. The pct bounds are the same interval expressed as a percentage of b's mean, so "a vs b" conclusions read as "a is faster/slower than b by X%–Y%".
Horizons
class Horizons
Resolution thresholds, as in Tachometer: a pairwise difference is resolved once its CI straddles no horizon boundary. The default single horizon of 0% resolves when the CI excludes zero (a definite faster/slower) — but a CI lying entirely inside [-1%, +1%] is also a conclusion ("differs by less than 1%") if you configure a 1% horizon.
Functions
nowMs
function nowMs(): f64
Monotonic time in milliseconds (nanosecond-resolution WASI clock).
realtimeMs
function realtimeMs(): f64
Wall-clock milliseconds since the Unix epoch, for stamping results.
collectMachineInfo
function collectMachineInfo(): MachineInfo
analyze
function analyze(suiteName: String, cases: Array<NamedSamples>, options: BenchOptions, durationMs: f64): BenchReport
Builds a full BenchReport from externally collected samples: the same statistics, resolution, machine-info, and JSON pipeline as BenchRunner.run(), minus the sampling loop. durationMs is the caller's total sampling time (0.0 if unknown).
formatFixed
function formatFixed(value: f64, decimals: i32): String
Formats a non-huge f64 with a fixed number of decimals (no exponent).
reportToString
function reportToString(report: BenchReport): String
Renders the Tachometer-style terminal report.
reportToJson
function reportToJson(report: BenchReport): String
Serializes a report for recording and later cross-run comparison. The schema is versioned; consumers must reject versions they don't know.
runTest
function runTest(name: String, testFunc: () => void, filter: String): void
Times testFunc once and prints the elapsed milliseconds, after one
untimed warm-up call. filter selects by substring; an empty filter
runs everything.
The one-shot counterpart to BenchRunner, which samples repeatedly and
reports a distribution. Use this when a single number read off stdout is
the whole point, as the benchmark programs under test-files/ do.
sortedCopy
function sortedCopy(samples: Array<f64>): GrowableArray<f64>
Returns a sorted copy of the samples. Insertion sort: sample arrays are at most a few thousand entries and typically nearly sorted regions dominate.
tCritical95
function tCritical95(df: f64): f64
Two-sided 95% critical value of Student's t distribution for the given (possibly fractional, from Welch–Satterthwaite) degrees of freedom.
summarize
function summarize(samples: Array<f64>): SummaryStats
Summarizes a sample distribution. With fewer than two samples the spread statistics are zero and the CI collapses to the mean.
differenceOfMeans
function differenceOfMeans(a: SummaryStats, b: SummaryStats): Difference
parseHorizons
function parseHorizons(specs: FixedArray<String>): Horizons
Parses horizon specs: "0%", "1.5%" (relative to the compared-to mean) or "0.5ms" / "2" (absolute milliseconds). Values are symmetric: "1%" tests both +1% and -1%.
isResolved
function isResolved(d: Difference, horizons: Horizons): boolean
True when the difference CI straddles no horizon boundary, i.e. more sampling is not expected to change the conclusion.