diff --git a/docs/workflow/testing/libraries/testing-wasm.md b/docs/workflow/testing/libraries/testing-wasm.md index bc6965cd5dc4e4..739dc943b5d35a 100644 --- a/docs/workflow/testing/libraries/testing-wasm.md +++ b/docs/workflow/testing/libraries/testing-wasm.md @@ -144,6 +144,59 @@ 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. +`TestUtilities.dll` stays interpreted while its guarded platform probes are affected by +[the R2R platform-probe issue](https://github.com/dotnet/runtime/issues/133614). +The library and test assemblies still use ReadyToRun. + +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 diff --git a/eng/liveILLink.targets b/eng/liveILLink.targets index d51194e4c25619..e31b6df4417f52 100644 --- a/eng/liveILLink.targets +++ b/eng/liveILLink.targets @@ -30,7 +30,11 @@ + false false - true + true <_WasmInTreeDefaults>false false @@ -60,6 +60,39 @@ so copy framework files to the output directory. See https://github.com/dotnet/runtime/issues/127257. --> <_WasmFrameworkCopyToOutputDirectory>PreserveNewest + + + + + <_WasmFrameworkCopyToOutputDirectory>Never + + + + + + + + + + + + + + true true @@ -67,6 +100,12 @@ true + + + + + + false diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index 7e352915ac39e3..47db2845b1777b 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -276,6 +276,7 @@ private static bool GetLinqExpressionsBuiltWithIsInterpretingOnly() public static bool IsSingleFile => !HasAssemblyFiles; public static bool IsReadyToRunCompiled => Environment.GetEnvironmentVariable("TEST_READY_TO_RUN_MODE") == "1"; + public static bool IsWasmReadyToRun => IsWasm && IsReadyToRunCompiled; private static volatile Tuple s_lazyNonZeroLowerBoundArraySupported; public static bool IsNonZeroLowerBoundArraySupported diff --git a/src/libraries/Directory.Build.props b/src/libraries/Directory.Build.props index b85797ae211c21..976eccc487e5b0 100644 --- a/src/libraries/Directory.Build.props +++ b/src/libraries/Directory.Build.props @@ -94,6 +94,11 @@ true + + + true + + --interpreter diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj index d5a40b9c28f982..f8338254bfdda4 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj @@ -20,6 +20,9 @@ Suppress the NU1511 warning in the whole project as putting it on a P2P doesn't work: https://github.com/NuGet/Home/issues/14121 --> $(NoWarn);NU1511 false + + + true diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj index e62c619d9c1bf9..4dcede7eb71324 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj @@ -375,7 +375,7 @@ - + diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ArrayTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ArrayTests.cs index 621185f3a32c6c..ba03fd1e58ac9c 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ArrayTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ArrayTests.cs @@ -479,6 +479,7 @@ public static IEnumerable BinarySearch_TypesNotComparable_TestData() [Theory] [MemberData(nameof(BinarySearch_TypesNotComparable_TestData))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133613", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public static void BinarySearch_TypesNotIComparable_ThrowsInvalidOperationException(T[] array, object value) { Assert.Throws(() => Array.BinarySearch(array, value)); diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs index 6494af3a1341cd..7486fdd6641030 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs @@ -48,6 +48,7 @@ private static void EmptyFunc() { } public delegate TestStruct StructReturningDelegate(); [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133618", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public static void ClosedStaticDelegate() { TestClass foo = new TestClass(); diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs index a64cdfdab07c09..32b1687d9e8f1e 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs @@ -3182,6 +3182,7 @@ private static async Task StateMachineAsync_SingleThread_ChainEventsAndCallstack } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task StateMachineAsync_SingleThread_ChainEventsAndCallstack() { var events = await CollectEventsAsync(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | StateMachineAsyncMethodKeywords, async () => @@ -3255,6 +3256,7 @@ private static async ValueTask StateMachineAsync_ValueTask_SingleThread_ChainEve } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack() { var events = await CollectEventsAsync(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | StateMachineAsyncMethodKeywords, async () => @@ -3331,6 +3333,7 @@ private static async ValueTask StateMachineAsync_PoolingValueTask_SingleThread_C } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack() { var events = await CollectEventsAsync(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | StateMachineAsyncMethodKeywords, async () => diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.cs index 927a1341d05ce9..f9824d2fde045c 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.cs @@ -251,6 +251,7 @@ private static async Task RuntimeAsync_SuspendResumeCompleteEvents_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_SuspendResumeCompleteEvents() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_SuspendResumeCompleteEvents_Marker); @@ -282,6 +283,7 @@ private static async Task RuntimeAsync_ContextEventIdLifecycle_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_ContextEventIdLifecycle() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_ContextEventIdLifecycle_Marker); @@ -328,6 +330,7 @@ private static async Task RuntimeAsync_EventSequenceOrder_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_EventSequenceOrder() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_EventSequenceOrder_Marker); @@ -377,6 +380,7 @@ private static async Task RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait_M } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait_Marker); @@ -404,6 +408,7 @@ private static async Task RuntimeAsync_CreateCallstackDepthMatchesChain_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CreateCallstackDepthMatchesChain() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CreateCallstackDepthMatchesChain_Marker); @@ -432,6 +437,7 @@ private static async Task RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait_Marke } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait_Marker); @@ -459,6 +465,7 @@ private static async Task RuntimeAsync_SuspendCallstackDepthMatchesChain_Marker( } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_SuspendCallstackDepthMatchesChain() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_SuspendCallstackDepthMatchesChain_Marker); @@ -487,6 +494,7 @@ private static async Task RuntimeAsync_SuspendCallstackPrecedesComplete_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_SuspendCallstackPrecedesComplete() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_SuspendCallstackPrecedesComplete_Marker); @@ -522,6 +530,7 @@ private static async Task RuntimeAsync_SuspendCallstackDeeperThanInitialResume_M } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_SuspendCallstackDeeperThanInitialResume() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_SuspendCallstackDeeperThanInitialResume_Marker); @@ -551,6 +560,7 @@ private static async Task RuntimeAsync_CreateCallstackPrecedesResumeCallstack_Ma } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CreateCallstackPrecedesResumeCallstack() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CreateCallstackPrecedesResumeCallstack_Marker); @@ -588,6 +598,7 @@ private static async Task RuntimeAsync_CreateAndFirstResumeCallstacksMatch_Marke } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CreateAndFirstResumeCallstacksMatch() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CreateAndFirstResumeCallstacksMatch_Marker); @@ -629,6 +640,7 @@ private static async Task RuntimeAsync_CallstackEmittedOnResume_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CallstackEmittedOnResume() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CallstackEmittedOnResume_Marker); @@ -656,6 +668,7 @@ private static async Task RuntimeAsync_CallstackDepthMatchesChain_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CallstackDepthMatchesChain() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CallstackDepthMatchesChain_Marker); @@ -683,6 +696,7 @@ private static async Task RuntimeAsync_MethodEventCountMatchesChainDepth_Marker( } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_MethodEventCountMatchesChainDepth() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords | RuntimeAsyncMethodKeywords, RuntimeAsync_MethodEventCountMatchesChainDepth_Marker); @@ -717,6 +731,7 @@ private static async Task RuntimeAsync_CallstackFramesHaveDistinctMethodIds_Mark } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CallstackFramesHaveDistinctMethodIds() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CallstackFramesHaveDistinctMethodIds_Marker); @@ -766,6 +781,7 @@ private static async Task RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Marker( } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_YieldAtEachLevel_CallstackShrinks() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Marker); @@ -793,6 +809,7 @@ private static async Task RuntimeAsync_CallstackSimulation_NormalCompletion_Mark } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CallstackSimulation_NormalCompletion() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CallstackSimulation_NormalCompletion_Marker); @@ -811,6 +828,7 @@ private static async Task RuntimeAsync_CallstackSimulation_HandledException_Mark } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CallstackSimulation_HandledException() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CallstackSimulation_HandledException_Marker); @@ -842,6 +860,7 @@ private static async Task RuntimeAsync_CallstackSimulation_UnhandledException_Ca } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CallstackSimulation_UnhandledException() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_CallstackSimulation_UnhandledException_Catcher_Marker); @@ -873,6 +892,8 @@ private static async Task RuntimeAsync_UnhandledExceptionUnwind_Catcher_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/132311", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_UnhandledExceptionUnwind() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_UnhandledExceptionUnwind_Catcher_Marker); @@ -907,6 +928,7 @@ private static async Task RuntimeAsync_HandledExceptionUnwind_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_HandledExceptionUnwind() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_HandledExceptionUnwind_Marker); @@ -1274,6 +1296,7 @@ private static async Task RuntimeAsync_KeywordGatekeeping_Marker() // Test parallelization is already disabled via XunitAssemblyAttributes.cs. [ConditionalTheory(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] [MemberData(nameof(KeywordGatekeepingData))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/132311", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_KeywordGatekeeping(long keywordValue, AsyncEventID[] allowedEventIds) { EventKeywords kw = (EventKeywords)keywordValue; @@ -1364,6 +1387,8 @@ private static async Task RuntimeAsync_CallstackNativeIPDeltaRoundtrip_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/132311", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CallstackNativeIPDeltaRoundtrip() { // Verify that delta-encoded NativeIPs in callstacks roundtrip correctly, @@ -1733,6 +1758,7 @@ await Task.WhenAll( } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_WhenAll_TracksAllBranches() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_WhenAll_TracksAllBranches_Marker); @@ -1807,6 +1833,7 @@ private static async Task RuntimeAsync_WhenAny_TracksAllBranches_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133627", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_WhenAny_TracksAllBranches() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_WhenAny_TracksAllBranches_Marker); @@ -1913,6 +1940,7 @@ private static async Task RuntimeAsync_TaskCancellation_Marker() } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_TaskCancellation() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_TaskCancellation_Marker); @@ -1963,6 +1991,7 @@ private static async Task RuntimeAsync_CustomSyncContext_EmitsContextEventsAndCa } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CustomSyncContext_EmitsContextEventsAndCallstack() { s_runtimeAsyncSyncContextCtx = new InlinePostSynchronizationContext(); @@ -2004,6 +2033,7 @@ private static async Task RuntimeAsync_CustomTaskScheduler_EmitsContextEventsAnd } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack() { var scheduler = new InlineRunTaskScheduler(); @@ -2052,6 +2082,7 @@ private static async ValueTask RuntimeAsync_ValueTask_EventSequenceOrder_Marker( } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_ValueTask_EventSequenceOrder() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, async () => await RuntimeAsync_ValueTask_EventSequenceOrder_Marker()); @@ -2113,6 +2144,7 @@ private static async ValueTask RuntimeAsync_ValueTask_CallstackDepthMatchesChain } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_ValueTask_CallstackDepthMatchesChainDepth() { var events = await CollectValueTaskEventsAsync(RuntimeAsyncCallstackKeywords, RuntimeAsync_ValueTask_CallstackDepthMatchesChainDepth_Marker); @@ -2136,6 +2168,7 @@ private static async ValueTask RuntimeAsync_ValueTask_CallstackFramesHaveDistinc } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_ValueTask_CallstackFramesHaveDistinctMethodIds() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords, async () => await RuntimeAsync_ValueTask_CallstackFramesHaveDistinctMethodIds_Marker()); @@ -2180,6 +2213,7 @@ private static async ValueTask RuntimeAsync_ValueTask_HandledException_EmitsUnwi } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/132311", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_ValueTask_HandledException_EmitsUnwindAndComplete() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords | UnwindRuntimeAsyncExceptionKeyword, async () => await RuntimeAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Marker()); @@ -2230,6 +2264,7 @@ private static async ValueTask RuntimeAsync_ValueTask_UnhandledException_EmitsUn } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete() { var events = await CollectEventsAsync(RuntimeAsyncCallstackKeywords | UnwindRuntimeAsyncExceptionKeyword, async () => @@ -2290,6 +2325,7 @@ private static async Task RuntimeAsync_ResetContext_ReplaysPendingV2Chain_Outer_ } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_ResetContext_ReplaysPendingV2Chain() { var events = await CollectEventsAsync(AllRuntimeAsyncKeywords, RuntimeAsync_ResetContext_ReplaysPendingV2Chain_Outer_Marker); @@ -2486,6 +2522,7 @@ private static async Task RuntimeAsync_ResetContext_ReplayResumeCompleteBalance_ } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/133626", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmReadyToRun))] public async Task RuntimeAsync_ResetContext_ReplayResumeCompleteBalance() { var events = await CollectEventsAsync(AllRuntimeAsyncKeywords, RuntimeAsync_ResetContext_ReplayResumeCompleteBalance_Outer_Marker); diff --git a/src/libraries/sendtohelix-browser.targets b/src/libraries/sendtohelix-browser.targets index 429078ca20d274..4762716bdfc717 100644 --- a/src/libraries/sendtohelix-browser.targets +++ b/src/libraries/sendtohelix-browser.targets @@ -290,6 +290,20 @@ + + + + + + + + diff --git a/src/mono/browser/build/WasmApp.InTree.props b/src/mono/browser/build/WasmApp.InTree.props index 27bc4d6b0670b7..2eb17ec0c0f94c 100644 --- a/src/mono/browser/build/WasmApp.InTree.props +++ b/src/mono/browser/build/WasmApp.InTree.props @@ -24,15 +24,6 @@ true - - - $(AfterMicrosoftNETSdkTargets);$(Crossgen2SdkOverrideTargetsPath) - $(AfterMicrosoftNETSdkTargets);$(MSBuildThisFileDirectory)WasmApp.ReadyToRun.targets - - - library diff --git a/src/mono/browser/build/WasmApp.ReadyToRun.targets b/src/mono/browser/build/WasmApp.ReadyToRun.targets deleted file mode 100644 index 1194acbb46f69d..00000000000000 --- a/src/mono/browser/build/WasmApp.ReadyToRun.targets +++ /dev/null @@ -1,18 +0,0 @@ - - - - - $([MSBuild]::NormalizePath('$(Crossgen2InBuildDir)', 'crossgen2$(ExeSuffix)')) - - - - - - - diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.ReadyToRun.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.ReadyToRun.targets new file mode 100644 index 00000000000000..6d09e1c20b68f8 --- /dev/null +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.ReadyToRun.targets @@ -0,0 +1,68 @@ + + + + + <_WasmCrossgen2InBuildUsable Condition="'$(Crossgen2InBuildDir)' != '' and Exists('$(Crossgen2InBuildDir)')">true + <_WasmCrossgen2ExeSuffix Condition="'$(ExeSuffix)' != ''">$(ExeSuffix) + <_WasmCrossgen2ExeSuffix Condition="'$(_WasmCrossgen2ExeSuffix)' == '' and $([MSBuild]::IsOSPlatform('windows'))">.exe + + <_WasmCrossgen2RootExe>$([MSBuild]::NormalizePath('$(Crossgen2InBuildDir)', 'crossgen2$(_WasmCrossgen2ExeSuffix)')) + <_WasmCrossgen2ToolsExe>$([MSBuild]::NormalizePath('$([MSBuild]::NormalizeDirectory('$(Crossgen2InBuildDir)', 'tools'))', 'crossgen2$(_WasmCrossgen2ExeSuffix)')) + $(_WasmCrossgen2RootExe) + $(_WasmCrossgen2ToolsExe) + + <_WasmResolvedCrossgen2Dir Condition="'$(Crossgen2Path)' != ''">$([MSBuild]::EnsureTrailingSlash($([System.IO.Path]::GetDirectoryName('$(Crossgen2Path)')))) + + <_WasmR2RTargetOS Condition="'$(TargetOS)' != ''">$(TargetOS) + <_WasmR2RTargetOS Condition="'$(_WasmR2RTargetOS)' == ''">browser + <_WasmR2RTargetArch Condition="'$(TargetArchitecture)' != ''">$(TargetArchitecture) + <_WasmR2RTargetArch Condition="'$(_WasmR2RTargetArch)' == ''">wasm + + + + + + + + + + + + + + diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.props b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.props new file mode 100644 index 00000000000000..0dc363e8833f6f --- /dev/null +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.props @@ -0,0 +1,26 @@ + + + + + + $(AfterMicrosoftNETSdkTargets);$(Crossgen2SdkOverrideTargetsPath) + $(AfterMicrosoftNETSdkTargets);$(MSBuildThisFileDirectory)Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.ReadyToRun.targets + + diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets index 70d3fc52add4e7..cb7de8a0102550 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets @@ -11,12 +11,11 @@ Copyright (c) .NET Foundation. All rights reserved. --> - - true + false - --> + - - <_WasmRuntimePackR2RDir Condition="'$(_WasmRuntimePackR2RDir)' == '' and '$(_RuntimePackNativeDir)' != ''">$([MSBuild]::NormalizeDirectory('$(_RuntimePackNativeDir)', 'r2r')) + + <_WasmCoreClrUnderPublish Condition="'$(_IsPublishing)' == 'true' or '$(WasmBuildingForNestedPublish)' == 'true' or '$(WasmBuildOnlyAfterPublish)' == 'true'">true + <_WasmPublishR2RDir Condition="'$(_WasmPublishR2RDir)' == ''">$([MSBuild]::NormalizeDirectory('$(IntermediateOutputPath)', 'R2R')) - <_WasmBuildPrebuiltR2RDirectory Condition="'$(PublishTrimmed)' == 'true'">$(_WasmPublishR2RDir) - <_WasmBuildPrebuiltR2RDirectory Condition="'$(PublishTrimmed)' != 'true'">$(_WasmRuntimePackR2RDir) - <_WasmPublishPrebuiltR2RDirectory Condition="'$(PublishTrimmed)' == 'true'">$(_WasmPublishR2RDir) - <_WasmPublishPrebuiltR2RDirectory Condition="'$(PublishTrimmed)' != 'true'">$(_WasmRuntimePackR2RDir) + <_WasmPublishPrebuiltR2RDirectory>$(_WasmPublishR2RDir) + <_WasmBuildPrebuiltR2RDirectory Condition="'$(_WasmCoreClrUnderPublish)' == 'true'">$(_WasmPublishR2RDir) + <_WasmBuildPrebuiltR2RDirectory Condition="'$(_WasmCoreClrUnderPublish)' != 'true'">$(_WasmRuntimePackR2RDir) + + + + + + + + + + <_WasmWebcilStampProperty Include="PublishTrimmed" /> + + @@ -69,6 +84,52 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + <_WasmTrimmedClosureDir Condition="'$(_WasmTrimmedClosureDir)' == '' and '$(IntermediateLinkDir)' != ''">$(IntermediateLinkDir) + <_WasmTrimmedClosureDir Condition="'$(_WasmTrimmedClosureDir)' == ''">$([MSBuild]::NormalizeDirectory('$(IntermediateOutputPath)', 'linked')) + + <_WasmCoreClrHasFinalAssemblies Condition="'@(WasmAssembliesFinal)' != ''">true + + + + + <_WasmInTrimmedClosure>false + + + <_WasmInTrimmedClosure>true + + + + + <_WasmTrimmedClosureRedirect Include="@(ReferenceCopyLocalPaths)" Condition="'%(_WasmInTrimmedClosure)' == 'true'" /> + + + + + + + + + + + + + + + @@ -93,11 +176,12 @@ Copyright (c) .NET Foundation. All rights reserved. Condition="'%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' and Exists('$(IntermediateLinkDir)%(ResolvedFileToPublish.FileName).dll')" /> - - <_WasmReadyToRunCompileInput Include="@(ManagedAssemblyToLink->'$(IntermediateLinkDir)%(FileName)%(Extension)')" - Condition="Exists('$(IntermediateLinkDir)%(FileName)%(Extension)')" /> + + <_WasmReadyToRunCompileInput Include="$(IntermediateLinkDir)*.dll" /> + + + + <_ReadyToRunCompilerInputs Include="@(_ReadyToRunCompileList);@(_ReadyToRunAssembliesToReference)" /> + + <_ReadyToRunCompilerInputs Include="$(_WasmResolvedCrossgen2Dir)crossgen2*;$(_WasmResolvedCrossgen2Dir)clrjit_universal_wasm_*" + Condition="'$(_WasmResolvedCrossgen2Dir)' != ''" /> + + + + + + + + <_WasmStalePerAppR2R Include="$(_WasmPublishR2RDir)*.wasm;$(_WasmPublishR2RDir)*.dll" + Exclude="@(_ReadyToRunCompileList->'%(OutputR2RImage)')" /> + + + + diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props index df683130079f51..902c930716bcf8 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props @@ -40,6 +40,14 @@ Copyright (c) .NET Foundation. All rights reserved. true + + + diff --git a/src/mono/sample/wasm/Directory.Build.props b/src/mono/sample/wasm/Directory.Build.props index c2daa07e81b05a..b63362260ba5e2 100644 --- a/src/mono/sample/wasm/Directory.Build.props +++ b/src/mono/sample/wasm/Directory.Build.props @@ -53,6 +53,7 @@ $(Nested_RuntimeFlavor) $(Nested_PublishReadyToRun) + false diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs index fb12b0830c7a0f..2bddb349b84042 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs @@ -28,5 +28,6 @@ internal static class EnvironmentVariables internal static readonly string? WasiSdkPath = Environment.GetEnvironmentVariable("WASI_SDK_PATH"); internal static readonly bool WorkloadsTestPreviousVersions = Environment.GetEnvironmentVariable("WORKLOADS_TEST_PREVIOUS_VERSIONS") is "true"; internal static readonly string? RuntimeFlavor = Environment.GetEnvironmentVariable("RUNTIME_FLAVOR_FOR_TESTS"); + internal static readonly string? BaseDir = Environment.GetEnvironmentVariable("BASE_DIR"); } } diff --git a/src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs b/src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs new file mode 100644 index 00000000000000..4788ef6bfd32e3 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs @@ -0,0 +1,264 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.NET.WebAssembly.Webcil; +using Microsoft.Playwright; +using Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests +{ + // CoreCLR browser-wasm ships ReadyToRun images as webcil-in-wasm; a non-zero R2R table size in the + // System.Private.CoreLib webcil is the marker that R2R was produced and staged. These tests cover R2R in + // both build and publish, with and without IL trimming, driving all pages in a real browser. CoreCLR only. + public class ReadyToRunTests : BlazorWasmTestBase + { + private const int InteractionTimeoutMs = 60_000; + + public ReadyToRunTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + _enablePerTestCleanup = true; + } + + [ConditionalTheory(typeof(BuildTestBase), nameof(IsCoreClrRuntime))] + [InlineData(Configuration.Release)] + [TestCategory("no-workload")] + public async Task BuildRunAllPages(Configuration config) + { + // A build stages the prebuilt framework R2R images from the runtime pack (no per-app crossgen2). + ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.BlazorBasicTestApp, "r2r_build", + extraProperties: "true"); + BlazorBuild(info, config); + + string webcilDir = GetBuildWebcilDir(config); + AssertCoreLibReadyToRun(webcilDir, expectReadyToRun: true); + AssertNoDuplicateAssemblies(webcilDir); + AssertPerAppCrossgenRan(config, expected: false); + + await RunForBuildWithDotnetRun(new BlazorRunOptions(config, + CheckCounter: false, + ExecuteAfterLoaded: (_, page) => InteractAllPagesAsync(page))); + } + + [ConditionalTheory(typeof(BuildTestBase), nameof(IsCoreClrRuntime))] + [InlineData(Configuration.Release, /*trimmed*/ true)] + [InlineData(Configuration.Release, /*trimmed*/ false)] + [TestCategory("no-workload")] + public Task PublishRunAllPages(Configuration config, bool trimmed) + => PublishRunAllPagesCore(config, trimmed, nativeRelink: false); + + // CoreCLR relinks dotnet.native.wasm for Blazor when WasmBuildNative=true; the relink is driven by the + // IsBrowserWasmProject triggers in BrowserWasmApp.CoreCLR.targets. AssertBundle(isNativeBuild: true) + // proves the served dotnet.native.wasm was relinked rather than the runtime-pack prebuilt. + [ConditionalTheory(typeof(BuildTestBase), nameof(IsCoreClrRuntime))] + [InlineData(Configuration.Release, /*trimmed*/ true)] + [InlineData(Configuration.Release, /*trimmed*/ false)] + [TestCategory("no-workload")] + public Task PublishRunAllPagesNativeRelink(Configuration config, bool trimmed) + => PublishRunAllPagesCore(config, trimmed, nativeRelink: true); + + private async Task PublishRunAllPagesCore(Configuration config, bool trimmed, bool nativeRelink) + { + // Publish runs per-app crossgen2 for the whole closure, trimmed or not: even the untrimmed CoreLib + // is a per-app image, not the runtime pack's. nativeRelink also relinks dotnet.native.wasm. + string label = $"r2r_pub_{(trimmed ? "trim" : "notrim")}{(nativeRelink ? "_native" : "")}"; + ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.BlazorBasicTestApp, label, + extraProperties: $"true{(trimmed ? "true" : "false")}"); + string extraArgs = GetR2RBuildArgs(config); + if (nativeRelink) + { + // CoreCLR relinks dotnet.native.wasm via the in-tree targets + EMSDK_PATH, not the browser + // workload; WasmBuildNative=true otherwise forces UsingBrowserRuntimeWorkload=true, which + // demands the (uninstalled) wasm-tools workload and disables the CoreCLR relink targets. + extraArgs += " -p:WasmBuildNative=true -p:UsingBrowserRuntimeWorkload=false"; + } + BlazorPublish(info, config, new PublishOptions(UseCache: false, ExtraMSBuildArgs: extraArgs), + // Assert the native runtime was actually relinked (from obj), not the runtime-pack prebuilt, + // so the relink is proven rather than silently skipped. See dotnet/runtime#133185. + isNativeBuild: nativeRelink ? true : (bool?)null); + + string frameworkDir = GetBlazorBinFrameworkDir(config, forPublish: true); + AssertCoreLibReadyToRun(frameworkDir, expectReadyToRun: true); + AssertNoDuplicateAssemblies(frameworkDir); + AssertNoManagedAssembliesOutsideFramework(frameworkDir); + AssertTrimmedClosureIsFullyStaged(config, frameworkDir); + AssertPerAppCrossgenRan(config, expected: true); + + await RunForPublishWithWebServer(new BlazorRunOptions(config, + CheckCounter: false, + ExecuteAfterLoaded: (_, page) => InteractAllPagesAsync(page))); + } + + [ConditionalTheory(typeof(BuildTestBase), nameof(IsCoreClrRuntime))] + [InlineData(Configuration.Release)] + [TestCategory("no-workload")] + public void FrameworkAssembliesAreNotReadyToRunWhenDisabled(Configuration config) + { + ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.BlazorBasicTestApp, "r2r_off", + extraProperties: "false"); + BlazorBuild(info, config); + + AssertCoreLibReadyToRun(GetBuildWebcilDir(config), expectReadyToRun: false); + AssertPerAppCrossgenRan(config, expected: false); + } + + // Navigate Home -> Counter (increment 0 -> 1) -> Weather (forecast rows) -> Home, asserting content + // at each step. DetectRuntimeFailures (default) fails the run on any unhandled managed/JS exception. + private static async Task InteractAllPagesAsync(IPage page) + { + var counterLink = page.Locator("text=Counter"); + await counterLink.WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = InteractionTimeoutMs }); + await counterLink.ClickAsync(new() { Timeout = InteractionTimeoutMs }); + + var status = page.Locator("p[role='status']"); + await status.WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = InteractionTimeoutMs }); + Assert.Equal("Current count: 0", await status.InnerHTMLAsync()); + + var clickMe = page.Locator("text=\"Click me\""); + await clickMe.ClickAsync(new() { Timeout = InteractionTimeoutMs }); + await page.WaitForFunctionAsync( + """selector => document.querySelector(selector)?.textContent?.trim() === 'Current count: 1'""", + "p[role='status']", + new() { Timeout = InteractionTimeoutMs }); + + var weatherLink = page.Locator("text=Weather"); + await weatherLink.WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = InteractionTimeoutMs }); + await weatherLink.ClickAsync(new() { Timeout = InteractionTimeoutMs }); + await page.WaitForFunctionAsync( + "() => document.querySelectorAll('table tbody tr').length > 0", + null, + new() { Timeout = InteractionTimeoutMs }); + + var homeLink = page.Locator("text=Home"); + await homeLink.WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = InteractionTimeoutMs }); + await homeLink.ClickAsync(new() { Timeout = InteractionTimeoutMs }); + await page.Locator("h1").WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = InteractionTimeoutMs }); + } + + private string GetBuildWebcilDir(Configuration config) => + Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFrameworkForBlazor, "webcil"); + + private string GetObjSubDir(Configuration config, string name) => + Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFrameworkForBlazor, name); + + // Static web assets are fingerprinted as .<10 chars>.wasm. The pattern is deliberately + // case-sensitive: a case-insensitive match also eats real trailing segments like ".Components". + private static string StripFingerprint(string filePath) + => Regex.Replace(Path.GetFileNameWithoutExtension(filePath), @"\.[a-z0-9]{10}$", string.Empty); + + private static string[] GetStagedAssemblyNames(string frameworkDir) + => Directory.EnumerateFiles(frameworkDir, "*.wasm") + .Where(f => !Path.GetFileName(f).StartsWith("dotnet", System.StringComparison.Ordinal)) + .Select(StripFingerprint) + .ToArray(); + + // Fingerprinted assets land beside their predecessors instead of replacing them, so a stale copy of an + // assembly survives as a second file and the runtime can bind the wrong version bubble. + private static void AssertNoDuplicateAssemblies(string frameworkDir) + { + string[] duplicates = GetStagedAssemblyNames(frameworkDir) + .GroupBy(n => n, System.StringComparer.Ordinal) + .Where(g => g.Count() > 1) + .Select(g => $"{g.Key} x{g.Count()}") + .OrderBy(n => n, System.StringComparer.Ordinal) + .ToArray(); + + Assert.True(duplicates.Length == 0, + $"Duplicate assemblies staged in '{frameworkDir}': {string.Join(", ", duplicates)}"); + } + + // A .wasm-named R2R image that is not routed back to a managed asset is treated as native and lands in + // the publish root, leaving the boot config without it. See dotnet/runtime#121257. + private static void AssertNoManagedAssembliesOutsideFramework(string frameworkDir) + { + string? wwwrootDir = Path.GetDirectoryName(frameworkDir); + if (wwwrootDir is null || !Directory.Exists(wwwrootDir)) + return; + + string[] stray = Directory.EnumerateFiles(wwwrootDir, "*.wasm").Select(Path.GetFileName).ToArray()!; + Assert.True(stray.Length == 0, + $"Managed assemblies leaked outside _framework into '{wwwrootDir}': {string.Join(", ", stray)}"); + } + + // Losing every crossgen'd assembly still exits 0 and can still leave a loadable-looking bundle, so + // compare the staged set against the linker's closure rather than trusting the exit code. + private void AssertTrimmedClosureIsFullyStaged(Configuration config, string frameworkDir) + { + string linkedDir = GetObjSubDir(config, "linked"); + if (!Directory.Exists(linkedDir)) + return; + + HashSet staged = new(GetStagedAssemblyNames(frameworkDir), System.StringComparer.Ordinal); + string[] missing = Directory.EnumerateFiles(linkedDir, "*.dll") + .Select(Path.GetFileNameWithoutExtension) + .Where(name => !staged.Contains(name!)) + .OrderBy(name => name, System.StringComparer.Ordinal) + .ToArray()!; + + Assert.True(missing.Length == 0, + $"Assemblies in the trimmed closure but missing from '{frameworkDir}': {string.Join(", ", missing)}"); + } + + // The dev loop serves the runtime pack's prebuilt native/r2r images; only publish crossgens per app. + private void AssertPerAppCrossgenRan(Configuration config, bool expected) + { + string r2rDir = GetObjSubDir(config, "R2R"); + int imageCount = Directory.Exists(r2rDir) ? Directory.EnumerateFiles(r2rDir).Count() : 0; + + if (expected) + Assert.True(imageCount > 0, $"Expected per-app ReadyToRun images under '{r2rDir}'."); + else + Assert.True(imageCount == 0, $"Expected no per-app crossgen2 output, found {imageCount} file(s) under '{r2rDir}'."); + } + + // Wire the wasm-aware Crossgen2Tasks shim (the wasm-container crossgen tasks) so R2R images use the + // right container, and the in-build crossgen2 when this leg shipped it. Each is passed only when present + // under BASE_DIR: the no-workload leg ships the shim but resolves crossgen2 itself from the SDK pack (the + // SDK restores it when PublishReadyToRun is set), so passing a non-existent Crossgen2InBuildDir there + // would break the call-helpers generator. All inert if BASE_DIR is unset. + private static string GetR2RBuildArgs(Configuration config) + { + string? baseDir = EnvironmentVariables.BaseDir; + if (string.IsNullOrEmpty(baseDir)) + return string.Empty; + + string hostArch = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); + string crossgenDir = Path.Combine(baseDir, "coreclr", $"browser.wasm.{config}", hostArch, "crossgen2"); + string shimDir = Path.Combine(baseDir, "Crossgen2Tasks", config.ToString()); + string shimProps = Path.Combine(shimDir, "Microsoft.NET.CrossGen.props"); + string shimTargets = Path.Combine(shimDir, "Microsoft.NET.CrossGen.targets"); + + var args = new List(); + if (Directory.Exists(crossgenDir)) + args.Add($"-p:Crossgen2InBuildDir=\"{crossgenDir}\""); + if (File.Exists(shimProps)) + args.Add($"-p:Crossgen2SdkOverridePropsPath=\"{shimProps}\""); + if (File.Exists(shimTargets)) + args.Add($"-p:Crossgen2SdkOverrideTargetsPath=\"{shimTargets}\""); + return string.Join(" ", args); + } + + private static void AssertCoreLibReadyToRun(string frameworkDir, bool expectReadyToRun) + { + string? coreLib = Directory.EnumerateFiles(frameworkDir, "System.Private.CoreLib*.wasm").FirstOrDefault(); + Assert.True(coreLib is not null, $"Expected a System.Private.CoreLib webcil under '{frameworkDir}'."); + + using FileStream stream = File.OpenRead(coreLib!); + bool ok = WebcilReader.TryReadWebcilInWasmSizes(stream, out _, out int tableSize, out string? failureReason); + Assert.True(ok, failureReason); + + if (expectReadyToRun) + Assert.True(tableSize > 0, $"Expected a ReadyToRun table in '{coreLib}', but the R2R table size was 0."); + else + Assert.Equal(0, tableSize); + } + } +} diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs index f9842bed1047b4..b44664c5b0f532 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs @@ -181,6 +181,9 @@ private static void AddCoreClrProjectProperties(ref string extraProperties, ref {{runtimePackVersion}} + + {{runtimePackVersion}} + """; diff --git a/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs b/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs index b02b1d10bcbf81..fc0719e5f8610f 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs @@ -200,6 +200,43 @@ public void ConvertDllsToWebcil_StagesR2RWebcilWithDllExtension() Assert.True(r2rWebcil.SequenceEqual(File.ReadAllBytes(Path.Combine(outputDirectory, "R2RAssembly.wasm")))); } + [Fact] + public void ConvertDllsToWebcil_FallsBackToIL_WhenPrebuiltMvidMismatches() + { + // A prebuilt R2R image whose MVID differs from the candidate must never be staged: it would fail-fast + // at load against the current version bubble. Use two real assemblies with distinct MVIDs. + string candidatePath = typeof(System.Console).Assembly.Location; + string mismatchedAssembly = typeof(object).Assembly.Location; + Assert.True(File.Exists(candidatePath), $"Candidate assembly not found: '{candidatePath}'."); + Assert.True(File.Exists(mismatchedAssembly), $"Mismatched assembly not found: '{mismatchedAssembly}'."); + + using var directory = new TempDirectory(); + string prebuiltDirectory = Path.Combine(directory.Path, "prebuilt"); + string outputDirectory = Path.Combine(directory.Path, "output"); + Directory.CreateDirectory(prebuiltDirectory); + File.Copy(mismatchedAssembly, Path.Combine(prebuiltDirectory, "System.Console.wasm")); + + var candidate = new TaskItem(candidatePath); + candidate.SetMetadata("RelativePath", "System.Console.dll"); + + var task = new ConvertDllsToWebcil + { + BuildEngine = new TestBuildEngine(), + Candidates = [candidate], + IntermediateOutputPath = Path.Combine(directory.Path, "intermediate"), + IsEnabled = true, + OutputPath = outputDirectory, + PrebuiltR2RDirectory = prebuiltDirectory, + }; + + Assert.True(task.Execute()); + + // The output must be a freshly converted IL webcil (no R2R table), not the mismatched prebuilt. + using FileStream output = File.OpenRead(Path.Combine(outputDirectory, "System.Console.wasm")); + Assert.True(WebcilReader.TryReadWebcilInWasmSizes(output, out _, out int tableSize, out string? failureReason), failureReason); + Assert.Equal(0, tableSize); + } + private const byte SectionCustom = 0x00; private const byte SectionData = 0x0b; diff --git a/src/mono/wasm/testassets/BlazorBasicTestApp/App/Pages/Weather.razor b/src/mono/wasm/testassets/BlazorBasicTestApp/App/Pages/Weather.razor new file mode 100644 index 00000000000000..1664dd16d557de --- /dev/null +++ b/src/mono/wasm/testassets/BlazorBasicTestApp/App/Pages/Weather.razor @@ -0,0 +1,60 @@ +@page "/weather" +@using System.Linq + +Weather + +

Weather

+ +

This component demonstrates showing data.

+ +@if (forecasts == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + } + +
DateTemp. (C)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.Summary
+} + +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() + { + // Async load with local data (no HttpClient) so the page is self-contained on wasm. + await Task.Yield(); + var startDate = DateOnly.FromDateTime(DateTime.Now); + var summaries = new[] { "Freezing", "Cool", "Mild", "Warm", "Hot" }; + forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = startDate.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }).ToArray(); + } + + private sealed class WeatherForecast + { + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + } +} diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs index ffb61356b95730..d348d2bc79f0e0 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs @@ -260,7 +260,7 @@ private bool PrebuiltR2RMatchesCandidate(string candidateDllPath, string prebuil return true; } - candidateHasILCode = AssemblyHasILCode(candidateDllPath, out Version candidateVersion); + candidateHasILCode = AssemblyHasILCode(candidateDllPath, out Guid candidateMvid); if (!candidateHasILCode) { Log.LogMessage(MessageImportance.Low, @@ -268,15 +268,17 @@ private bool PrebuiltR2RMatchesCandidate(string candidateDllPath, string prebuil return false; } - Version prebuiltVersion = TryReadAssemblyVersion(prebuiltImagePath); + Guid? prebuiltMvid = TryReadMvid(prebuiltImagePath); - // If the prebuilt identity is unreadable, keep it (prior behavior) so the common case where - // every candidate is the pack's own assembly is never regressed. - if (prebuiltVersion is null || candidateVersion.Equals(prebuiltVersion)) + // Compare MVIDs, not assembly versions: with cross-module inlining every image in the bundle shares one + // version bubble the runtime checks by MVID at load, and the assembly version rarely changes between + // incremental builds, so a stale prebuilt R2R would pass a version check yet fail-fast at startup. + // If the prebuilt identity is unreadable, keep it (prior behavior). + if (prebuiltMvid is null || candidateMvid.Equals(prebuiltMvid.Value)) return true; Log.LogMessage(MessageImportance.Normal, - $"Not staging prebuilt R2R image '{prebuiltImagePath}' (v{prebuiltVersion}) for '{candidateDllPath}' (v{candidateVersion}): assembly version mismatch; converting IL instead."); + $"Not staging prebuilt R2R image '{prebuiltImagePath}' (MVID {prebuiltMvid}) for '{candidateDllPath}' (MVID {candidateMvid}): module version mismatch; converting IL instead."); return false; } @@ -287,12 +289,12 @@ private static bool IsR2RWebcil(string path) && tableSize > 0; } - private static bool AssemblyHasILCode(string path, out Version version) + private static bool AssemblyHasILCode(string path, out Guid mvid) { using FileStream stream = File.OpenRead(path); using var peReader = new PEReader(stream); MetadataReader metadataReader = peReader.GetMetadataReader(); - version = metadataReader.GetAssemblyDefinition().Version; + mvid = metadataReader.GetGuid(metadataReader.GetModuleDefinition().Mvid); foreach (MethodDefinitionHandle methodDefinitionHandle in metadataReader.MethodDefinitions) { @@ -305,23 +307,52 @@ private static bool AssemblyHasILCode(string path, out Version version) return false; } - private static Version TryReadAssemblyVersion(string path) + private static Guid? TryReadMvid(string path) { try { using FileStream stream = File.OpenRead(path); - if (path.EndsWith(Utils.WebcilInWasmExtension, StringComparison.OrdinalIgnoreCase)) + // Detect webcil-in-wasm by content (the '\0asm' magic), not by extension: a prebuilt R2R image + // may still be named *.dll, and a PEReader would throw on it, returning null and silently + // bypassing the MVID guard. + if (IsWebcilInWasm(stream)) { using var webcilReader = new WebcilReader(stream, path); - return webcilReader.GetMetadataReader().GetAssemblyDefinition().Version; + MetadataReader webcilMetadata = webcilReader.GetMetadataReader(); + return webcilMetadata.GetGuid(webcilMetadata.GetModuleDefinition().Mvid); } using var peReader = new PEReader(stream); - return peReader.GetMetadataReader().GetAssemblyDefinition().Version; + MetadataReader peMetadata = peReader.GetMetadataReader(); + return peMetadata.GetGuid(peMetadata.GetModuleDefinition().Mvid); } catch { return null; } } + + // The WebAssembly module magic "\0asm" (0x00 0x61 0x73 0x6D). Leaves the stream position unchanged. + private static bool IsWebcilInWasm(Stream stream) + { + long position = stream.Position; + try + { + byte[] magic = new byte[4]; + int read = 0; + while (read < magic.Length) + { + int n = stream.Read(magic, read, magic.Length - read); + if (n == 0) + return false; + read += n; + } + + return magic[0] == 0x00 && magic[1] == 0x61 && magic[2] == 0x73 && magic[3] == 0x6D; + } + finally + { + stream.Position = position; + } + } } diff --git a/src/tasks/Microsoft.NET.WebAssembly.Webcil/WebcilReader.cs b/src/tasks/Microsoft.NET.WebAssembly.Webcil/WebcilReader.cs index 1975b55ecd666e..6c56803b1e15af 100644 --- a/src/tasks/Microsoft.NET.WebAssembly.Webcil/WebcilReader.cs +++ b/src/tasks/Microsoft.NET.WebAssembly.Webcil/WebcilReader.cs @@ -421,6 +421,10 @@ private unsafe ImmutableArray ReadSections() public void Dispose() { + // The provider owns a memory-mapped section over _stream; leaving it to the finalizer keeps the file + // mapped inside long-lived MSBuild task hosts and later writers fail with "user-mapped section open". + _metadataReaderProvider?.Dispose(); + _metadataReaderProvider = null; _stream.Dispose(); }