Skip to content

[wasm][R2R] Partitioned modules with MIBC-guided hot/cold splitting - #136607

Draft
jtschuster wants to merge 23 commits into
dotnet:mainfrom
jtschuster:wasm-r2r-partitioned-modules
Draft

jtschuster wants to merge 23 commits into
dotnet:mainfrom
jtschuster:wasm-r2r-partitioned-modules

Conversation

@jtschuster

Copy link
Copy Markdown
Member

Warning

This is a totally AI-generated, unreviewed implementation draft.
The implementation and this PR description were generated by GitHub Copilot and have not received human code review. Passing local tests is not a substitute for review or proof of conformance to the specification. This is not ready to merge.

Intent

This should implement the partitioned WebAssembly ReadyToRun (R2R) module specification discussed in #135544, including physical auxiliary modules and MIBC-guided hot/cold placement. It includes the earlier delayed-activation prototype from #136498.

The goal is to enable useful size and performance measurements once we have representative, execution-derived profiles. No real Havit startup profile has been collected yet. The current MIBC measurements use synthetic method lists and do not establish realistic startup size, startup performance, or a production-quality hot set.

The design and implementation notes describe the intended contracts and the limits of the existing evidence.

Implementation

  • Emit a primary module, an auxiliary entrypoint-mapping module, and a cold-body module with its owned fixup, GC/unwind, and exception-handling data.
  • Keep unavailable methods callable through interpreter fallback; activate mapping and body modules independently, in either order, with activation status published last.
  • Integrate loading and activation with the browser host and corerun, and integrate publication, compression, and fingerprinting with the WebAssembly SDK.
  • Use the union of explicitly supplied MIBC method membership for placement, including canonical generic matching. Mandatory-primary methods remain resident. An explicit empty/unmatched profile differs from having no profile; no-profile publishing retains the heuristic.
  • Avoid implicitly enabling partial compilation when an auxiliary publish supplies a profile, so the profile controls placement rather than removing the remaining compiled code. Include the generic entrypoint-offset repair exposed by the added coverage.

The implemented boundary is single-threaded CoreCLR Browser-WASM, composite R2R, with retained IL. This is not non-composite, multithreaded, or stripped-IL support.

Using profiles

With matching updated compiler and SDK build targets, add the existing item to the application:

<ItemGroup>
  <PublishReadyToRunPgoFiles Include="startup.mibc" />
</ItemGroup>
dotnet publish \
  -p:PublishReadyToRun=true \
  -p:PublishReadyToRunComposite=true \
  -p:WasmEnableAuxiliaryR2RModules=true

Crossgen2 also accepts --wasm-auxiliary-modules --mibc startup.mibc directly. This consumes existing MIBC data; it does not implement browser profile collection. Selection is by method membership, not a CPU-weight threshold.

Measurements so far

The committed documentation contains the earlier nine-app, no-profile comparison and a separate matched Havit MIBC size-sensitivity comparison. These measurements predate the merge of current main and were not regenerated after that merge. They must not be interpreted as measured startup PGO results.

For Havit, both sides used the same updated compiler and retained 113,991 managed function/funclet slots. Brotli-compressed R2R module sizes in KiB:

Module No MIBC heuristic Synthetic four-method MIBC
Primary 1111.5 378.3
Entrypoint map 699.5 750.4
Cold body 3286.9 3822.6
Combined 5097.8 4951.4

This is an almost-all-cold configuration: the synthetic profile contains only String.Concat(string, string), String.IsNullOrEmpty(string), Int32.ToString(), and String.StartsWith(string, StringComparison). It demonstrates placement and size sensitivity, not a suitable startup hot set. These are module-file sizes, not whole-app completed network transfers.

The earlier heuristic comparison showed smaller primary modules but increased combined R2R payload and regressions in initial interactions and some steady-state throughput. This PR does not claim an overall performance improvement. Long synchronous managed calls can block JavaScript-side activation; representative profiles and activation-aware measurements are still needed.

Exact raw/gzip/Brotli inventories:

Local validation

The merged feature was validated at e01332100e8, which incorporates upstream main at 82b3c9469e092824425186a37ace7b6ab6bc89b0.

./build.sh clr+libs+host -os browser -arch wasm -c Release /p:RuntimeFlavor=CoreCLR

Passed: Browser-WASM Release runtime, libraries, and host build, zero warnings/errors.

dotnet build \
  src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/ILCompiler.ReadyToRun.Tests.csproj \
  --no-restore -c Release /t:Build,Test \
  /p:TargetOS=browser /p:TargetArchitecture=wasm \
  /p:CoreCLRConfiguration=Release /p:LibrariesConfiguration=Release

Passed: 233 tests, 39 skipped, zero failures, zero build warnings/errors. The six new MIBC selection cases failed before the selector change and passed afterward.

npm --prefix src/native run test:auxiliary-modules

Passed: 17 tests, zero failures.

