[wasm][coreclr] Enable composite ReadyToRun publishing - #134618
Conversation
Enable composite R2R output for browser CoreCLR, load the owner image before runtime initialization, and preserve component stubs and linked inputs across incremental publishes. Fix WebCIL function relocations and add browser publish coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 4 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Composite mode is silently accepted during ordinary builds, and stale per-assembly R2R outputs may remain after switching to composite publishing.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
This PR enables composite ReadyToRun publishing for CoreCLR browser-WASM with WebCIL, including runtime loading, asset routing, relocation handling, and browser coverage.
Changes:
- Routes composite assemblies through CoreCLR startup while retaining component stubs in the TPA.
- Adds composite R2R staging, fingerprinting, invalidation, and WebCIL relocation support.
- Adds browser tests and documentation.
Two moderate issues remain in CoreCLR target validation and stale R2R output cleanup.
| File | Summary |
|---|---|
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs |
Classifies composite images as core assemblies. |
src/native/libs/Common/JavaScript/loader/run.ts |
Loads composite core assemblies before CoreCLR initialization. |
src/native/libs/Common/JavaScript/loader/assets.ts |
Preserves composite .r2r.wasm paths. |
src/native/libs/Common/JavaScript/host/host.ts |
Excludes composite images from the TPA. |
src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs |
Adds composite publishing and browser coverage. |
src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets |
Publishes composite assets and manages invalidation stamps. |
src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets |
Adds composite routing, validation, and output handling. |
src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs |
Handles WebCIL function-index relocations. |
docs/workflow/testing/libraries/testing-wasm.md |
Documents composite ReadyToRun testing. |
|
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara |
The loader now decides once, while fetching resources.coreAssembly, whether an asset is the composite ReadyToRun owner and records it on the asset; the host builds the TPA from that flag instead of re-deriving it from the .r2r.wasm suffix. The composite test app now calls into the referenced R2rSuffixLibrary.r2r at startup and asserts it loaded, so an ordinary library whose name ends in .r2r is verified to load as a managed component rather than only checked in the boot manifest. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@jkotas @davidwrighton Could you please explain what does "composite" mean technically for WASM ? My mental map:
We can also ship
A) is opt-in until we fix all/most library tests. In my testing of C) so far, loading the second half (of the same assembly) doesn't work for various reasons. I have not tested single-file composite yet. The challenge for E) F) is with I think C) is most likely, but I would not like to give up on possibility of F) just yet. |
Stock SDK ReadyToRun tasks name the browser composite owner '<entry>.r2r.dll' and plan component outputs as '<name>.dll', while crossgen2 emits '<entry>.r2r.wasm' plus '<name>.wasm' stubs. Fail fast with an actionable error instead of a downstream ConvertDllsToWebcil file-not-found. Ship the Crossgen2Tasks shim to BuildWasmApps Helix payloads and use it only for composite Wasm.Build.Tests so per-assembly R2R keeps stock SDK coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Boot config: flag the composite owner with isCompositeImage (new WasmResource/readyToRunComposite trait) instead of the loader inferring it from the .r2r.wasm file name. - Define the composite owner static web asset with DefineStaticWebAssets rather than a hand-built candidate. - Stale-prune keeps what crossgen2 actually emitted (compile outputs plus composite component stubs), not the task's publish plan. - Move R2rSuffixLibrary into testassets; GetBootConfigPath picks the newest fingerprinted dotnet.js so composite assertions use the shared helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
#132721 duplicated the composite and container-format errors into _WasmCoreClrSelectR2RDirectories. That brought back the composite rejection this PR removes and failed PublishRunAllPagesComposite on CI. Keep the checks in _WasmCoreClrValidateReadyToRun, which runs first, and move the new WasmPerformanceInstrumentation check there. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ish (#134689) ## Summary Pass `--strip-debug-info` to crossgen2 for CoreCLR browser-wasm ReadyToRun publish. This drops the R2R `DebugInfo` section (native-to-IL offset maps and variable locations) from shipped images. Opt out with `PublishReadyToRunStripDebugInfo=false` to keep the data for debugging R2R code (e.g. the cDAC work in #133086 / #133890). Also makes crossgen2 argument changes invalidate per-app R2R images. `_CreateR2RImages` only tracks file inputs, so toggling `PublishReadyToRunStripDebugInfo` (or any `PublishReadyToRunCrossgen2ExtraArgs` change) previously left stale images in `obj/R2R`. The arguments are now written to `obj/wasm-r2r-args.stamp` (only when different) and added to `_ReadyToRunCompilerInputs`, mirroring the existing P/Invoke manifest input. > [!IMPORTANT] > Stacked on #134618, which rewrites the same targets file. Retarget to `main` after it merges. ## Size impact Per-assembly R2R images compiled directly with crossgen2 using the SDK's browser-wasm arguments (`--obj-format:wasm --opt-cross-module:* --codegenopt:JitWasm*NyiToR2RUnsupported=1`), with and without `--strip-debug-info`. Total for System.Private.CoreLib, System.Text.Json, System.Linq, and System.Collections: | Compiler | Raw saved | gzip -9 saved | brotli -q 11 saved | | --- | --- | --- | --- | | Current (#134618 base) | 859,712 B (2.4%) | 624,690 B (7.1%) | 565,366 B (9.4%) | | With #133086 variable info | 2,105,520 B (5.6%) | 1,332,733 B (13.9%) | 1,121,574 B (17.0%) | <details> <summary>Per-assembly brotli sizes (bytes, keep → strip)</summary> | Assembly | Current | With #133086 | | --- | --- | --- | | System.Private.CoreLib | 4,822,231 → 4,347,541 | 5,331,799 → 4,403,849 | | System.Text.Json | 823,314 → 761,533 | 887,982 → 754,444 | | System.Linq | 219,177 → 200,471 | 238,853 → 200,441 | | System.Collections | 119,635 → 109,446 | 130,742 → 109,068 | The #133086 compiler is based on an older commit, so compare keep vs. strip within a column rather than across columns. </details> ## Validation - MSBuild evaluation: `--strip-debug-info` is present by default, absent with `PublishReadyToRunStripDebugInfo=false`, and absent when `PublishReadyToRun` is off. - Incremental harness: `_CreateR2RImages` runs on first build, skips when unchanged, reruns on opt-out, skips when repeated, and reruns when switching back. - Not yet run: an end-to-end browser-wasm publish loading stripped images in a browser (Wasm.Build.Tests in CI will cover this). Runtime-pack framework R2R images (used only by the dev-loop build) are unchanged; publish recompiles the whole closure through these targets. `--strip-inlining-info` is intentionally not included: it removes `CrossModuleInlineInfo`, and cross-module inlining is load-bearing on wasm, so it needs separate validation. > [!NOTE] > This pull request was created with assistance from GitHub Copilot. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
#134690 adds --strip-debug-info and --strip-inlining-info for browser and wasi ReadyToRun where the other strip defaults live, so the wasm pack shouldn't add its own. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Composite component stubs are written as <name>.wasm, the same names as per-assembly images. After a composite publish, a per-assembly republish (-p:PublishReadyToRunComposite=false) found them up to date, pruned the composite owner, and shipped stubs that fail startup. Add the mode to the crossgen2 configuration stamp, and switch modes in the trimmed composite test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
## Why CoreCLR WASI composite ReadyToRun currently names the composite owner image `composite-r2r.wasm`. `PrepareForReadyToRunCompilation` in Crossgen2Tasks forces that name whenever `Crossgen2Tool` TargetOS is `wasi`. The browser composite work in #134618 uses the task's default name instead: `<entry>.r2r.wasm`, the main assembly name with `.r2r.wasm`. This PR drops the TargetOS special case so WASI gets the same owner name as browser. It lands ahead of the dotnet/sdk#56395 port, so that PR doesn't carry the WASI rule into the SDK. The two delivery models stay different on purpose. Browser instantiates the composite at runtime through the JS loader. WASI composes it offline into the host with `wasm-merge`/`wasm-opt`. Only the naming contract changes. ## Design The composite's own file name, as written by crossgen2, is the single authority. It is also the owner name every component stub records. - **`PrepareForReadyToRunCompilation`:** the `wasi` branch is removed. A wasm composite owner is now `<entry>.r2r.wasm` on both targets. - **`WasiApp.CoreCLR.targets`:** the composite path now comes from the planned composite compilation, `@(_ReadyToRunCompileList)` with `CreateCompositeImage=true`, via `%(OutputR2RImage)`. The build errors unless there is exactly one composite compilation. Because the path isn't hardcoded, WASI keeps working with either SDK naming. - **`wasi_r2r_probe.hpp`:** the host reserves a 256-byte composite name buffer, zero-initialized, and exports its address and capacity (`wasi_r2r_composite_name_base`, `wasi_r2r_composite_name_cap`). The probe serves the composite only when the requested name matches the recorded name, and only if that name is non-empty. An uncomposed host therefore serves no composite. There is no wildcard matching and no name compiled into C++. - **`ComposeWasiReadyToRun`:** the composer reads the two new exports and checks that the name is non-empty, contains no NUL, and fits the buffer. Its shim now imports `webcil.memory` and includes an active data segment that writes `Path.GetFileName(CompositePath)`, NUL-terminated, into the host buffer. This is the same mechanism that already installs the payload. A prebuilt host, such as the runtime-test corerun, can therefore be composed with a composite of any name. - **Docs:** updated the WASI host composition section of `docs/design/mono/webcil.md` and `docs/workflow/building/coreclr/wasi-r2r.md`. ## Validation - `./build.sh clr+libs+host+packs -os wasi -arch wasm -c Release` finished with 0 warnings and 0 errors. - `./dotnet.sh publish src/mono/sample/wasi/console/Wasi.Console.Sample.csproj -c Release -p:TargetOS=wasi -p:TargetArchitecture=wasm -p:RuntimeFlavor=CoreCLR -p:PublishReadyToRun=true` succeeded. The composer logged `composite='Wasi.Console.Sample.r2r.wasm'`. - The published app ran under wasmtime with `DOTNET_ReadyToRunLogFile` set. It exited 0, and the log shows `Ready to Run initialized successfully` for System.Private.CoreLib, Wasi.Console.Sample, System.Runtime, System.Console, System.Threading and System.Runtime.InteropServices. - **Negative check:** I composed the same host with a copy of the composite renamed to `Wrong.r2r.wasm`. Startup then traps in `EEStartup` because CoreLib's owner composite isn't served. A name mismatch fails loudly rather than silently interpreting. - **Non-R2R publish** of the same sample: the app runs on the interpreter, and the log shows `Ready to Run header not found`. - **Not run:** browser `ReadyToRunTests`. The removed code only executed when TargetOS was `wasi`, and the browser naming path is unchanged. ## Interaction with #134813 This PR merges cleanly with #134813's head (checked with `git merge-tree`); it doesn't depend on #134813. The runtime-test harness in #134813 passes the same `composite-r2r.wasm` path to both crossgen2 and `WasiR2RComposer.proj`. The composer records that name in the shared corerun, so the harness needs no changes. I did not build or run the runtime tests with both PRs merged. ## Follow-up in dotnet/sdk#56395 - Remove the `wasi` TargetOS block from `CreateReadyToRunFileToPublish`. - Change `It_uses_the_fixed_wasm_path_for_a_wasi_composite_owner` to expect `<entry>.r2r.wasm`, the same as browser. > [!NOTE] > This pull request description was generated with assistance from GitHub Copilot. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…unchanged Mark the Crossgen2Tasks shim wiring (Helix payload, composite task validation, test args) with TODO-WASM pointing at #135023. Revert the newest-file selection in ProjectProviderBase.GetBootConfigPath. Only the composite mode-switch republish leaves another mode's fingerprinted dotnet.js behind, so clear the published _framework there instead. The mode switch only needs obj to be incremental. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
/ba-g failures are all filed or known |


Summary
PublishReadyToRunCompositefor CoreCLR browser-wasm when WebCIL is enabled. Composite is opt-in; the default per-assembly R2R mode is unchanged.<entry>.r2r.wasm, the name crossgen2 records in each component stub) as a static web asset with areadyToRunCompositetrait, defined throughDefineStaticWebAssets.GenerateWasmBootJsonroutes the owner toresources.coreAssemblyand flags itisCompositeImage: true. The loader keeps its.wasmvirtual path and leaves it out of the TPA, and component stubs stay in the TPA. Owner identity comes from the flag, not the file name, so an ordinary library named*.r2rstill loads as a managed assembly.ConvertDllsToWebcil, keep the linked R2R closure across repeated trimmed publishes, and prune stale per-app images and stubs.<name>.wasmnames, so without this, switching composite to per-assembly shipped stale stubs that failed at startup.<entry>.r2r.wasm. The base SDK doesn't yet (Port runtime PR dotnet/runtime#133111 R2R changes to SDK Crossgen tasks sdk#56395), so a composite publish with stock SDK tasks fails fast with an actionable error. Wasm.Build.Tests use the in-repo Crossgen2Tasks, which now also ship in theBuildWasmAppsHelix payload.Validation
On
mainas of 2026-09-29, including WebCIL wrapper v2 (#134312), WASI composite in the shared Crossgen2Tasks (#133265), and the browser R2R strip defaults (#134690):clr+libs+host) and the WebAssembly SDK pack.Wasm.Build.Tests.ReadyToRunTests: 10/10 passed. This covers:PublishRunAllPagesCompositepassed on Helix on Linux and Windows. The remaining failures matched known issues (Async2TaskAdapters.TestValueTaskOfTaskValueVersusAsync fails with SanityCheck() in Wasm R2R test #133953, [ci-scan] Test failure: HandleEventShutdownInitiatedByTransport fails with QuicException : The connection timed out from inactivity. #133373).Benchmark (
IndexOfMax<double>(3079), browser, earlier revision of this PR): composite R2R ran the SIMD path with a median of 4,040 ns/op, vs. 1,013,940 ns/op interpreted and 10,652,060 ns/op with per-assembly R2R.Part of testing #134559. That issue's default per-assembly scenario is not changed by this PR.
Note
This pull request description was generated with GitHub Copilot assistance.