Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
471cd1b
[wasm][coreclr] Wire ReadyToRun into the WebAssembly publish and buil…
pavelsavara Sep 7, 2026
0dce04c
[wasm][coreclr] Trigger the native relink on WasmBuildNative as well
pavelsavara Sep 7, 2026
3e5ee64
[wasm][coreclr] Address PR review feedback on the ReadyToRun wiring
pavelsavara Sep 8, 2026
202ccac
[wasm][coreclr] Move test-only asset-copy targets to the test infra
pavelsavara Sep 9, 2026
50047db
Enable trimmed CoreCLR WebAssembly R2R library tests
lewing Sep 11, 2026
94be089
[wasm][coreclr] Raise R2R library-test lane timeout to 480 min
pavelsavara Sep 11, 2026
be7931e
Re-enable WebAssembly caller-identification conformance tests
lewing Sep 12, 2026
6683d3a
Fix WebAssembly R2R async resume target lookup
lewing Sep 17, 2026
c2a07aa
Revert provisional async resume lookup fix
lewing Sep 17, 2026
bff260e
Preserve WebAssembly R2R method table ordering
lewing Sep 17, 2026
954ccfd
Retain the known WebAssembly WhenAny quarantine
lewing Sep 17, 2026
1e724c2
Strengthen Wasm async resume regression
lewing Sep 17, 2026
4f48eca
Unblock trimmed WebAssembly R2R library tests
lewing Sep 18, 2026
87a3e11
Merge remote-tracking branch 'origin/main' into lewing-wasm-r2r-libra…
lewing Sep 18, 2026
5c33b99
Honor WebAssembly R2R control mode
lewing Sep 18, 2026
5d757e1
Restore TestUtilities ReadyToRun coverage
lewing Sep 18, 2026
4f1cb17
Scope Wasm test R2R opt-in per project
lewing Sep 18, 2026
7253c9b
Prevent implicit Wasm R2R propagation
lewing Sep 18, 2026
7d097bb
Handle cross-method Wasm resume fixups
lewing Sep 18, 2026
a875f49
Merge remote-tracking branch 'origin/main' into lewing-wasm-r2r-libra…
lewing Sep 19, 2026
4ff49f7
Address browser Wasm R2R test failures
lewing Sep 19, 2026
c5b807d
Update process elevation check for browser platform
pavelsavara Sep 19, 2026
82eb4ab
Preserve browser process elevation guard
lewing Sep 19, 2026
a63daed
Quarantine Wasm R2R ref-like invoke failure
lewing Sep 19, 2026
660987f
Support browser elevation check on .NET Framework
lewing Sep 19, 2026
2404e91
Use intrinsic platform checks in test utilities
lewing Sep 19, 2026
72920cf
Restore runtime browser platform detection
lewing Sep 19, 2026
f45aa16
Remove unrelated platform detection reorder
lewing Sep 19, 2026
0472a67
Guard browser process elevation for trimming
lewing Sep 19, 2026
e8abe21
Parallelize CoreCLR browser library builds
lewing Sep 19, 2026
27d0469
[wasm] Skip forced MSBuild GC for CoreCLR tests
lewing Sep 20, 2026
943e413
Use platform PInvoke for process elevation test
lewing Sep 20, 2026
73129b0
Avoid privilege PInvoke on WASI
lewing Sep 20, 2026
44cdd8c
Remove obsolete PInvoke issue marker
lewing Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/workflow/testing/libraries/testing-wasm.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,56 @@ At the moment supported values are:

By default, `chrome` browser is used.

## CoreCLR ReadyToRun library tests

Build the browser CoreCLR runtime, libraries, packs, and host crossgen2 first:

```bash
./build.sh clr+libs+host+packs -os browser -c Release /p:AotHostArchitecture=arm64 /p:AotHostOS=osx
```

Use the architecture and OS of the build machine for `AotHostArchitecture` and `AotHostOS`
(for example, `x64` and `linux` on an x64 Linux host).
On macOS, ensure a supported Python installation is ahead of Xcode's Python on `PATH`.

Run a library suite with trimmed ReadyToRun images:

