From ee7289807236ff4093f341179838b3313a9af0ad Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 28 Sep 2026 14:54:52 -0500 Subject: [PATCH] Enable validated async microbenchmarks on WASM Remove NoWasmValidator, which required every Task/ValueTask benchmark to be tagged NoWASM. BenchmarkDotNet now awaits async workloads, so returning an awaitable is no longer a reason to exclude a benchmark from WASM. Enable Perf_AsyncMethods, ValueTaskPerfTest, MemoryStreamTests.CopyToAsync and the MemoryStreamChunkedTests async benchmarks. Validated on Mono WASM (Node) and CoreCLR WASM (V8). Other async benchmarks keep their NoWASM tags. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/benchmarking-workflow-dotnet-runtime.md | 8 ++++ src/benchmarks/micro/Program.cs | 3 +- .../System.IO/MemoryStreamChunkedTests.cs | 3 -- .../libraries/System.IO/MemoryStreamTests.cs | 1 - .../Perf.ValueTask.cs | 2 +- .../Perf.AsyncMethods.cs | 2 +- .../NoWasmValidator.cs | 47 ------------------- .../WasmAsyncBenchmarkSelectionTests.cs | 45 ++++++++++++++++++ 8 files changed, 56 insertions(+), 55 deletions(-) delete mode 100644 src/harness/BenchmarkDotNet.Extensions/NoWasmValidator.cs create mode 100644 src/tests/harness/BenchmarkDotNet.Extensions.Tests/WasmAsyncBenchmarkSelectionTests.cs diff --git a/docs/benchmarking-workflow-dotnet-runtime.md b/docs/benchmarking-workflow-dotnet-runtime.md index 53ac9b1ceb1..8980660135b 100644 --- a/docs/benchmarking-workflow-dotnet-runtime.md +++ b/docs/benchmarking-workflow-dotnet-runtime.md @@ -183,6 +183,14 @@ Make sure you have the v8 engine installed and in the PATH. Follow the installat #### Run the benchmarks with the interpreter +`NoWASM` excludes benchmarks that still require WASM-specific support. Async +benchmarks are not excluded solely because they return `Task` or `ValueTask`: +BenchmarkDotNet awaits them by default. The self-contained async benchmarks +in `Perf_AsyncMethods`, `ValueTaskPerfTest`, `MemoryStreamTests`, and +`MemoryStreamChunkedTests` are eligible for WASM runs. Other benchmarks retain +their exclusions until their platform dependencies and runtime behavior are +validated individually. + 1. Configure NuGet Feeds to include local packages. Open the `NuGet.config` and modify `packageSources` section ```xml diff --git a/src/benchmarks/micro/Program.cs b/src/benchmarks/micro/Program.cs index 28e9eca5312..bca89678baa 100644 --- a/src/benchmarks/micro/Program.cs +++ b/src/benchmarks/micro/Program.cs @@ -59,8 +59,7 @@ static async Task Main(string[] args) exclusionFilterValue: exclusionFilterValue, categoryExclusionFilterValue: categoryExclusionFilterValue, getDiffableDisasm: getDiffableDisasm, - msBuildArguments: msBuildArguments) - .AddValidator(new NoWasmValidator(Categories.NoWASM))) + msBuildArguments: msBuildArguments)) .ConfigureAwait(false); return summaries.ToExitCode(); diff --git a/src/benchmarks/micro/libraries/System.IO/MemoryStreamChunkedTests.cs b/src/benchmarks/micro/libraries/System.IO/MemoryStreamChunkedTests.cs index 46ec5b66949..7999fa042b9 100644 --- a/src/benchmarks/micro/libraries/System.IO/MemoryStreamChunkedTests.cs +++ b/src/benchmarks/micro/libraries/System.IO/MemoryStreamChunkedTests.cs @@ -69,7 +69,6 @@ public int ReadSpan() } [Benchmark] - [BenchmarkCategory(Categories.NoWASM)] [MemoryRandomization] public async Task ReadAsyncMemory() { @@ -113,7 +112,6 @@ public void WriteSpan() } [Benchmark] - [BenchmarkCategory(Categories.NoWASM)] [MemoryRandomization] public async Task WriteAsyncMemory() { @@ -138,7 +136,6 @@ public void CopyToWithBufferSize() } [Benchmark] - [BenchmarkCategory(Categories.NoWASM)] [MemoryRandomization] public async Task CopyToAsyncWithBufferSize() { diff --git a/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs b/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs index 24db4f653ee..a1999513dd5 100644 --- a/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs +++ b/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs @@ -69,7 +69,6 @@ public void CopyTo() } [Benchmark] - [BenchmarkCategory(Categories.NoWASM)] [MemoryRandomization] public async Task CopyToAsync() { diff --git a/src/benchmarks/micro/libraries/System.Threading.Tasks.Extensions/Perf.ValueTask.cs b/src/benchmarks/micro/libraries/System.Threading.Tasks.Extensions/Perf.ValueTask.cs index c02f2653bd7..b4b04834303 100644 --- a/src/benchmarks/micro/libraries/System.Threading.Tasks.Extensions/Perf.ValueTask.cs +++ b/src/benchmarks/micro/libraries/System.Threading.Tasks.Extensions/Perf.ValueTask.cs @@ -10,7 +10,7 @@ namespace System.Threading.Tasks { - [BenchmarkCategory(Categories.Libraries, Categories.NoWASM)] + [BenchmarkCategory(Categories.Libraries)] [MinWarmupCount(2, forceAutoWarmup: true)] // these benchmarks require more warmups than in our default config [MaxWarmupCount(10, forceAutoWarmup: true)] public class ValueTaskPerfTest diff --git a/src/benchmarks/micro/libraries/System.Threading.Tasks/Perf.AsyncMethods.cs b/src/benchmarks/micro/libraries/System.Threading.Tasks/Perf.AsyncMethods.cs index 38dbaaccf30..efc6fac81ea 100644 --- a/src/benchmarks/micro/libraries/System.Threading.Tasks/Perf.AsyncMethods.cs +++ b/src/benchmarks/micro/libraries/System.Threading.Tasks/Perf.AsyncMethods.cs @@ -9,7 +9,7 @@ namespace System.Threading.Tasks.Tests { - [BenchmarkCategory(Categories.Libraries, Categories.NoWASM)] + [BenchmarkCategory(Categories.Libraries)] public class Perf_AsyncMethods { [Benchmark(OperationsPerInvoke = 100_000)] diff --git a/src/harness/BenchmarkDotNet.Extensions/NoWasmValidator.cs b/src/harness/BenchmarkDotNet.Extensions/NoWasmValidator.cs deleted file mode 100644 index 68fe73a4645..00000000000 --- a/src/harness/BenchmarkDotNet.Extensions/NoWasmValidator.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using BenchmarkDotNet.Validators; - -namespace BenchmarkDotNet.Extensions -{ - /// - /// This class makes sure that every benchmark that returns an awaitable object belongs to the "NoWASM" category. - /// CI uses it to filter out multithreaded benchmarks, which are not supported by WASM. - /// - public class NoWasmValidator : IValidator - { - private readonly string _noWasmCategory; - - public bool TreatsWarningsAsErrors => true; - - public NoWasmValidator(string noWasmCategory) => _noWasmCategory = noWasmCategory; - - public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) - => validationParameters.Benchmarks - .Where(benchmark => IsAsyncMethod(benchmark.Descriptor.WorkloadMethod) && !benchmark.Descriptor.Categories.Any(category => category.Equals(_noWasmCategory, StringComparison.Ordinal))) - .Select(benchmark => benchmark.Descriptor.GetFilterName()) - .Distinct() - .Select(benchmarkId => - new ValidationError( - isCritical: TreatsWarningsAsErrors, - $"{benchmarkId} returns an awaitable object and has no: {_noWasmCategory} category applied. Use [BenchmarkCategory(Categories.NoWASM)]") - ) - .ToAsyncEnumerable(); - - private bool IsAsyncMethod(MethodInfo workloadMethod) - { - Type returnType = workloadMethod.ReturnType; - - return returnType == typeof(Task) - || returnType == typeof(ValueTask) - || (returnType.IsGenericType && (returnType.GetGenericTypeDefinition() == typeof(Task<>) || returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))); - } - } -} \ No newline at end of file diff --git a/src/tests/harness/BenchmarkDotNet.Extensions.Tests/WasmAsyncBenchmarkSelectionTests.cs b/src/tests/harness/BenchmarkDotNet.Extensions.Tests/WasmAsyncBenchmarkSelectionTests.cs new file mode 100644 index 00000000000..cc7ca3b0c5e --- /dev/null +++ b/src/tests/harness/BenchmarkDotNet.Extensions.Tests/WasmAsyncBenchmarkSelectionTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO.Pipelines.Tests; +using System.IO.Tests; +using System.Linq; +using System.Threading.Tasks; +using System.Threading.Tasks.Tests; +using BenchmarkDotNet.Running; +using MicroBenchmarks; +using Xunit; + +namespace Tests +{ + public class WasmAsyncBenchmarkSelectionTests + { + [Theory] + [InlineData(typeof(Perf_AsyncMethods), nameof(Perf_AsyncMethods.Yield))] + [InlineData(typeof(ValueTaskPerfTest), nameof(ValueTaskPerfTest.CreateAndAwait_FromYieldingAsyncMethod))] + [InlineData(typeof(MemoryStreamTests), nameof(MemoryStreamTests.CopyToAsync))] + [InlineData(typeof(MemoryStreamChunkedTests), nameof(MemoryStreamChunkedTests.ReadAsyncMemory))] + [InlineData(typeof(MemoryStreamChunkedTests), nameof(MemoryStreamChunkedTests.WriteAsyncMemory))] + [InlineData(typeof(MemoryStreamChunkedTests), nameof(MemoryStreamChunkedTests.CopyToAsyncWithBufferSize))] + public void AsyncBenchmarkIsNotExcludedFromWasm(Type benchmarkType, string methodName) + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(benchmarkType).BenchmarksCases + .Where(benchmark => benchmark.Descriptor.WorkloadMethod.Name == methodName) + .ToArray(); + + Assert.NotEmpty(benchmarks); + Assert.All(benchmarks, benchmark => Assert.DoesNotContain(Categories.NoWASM, benchmark.Descriptor.Categories)); + } + + [Fact] + public void OtherAsyncBenchmarksRemainExcludedFromWasm() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(Perf_Pipe)).BenchmarksCases; + + Assert.NotEmpty(benchmarks); + Assert.All(benchmarks, benchmark => Assert.Contains(Categories.NoWASM, benchmark.Descriptor.Categories)); + } + } +}