Conversation
Wasmtime heap-allocates each instance's VMContext (~450 KB for this runtime's ~10k function references) and frees it on teardown. With glibc's defaults that block is the top of the heap, so every execution trims it back to the kernel and grows it again: three brk calls and ~100 page faults on the request path. Set M_TRIM_THRESHOLD=-1 and M_TOP_PAD=64MiB once when the engine is created (Linux/glibc only, ERYX_GLIBC_MALLOC_TUNING=0 opts out) and document it in the performance guide. Cold `pass` on the pooling allocator: 1279 µs -> 1140 µs, 344 -> 248 faults per execution. Also add ERYX_PROFILE_TRACE=0 to profile_stateless so the harness can measure without sys.settrace overhead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Closing for now. The effect is real but small on the paths that matter, and the shape is wrong: the allocator is the embedding binary's decision, not something the library should set process-wide with What the follow-up measurements showed (stock wasmtime 48.0.1, Harness (
Criterion
All within noise: ~4 ms of each of those iterations is the guest re-running If we still want the ~100 heap faults per execution gone afterwards, the way to do it is The |
Summary
Wasmtime heap-allocates each instance's
VMContext(~450 KB for this runtime's ~10k function references) and frees it on teardown. With glibc's defaultmallocsettings that block is the top of the heap, so everySandbox::execute()trims it back to the kernel on free and grows it again on the next instantiation: threebrkcalls and roughly 100 page faults per execution, all on the request path.This sets two glibc tunables once, when the process-wide engine is created (
AllocatorSettings::apply_malloc_tuning, called frombuild_engine):M_TRIM_THRESHOLD = -1— never return freed heap to the kernel;M_TOP_PAD = 64 MiB— grow the heap in large steps so the growth happens once.Both are process-wide, which is why it is documented in
guide/performance.mdnext to the other engine-level knobs and has an opt-out,ERYX_GLIBC_MALLOC_TUNING=0. It only compiles in on Linux + glibc with theembedded/preinitfeatures (themalloptcall is the crate's usual per-item#[allow(unsafe_code)]underdeny(unsafe_code), likefrom_precompiled); elsewhere it is a no-op. A process using jemalloc/mimalloc/musl is unaffected.The cold-start spike bounded this first with
MALLOC_TRIM_THRESHOLD_/MALLOC_TOP_PAD_in the environment and got identical numbers, so a wasmtime-side instance pool (the original idea for this cost) is not needed.Also adds an
ERYX_PROFILE_TRACE=0knob toexamples/profile_stateless.rsso the harness can measure withoutsys.settraceoverhead (trace collection is on by default inSandboxBuilderand dominates anything that is notpass).Stacked on #411 (
perf/instantiation-overhead) because that is where the harness and the allocator settings live.Benchmarks
profile_stateless, stock wasmtime 48.0.1, Ryzen 9 7950X, 2000 executions, median of 3, pooling allocator (default). "off" =ERYX_GLIBC_MALLOC_TUNING=0, i.e. current behaviour.pass, trace collection onpass, trace collection offjson+string.Template), trace onERYX_WARM_INSTANCES=1, 2 ms gap)The ~100 faults removed are exactly the heap ones; the remainder are linear-memory pages that wasmtime 48.0.1's
PAGEMAP_SCANreset decommits (its 32-region cap — separate wasmtime fix from the same spike). With that fix in place the same change measured 502 → 378 µs (−25%) on coldpassand left zero page faults per execution.Testing
cargo nextest run --workspace --features embedded --cargo-profile release: 639 passed, 0 failed (includes newmalloc_tuning_is_on_by_default_and_can_be_disabled; invalid values are rejected like the otherERYX_POOL_*knobs).cargo clippy --workspace --all-targets --all-features -- -D warningsandcargo clippy -p eryx(default features,forbid(unsafe_code)path): clean.cargo fmt --all --check: clean.🤖 Generated with Claude Code