```bash
XHARNESS_COMMAND=test-browser ./dotnet.sh build /t:Test \
src/libraries/System.Runtime/tests/System.IO.UnmanagedMemoryStream.Tests/System.IO.UnmanagedMemoryStream.Tests.csproj \
/p:TargetOS=browser /p:TargetArchitecture=wasm /p:RuntimeFlavor=CoreCLR /p:Configuration=Release \
/p:TestWasmReadyToRun=true /p:EnableAggressiveTrimming=true \
/p:Scenario=WasmTestOnChrome /p:InstallChromeForTests=true
```

`TestWasmReadyToRun` enables `PublishReadyToRun` only in browser CoreCLR library test projects.
Do not pass `PublishReadyToRun=true` globally to `build.sh`: that also attempts to publish
host-side build tools with ReadyToRun.
`EnableAggressiveTrimming` selects the shared mobile test trimming configuration, including
the xUnit and test-utility descriptors and trimming-aware test exclusions.
Library-specific descriptors retain their existing platform conditions; Apple-only roots
are not enabled for browser tests. The property must also be passed when building the test utilities.
R2R browser test apps also set `TEST_READY_TO_RUN_MODE=1`, so existing
`PlatformDetection.IsReadyToRunCompiled` conditions apply. Quarantines for shared WebAssembly
R2R issues use `PlatformDetection.IsWasmReadyToRun`, covering browser and WASI R2R while
leaving interpreter coverage enabled on both hosts.

For the existing CoreCLR library smoke set, use:

```bash
XHARNESS_COMMAND=test-browser ./build.sh libs.tests -test -os browser -c Release \
/p:RuntimeFlavor=CoreCLR /p:TestWasmReadyToRun=true /p:EnableAggressiveTrimming=true \
/p:RunSmokeTestsOnly=true /p:Scenario=WasmTestOnChrome /p:InstallChromeForTests=true
```

Omit `RunSmokeTestsOnly` to build and run all supported library suites. To isolate trimming
from R2R, keep `EnableAggressiveTrimming=true` and pass `PublishReadyToRun=false`.
Use clean project-specific `bin` and `obj` browser-wasm outputs when switching modes;
stale staged assets can otherwise cause assembly-loading failures before tests start.

The `LibraryTestsCoreCLR_R2R` CI jobs use the same configuration and archive the published
tests for execution on Helix. The existing interpreter jobs remain separate.

## AOT library tests

- Building library tests with AOT, and (even) with `EnableAggressiveTrimming` takes 3-9mins on CI, and that adds up for all the assemblies, causing
Expand Down
17 changes: 13 additions & 4 deletions eng/pipelines/common/templates/wasm-coreclr-library-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ parameters:
isWasmOnlyBuild: false
nameSuffix: ''
platforms: []
readyToRun: false
scenarios: ['WasmTestOnChrome']
shouldContinueOnError: false
shouldRunSmokeOnly: false
# Default matches the fast interpreter lane. The R2R lane trims and crossgen2-publishes
# every test app, which is substantially slower, so its callers raise this.
timeoutInMinutes: 240
useHelixMonitor: false

jobs:
Expand Down Expand Up @@ -77,13 +81,18 @@ jobs:
value: /p:InstallV8ForTests=true
${{ else }}:
value: ''
- name: wasmReadyToRunArgs
${{ if eq(parameters.readyToRun, true) }}:
value: /p:TestWasmReadyToRun=true /p:EnableAggressiveTrimming=true
${{ else }}:
value: ''

