Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/BenchmarkDotNet/Code/CodeGenBenchmarkRunCallType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace BenchmarkDotNet.Code;

/// <summary>
/// Specifies how to generate the code that calls the benchmark's Run method.
/// </summary>
public enum CodeGenBenchmarkRunCallType
{
/// <summary>
/// Use reflection to call the benchmark's Run method indirectly.
/// </summary>
/// <remarks>
/// This is to avoid strong dependency Main-to-Runnable
/// which could cause the jitting/assembly loading to happen before we do anything.
/// We have some jitting diagnosers and we want them to catch all the informations.
/// </remarks>
Reflection,
/// <summary>
/// Uses a switch to select the benchmark to call Run directly.
/// </summary>
/// <remarks>
/// This is for AOT runtimes where reflection may not exist or the benchmark types
/// could be trimmed out when they are not directly referenced.
/// </remarks>
Direct
}
33 changes: 15 additions & 18 deletions src/BenchmarkDotNet/Code/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,12 @@ namespace BenchmarkDotNet.Code
{
internal static class CodeGenerator
{
internal static string Generate(BuildPartition buildPartition)
internal static string Generate(BuildPartition buildPartition, CodeGenBenchmarkRunCallType benchmarkRunCallType)
{
(bool useShadowCopy, string shadowCopyFolderPath) = GetShadowCopySettings();

var benchmarksCode = new List<string>(buildPartition.Benchmarks.Length);

var extraDefines = new List<string>();

foreach (var buildInfo in buildPartition.Benchmarks)
{
var benchmark = buildInfo.BenchmarkCase;
Expand Down Expand Up @@ -64,19 +62,12 @@ internal static string Generate(BuildPartition buildPartition)
benchmarksCode.Add(benchmarkTypeCode);
}

if (buildPartition.IsNativeAot)
extraDefines.Add("#define NATIVEAOT");
else if (buildPartition.IsNetFramework)
extraDefines.Add("#define NETFRAMEWORK");
else if (buildPartition.IsWasm)
extraDefines.Add("#define WASM");

string benchmarkProgramContent = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkProgram.txt"))
.Replace("$ShadowCopyDefines$", useShadowCopy ? "#define SHADOWCOPY" : null).Replace("$ShadowCopyFolderPath$", shadowCopyFolderPath)
.Replace("$ExtraDefines$", string.Join(Environment.NewLine, extraDefines))
.Replace("$ExtraDefines$", buildPartition.IsNetFramework ? "#define NETFRAMEWORK" : string.Empty)
.Replace("$DerivedTypes$", string.Join(Environment.NewLine, benchmarksCode))
.Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition.RepresentativeBenchmarkCase.Descriptor))
.Replace("$NativeAotSwitch$", GetNativeAotSwitch(buildPartition))
.Replace("$BenchmarkRunCall$", GetBenchmarkRunCall(buildPartition, benchmarkRunCallType))
.ToString();

return benchmarkProgramContent;
Expand Down Expand Up @@ -251,14 +242,20 @@ private static string GetParameterModifier(ParameterInfo parameterInfo)
return "ref";
}

/// <summary>
/// for NativeAOT we can't use reflection to load type and run a method, so we simply generate a switch for all types..
/// </summary>
private static string GetNativeAotSwitch(BuildPartition buildPartition)
private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGenBenchmarkRunCallType runCallType)
{
if (!buildPartition.IsNativeAot)
return default;
if (runCallType == CodeGenBenchmarkRunCallType.Reflection)
{
// Use reflection to call benchmark's Run method indirectly.
return """
typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly
.GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}")
.GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static)
.Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode });
""";
}

// Generate a switch to call benchmark's Run method directly.
var @switch = new StringBuilder(buildPartition.Benchmarks.Length * 30);
@switch.AppendLine("switch (id) {");

Expand Down
4 changes: 0 additions & 4 deletions src/BenchmarkDotNet/Running/BuildPartition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using BenchmarkDotNet.Toolchains;
using BenchmarkDotNet.Toolchains.CsProj;
using BenchmarkDotNet.Toolchains.DotNetCli;
using BenchmarkDotNet.Toolchains.MonoWasm;
using BenchmarkDotNet.Toolchains.Roslyn;
using JetBrains.Annotations;

Expand Down Expand Up @@ -57,9 +56,6 @@ public BuildPartition(BenchmarkBuildInfo[] benchmarks, IResolver resolver)

public bool IsNativeAot => RepresentativeBenchmarkCase.Job.IsNativeAOT();

public bool IsWasm => Runtime is WasmRuntime // given job can have Wasm toolchain set, but Runtime == default ;)
|| (RepresentativeBenchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain) && toolchain is WasmToolchain);

public bool IsNetFramework => Runtime is ClrRuntime
|| (RepresentativeBenchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain) && (toolchain is RoslynToolchain || toolchain is CsProjClassicNetToolchain));

Expand Down
17 changes: 4 additions & 13 deletions src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ namespace BenchmarkDotNet.Autogenerated

private static global::System.Int32 AfterAssemblyLoadingAttached(global::System.String[] args)
{
global::BenchmarkDotNet.Engines.IHost host; // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
global::BenchmarkDotNet.Engines.IHost host; // this variable name is used by CodeGenerator.GetBenchmarkRunCall, do NOT change it
if (global::BenchmarkDotNet.Engines.AnonymousPipesHost.TryGetFileHandles(args, out global::System.String writeHandle, out global::System.String readHandle))
host = new global::BenchmarkDotNet.Engines.AnonymousPipesHost(writeHandle, readHandle);
else
Expand All @@ -49,27 +49,18 @@ namespace BenchmarkDotNet.Autogenerated

try
{
// we are not using Runnable here in any direct way in order to avoid strong dependency Main<=>Runnable
// which could cause the jitting/assembly loading to happen before we do anything
// we have some jitting diagnosers and we want them to catch all the informations!!

// this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
// These variable names are used by CodeGenerator.GetBenchmarkRunCall, do NOT change them!
global::System.String benchmarkName = global::System.Linq.Enumerable.FirstOrDefault(global::System.Linq.Enumerable.Skip(global::System.Linq.Enumerable.SkipWhile(args, arg => arg != "--benchmarkName"), 1)) ?? "not provided";
global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode = (global::BenchmarkDotNet.Diagnosers.RunMode) global::System.Int32.Parse(global::System.Linq.Enumerable.FirstOrDefault(global::System.Linq.Enumerable.Skip(global::System.Linq.Enumerable.SkipWhile(args, arg => arg != "--diagnoserRunMode"), 1)) ?? "0");
global::System.Int32 id = args.Length > 0
? global::System.Int32.Parse(args[args.Length - 1]) // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
? global::System.Int32.Parse(args[args.Length - 1])
: 0; // used when re-using generated exe without BDN (typically to repro a bug)

if (args.Length == 0)
{
host.WriteLine("You have not specified benchmark id (an integer) so the first benchmark will be executed.");
}
#if NATIVEAOT
$NativeAotSwitch$
#else
global::System.Type type = typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly.GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}");
type.GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static).Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode });
#endif
$BenchmarkRunCall$
return 0;
}
catch (global::System.Exception oom) when (oom is global::System.OutOfMemoryException || oom is global::System.Reflection.TargetInvocationException reflection && reflection.InnerException is global::System.OutOfMemoryException)
Expand Down
1 change: 0 additions & 1 deletion src/BenchmarkDotNet/Templates/WasmLinkerDescription.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
<assembly fullname="System.Private.CoreLib">
<type fullname="System.IntPtr" />
</assembly>
<assembly fullname="$PROGRAMNAME$" preserve="all" />
</linker>
6 changes: 4 additions & 2 deletions src/BenchmarkDotNet/Toolchains/GeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Portability;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Toolchains.Results;
using JetBrains.Annotations;
Expand All @@ -16,6 +15,9 @@ namespace BenchmarkDotNet.Toolchains
[PublicAPI]
public abstract class GeneratorBase : IGenerator
{
/// <inheritdoc cref="CodeGenBenchmarkRunCallType"/>
public CodeGenBenchmarkRunCallType BenchmarkRunCallType { get; init; }

public GenerateResult GenerateProject(BuildPartition buildPartition, ILogger logger, string rootArtifactsFolderPath)
{
var artifactsPaths = ArtifactsPaths.Empty;
Expand Down Expand Up @@ -119,7 +121,7 @@ [PublicAPI] protected virtual void GenerateAppConfig(BuildPartition buildPartiti
/// <remarks>You most probably do NOT need to override this method!!</remarks>
/// </summary>
[PublicAPI] protected virtual void GenerateCode(BuildPartition buildPartition, ArtifactsPaths artifactsPaths)
=> File.WriteAllText(artifactsPaths.ProgramCodePath, CodeGenerator.Generate(buildPartition));
=> File.WriteAllText(artifactsPaths.ProgramCodePath, CodeGenerator.Generate(buildPartition, BenchmarkRunCallType));

protected virtual string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, $"{programName}{GetExecutableExtension()}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public MonoAotLLVMGenerator(string targetFrameworkMoniker, string cliPath, strin
CustomRuntimePack = customRuntimePack;
AotCompilerPath = aotCompilerPath;
AotCompilerMode = aotCompilerMode;
BenchmarkRunCallType = Code.CodeGenBenchmarkRunCallType.Direct;
}

protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger)
Expand Down
20 changes: 5 additions & 15 deletions src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@ namespace BenchmarkDotNet.Toolchains.MonoWasm
public class WasmGenerator : CsProjGenerator
{
private readonly string CustomRuntimePack;
private readonly bool Aot;
private readonly string MainJS;

public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packagesPath, string customRuntimePack, bool aot)
: base(targetFrameworkMoniker, cliPath, packagesPath, runtimeFrameworkVersion: null)
{
Aot = aot;
CustomRuntimePack = customRuntimePack;
MainJS = (targetFrameworkMoniker == "net5.0" || targetFrameworkMoniker == "net6.0") ? "main.js" : "test-main.js";
BenchmarkRunCallType = aot ? Code.CodeGenBenchmarkRunCallType.Direct : Code.CodeGenBenchmarkRunCallType.Reflection;
}

protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger)
{
if (((WasmRuntime)buildPartition.Runtime).Aot)
{
GenerateProjectFile(buildPartition, artifactsPaths, aot: true, logger);
GenerateLinkerDescriptionFile(artifactsPaths);
}
else

var linkDescriptionFileName = "WasmLinkerDescription.xml";
File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath), linkDescriptionFileName), ResourceHelper.LoadTemplate(linkDescriptionFileName));
} else
{
GenerateProjectFile(buildPartition, artifactsPaths, aot: false, logger);
}
Expand Down Expand Up @@ -66,16 +66,6 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths
GatherReferences(buildPartition, artifactsPaths, logger);
}

protected void GenerateLinkerDescriptionFile(ArtifactsPaths artifactsPaths)
{
const string linkDescriptionFileName = "WasmLinkerDescription.xml";

var content = ResourceHelper.LoadTemplate(linkDescriptionFileName)
.Replace("$PROGRAMNAME$", artifactsPaths.ProgramName);

File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath)!, linkDescriptionFileName), content);
}

protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "AppBundle", MainJS);

protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration)
Expand Down
1 change: 1 addition & 0 deletions src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ internal Generator(string ilCompilerVersion,
this.ilcGenerateStackTraceData = ilcGenerateStackTraceData;
this.ilcOptimizationPreference = ilcOptimizationPreference;
this.ilcInstructionSet = ilcInstructionSet;
BenchmarkRunCallType = Code.CodeGenBenchmarkRunCallType.Direct;
}

internal readonly IReadOnlyDictionary<string, string> Feeds;
Expand Down
21 changes: 13 additions & 8 deletions tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using BenchmarkDotNet.Tests.Loggers;
using BenchmarkDotNet.Tests.XUnit;
using BenchmarkDotNet.Toolchains.DotNetCli;
using BenchmarkDotNet.Toolchains.MonoAotLLVM;
using BenchmarkDotNet.Toolchains.MonoWasm;
using Xunit;
using Xunit.Abstractions;
Expand All @@ -27,11 +28,11 @@ namespace BenchmarkDotNet.IntegrationTests
/// </summary>
public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output)
{
private ManualConfig GetConfig()
private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode)
{
var dotnetVersion = "net8.0";
var logger = new OutputLogger(Output);
var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm");
var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm", aotCompilerMode: aotCompilerMode);
var mainJsPath = Path.Combine(AppContext.BaseDirectory, "AppBundle", "test-main.js");

return ManualConfig.CreateEmpty()
Expand All @@ -44,20 +45,24 @@ private ManualConfig GetConfig()
.WithOption(ConfigOptions.GenerateMSBuildBinLog, true);
}

[FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
public void WasmIsSupported()
[TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
[InlineData(MonoAotCompilerMode.mini)]
[InlineData(MonoAotCompilerMode.wasm)]
public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode)
{
// Test fails on Linux non-x64.
if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64)
{
return;
}

CanExecute<WasmBenchmark>(GetConfig());
CanExecute<WasmBenchmark>(GetConfig(aotCompilerMode));
}

[FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
public void WasmSupportsInProcessDiagnosers()
[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)
Expand All @@ -68,7 +73,7 @@ public void WasmSupportsInProcessDiagnosers()
try
{
var diagnoser = new MockInProcessDiagnoser1(BenchmarkDotNet.Diagnosers.RunMode.NoOverhead);
var config = GetConfig().AddDiagnoser(diagnoser);
var config = GetConfig(aotCompilerMode).AddDiagnoser(diagnoser);

CanExecute<WasmBenchmark>(config);

Expand Down
18 changes: 12 additions & 6 deletions tests/BenchmarkDotNet.Tests/CodeGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ namespace BenchmarkDotNet.Tests
{
public class CodeGeneratorTests
{
[Fact]
public void AsyncVoidIsNotSupported()
public static TheoryData<CodeGenBenchmarkRunCallType> CodeGenBenchmarkRunCallTypes => [.. (CodeGenBenchmarkRunCallType[]) Enum.GetValues(typeof(CodeGenBenchmarkRunCallType))];

[Theory]
[MemberData(nameof(CodeGenBenchmarkRunCallTypes))]
public void AsyncVoidIsNotSupported(CodeGenBenchmarkRunCallType benchmarkRunCallType)
{
var asyncVoidMethod =
typeof(CodeGeneratorTests)
Expand All @@ -28,12 +31,14 @@ public void AsyncVoidIsNotSupported()

Assert.Throws<NotSupportedException>(() => CodeGenerator.Generate(new BuildPartition(
[new BenchmarkBuildInfo(benchmark, ManualConfig.CreateEmpty().CreateImmutableConfig(), 0, new([]))],
BenchmarkRunnerClean.DefaultResolver)
BenchmarkRunnerClean.DefaultResolver),
benchmarkRunCallType
));
}

[Fact]
public void UsingStatementsInTheAutoGeneratedCodeAreProhibited()
[Theory]
[MemberData(nameof(CodeGenBenchmarkRunCallTypes))]
public void UsingStatementsInTheAutoGeneratedCodeAreProhibited(CodeGenBenchmarkRunCallType benchmarkRunCallType)
{
var fineMethod =
typeof(CodeGeneratorTests)
Expand All @@ -46,7 +51,8 @@ public void UsingStatementsInTheAutoGeneratedCodeAreProhibited()

var generatedSourceFile = CodeGenerator.Generate(new BuildPartition(
[new BenchmarkBuildInfo(benchmark, ManualConfig.CreateEmpty().CreateImmutableConfig(), 0, new([]))],
BenchmarkRunnerClean.DefaultResolver)
BenchmarkRunnerClean.DefaultResolver),
benchmarkRunCallType
);

using (StringReader stringReader = new StringReader(generatedSourceFile))
Expand Down