zena:bench

zena
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

zena
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.

Constructors
zena
new()
#
Methods
zena
start(): void
#
zena
stop(): void
#
zena
sampleMs(wholeInvocationMs: f64): f64
#

Internal: the measured time for this invocation.

BenchOptions

zena
class BenchOptions

TODO: replace with a record type when records get optional fields.

Constructors
zena
new()
#
Properties
zena
var minSamples: i32
#

Samples collected per variant before the first conclusiveness check.

zena
var extraSamples: i32
#

Additional samples per variant per auto-sample round.

zena
var warmupRuns: i32
#

Untimed invocations per variant before sampling begins.

zena
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).

zena
var horizons: FixedArray<String>
#

Resolution horizons; see ./stats.zena. Default: 0% only.

PairwiseDifference

zena
class PairwiseDifference
Constructors
zena
new()
#
Properties
zena
vs: String
#

Name of the variant compared against.

zena
diff: Difference
#

CI of (this variant's mean - other's mean); pct relative to other.

zena
resolved: boolean
#

BenchResult

zena
class BenchResult
Constructors
zena
new()
#
Properties
zena
name: String
#
zena
samples: Array<f64>
#
zena
stats: SummaryStats
#
zena
differences
#

BenchReport

zena
class BenchReport
Constructors
zena
new()
#
Properties
zena
suiteName: String
#
zena
results: Array<BenchResult>
#
zena
machine: MachineInfo
#
zena
timestampMs: f64
#

Wall-clock timestamp (ms since epoch) when the run finished.

zena
durationMs: f64
#

Total sampling duration, monotonic ms.

zena
resolved: boolean
#

True when every pairwise comparison resolved within budget.

zena
options: BenchOptions
#

BenchRunner

zena
class BenchRunner
Constructors
zena
new()
#
Properties
zena
name: String
#
zena
var options
#
Methods
zena
bench(name: String, fn: (ctx: BenchContext) => void): void
#

Registers a variant. Names must be unique within the runner.

zena
run(): BenchReport
#

Runs all variants: warmup, then minSamples round-robin rounds, then auto-sampling rounds until conclusive or over budget.

MachineInfo

zena
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.

Constructors
zena
new()
#
Properties
zena
var cpuModel: String | null
#
zena
var cpuCores: i32
#
zena
var memTotalKb: i64
#
zena
var memAvailableKb: i64
#
zena
var load1: f64
#
zena
var load5: f64
#
zena
var load15: f64
#
zena
var hostname: String | null
#

NamedSamples

zena
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.

Constructors
zena
new()
#
Properties
zena
name: String
#
zena
samples: Array<f64>
#

SummaryStats

zena
class SummaryStats
Re-exported from zena:bench/stats.zena

Summary of one benchmark variant's sample distribution. Times are in milliseconds. The confidence interval is a 95% t-interval of the mean.

Constructors
zena
new()
#
Properties
zena
n: i32
#
zena
mean: f64
#
zena
variance: f64
#
zena
stdDev: f64
#
zena
min: f64
#
zena
max: f64
#
zena
median: f64
#
zena
ciLow: f64
#
zena
ciHigh: f64
#

Difference

zena
class Difference
Re-exported from zena:bench/stats.zena

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%".

Constructors
zena
new()
#
Properties
zena
absLow: f64
#
zena
absHigh: f64
#
zena
pctLow: f64
#
zena
pctHigh: f64
#
Methods
zena
excludesZero(): boolean
#

True when the CI excludes zero, i.e. a and b measurably differ.

Horizons

zena
class Horizons
Re-exported from zena:bench/stats.zena

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.

Constructors
zena
new()
#
Properties
zena
percents
#
zena
absolutes
#

Functions

nowMs

zena
function nowMs(): f64

Monotonic time in milliseconds (nanosecond-resolution WASI clock).

realtimeMs

zena
function realtimeMs(): f64

Wall-clock milliseconds since the Unix epoch, for stamping results.

collectMachineInfo

zena
function collectMachineInfo(): MachineInfo

analyze

zena
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

zena
function formatFixed(value: f64, decimals: i32): String

Formats a non-huge f64 with a fixed number of decimals (no exponent).

reportToString

zena
function reportToString(report: BenchReport): String

Renders the Tachometer-style terminal report.

reportToJson

zena
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

zena
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

zena
function sortedCopy(samples: Array<f64>): GrowableArray<f64>
Re-exported from zena:bench/stats.zena

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

zena
function tCritical95(df: f64): f64
Re-exported from zena:bench/stats.zena

Two-sided 95% critical value of Student's t distribution for the given (possibly fractional, from Welch–Satterthwaite) degrees of freedom.

summarize

zena
function summarize(samples: Array<f64>): SummaryStats
Re-exported from zena:bench/stats.zena

Summarizes a sample distribution. With fewer than two samples the spread statistics are zero and the CI collapses to the mean.

differenceOfMeans

zena
function differenceOfMeans(a: SummaryStats, b: SummaryStats): Difference
Re-exported from zena:bench/stats.zena

parseHorizons

zena
function parseHorizons(specs: FixedArray<String>): Horizons
Re-exported from zena:bench/stats.zena

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

zena
function isResolved(d: Difference, horizons: Horizons): boolean
Re-exported from zena:bench/stats.zena

True when the difference CI straddles no horizon boundary, i.e. more sampling is not expected to change the conclusion.