Skip to content

[browser][coreCLR] Multiple flavors of dotnet.native.wasm #132772

Open

Description

@pavelsavara

At the moment we ship only one large dotnet.native.wasm that includes all CoreCLR VM modules, including event-pipe. We instead should ship multiple flavors

  • production version - as small as possible
  • dev loop version - with debugger support and event pipe
  • InvariantTimezone, InvariantGlobalization
  • WasmNativeDebugSymbols, WasmNativeStrip, WasmProfilers,
  • NativeFileReference
  • RelaxedSIMD interp
  • WASI single file - stretch goal

Activity

  1. added this to the 12.0.0 milestone on Aug 26, 2026
  2. self-assigned this
    on Aug 26, 2026
  3. dotnet-policy-service commented on Aug 26, 2026

    @dotnet-policy-service
    Contributor

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

  4. pavelsavara commented on Oct 1, 2026

    @pavelsavara
    MemberAuthor

    Implementation plan

    Decisions confirmed with Pavel

    Question Decision
    First milestone Browser production/development flavors, with debugger and EventPipe component boundaries. Other dimensions follow later.
    Component independence Debugger and EventPipe are independently selectable when relinking.
    Initial prelinked set Two combinations: production = both off; development = both on.
    Default selection Build/run defaults to development; publish defaults to production. Explicit feature properties override these defaults.
    Unshipped combination Exact match required. Relink with wasm-tools, or report an actionable error if unavailable or explicitly disabled. No larger-flavor fallback.
    Installed workload Preserve today's implicit Release-publish relinking when wasm-tools is installed. Do not universally prefer prelinked assets over that policy.
    No workload Prefer an exact prelinked flavor when the app does not otherwise require native linking.
    Execution coverage Interpreter and existing supported ReadyToRun scenarios. App-specific native glue limitations remain explicit.
    Browser debugger bring-up Separate dependency. Define its component boundary now; validate real debugger behavior when bring-up lands. Do not absorb browser debugger implementation into this project.

    Production/development describe native capabilities, not the runtime repository's Debug/Checked/Release build configuration. A development-capable runtime can be optimized. The initial milestone does not make DWARF/unstripped binaries another prelinked dimension.

    Initial research: Mono's existing model

    Component construction and selection

    1. Mono component CMake definitions enumerate debugger, hot_reload, diagnostics_tracing, and marshal-ilgen. Each has real and stub source lists and object targets. Static builds produce pairs named libmono-component-<name>-static.a and libmono-component-<name>-stub-static.a.
    2. The common Mono component interface has an interface version and an availability callback. Component-specific interfaces supply the actual operations. The EventPipe stub is a real implementation of that contract, not an empty archive.
    3. RuntimeComponentManifest.targets reads the runtime-pack manifest, computes component filenames, and selects the real implementation for requested components and the stub for unselected components. It exposes explicit link and exclusion item lists.
    4. WasmApp.Common.targets requests debugger/hot_reload based on the effective Wasm debug level and requests diagnostics_tracing through EnableDiagnostics.
    5. BrowserWasmApp.targets consumes those exclusions while constructing the customer link command. It also selects timezone, exception-handling, and SIMD alternatives.

    Mono also demonstrates a second useful pattern: compiling the same small source set differently, rather than defining a full component interface. Mono mini CMake builds JS-EH/Wasm-EH alternatives from llvm-runtime.cpp and SIMD/non-SIMD interpreter alternatives from separate sources.

    Reuse the build-time selection model, not necessarily Mono's runtime indirection. CoreCLR can retain direct calls and expose the same link-facing symbols from real and stub archives.

    Initial research: CoreCLR today

    Existing relinking is a foundation, not new work

    BrowserWasmApp.CoreCLR.targets already:

    • Infers native linking from native inputs and selected property changes.
    • Sets up Emscripten, generates interop helpers, compiles native inputs, and links the runtime archives.
    • Selects full/invariant timezone archives.
    • Omits globalization/ICU archives for invariant globalization in its link-input list.
    • Publishes relinked Wasm, matching Emscripten JavaScript, and optional symbol-map assets through WasmNativeAsset.
    • Handles build and nested-publish asset ordering.

    These paths should be extended, not replaced with a parallel relinker.

    The browser host CMake link currently produces one prelinked native asset set. It always links the full timezone/globalization inputs in the inspected source. The customer link already has more configurability than this in-tree prelink.

    EventPipe has useful boundaries, but is not yet swappable

    • CoreCLR feature defaults enable eventing/perftracing for the normal browser runtime.
    • CoreCLR definitions couple FEATURE_EVENT_TRACE to the native FEATURE_PERFTRACING definition. Disabling just one CMake variable is not sufficient to establish a consistent feature-off build.
    • EventPipe CMake already separates generated event writers from runtime integration objects. The latter pull in shared EventPipe and diagnostic-server sources; the eventpipe interface target contributes their objects to its consumers.
    • CoreCLR static-library construction includes that EventPipe target, the interpreter, VM, and other dependencies in the static runtime construction.
    • VM integration is wider than the EventPipe implementation: initialization/shutdown, QCall registration, adapters, generated event calls, interpreter sampling, and shared native types all matter.
    • A concrete layout hazard exists in threads.h: FEATURE_PERFTRACING controls the m_activityId member. Building interchangeable archives with inconsistent feature defines can silently disagree about Thread layout.

    Conclusion: introduce implementation-level real/stub guards while preserving a compatible native contract. Do not independently toggle global feature/layout macros on arbitrary object targets and assume symbol-name compatibility proves ABI compatibility.

    Browser debugger is a dependency, not a finished component

    On the inspected commit:

    Therefore, current feature defines do not establish working browser debugging or Hot Reload. The planned debugger-on contract must be coordinated with bring-up; an existing placeholder must never be advertised as the real implementation.

    The broader browser CoreCLR roadmap, dotnet/runtime#134337, tracks debugger and Hot Reload bring-up. dotnet/runtime#124467 is specifically WebCIL/DAC compatibility, not the entire debugger implementation.

    No-workload selection crosses packaging and SDK boundaries

    • native.wasm.targets has a CoreCLR relink-property registry containing single-prebuilt defaults. Its EnableDiagnostics=false default is not evidence that EventPipe implementation code is absent from the current binary.
    • The current workload manifest targets infer workload use from diagnostics, invariant timezone, native symbol options, and other properties before importing native build targets. Simply adding flavor selection inside the workload relinker would be too late and would not solve no-workload builds.
    • WasmFeatures.props currently derives DebuggerSupport=false for non-Debug configurations. The agreed build-versus-publish defaults require a CoreCLR-specific reconciliation without changing Mono defaults.
    • WebAssembly Browser targets select build/publish assets, fingerprints, boot configuration, and the diagnostics JavaScript module.
    • The CoreCLR WebAssembly Browser targets already house non-Mono R2R behavior. This is a candidate home for non-workload native-flavor resolution, subject to verifying import/evaluation timing in packaged consumers.
    • Corehost staging, live-build harvesting, and runtime-pack packaging need consistent treatment of new archives and prelinked subdirectories.

    Important test-harness caveat

    ReadyToRunTests.cs includes a test marked no-workload that deliberately relinks using in-tree targets and EMSDK_PATH. Consequently, passing a no-workload test category alone does not prove that an app can build without a native toolchain. The new acceptance test must remove that escape hatch and inspect executed tasks and selected assets.

    Proposed architecture

    1. Common native base plus independent real/stub archives

    Conceptual link:

    browser host
      + common CoreCLR/VM/interpreter/platform archives
      + exactly one debugger implementation: real OR stub
      + exactly one EventPipe implementation: real OR stub
      + framework-generated or app-generated interop/thunk tables
      -> matching dotnet.native.wasm + dotnet.native.js + symbol assets
    

    Archive names and precise source ownership are provisional. Prefer existing target structure and small changes over a new generic component framework.

    Contract requirements:

    • Real/stub archives provide the same externally required functions, globals, and any vtable/type-related linkage at the component boundary.
    • Keep signatures, calling conventions, native layouts, declarations, and generated metadata compatible with the common base.
    • New #ifdef guards select real bodies versus neutral/unavailable implementations. Do not use them to create conflicting shared type layouts.
    • Real-only internal helpers need not be artificially exported by the stub; the required link-facing symbol set must have parity.
    • Feature-off event emission and lifecycle hooks may be no-ops. Availability/session/debugger operations must accurately report unsupported/unavailable behavior rather than fabricate success.
    • Preserve managed behavior not equivalent to native diagnostics: EventPipe-off must not automatically disable in-process EventListener, all EventSource functionality, metrics, or managed exception stack traces.
    • Do not duplicate selected implementation objects inside the base archive and the component archive.
    • Avoid shipping both real and stub on the same link line and relying on archive order to pick the winner.
    • Scope new componentization to browser CoreCLR initially. Preserve desktop, Mono, and WASI construction paths.

    Start with EventPipe: generated writers, runtime adapter, diagnostic-server integration, lifecycle/QCall boundary, and interpreter sampling form the initial ownership inventory. Keep shared GC, stack walking, threading, and metadata support in the base unless evidence proves that it is exclusively owned by the optional component.

    For the debugger, inventory existing VM-facing declarations and the intended bring-up boundary now. The actual real archive and functional capability tests depend on that separate implementation becoming available.

    2. Small authoritative description of components and prelinked flavors

    Use one build-owned description to drive component selection, prelink construction, runtime-pack metadata, and app-side matching. Reuse current properties/manifests where practical; avoid independent CMake/MSBuild hardcoded flavor matrices.

    The packaged description needs:

    • A schema/version identifier and runtime-pack identity.
    • Each component's real/stub archive paths and dependency constraints.
    • The effective feature values represented by each prelinked flavor.
    • Paths to the complete, matching native asset set.
    • Relevant baked-in native settings needed to reject incompatible reuse.
    • Enough information to explain why a prelinked match was selected or a relink was required.

    Do not introduce a new public flavor switch by default. Resolve from existing feature properties, retaining an internal selected-flavor identity for logging and incremental build inputs. If existing property semantics cannot express the agreed contract cleanly, return to Pavel with that specific issue before adding a new public property.

    Potential pack organization is a flavor subdirectory per native asset set plus shared archives. Final paths must be validated against runtime-pack manifests, NuGet harvesting, SDK conflict resolution, and asset fingerprinting; this is not a committed layout.

    3. Two prelinked combinations, four customer-link combinations

    Debugger EventPipe Initial prelinked asset set Customer relink
    Off Off Production Supported
    On On Development, dependent on real debugger bring-up Supported, same dependency
    Off On None Supported
    On Off None Supported, dependent on real debugger bring-up

    Initial prelinks share baseline globalization, timezone, ISA, and native-symbol settings. Additional variations are not silently folded into these two names.

    Treat the Emscripten-generated JavaScript and Wasm as an atomic pair. Include matching symbol maps/debug companions when present. Share loader/runtime JavaScript only after proving its imports, exports, feature assumptions, and boot configuration are compatible with both native flavors.

    4. App-side resolution algorithm

    1. Determine the effective build operation. A build invoked as part of publish uses publish intent; do not accidentally stage development assets before the nested production publish.
    2. Resolve component settings: build/run defaults both on, publish defaults both off, explicit settings win.
    3. Reconcile DebuggerSupport, WasmDebugLevel, diagnostics settings, and SDK-injected defaults at the correct import/target stages. Detect contradictory explicit requests. Do not mistake a default injected by an earlier props file for an intentional user override.
    4. Identify genuine relink requirements: custom native sources/archives, custom link inputs/options, app-specific generated callbacks/thunks, unsupported prelinked settings, or explicit WasmBuildNative=true.
    5. Preserve the current implicit relink policy for trimmed Release publish when wasm-tools is installed, including existing opt-outs. A matching prelink does not automatically bypass that policy.
    6. Otherwise select an exact prelinked match when available. This is the default route without wasm-tools.
    7. If no exact match exists, relink when available and permitted. If linking is unavailable or WasmBuildNative=false, fail with the requested combination and the precise requirement. Never silently use a larger flavor or discard native references.
    8. Feed one selected native asset set into both build and publish asset pipelines before conflict resolution/fingerprinting. Exclude every unselected flavor from deployed output.
    9. Include effective flavor and link inputs in incremental dependency/stamp handling. Recompute correctly across build -> publish -> build and property-only changes.

    No automatic workload installation is part of this design. Existing R2R crossgen2 requirements remain distinct from Emscripten/native relinking.

    The exact-match rule initially governs the new component capabilities. Deferred axes must retain their existing supported semantics until explicitly extended; do not accidentally turn today's valid no-workload application into a workload-required application just by generalizing the matcher.

    Implementation stages and boundaries

    Stage 0: Establish evidence and final component boundaries

    • Create a convention-named worktree after approval of this plan.
    • Load the build/test guidance and applicable native/CoreCLR conventions before code changes.
    • Build a baseline in that worktree; retain the original checkout and its artifacts unchanged.
    • Inventory actual archive members, defined/undefined symbols, generated writers, imports/exports, and link maps.
    • Identify precisely which object sets are currently incorporated into the static runtime and which remain separate archives.
    • Enumerate layout-affecting macros and direct/inline VM dependencies crossing each proposed component boundary.
    • Establish native raw/Brotli size, full deployed asset size, startup, and representative execution baselines in an optimized configuration.
    • Verify package/SDK import paths using real packaged consumers, not only local imports.
    • Coordinate the debugger contract with its separate bring-up work.

    Exit: a concrete source/symbol ownership table and no unresolved ABI assumption at the EventPipe boundary. Numerical size/performance budgets remain to be agreed after this baseline; no size win has been measured during this research.

    Stage 1: EventPipe real/stub component

    • Extract native object ownership into real/stub alternatives without changing the default existing link.
    • Add implementation guards and compatible disabled behavior at the identified boundaries.
    • Make code generation produce real/stub event entry points from the same authoritative event definitions where needed.
    • Wire both in-tree browserhost and customer relinking to explicit component choices.
    • Exercise EventPipe on/off with interpreter and supported R2R.
    • Keep the debugger contract independent; do not claim its real implementation is complete.

    Exit: both EventPipe variants link and run, feature-on behavior is preserved, and feature-off no longer includes the real EventPipe implementation.

    Stage 2: Debugger boundary and dependency integration

    • Isolate the intended debugger implementation from VM support shared with ordinary execution and diagnostics.
    • Define and test the stub's native contract.
    • Integrate the real implementation once the independent browser-debugger work is available.
    • Test debugger on/off separately from EventPipe; ensure disabling one does not disable the other.
    • Do not expand this phase into Hot Reload bring-up or complete desktop-debugger removal.

    Exit: symbol/ABI contract is established; functional debugger-on acceptance is explicitly blocked until the external bring-up dependency is satisfied. Do not mark this exit complete from link success alone.

    Stage 3: Prelink production/development and package metadata

    • Build selected combinations from the same component choices used by customer relinking.
    • Stage matching native Wasm/JS/symbol assets and the authoritative selection metadata.
    • Update local pack copies, live-build harvesting, shipping packages, and platform-manifest bookkeeping.
    • Keep ordinary pack consumption from copying every flavor into the app.
    • Complete the development capability claim only after Stage 2's real-debugger dependency is satisfied.

    Stage 4: No-workload resolver and defaults

    • Put resolution on a path available to both workload and no-workload consumers.
    • Adjust CoreCLR workload inference so shipped configurations do not require loading the native workload merely to choose an asset.
    • Implement the operation defaults and exact-match/error policy above.
    • Preserve workload-installed Release-publish relinking and explicit WasmBuildNative behavior.
    • Wire diagnostics JavaScript and boot configuration to the effective selection.
    • Validate build, publish, nested publish, incremental switching, and explicit property overrides.

    Stage 5: Acceptance, documentation, and shipping

    • Run the matrix below against freshly generated packages.
    • Record sizes and performance results using identical managed payloads where isolating native differences.
    • Document defaults, prelinked combinations, required workload cases, unsupported combinations, debugger dependency status, and debugging-symbol distinctions.
    • Document the intentional default/error changes as breaking changes before shipping.
    • Keep mechanical extraction, stub logic, and SDK/default policy in separately reviewable concerns where feasible; do not enable a default that depends on an unshipped resolver or missing real component.

    Validation and acceptance

    Use existing Wasm.Build.Tests guidance, native-default tests, workload-required tests, and R2R tests. Prefer extending their data-driven coverage over inventing another harness.

    Native contract and feature behavior

    • Compare real/stub required symbol sets, including data symbols, and check for duplicate ownership.
    • Verify common layouts and relevant generated offsets; matching symbol names alone are insufficient.
    • Link all four component combinations once real debugger support is available.
    • Production link map/archive provenance contains stub implementations, not the real debugger/EventPipe implementation.
    • EventPipe-on produces usable events/traces; off does not initialize a diagnostic server or claim a successful session.
    • Managed EventListener/stack-trace behavior unrelated to native diagnostics remains intact.
    • Debugger-on eventually passes real breakpoint/step/inspection scenarios against the brought-up debugger; off reports unavailability correctly.
    • Exercise GC, exceptions, JS interop, framework P/Invoke, and interpreter/R2R transitions.

    Packaged application matrix

    Scenario Required evidence
    No-workload build/run Development asset set selected; actual browser startup; no native toolchain invocation.
    No-workload publish Production asset set selected in both Debug and Release app configurations; actual browser startup.
    Workload-installed trimmed Release publish Existing implicit native relink preserved; effective production components selected.
    Explicit shipped combination Correct matching prelink when linking is not otherwise required.
    Explicit mixed combination Customer relink with workload; clear error without workload or with linking disabled.
    Explicit WasmBuildNative=true Real relink, not prelink reuse.
    Native references and app-generated glue Native inputs actually present and callable, or a clear unsupported/missing-workload error.
    R2R build Supported prebuilt framework R2R and native thunk tables remain compatible.
    R2R publish Supported trimmed/untrimmed closures run; crossgen2 activity is distinguished from native linking.
    Property-only and operation switches Correct assets after build -> publish -> build and feature toggles, without cleaning.
    Boot/static-web-assets/fingerprints Exactly one coherent native set is deployed; hashes refer to the selected content.

    For the no-workload proof, use the SDK without installed wasm-tools and without EMSDK_PATH or in-tree toolchain fallback. Inspect binlogs/task execution to show no emcc/wasm-ld/wasm-opt work. Verify served asset content against the selected packaged flavor, accounting for fingerprinted names.

    A test category, a generated response file, or a successful runtime-pack build is not sufficient evidence. Run a real browser WebAssembly/Blazor app from the produced output. Include no-relink ordinary apps and negative tests for unsupported app-specific native glue.

    Use Debug for correctness iteration and an optimized native build for size/performance comparisons. Keep component capabilities distinct from compilation configuration. Report raw/compressed native sizes, total deployed asset sizes, and runtime-pack growth separately. Production must be demonstrably smaller than development in the comparison used to justify the change; set numerical budgets from the baseline rather than inventing them now.

    No validation commands were run during planning. At implementation time, obtain current commands from the build-and-test skill, use binary logs, rebuild host/native staging after VM/native changes, regenerate shipping packages before workload tests, and verify test execution counts.

    Deferred dimensions

    • Invariant globalization/timezone: preserve current relink support; add prelinked combinations only after measuring demand and pack-size cost. Timezone already has real/invariant archive prior art.
    • Native symbols and stripping: WasmNativeDebugSymbols, WasmNativeStrip, DWARF, and symbol maps are not equivalent to managed debugger capability. Define their matrix separately.
    • Profilers: do not map Mono profiler names directly to CoreCLR without defining supported equivalents. CoreCLR's normal ICorProfiler feature is disabled on Wasm in the inspected source.
    • Relaxed SIMD interpreter: a future code-generation/ISA dimension, not a debugger/EventPipe component. Investigate separately, including matching feature detection and fallback behavior.
    • Hot Reload: depends on separate bring-up; not implied by debugger-on.
    • WASI single-file: stretch goal with different host/linking constraints; no initial WASI flavor implementation.
    • General SDK cleanup after Mono removal: dotnet/runtime#133411 is adjacent work, not a prerequisite to replace the SDK wholesale here.

    Remaining technical investigations

    These are implementation investigations, not unanswered product-scope decisions:

    1. Exact EventPipe and debugger symbol/source ownership, including generated entry points and shared VM helpers.
    2. The debugger bring-up revision/PR providing the real browser implementation and stable integration boundary.
    3. Final manifest format and pack directory layout that survive both local and shipping package paths.
    4. The import point where CoreCLR operation defaults and user overrides can be distinguished without depending on an installed workload.
    5. Safe stub behavior for every externally callable operation and consistency with managed feature switches.
    6. Which supported R2R/native-callback cases reuse the common prelinked thunk inventory and which require app-specific linking.
    7. Numerical size/startup/throughput budgets after a reproducible optimized baseline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

  • Status
    No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions