Debugging Wasm in Firefox DevTools

This page answers one task: debug and profile a WebAssembly module in Firefox — useful when a bug reproduces only in Firefox, when you want SpiderMonkey’s view of performance, or when Firefox is simply your browser of choice.

Prerequisites

  • [ ] Firefox (current release) with DevTools.
  • [ ] A module built with debug information — DWARF for C, C++ and Rust, or a source map — for source-level work.
  • [ ] The page served from a local dev server, so DevTools can fetch sources.

What Firefox offers for WebAssembly

Firefox’s debugger treats WebAssembly modules as sources. Without debug information, a module appears as disassembled WebAssembly text, one listing per module, where you can set breakpoints on instructions, step, and inspect locals and the operand stack. With debug information, the debugger maps instructions back to the original source files and shows those instead, so you step through Rust or C lines rather than Wasm instructions.

Firefox historically supported two kinds of mapping: source maps, a JSON format that maps byte offsets to source positions (produced by Emscripten’s -gsource-map), and DWARF embedded in custom sections. Source maps give line-level stepping and file navigation; DWARF additionally describes variables and types. The Firefox Profiler, a separate tool reached from the Performance panel, records and analyses performance with full support for Wasm frames.

Debug information formats in Firefox Without debug information, Firefox shows disassembled Wasm text. A source map adds file and line mapping for stepping. DWARF adds variable and type information on top of line mapping. no debug info disassembled WAT per module breakpoints on instructions locals by index always available source map (-gsource-map) original files and lines step by source line variables not described lightweight, line-level DWARF (-g) files, lines and scopes variables with types larger build output full source-level

Step 1 — build with debug information

For Emscripten, choose the format explicitly:

# source map: small, line-level stepping
emcc -O1 -gsource-map --source-map-base http://localhost:8080/ src/*.c -o dist/app.js

