Repository navigation
[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>
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 |
Add a coreclr_r2r_composite_v8 job (r2rRunType r2r_composite) alongside the per-assembly coreclr_r2r_v8 lane. The mode flows through run_performance_job.py (R2RType=r2r_composite, a separate PerfLab history) to a new --wasm-ready-to-run-composite micro_benchmarks option, which sets PERFLAB_WASM_READY_TO_RUN_COMPOSITE for MSBuild. MicroBenchmarks.Wasm.targets now sets PublishReadyToRunComposite from the selected mode, validates it, and in composite mode verifies that exactly one <entry>.r2r.wasm composite image was produced with component inputs and that it was defined as the readyToRunComposite publish asset that GenerateWasmBootJson routes to coreAssembly. Depends on dotnet/runtime#134618. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ifact (#135204) The new composite R2R microbenchmark lane in dotnet/performance#5324 (`coreclr_r2r_composite_v8`) publishes CoreCLR browser-wasm with `PublishReadyToRunComposite`. #134618's `_WasmCoreClrValidateCompositeTasks` rejects that publish unless the SDK ReadyToRun tasks name the owner `<entry>.r2r.wasm`. The SDK tasks only learn to do that in dotnet/sdk#56395, which hasn't reached runtime's global.json SDK yet (11.0.100-rc.1.26420.103). This PR copies the in-tree wasm-aware task shim, the same one Wasm.Build.Tests use via `Crossgen2SdkOverridePropsPath`/`Crossgen2SdkOverrideTargetsPath`, into the `BrowserWasmCoreCLR` perf artifact. The shim comes from `artifacts/bin/Crossgen2Tasks/<config>/`, which `Build.proj` already produces. Staged layout (flat, no config subfolder; this is the path dotnet/performance consumes): ``` staging/Crossgen2Tasks/Crossgen2Tasks.dll staging/Crossgen2Tasks/Microsoft.NET.CrossGen.props staging/Crossgen2Tasks/Microsoft.NET.CrossGen.targets ``` The step fails if the source directory or any of these three files is missing. Only the CoreCLR leg (`includeCoreClrToolchainPacks: true`) changes; the Mono artifact is untouched. This is temporary: remove it once dotnet/sdk#56395 reaches global.json. Tracked by #135023. Validation: YAML parses (`python3 yaml.safe_load`) and `git diff --check` is clean. The pipeline itself hasn't run yet. > [!NOTE] > This PR was generated with the help of GitHub Copilot. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a287cfc0-a881-4272-9024-bdaf9d9db4ef
* Add CoreCLR WASM composite R2R microbenchmark lane Add a coreclr_r2r_composite_v8 job (r2rRunType r2r_composite) alongside the per-assembly coreclr_r2r_v8 lane. The mode flows through run_performance_job.py (R2RType=r2r_composite, a separate PerfLab history) to a new --wasm-ready-to-run-composite micro_benchmarks option, which sets PERFLAB_WASM_READY_TO_RUN_COMPOSITE for MSBuild. MicroBenchmarks.Wasm.targets now sets PublishReadyToRunComposite from the selected mode, validates it, and in composite mode verifies that exactly one <entry>.r2r.wasm composite image was produced with component inputs and that it was defined as the readyToRunComposite publish asset that GenerateWasmBootJson routes to coreAssembly. Depends on dotnet/runtime#134618. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Use runtime's Crossgen2Tasks shim for CoreCLR WASM composite R2R Composite CoreCLR WASM R2R needs ReadyToRun SDK tasks that name the owner image <entry>.r2r.wasm (dotnet/sdk#56395), which the SDK shipped in the BrowserWasmCoreCLR perf artifact does not have yet. dotnet/runtime#135204 stages runtime's wasm-aware Crossgen2Tasks shim into that artifact as staging/Crossgen2Tasks. Copy the shim into the Helix payload when present, export its location as PERFLAB_WASM_CROSSGEN2_TASKS_DIR for the r2r_composite lane only, and in composite mode pass it to the WebAssembly SDK through Crossgen2SdkOverridePropsPath/Crossgen2SdkOverrideTargetsPath. These must be environment properties because the SDK reads them during props evaluation. Per-assembly R2R keeps the SDK's own ReadyToRun tasks. TODO: remove once the SDK carries dotnet/sdk#56395 (dotnet/runtime#135023). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Clear inherited Crossgen2 SDK override paths for WASM R2R configure_wasm_crossgen2_sdk_override now removes inherited Crossgen2SdkOverridePropsPath/Crossgen2SdkOverrideTargetsPath before deciding whether to apply the shim, so per-assembly R2R always keeps the SDK's ReadyToRun tasks and a composite run without a staged shim cannot pick up a stale path. PERFLAB_WASM_CROSSGEN2_TASKS_DIR remains the only way to select a shim. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fail fast on a present but incomplete Crossgen2Tasks shim A BrowserWasmCoreCLR directory artifact with a staging/Crossgen2Tasks folder that lacks the required files was treated like an artifact without the shim, silently falling back to the SDK ReadyToRun tasks. Only a missing folder (directory artifacts) or no extracted entries (archives) now means "no shim"; anything else must contain Crossgen2Tasks.dll and Microsoft.NET.CrossGen.props/.targets or build_wasm_coreclr_payload raises ValueError. Add archive coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>


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.