## 0.1.0 - 2026-06-16

First public release.

- Results output redesign: standalone benches render as a card (time / ops/s / samples / outliers); suite benches stream into an aligned table (`benchmark · time · ops/s · vs baseline`) that reprints in place on a TTY as columns grow, with a `baseline:` line and a collapsed `outliers:` section shown only when a bench actually had outliers. ops/s uses SI prefixes (`M`/`G`) above 1e6.
- Default run targets ~3000 ms measurement time and auto-sizes the sample count from warmup (`sampleSize: 0` → ~10 ms/sample, clamped to [10, 500]); `--samples N` still overrides. Buffer allocation moved to after warmup so it can use the measured per-iteration time.
- `asb profile` row selection: each bench shows the rows ≥ 1% of total cost, capped at `--top` (default 10), with a `top N rows account for X% of total cost` line; `--all` reveals internal/sub-1% rows. Replaces the previous fixed-coverage listing — tiny profiles no longer pad, giant ones shed their sub-1% tail.
- `asb profile` short mode flags: `--instr` (default), `--time`, `--alloc` (alias `--heap`) are the preferred forms; `--heaviest=instr|time|alloc` is still accepted.
- `asb compare`: `--significance` / `--noise` flags now override the configured render thresholds (previously the configured values were silently ignored).
- `asb doctor`: validates config loading, Node/dependency setup, benchmark file discovery, and selected runtime commands, mirroring the as-test diagnostic workflow.
- `asb clean`: removes generated build/chart outputs while preserving saved baselines by default (`--baselines` / `--all` removes them too).
- `asb init` now accepts the as-test-style `--yes` / `-y`, `--dir <path>`, positional target-directory, and `--install` forms, and creates/updates `package.json` with bench scripts + required dev dependencies.
- Help menu redesign (`asb --help`): colored, as-test-style command overview — brand and working commands (`run`/`build`/`profile`/`compare`/`watch`) in blueBright, lifecycle commands (`init`/`doctor`/`clean`) in magentaBright, a global `Flags:` section, and sponsor/docs/repo links. Per-command flag detail lives in the README.
- Fixes:
  - Change detection p-value was always 0 (integer division zeroed `min(hits, n-hits)/n`); it is now computed in floating point, so `compare` / `--baseline` / suite significance verdicts are meaningful.
  - `--json` no longer emits the runtime-comparison table or the `--baseline`-needs-node warning onto stdout, keeping the JSON document parseable under multiple runtimes.
  - `run` numeric flags (`--measure`, `--samples`, `--resamples`, `--confidence`, …) are validated against the same bounds as the config file; out-of-range values are rejected instead of silently producing garbage stats.
  - A sub-resolution / empty routine (warmup met == 0) can no longer produce an unbounded iteration plan (Inf → `u64::MAX`); met is floored before the sampling plan.
  - Packaging: `exports["./lib"]` and the published `files` list referenced the old `lib/as-bs` path from the `host.ts` rename and would break `import "as-bench/lib"`; dropped the unused `wipc-js` dependency.

