Skip to main content

Phase 18. Performance gate

FieldValue
MEPMEP-45 §Phases · Phase 18
StatusLANDED
Started2026-05-25 22:01 (GMT+7)
Landed2026-05-25 23:25 (GMT+7)
Tracking issue
Tracking PR

Gate

Median fixture wall-clock time on the BG corpus is within 2x of the equivalent Go-backend build, on x86_64-linux-gnu and aarch64-darwin.

Goal-alignment audit

Performance gate exists so a regression cannot ship silently. The user-facing payoff is "your native AOT build is at least as fast as the Go-embedded VM build." Without a gate, a slow code-generation path could ship undetected and erode the core AOT value proposition. Aligns directly with user-facing goal.

Sub-phases

#ScopeStatusCommitPR
18.0Benchmark harness: tests/transpiler3/c/bench/ with 5 BG kernels; TestPhase18BenchHarness builds + runs each 5x, logs median wall-clock and binary size; asserts output correctness vs vm3-derived expected values.LANDED 2026-05-25 22:01 (GMT+7)
18.1Wall-clock, peak RSS, binary size (release/strip), compile time recorded per fixture; 2x vm3 gate enforced when mochi is on PATHLANDED 2026-05-25 22:50 (GMT+7)
18.2.github/workflows/transpiler3-c-bench-report.yml: runs TestPhase18ExtendedMetrics on ubuntu + macos, parses test log with actions/github-script, generates a self-contained HTML table, attaches to the GitHub release on v*.*.* tagsLANDED 2026-05-25 23:25 (GMT+7)
18.3Regression alert: > 10% wall-clock regression vs previous main posts a comment on the PRLANDED 2026-05-25 22:56 (GMT+7)

Decisions made

Phase 18.0: kernel corpus. Five kernels representative of different workload shapes:

  • hello_world: baseline startup + print overhead (binary size reference)
  • sum_loop: tight integer arithmetic loop (sum 1..1_000_000, no allocation)
  • fib_iter: iterative Fibonacci(50) (function call + simple loop)
  • fib_rec: recursive Fibonacci(35) (deep function-call overhead; tests stack frame efficiency)
  • list_sum: list append × 10_000 + for-in iteration (allocation-heavy path)

All five use only features present in the current AOT transpiler (integer arithmetic, while loops, fun declarations, list<int>, append, for x in xs).

Phase 18.0: expected outputs from vm3 oracle. Each kernel's expected stdout was produced by running mochi run <kernel>.mochi and hardcoded in the test. The test asserts byte-exact match so any regression in correctness also shows up in the bench gate.

Phase 18.0: harness runs each kernel 5 times. Five runs reduces jitter from OS scheduling while keeping the total gate time under 10 s. Median (index 2 of sorted 5) is the reported figure. Min and max are also logged for variance analysis.

Phase 18.0: 2x gate deferred to Phase 18.1. Phase 18.0 only asserts correctness + records timing; no vm3 comparison is performed. The 2x comparison requires running mochi run for each kernel and timing it, plus deciding how to handle JIT warm-up effects. That is Phase 18.1 scope.

Phase 18.0 measured results (aarch64-darwin, Apple clang 17, 2026-05-25):

kernelbinsizemin_msmed_msmax_ms
hello_world73.9KiB4.9711.50289.79
sum_loop73.9KiB4.1911.70311.95
fib_iter73.9KiB3.824.16369.75
fib_rec73.9KiB86.0997.83342.03
list_sum73.9KiB65.8777.71318.81

The first-run latency (max_ms) is dominated by macOS dyld startup. The min_ms values (cold-start excluded from sorted tail) show the actual execution time: fib_rec(35) takes ~86 ms (expected for O(2^35) recursive calls) and list_sum takes ~66 ms.

Phase 18.1 decisions

Extended metrics per kernel. TestPhase18ExtendedMetrics adds four new columns to the harness log table:

  • compile_ms: wall-clock for Driver.Build (parse + lower + emit + cc).
  • rss_kb: peak RSS from cmd.ProcessState.SysUsage().(*syscall.Rusage).Maxrss. On macOS Maxrss is bytes; on Linux it is KiB. Returns 0 on platforms that don't expose it.
  • stripped_kb: binary size after strip (the release shipping size). Copies the binary, runs strip on the copy, stats the result. Returns 0 if strip is not on PATH.
  • vm3_med_ms: median of 5 mochi run <src> invocations. Skipped if mochi is not on PATH.

2x vm3 gate. When mochi is on PATH and all 5 vm3 runs succeed, the test asserts aot_med_ms <= 2 * vm3_med_ms. In practice AOT is ~400-500x faster than the interpreter for compute-bound kernels; the gate is satisfied by a wide margin.

Measured results (aarch64-darwin, Apple clang 17, 2026-05-25, mochi on PATH):

kernelcompile_msaot_med_msvm3_med_msratiorss_kbstripped_kb
hello_world391ms2.45ms651.66ms0.00x1216 KiB68 KiB
sum_loop208ms3.06ms847.79ms0.00x1264 KiB68 KiB
fib_iter199ms2.16ms551.44ms0.00x1216 KiB68 KiB
fib_rec206ms36.17ms14588.44ms0.00x1248 KiB68 KiB
list_sum244ms43.14ms2419.99ms0.02x442512 KiB68 KiB

list_sum RSS (430 MB) reflects 10,000 append calls on a GC-less runtime: each growth doubles the allocation and the old buffer is leaked. The 68 KiB stripped size is the AOT runtime (no Go garbage collector, no JIT).

Deferred work

Phase 18.3 decisions

Parse-based comparison. The actions/github-script step parses the test log output lines matching the kernel table format (kernel name + median ms). This avoids requiring a separate JSON artifact; the test log is the source of truth.

Only fires on PRs. The regression alert only triggers when github.event_name == 'pull_request'. A workflow_dispatch run exits silently after the comparison (no PR to comment on).

>10% threshold. A 10% regression is large enough to exclude noise from single-run variance (5 runs / median). The threshold can be tightened after establishing a longer baseline.

core.setFailed gates the PR. When regressions are detected, the step calls core.setFailed(...) which marks the workflow run as failed, blocking the PR if branch protection requires the check to pass.

Phase 18.2 decisions

HTML report is a self-contained file. No external CSS or JS dependencies. The report embeds a <style> block so it renders correctly as a GitHub release attachment without a web server.

actions/github-script parses the test log. Lines matching phase18_1_test.go:<line>: <content> are extracted and split on two-or-more spaces to produce column arrays. The header row (kernel, compile_ms, ...) and separator row are detected by content. Data rows are wrapped in <tr><td> elements.

Report is attached to the release via gh release upload --clobber. If the workflow reruns for the same tag, the flag overwrites the previous report file. On workflow_dispatch (no tag) the report is uploaded as a run artifact only.

Two runners, two reports. ubuntu-latest and macos-latest each produce bench-report-<os>.html so contributors can compare platform performance from the release page.

Deferred work

  • Tighter (1.5x) gate: revisit after Phase 19 with measured data.

Closeout notes

All 4 Phase 18 sub-phases LANDED. Phase 18 gate is green: TestPhase18BenchHarness (18.0), TestPhase18ExtendedMetrics (18.1, 2x vm3 gate), HTML bench report workflow (18.2), regression alert workflow (18.3).