# DWARF: full variable information
emcc -O1 -g src/*.c -o dist/app.js

--source-map-base tells the module where to find the .wasm.map file; it must be reachable from the page. For Rust, a dev build with wasm-pack keeps DWARF: wasm-pack build --dev --target web. Keep optimization low (-O1 or opt-level = 1) while debugging so variables are not optimized away.

Step 2 — find the module in the Debugger

Open DevTools (F12), choose the Debugger panel, and look in the Sources tree. Modules appear under a wasm:// entry; with debug information, the original source files appear under their paths. Open one, click a line number to set a breakpoint, and trigger the code from the page.

When execution pauses, the right-hand pane shows the call stack — Wasm and JavaScript frames interleaved — and the Scopes section shows variables. For a Wasm frame without DWARF, scopes list the function’s locals by index and the current operand stack values; with DWARF, they show named variables.

// a quick way to reach the paused frame: call the export from the console
const { instance } = window.__app;                // expose the instance during development
instance.exports.process(ptr, len);              // pauses at your breakpoint

Step 3 — inspect memory and values

The Scopes pane shows locals, but for data in linear memory — buffers, structs behind pointers — evaluate expressions in the console while paused. The memory object is reachable from the instance’s exports:

// while paused: show 64 bytes at the pointer held in a local
const mem = new Uint8Array(window.__app.instance.exports.memory.buffer);
mem.slice(ptr, ptr + 64);

Watch expressions in the Debugger panel re-evaluate on every pause, which is handy for keeping an eye on a buffer region while stepping through a loop. For a richer memory view, Chrome’s memory inspector is more capable, as described in inspecting Wasm memory in Chrome DevTools.

Step 4 — profile with the Firefox Profiler

Open the Performance panel, start recording, perform the slow interaction, and stop; the recording opens in the Firefox Profiler. The Call Tree tab shows Wasm functions by name, and the Flame Graph tab aggregates call stacks:

Running Time (ms)  Self (ms)  Function
        412           3       ▼ onClick                     main.js
        398           2         ▼ process                   wasm-function: app.wasm
        371         288           ▼ convolve                wasm-function: app.wasm
         83          83               simd_dot              wasm-function: app.wasm

The Profiler also shows SpiderMonkey’s compiler activity — baseline and Ion compilation of Wasm functions — on its own markers, which makes it easy to see whether a slow first call was compilation. Profiles can be uploaded to a sharing URL for colleagues, which is convenient for comparing a Firefox-specific slowdown with a Chrome trace.

From a recording to a hot function in Firefox Record in the Performance panel, open the result in the Firefox Profiler, use the Call Tree to find the Wasm function with the most self time, and inspect compiler markers to separate compilation from execution. record interaction Performance panel Firefox Profiler opens automatically Call Tree self time per Wasm function markers baseline / Ion compile

Step 5 — reproduce engine-specific bugs

Most Wasm bugs reproduce in every browser, because module semantics are precisely specified. The ones that do not are worth recognising. Feature support differs: a module using a newer proposal may fail to compile in one engine; check for CompileError mentioning an unknown opcode. Resource limits differ: maximum memory, maximum table size, and stack depth before a “too much recursion” error vary by engine. And JavaScript-side behaviour differs: glue code relying on an API one browser implements differently.

When something fails only in Firefox, compare the module’s features against Firefox’s support first, then look at the JavaScript glue’s behaviour, and only then suspect the engine. The approach in detecting proposal support at runtime turns feature mismatches into a graceful fallback rather than a crash.

A debugging session that crosses the boundary

Most real bugs in Wasm apps involve both sides of the boundary, and the Firefox debugger handles that well because it shows JavaScript and Wasm frames in one call stack. A typical session runs like this. A user reports that exporting a document produces garbled text in Firefox. You set a breakpoint in the JavaScript function that reads the module’s output and reproduce the export. When it pauses, the console shows the bytes the module returned — already garbled — so the problem is upstream. You step into the export call, which drops into the module’s source, and walk through the function that encodes text. A watch expression on the output buffer shows the bytes as they are written, and the bug appears: the code writes UTF-16 code units into a buffer the JavaScript side decodes as UTF-8.

The fix is on one side, but finding it required seeing both. That is the general pattern: start where the symptom is visible, establish whether the data is already wrong when it crosses the boundary, and step to the side where it went wrong. The encoding details that cause this particular class of bug are covered in encoding strings across the Wasm boundary.

Firefox’s conditional breakpoints and logpoints help keep such sessions short. A logpoint — right-click a line, Add log — prints an expression every time a line runs without pausing, which is an easy way to trace values through a Wasm loop without a rebuild.

Expected output

Paused at a breakpoint in a Rust function with DWARF, the Debugger shows:

Call stack
  apply_filter        src/filter.rs:42
  __wbg_apply_filter  app_bg.wasm
  onClick             main.js:58
Scopes
  Block
    strength: 0.75
    row: 118
    pixels: &mut [u8] (len 8294400)

Gotchas

  • Sources tree shows only wasm://. No debug information, or the source map’s base URL is wrong. Check the network request for the .map file.
  • Breakpoints in source files do not hit. The source map points at paths that do not match; open the module’s mapped sources from the tree rather than local files.
  • Variables shown as optimized out. Build with lower optimization while debugging.
  • The Profiler shows wasm-function[N]. The name section was stripped. Rebuild with names.

Performance note

Debug builds are larger and slower: the DWARF build of the example was 5.2 MB against 640 KB stripped, and its unoptimized code ran the filter four times slower. Debugging and profiling call for different builds — debug information and low optimization for stepping, optimized code with names for profiling.

Build size by debug configuration The same module built for release with names only, with a source map, and with full DWARF. module size in KB (source map file excluded) release + names 640 KB -O1 -gsource-map 910 KB -O1 -g (DWARF) 5,200 KB

Frequently Asked Questions

Does Firefox support the DWARF extension Chrome uses? Chrome’s C/C++ DevTools Support is a Chrome extension. Firefox has its own built-in handling; feature parity varies by release, so for complex C++ type inspection Chrome is often stronger.

Can I debug Wasm in a Firefox worker? Yes. Workers appear in the Debugger’s thread list; select one to see its sources and set breakpoints.

Is there a way to break on traps? Enable Pause on exceptions; a Wasm trap surfaces as a RuntimeError and the debugger stops at the faulting instruction.

Can I edit and re-run without reloading? No — a compiled module cannot be patched in place. Rebuild and reload; keep the dev build fast so that loop stays short.

Should I profile in Firefox if my users mostly use Chrome? Profile where your users are first. Firefox profiling is valuable for Firefox-specific complaints and as a second opinion on whether a hot spot is in your code or in one engine’s handling of it.

← Back to Debugging & Profiling Wasm Modules