From 41aee37ccc6ef284a738162d60256746421c68fb Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Wed, 17 Dec 2025 17:29:23 +0100 Subject: [PATCH 01/12] wip(Toolchain)!: Add wasm main.js template option, resolve wasmEngine from PATH - resolve js runtime from path - add option to specify wasm main.js template - add custom js runtime argument formatter option --- samples/BenchmarkDotNet.Samples/IntroWasm.cs | 6 +- .../ConsoleArguments/CommandLineOptions.cs | 12 ++-- .../ConsoleArguments/ConfigParser.cs | 33 +++++---- .../Environments/Runtimes/WasmRuntime.cs | 67 ++++++++++++------- src/BenchmarkDotNet/Helpers/ProcessHelper.cs | 50 ++++++++++++++ src/BenchmarkDotNet/Templates/WasmCsProj.txt | 3 +- .../Toolchains/MonoWasm/WasmExecutor.cs | 3 +- .../Toolchains/MonoWasm/WasmGenerator.cs | 41 +++++++----- .../Toolchains/MonoWasm/WasmToolchain.cs | 37 +++++----- .../BenchmarkDotNet.IntegrationTests.csproj | 1 + .../WasmTests.cs | 39 +++++++++-- .../wwwroot/custom-main.mjs | 11 +++ .../ConfigParserTests.cs | 34 ++++++++-- 13 files changed, 242 insertions(+), 95 deletions(-) create mode 100644 tests/BenchmarkDotNet.IntegrationTests/wwwroot/custom-main.mjs diff --git a/samples/BenchmarkDotNet.Samples/IntroWasm.cs b/samples/BenchmarkDotNet.Samples/IntroWasm.cs index 29411e37d4..108c03b022 100644 --- a/samples/BenchmarkDotNet.Samples/IntroWasm.cs +++ b/samples/BenchmarkDotNet.Samples/IntroWasm.cs @@ -34,9 +34,9 @@ public static void Run() // the Wasm Toolchain requires two mandatory arguments: const string cliPath = @"/home/adam/projects/runtime/dotnet.sh"; - WasmRuntime runtime = new WasmRuntime(msBuildMoniker: "net5.0"); + WasmRuntime runtime = new WasmRuntime(msBuildMoniker: "net8.0", RuntimeMoniker.WasmNet80, "Wasm .net8.0", false, "v8"); NetCoreAppSettings netCoreAppSettings = new NetCoreAppSettings( - targetFrameworkMoniker: "net5.0", runtimeFrameworkVersion: "", name: "Wasm", + targetFrameworkMoniker: "net8.0", runtimeFrameworkVersion: "", name: "Wasm", customDotNetCliPath: cliPath); IToolchain toolChain = WasmToolchain.From(netCoreAppSettings); @@ -50,4 +50,4 @@ public void Foo() // Benchmark body } } -} \ No newline at end of file +} diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index 44ea9447de..de5a08b14d 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -201,12 +201,15 @@ public bool UseDisassemblyDiagnoser [Option("memoryRandomization", Required = false, HelpText = "Specifies whether Engine should allocate some random-sized memory between iterations. It makes [GlobalCleanup] and [GlobalSetup] methods to be executed after every iteration.")] public bool MemoryRandomization { get; set; } - [Option("wasmEngine", Required = false, HelpText = "Full path to a java script engine used to run the benchmarks, used by Wasm toolchain.")] - public FileInfo? WasmJavascriptEngine { get; set; } + [Option("wasmEngine", Required = false, HelpText = "Specifies the executable (in PATH) or full path to a java script engine used to run the benchmarks, used by Wasm toolchain.", Default = "v8")] + public string? WasmJavaScriptEngine { get; set; } = "v8"; - [Option("wasmArgs", Required = false, Default = "--expose_wasm", HelpText = "Arguments for the javascript engine used by Wasm toolchain.")] + [Option("wasmArgs", Required = false, HelpText = "Arguments for the javascript engine used by Wasm toolchain.")] public string? WasmJavaScriptEngineArguments { get; set; } + [Option("wasmMainJsTemplate", Required = false, HelpText = "Path to main.js template.")] + public FileInfo? WasmMainJsTemplate { get; set; } + [Option("customRuntimePack", Required = false, HelpText = "Path to a custom runtime pack. Only used for wasm/MonoAotLLVM currently.")] public string? CustomRuntimePack { get; set; } @@ -216,9 +219,6 @@ public bool UseDisassemblyDiagnoser [Option("AOTCompilerMode", Required = false, Default = MonoAotCompilerMode.mini, HelpText = "Mono AOT compiler mode, either 'mini' or 'llvm'")] public MonoAotCompilerMode AOTCompilerMode { get; set; } - [Option("wasmDataDir", Required = false, HelpText = "Wasm data directory")] - public DirectoryInfo? WasmDataDirectory { get; set; } - [Option("wasmCoreCLR", Required = false, Default = false, HelpText = "Use CoreCLR runtime pack (Microsoft.NETCore.App.Runtime.browser-wasm) instead of the Mono runtime pack for WASM benchmarks.")] public bool WasmCoreCLR { get; set; } diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 306462974b..83b86e02d3 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -14,21 +14,22 @@ using BenchmarkDotNet.Exporters.Xml; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Filters; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Portability; using BenchmarkDotNet.Reports; -using BenchmarkDotNet.Toolchains.R2R; using BenchmarkDotNet.Toolchains.CoreRun; using BenchmarkDotNet.Toolchains.CsProj; using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.Mono; using BenchmarkDotNet.Toolchains.MonoAotLLVM; using BenchmarkDotNet.Toolchains.MonoWasm; using BenchmarkDotNet.Toolchains.NativeAot; +using BenchmarkDotNet.Toolchains.R2R; using CommandLine; using Perfolizer.Horology; using Perfolizer.Mathematics.OutlierDetection; -using BenchmarkDotNet.Toolchains.Mono; using Perfolizer.Metrology; namespace BenchmarkDotNet.ConsoleArguments @@ -249,6 +250,15 @@ private static bool Validate(CommandLineOptions options, ILogger logger) { logger.WriteLineError($"The provided {nameof(options.AOTCompilerPath)} \"{options.AOTCompilerPath}\" does NOT exist. It MUST be provided."); } + // TODO: find a better way to check this. + else if (runtimeMoniker == RuntimeMoniker.WasmNet80 || runtimeMoniker == RuntimeMoniker.WasmNet90 || runtimeMoniker == RuntimeMoniker.WasmNet10_0 || runtimeMoniker == RuntimeMoniker.WasmNet11_0) + { + if (!ProcessHelper.TryResolveExecutableInPath(options.WasmJavaScriptEngine, out _)) + { + logger.WriteLineError($"The provided {nameof(options.WasmJavaScriptEngine)} \"{options.WasmJavaScriptEngine}\" does NOT exist."); + return false; + } + } } foreach (string exporter in options.Exporters) @@ -285,12 +295,6 @@ private static bool Validate(CommandLineOptions options, ILogger logger) return false; } - if (options.WasmJavascriptEngine.IsNotNullButDoesNotExist()) - { - logger.WriteLineError($"The provided {nameof(options.WasmJavascriptEngine)} \"{options.WasmJavascriptEngine}\" does NOT exist."); - return false; - } - if (options.IlcPackages.IsNotNullButDoesNotExist()) { logger.WriteLineError($"The provided {nameof(options.IlcPackages)} \"{options.IlcPackages}\" does NOT exist."); @@ -701,18 +705,21 @@ private static Job MakeWasmJob(Job baseJob, CommandLineOptions options, string m var wasmRuntime = new WasmRuntime( msBuildMoniker: msBuildMoniker, - javaScriptEngine: options.WasmJavascriptEngine?.FullName ?? "v8", - javaScriptEngineArguments: options.WasmJavaScriptEngineArguments ?? "", - aot: wasmAot, - wasmDataDir: options.WasmDataDirectory?.FullName ?? "", moniker: moniker, + displayName: "Wasm", + javaScriptEngine: options.WasmJavaScriptEngine, + javaScriptEngineArguments: options.WasmJavaScriptEngineArguments, + aot: wasmAot, isMonoRuntime: !options.WasmCoreCLR); var toolChain = WasmToolchain.From(new NetCoreAppSettings( targetFrameworkMoniker: wasmRuntime.MsBuildMoniker, runtimeFrameworkVersion: "", name: wasmRuntime.Name, - options: options)); + customDotNetCliPath: options.CliPath?.FullName ?? "", + packagesPath: options.RestorePath?.FullName ?? "", + customRuntimePack: options.CustomRuntimePack ?? "", + aotCompilerMode: options.AOTCompilerMode), options.WasmMainJsTemplate?.FullName); return baseJob.WithRuntime(wasmRuntime).WithToolchain(toolChain).WithId(wasmRuntime.Name); } diff --git a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs index 0c148c0352..0e701ba13f 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs @@ -1,13 +1,17 @@ using System; using System.ComponentModel; using System.IO; -using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Portability; +using BenchmarkDotNet.Toolchains; namespace BenchmarkDotNet.Environments { public class WasmRuntime : Runtime, IEquatable { + public delegate string ArgumentFormatter(WasmRuntime runtime, ArtifactsPaths artifactsPaths, string args); + [EditorBrowsable(EditorBrowsableState.Never)] internal static readonly WasmRuntime Default = new WasmRuntime(); @@ -15,9 +19,9 @@ public class WasmRuntime : Runtime, IEquatable public string JavaScriptEngineArguments { get; } - public bool Aot { get; } + public ArgumentFormatter JavaScriptEngineArgumentFormatter { get; } - public string WasmDataDir { get; } + public override bool IsAOT { get; } /// /// When true (default), the generated project uses Microsoft.NET.Sdk.WebAssembly which sets UseMonoRuntime=true @@ -29,42 +33,55 @@ public class WasmRuntime : Runtime, IEquatable /// /// creates new instance of WasmRuntime /// - /// Full path to a java script engine used to run the benchmarks. "v8" by default - /// Arguments for the javascript engine. "--expose_wasm" by default - /// moniker, default: "net5.0" - /// default: "Wasm" - /// Specifies whether AOT or Interpreter (default) project should be generated. - /// Specifies a wasm data directory surfaced as $(WasmDataDir) for the project + /// moniker /// Runtime moniker + /// display name + /// Specifies whether AOT or Interpreter project should be generated. + /// Full path to a java script engine used to run the benchmarks. /// When true (default), use Mono runtime pack; when false, use CoreCLR runtime pack. + /// Arguments for the javascript engine. + /// Allows to format or customize the arguments passed to the javascript engine. public WasmRuntime( - string msBuildMoniker = "net8.0", - string displayName = "Wasm", - string javaScriptEngine = "v8", - string javaScriptEngineArguments = "--expose_wasm", - bool aot = false, - string wasmDataDir = "", - RuntimeMoniker moniker = RuntimeMoniker.WasmNet80, - bool isMonoRuntime = true) - : base(moniker, msBuildMoniker, displayName) + string msBuildMoniker, + RuntimeMoniker moniker, + string displayName, + bool aot, + string? javaScriptEngine, + bool isMonoRuntime = true, + string? javaScriptEngineArguments = "", + ArgumentFormatter? javaScriptEngineArgumentFormatter = null) : base(moniker, msBuildMoniker, displayName) { - if (javaScriptEngine.IsNotBlank() && javaScriptEngine != "v8" && !File.Exists(javaScriptEngine)) - throw new FileNotFoundException($"Provided {nameof(javaScriptEngine)} file: \"{javaScriptEngine}\" doest NOT exist"); + // Resolve path for windows because we can't use ProcessStartInfo.UseShellExecute while redirecting std out in the executor. + if (!ProcessHelper.TryResolveExecutableInPath(javaScriptEngine, out javaScriptEngine)) + throw new FileNotFoundException($"Provided {nameof(javaScriptEngine)} file: \"{javaScriptEngine}\" does NOT exist"); JavaScriptEngine = javaScriptEngine; - JavaScriptEngineArguments = javaScriptEngineArguments; - Aot = aot; - WasmDataDir = wasmDataDir; + JavaScriptEngineArguments = javaScriptEngineArguments ?? ""; + JavaScriptEngineArgumentFormatter = javaScriptEngineArgumentFormatter ?? DefaultArgumentFormatter; IsMonoRuntime = isMonoRuntime; + IsAOT = aot; + } + + private WasmRuntime() : base(RuntimeMoniker.WasmNet80, "Wasm", "Wasm") + { + IsAOT = RuntimeInformation.IsAot; + JavaScriptEngine = ""; + JavaScriptEngineArguments = ""; + JavaScriptEngineArgumentFormatter = DefaultArgumentFormatter; } public override bool Equals(object? obj) => obj is WasmRuntime other && Equals(other); public bool Equals(WasmRuntime? other) - => other != null && base.Equals(other) && other.JavaScriptEngine == JavaScriptEngine && other.JavaScriptEngineArguments == JavaScriptEngineArguments && other.Aot == Aot && other.IsMonoRuntime == IsMonoRuntime; + => other != null && base.Equals(other) && other.JavaScriptEngine == JavaScriptEngine && other.JavaScriptEngineArguments == JavaScriptEngineArguments && other.IsAOT == IsAOT && other.IsMonoRuntime == IsMonoRuntime; public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), JavaScriptEngine, JavaScriptEngineArguments, Aot, IsMonoRuntime); + => HashCode.Combine(base.GetHashCode(), JavaScriptEngine, JavaScriptEngineArguments, IsAOT, IsMonoRuntime); + + private static string DefaultArgumentFormatter(WasmRuntime runtime, ArtifactsPaths artifactsPaths, string args) + { + return $"{runtime.JavaScriptEngineArguments} --module {artifactsPaths.ExecutablePath} -- --run {artifactsPaths.ProgramName}.dll {args}"; + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/ProcessHelper.cs b/src/BenchmarkDotNet/Helpers/ProcessHelper.cs index 03ebf61f39..aba526a516 100644 --- a/src/BenchmarkDotNet/Helpers/ProcessHelper.cs +++ b/src/BenchmarkDotNet/Helpers/ProcessHelper.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Loggers; @@ -119,5 +121,53 @@ internal static bool TestCommandExists(string commandName, string arguments = "- return false; } } + + internal static bool TryResolveExecutableInPath(string? value, [NotNullWhen(true)] out string? result) + { + result = value!; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (File.Exists(value)) + return true; + + // Typed to char[] because it could be a string or char[] with newer .net versions + var directories = Environment.GetEnvironmentVariable("PATH")! + .Split((char[])[Path.PathSeparator], StringSplitOptions.RemoveEmptyEntries); + + if (OsDetector.IsWindows()) + { + var extensions = Environment.GetEnvironmentVariable("PATHEXT")! + .Split((char[])[Path.PathSeparator], StringSplitOptions.RemoveEmptyEntries); + + foreach (var directory in directories) + { + foreach (var ext in extensions) + { + var candidate = Path.Combine(directory, value + ext); + if (File.Exists(candidate)) + { + result = candidate; + return true; + } + } + } + } + else + { + foreach (var directory in directories) + { + var candidate = Path.Combine(directory, value); + if (File.Exists(Path.Combine(directory, value))) + { + result = candidate; + return true; + } + } + } + + return false; + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Templates/WasmCsProj.txt b/src/BenchmarkDotNet/Templates/WasmCsProj.txt index 3daf85e070..5ab10b2a57 100644 --- a/src/BenchmarkDotNet/Templates/WasmCsProj.txt +++ b/src/BenchmarkDotNet/Templates/WasmCsProj.txt @@ -3,8 +3,7 @@ $CSPROJPATH$ $([System.IO.Path]::ChangeExtension('$(OriginalCSProjPath)', '.Wasm.props')) $([System.IO.Path]::ChangeExtension('$(OriginalCSProjPath)', '.Wasm.targets')) - $WASMDATADIR$ - $([MSBuild]::NormalizeDirectory($(WasmDataDir))) + $MAINJS$ diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs index 4534670f50..50b3e2606c 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs @@ -56,12 +56,11 @@ private static ExecuteResult Execute(BenchmarkCase benchmarkCase, BenchmarkId be private static Process CreateProcess(BenchmarkCase benchmarkCase, ArtifactsPaths artifactsPaths, string args, IResolver resolver) { WasmRuntime runtime = (WasmRuntime)benchmarkCase.GetRuntime(); - const string mainJs = "benchmark-main.mjs"; var start = new ProcessStartInfo { FileName = runtime.JavaScriptEngine, - Arguments = $"{runtime.JavaScriptEngineArguments} {mainJs} -- --run {artifactsPaths.ProgramName}.dll {args} ", + Arguments = runtime.JavaScriptEngineArgumentFormatter(runtime, artifactsPaths, args), WorkingDirectory = Path.Combine(artifactsPaths.BinariesDirectoryPath, "wwwroot"), UseShellExecute = false, RedirectStandardOutput = true, diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index 9bd1835fe6..4cf2d7fa68 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -13,35 +13,41 @@ namespace BenchmarkDotNet.Toolchains.MonoWasm public class WasmGenerator : CsProjGenerator { private readonly string CustomRuntimePack; - private const string MainJS = "benchmark-main.mjs"; + private readonly string? MainJsTemplatePath; - public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packagesPath, string customRuntimePack, bool aot) + public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packagesPath, string customRuntimePack, bool aot, string? mainJsTemplatePath) : base(targetFrameworkMoniker, cliPath, packagesPath) { CustomRuntimePack = customRuntimePack; + MainJsTemplatePath = mainJsTemplatePath; BenchmarkRunCallType = aot ? Code.CodeGenBenchmarkRunCallType.Direct : Code.CodeGenBenchmarkRunCallType.Reflection; } protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { - if (((WasmRuntime)buildPartition.Runtime).Aot) + var targetMainJsPath = GetExecutablePath(Path.GetDirectoryName(artifactsPaths.ProjectFilePath)!, ""); + + if (buildPartition.Runtime.IsAOT) { - GenerateProjectFile(buildPartition, artifactsPaths, aot: true, logger); + GenerateProjectFile(buildPartition, artifactsPaths, aot: true, logger, targetMainJsPath); var linkDescriptionFileName = "WasmLinkerDescription.xml"; File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath)!, linkDescriptionFileName), ResourceHelper.LoadTemplate(linkDescriptionFileName)); - } else + } + else { - GenerateProjectFile(buildPartition, artifactsPaths, aot: false, logger); + GenerateProjectFile(buildPartition, artifactsPaths, aot: false, logger: logger, targetMainJsPath); } + + GenerateMainJS(buildPartition, targetMainJsPath); } - protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, bool aot, ILogger logger) + protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, bool aot, ILogger logger, string targetMainJsPath) { BenchmarkCase benchmark = buildPartition.RepresentativeBenchmarkCase; var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger); - WasmRuntime runtime = (WasmRuntime) buildPartition.Runtime; + WasmRuntime runtime = (WasmRuntime)buildPartition.Runtime; var xmlDoc = new XmlDocument(); xmlDoc.Load(projectFile.FullName); @@ -72,22 +78,27 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths .Replace("$PROGRAMNAME$", artifactsPaths.ProgramName) .Replace("$COPIEDSETTINGS$", customProperties) .Replace("$SDKNAME$", sdkName) - .Replace("$WASMDATADIR$", runtime.WasmDataDir) .Replace("$TARGET$", CustomRuntimePack.IsNotBlank() ? "PublishWithCustomRuntimePack" : "Publish") + .Replace("$MAINJS$", targetMainJsPath) .Replace("$CORECLR_OVERRIDES$", coreclrOverrides) .ToString(); File.WriteAllText(artifactsPaths.ProjectFilePath, content); - // Place benchmark-main.mjs in wwwroot/ next to the generated csproj. - string projectWwwroot = Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath)!, "wwwroot"); - Directory.CreateDirectory(projectWwwroot); - File.WriteAllText(Path.Combine(projectWwwroot, MainJS), ResourceHelper.LoadTemplate(MainJS)); - GatherReferences(buildPartition, artifactsPaths, logger); } - protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "wwwroot", MainJS); + protected void GenerateMainJS(BuildPartition buildPartition, string targetMainJsPath) + { + string content = MainJsTemplatePath is null + ? ResourceHelper.LoadTemplate("benchmark-main.mjs") + : File.ReadAllText(Path.Combine(Path.GetDirectoryName(buildPartition.AssemblyLocation)!, MainJsTemplatePath)); + + targetMainJsPath.EnsureFolderExists(); + File.WriteAllText(targetMainJsPath, content); + } + + protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "wwwroot", "main.js"); protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration) => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker, "browser-wasm"); diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs index 6a546714ce..fc3652c7ec 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Detectors; -using BenchmarkDotNet.Portability; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; using BenchmarkDotNet.Validators; @@ -14,10 +13,14 @@ public class WasmToolchain : Toolchain { private string CustomDotNetCliPath { get; } - private WasmToolchain(string name, IGenerator generator, IBuilder builder, IExecutor executor, string customDotNetCliPath) + internal string? MainJsTemplatePath { get; } + + + private WasmToolchain(string name, IGenerator generator, IBuilder builder, IExecutor executor, string customDotNetCliPath, string? mainJsTemplatePath) : base(name, generator, builder, executor) { CustomDotNetCliPath = customDotNetCliPath; + MainJsTemplatePath = mainJsTemplatePath; } public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) @@ -41,18 +44,22 @@ public override IEnumerable Validate(BenchmarkCase benchmarkCas } [PublicAPI] - public static IToolchain From(NetCoreAppSettings netCoreAppSettings) - => new WasmToolchain(netCoreAppSettings.Name, - new WasmGenerator(netCoreAppSettings.TargetFrameworkMoniker, - netCoreAppSettings.CustomDotNetCliPath, - netCoreAppSettings.PackagesPath, - netCoreAppSettings.CustomRuntimePack, - netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm), - new DotNetCliPublisher(netCoreAppSettings.TargetFrameworkMoniker, - netCoreAppSettings.CustomDotNetCliPath, - // aot builds can be very slow - logOutput: netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm), - new WasmExecutor(), - netCoreAppSettings.CustomDotNetCliPath); + public static IToolchain From(NetCoreAppSettings netCoreAppSettings, string? mainJsTemplatePath = null) + { + var generator = new WasmGenerator(netCoreAppSettings.TargetFrameworkMoniker, + netCoreAppSettings.CustomDotNetCliPath, + netCoreAppSettings.PackagesPath, + netCoreAppSettings.CustomRuntimePack, + netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm, + mainJsTemplatePath); + + var cliBuilder = new DotNetCliBuilder(netCoreAppSettings.TargetFrameworkMoniker, + netCoreAppSettings.CustomDotNetCliPath, + logOutput: netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm); + + var executor = new WasmExecutor(); + + return new WasmToolchain(netCoreAppSettings.Name, generator, cliBuilder, executor, netCoreAppSettings.CustomDotNetCliPath, mainJsTemplatePath); + } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj index 6d3450dea1..80caddc02b 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj @@ -18,6 +18,7 @@ Always + diff --git a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs index 16f169d089..ac3053bc22 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; @@ -27,18 +28,21 @@ namespace BenchmarkDotNet.IntegrationTests /// public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) { - private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode) + private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode, bool useMainJsTemplate = false, bool keepBenchmarkFiles = false) { var dotnetVersion = "net8.0"; var logger = new OutputLogger(Output); var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, runtimeFrameworkVersion: null!, "Wasm", aotCompilerMode: aotCompilerMode); + var mainJsTemplatePath = useMainJsTemplate ? Path.Combine("wwwroot", "custom-main.mjs") : null; + return ManualConfig.CreateEmpty() .AddLogger(logger) .AddJob(Job.Dry - .WithRuntime(new WasmRuntime(dotnetVersion, moniker: RuntimeMoniker.WasmNet80, javaScriptEngineArguments: "--expose_wasm --module")) - .WithToolchain(WasmToolchain.From(netCoreAppSettings))) + .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet80, "wasm", false, "v8")) + .WithToolchain(WasmToolchain.From(netCoreAppSettings, mainJsTemplatePath))) .WithBuildTimeout(TimeSpan.FromSeconds(240)) + .WithOption(ConfigOptions.KeepBenchmarkFiles, keepBenchmarkFiles) .WithOption(ConfigOptions.GenerateMSBuildBinLog, true); } @@ -47,8 +51,7 @@ private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode) [InlineData(MonoAotCompilerMode.wasm)] public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) { - // Test fails on Linux non-x64. - if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64) + if (SkipTestRun()) { return; } @@ -56,13 +59,29 @@ public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) CanExecute(GetConfig(aotCompilerMode)); } + + [FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] + public void WasmSupportsCustomMainJs() + { + if (SkipTestRun()) + { + return; + } + + var summary = CanExecute(GetConfig(MonoAotCompilerMode.mini, true, true)); + + var artefactsPaths = summary.Reports.Single().GenerateResult.ArtifactsPaths; + Assert.Contains("custom-template-identifier", File.ReadAllText(artefactsPaths.ExecutablePath)); + + Directory.Delete(Path.GetDirectoryName(artefactsPaths.ProjectFilePath)!, true); + } + [TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] [InlineData(MonoAotCompilerMode.mini)] [InlineData(MonoAotCompilerMode.wasm)] public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) { - // Test fails on Linux non-x64. - if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64) + if (SkipTestRun()) { return; } @@ -83,6 +102,12 @@ public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) } } + private static bool SkipTestRun() + { + // Test fails on Linux non-x64. + return OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64; + } + public class WasmBenchmark { [Benchmark] diff --git a/tests/BenchmarkDotNet.IntegrationTests/wwwroot/custom-main.mjs b/tests/BenchmarkDotNet.IntegrationTests/wwwroot/custom-main.mjs new file mode 100644 index 0000000000..764a322100 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/wwwroot/custom-main.mjs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// custom-template-identifier + +import { dotnet } from './_framework/dotnet.js' + +await dotnet + .withDiagnosticTracing(false) + .withApplicationArguments(...arguments) + .run() diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index 80c2841e6e..22b949ea59 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -2,27 +2,29 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.ConsoleArguments; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Portability; using BenchmarkDotNet.Tests.Loggers; using BenchmarkDotNet.Tests.Mocks; using BenchmarkDotNet.Tests.XUnit; using BenchmarkDotNet.Toolchains; -using BenchmarkDotNet.Toolchains.NativeAot; using BenchmarkDotNet.Toolchains.CoreRun; using BenchmarkDotNet.Toolchains.CsProj; using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.MonoWasm; +using BenchmarkDotNet.Toolchains.NativeAot; +using Perfolizer.Horology; using Xunit; using Xunit.Abstractions; -using BenchmarkDotNet.Portability; -using Perfolizer.Horology; namespace BenchmarkDotNet.Tests { @@ -685,7 +687,7 @@ public void UsersCanSpecifyWithoutOverheadEvalution() [Fact(Skip = "This should be handled somehow at CommandLineParser level. See https://github.com/commandlineparser/commandline/pull/892")] public void UserCanSpecifyWasmArgs() { - var parsedConfiguration = ConfigParser.Parse(["--runtimes", "wasm", "--wasmArgs", "--expose_wasm --module"], new OutputLogger(Output)); + var parsedConfiguration = ConfigParser.Parse(["--runtimes", "wasmnet80", "--wasmArgs", "--expose_wasm --module", GetDummyWasmEngine()], new OutputLogger(Output)); Assert.True(parsedConfiguration.isSuccess); Assert.NotNull(parsedConfiguration.config); var jobs = parsedConfiguration.config.GetJobs(); @@ -699,7 +701,7 @@ public void UserCanSpecifyWasmArgs() [Fact] public void UserCanSpecifyWasmArgsUsingEquals() { - var parsedConfiguration = ConfigParser.Parse(["--runtimes", "wasmnet80", "--wasmArgs=--expose_wasm --module"], new OutputLogger(Output)); + var parsedConfiguration = ConfigParser.Parse(["--runtimes", "wasmnet80", "--wasmArgs=--expose_wasm --module" , GetDummyWasmEngine()], new OutputLogger(Output)); Assert.True(parsedConfiguration.isSuccess); Assert.NotNull(parsedConfiguration.config); var jobs = parsedConfiguration.config.GetJobs(); @@ -717,7 +719,8 @@ public void UserCanSpecifyWasmArgsViaResponseFile() File.WriteAllLines(tempResponseFile, [ "--runtimes wasmnet80", - "--wasmArgs \"--expose_wasm --module\"" + "--wasmArgs \"--expose_wasm --module\"", + GetDummyWasmEngine() ]); var parsedConfiguration = ConfigParser.Parse([$"@{tempResponseFile}"], new OutputLogger(Output)); Assert.True(parsedConfiguration.isSuccess); @@ -732,6 +735,17 @@ public void UserCanSpecifyWasmArgsViaResponseFile() } } + [Fact] + public void UserCanSpecifyWasmMainJsTemplate() + { + var parsedConfiguration = ConfigParser.Parse(["--runtimes", "wasmnet80", "--wasmMainJsTemplate", "./dummyFile.js", GetDummyWasmEngine()], new OutputLogger(Output)); + Assert.True(parsedConfiguration.isSuccess); + var job = parsedConfiguration.config!.GetJobs().Single(); + + var toolchain = Assert.IsType(job.Infrastructure.Toolchain); + Assert.EndsWith("dummyFile.js", toolchain.MainJsTemplatePath); + } + [Theory] [InlineData("--filter abc", "--filter *")] [InlineData("-f abc", "--filter *")] @@ -757,5 +771,11 @@ public void CheckUpdateInvalidArgs(string strArgs) Assert.Null(updatedArgs); Assert.False(isSuccess); } + + private string GetDummyWasmEngine() + { + // We know, that this file exists, that's enough. + return $"--wasmEngine={Assembly.GetExecutingAssembly().Location}"; + } } } \ No newline at end of file From c9890a043ce3d285ef9b083e65d5fa260879e866 Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Wed, 18 Feb 2026 17:28:59 +0100 Subject: [PATCH 02/12] feat: wasm windows support --- .github/workflows/run-tests.yaml | 24 +++++ .../Toolchains/MonoWasm/WasmToolchain.cs | 8 -- .../WasmTests.cs | 93 ++++++++++--------- 3 files changed, 75 insertions(+), 50 deletions(-) diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 4af516311f..499ea12638 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -24,6 +24,18 @@ jobs: Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE Add-MpPreference -ExclusionPath $env:TEMP - uses: actions/checkout@v6 + # Setup wasm + - name: Set up node + uses: actions/setup-node@v6 + with: + node-version: "24" + - name: Set up v8 + run: | + npm install jsvu -g + jsvu --os=win64 --engines=v8 + Add-Content -Path $env:GITHUB_PATH -Value "$env:USERPROFILE\.jsvu\bin" + - name: Install wasm-tools workload + run: ./build.cmd install-wasm-tools # Build and Test - name: Run task 'build' shell: cmd @@ -59,6 +71,18 @@ jobs: Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE Add-MpPreference -ExclusionPath $env:TEMP - uses: actions/checkout@v6 + # Setup wasm + - name: Set up node + uses: actions/setup-node@v6 + with: + node-version: "24" + - name: Set up v8 + run: | + npm install jsvu -g + jsvu --os=win64 --engines=v8 + Add-Content -Path $env:GITHUB_PATH -Value "$env:USERPROFILE\.jsvu\bin" + - name: Install wasm-tools workload + run: ./build.cmd install-wasm-tools # Build and Test - name: Run task 'build' shell: cmd diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs index fc3652c7ec..1f897dd87a 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using BenchmarkDotNet.Characteristics; -using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; using BenchmarkDotNet.Validators; @@ -30,13 +29,6 @@ public override IEnumerable Validate(BenchmarkCase benchmarkCas yield return validationError; } - if (OsDetector.IsWindows()) - { - yield return new ValidationError(true, - $"{nameof(WasmToolchain)} is supported only on Unix, benchmark '{benchmarkCase.DisplayInfo}' might not work correctly", - benchmarkCase); - } - foreach (var validationError in DotNetSdkValidator.ValidateCoreSdks(CustomDotNetCliPath, benchmarkCase)) { yield return validationError; diff --git a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs index ac3053bc22..ddeb476769 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs @@ -28,30 +28,12 @@ namespace BenchmarkDotNet.IntegrationTests /// public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) { - private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode, bool useMainJsTemplate = false, bool keepBenchmarkFiles = false) - { - var dotnetVersion = "net8.0"; - var logger = new OutputLogger(Output); - var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, runtimeFrameworkVersion: null!, "Wasm", aotCompilerMode: aotCompilerMode); - - var mainJsTemplatePath = useMainJsTemplate ? Path.Combine("wwwroot", "custom-main.mjs") : null; - - return ManualConfig.CreateEmpty() - .AddLogger(logger) - .AddJob(Job.Dry - .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet80, "wasm", false, "v8")) - .WithToolchain(WasmToolchain.From(netCoreAppSettings, mainJsTemplatePath))) - .WithBuildTimeout(TimeSpan.FromSeconds(240)) - .WithOption(ConfigOptions.KeepBenchmarkFiles, keepBenchmarkFiles) - .WithOption(ConfigOptions.GenerateMSBuildBinLog, true); - } - - [TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] + [Theory] [InlineData(MonoAotCompilerMode.mini)] [InlineData(MonoAotCompilerMode.wasm)] public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) { - if (SkipTestRun()) + if (SkipTestRun(aotCompilerMode)) { return; } @@ -59,29 +41,12 @@ public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) CanExecute(GetConfig(aotCompilerMode)); } - - [FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] - public void WasmSupportsCustomMainJs() - { - if (SkipTestRun()) - { - return; - } - - var summary = CanExecute(GetConfig(MonoAotCompilerMode.mini, true, true)); - - var artefactsPaths = summary.Reports.Single().GenerateResult.ArtifactsPaths; - Assert.Contains("custom-template-identifier", File.ReadAllText(artefactsPaths.ExecutablePath)); - - Directory.Delete(Path.GetDirectoryName(artefactsPaths.ProjectFilePath)!, true); - } - - [TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] + [Theory] [InlineData(MonoAotCompilerMode.mini)] [InlineData(MonoAotCompilerMode.wasm)] public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) { - if (SkipTestRun()) + if (SkipTestRun(aotCompilerMode)) { return; } @@ -102,10 +67,54 @@ public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) } } - private static bool SkipTestRun() + [Fact] + public void WasmSupportsCustomMainJs() { - // Test fails on Linux non-x64. - return OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64; + if (SkipTestRun()) + { + return; + } + + var summary = CanExecute(GetConfig(MonoAotCompilerMode.mini, true, true)); + + var artefactsPaths = summary.Reports.Single().GenerateResult.ArtifactsPaths; + Assert.Contains("custom-template-identifier", File.ReadAllText(artefactsPaths.ExecutablePath)); + + Directory.Delete(Path.GetDirectoryName(artefactsPaths.ProjectFilePath)!, true); + } + + private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode, bool useMainJsTemplate = false, bool keepBenchmarkFiles = false) + { + var dotnetVersion = "net8.0"; + var logger = new OutputLogger(Output); + var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, runtimeFrameworkVersion: null!, "Wasm", aotCompilerMode: aotCompilerMode); + + var mainJsTemplatePath = useMainJsTemplate ? Path.Combine("wwwroot", "custom-main.mjs") : null; + + return ManualConfig.CreateEmpty() + .AddLogger(logger) + .AddJob(Job.Dry + .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet80, "wasm", aotCompilerMode == MonoAotCompilerMode.wasm, "v8")) + .WithToolchain(WasmToolchain.From(netCoreAppSettings, mainJsTemplatePath))) + .WithBuildTimeout(TimeSpan.FromSeconds(240)) + .WithOption(ConfigOptions.KeepBenchmarkFiles, keepBenchmarkFiles) + .WithOption(ConfigOptions.LogBuildOutput, true) + .WithOption(ConfigOptions.GenerateMSBuildBinLog, false); + } + + private static bool SkipTestRun(MonoAotCompilerMode aotCompilerMode = MonoAotCompilerMode.mini) + { + // jsvu only supports arm for mac. + if (RuntimeInformation.GetCurrentPlatform() != Platform.X64 && !OsDetector.IsMacOS()) + { + return true; + } + + // AOT crashes because of BenchmarkDotNet.Running.WakeLock+PInvoke. + if (OsDetector.IsWindows() && aotCompilerMode == MonoAotCompilerMode.wasm) + return true; + + return false; } public class WasmBenchmark From 43e9835ce7dbe475a6df290bc48d2d4250dca213 Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Fri, 20 Feb 2026 15:51:56 +0100 Subject: [PATCH 03/12] feat: improve js runtime support --- .../Environments/Runtimes/WasmRuntime.cs | 6 ++++- .../Templates/benchmark-main.mjs | 27 ++++++++++++++++--- .../WasmTests.cs | 15 +++++++++-- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs index 0e701ba13f..0a28c10219 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs @@ -81,7 +81,11 @@ public override int GetHashCode() private static string DefaultArgumentFormatter(WasmRuntime runtime, ArtifactsPaths artifactsPaths, string args) { - return $"{runtime.JavaScriptEngineArguments} --module {artifactsPaths.ExecutablePath} -- --run {artifactsPaths.ProgramName}.dll {args}"; + return Path.GetFileNameWithoutExtension(runtime.JavaScriptEngine).ToLower() switch + { + "node" or "bun" => $"{runtime.JavaScriptEngineArguments} {artifactsPaths.ExecutablePath} -- --run {artifactsPaths.ProgramName}.dll {args}", + _ => $"{runtime.JavaScriptEngineArguments} --module {artifactsPaths.ExecutablePath} -- --run {artifactsPaths.ProgramName}.dll {args}", + }; } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Templates/benchmark-main.mjs b/src/BenchmarkDotNet/Templates/benchmark-main.mjs index c9f705d547..bb3b7cccaf 100644 --- a/src/BenchmarkDotNet/Templates/benchmark-main.mjs +++ b/src/BenchmarkDotNet/Templates/benchmark-main.mjs @@ -1,9 +1,30 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { dotnet } from './_framework/dotnet.js' +import { dotnet } from "./_framework/dotnet.js"; + +function getAppArgs() { + // v8 + if (globalThis.arguments !== undefined) + return globalThis.arguments; + + // spdermonkey + if (globalThis.scriptArgs !== undefined) + return globalThis.scriptArgs; + + // Node / Bun + if (globalThis.process !== undefined) { + const argv = globalThis.process.argv ?? []; + const sep = argv.indexOf("--"); + return sep >= 0 ? argv.slice(sep + 1) : argv.slice(2); + } + + throw new Error("Unable to determine application arguments for the current runtime."); +} + +const args = getAppArgs(); await dotnet .withDiagnosticTracing(false) - .withApplicationArguments(...arguments) - .run() + .withApplicationArguments(...args) + .run(); diff --git a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs index ddeb476769..5526d9a8b2 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs @@ -83,7 +83,18 @@ public void WasmSupportsCustomMainJs() Directory.Delete(Path.GetDirectoryName(artefactsPaths.ProjectFilePath)!, true); } - private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode, bool useMainJsTemplate = false, bool keepBenchmarkFiles = false) + [Fact] + public void WasmSupportsNode() + { + if (SkipTestRun()) + { + return; + } + + CanExecute(GetConfig(MonoAotCompilerMode.mini, javaScriptEngine: "node")); + } + + private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode, bool useMainJsTemplate = false, bool keepBenchmarkFiles = false, string javaScriptEngine = "v8") { var dotnetVersion = "net8.0"; var logger = new OutputLogger(Output); @@ -94,7 +105,7 @@ private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode, bool useMain return ManualConfig.CreateEmpty() .AddLogger(logger) .AddJob(Job.Dry - .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet80, "wasm", aotCompilerMode == MonoAotCompilerMode.wasm, "v8")) + .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet80, "wasm", aotCompilerMode == MonoAotCompilerMode.wasm, javaScriptEngine)) .WithToolchain(WasmToolchain.From(netCoreAppSettings, mainJsTemplatePath))) .WithBuildTimeout(TimeSpan.FromSeconds(240)) .WithOption(ConfigOptions.KeepBenchmarkFiles, keepBenchmarkFiles) From e366218246d17a27cc7fdbb4ef20ced98ef76d1d Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Sun, 22 Feb 2026 14:23:14 +0100 Subject: [PATCH 04/12] test(wasm): disable aot tests --- .../BenchmarkDotNet.IntegrationTests/WasmTests.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs index 5526d9a8b2..2b813aac23 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs @@ -30,10 +30,10 @@ public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) { [Theory] [InlineData(MonoAotCompilerMode.mini)] - [InlineData(MonoAotCompilerMode.wasm)] + [InlineData(MonoAotCompilerMode.wasm, Skip = "AOT is broken")] public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) { - if (SkipTestRun(aotCompilerMode)) + if (SkipTestRun()) { return; } @@ -43,10 +43,10 @@ public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) [Theory] [InlineData(MonoAotCompilerMode.mini)] - [InlineData(MonoAotCompilerMode.wasm)] + [InlineData(MonoAotCompilerMode.wasm, Skip = "AOT is broken")] public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) { - if (SkipTestRun(aotCompilerMode)) + if (SkipTestRun()) { return; } @@ -113,7 +113,7 @@ private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode, bool useMain .WithOption(ConfigOptions.GenerateMSBuildBinLog, false); } - private static bool SkipTestRun(MonoAotCompilerMode aotCompilerMode = MonoAotCompilerMode.mini) + private static bool SkipTestRun() { // jsvu only supports arm for mac. if (RuntimeInformation.GetCurrentPlatform() != Platform.X64 && !OsDetector.IsMacOS()) @@ -121,10 +121,6 @@ private static bool SkipTestRun(MonoAotCompilerMode aotCompilerMode = MonoAotCom return true; } - // AOT crashes because of BenchmarkDotNet.Running.WakeLock+PInvoke. - if (OsDetector.IsWindows() && aotCompilerMode == MonoAotCompilerMode.wasm) - return true; - return false; } From af6981c188cab098c2e21a43430492628f9d4759 Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Tue, 24 Feb 2026 20:39:34 +0100 Subject: [PATCH 05/12] ci: fix wasm in run-tests-selected pipeline --- .github/workflows/run-tests-selected.yaml | 26 +++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests-selected.yaml b/.github/workflows/run-tests-selected.yaml index 2762ec726e..9d0f4c6ee4 100644 --- a/.github/workflows/run-tests-selected.yaml +++ b/.github/workflows/run-tests-selected.yaml @@ -13,7 +13,7 @@ on: - windows-latest - ubuntu-latest - macos-latest - - windows-11-arm + - windows-11-arm - ubuntu-24.04-arm - macos-15-intel project: @@ -50,12 +50,34 @@ jobs: timeout-minutes: 60 # Explicitly set timeout. When wrong input parameter is passed. It may continue to run until it times out (Default:360 minutes)) steps: - uses: actions/checkout@v4 - + # Setup - name: Setup run: | mkdir artifacts + - name: Install workloads + run: | + dotnet workload install wasm-tools + dotnet workload install wasm-tools-net8 + + - name: Set up node + uses: actions/setup-node@v6 + with: + node-version: "24" + - name: Set up v8 + shell: pwsh + run: | + npm install jsvu -g + jsvu --os=default --engines=v8 + + $homeDir = $env:HOME + if (-not $homeDir) { + $homeDir = $env:USERPROFILE + } + + Add-Content -Path $env:GITHUB_PATH -Value (Join-Path $homeDir ".jsvu/bin") + # Build - name: Run build working-directory: ${{ github.event.inputs.project }} From db484988698d52e457819d1e679672f71403aa89 Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Tue, 24 Feb 2026 19:27:58 +0100 Subject: [PATCH 06/12] fix(WasmGenerator): binary path --- src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index 4cf2d7fa68..c09e80106d 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -101,6 +101,6 @@ protected void GenerateMainJS(BuildPartition buildPartition, string targetMainJs protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "wwwroot", "main.js"); protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration) - => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker, "browser-wasm"); + => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker, "publish"); } } From e2511ae7ddbcaa66d6e5b6308422f4d12c4562b6 Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Thu, 26 Feb 2026 19:28:44 +0100 Subject: [PATCH 07/12] refactor: MainJsTemplate FileInfo, cleanup --- .../ConsoleArguments/ConfigParser.cs | 8 ++++---- .../Environments/Runtimes/WasmRuntime.cs | 8 ++++++-- .../Toolchains/MonoWasm/WasmGenerator.cs | 12 +++++------- .../Toolchains/MonoWasm/WasmToolchain.cs | 13 ++++--------- tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs | 6 +++--- tests/BenchmarkDotNet.Tests/ConfigParserTests.cs | 4 ++-- 6 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 8cce66e4d3..172d57a0e0 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -250,8 +250,7 @@ private static bool Validate(CommandLineOptions options, ILogger logger) { logger.WriteLineError($"The provided {nameof(options.AOTCompilerPath)} \"{options.AOTCompilerPath}\" does NOT exist. It MUST be provided."); } - // TODO: find a better way to check this. - else if (runtimeMoniker == RuntimeMoniker.WasmNet80 || runtimeMoniker == RuntimeMoniker.WasmNet90 || runtimeMoniker == RuntimeMoniker.WasmNet10_0 || runtimeMoniker == RuntimeMoniker.WasmNet11_0) + else if (runtimeMoniker >= RuntimeMoniker.WasmNet80 && runtimeMoniker < RuntimeMoniker.MonoAOTLLVM) { if (!ProcessHelper.TryResolveExecutableInPath(options.WasmJavaScriptEngine, out _)) { @@ -707,17 +706,18 @@ private static Job MakeWasmJob(Job baseJob, CommandLineOptions options, string m msBuildMoniker: msBuildMoniker, moniker: moniker, displayName: "Wasm", - javaScriptEngine: options.WasmJavaScriptEngine, + javaScriptEngine: options.WasmJavaScriptEngine ?? "", javaScriptEngineArguments: options.WasmJavaScriptEngineArguments, aot: wasmAot, runtimeFlavor: options.WasmRuntimeFlavor, + mainJsTemplate: options.WasmMainJsTemplate, processTimeoutMinutes: options.WasmProcessTimeoutMinutes); var toolChain = WasmToolchain.From(new NetCoreAppSettings( targetFrameworkMoniker: wasmRuntime.MsBuildMoniker, runtimeFrameworkVersion: "", name: wasmRuntime.Name, - options: options), options.WasmMainJsTemplate?.FullName); + options: options)); return baseJob.WithRuntime(wasmRuntime).WithToolchain(toolChain).WithId(wasmRuntime.Name); } diff --git a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs index 2274df2568..22fcccd35f 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs @@ -35,6 +35,8 @@ public class WasmRuntime : Runtime, IEquatable /// public int ProcessTimeoutMinutes { get; } + public FileInfo? MainJsTemplate { get; set; } + /// /// creates new instance of WasmRuntime /// @@ -52,14 +54,15 @@ public WasmRuntime( RuntimeMoniker moniker, string displayName, bool aot, - string? javaScriptEngine, + string javaScriptEngine, string? javaScriptEngineArguments = "", RuntimeFlavor runtimeFlavor = RuntimeFlavor.Mono, int processTimeoutMinutes = 10, + FileInfo? mainJsTemplate = null, ArgumentFormatter? javaScriptEngineArgumentFormatter = null) : base(moniker, msBuildMoniker, displayName) { // Resolve path for windows because we can't use ProcessStartInfo.UseShellExecute while redirecting std out in the executor. - if (!ProcessHelper.TryResolveExecutableInPath(javaScriptEngine, out javaScriptEngine)) + if (!ProcessHelper.TryResolveExecutableInPath(javaScriptEngine, out javaScriptEngine!)) throw new FileNotFoundException($"Provided {nameof(javaScriptEngine)} file: \"{javaScriptEngine}\" does NOT exist"); JavaScriptEngine = javaScriptEngine; @@ -68,6 +71,7 @@ public WasmRuntime( RuntimeFlavor = runtimeFlavor; IsAOT = aot; ProcessTimeoutMinutes = processTimeoutMinutes; + MainJsTemplate = mainJsTemplate; } private WasmRuntime() : base(RuntimeMoniker.WasmNet80, "Wasm", "Wasm") diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index b69bce6325..32d57ef7e1 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -13,13 +13,11 @@ namespace BenchmarkDotNet.Toolchains.MonoWasm public class WasmGenerator : CsProjGenerator { private readonly string CustomRuntimePack; - private readonly string? MainJsTemplatePath; - public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packagesPath, string customRuntimePack, bool aot, string? mainJsTemplatePath) + public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packagesPath, string customRuntimePack, bool aot) : base(targetFrameworkMoniker, cliPath, packagesPath) { CustomRuntimePack = customRuntimePack; - MainJsTemplatePath = mainJsTemplatePath; BenchmarkRunCallType = aot ? Code.CodeGenBenchmarkRunCallType.Direct : Code.CodeGenBenchmarkRunCallType.Reflection; } @@ -39,7 +37,7 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts GenerateProjectFile(buildPartition, artifactsPaths, aot: false, logger: logger, targetMainJsPath); } - GenerateMainJS(buildPartition, targetMainJsPath); + GenerateMainJS(buildPartition, ((WasmRuntime)buildPartition.Runtime).MainJsTemplate, targetMainJsPath); } protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, bool aot, ILogger logger, string targetMainJsPath) @@ -88,11 +86,11 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths GatherReferences(buildPartition, artifactsPaths, logger); } - protected void GenerateMainJS(BuildPartition buildPartition, string targetMainJsPath) + protected void GenerateMainJS(BuildPartition buildPartition, FileInfo? mainJsTemplate, string targetMainJsPath) { - string content = MainJsTemplatePath is null + string content = mainJsTemplate is null ? ResourceHelper.LoadTemplate("benchmark-main.mjs") - : File.ReadAllText(Path.Combine(Path.GetDirectoryName(buildPartition.AssemblyLocation)!, MainJsTemplatePath)); + : File.ReadAllText(mainJsTemplate.FullName); targetMainJsPath.EnsureFolderExists(); File.WriteAllText(targetMainJsPath, content); diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs index 1f897dd87a..657dcf5734 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs @@ -12,14 +12,10 @@ public class WasmToolchain : Toolchain { private string CustomDotNetCliPath { get; } - internal string? MainJsTemplatePath { get; } - - - private WasmToolchain(string name, IGenerator generator, IBuilder builder, IExecutor executor, string customDotNetCliPath, string? mainJsTemplatePath) + private WasmToolchain(string name, IGenerator generator, IBuilder builder, IExecutor executor, string customDotNetCliPath) : base(name, generator, builder, executor) { CustomDotNetCliPath = customDotNetCliPath; - MainJsTemplatePath = mainJsTemplatePath; } public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) @@ -36,14 +32,13 @@ public override IEnumerable Validate(BenchmarkCase benchmarkCas } [PublicAPI] - public static IToolchain From(NetCoreAppSettings netCoreAppSettings, string? mainJsTemplatePath = null) + public static IToolchain From(NetCoreAppSettings netCoreAppSettings) { var generator = new WasmGenerator(netCoreAppSettings.TargetFrameworkMoniker, netCoreAppSettings.CustomDotNetCliPath, netCoreAppSettings.PackagesPath, netCoreAppSettings.CustomRuntimePack, - netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm, - mainJsTemplatePath); + netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm); var cliBuilder = new DotNetCliBuilder(netCoreAppSettings.TargetFrameworkMoniker, netCoreAppSettings.CustomDotNetCliPath, @@ -51,7 +46,7 @@ public static IToolchain From(NetCoreAppSettings netCoreAppSettings, string? mai var executor = new WasmExecutor(); - return new WasmToolchain(netCoreAppSettings.Name, generator, cliBuilder, executor, netCoreAppSettings.CustomDotNetCliPath, mainJsTemplatePath); + return new WasmToolchain(netCoreAppSettings.Name, generator, cliBuilder, executor, netCoreAppSettings.CustomDotNetCliPath); } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs index 2b813aac23..cdca695663 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs @@ -100,13 +100,13 @@ private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode, bool useMain var logger = new OutputLogger(Output); var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, runtimeFrameworkVersion: null!, "Wasm", aotCompilerMode: aotCompilerMode); - var mainJsTemplatePath = useMainJsTemplate ? Path.Combine("wwwroot", "custom-main.mjs") : null; + var mainJsTemplate = useMainJsTemplate ? new FileInfo(Path.Combine("wwwroot", "custom-main.mjs")) : null; return ManualConfig.CreateEmpty() .AddLogger(logger) .AddJob(Job.Dry - .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet80, "wasm", aotCompilerMode == MonoAotCompilerMode.wasm, javaScriptEngine)) - .WithToolchain(WasmToolchain.From(netCoreAppSettings, mainJsTemplatePath))) + .WithRuntime(new WasmRuntime(dotnetVersion, RuntimeMoniker.WasmNet80, "wasm", aotCompilerMode == MonoAotCompilerMode.wasm, javaScriptEngine, mainJsTemplate: mainJsTemplate)) + .WithToolchain(WasmToolchain.From(netCoreAppSettings))) .WithBuildTimeout(TimeSpan.FromSeconds(240)) .WithOption(ConfigOptions.KeepBenchmarkFiles, keepBenchmarkFiles) .WithOption(ConfigOptions.LogBuildOutput, true) diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index 22b949ea59..51d300b521 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -742,8 +742,8 @@ public void UserCanSpecifyWasmMainJsTemplate() Assert.True(parsedConfiguration.isSuccess); var job = parsedConfiguration.config!.GetJobs().Single(); - var toolchain = Assert.IsType(job.Infrastructure.Toolchain); - Assert.EndsWith("dummyFile.js", toolchain.MainJsTemplatePath); + var runtime = Assert.IsType(job.Environment.Runtime); + Assert.Equal("dummyFile.js", runtime.MainJsTemplate?.Name); } [Theory] From 92f5a9693b48521dd407193ba99b509366e1aaa2 Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Thu, 26 Feb 2026 19:35:41 +0100 Subject: [PATCH 08/12] fix(WasmRuntime): add missing xml comment --- src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs index 22fcccd35f..f2860b2697 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs @@ -48,6 +48,7 @@ public class WasmRuntime : Runtime, IEquatable /// Arguments for the javascript engine. /// Runtime flavor to use: Mono (default) or CoreCLR. /// Maximum time in minutes to wait for a single benchmark process to finish. Default is 10. + /// Optional custom template for the generated main.js file. If not provided, a default template will be used. /// Allows to format or customize the arguments passed to the javascript engine. public WasmRuntime( string msBuildMoniker, From 9c68a25ce0820fd673c11185f6b7b9fb3da20104 Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Thu, 26 Feb 2026 20:01:10 +0100 Subject: [PATCH 09/12] revert: WasmToolchain changes --- .../Toolchains/MonoWasm/WasmToolchain.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs index 657dcf5734..4eaee95fba 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs @@ -33,20 +33,17 @@ public override IEnumerable Validate(BenchmarkCase benchmarkCas [PublicAPI] public static IToolchain From(NetCoreAppSettings netCoreAppSettings) - { - var generator = new WasmGenerator(netCoreAppSettings.TargetFrameworkMoniker, - netCoreAppSettings.CustomDotNetCliPath, - netCoreAppSettings.PackagesPath, - netCoreAppSettings.CustomRuntimePack, - netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm); - - var cliBuilder = new DotNetCliBuilder(netCoreAppSettings.TargetFrameworkMoniker, - netCoreAppSettings.CustomDotNetCliPath, - logOutput: netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm); - - var executor = new WasmExecutor(); - - return new WasmToolchain(netCoreAppSettings.Name, generator, cliBuilder, executor, netCoreAppSettings.CustomDotNetCliPath); - } + => new WasmToolchain(netCoreAppSettings.Name, + new WasmGenerator(netCoreAppSettings.TargetFrameworkMoniker, + netCoreAppSettings.CustomDotNetCliPath, + netCoreAppSettings.PackagesPath, + netCoreAppSettings.CustomRuntimePack, + netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm), + new DotNetCliPublisher(netCoreAppSettings.TargetFrameworkMoniker, + netCoreAppSettings.CustomDotNetCliPath, + // aot builds can be very slow + logOutput: netCoreAppSettings.AOTCompilerMode == MonoAotLLVM.MonoAotCompilerMode.wasm), + new WasmExecutor(), + netCoreAppSettings.CustomDotNetCliPath); } } \ No newline at end of file From 9ccecaa5ce83e8a1ab3bb25b4453a13db09c1c6c Mon Sep 17 00:00:00 2001 From: Luis Frey Date: Sun, 1 Mar 2026 17:42:14 +0100 Subject: [PATCH 10/12] refactor: minor cleanup --- src/BenchmarkDotNet/Templates/benchmark-main.mjs | 2 +- src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs | 2 +- tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/BenchmarkDotNet/Templates/benchmark-main.mjs b/src/BenchmarkDotNet/Templates/benchmark-main.mjs index 0a24a58c4a..f19a3f0730 100644 --- a/src/BenchmarkDotNet/Templates/benchmark-main.mjs +++ b/src/BenchmarkDotNet/Templates/benchmark-main.mjs @@ -23,7 +23,7 @@ function getAppArgs() { if (globalThis.arguments !== undefined) return globalThis.arguments; - // spdermonkey + // spidermonkey if (globalThis.scriptArgs !== undefined) return globalThis.scriptArgs; diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index 32d57ef7e1..cda8087767 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -96,7 +96,7 @@ protected void GenerateMainJS(BuildPartition buildPartition, FileInfo? mainJsTem File.WriteAllText(targetMainJsPath, content); } - protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "wwwroot", "main.js"); + protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "wwwroot", "main.mjs"); protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration) => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker, "publish"); diff --git a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs index cdca695663..36ba3b6d20 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs @@ -30,6 +30,7 @@ public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) { [Theory] [InlineData(MonoAotCompilerMode.mini)] + // BUG: https://github.com/dotnet/BenchmarkDotNet/issues/3036 [InlineData(MonoAotCompilerMode.wasm, Skip = "AOT is broken")] public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) { @@ -43,6 +44,7 @@ public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) [Theory] [InlineData(MonoAotCompilerMode.mini)] + // BUG: https://github.com/dotnet/BenchmarkDotNet/issues/3036 [InlineData(MonoAotCompilerMode.wasm, Skip = "AOT is broken")] public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) { From 5a31363287143e0b43d4c04bb88886aa8a507725 Mon Sep 17 00:00:00 2001 From: Luis Frey <58328398+FreyLuis@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:34:08 +0100 Subject: [PATCH 11/12] Update src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs Co-authored-by: Tim Cassell --- src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs index f2860b2697..7112490cde 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs @@ -48,7 +48,7 @@ public class WasmRuntime : Runtime, IEquatable /// Arguments for the javascript engine. /// Runtime flavor to use: Mono (default) or CoreCLR. /// Maximum time in minutes to wait for a single benchmark process to finish. Default is 10. - /// Optional custom template for the generated main.js file. If not provided, a default template will be used. + /// Optional custom template for the generated main.mjs file. If not provided, a default template will be used. /// Allows to format or customize the arguments passed to the javascript engine. public WasmRuntime( string msBuildMoniker, From 170c969671c96a0fe9b9c0abdd050c624f375f30 Mon Sep 17 00:00:00 2001 From: Luis Frey <58328398+FreyLuis@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:34:28 +0100 Subject: [PATCH 12/12] Update src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs Co-authored-by: Tim Cassell --- src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index a31c1abf2d..798522c6d9 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -207,7 +207,7 @@ public bool UseDisassemblyDiagnoser [Option("wasmArgs", Required = false, HelpText = "Arguments for the javascript engine used by Wasm toolchain.")] public string? WasmJavaScriptEngineArguments { get; set; } - [Option("wasmMainJsTemplate", Required = false, HelpText = "Path to main.js template.")] + [Option("wasmMainJsTemplate", Required = false, HelpText = "Path to main.mjs template.")] public FileInfo? WasmMainJsTemplate { get; set; } [Option("customRuntimePack", Required = false, HelpText = "Path to a custom runtime pack. Only used for wasm/MonoAotLLVM currently.")]