Skip to content

Fix DWARF line attribution after deleted instructions and Asyncify - #9194

Open
cpunion wants to merge 7 commits into
WebAssembly:mainfrom
cpunion:codex/asyncify-dwarf-generated-code
Open

cpunion wants to merge 7 commits into
WebAssembly:mainfrom
cpunion:codex/asyncify-dwarf-generated-code

Conversation

@cpunion

@cpunion cpunion commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #9196, which depends on #9192. Standalone diff. Regenerated snapshots are in a separate commit; GitHub's main-based diff includes the prerequisites.

Problems

Deleting the first instruction of a line-table row must not discard the following call's location:

nop           ;; input DWARF line 10 starts here
call $g       ;; still line 10 after removing the nop
i32.const 42  ;; line 20 starts here
i32.add

Vacuum or O1 removes this nop; DCE alone does not.

Asyncify's unwind path must not appear to execute the assignment or source return below:

__attribute__((noinline)) int f(void) {
  int value = suspend_once(); // line 28: first call starts unwinding
  observed = value;          // line 29: executed only after resuming
  return value + 42;         // line 30: executed only after resuming
}

Fix and results

Resolve source rows by address interval. Preserve surviving instruction origins and use line 0 for originless code, including Asyncify's new terminal end. Ordinary function ends and ranges remain intact.

Check Baseline Before Current (5f75465)
Call after deleted nop #9196 Loses line 10 Retains line 10
GDB: unwind Inherited-row control 28 → 28 → 30 → caller 28 → caller
LLDB: unwind Previous PR head 28 → 30 → 30 → caller 28 → caller
GDB: resumed execution Inherited-row control 28 → 29 → 30 → caller Unchanged

The control changes only the line table; current results use actual rebuilt output. Generated instructions implementing source operations may legitimately retain their locations.

Cost

Same TinyGo goroutine/JSON input; sizes include DWARF:

Measurement #9196 This PR
Roundtrip module bytes 1,893,879 1,895,081
O1 module bytes 1,872,431 1,937,517
O1 + Asyncify module bytes 2,146,432 2,459,732
Asyncify processing time, ms (earlier measurement) 423 458
Asyncify peak RSS, MiB (earlier measurement) 150 185

Stripped outputs are byte-identical (Asyncify code: 671,942 bytes). The final fix saves 1,125 bytes. Time/RSS are earlier nine-/three-run medians, before that final fix; they measure debug processing, not runtime performance.

Validation and limits

  • 96 Python tests (13 DWARF), the wasm-opt suite, LLVM DWARF verification and TinyGo execution pass. C++: 437 passed, one existing skip. Local V8 execution checks were disabled.
  • All 12 end cases fail before/pass after: void/value/early returns, optimization, Stack IR and roundtripping.
  • Three repetitions: 6 bounded LLDB runs and 21 complete GDB runs pass with Wasmtime 48.0.1's cache disabled. Checks include unwind/resume, breakpoints, caller frames and finish. Debugger details.

Local-variable reindexing, generated helper DIEs, and missing TinyGo metadata are outside this PR. Netbird itself was not tested; this does not fully resolve tinygo-org/tinygo#5736.

Original contribution, developed with AI assistance.

@cpunion
cpunion requested a review from a team as a code owner October 2, 2026 04:20
@cpunion
cpunion requested review from tlively and removed request for a team October 2, 2026 04:20
@tlively

tlively commented Oct 2, 2026

Copy link
Copy Markdown
Member

@kripken, do you have more context for this?

@kripken

kripken commented Oct 2, 2026

Copy link
Copy Markdown
Member

No, I have no more context here.

@cpunion it would be good to clarify what is fixed here with a small example.

Also the overview could be more specific: "Preserve instruction origins in rewritten DWARF line tables" is something we were already doing, or at least trying to.

@tlively

tlively commented Oct 2, 2026

Copy link
Copy Markdown
Member

Asyncify's inserted rewind/unwind code must not inherit a neighboring user's source line.

Why is this?

@cpunion cpunion changed the title Preserve instruction origins in rewritten DWARF line tables Fix DWARF line attribution after deleted instructions and Asyncify Oct 3, 2026
@cpunion

cpunion commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

@kripken @tlively I've clarified the title and narrowed the claim. There are two concrete problems.

1. Deleting a row's first instruction loses a surviving instruction's location. Consider these instructions with the indicated input DWARF rows:

nop           ;; line 10 starts here
call $g       ;; still covered by line 10
i32.const 42  ;; line 20 starts here
i32.add

Removing the nop with either command below must leave the call attributed to line 10. On #9196 it loses that attribution; this PR preserves it by treating each input row as an address interval. DCE alone does not remove this nop.

wasm-opt input.wasm --vacuum -g -o output.wasm
wasm-opt input.wasm -O1 -g -o output.wasm

2. Asyncify's unwind machinery can stop on source statements that were not executed. The debugger fixture contains:

__attribute__((noinline)) int f(void) {
  int value = suspend_once(); // line 28
  observed = value;          // line 29
  return value + 42;         // line 30
}

The first call starts unwinding: neither line 29 nor line 30 executes, and Asyncify returns a placeholder. The second call rewinds/resumes, executes both statements, and returns 49. The driver checks those side effects.

Source-level next Before/control Current output (5f75465)
GDB: unwind 28 → 28 → 30 → caller 28 → caller
LLDB: unwind 28 → 30 → 30 → caller 28 → caller
GDB: resumed execution 28 → 29 → 30 → caller 28 → 29 → 30 → caller

The GDB control inherits neighboring source rows and differs only in the line table; every other section is byte-identical. The LLDB baseline is the previous PR head, which still mapped Asyncify's synthetic terminal end to the original return. The current column uses actual rebuilt output. Ordinary function ends retain their locations.

This demonstrates why line 0 helps for these separate unwind/rewind paths, not a rule that all generated instructions must lack source locations. Generated instructions implementing a source operation can legitimately retain its location.

Each trace was repeated three times. The final checks ran with Wasmtime's compilation cache disabled: 6 bounded LLDB runs (three before/after pairs) and 21 complete GDB runs passed, including breakpoints, caller frames and finish. The regression tests cover instruction origins and void/value/early-return function ends with optimization, Stack IR and roundtripping; all 12 new end cases fail before the fix and pass after it.

For the representative TinyGo module, updated module sizes with DWARF are:

Transformation #9196 bytes Current bytes
Roundtrip 1,893,879 1,895,081
O1 1,872,431 1,937,517
O1 + Asyncify 2,146,432 2,459,732

The terminal-end fix removes 1,125 bytes from the previous Asyncify output. Stripped outputs remain byte-identical; Asyncify executable code remains 671,942 bytes. The extra size is debug information, not executable code. Full timing/cost details and validation limits are in the PR body.

This branch has not been deployed

No deployments
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.

compiler: WASM DWARF misses ~40% of code bytes with opts=1

3 participants