bench: cross-version benchmarks for v0.4.0, v1.0.0 and v1.0.2 - #420
Conversation
Full-mode benchee measurement of benchmarks/vm_new.exs across v0.4.0, v1.0.0 and the 1.0.2 release commit puts the default Lua.new/1 at 36.7us -> 0.6us median (~60x, allocation ~92KB -> <1KB) and the custom-sandbox path at 36.0us -> 6.5us (~5.5x), measured under mix run. See bench_results/versions-2026-07-28.md.
New workloads closing representativeness gaps: patterns (string.find/ match/gsub — the pattern engine had zero coverage), metamethods (colon-syntax self-dispatch, multi-level __index chains, arithmetic/ relational metamethods), pcall_varargs (protected calls, varargs, multiple returns), and vm_new (VM instantiation incl. cold vs steady-state template cost). Existing workloads now thread the state returned by Lua.load_chunk!: on v0.4.0 the chunk ref is only valid in the state that loaded it, so discarding it broke the chunk jobs there; on 1.x the change is a no-op. Also corrects a stale oop.exs header comment (colon method syntax works).
Full-mode serial runs of all 11 workloads on the same machine in the same sitting, with Luerl 1.5.1 as a same-run control in every table (control spread across the three runs: <1%). One directory per released version — the convention going forward is that every release gets a directory here. Report: versions-2026-07-28.md. Supersedes the 1.0.0-era numbers in benchmarks/BASELINE.md. Refs #267
Self-contained static page (no external assets, light/dark aware) charting the v0.4.0 -> v1.0.0 -> v1.0.2 medians against the same-run Luerl control, with the honest-gaps and methodology notes from bench_results/versions-2026-07-28.md.
…ngelog The changelog ships as a hexdocs extra, where repo-relative file links don't resolve and fail mix docs --warnings-as-errors.
| zoxide: detected a possible configuration issue. | ||
| Please ensure that zoxide is initialized right at the end of your shell configuration file (usually ~/.zshrc). | ||
|
|
||
| If the issue persists, consider filing an issue at: | ||
| https://github.com/ajeetdsouza/zoxide/issues | ||
|
|
||
| Disable this message by setting _ZO_DOCTOR=0. | ||
|
|
||
| warning: this clause of defp format_error/2 is never used (or it will always fail/warn when invoked) | ||
| │ | ||
| 177 │ defp format_error(_, {:undefined, args}) do | ||
| │ ~ | ||
| │ | ||
| └─ lib/lua/util.ex:177:8: Lua.Util.format_error/2 |
There was a problem hiding this comment.
why is this in here?
There was a problem hiding this comment.
Two different kinds of junk, both now stripped in 971d18c.
Lines 1–7 are a zoxide shell-configuration notice — local shell state that leaked into the capture because the benchmark was driven through an interactive shell.
Lines 9–14 are a real compiler warning, but it belongs to v0.4.0 itself, not to main. At that tag Lua.Util has a format_error/2 whose last clause is unreachable:
$ git show v0.4.0:lib/lua/util.ex | sed -n "177p"
defp format_error(_, {:undefined, args}) do
main has only format_error/1, so there is nothing to fix here — it is compile output from a released tag we measure but do not change. That is also why the warning appears in the v0.4.0/ captures and not in v1.0.0/ or v1.0.2/.
The strip removes leading lines only, stopping at each file's first real output line: 344 deletions, 0 insertions, nothing inside a capture touched.
|
|
||
| ``` | ||
| git -C /Users/dave/code/tvlabs/lua worktree add --detach \ | ||
| /private/tmp/claude-501/-Users-dave-code-tvlabs-lua/baeced00-0f95-4dc6-8d8e-c16607ffb143/scratchpad/bench-v0.4.0 v0.4.0 |
There was a problem hiding this comment.
this is specific to my machine, this shouldn't be in here
There was a problem hiding this comment.
Agreed — replaced with <repo> and <worktree> placeholders in c9dce4f.
The file already used <worktree> a few lines further down (cp -R <repo>/benchmarks <worktree>/benchmarks), and the v1.0.0/environment.md notes were written generically from the start, so this just brings v0.4.0 in line. It was the only file in bench_results/ with machine paths.
| @@ -0,0 +1,388 @@ | |||
| <!DOCTYPE html> | |||
There was a problem hiding this comment.
why are we doing this this way (html file instead of writing a live view / dead view and rendering data)???? we also never link to t his anywhere
There was a problem hiding this comment.
Fair — rewritten as a dead view in d50cb56. priv/static/benchmarks.html is gone, along with its static_paths entry.
Now: get "/benchmarks" → PageController.benchmarks/2 → page_html/benchmarks.html.heex, inside Layouts.app, so it gets the nav, footer and theme toggle for free. Linked from the desktop nav, the mobile menu, and the footer's Product column.
Rendering data, not transcribing it. Website.Benchmarks reads the committed bench_results/<version>/summary.json files at compile time (@external_resource, so editing results recompiles). The table, the ratio plot and the headline tiles all derive from it — the plot is server-rendered divs with computed percentages, so the client JS is gone too. As a check that the parsing is right, every figure was diffed against what the static page carried by hand: all 36 table cells and all 27 plot ratios match.
On keeping it easy to update — this was the part worth getting right, so the page tracks the directories rather than a hardcoded list:
- a new
bench_results/<version>/summary.jsonbecomes a new column (glob +Version.compare/2, no list to extend) - the newest
versions-<date>.mdbecomes the linked report and the page's dated eyebrow - the tiles and the "still behind Luerl" figures re-derive, including the "N× faster than " deltas
Two things still need a human, and bench_results/README.md now says so under What updates itself: a new workload needs a row spec (which cases are worth showing is editorial), and the prose is written, not generated.
One consequence worth flagging: the compile-time read means the results directory has to be in the Docker build context, which it was not — the Dockerfile only copied mix.exs, lib/ and website/. Added a COPY bench_results line next to the existing README.md-is-read-at-compile-time precedent, and the module raises a pointed error rather than silently rendering an empty page if it is ever missing. I built the release container and fetched the page from it to confirm: 200 with all figures present, old /benchmarks.html path 404s.
Also fixed the "1.0.2 is the release cut today" line you would have hated in six months — it now reads from the data.
Code review (AI)OverviewRecords the first cross-version benchmark campaign (v0.4.0 / v1.0.0 / v1.0.2 with Luerl 1.5.1 as a same-run control), adds four workloads ( Verified
Findings
No correctness, security, or performance concerns in the code itself — the only library-code change is one entry appended to Verdict: ship it (after pushing the tag). 🌿 |
Every committed capture opened with a zoxide shell-configuration notice, and the v0.4.0 ones additionally carried the unused-clause warning that tag's own `Lua.Util.format_error/2` emits when compiled. Neither is benchmark output: the notice is local shell state, and the warning belongs to a released tag we measure but do not change (`main` has only `format_error/1`). Strips leading noise only, up to each file's first real line — 344 deletions, no insertions, nothing inside a capture touched.
The worktree setup and cleanup commands were recorded with this machine's absolute checkout and scratchpad paths, which are noise to anyone reproducing the run. Uses `<repo>` and `<worktree>` instead, matching how the file already referred to the worktree further down and how the v1.0.0 notes were written.
The benchmark page shipped as a hand-written static file under priv/static: its numbers were transcribed into JS literals with no link to the data they came from, it rendered outside the site layout, and nothing linked to it. Replaces it with a dead view at `/benchmarks`, linked from the nav and footer. `Website.Benchmarks` reads the committed `bench_results/<version>/summary.json` files at compile time, so the page has one source of truth for every figure — the tables, the ratio plot and the headline tiles are all derived, and the plot is rendered server-side rather than assembled by client JS. Recording a release is the whole update: version directories are found by glob and ordered with `Version.compare/2`, so a new `summary.json` becomes a new column and the newest `versions-<date>.md` becomes the linked report. Only a new *workload* needs a row spec, and only the prose is written by hand. The loader normalises the case-key differences in the v0.4.0 run (`" (mode: full)"` suffixes, `inputs` vs `by_input`) so one row spec resolves against every ref. Every derived figure was checked against the numbers the static page carried; all 36 cells and 27 ratios match. Verified in a release container, since the results directory has to be copied into the image for the compile-time read to work.
Records the first cross-version benchmark campaign and establishes the convention for benchmarking every release going forward. Related to #267 (left open — the per-workload perf-parity work it tracks continues).
What's here
bench_results/— one directory per released version (v0.4.0/,v1.0.0/,v1.0.2/), each with raw Benchee stdout for all 11 workloads, a parsedsummary.json, and anenvironment.md. The cross-version report isversions-2026-07-28.md; it supersedes the 1.0.0-era numbers inbenchmarks/BASELINE.md(left untouched as the historical gate record).README.mddocuments how to add the next release's column.patterns(the pattern engine had zero benchmark coverage),metamethods(:self-dispatch, multi-hop__index, arithmetic metamethods),pcall_varargs, andvm_new. Existing workloads now threadLua.load_chunk!state so the chunk path measures correctly on pre-1.0 refs./benchmarkson the website app — a dead view that renders the committedsummary.jsonfiles, so every figure on it derives from the recorded data rather than being transcribed. Version directories are discovered by glob, so recording a new release adds a column without a code change; see What updates itself inbench_results/README.md.Lua.new/1entry, now quoting measured figures.Headlines (medians, chunk path, Luerl 1.5.1 as same-run control)
Lua.new()steady-stateLua.new()allocation1.0.2 is the first release faster than Luerl on most workloads. The two⚠️ rows plus large-list host-boundary decode are called out in the report as the 1.1.x targets (the raise path partly reflects PUC-conformant position-prefixed error messages that Luerl doesn't build).
Measurement discipline
Apple M4 · Elixir 1.20.0 / OTP 29 ·
LUA_BENCH_MODE=full· strictly serial, onemix runat a time, quiet machine · each ref in a throwaway detached worktree · medians quoted. Luerl runs inside every benchmark as a control; its spread across the three runs was under 1%, so cross-version deltas are attributable to the library.Changelog note
The 1.0.2 entry now reads "~60x faster for the default configuration (36.7µs → 0.6µs median …) and ~5.5x faster when a custom sandbox is passed" — the measured full-mode figures from
benchmarks/vm_new.exsundermix run.