jobParameters:
isExtraPlatforms: ${{ parameters.isExtraPlatformsBuild }}
testGroup: innerloop
nameSuffix: LibraryTestsCoreCLR${{ parameters.nameSuffix }}
buildArgs: -s clr+libs+host+packs+libs.tests -c $(_BuildConfig) /p:ArchiveTests=true /p:BrowserHost=$(_hostedOs) $(_wasmRunSmokeTestsOnlyArg) $(chromeInstallArg) $(firefoxInstallArg) $(v8InstallArg) /maxcpucount:1 ${{ parameters.extraBuildArgs }}
timeoutInMinutes: 240
buildArgs: -s clr+libs+host+packs+libs.tests -c $(_BuildConfig) /p:ArchiveTests=true /p:BrowserHost=$(_hostedOs) $(_wasmRunSmokeTestsOnlyArg) $(chromeInstallArg) $(firefoxInstallArg) $(v8InstallArg) $(wasmReadyToRunArgs) ${{ parameters.extraBuildArgs }}
timeoutInMinutes: ${{ parameters.timeoutInMinutes }}
# if !alwaysRun, then:
# if this is runtime-wasm (isWasmOnlyBuild):
# - then run only if it would not have run on default pipelines (based
Expand All @@ -98,7 +107,7 @@ jobs:
- template: /eng/pipelines/libraries/helix.yml
parameters:
creator: dotnet-bot
testRunNamePrefixSuffix: CoreCLR_$(_BuildConfig)
extraHelixArguments: /p:BrowserHost=$(_hostedOs) $(_wasmRunSmokeTestsOnlyArg) ${{ parameters.extraHelixArguments }}
testRunNamePrefixSuffix: CoreCLR${{ parameters.nameSuffix }}_$(_BuildConfig)
extraHelixArguments: /p:BrowserHost=$(_hostedOs) $(_wasmRunSmokeTestsOnlyArg) $(wasmReadyToRunArgs) ${{ parameters.extraHelixArguments }}
scenarios: ${{ parameters.scenarios }}
useHelixMonitor: ${{ parameters.useHelixMonitor }}
16 changes: 16 additions & 0 deletions eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ jobs:
scenarios:
- WasmTestOnChrome

# CoreCLR ReadyToRun library tests, complementary to the default pipeline's lane.
- template: /eng/pipelines/common/templates/wasm-coreclr-library-tests.yml
parameters:
platforms:
- browser_wasm
nameSuffix: _R2R
readyToRun: true
timeoutInMinutes: 480
extraBuildArgs: /p:AotHostArchitecture=x64 /p:AotHostOS=$(_hostedOS)
isExtraPlatformsBuild: ${{ parameters.isExtraPlatformsBuild }}
isWasmOnlyBuild: ${{ parameters.isWasmOnlyBuild }}
alwaysRun: ${{ parameters.isWasmOnlyBuild }}
useHelixMonitor: ${{ parameters.useHelixMonitor }}
scenarios:
- WasmTestOnChrome

# Library tests with full threading
- template: /eng/pipelines/common/templates/wasm-library-tests.yml
parameters:
Expand Down
14 changes: 14 additions & 0 deletions eng/pipelines/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,20 @@ extends:
scenarios:
- WasmTestOnChrome

# WebAssembly CoreCLR with trimmed ReadyToRun library tests
- template: /eng/pipelines/common/templates/wasm-coreclr-library-tests.yml
parameters:
platforms:
- browser_wasm
alwaysRun: ${{ variables.isRollingBuild }}
nameSuffix: _R2R
readyToRun: true
timeoutInMinutes: 480
extraBuildArgs: /p:AotHostArchitecture=x64 /p:AotHostOS=$(_hostedOS)
useHelixMonitor: ${{ variables.enableHelixJobMonitor }}
scenarios:
- WasmTestOnChrome

# WebAssembly CoreCLR - smoke tests only on Firefox and V8
- template: /eng/pipelines/common/templates/wasm-coreclr-library-tests.yml
parameters:
Expand Down
3 changes: 3 additions & 0 deletions eng/testing/ILLink.Descriptor.xunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
<namespace fullname="Xunit.Sdk" />
</assembly>
<assembly fullname="xunit.assert">
<type fullname="Xunit.Assert">
<method name="IsType" />
</type>
<type fullname="Xunit.Sdk.CollectionTracker">
<method name="CompareTypedSets" />
</type>
Expand Down
6 changes: 5 additions & 1 deletion eng/testing/tests.browser.targets
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@
<WasmTestLogExitCode>true</WasmTestLogExitCode>
</PropertyGroup>

<ItemGroup Condition="'$(RuntimeFlavor)' == 'CoreCLR' and '$(PublishReadyToRun)' == 'true'">
<WasmEnvironmentVariable Include="TEST_READY_TO_RUN_MODE" Value="1" />
</ItemGroup>

<!-- TODO-WASM https://github.com/dotnet/runtime/issues/120226 (CoreCLR on wasm defaults) -->
<PropertyGroup Condition="'$(RuntimeFlavor)' == 'CoreCLR'">
<InvariantGlobalization Condition="'$(InvariantGlobalization)' == ''">false</InvariantGlobalization>
Expand Down Expand Up @@ -382,7 +386,7 @@
<WasmAssembliesToBundle Condition="'%(WasmAssembliesToBundle.FileName)' == 'WasmSymbolicator'" AOT_InternalForceToInterpret="true" />
</ItemGroup>