src/tests/build.sh -browser -release -generatelayoutonly -skiprestorepackages /p:LibrariesConfiguration=Release

Passed: refreshed Browser-WASM Core_Root. The installed Crossgen2 hash matched the newly built compiler, and the runtime/CoreLib timestamps were checked.

The following wrapper was run for each of PartitionedR2RActivation_Eager, PartitionedR2RActivation_EntryPointsFirst, PartitionedR2RActivation_ModuleFirst, PartitionedR2RActivation_AllHot, and PartitionedR2RActivation_AllCold:

bash artifacts/tests/coreclr/browser.wasm.Release/readytorun/wasm/DelayedR2RActivation/<variant>/<variant>.sh \
  -coreroot "$PWD/artifacts/tests/coreclr/browser.wasm.Release/Tests/Core_Root"

Passed: all five physical runtime variants, wrapper exit 0 and actual managed exit 100. Each variant's cached IL-CG2 directory was removed before execution, and a fresh Crossgen2 invocation was verified in every log.

Additional retained evidence from the pre-merge checkout at 53380e2f8bb:

  • Six real Havit publishes and six browser smokes passed, with five rendered UI navigations each, both activation cells active, and a real native mapping-activation notification.
  • Six direct MSBuild profile-flow cases passed after the SDK fix; the auxiliary/profile case failed before it.
  • A CoreLib-inclusive native run with an explicit unmatched MIBC passed activation and 200 exceptions, 400 finally executions, and 400 forced collections with live roots.
  • Earlier V1 validation covered both activation orders, idempotence, nonzero bases, all-hot/all-cold/empty boundaries, and five physical runtime variants.

The extended Wasm.Build.Tests xUnit fixture was not built or run because its assets/test SDK were not prepared. Direct target probes and real publishes were used for the SDK change instead. Publishing used an isolated SDK package copy with the changed source target overlaid; archived runtime/SDK packages and earlier measurement outputs were preserved.

Post-merge build/test logs are under artifacts/aux-pr-merged-* in the feature checkout. Earlier profile/measurement commands and raw results are indexed by artifacts/mibc-selection-validation.json in the feature checkout and artifacts/mibc-hotcold-full-f8b896-20261028/report.json in the macro-benchmarks checkout. These local artifacts are not attached to this PR.

Draft status and remaining work

Human review of specification conformance, ownership/activation safety, code generation, SDK integration, and tests is required. Real startup/interaction profiles and matched size/performance measurements are also required before drawing conclusions about the tradeoff.

This branch includes the phase-1 prototype ancestry. Upstream main was incorporated with explicit merge commit e01332100e8; both ReadyToRun format documentation conflict hunks were resolved in favor of main, and that file now matches main exactly. The merged runtime/compiler and physical activation tests passed as described above.

Note

This implementation and PR description are entirely AI/Copilot-generated and have not been human-reviewed.

jtschuster and others added 19 commits October 26, 2026 22:52
(cherry picked from commit 9fdb27e)
(cherry picked from commit c517c3a)
(cherry picked from commit 2f27390)
…otype

Describe the two-phase flow as implemented: methods that miss wait for an
auxiliary EntryPoints era, methods found in an inactive auxiliary module wait
for its activation flag, and activation happens inline at call boundaries.
Document that EntryPoints and module activation are independent, how failed
activation is handled, and the simulation knobs used by the prototype.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9444d65-1735-42ee-a4a7-119174358f8d
…ulated)

Prototype of the first phase of partitioned R2R modules for browser-wasm: a
composite image's methods stay interpreted until their auxiliary module is
"activated", then switch to their R2R code. The download is simulated by
counting calls, without any format, crossgen2 or host changes.

- Add FEATURE_AUXILIARY_R2R_MODULES (single-threaded browser-wasm with the
  interpreter) and DOTNET_R2RDelayedActivationCallCount. Zero preserves
  eager R2R resolution.
- When lookup finds a method in an image whose auxiliary module is not yet
  activated, record that image's activation flag, keep the method
  interpreted, and set kPendingAuxiliaryModule on its portable entry point.
  Stop searching other images once a dependency is recorded.
- Cache the flag on InterpMethod (AuxiliaryMethodInfo). Interpreter calls,
  the R2R-to-interpreter thunk and CallDescrWorkerInternal check it at call
  boundaries and activate through the prestub. Activation that is rejected
  or throws leaves the method interpreted.
- Do not delay UnmanagedCallersOnly methods, wrapper stubs, stripped-IL
  images, or lookups for direct calls from R2R code.
- Re-enter ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex for
  methods waiting on activation, keeping its MethodImpl handling.
- Add the DelayedR2RActivation tests, including a MethodImpl alias case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9444d65-1735-42ee-a4a7-119174358f8d
(cherry picked from commit d397db8)
Second phase of partitioned R2R modules: a method's R2R lookup can also miss
because the module that maps methods to code (the Auxiliary EntryPoints
Module) is not yet available, or because the image that has the code is
loaded later. Such methods wait for the next auxiliary EntryPoints era
instead of for a specific module, then repeat the lookup.

