| c07c94d2 | 24-Feb-2026 |
Chris Fallin <[email protected]> |
Debugging: preserve original Wasm bytecode inside of compiled ELF artifact. (#12636)
* Debugging: preserve original Wasm bytecode inside of compiled ELF artifact.
This PR adds logic to embed the or
Debugging: preserve original Wasm bytecode inside of compiled ELF artifact. (#12636)
* Debugging: preserve original Wasm bytecode inside of compiled ELF artifact.
This PR adds logic to embed the original core Wasm module(s) from a compilation into a new ELF section, alongside other metadata sections. When a component is compiled, the core Wasms inside are preserved, accessible by their `StaticModuleIndex`es.
The need for this support arises from the guest-debugger ecosystem. Consider either a debug component (bytecodealliance/rfcs#45) or a bespoke debugger in native code using Wasmtime's APIs. In either case, the existing APIs to introspect execution state provide `Module` references for each instance from each stack frame, and PC offsets into these `Module`s are the way in which breakpoints are configured. The debugger will somehow need to associate these `Module`s with the original Wasm bytecode, including e.g. any custom sections containing the producer-specific ways of encoding debug metadata, to do something useful. In particular also note that the GDB-stub protocol as extended for Wasm requires read access directly to the Wasm bytecode (it shows up as part of a "memory map" that is viewed by the standard read-remote-memory command); we can't delegate this requirement to the remote end of the stub connection, but have to handle it in the stub server that runs inside Wasmtime (as a component or bespoke).
We have two main choices: carry the original bytecode all the way through the Wasmtime compilation pipeline and present it via `Module::bytecode()`, ready to use; or say that this task is out-of-scope and that the debugger top-half can find it on disk somehow.
Unfortunately the latter ("out of scope, find the file") is somewhat at odds with the desired developer experience:
- It means that we need some way of mapping a compiled Wasm artifact back to a source Wasm; absent "here's the full bytecode", that means "here's the path to the full bytecode", but that path is an identifier that may not be universally accessible (consider e.g. capabilities/preopens present for a debugger component) or portable (consider e.g. moving the artifact to a different machine).
- Or we don't even provide that metadata, and require the user to explicitly specify the same module filename twice -- once to actually run it, and once as an argument to the debugger.
- It means that we should account for stale artifacts and mark the mismatch somehow; e.g. if the user starts debugging with Wasmtime, either from a `.cwasm` on disk or with one produced in-memory just for this run, and then subsequently rebuilds their source `.wasm`, we no longer have a reference for it. (The same problem exists one level up if source code is edited, but source to a Wasm producer toolchain is definitely out-of-scope for Wasmtime.)
- It means that special logic is required in the case of components to map a module back to a specific component section (we would essentially have to expose the static module IDs, then require the debugger top-half to re-implement our exact flattening algorithm to find that core module).
The permissions issue alone was enough to convince me that we should do something better than providing a filename (why should we have to authorize the adapter to read the user's filesystem?) but all of the other benefits -- ensuring an exact match and ensuring perfect availability -- are a nice bonus. The main downside is making the `.cwasm` larger (possibly substantially so), but this overhead is only present when enabling guest-debugging, the data has to be present anyway, and this is likely not a dealbreaker.
* miri ignore tests with compilation
* Review feedback.
show more ...
|
| 1806c265 | 15-Sep-2025 |
Nick Fitzgerald <[email protected]> |
Lookup functions in the text section by `FuncKey` at runtime (#11630)
This commit refactors our metadata, treating compiled functions homogeneously and removing the need to add new tables to places
Lookup functions in the text section by `FuncKey` at runtime (#11630)
This commit refactors our metadata, treating compiled functions homogeneously and removing the need to add new tables to places like `CompiledModuleInfo` whenever we add a new kind of function. This also simplifies the process of constructing the metadata for a final, linked compilation artifact. Finally, it paves the way to doing gc-sections during our linking process (which would give us smaller code sizes by removing functions that have been inlined into every caller, for example) as we now allow holes in certain types of function index spaces that were previously always densely populated.
We have two kinds of index spaces:
1. Mostly-dense index spaces, which take O(max_index) space and provide O(1) lookups.
2. Sparse index spaces, which take O(num_members) space and provide O(log n) lookups.
Most of our function index spaces are currently dense, but we can tweak that in the future if necessary.
Furthermore, code size of `.cwasm` binaries has shrunk very slightly with this refactoring. Consider `spidermonkey.wasm`'s compiled `.cwasm`:
* Size before: 218756 `.wasmtime.info` section bytes, 20052632 total bytes * Size after: 213761 `.wasmtime.info` section bytes, 20047640 total bytes
That is a 2.28% reduction on the size of the `.wasmtime.info` section, or a 0.025% reduction total.
However, we previously did a single metadata lookup to get the location of both a Wasm function itself and its array-to-Wasm trampoline at the same time, and in the new version of the code two lookups are performed. This is slightly slower, as shown in our call-indirect micro-benchmark that combines lazy table initialization (which delays looking up the function element's location until runtime) with indirect-calling each table element exactly once (which defeats the amortization of that lookup). So this micro-benchmark is both synthetic and the worst-case scenario for this commit's change: we are measuring, as much as we can, *only* the force-initialization-of-a-lazy-funcref-table-slot path.
Ultimately, I believe that the simplification is worth the regression in this micro-benchmark.
<details>
<summary>call-indirect micro-benchmarks results</summary>
``` call-indirect/same-callee/table-init-lazy/65536-calls time: [152.77 µs 154.92 µs 157.39 µs] thrpt: [416.40 Melem/s 423.04 Melem/s 428.99 Melem/s] change: time: [−13.749% −10.205% −6.2864%] (p = 0.00 < 0.05) thrpt: [+6.7081% +11.365% +15.941%] Performance has improved. Found 13 outliers among 100 measurements (13.00%) 8 (8.00%) high mild 5 (5.00%) high severe call-indirect/different-callees/table-init-lazy/65536-calls time: [4.3564 ms 4.4641 ms 4.5843 ms] thrpt: [14.296 Melem/s 14.681 Melem/s 15.044 Melem/s] change: time: [+38.134% +44.404% +50.927%] (p = 0.00 < 0.05) thrpt: [−33.743% −30.750% −27.606%] Performance has regressed. Found 5 outliers among 100 measurements (5.00%) 2 (2.00%) high mild 3 (3.00%) high severe call-indirect/same-callee/table-init-strict/65536-calls time: [144.91 µs 148.41 µs 152.02 µs] thrpt: [431.10 Melem/s 441.58 Melem/s 452.24 Melem/s] change: time: [−13.665% −10.470% −7.2626%] (p = 0.00 < 0.05) thrpt: [+7.8313% +11.694% +15.828%] Performance has improved. Found 4 outliers among 100 measurements (4.00%) 1 (1.00%) high mild 3 (3.00%) high severe call-indirect/different-callees/table-init-strict/65536-calls time: [195.18 µs 200.67 µs 206.49 µs] thrpt: [317.38 Melem/s 326.59 Melem/s 335.77 Melem/s] change: time: [−15.936% −11.568% −7.0835%] (p = 0.00 < 0.05) thrpt: [+7.6235% +13.081% +18.957%] Performance has improved. Found 5 outliers among 100 measurements (5.00%) 5 (5.00%) high mild ```
</details>
show more ...
|