Skip to content

Add a --symbolicate-wasm arg to profiler-edit.#6008

Open
mstange wants to merge 2 commits into
firefox-devtools:mainfrom
mstange:symbolicate-wasm-script
Open

Add a --symbolicate-wasm arg to profiler-edit.#6008
mstange wants to merge 2 commits into
firefox-devtools:mainfrom
mstange:symbolicate-wasm-script

Conversation

@mstange
Copy link
Copy Markdown
Contributor

@mstange mstange commented May 7, 2026

This allows applying wasm symbols to existing profiles that were captured with a stripped wasm bundle.

The script looks for functions with names of the shape wasm-function[123], which is what Firefox uses when the wasm file doesn't have a names section.

Usage:

yarn build-node-tools && \
node node-tools-dist/profiler-edit.js -i input.json.gz \
--symbolicate-wasm http://host/a.wasm=./a-unstripped.wasm \
--symbolicate-wasm http://host/b.wasm=./b-unstripped.wasm \
-o out.json.gz

I've successfully used it to turn https://share.firefox.dev/4f9O7oh into https://share.firefox.dev/4nd6GKk .

@mstange mstange force-pushed the symbolicate-wasm-script branch from 52ba22e to ab1e4b6 Compare May 7, 2026 19:31
@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

Codecov Report

❌ Patch coverage is 74.45255% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.78%. Comparing base (5783a76) to head (a8608c4).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
src/profile-logic/wasm-symbolication.ts 84.21% 17 Missing and 1 partial ⚠️
src/node-tools/profiler-edit.ts 26.08% 17 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6008      +/-   ##
==========================================
- Coverage   83.82%   83.78%   -0.04%     
==========================================
  Files         328      329       +1     
  Lines       34254    34391     +137     
  Branches     9572     9607      +35     
==========================================
+ Hits        28712    28814     +102     
- Misses       5114     5148      +34     
- Partials      428      429       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mstange mstange force-pushed the symbolicate-wasm-script branch 3 times, most recently from 0202adf to be15b28 Compare May 7, 2026 19:49
@mstange mstange requested a review from canova May 7, 2026 19:49
This allows applying wasm symbols to existing profiles that were captured
with a stripped wasm bundle.

The script looks for functions with names of the shape `wasm-function[123]`,
which is what Firefox uses when the wasm file doesn't have a names section.

Usage:

```
yarn build-node-tools && \
node node-tools-dist/profiler-edit.js -i input.json.gz \
--symbolicate-wasm http://host/a.wasm=./a-unstripped.wasm \
--symbolicate-wasm http://host/b.wasm=./b-unstripped.wasm \
-o out.json.gz
```
@mstange mstange force-pushed the symbolicate-wasm-script branch from be15b28 to 2b6584b Compare May 7, 2026 19:50
@mstange mstange force-pushed the symbolicate-wasm-script branch from a70577c to a8608c4 Compare May 7, 2026 21:54
Copy link
Copy Markdown
Member

@canova canova left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for adding some tests! I added a bunch of nits to make things a bit clearer, but otherwise I don't see any issues.

This also made me look into the wasm spec a bit and learn more about it, so I think it was a good use of time 😄

This also made me think more about the source map support that I've been working on. These 2 things seem like solving it for 2 different use cases, so it seems very interesting. I didn't know about the name section in the wasm spec. I wonder how devtools handles it (or if it does). It looks like it's mostly looking at the dwarf info for now, but need to look deeper.

Comment thread .github/workflows/ci.yml
Comment on lines +82 to +88
- name: Upload node-tools-dist artifact
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v7
with:
name: node-tools-dist
path: node-tools-dist/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's a good idea. It makes me think if we should do the same for the profiler-cli.

Comment on lines +48 to +49
path: string;
url?: string;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't immediately understand which one is from the stripped one and which one is from unstripped. How about strippedWasmUrl / unstrippedWasmPath (or symbolsWasmPath) or something?

Comment on lines +229 to +256
const symbolicateWasm: WasmSymbolicationCliSpec[] = [];
const rawWasmArg = argv['symbolicate-wasm'];
let wasmArgs: unknown[];
if (rawWasmArg === undefined) {
wasmArgs = [];
} else if (Array.isArray(rawWasmArg)) {
wasmArgs = rawWasmArg;
} else {
wasmArgs = [rawWasmArg];
}
for (const arg of wasmArgs) {
if (typeof arg !== 'string' || arg === '') {
throw new Error('--symbolicate-wasm requires a value');
}
// Accept "<url>=<path>" if the LHS looks like a URL, otherwise treat the
// whole string as a path and infer the URL from the profile. Split on
// the last `=` so URLs containing `=` (e.g. in query strings) survive
// intact; this assumes file paths don't contain `=`.
const eqIndex = arg.lastIndexOf('=');
if (eqIndex !== -1 && /^[a-z]+:\/\//i.test(arg.slice(0, eqIndex))) {
symbolicateWasm.push({
url: arg.slice(0, eqIndex),
path: arg.slice(eqIndex + 1),
});
} else {
symbolicateWasm.push({ path: arg });
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but now that we have commander in the package.json, we can possibly simplify these things by using it.

Comment on lines +243 to +246
// Accept "<url>=<path>" if the LHS looks like a URL, otherwise treat the
// whole string as a path and infer the URL from the profile. Split on
// the last `=` so URLs containing `=` (e.g. in query strings) survive
// intact; this assumes file paths don't contain `=`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment to the WasmSymbolicationCliSpec type about this wasn't clear to me at first.

profile.meta.symbolicated = true;
}

if (options.symbolicateWasm !== undefined) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We construct symbolicateWasm as an empty array, so it should never be undefined.

// Parses the function-name subsection of a wasm "name" custom section and
// returns a map from function index to name. Returns an empty map if the
// module has no name section. The function index space includes imports
// (imports come first) — same numbering used in `wasm-function[N]` strings.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh can we add a link to the Firefox source code where we generate this N number?

const namesByIndex = parseWasmFunctionNames(spec.bytes);
if (namesByIndex.size === 0) {
throw new Error(
`${tag} has no function names — is this an unstripped wasm file?`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: ${tag}: would be better for consistency. (the others seem to be that way)

continue;
}
const sectionName = readName();
if (sectionName !== 'name') {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, so this looks for the "name" section and the creates a map of the names. But this is not a dwarf info. I found some info here, maybe we can put it a link here for future reference: https://webassembly.github.io/spec/core/appendix/custom.html#name-section

const subId = bytes[pos++];
const subSize = readVarUint();
const subEnd = pos + subSize;
if (subId === 1) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to demystify 1 here, maybe we can create a constant called FUNC_NAMES_SUB_ID.

const sectionId = bytes[pos++];
const sectionSize = readVarUint();
const sectionEnd = pos + sectionSize;
if (sectionId !== 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also demystify this section id by adding a const like CUSTOM_SECTION_ID? And possibly link: https://webassembly.github.io/exception-handling/core/binary/modules.html#sections

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