- `asb profile --heaviest=alloc`: per-function allocation profiling — exact and deterministic (verified bit-identical across runs). The runtime's allocation chokepoint gets a prelude bumping shared monotone byte/count globals — the deepest layer that survives `-O` inlining (tlsf `allocateBlock`, falling back to `__alloc` then `__new`), so managed `__new`, unmanaged `heap.alloc`, and realloc/renew moves are all counted exactly once (`__new`/`__alloc` nest, and asc inlines `__alloc` away — counting any two layers would double-count). `__new` is also instrumented separately for the managed/unmanaged split: summary lines show `managed (N objs)`, `unmanaged`, `N realloc(s) (X requested)` (move-based reallocs via `tlsf/moveBlock` — `reallocateBlock` inlines away under `-O`, but `moveBlock` survives; in-place growth claims no new block and counts 0), and `memory +P pages` (linear-memory page growth for the bench window). Per-function page growth is attributed via `memory.size()` as a monotone counter — any `memory.grow` charges the live frame, zero grow-site instrumentation needed. Every user-level function is outlined with the same move-body wrapper as `=time` but reads those globals instead of a clock — the identical save/zero/restore algebra yields exact self bytes (own frame minus wrapped callees), outermost-gated inclusive bytes (recursion-safe), and allocation counts, with no calibration needed since a wrapper can't distort a byte counter. `~lib/rt/*` itself is never wrapped, so allocator and runtime-helper bytes charge the user-level caller. Benches whose module contains no allocator (fully DCE'd runtime) report 0 B with a note. Default 1 iteration (`--iters` to override). Measures allocation pressure (bytes claimed from the allocator: managed sizes include the 16 B object header, `heap.alloc` sizes are exact as requested), not live/peak — GC frees don't subtract. Validated: 64 × `heap.alloc(256)` reports exactly 16.00 KiB / 64 allocs; 64 × `new Array<i32>(16)` exactly 7.00 KiB / 128 allocs (object 32 B + buffer 80 B, headers included); forced-move realloc bench reports exactly 1 move at the correct requested size; a pass-through caller shows 0 B self / full incl.

- `asb profile --heaviest=instr` now weights instructions by a static cost table (third counter `__prof_w_<k>` in the same pass): ALU/const/local = 1, int mul 3, loads 3 / stores 2 (L1 assumption), float arithmetic 2, calls 5 (indirect 8), divisions/sqrt 12–15, float→int truncation 3, atomics 10, memory.grow 100. Tables rank by weighted cost and show both columns; raw counts stay exact and deterministic. Validated: an int-division loop vs an equal-length add/xor loop reads 53%/47% by raw counts, 66%/34% weighted, 83%/17% by measured wall-clock — weights move the ranking decisively toward reality. The table is deliberately hardcoded rather than timing-calibrated: per-instruction times aren't additive under superscalar execution (the residual 66→83 gap is exactly that ILP effect), and `--heaviest=time` already measures reality; weights also can't see cache behavior (a memory-thrashing function stays ~80% weighted vs 84% by time — `load` is costed as an L1 hit).

- `asb profile --heaviest=time`: per-function wall-clock self time, recursion-safe and overhead-corrected. A binaryen pass outlines each function (body moves to `<name>$tprof_inner`, a timing wrapper takes the original name, so direct calls, exports, and call_indirect element segments all flow through it) and accounts exactly: self = own duration − direct wrapped callees (shared-accumulator subtraction, per-frame state in wrapper locals — the real call stack is the bookkeeping stack); inclusive time is gated to outermost frames, so recursive functions don't multi-count. Instrumentation cost (~2 clock calls + 1 frame per call) is measured per bench by an injected empty calibration function + in-wasm driver, split into inside-window (charged per own call) and outside-window (charged per child call) components, and subtracted. `--iters <n>` (default 10) loops each routine in profile mode (tune kind 8's value is now the iteration count); `--min-instrs <w>` (default 4) skips wrapping trivial functions, folding their time into callers; `profile.iters`/`profile.minInstrs` config keys. Clock = `__asbench.tnow` (`hrtime.bigint`), node host only. Validated: a memory-thrashing function takes 84% of wall-clock vs 80% of instructions next to a compute loop's 16%/20% — time and instruction shares diverge where cache behavior differs, which is the mode's purpose; recursive fib reports incl ≈ self. Caveat: trust self times ≥ ~1µs/call; below that the correction dominates the signal — `--heaviest=instr` is the exact tool there.

- `runOptions.runtime.cmd` (as-test-style): run benches under any runtime by giving a command, in the config or per mode — e.g. `{"runOptions": {"runtime": {"cmd": "wazero run <env:-env> <file>"}}}`. `<file>` is replaced with the bench wasm (appended as the last argument when omitted); `<env:PREFIX>` expands the `AS_BENCH_TUNE_*` settings pairs as flags for runtimes that don't forward host env to the guest (trailing `=` fuses into one argument: `<env:--env=>` → `--env=K=V`). Takes precedence over the top-level `runtime` shorthand; `--runtime` accepts the same command form and command parsing is now quote-aware. The repo's `wazero` mode and `asb init`'s starter config demonstrate the shape. `runOptions.runtime` also takes a list — every bench runs under each runtime, rendered as a comparison table — whose entries mix `{cmd, name}` objects and plain strings (a named runtime or command template, as in the shorthand); the repo's `compare` mode demonstrates.

- Configuration file: `as-bench.config.json` (auto-discovered; `--config <path>` to point elsewhere) with a shipped JSON schema (`as-bench.config.schema.json`) for editor autocomplete. Options: `input` globs, `outDir`, `baselineDir`, `runtime`, `verbose`, `deterministic`, `settings.*` (all engine tunables incl. warmup/sampling/bootstrap/confidence), `render.*` (significanceLevel, noiseThreshold — now wired through the renderer instead of constants), `buildOptions.*` (optimize/debug/extra asc args), `profile.*` (top/all).
- Modes: named partial-config overlays under `"modes"`, applied with `--mode <name>` (objects merge a level deep, scalars/arrays replace). Precedence: defaults < config < mode < CLI flags. The repo's own config ships `full`, `wasmtime`, `wasmer`, `wazero`, and `deterministic` modes.
- `asb init`: scaffolds a starter `as-bench.config.json` (with `quick` + `wasmtime` modes) and an example bench; `--force` overwrites.

- `asb run --runtime wasmtime|wasmer|wazero|<template>`: external WASI runtimes. WIPC builds (`AS_BENCH_WIPC`) stream all engine events as framed binary messages over stdout (`assembly/util/wipc.ts` → `lib/wipc.ts` parser feeding the same renderer) and read tune overrides from `AS_BENCH_TUNE_<kind>` env vars — the module's only imports are `wasi_snapshot_preview1`, so any WASI runtime can run it. Non-frame stdout passes through (user console.log). `--save-baseline` works externally (sampleDone frame); `--baseline` comparison and `--deterministic` remain node-host-only (request/reply). Validated on all three runtimes: fib(20)/fib(15) = +1004–1018% everywhere (φ⁵ predicts +1009%), with wasmtime CIs at ±0.03µs — tier-free wall-clock.
- `assembly/util/host.ts` is now a compile-time transport dispatcher (imports vs WIPC frames).