- Add AuxiliaryEntryPointsEra, an opaque value only ReadyToRunInfo creates
  or compares, and a global era next to the unrelated R2R modules list.
  The era advances when the simulated EntryPoints download completes and
  when an image with code for unrelated generics is registered.
- Record the era read before the lookup when it misses, set
  kAwaitingAuxiliaryEntryPoints, and cache the era in AuxiliaryMethodInfo,
  which becomes a discriminated union of the activation flag and the era.
- The lookup treats methods of an image as not found while its EntryPoints
  module is inactive.
- Add DOTNET_R2RDelayedEntryPointsActivationCallCount, defaulting to
  DOTNET_R2RDelayedActivationCallCount, and the DelayedModuleOnly and
  DelayedEntryPointsOnly test variants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9444d65-1735-42ee-a4a7-119174358f8d
(cherry picked from commit 8806b13)
Same change as dotnet#136497. Revert this commit once that fix is in
the branch's base.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9444d65-1735-42ee-a4a7-119174358f8d
A composite image's components register the composite's own ReadyToRunInfo,
which has no module, on the unrelated R2R modules list. Looking it up decodes
signatures against a NULL module and crashes with "function signature
mismatch" on browser-wasm. Skip it until the registration is fixed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9444d65-1735-42ee-a4a7-119174358f8d
The fix landed in main as dotnet#136497.

This reverts commit 984183f.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9444d65-1735-42ee-a4a7-119174358f8d
Emit physical entrypoint and cold-code modules with deferred metadata ownership, activate them independently through CoreCLR and browser hosting, and integrate opt-in publishing and compression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0797c2c0-9a42-4488-8454-7761c25a6e19
Duplicate eligible managed helper code and funclets without duplicating logical table slots or metadata. Validate cold helper calls, native GC and exception handling, and document activation scheduling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0797c2c0-9a42-4488-8454-7761c25a6e19
Record all nine macro scenarios, full module compression sizes, monitored startup/interaction measurements, and separately activation-gated throughput. Preserve reproducible evidence and limitations without claiming statistical significance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0797c2c0-9a42-4488-8454-7761c25a6e19
Use explicit MIBC method membership with canonical generic matching, preserving mandatory primary methods and the no-profile heuristic. Keep auxiliary PGO publishes fully compiled and fix doubled entrypoint-table offsets exposed by generic coverage. Add compiler/SDK regression cases and matched Havit size evidence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0797c2c0-9a42-4488-8454-7761c25a6e19
Prefer upstream's IMAGE_DATA_DIRECTORY definition and fixup-list wording in the format documentation conflicts while retaining the auxiliary-module additions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0797c2c0-9a42-4488-8454-7761c25a6e19
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/crossgen-contrib
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Publish the auxiliary compiler as a standalone module worker, keep generic downloads Response-only, and preserve shared retry and throttle policy. Cover worker preparation and loader compatibility without diagnostic tracing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0797c2c0-9a42-4488-8454-7761c25a6e19
Group cold-exclusive WebAssembly R2R imports before layout

Before assigning import offsets, the WebCIL writer classifies import usage from method fixups and relocations. Each import table and its parallel signature-RVA array use the same primary-first, cold-exclusive-last partition key, retaining the existing deterministic order within each group. GC reference maps follow physical cell order, including empty records for non-method cells. Table identities and entry sizes do not change; fixup lists and relocations are emitted against the new indices and offsets rather than patched afterward.

Eager, shared, unused, and resident-data-referenced cells stay primary. Signature ownership is computed after symbol deduplication: signatures shared with primary cells or directly referenced by resident data remain primary even when some referencing cells move cold. Cold-exclusive signature blobs are stably grouped at the end of data emission. Alignment gaps are not claimed as cold data, so aligned blobs can still require multiple passive segments. Portable import-thunk records are resident data and pin their referenced cells primary; moving those records would require a separate ownership analysis and is deliberately not inferred from a cold method's use of the cell. GC reference maps remain resident because their stride-indexed encoding covers both groups.

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.

stride-indexed encoding

This will be gone once you resolve conflicts with main

Preserve main's per-signature GC reference maps and remove the obsolete stride-indexed map node and tests. Include signature prefixes in cold-data ownership and update the design documentation for Jan's feedback.

Advance the partitioned format to version 34 after main independently introduced version 33, and adapt auxiliary function names to the updated WasmSymbol contract.

Validated Linux and Browser-WASM Release builds; Browser-WASM ReadyToRun compiler tests: 256 passed, 40 skipped, 0 failed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 776e597c-5437-4660-8e83-6d4a3aa5634e

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

arch-wasm WebAssembly architecture area-ReadyToRun

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants