| name | moon-pprof |
| description | Use when investigating MoonBit performance, locating CPU/allocation/retained-heap hot spots, comparing baseline vs patched, converting profiler formats to/from pprof/Chrome trace/Speedscope/folded stacks, or profiling MoonBit code across native / wasm-gc / wasm / js backends. Trigger on requests like 'find what's slow / allocating', 'show retained heap', 'convert this profile', 'make it visible in go tool pprof', 'why is X expensive', 'profile this binary', 'where do allocs go', 'does this PR actually improve perf', or whenever a MoonBit benchmark or upstream-core / x / async investigation is on the table. Out of scope: non-MoonBit code that doesn't go through `moon build`, except for generic profile format conversion commands. |
moon-pprof — MoonBit performance profiler
A single CLI that produces pprof from all four MoonBit backends, plus
allocation profilers for wasm / wasm-gc / native, plus a baseline ↔
patched harness for upstream PR experiments. Built on top of
samply / wasmtime GuestProfiler / Node V8 inspector — the value is
that everything lands in the same pprof schema with MoonBit
demangling applied.
Decide what you actually need
Don't reach for the heavy tools first. Pick by what you want to learn:
| You want to know | Run |
|---|
| "Where does this MoonBit binary spend CPU on native?" | moon-pprof memprofile-native <exe> (uses alloc count as a CPU proxy on macOS / Linux) or perf record … && moon-pprof perf2pprof on Linux |
| "Where does this wasm spend CPU?" | moon-pprof profile <wasm> (wasmtime + GuestProfiler) |
| "Where does the js backend spend CPU?" | runners/v8/run-js.mjs … && moon-pprof cpuprofile2pprof |
| "What's allocating in wasm / wasm-gc?" | moon-pprof memprofile <wasm> |
| "What's allocating in native?" | moon-pprof memprofile-native <exe> |
| "What is still live at process exit?" | moon-pprof memprofile-native <exe> --retained --sample-rate 1 |
| "Can I see alloc activity over time?" | moon-pprof memprofile <wasm> --trace-out alloc.trace.json |
| "Can I see off-CPU / blocking folded stacks in pprof?" | moon-pprof folded2pprof wait.folded wait.pb.gz |
| "Can I move profiles between UIs?" | see references/profile-formats.md |
| "Did this patch actually help?" | moon-pprof summary --diff baseline.pb.gz patched.pb.gz |
| "Compare baseline vs patched across all backends" | moon-pprof bench with --baseline-moon / --patched-moon / --mooncakes-baseline / --mooncakes-patched |
When in doubt: start with summary, drill down only when a hot
site is in question. go tool pprof -http :8000 <file> is the
graphical fallback.
The five canonical workflows
Most past investigations boiled down to one of these. They're
documented in detail in references/:
- Allocation hunt (
references/allocation-hunt.md) — wasm or
native, find which user-level function the bytes flow into.
- CPU hot-spot identification (
references/cpu-hotspots.md) —
wasmtime / samply / perf paths.
- Baseline ↔ patched verification (
references/baseline-patched.md)
— try a patch against moonbitlang/core or a .mooncakes/ dep,
measure, decide whether to PR.
- Server / long-running binary (
references/long-running.md) —
memprofile-native --duration N, plus perf record --weight
guidance.
- Cross-backend bench (
references/cross-backend-bench.md) —
prove the same MoonBit code behaves consistently (or doesn't)
across native / wasm-gc / wasm / js.
- Profile format conversion (
references/profile-formats.md) —
pprof ↔ Chrome trace / Speedscope / folded stacks, off-CPU import,
retained heap sample types, and which UI can read each output.
Optimisation patterns worth knowing
Hard-won from prior PRs to moonbitlang/core / x / async and
the in-repo investigations:
for c in self.view(...) desugars to an Iter heap alloc per call.
Manual UTF-16 loops via self.unsafe_get(i) skip it — see
PR #3635 (to_lower).
Hash::hash default implementation allocates a Hasher struct.
Override per-type with the xxHash math inlined — PR #3634
(-56 % hashset, -98 % hashmap_update).
priv struct X { ... } does not automatically stack-allocate.
Apply #valtype to multi-field non-generic priv structs.
Option<X> wraps even when X is #valtype. NaN sentinel pattern
(Double::nan()) or a dedicated is_valid bit avoids the box —
PR #3633 (try_fast_double).
async fn allocates its coroutine state per call on native.
Wrapping helper async fns can backfire (Sender::write_bytes
experiment, net +10 % allocs/req). See
notes/async-server-alloc-report.md for the full story.
Int::to_string, StringBuilder::write_*, etc. don't always go
through moonbit_malloc_inlined, so memprofile-native will
under-count them. Cross-check with overall RSS or valgrind
callgrind if numbers look suspicious.
Gotchas before recording
memprofile --sample-rate N (N>1) is the right move on large
workloads — within 0.1 % of exact top sites but 20–70× faster.
memprofile-native on Linux needs the relink output to have
-rdynamic -ldl -lpthread (handled automatically since
b115521) so dladdr sees MoonBit symbols.
memprofile-native --retained emits inuse_objects/count +
inuse_space/bytes. Use --sample-rate 1 for exact retained heap;
larger rates are sampled estimates.
memprofile --trace-out emits Chrome trace allocation activity,
not true runtime GC pause events. Treat it as "when allocations
happened", not "when GC stopped the world".
folded2pprof defaults to delay/microseconds, so go tool pprof
shows Type: delay and duration in seconds. Use it for folded
off-CPU / blocking input, not for ordinary CPU pprof round-trips.
perf record needs --weight for perf script to emit periods.
Without it every sample becomes period=1 and the pprof has no
wall-time scale. moon-pprof perf2pprof warns on this.
perf may show [unknown] for MoonBit frames inside Docker
Desktop (virtiofs path quirk) even though the binary has full
.symtab. Try perf script --symfs=<root> or work outside the
container if possible.
- moon's build dir changed from
target/ to _build/ — older docs
may say target/ for the .exe / .wasm location.
moon clean between baseline and patched runs is mandatory if you
swapped toolchains or .mooncakes/.
Reading a summary
moon-pprof summary <file> rolls up self time AND classifies
memory-management frames (mimalloc, refcount, dealloc) separately.
The "Memory-management self time" % is how much CPU is in pure
alloc/free plumbing — if it's > 30 %, allocation reduction beats
algorithm tweaks. Past investigations on core showed mem-mgmt
self-time of 50–60 % on json / hashmap workloads, which is what
made the Hash + numeric-parse PRs land big numbers.
summary --diff baseline.pb.gz patched.pb.gz prints per-function
delta in both directions, sorted by absolute change. Use this
before drafting a PR description so the numbers in the description
match what reviewers will see if they re-run.
When you'd skip moon-pprof entirely
- Micro-bench where wall-clock noise dominates →
hyperfine /
criterion instead.
- Suspected runtime / mimalloc bug →
valgrind callgrind for an
instruction-count view that's invariant under load.
- "Why does this fail to compile" type questions → not a pprof
question, drop the skill.