Memory Footprint

How memory-efficient are these libraries at runtime? Three things to know before reading the charts.

What we measure

For each iteration of every scenario, the harness:

  1. Snapshots heap and RSS via Deno.memoryUsage() / process.memoryUsage() before the scoped scenario run.
  2. Runs the scenario, then snapshots again — that gives heapUsedDelta and rssDelta.
  3. Forces a major GC (globalThis.gc() via --expose-gc on Node/Deno; Bun.gc(true) on Bun) and snapshots heap a third time as heapUsedAfterGc.
  4. Peak memory during the iteration is collected by the scenario itself: each scenario calls ctx.markPeak() at its structural high-water moment (the leaf of recursion, the moment after all events have been dispatched but before teardown). The harness combines those marks with the before/after snapshots and stores the max as heapUsedPeak / rssPeak. Because the marks are placed deterministically, peak capture works equally well for sub-millisecond recursion and longer-running events scenarios — no sampling timer to fight the event loop.

The forced-GC snapshot and peak snapshots are taken outside the timed window, so they don't contaminate latency.

Two heap metrics, two questions

RSS varies wildly across runtimes

The Trimmed-mean RSS Δ chart looks completely different depending on the runtime, and most of the difference is allocator behavior, not library behavior:

The RSS chart is included for completeness; the post-GC heap chart is the one you actually want for cross-library comparison.


Median Post-GC Retained Heap (relative)

Per-library, faceted by scenario type. Bars show how much more memory each library retains compared to the most efficient library in the same scenario type — the lightest library sits at 0 and others show their cost above it. The absolute floor is around 37-41 MB across the board (V8 + module/runtime + JIT code + scenario state); the differences between libraries (0.1-3 MB) are the actual signal but get visually drowned out when plotted as absolute values.

The bar height shows each library's heap above the most efficient one in the same scenario type — the lightest library sits at 0 by definition. The number above each bar is its absolute median post-GC heap in MB, so you can read the baseline value without hunting through the data.

Median Peak Heap During Iteration (relative)

Median high-water-mark of heap usage during each iteration, again shown above the lightest library in the same scenario type. Schema v5 added a ScenarioCtx.markPeak() hook that scenarios call at their structural peak (the leaf of recursion, the moment after all events are dispatched but before teardown). Peaks are deterministic snapshots, not sampled — the harness combines explicit marks with the before/after snapshots and keeps the max.

This answers a different question than the post-GC chart above: what's the working-set high-water mark while the scenario is running, vs. what's left over after a major GC. Events scenarios should show much higher peaks than retained heap because their listener chain is alive during dispatch and freed at teardown; short recursion scenarios should look similar to their retained heap because there's barely any time between peak and end.

Peak ≥ post-GC retained for any given scenario. The gap between them is the per-iteration "working" allocation — temporary objects allocated during the scenario and reclaimed by the time the forced GC runs.

Trimmed-mean RSS Δ per Iteration

Process-wide RSS change from start to end of each iteration, with a 10% trimmed mean across all measured iterations: sort, drop the highest and lowest, average the rest. We can't use the median because V8's allocator grows arenas in 128 KB chunks — per-iteration deltas are quantized to either 0 or +128 KB and the median collapses to 0. We can't use a plain average because cold-start arena commits on the first measured iteration can be tens of MB and dominate the result for the rest of the run. The trimmed mean is the middle ground: discards the cold-start outlier and any one-off GC reclaim, keeps the steady-state signal.

Note Bun runs negative on this chart for many scenarios because mimalloc decommits pages back to the OS after scoped() teardown.


Caveats

← Back to Overview