Skip to content

feat(symcache): Build symcaches for WASM modules from Emscripten source maps - #1070

Open
d2anamaria wants to merge 3 commits into
masterfrom
ana/feat/wasm/sourcemaps
Open

d2anamaria wants to merge 3 commits into
masterfrom
ana/feat/wasm/sourcemaps

Conversation

@d2anamaria

Copy link
Copy Markdown

Summary

Emscripten can ship a Source Map v3 file next to a .wasm module instead of DWARF. This change
turns such a module plus its map into a SymCache, so WASM builds without DWARF become
symbolicatable. Everything sits behind the wasm-sourcemap feature flag, which keeps the
sourcemap dependency out of the way for consumers that don't need it.

Nothing calls this yet. The symbolicator wiring is a follow-up.

Why a source map works as debug info

An Emscripten WASM map abuses the source map format:

Source map field Normal JS meaning Emscripten WASM meaning
destination line line in the minified file always 0
destination column column in the minified file byte offset into the module
source + source line original location original location

Why SymCache

That byte offset is already a symcache key. SourceMapCache is keyed by a line and a column
instead, so using it would mean faking a position and unwinding it on every lookup. Reusing SymCache
means WASM frames flow through the existing address lookup path, with no new format and no new
lookup API.

Inputs

  • The .wasm module is required, not optional. A map has no notion of a function.
  • Function bounds and names come from the module; lines come from the map.
  • Both are joined by address.

What lands in the cache

  • Every function body, even one the map says nothing about. Partial info is still useful, and it
    stops a lookup from bleeding into the previous function.
  • Unnamed bodies get wasm-function[<index>], matching what browsers show.
  • A range with no known location resolves to the function with no file and no line, instead of
    inheriting the location before it.
  • Mappings outside any function body are dropped. They fall in the padding between bodies.
  • An Emscripten one-field segment marks a hole and closes the preceding line range.

WASM object changes

Exact function body bounds and WASM function indices are now available. The existing symbol
iteration stretches each body to be contiguous with the next, hiding the padding between bodies.
Joining a map to stretched bounds would let padding inherit a line from the function before it.

Debug ids are out of scope

This change derives no identifier. It reads WasmObject::debug_id() as-is.

An earlier revision also made the build_id custom section length-prefix aware, per the
tool convention. That is
correct in isolation but breaks matching: @sentry/wasm reads the same section without stripping
the prefix, so a corrected reader here would file uploads under an id no event carries. The parsing
change was removed and the whole question moved to
#1069, which needs a coordinated decision
across symbolic, the SDK, and the CLI.

Whichever way that lands, this change needs no rework — the embedded id follows debug_id()
automatically.

Limitations

  • Only plain maps. Index maps and Hermes maps are rejected.
  • A map referencing an undeclared name is rejected.

- reuse SymCache so native symbolication, caching, and frame shape stay unchanged
- reject SourceMapCache: native path has no line/column lookup and still needs the wasm binary for names and bounds
- join source map byte offsets to wasm function bodies; map alone cannot define functions
- prefer exact body sizes over stretched symbol ranges so padding cannot inherit a line
- treat one-field map segments as unmapped holes, not missing source data
- parse emscripten length-prefixed build_id so wasm debug_id matches injected map debugId
- gate behind wasm-sourcemap; DWARF and existing wasm paths are untouched
- add minimal wasm/map fixtures for padding, holes, and past-end tokens
- cover five-field maps with empty vs populated names arrays
- reject JS-shaped maps so byte offsets are not treated as wasm addresses
- add wasm_sourcemap_debug example for manual lookup on local wasm/map pairs
- keep raw custom-section payload so debug_id matches @sentry/wasm at runtime
- stripping the ULEB128 prefix would file uploads under a different id than events
- delete parse_build_id and its five unit tests; no callers remain
@d2anamaria
d2anamaria requested a review from a team as a code owner September 18, 2026 16:27
@github-actions

Copy link
Copy Markdown
Fails
🚫 Please consider adding a changelog entry for the next release.
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

## Unreleased

### Features

- Build symcaches for WASM modules from Emscripten source maps ([#1070](https://github.com/getsentry/symbolic/pull/1070))

If none of the above apply, you can opt out of this check by adding #skip-changelog to the PR description or adding a skip-changelog label.

Generated by 🚫 dangerJS against aeebbc9

@loewenheim

Copy link
Copy Markdown
Contributor

Once #1081 is merged the logic for preserving exact function sizes will be unnecessary (it will just work that way in general).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants