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 }} diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index a23be5a0ed..679e4754dd 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 @@ -65,6 +77,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/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 a494f42d46..798522c6d9 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.mjs 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("wasmRuntimeFlavor", Required = false, Default = Environments.RuntimeFlavor.Mono, HelpText = "Runtime flavor for WASM benchmarks: 'Mono' (default) uses the Mono runtime pack, 'CoreCLR' uses the CoreCLR runtime pack.")] public Environments.RuntimeFlavor WasmRuntimeFlavor { get; set; } diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index a3eebd9fa8..172d57a0e0 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,14 @@ private static bool Validate(CommandLineOptions options, ILogger logger) { logger.WriteLineError($"The provided {nameof(options.AOTCompilerPath)} \"{options.AOTCompilerPath}\" does NOT exist. It MUST be provided."); } + else if (runtimeMoniker >= RuntimeMoniker.WasmNet80 && runtimeMoniker < RuntimeMoniker.MonoAOTLLVM) + { + 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 +294,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,12 +704,13 @@ 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, runtimeFlavor: options.WasmRuntimeFlavor, + mainJsTemplate: options.WasmMainJsTemplate, processTimeoutMinutes: options.WasmProcessTimeoutMinutes); var toolChain = WasmToolchain.From(new NetCoreAppSettings( diff --git a/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/WasmRuntime.cs index 728cb5ff5f..7112490cde 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; } /// /// Specifies the runtime flavor used for WASM benchmarks. (default) resolves the @@ -31,48 +35,80 @@ public class WasmRuntime : Runtime, IEquatable /// public int ProcessTimeoutMinutes { get; } + public FileInfo? MainJsTemplate { get; set; } + /// /// 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. + /// 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.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 = "net8.0", - string displayName = "Wasm", - string javaScriptEngine = "v8", - string javaScriptEngineArguments = "--expose_wasm", - bool aot = false, - string wasmDataDir = "", - RuntimeMoniker moniker = RuntimeMoniker.WasmNet80, + string msBuildMoniker, + RuntimeMoniker moniker, + string displayName, + bool aot, + string javaScriptEngine, + string? javaScriptEngineArguments = "", RuntimeFlavor runtimeFlavor = RuntimeFlavor.Mono, - int processTimeoutMinutes = 10) - : base(moniker, msBuildMoniker, displayName) + int processTimeoutMinutes = 10, + FileInfo? mainJsTemplate = null, + 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; RuntimeFlavor = runtimeFlavor; + IsAOT = aot; ProcessTimeoutMinutes = processTimeoutMinutes; + MainJsTemplate = mainJsTemplate; + } + + private WasmRuntime() : base(RuntimeMoniker.WasmNet80, "Wasm", "Wasm") + { + IsAOT = RuntimeInformation.IsAot; + JavaScriptEngine = ""; + JavaScriptEngineArguments = ""; + ProcessTimeoutMinutes = 10; + 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.RuntimeFlavor == RuntimeFlavor; + { + return other != null + && base.Equals(other) + && other.JavaScriptEngine == JavaScriptEngine + && other.JavaScriptEngineArguments == JavaScriptEngineArguments + && other.JavaScriptEngineArgumentFormatter == JavaScriptEngineArgumentFormatter + && other.IsAOT == IsAOT + && other.ProcessTimeoutMinutes == ProcessTimeoutMinutes + && other.RuntimeFlavor == RuntimeFlavor; + } public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), JavaScriptEngine, JavaScriptEngineArguments, Aot, RuntimeFlavor); + => HashCode.Combine(base.GetHashCode(), JavaScriptEngine, JavaScriptEngineArguments, JavaScriptEngineArgumentFormatter, IsAOT, RuntimeFlavor, ProcessTimeoutMinutes); + + private static string DefaultArgumentFormatter(WasmRuntime runtime, ArtifactsPaths artifactsPaths, string 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/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/Templates/benchmark-main.mjs b/src/BenchmarkDotNet/Templates/benchmark-main.mjs index 7acbbb2a23..f19a3f0730 100644 --- a/src/BenchmarkDotNet/Templates/benchmark-main.mjs +++ b/src/BenchmarkDotNet/Templates/benchmark-main.mjs @@ -1,6 +1,8 @@ // 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"; + const ENVIRONMENT_IS_NODE = typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string"; const ENVIRONMENT_IS_WEB_WORKER = typeof importScripts == "function"; const ENVIRONMENT_IS_WEB = typeof window == "object" || (ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_NODE); @@ -16,12 +18,28 @@ if (!ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WEB && typeof globalThis.crypto === } } -import { dotnet } from './_framework/dotnet.js' +function getAppArgs() { + // v8 + if (globalThis.arguments !== undefined) + return globalThis.arguments; + + // spidermonkey + 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."); +} -// Get command line arguments: Node.js uses process.argv, v8 uses arguments/scriptArgs -const args = typeof process !== 'undefined' ? process.argv.slice(2) : (typeof arguments !== 'undefined' ? [...arguments] : (typeof scriptArgs !== 'undefined' ? scriptArgs : [])); +const args = getAppArgs(); await dotnet .withDiagnosticTracing(false) .withApplicationArguments(...args) - .run() + .run(); diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs index 01d73f2ba4..2bec343667 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 fedfa7751b..cda8087767 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -13,7 +13,6 @@ namespace BenchmarkDotNet.Toolchains.MonoWasm public class WasmGenerator : CsProjGenerator { private readonly string CustomRuntimePack; - private const string MainJS = "benchmark-main.mjs"; public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packagesPath, string customRuntimePack, bool aot) : base(targetFrameworkMoniker, cliPath, packagesPath) @@ -24,24 +23,29 @@ public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packa 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, ((WasmRuntime)buildPartition.Runtime).MainJsTemplate, 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,24 +76,29 @@ 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, FileInfo? mainJsTemplate, string targetMainJsPath) + { + string content = mainJsTemplate is null + ? ResourceHelper.LoadTemplate("benchmark-main.mjs") + : File.ReadAllText(mainJsTemplate.FullName); + + targetMainJsPath.EnsureFolderExists(); + File.WriteAllText(targetMainJsPath, content); + } + + 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, "browser-wasm"); + => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker, "publish"); } } diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs index 6a546714ce..4eaee95fba 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; using BenchmarkDotNet.Characteristics; -using BenchmarkDotNet.Detectors; -using BenchmarkDotNet.Portability; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; using BenchmarkDotNet.Validators; @@ -27,13 +25,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/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..36ba3b6d20 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,28 +28,13 @@ namespace BenchmarkDotNet.IntegrationTests /// public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) { - private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode) - { - var dotnetVersion = "net8.0"; - var logger = new OutputLogger(Output); - var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, runtimeFrameworkVersion: null!, "Wasm", aotCompilerMode: aotCompilerMode); - - return ManualConfig.CreateEmpty() - .AddLogger(logger) - .AddJob(Job.Dry - .WithRuntime(new WasmRuntime(dotnetVersion, moniker: RuntimeMoniker.WasmNet80, javaScriptEngineArguments: "--expose_wasm --module")) - .WithToolchain(WasmToolchain.From(netCoreAppSettings))) - .WithBuildTimeout(TimeSpan.FromSeconds(240)) - .WithOption(ConfigOptions.GenerateMSBuildBinLog, true); - } - - [TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] + [Theory] [InlineData(MonoAotCompilerMode.mini)] - [InlineData(MonoAotCompilerMode.wasm)] + // BUG: https://github.com/dotnet/BenchmarkDotNet/issues/3036 + [InlineData(MonoAotCompilerMode.wasm, Skip = "AOT is broken")] public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) { - // Test fails on Linux non-x64. - if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64) + if (SkipTestRun()) { return; } @@ -56,13 +42,13 @@ public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) CanExecute(GetConfig(aotCompilerMode)); } - [TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] + [Theory] [InlineData(MonoAotCompilerMode.mini)] - [InlineData(MonoAotCompilerMode.wasm)] + // BUG: https://github.com/dotnet/BenchmarkDotNet/issues/3036 + [InlineData(MonoAotCompilerMode.wasm, Skip = "AOT is broken")] public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) { - // Test fails on Linux non-x64. - if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64) + if (SkipTestRun()) { return; } @@ -83,6 +69,63 @@ public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) } } + [Fact] + 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); + } + + [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); + var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, runtimeFrameworkVersion: null!, "Wasm", aotCompilerMode: aotCompilerMode); + + 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, mainJsTemplate: mainJsTemplate)) + .WithToolchain(WasmToolchain.From(netCoreAppSettings))) + .WithBuildTimeout(TimeSpan.FromSeconds(240)) + .WithOption(ConfigOptions.KeepBenchmarkFiles, keepBenchmarkFiles) + .WithOption(ConfigOptions.LogBuildOutput, true) + .WithOption(ConfigOptions.GenerateMSBuildBinLog, false); + } + + private static bool SkipTestRun() + { + // jsvu only supports arm for mac. + if (RuntimeInformation.GetCurrentPlatform() != Platform.X64 && !OsDetector.IsMacOS()) + { + return true; + } + + return false; + } + 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..51d300b521 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 runtime = Assert.IsType(job.Environment.Runtime); + Assert.Equal("dummyFile.js", runtime.MainJsTemplate?.Name); + } + [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