Benchmark the portable kernel
Use harn bench portable to measure the canonical compiler, artifact decoder,
and native execution kernel separately. The command runs pure executions with
no host grants, checks that repeated compilation produces identical artifact
bytes, checks that every iteration produces the same terminal value, and can
distribute independent dispatches across operating-system threads.
Prepare an input
The benchmark accepts one JSON value as the entry function's input. For the
standalone reducer in crates/harn-wasm/demo/reducer.harn, save this as
/tmp/reducer-input.json:
{
"state": {"count": 0, "history": [], "label": "portable"},
"event": {"kind": "increment", "amount": 1}
}
Run a native benchmark
harn bench portable crates/harn-wasm/demo/reducer.harn \
--entry reduce \
--entry-kind function \
--input /tmp/reducer-input.json \
--compile-iterations 30 \
--iterations 500 \
--threads 4 \
--json \
--output /tmp/portable-kernel-benchmark.json
--threads measures separate executions that share one decoded, immutable
program artifact. It does not split one execution across threads. Each worker
gets isolated execution state and the command fails if any terminal value
differs from the first result. Use --threads 1 when comparing instruction
cost without scheduler contention, then repeat with representative thread
counts to measure host throughput. The accepted range is 1 through 256; when
there are fewer dispatch iterations than requested threads, the command starts
one worker per iteration.
The command reports the first compile separately as the cold compile sample for that process, then compiles the same source repeatedly for steady-state statistics. Parallel dispatch workers wait at a barrier after they are ready; batch wall time and throughput begin when the coordinator releases that barrier. Per-dispatch samples still measure each independent kernel call.
Portable receipts deliberately replace the full-VM profiler for this command.
harn bench portable rejects --profile, --profile-json, HARN_PROFILE, and
HARN_PROFILE_JSON instead of accepting flags whose data would describe a
different execution boundary.
The receipt uses schema harn.portable_kernel.benchmark.v1; its checked JSON
Schema is spec/schemas/portable-kernel-benchmark.v1.schema.json. It is
generated with make gen-portable-benchmark-schema from the kernel-owned Rust
receipt contract and limits; make check-portable-benchmark-schema prevents a
consumer-facing file from becoming a second owner. The closed
Rust receipt type, validation limits, statistics, provenance, and terminal
digest live in harn-kernel. The CLI serializes that type directly, and the
browser worker must round-trip its measurements through the same Wasm-backed
type before publishing them. A schema parity test compares every object field
set and validates both target variants. The receipt records:
- target and build/runtime provenance, including artifact, semantic ABI, and opcode ABI versions or fingerprints;
- source path, entry name, and kind;
- artifact byte length and digest;
- sample and worker counts;
- initialization latency when the target has an initialization boundary;
- first and repeated compile statistics;
- decode statistics when decode is independently measurable;
- first and repeated dispatch statistics, batch wall time, and throughput; and
- a digest of the terminal value.
Repeated statistics use the kernel's canonical R-7 percentiles and population
standard deviation. Their JSON fields are iterations, min_ms, mean_ms,
p50_ms, p95_ms, max_ms, stddev_ms, and total_ms; receipt envelope
fields use camelCase.
The host measures these durations with a monotonic clock. Clock access is not granted to the Harn program and is not embedded in the artifact.
Measure the browser path
First produce the release adapter and run its worker conformance checks:
make wasm-check
Then start the local server with make wasm-demo and open
http://127.0.0.1:8765/demo/benchmark.html. A dedicated module worker records
Wasm initialization, first and repeated compilation, and 500 calls through the
same generated compile and start exports used by the reducer worker. The
page only presents the worker's result. Read the JSON receipt from the page or
window.__HARN_BENCHMARK__.
Every repeated compile must produce the same artifact bytes and digest. Every
start receives the same immutable artifact and JSON input and must produce the
same terminal value. The worker sends timing samples through the Wasm adapter's
bounded projection of harn_kernel::BenchmarkStatistics, so native and browser
receipts use one percentile and standard-deviation definition.
Browser instances run in dedicated Web Workers. To measure parallel browser
throughput, create several workers and give each worker the same artifact bytes
and independent input state. Portable Kernel v1 does not require
SharedArrayBuffer, Wasm atomics, JavaScript Promise Integration, or Wasm
threads.
Interpret the numbers
Do not combine initialization, compile, decode, and dispatch into one latency claim. Browser initialization is a deployment cost; first compilation is a cold frontend cost; repeated compilation shows frontend steady state.
Target boundaries differ. Native dispatch calls the kernel with an already
decoded ProgramArtifact, DataValue, and GrantSet; native decode is reported
separately. Browser dispatch measures the public start(artifactBytes, inputJson, grantsJson) adapter boundary, so it includes artifact decoding,
JSON/grant adaptation, kernel execution, and result projection. Treat it as
adapter-start latency, not as an exact comparison with decoded native dispatch.
Compare like-for-like release builds on the same machine and browser, and preserve the receipt with the revision under test. Measurements from an earlier candidate were removed after the compiler/executor changed; publish new numbers only after rebuilding and exercising the final candidate.
Choose the right profiling surface
harn bench portable measures the compiler/artifact/kernel boundary. It does
not emit full-VM phase profiles, user timing spans, provider timings, or tool
timings.
- Use
harn bench <file> --profileor--profile-jsonto aggregate full native VM categories over repeated executions. - Use
harn time run <file>to attribute one full run to parse, typecheck, bytecode compilation, setup, main execution, and module work. - Use
std/timingto add named application spans to a hostful Harn run and export them through the normal profile and OTel paths. - Use
harn bench replayfor deterministic transcript and permission-fidelity measurements. It measures a different contract from portable dispatch.
Keeping these surfaces separate prevents host observation policy from becoming part of portable execution semantics.
Measured cutover snapshot
These measurements are evidence for this implementation boundary, not a
performance promise. They were collected on 2026-08-01 on an Apple M5 Pro
(18 logical CPUs, 48 GiB), macOS 26.5.1, Chrome 150.0.7871.187, and
wasm-pack 0.15.0. Both modules used the release profile and wasm-opt.
The repository state immediately before the cutover (49773b492) could not
produce the documented browser artifact: the excluded crate could not inherit
workspace lints, and its transitive linker-section capability registry was not
implemented for wasm32-unknown-unknown. For a useful size reference, the
legacy module below is the last revision before that linker-section dependency
(4c1655014), with only the invalid inherited-lints stanza removed. Its
942-line interpreter is otherwise unchanged.
| Release module | Raw Wasm | gzip -9 | Scope |
|---|---|---|---|
| Legacy subset interpreter | 755,233 bytes | 254,183 bytes | Lexer, parser, formatter, and separate AST interpreter |
| Portable Kernel v1 | 2,267,799 bytes | 784,917 bytes | Canonical strict frontend, compiler, artifact verifier, and resumable execution kernel |
The portable artifact is about 3.0 times the legacy module size. That cost is currently the canonical compiler and hardened artifact/runtime contract, not a second language implementation. Size remains a deployment constraint: future work should profile code-size ownership and split compilation from execution only behind the same artifact interface. Removing validation or restoring a subset compiler would invalidate the architectural result.
Five fresh dedicated-worker trials produced these medians:
| Browser measurement | Legacy | Portable Kernel v1 |
|---|---|---|
| Wasm initialization | 5.8 ms | 11.6 ms |
| First compile | Included in first execute | 50.2 ms |
| Repeated compile p50 / p95 | Included in every execute | 3.95 / 12.83 ms |
| First execute/start | 9.3 ms | 6.5 ms |
| Repeated execute/start p50 / p95 | 0.0 / 0.10 ms | 1.30 / 2.70 ms |
| 500-call batch wall time | 47.1 ms | 845.1 ms |
| Batch throughput | 10,616 calls/s | 592 starts/s |
The timing columns are not equivalent workloads. The legacy number uses a
small arithmetic pipeline and its combined parse-plus-subset-interpreter
execute(source) call; sub-millisecond samples also fall below Chrome's timer
resolution. The v1 number uses the typed reducer and public
start(artifactBytes, inputJson, grantsJson) adapter, so every start includes
artifact decoding, validation, JSON conversion, grant adaptation, execution,
and result projection. The separated v1 receipt is the useful baseline for
subsequent optimization. Its Wasm SHA-256 for this snapshot is
c4b28f2d20416d8f2f6ca183bef7fd6225a1c57f9d6f5e4d29d958279d5b1a4f.