<ForceMSBuildGC />
<ForceMSBuildGC Condition="'$(RuntimeFlavor)' != 'CoreCLR'" />
</Target>

<Target Name="_PrepareForAOTOnHelix">
Expand Down
2 changes: 1 addition & 1 deletion eng/testing/tests.wasi.targets
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
Condition="'%(WasmFilesToIncludeFromPublishDir.Identity)' != ''" />
</ItemGroup>

<ForceMSBuildGC />
<ForceMSBuildGC Condition="'$(RuntimeFlavor)' != 'CoreCLR'" />
</Target>

<Target Name="_PrepareForAOTOnHelix">
Expand Down
2 changes: 1 addition & 1 deletion eng/testing/tests.wasm.targets
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
</ItemGroup>

<Target Name="BundleTestWasmApp" DependsOnTargets="$(BundleTestWasmAppDependsOn)" Condition="'$(IsWasmProject)' != 'false'">
<ForceMSBuildGC />
<ForceMSBuildGC Condition="'$(RuntimeFlavor)' != 'CoreCLR'" />
</Target>

<UsingTask Condition="'$(BuildAOTTestsOnHelix)' == 'true'"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,9 @@ static void Validate(ReadyToRunReader reader)
Assert.True(
WasmR2RAssert.HasExpectedAsyncResumeInfoFixups(reader, out string diagnostic),
diagnostic);
Assert.True(
R2RAssert.WasmAsyncResumeTargetsMatchRuntimeFunctionOrder(reader, out diagnostic),
diagnostic);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@

public static class AsyncMultipleSuspensionPoints
{
private class VirtualTarget
{
public virtual int Transform(int value) => value;
}

private sealed class DerivedVirtualTarget : VirtualTarget
{
public override int Transform(int value) => value + 1;
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static int CallVirtual(VirtualTarget target, int value) => target.Transform(value);

[MethodImpl(MethodImplOptions.NoInlining)]
public static async Task<int> MultipleAwaits()
{
int x = 1;
int x = CallVirtual(new DerivedVirtualTarget(), 0);
await Task.Yield();
x++;
await Task.Yield();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,119 @@ public static bool AsyncMethodsWithResumptionStubsAreAdjacent(ReadyToRunReader r
return true;
}

/// <summary>
/// Returns true if each Wasm async resume target uses the RuntimeFunctions index immediately
/// following its parent async method and its funclets.
/// </summary>
public static bool WasmAsyncResumeTargetsMatchRuntimeFunctionOrder(ReadyToRunReader reader, out string diagnostic)
{
var failures = new List<string>();
var resumptionStubTargets = new HashSet<uint>();
var storeMultiTargets = new List<(string Owner, uint Target)>();
int checkedMethodCount = 0;

foreach (ReadyToRunMethod method in GetAllMethods(reader))
{
if (method.Fixups is null)
continue;

bool foundResumptionStub = false;
foreach (FixupCell cell in method.Fixups)
{
ReadyToRunImportSection importSection = reader.ImportSections[(int)cell.TableIndex];
ReadyToRunImportSection.ImportSectionEntry entry = importSection.Entries[(int)cell.CellOffset];
ReadyToRunFixupKind? kind = entry.Signature?.FixupKind;
if (kind is not (ReadyToRunFixupKind.ResumptionStubEntryPoint or ReadyToRunFixupKind.StoreMultiCallableAddrOfCode))
continue;

int offset = reader.GetOffset(checked((int)entry.SignatureRVA)) + sizeof(byte);
uint targetIndex = BinaryPrimitives.ReadUInt32LittleEndian(reader.Image.AsSpan(offset, sizeof(uint)));

if (kind == ReadyToRunFixupKind.StoreMultiCallableAddrOfCode)
{
storeMultiTargets.Add((method.SignatureString, targetIndex));
continue;
}

foundResumptionStub = true;
resumptionStubTargets.Add(targetIndex);
uint expectedIndex = checked((uint)(method.EntryPointRuntimeFunctionId + method.RuntimeFunctionCount - 1));
Comment thread
lewing marked this conversation as resolved.
if (targetIndex != expectedIndex)
{
failures.Add(
$"'{method.SignatureString}' has {kind} target {targetIndex}; " +
$"expected RuntimeFunctions index {expectedIndex}.");
}
}

if (foundResumptionStub)
checkedMethodCount++;
}

foreach ((string owner, uint target) in storeMultiTargets)
{
if (!resumptionStubTargets.Contains(target))
{
failures.Add(
$"'{owner}' has StoreMultiCallableAddrOfCode target {target}, " +
"which is not registered by a ResumptionStubEntryPoint fixup.");
}
}

if (checkedMethodCount == 0)
{
diagnostic = "No methods with ResumptionStubEntryPoint fixups were found.";
return false;
}

if (storeMultiTargets.Count == 0)
{
diagnostic = "No StoreMultiCallableAddrOfCode fixups were found.";
return false;
}

if (!HasWasmVirtualDispatchThunk(reader))
{
diagnostic = "No virtual-dispatch thunk was found.";
return false;
}

diagnostic = failures.Count == 0
? $"Found {checkedMethodCount} async method(s) and {storeMultiTargets.Count} StoreMultiCallableAddrOfCode fixup(s) whose resume targets match RuntimeFunctions ordering in an image containing a virtual-dispatch thunk."
: string.Join(Environment.NewLine, failures);
return failures.Count == 0;
}

private static bool HasWasmVirtualDispatchThunk(ReadyToRunReader reader)
{
foreach (ReadyToRunImportSection section in reader.ImportSections)
{
if (section.Entries is null)
continue;

foreach (ReadyToRunImportSection.ImportSectionEntry entry in section.Entries)
{
if (entry.Signature?.FixupKind != ReadyToRunFixupKind.InjectStringThunks)
continue;

int offset = reader.GetOffset(checked((int)entry.SignatureRVA)) + sizeof(byte);
while (reader.Image[offset] != 0)
{
int terminator = reader.Image.AsSpan(offset).IndexOf((byte)0);
if (terminator < 0)
return false;

if (reader.Image[offset] == (byte)'V')
return true;

offset += terminator + 1 + sizeof(uint);
}
}
}

return false;
}

/// <summary>
/// Returns true if the R2R image contains at least one ContinuationLayout fixup.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ protected override string GetName(NodeFactory factory)
return sb.ToString();
}

public override int ClassCode => 115732791;
// Keep virtual dispatch thunks after compiled methods and the existing Wasm transition thunks.
public override int ClassCode => 948271451;

public override int CompareToImpl(ISortableNode other, CompilerComparer comparer)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,8 @@ public static IEnumerable<object[]> MaxDouble
yield return new object[] { double.NaN, double.PositiveInfinity, double.NaN };
yield return new object[] { double.NaN, double.NegativeInfinity, double.NaN };
// [ActiveIssue("https://github.com/dotnet/runtime/issues/133311")]
if (!(PlatformDetection.IsMonoRuntime && PlatformDetection.IsWasm))
// [ActiveIssue("https://github.com/dotnet/runtime/issues/134268")]
if (!((PlatformDetection.IsMonoRuntime && PlatformDetection.IsWasm) || PlatformDetection.IsWasmReadyToRun))
{
yield return new object[] { PositiveNaNDouble, -0.0, PositiveNaNDouble };
yield return new object[] { -0.0, NegativeNaNDouble, NegativeNaNDouble };
Expand Down Expand Up @@ -1577,7 +1578,8 @@ public static IEnumerable<object[]> MinDouble
yield return new object[] { double.NaN, double.PositiveInfinity, double.NaN };
yield return new object[] { double.NaN, double.NegativeInfinity, double.NaN };
// [ActiveIssue("https://github.com/dotnet/runtime/issues/133311")]
if (!(PlatformDetection.IsMonoRuntime && PlatformDetection.IsWasm))
// [ActiveIssue("https://github.com/dotnet/runtime/issues/134268")]
if (!((PlatformDetection.IsMonoRuntime && PlatformDetection.IsWasm) || PlatformDetection.IsWasmReadyToRun))
{
yield return new object[] { PositiveNaNDouble, -0.0, PositiveNaNDouble };
yield return new object[] { -0.0, NegativeNaNDouble, NegativeNaNDouble };
Expand Down
Loading
Loading