Profiling Memory in Emscripten with the Memory Profiler
This page answers one task: a C or C++ application compiled with Emscripten uses more memory than expected, or its memory grows over time, and you want to see inside the heap — what is allocated, where from, and how fragmented it is — without leaving the browser.
Prerequisites
- [ ] A C or C++ project built with Emscripten (emcc).
- [ ] A development build configuration you can add flags to.
- [ ] A scenario that reproduces the memory behaviour you want to understand.
What the memory profiler shows
Emscripten ships a simple memory profiler, enabled at link time with --memoryprofiler. It instruments the generated JavaScript to track allocations
made through malloc, free and related functions, and draws an overlay on the page that updates while the application runs. The overlay shows the
total heap size and how much of it is in use, a visual map of the heap with allocated and free regions, static data and stack usage, and — the most
useful part — a list of the call sites responsible for the most live allocations, grouped by the JavaScript-side stack trace at the time of allocation.
It is a development tool: tracking every allocation and capturing stacks is slow, and the overlay is meant for a person looking at it. It complements,
rather than replaces, LeakSanitizer (which reports unfreed allocations at exit with native-style stacks) and mallinfo (which gives numbers you can log
or assert on).
Step 1 — build with the profiler
emcc src/*.cpp -O1 -g2 --memoryprofiler \
-sALLOW_MEMORY_GROWTH=1 \
-o app.html
-g2 keeps function names so stack traces in the overlay are readable. Use -O1 or -O2 rather than -O3 with heavy inlining, which merges call sites.
Output to .html is simplest, because Emscripten’s default shell page has room for the overlay; with a custom shell, the overlay attaches to the page
body.
Step 2 — run the scenario and watch the overlay
Open the page and run the scenario. The heap usage line shows whether memory plateaus or keeps rising. The heap map shows how allocations are distributed: a few large blocks, or a dense field of small ones. Watch it during the operation you care about — loading a level, opening a document, processing a batch — and again after it ends, when memory should be released.
Step 3 — find the call sites holding memory
The call-site table lists stacks with the number and total size of live allocations from each. Sort by size. A stack with many live allocations after an
operation finished is a leak candidate or a cache that grows without bound. Because stacks are captured with the JavaScript Error().stack mechanism,
they include the Wasm functions on the stack at allocation time, by name if -g2 was used. For more precise attribution — including inlined frames and
source lines — reproduce the scenario in a native build or use LeakSanitizer.
Step 4 — look for fragmentation
The heap map makes fragmentation visible: a heap that is mostly free but chopped into many small gaps between live blocks. Fragmentation forces the
allocator to grow memory for large requests even when total free memory is ample, and because Wasm memory never shrinks, the growth persists. If the
map shows this pattern, consider pooling objects of common sizes, allocating long-lived and short-lived objects from different arenas, or switching
allocator (-sMALLOC=emmalloc is smaller but can fragment differently from dlmalloc; measure both).
Step 5 — confirm with numbers
The overlay is visual; decisions need numbers. Log mallinfo() totals at defined points — after startup, after each operation, after cleanup — and
compare across repetitions:
#include <malloc.h>
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE void log_heap(const char* label) {
struct mallinfo mi = mallinfo();
emscripten_log(EM_LOG_CONSOLE, "%s: in use %d bytes, free %d bytes, heap %d bytes",
label, mi.uordblks, mi.fordblks, mi.arena);
}
In-use bytes that return to the same value after each cycle mean no leak; in-use bytes that rise mean a leak or growing cache; a heap that grows while in-use bytes stay flat means fragmentation. Then confirm leaks with LeakSanitizer, which reports each leaked allocation with a stack at exit, as described in detecting leaks in Emscripten with LeakSanitizer.
Stack usage
The overlay also reports stack usage against the configured STACK_SIZE. A high-water mark close to the limit means deep recursion or large stack
arrays are close to overflowing — which, depending on layout, either traps or corrupts data. Increase STACK_SIZE or reduce stack usage (move large
arrays to the heap) before it becomes an intermittent crash. -sSTACK_OVERFLOW_CHECK=2 adds precise checking in debug builds.
Limits of the tool
The profiler tracks allocations through Emscripten’s malloc; memory allocated by other means — a custom allocator on top of a big malloc block, a
Rust component with its own allocator, memory used directly by static data — appears as one allocation or not at all. Its overhead makes timing
measurements meaningless while it is enabled, and it only works in the browser page it draws on, not in workers or Node. For workers, log mallinfo
from the worker and inspect the numbers instead.
A repeatable profiling routine
Ad-hoc profiling finds the obvious problems; a routine finds the rest. Define a short script of actions that represents a typical session — start, load
three documents, edit, close them, idle — and run it the same way each time you profile, ideally automated with a small JavaScript driver that calls
into the application. Note heap totals and the top five call sites at fixed checkpoints in the script. Comparing those notes between releases shows
whether a change increased memory use, and where. The automated driver can also run without the profiler, logging mallinfo at the same checkpoints, so
the same routine produces both the investigative view and numbers suitable for CI.
Memory used by Emscripten’s runtime itself
Not all memory belongs to your code. Emscripten’s runtime allocates for file-system emulation (MEMFS keeps files in memory, so preloaded data and files
written at runtime occupy the heap), for printf buffers, for pthread stacks in threaded builds, and for exception handling. The profiler attributes these
allocations to runtime functions on the stack. Large MEMFS usage is common in ports of desktop applications that write temporary files; moving those to
IDBFS or OPFS-backed storage, or avoiding the files altogether, can release substantial memory. Preloaded packages (--preload-file) are copied into
MEMFS, so a 50 MB asset package costs 50 MB of heap for the whole session.
Sharing findings
The overlay is a visual tool, and screenshots are the easiest way to share what it shows, but they lose detail. When reporting a memory problem to a
team, include the mallinfo numbers at each checkpoint, the top call sites with their sizes, the build flags used, and the scenario script, so others can
reproduce the measurement exactly and verify a fix against the same baseline.
Expected output
The overlay shows heap usage rising by 2.1 MB per level load and not returning after unload; the call-site table attributes 1.9 MB of it to
TextureCache::load; the fix bounds the cache; mallinfo in-use bytes return to 48 MB after each unload; and the heap map shows no persistent
fragmentation after twenty cycles.
Gotchas
- Profiling optimised builds with heavy inlining. Call sites merge. Use
-O1/-O2with-g2. - Trusting timings with the profiler on. It slows allocation. Measure performance separately.
- Expecting it in workers. The overlay draws on the main page. Log
mallinfoin workers. - Custom allocators on top of
malloc. Show as single large blocks. Instrument them separately. - Ignoring stack usage. A near-full stack becomes an intermittent crash.
- Forgetting runtime allocations. MEMFS files and preloaded packages live on the heap. Check runtime call sites too.
Performance note
With the profiler enabled, the level-loading scenario took 4.6 s instead of 1.2 s due to per-allocation tracking and stack capture — fine for investigation, unusable for performance work.
Frequently Asked Questions
Does --memoryprofiler work with Embind and C++ classes?
Yes — it tracks the underlying malloc calls regardless of what made them.
Can I use it with MODULARIZE?
Yes, though the overlay expects a page to draw on.
Does it show JavaScript heap usage? No — use DevTools heap snapshots for the JavaScript side.
Is it available for Rust? No; for Rust, use a counting allocator or allocator statistics.
Why does the heap include my asset files?
Files preloaded with --preload-file or written at runtime live in MEMFS, which keeps them on the heap for the whole session.
Related
- Detecting leaks in Emscripten with LeakSanitizer — precise leak reports.
- Tracking linear memory growth over time — growth trends.
- Measuring allocator fragmentation in Wasm — fragmentation in depth.
- Building an in-app memory panel for Wasm — a custom overlay.
← Back to Memory Profiling & Leak Detection