Performance Notes
How fast is it, and how do you write fast code in it? This chapter is the site's one home for both answers: the design that makes the VM quick, the cost model that makes your code quick, and every measured number: microbenchmarks against Lua, Squirrel, and ChaiScript (a separate comparison harness outside this package, MSVC x64 Release, min-of-5), the embedding cost sheet, and complete games measured end-to-end. The numbers come from two machines, and the top of the measured section says which is which. One caveat travels with all of it: the benchmark harness reports integer µs with real run-to-run variance. Compare bands and medians, not ±1 µs deltas.
What the VM does for you
You write ordinary script; the bytecode VM earns its keep below the waterline:
- Fused superinstructions and register temps. The compiler collapses the common
shapes (
sum += a[i] * 2.0, compare-and-branch, element-read-into-local, nested arithmetic through frame-slot temporaries) into single dest-addressed opcodes, so a hot statement is one dispatch, not five, and intermediate results never touch the operand stack. A fused compound loop runs in the low tens of nanoseconds per iteration (the numbers are below). - Counted loops without ceremony.
for (int i = 0; i < n; ++i)compiles to dedicated loop opcodes; there is no per-iteration environment allocation, and variable resolution is inline-cached. - Typed arrays.
array<int>/array<float>keep raw primitive buffers behind the same script API as ordinary arrays: a 13.6k-element pixel grid drops from 435 KB to 109 KB, a value-semantics clone becomes amemcpy, and element access skips boxing entirely. This is the intended container for bulk numeric data. - Calls stay in the loop. Script calls never recurse natively. Frames are pooled and pushed inside the dispatch loop, and arguments bind zero-copy from the stack window.
- In-language parallelism.
parallel_for/parallel_transformrun bodies that the engine can prove safe across a worker pool, deterministic by construction (identical results and hashes at any worker count), reaching 4.8× over the serial loop at 8 workers on a 4C/8T machine for a 100k-element map. - Thin values.
script_valueis 16 bytes, static-assert-gated: four per cache line. - Two backends, one contract. The tree-walking interpreter is the oracle that keeps the VM honest. The full test suite and a differential fuzzer hold both to byte-identical behavior (see Backends), so the fast paths above carry no semantic drift, and any workload can A/B the backends to separate "slow" from "wrong".
- Safety that stays out of the profile. Checked integer overflow detects with intrinsics (not division), the execution-budget clock samples every 1024 ticks, and the idle debugger hook is one predictable branch per statement. None of them is measurable in hot loops.
What idiomatic-fast code looks like
// The fast shape: typed locals, typed arrays, & params, compound element stores.
array<int> pix = [];
for (int i = 0; i < 64; ++i) { pix.push(0); }
function shade(array<int>& dst, int base) { // & = no copy, writes land in the caller
for (int i = 0; i < dst.size(); ++i) {
dst[i] += base + (i & 7); // compound element store: one fused op
}
}
shade(pix, 100);
int sum = 0;
for (int i = 0; i < pix.size(); ++i) { sum += pix[i]; }
print("sum = {}", sum);
function square(int x) { return x * x; }
array<int> nums = [];
for (int i = 1; i <= 8; ++i) { nums.push(i); }
var squares = parallel_transform(nums, square); // deterministic at any worker count
print(to_json(squares));sum = 6624
[1,4,9,16,25,36,49,64]The rules this example encodes, in priority order:
- Declare types.
intandautolocals ride slot storage and the typed fast paths;varis the dynamic opt-out and costs accordingly. - Pass anything heavy by
&. Value semantics means a by-value parameter deep-copies unless the callee provably only reads it (then the argument binds in place at reference speed, see the elision);&is the cheap-by-design idiom, and it makes mutation visible in the signature. Decision guidance: Value Semantics; the three-way measurements (value vs&vsshared_ptr) are below. - Use
array<int>/array<float>for bulk numeric data: grids, pools, geometry tables. Same API, flat storage. - Prefer compound element stores.
a[i] += vis one fused dispatch, measurably cheaper than spelling out the read-modify-write. - Define classes once. Executing a
classdeclaration does redefinition/hot-reload bookkeeping; keep class definitions out of per-frame scripts and re-run only the code that ticks. - Stay on the VM. An engine you make runs the bytecode VM, as the standalone runner does;
eng->set_backend(jai::backend_type::interpreter)selects the tree-walking interpreter, the reference for checking rather than for speed (Backends). Measure Release builds: a Debug build of the engine is many times slower. - Walk containers by reference.
for (auto u : units)copies each element, as in C++, and a write touchanges the copy;for (auto& u : units)visits the elements themselves and copies nothing. - Copies are real, but read-only ones are free.
Unit u = units[i];is a copy. Inside a function, when nothing after it writesu, uses it whole or can changeunits[i], the engine binds the element itself instead, and a by-value parameter the callee only reads binds the same way. Nothing can observe the difference, so write the copy when you mean a copy andauto&when you mean the element, and treat the saving as a bonus rather than a rule.
The cost model
The intuition, cheapest to dearest (every rung quantified in the measured section below):
- Typed-local arithmetic and fused element ops are the floor. A hot fused loop iteration is tens of nanoseconds; a single statement is sub-microsecond on either backend.
- Function calls are the unit of cost. A script call in a tight loop costs roughly ten fused-loop iterations on the VM; method invocation adds receiver binding and dispatch on top. Typed loops over locals and arrays are where JaiScript holds its own (the bytecode-VM app runs ahead of Lua 5.4); call-dense recursion and object graphs walked through many small methods are where rivals with cheaper frames pull ahead (the benchmark apps below).
- Value semantics is a tax you opt out of with
&. Assignment and by-value parameters deep-copy; reference parameters andvar&declarations are the cheap-by-design idiom. Decision guidance: Value Semantics; three-way measurements: below. - Building a string in a loop is amortized O(1) per append, in every spelling. There is
no string builder to reach for:
s += x,s = s + x,s = s + a + "-" + band template appends grow the string's own buffer in place, whethersis a local, a global, a field (obj.s,this.s, a bare field), a static, an array element, a map value, astring¶meter or a map entry's binding. Strings stay values: a copy taken before an append shares the text until the append, which then writes its own copy, so the copy never sees it. A chain such asa + "/" + b + "/" + cor a template string joins into one accumulator sized for its text, andformat()writes its values straight into its result. - Class declaration is bookkeeping, not construction. Executing a
classdeclaration pays redefinition/hot-reload machinery every time: cheap once, wasteful per-frame. - Engine lifecycle sits far above script cost. Creating an engine and registering the stdlib cost hundreds of warm executes. Reuse engines; never build one per evaluation.
What's slower, and what to use instead
- Call-dense recursion. Lua runs fib(15) ~2.8× faster and Squirrel ~1.25×;
the cost is per-call frame setup, not opcode count. Bare self tail calls are the exception: on the VM,
return f(args)reuses the live frame (constant stack space, no depth limit, ~40 ns per call), so accumulator-style tail recursion runs at loop speed and any depth. The interpreter keeps its call-depth cap (256 active calls by default, about 150 to 200 on a 1 MB thread, Safety Rails). The caveat applies to non-tail shapes like fib'sf(n-1) + f(n-2), and the same per-call cost shows in object graphs walked through many small methods: Richards and DeltaBlue take about 2.4× Lua 5.4's time (the benchmark apps below). Keep naive deep non-tail recursion out of per-frame hot paths, or express the algorithm iteratively. - By-value copies of big structures. Use
¶meters andvar&declarations for anything heavy. The BST benchmark written with&runs 18 µs on the reference machine against 74 µs for the naive by-value spelling (~4×): by-value deep-copies a subtree at every recursion level, and that copy is the whole difference. - Bulk per-element pixel pushing. Software rendering (tens of thousands of scripted
element reads and stores per frame) is where any tagged-value VM pays list price against Lua
(GLOOM serial takes about 1.6× Lua 5.4.8's time per tick in the
GLOOM row below). The
mitigations, in order: typed arrays for the grids,
parallel_transformfor the bulk stages (it roughly halved GLOOM's dominant stage), and binding the one innermost loop to C++ if the budget still doesn't close. Full game sim is not the problem: DOOM's entire playsim takes under a millisecond a frame on the VM with six monsters awake and firing (the DOOM row below). - Per-execute class redefinition. Re-executing class declarations pays hot-reload bookkeeping every time (~250 µs for a 30-line class script, both backends equally). Define classes once; per-frame scripts should call, not declare.
Measured results
Three altitudes: the raw cost sheet for embedders, microbenchmarks against three rival runtimes, and complete games measured end-to-end. Sources are named per table.
Two machines appear below; never compare a number from one with a number from the other. The report machine is a 2016-era 4-core/8-thread laptop: the cost sheet, the dispatch regimes, the aliasing table, and the parallel-scaling figures come from it. The reference machine is a current 24-thread laptop CPU (Core Ultra 9 285HX): every comparison against another runtime (the head-to-head table, the kernel suite, the benchmark apps and every whole-game row) was measured there, each row naming its date. It runs everything faster, and the two machines ran different builds of the VM. Read ratios within one table.
The embedding cost sheet
Order-of-magnitude operation costs, report machine (interp / VM):
| operation | cost |
|---|---|
| single arithmetic op | <1 µs both backends |
| function call | ~2 µs interp / ~1 µs VM (in a tight loop: 0.9 / 0.44 µs) |
| class creation | ~15 µs interp / ~14 µs VM |
| method invocation | ~12 µs interp / ~9 µs VM (per-call in a tight loop: ~1 µs either backend) |
| hot loop, per iteration | ~45 ns (VM, fused compound loop; ~9 ns on the reference machine) |
| engine creation | ~73–77 µs (+ lazy backend init on first execute; ~90 µs all-in) |
| stdlib registration | ~311 µs |
Dispatch regimes, report machine (Release builds, medians of interleaved samples; scripts: a 1-line predicate, a 6-line function script, a ~30-line class+loop script):
| regime | interpreter | vm |
|---|---|---|
| engine create + first trivial execute | ~90 µs | ~87 µs (identical) |
| cold one-shot, fresh engine (tiny script) | 36.9 µs | 34.8 µs (VM already ahead) |
| warm engine, novel string every time (tiny) | 7.1 µs | 8.4 µs (interp +18–20% ahead) |
| warm engine, repeated string (tiny) | 1.03 µs | 0.60 µs (VM 1.7–1.8x faster) |
| warm jaibite execute (tiny) | 0.85 µs | 0.38 µs |
| warm jaibite execute (6-line fn script) | 3.10 µs | 1.69 µs |
| ~30-line class/loop script, warm | 251.6 µs | 241.4 µs (1.04x) |
- VM compilation costs about one third of parsing (tiny: parse 3.8 µs, compile ~1.5 µs; 30-line: parse 67.5 µs, compile ~21 µs) and is cached (in the source-cache entry or the jaibite), so it amortizes by the 3rd–4th execution in every re-execution regime.
- The interpreter's only durable win is genuinely unique, never-repeated small strings on a warm engine, worth 1.3–5 µs per execution, gone entirely at ~30 lines.
- Class-heavy scripts see only ~4% VM advantage warm: per-execute class redefinition/hot-reload machinery costs the same on both.
- The automatic jaibite disk cache turns file loads from parse-bound to IO-bound: a measured 535-line game file's load-and-run drops from 5.8 ms to 2.4 ms: the ~3.4 ms parse replaced by a sub-millisecond binary load (see jaibite for the mechanism).
What the debugger costs
The debugger is designed to be free until a session attaches. The 1000-iteration hot-loop script from the cost sheet, report machine, min-of-3, µs per run:
| configuration | interpreter | vm |
|---|---|---|
| no debugger constructed | 149 | 47 |
| controller constructed, no session | 148 | 46 |
| session enabled, no breakpoints | 148 | 50 |
| session enabled, breakpoint in a cold file | 146 | 51 |
| session enabled, breakpoint whose line collides with the hot statement | 165 | 64 |
The two disabled rows sit on the pre-debugger band exactly; an armed session costs the VM ~10% (it detects statement edges per dispatch iteration); the only expensive row is the deliberate worst case: a breakpoint in another file whose line number matches the hot statement, one bounded string compare per collision.
What each aliasing strategy costs
Value Semantics teaches the strategies; this is the bill. Same computation, three ways of holding and passing a meaty object (string name + 8-int stats array + 4 scalars; 200 precompiled calls per row, best of five runs, report machine):
| strategy | interp µs/call | vm µs/call | vs & |
|---|---|---|---|
read-only compute, pass Creature c (by value) | 8.0 | 6.6 | 1.3–1.4×. The deep copy, ~1.8 µs/call here, grows with payload |
read-only compute, pass Creature& c | 6.2 | 4.8 | 1.0 |
read-only compute, pass shared_ptr<Creature> c | 6.4 | 4.9 | tie (within noise) |
mutate: heal(Unit& u) vs heal(shared_ptr<Unit> u) | 1.7 / 1.8 | 1.4 / 1.4 | tie (within noise) |
method call, held as value / auto& / shared_ptr | 5.7–5.8 | 4.7 | all tie: the holder doesn't matter |
The structure-heavy version of the same story, on the node-aliasing BST benchmark (15 nodes):
in-place & params are ~1.35× faster than the shared_ptr return-and-reassign
shape (that delta is the shape, not a per-access penalty), and both are ~3.5× faster than naive
by-value, which deep-copies subtrees at every recursion level.
Against Lua, Squirrel, and ChaiScript
All three rivals are measured in one comparison harness, same machine, same protocol: the reference-machine run of 2026-11-04, each suite run five times pinned to one P core, each cell the fastest of the five. Headline rows, µs/iteration (a plain function call rounds to 0 or 1 µs everywhere but ChaiScript, so that row is omitted; the cost sheet above has it in nanoseconds):
| benchmark | Jai VM | Lua 5.4 | Squirrel | ChaiScript |
|---|---|---|---|---|
| Method invocation | 0 | 1 | 2 | 13 |
| Class creation | 0 | 1 | 2 | 16 |
| Array push/pop | 0 | 2 | 3 | 17 |
| Map/table insert + lookup | 0 | 1 | 2 | 10 |
| For loop, 100 iters | 1 | 1 | 3 | 12 |
Hot loop 1000 (sum += i) | 9 | 3 | 10 | n/a |
| String concat, 20 iters | 1 | 1 | 3 | 19 |
| Fibonacci(15) | 69 | 25 | 55 | n/a |
BST 15 nodes, idiomatic & | 18 | 8 | 11 | n/a |
- vs Squirrel: the VM wins or ties everything statement-, container- and string-shaped: method dispatch, class creation, containers, short loops, the 1,000-iteration hot loop (9 µs against 10) and string work; Squirrel keeps call-dense recursion (~1.25× on fib(15)) and the script-built BST (~1.6×), where its minimal per-call frames pull ahead, and at whole-program scale it runs Richards, DeltaBlue and the spatial hash faster (the benchmark apps below).
- vs Lua 5.4 (via sol2): the VM wins or ties statements, containers, method dispatch, class creation, range-for and null-checks; string work is level; Lua keeps the hot-loop row (~3×), call-dense recursion (~2.8× on fib(15)) and the script-built BST (~2.3×).
- vs ChaiScript: a clean sweep across every live row: method dispatch, class creation, push/pop and map round to 0 µs on the VM against ChaiScript's 13, 16, 17 and 10, and the loop and string rows run 12–19× faster.
The kernel suite. The sixteen interpreter kernels of daScript's
dasProfile (recursion, loops, n-queens, mandelbrot,
n-bodies, spectral norm, particles, sha256, a treap, sort, dictionary, exp, string/float conversion, host
calls) run under dasProfile's protocol in the same comparison harness: each sample is one process that
repeats the kernel for a 0.5 s budget, each cell the median of five, implementations interleaved,
checksums equal across implementations, reference machine. Against Lua 5.5.1, idiomatic typed JaiScript on
the VM takes a geometric mean of 1.09× Lua's time and the same algorithms with
var everywhere 1.35×. The VM's widest wins are mandelbrot (0.04×: two float locals
against Lua's complex-number tables), sha256 (0.12×), float-to-string (0.30×) and string-to-float
(0.88×); its widest losses are the treap (5.5×), host calls (4.4×), n-queens (3.3×), the
dictionary (2.6×), n-bodies (2.1×) and recursive fibonacci (1.8×). LuaJIT's JIT runs the suite
at 0.01–0.73× Lua's time.
The benchmark apps. Four general programs, each one algorithm written the ordinary way in every language and hash-checked: Richards (an OS scheduler simulation: polymorphic methods over linked objects), DeltaBlue (a constraint solver: a deep class hierarchy and virtual dispatch), a bytecode VM written in the script (a dispatch loop over integer arrays) and a spatial hash over 100,000 entities. Each cell is the median of seven processes, the implementations interleaved, on one P core, 2026-11-04; each program times its own workload, so start-up and compilation stay out.
| app | Jai VM | Lua 5.4 | Squirrel 3.2 | VM / Lua | VM / Squirrel |
|---|---|---|---|---|---|
| Richards | 788 ms | 334 ms | 609 ms | 2.36 | 1.29 |
| DeltaBlue | 948 ms | 398 ms | 588 ms | 2.38 | 1.61 |
| bytecode VM | 673 ms | 744 ms | 1,104 ms | 0.90 | 0.61 |
| spatial hash | 2,644 ms | 972 ms | 1,130 ms | 2.72 | 2.34 |
The script-level dispatch loop over typed arrays runs ahead of both; programs made of small methods over shared objects, and the spatial hash's map of per-cell arrays, run at about 2.4–2.7× Lua's time. The per-call and per-object costs those programs pay are the VM's slowest ground.
Whole games, measured
Microbenchmarks reward fast statements; games reward fast frames. Four complete programs are
measured end-to-end: DOOM, GLOOM, the demoreel (the whole reel on both backends, and its OSSUARY scene against the other runtimes) and Gator Quest's simulation. All four
ship in examples/ and reproduce on your machine; the Lua, Squirrel and Luau ports they are timed
against live in the separate comparison harness. JaiDoom ships as
source and downloads its game data on first launch: the freely distributable Freedoom IWADs and the DOOM
shareware episode, from their public releases (Examples).
| workload | measured | the takeaway |
|---|---|---|
DOOM, 2026-11-04: E1M1 on the VM, textured software render at
120×78 plus status bar, six monsters awake and firing, the full weapon/powerup/lighting roster,
300-frame autopilot play session (p7_bench.jai, three runs, one P core); reference machine | ~1.3 ms/frame (sim ~0.39, render ~0.71, blit ~0.21) | more than 21× inside DOOM's 28.5 ms (35 fps) frame budget, with the enemy brains written as coroutines and the movers and weapons as classes |
| DOOM timedemo, 2026-11-04: 3,984 recorded tics of E1M1 at 120×78, one full replay warming the same engine before the measured replay; one P core | 1.389 ms/tic
(about 20× real time) layout control: 1.387, -0.43% to -0.05% | Six runs per build in three interleaved rounds. The layout control is the same program relinked with unused padding code, so a change smaller than its spread is code layout, not speed. Both replay hashes match 1746772898. The script timer excludes level/demo setup. |
| GLOOM, 2026-11-04: 600 measured ticks after 600 warm-up ticks, seed 666, 100×40 quad output. One P core for serial configurations; eight for parallel JaiScript. MSVC Release with LTCG, Core Ultra 9 285HX. |
JaiScript VM serial: 0.634 ms/tick (layout control -0.48% to +1.15%) JaiScript VM 8 workers: 0.461 ms/tick (layout control -4.34% to +7.96%) Lua 5.4.8: 0.397 ms/tick (layout control -0.27% to +1.92%) Lua 5.5.1: 0.389 ms/tick (layout control -0.38% to +1.19%) LuaJIT 2.1 JIT: 0.097 ms/tick (layout control -14.08% to +21.81%) Squirrel 3.2: 1.165 ms/tick (layout control -1.09% to +0.39%) | All frame and state hashes match. Six runs per build in three interleaved rounds; the 25 runs that started with more than one core of background load were discarded. The serial VM takes 1.60× Lua 5.4.8's time per tick and 0.54× Squirrel's. Eight workers take JaiScript from 0.634 to 0.461 ms/tick (1.38×): only the stages whose loops the engine proves safe run in parallel, and the rest stay serial. Lua 5.5.1 runs within 2% of 5.4.8, and LuaJIT's spread is wide. LuaJIT's JIT generates machine code at run time, which some shipping targets (iOS) forbid, so it is a ceiling rather than a target. |
| OSSUARY, 2026-11-04: the demoreel's raymarched skull-spider scene at 100×40, seed 20260705, from scene time 3.0; five measured frames after five warm-up frames and a reboot to the same state in the same runtime. One P core for serial configurations; six for parallel JaiScript. MSVC Release with LTCG. |
JaiScript VM serial: 127.1 ms/frame (layout control -0.82% to +8.57%) JaiScript VM 6 workers: 23.7 ms/frame (layout control -0.66% to +1.70%) Lua 5.4.8: 167.7 ms/frame (layout control -0.60% to -0.05%) Squirrel 3.2: 388.5 ms/frame (layout control -5.37% to -1.58%) | Every frame hash
matches. Six runs per layout in three interleaved rounds. The serial VM takes
0.76× Lua 5.4.8's time per frame and 0.33× Squirrel's; six workers trace the rows in parallel
for 5.4×. skull_natural.jai, the same scene written the way a user would write it, runs
3.8% slower serial (131.9 ms/frame). |
Demoreel, 2026-11-04: the whole reel headless (jai_demoreel --smoke --workers 1):
seventeen truecolor scenes and their transitions at 100×40 cells (100×80 half-block pixels), 1,700 frames,
seed 20260705, fixed dt 1/30. Five runs, each on a fresh copy of the executable, on one P core; the median is shown. MSVC
Release with LTCG, Core Ultra 9 285HX. | JaiScript VM: 9.9 ms/frame (best 9.87) | Every run prints the same frame-stream hash (006d304a70772e5f). On one core the VM runs the whole reel in under a third of the reel's 33.3 ms frame budget at its 30 fps pacing. |
| Gator Quest, 2026-11-04: the smoke session's simulation (mobiles, spawners, combat, items, quests, bots), seed 1, 6,000 ticks with 4 bots; the in-script tick loop is timed, world generation is not. One process per sample on one P core, five interleaved rounds. |
JaiScript VM: 1.354 ms/tick Lua 5.4.8: 1.151 ms/tick Lua 5.5.1: 1.151 ms/tick Luau 0.741 interpreter (-O2): 1.126 ms/tick Luau 0.741 native (-O2 --codegen): 0.655 ms/tick LuaJIT 2.1 interpreter (-joff): 0.777 ms/tick LuaJIT 2.1 JIT: 0.190 ms/tick | Every run's STATE_HASH matches. Medians of five; every range is within 1% of its median except Luau native code's (-0.2% to +3.3%) and LuaJIT's JIT (-2.5% to +2.5%). The VM takes 1.18× Lua 5.4.8's time per tick here, and 1.21× on the shorter 2,000-tick, 2-bot session (0.68 against 0.56 ms/tick). The Lua twins port the simulation only; rendering, audio, netplay and save/load are not in the measured session. |
The five ports say something beyond the numbers: the same enemy brain is ~23 lines of coroutine or generator method in JaiScript, Lua, Squirrel, and Python. In ChaiScript, which has no coroutines, it is a 50-line hand-lowered state machine. Expressiveness parity is table stakes; the differentiators are speed and what the language lets you say directly.
Style is near-free, and provably so. GLOOM exists in a second complete flavor,
examples/gloom_idiomatic, a full rewrite in modern class-based style: subsystem classes, an
enemy hierarchy with virtual coroutine brains, typed classes for the record types instead of string-keyed maps. It produces bit-identical state and
frame hashes to the flat original on both backends at every worker count, and measures within noise on
the interpreter and ~4–7% behind on the VM. The moral for code review: choose the architecture you want
to maintain; the per-pixel hot stages are held to the flat shape by the parallel-admission rules either
way, and that is where the time goes.
Measuring yourself
Build the test executable with the performance suites enabled (-DJAISCRIPT_ENABLE_BENCHMARKS=ON at configure time, Release; see docs/BUILD.md, "Comparison benchmarks") and then:
jaiscript_tests "Performance Benchmarks" --verbose [--backend=vm]These suites time JaiScript alone. The head-to-head numbers against Lua, Squirrel and ChaiScript come from the separate comparison harness, which is not part of this package.
Measure Release builds only. A Debug build of the engine runs script ~25–40× slower, and no
number taken from one means anything.
Run min-of-3 and compare medians; the integer-µs harness cannot resolve sub-µs changes (use a
dedicated ns micro-bench for those). For end-to-end embedding costs (engine create, parse, compile,
warm dispatch, crossover points) see the cost sheet
above. The whole-game harnesses that ship reproduce too: jai_gloom.exe --smoke and
jai_demoreel --smoke run each game headless on both backends and print ms per tick or frame
next to the parity hashes (each example's README has the flags).
To see where the VM spends its operations, configure with JAISCRIPT_VM_COVERAGE: the VM then counts every instruction where it runs (a cell block, the general form typed or untyped) and, for the general form, the reason the compiler left it there, by function and source line, the parallel workers included. engine::vm_coverage_report(top) ranks the reasons by executed operations with their top sites (default 25), vm_coverage_summary() gives the totals as numbers for a harness that aggregates many runs, and vm_coverage_reset() zeroes the counters so a warm-up drops out. engine::vm_coverage_available() says whether the build counts; without the define the other calls throw std::logic_error. The runner's --coverage=FILE writes the report at exit.