- `asb run --deterministic`: record/replay of host imports (design from the playground replay system, adapted in-process). The engine announces each routine invocation via a new `iter()` import; the harness keeps iteration 1 live (lazy inits fire), records iteration 2's steady-state call pattern + memory diffs, and replays it for every later iteration with per-iteration verification (import order, tagged args, full tape consumption). Divergence — e.g. a routine whose call pattern or pointer args vary between iterations — fails loudly with the call index. Engine timing stays live: deterministic builds define `AS_BENCH_DETERMINISTIC` to route `timeNow()` through the passthrough `__asbench.now`, leaving the WASI clock recordable for user `Date.now`. The engine's analysis phase drops back to live imports (`analyzing` resets the harness). Per-iteration overhead ~3–5ns — compare deterministic runs with deterministic runs.

- `asb profile` (--heaviest=instr): tier-free work profiling. A binaryen pass injects per-function `calls` + executed-instruction counters (region granularity: function entry, loop bodies, if-arms; exported i64 globals), the engine's new profile mode runs each routine exactly once, and the CLI renders per-bench tables (%, instrs, calls, instrs/call). Engine overhead inside the counted window: 6 instructions. Fully deterministic — identical totals across runs and builds. Validated analytically: fib(20) reports exactly 21,891 calls (= 2·F(21)−1). `--top`, `--all` flags; `--heaviest=time` reserved.
- Profile builds add `--debug` for the name section only — verified bit-identical instruction totals with and without.

- Port the as-tral/Criterion statistics engine into `assembly/engine.ts` (Apache-2.0 attributed): warmup, auto/linear/flat sampling, bootstrap CIs (mean/median/std dev/MAD/slope), Welch-t + permutation p-value comparison, Tukey outliers. Fixes as-tral's resample-median bug.
- `bench()` now measures for real; `suite()` reports each bench's delta vs the suite's first bench.
- `__asbench` host-import namespace in `lib/as-bs.ts`: `now`, `tune` (settings overrides), progress/result events; `runBenchFile()` runs a compiled bench under WASI with a pluggable reporter.
- `as-bench run` / `as-bench build` implemented: glob → asc (in-process, wasi-shim + transform) → run → criterion-style render; flags `--warmup --measure --samples --resamples --sampling --confidence --verbose`.
- Playground now runs the real engine.
- WASI builds time via the shim's `performance.now()` (`clock_time_get(MONOTONIC)`) instead of the `__asbench.now` JS import — no host import on the hot path; `__asbench.now` remains the fallback for non-WASI targets.
- Proper Apache-2.0 vendoring for the engine: full license text in `licenses/as-tral.LICENSE` + `NOTICE` crediting romdotdog/as-tral and Criterion.rs; both ship in the npm package.
- Baseline persistence: `--save-baseline <id>` stores each bench's raw sample (iters + times) in `.as-bench/baselines/<id>.json`; `--baseline <id>` replays it through the engine's Welch-t + permutation comparison and renders `delta: [...] (p = ...) slower than baseline '<id>'`. Engine gains `sampleDone`/`loadBaseline`/`change` host hooks (pull-based baseline injection). Sample-size mismatches skip comparison with a warning. Verified: fib(21) vs fib(20) baseline reports +61.9% (golden ratio predicts +61.8%).
- Delta verdicts now use criterion's rule — "no change" when the entire CI lies inside the noise band (was: only when the CI spanned zero).
- Adaptive warmup: exits early once per-batch met stabilizes (2 consecutive batches within `warmupTolerance`, default 2%, after `warmupMinTime`); `warmupTime` is now a cap and `--warmup-tolerance 0` restores fixed-time warmup. Converged warmups derive met from the stable tail, not the cold-biased cumulative average. New `warmupEnded` event + `--warmup-tolerance`/`--warmup-min` flags. Example bench: 17.1s → 8.7s, identical results.

- Scaffold the three build targets (`cli/`→`bin/`, `lib/`→`lib/build/`, `transform/src/`→`transform/lib/`) mirroring as-test.
- Thin runtime-agnostic host (`lib/as-bs.ts`): `instantiate()` for node bindings + WASI, live `now()`, default imports.
- CLI skeleton (`as-bench` / `asb`): `help`/`version` wired; `run`/`profile`/`build`/`init` stubbed.
- AssemblyScript API skeleton (`bench`, `suite`, `set`, `blackbox`) + descriptors/settings; no `run()` — bench files execute at module start (as-tral style).
- No-op `asc` transform plugin skeleton.
- Project plan in `PLAN.md`.
