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
49 changes: 34 additions & 15 deletions src/BenchmarkDotNet/Code/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ internal static string Generate(BuildPartition buildPartition)

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 @@ -62,11 +64,19 @@ 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$", buildPartition.IsNetFramework ? "#define NETFRAMEWORK" : string.Empty)
.Replace("$ExtraDefines$", string.Join(Environment.NewLine, extraDefines))
.Replace("$DerivedTypes$", string.Join(Environment.NewLine, benchmarksCode))
.Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition))
.Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition.RepresentativeBenchmarkCase.Descriptor))
.Replace("$NativeAotSwitch$", GetNativeAotSwitch(buildPartition))
.ToString();

return benchmarkProgramContent;
Expand Down Expand Up @@ -184,19 +194,8 @@ private static string GetPassArguments(BenchmarkCase benchmarkCase)
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
.Select((parameter, index) => $"{GetParameterModifier(parameter)} arg{index}"));

private static string GetExtraAttributes(BuildPartition buildPartition)
{
var attrs = new List<string>(buildPartition.Benchmarks.Length + 1);
if (buildPartition.RepresentativeBenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes(false).OfType<STAThreadAttribute>().Any())
{
attrs.Add("[global::System.STAThread]");
}
foreach (var buildInfo in buildPartition.Benchmarks)
{
attrs.Add($"[global::System.Diagnostics.CodeAnalysis.DynamicDependency(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods, typeof(global::BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}))]");
}
return string.Join(Environment.NewLine, attrs);
}
private static string GetExtraAttributes(Descriptor descriptor)
=> descriptor.WorkloadMethod.GetCustomAttributes(false).OfType<STAThreadAttribute>().Any() ? "[System.STAThreadAttribute]" : string.Empty;

private static string GetEngineFactoryTypeName(BenchmarkCase benchmarkCase)
{
Expand Down Expand Up @@ -252,6 +251,26 @@ 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)
{
if (!buildPartition.IsNativeAot)
return default;

var @switch = new StringBuilder(buildPartition.Benchmarks.Length * 30);
@switch.AppendLine("switch (id) {");

foreach (var buildInfo in buildPartition.Benchmarks)
@switch.AppendLine($"case {buildInfo.Id.Value}: BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;");

@switch.AppendLine("default: throw new System.NotSupportedException(\"invalid benchmark id\");");
@switch.AppendLine("}");

return @switch.ToString();
}

private static Type GetFieldType(Type argumentType, ParameterInstance argument)
{
// #774 we can't store Span in a field, so we store an array (which is later casted to Span when we load the arguments)
Expand Down
4 changes: 4 additions & 0 deletions src/BenchmarkDotNet/Running/BuildPartition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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 @@ -56,6 +57,9 @@ 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
78 changes: 11 additions & 67 deletions src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,81 +4,20 @@ $ExtraDefines$
// this file must not be importing any namespaces
// we should use full names everywhere to avoid any potential naming conflicts, example: #1007, #778

#pragma warning disable CS0436 // Type conflicts with imported type
#if !NET7_0_OR_GREATER
namespace System.Diagnostics.CodeAnalysis
{
/// <summary>
/// Specifies that this constructor sets all required members for the current type,
/// and callers do not need to set any required members themselves.
/// </summary>
[global::System.AttributeUsage(global::System.AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal sealed class SetsRequiredMembersAttribute : global::System.Attribute
{
}
}
#endif
#if !NET5_0_OR_GREATER
namespace System.Diagnostics.CodeAnalysis
{
[global::System.Flags]
internal enum DynamicallyAccessedMemberTypes
{
None = 0,
PublicParameterlessConstructor = 0x0001,
PublicConstructors = 0x0002 | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor,
NonPublicConstructors = 0x0004,
PublicMethods = 0x0008,
NonPublicMethods = 0x0010,
PublicFields = 0x0020,
NonPublicFields = 0x0040,
PublicNestedTypes = 0x0080,
NonPublicNestedTypes = 0x0100,
PublicProperties = 0x0200,
NonPublicProperties = 0x0400,
PublicEvents = 0x0800,
NonPublicEvents = 0x1000,
Interfaces = 0x2000,
All = ~global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None
}
[global::System.AttributeUsage(global::System.AttributeTargets.Constructor | global::System.AttributeTargets.Field | global::System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.Conditional("MULTI_TARGETING_SUPPORT_ATTRIBUTES")]
internal sealed class DynamicDependencyAttribute : global::System.Attribute
{
public DynamicDependencyAttribute(string memberSignature)
{
MemberSignature = memberSignature;
}
public DynamicDependencyAttribute(string memberSignature, global::System.Type type)
{
MemberSignature = memberSignature;
Type = type;
}
public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName)
{
MemberSignature = memberSignature;
TypeName = typeName;
AssemblyName = assemblyName;
}
public DynamicDependencyAttribute(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes memberTypes, global::System.Type type)
{
MemberTypes = memberTypes;
Type = type;
}
public DynamicDependencyAttribute(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes memberTypes, string typeName, string assemblyName)
{
MemberTypes = memberTypes;
TypeName = typeName;
AssemblyName = assemblyName;
}
public string MemberSignature { get; }
public global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes MemberTypes { get; }
public global::System.Type Type { get; }
public string TypeName { get; }
public string AssemblyName { get; }
public string Condition { get; set; }
}
}
#endif
#pragma warning restore CS0436 // Type conflicts with imported type

// the namespace name must be in sync with WindowsDisassembler.BuildArguments
namespace BenchmarkDotNet.Autogenerated
Expand All @@ -99,7 +38,7 @@ namespace BenchmarkDotNet.Autogenerated

private static global::System.Int32 AfterAssemblyLoadingAttached(global::System.String[] args)
{
global::BenchmarkDotNet.Engines.IHost host;
global::BenchmarkDotNet.Engines.IHost host; // this variable name is used by CodeGenerator.GetCoreRtSwitch, 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 @@ -115,18 +54,23 @@ namespace BenchmarkDotNet.Autogenerated
// 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
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])
? global::System.Int32.Parse(args[args.Length - 1]) // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
: 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
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: 1 addition & 0 deletions src/BenchmarkDotNet/Templates/WasmLinkerDescription.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<assembly fullname="System.Private.CoreLib">
<type fullname="System.IntPtr" />
</assembly>
<assembly fullname="$PROGRAMNAME$" preserve="all" />
</linker>
17 changes: 13 additions & 4 deletions src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts
if (((WasmRuntime)buildPartition.Runtime).Aot)
{
GenerateProjectFile(buildPartition, artifactsPaths, aot: true, logger);

var linkDescriptionFileName = "WasmLinkerDescription.xml";
File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath), linkDescriptionFileName), ResourceHelper.LoadTemplate(linkDescriptionFileName));
} else
GenerateLinkerDescriptionFile(artifactsPaths);
}
else
{
GenerateProjectFile(buildPartition, artifactsPaths, aot: false, logger);
}
Expand Down Expand Up @@ -67,6 +66,16 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@ public class NaiveRunnableEmitDiff
private static readonly HashSet<string> IgnoredTypeNames = new HashSet<string>()
{
"BenchmarkDotNet.Autogenerated.UniqueProgramName",
// not required to be used in the InProcess toolchains (it's already used in the host process)
"BenchmarkDotNet.Autogenerated.DirtyAssemblyResolveHelper",
// Polyfill types added for older runtimes.
"System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute",
"System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes",
"System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute",
"BenchmarkDotNet.Autogenerated.DirtyAssemblyResolveHelper", // not required to be used in the InProcess toolchains (it's already used in the host process)
"System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", // Conditionally added in runtimes older than .Net 7.
};

private static readonly HashSet<string> IgnoredAttributeTypeNames = new HashSet<string>()
Expand Down
21 changes: 8 additions & 13 deletions tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
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 @@ -28,11 +27,11 @@ namespace BenchmarkDotNet.IntegrationTests
/// </summary>
public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output)
{
private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode)
private ManualConfig GetConfig()
{
var dotnetVersion = "net8.0";
var logger = new OutputLogger(Output);
var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm", aotCompilerMode: aotCompilerMode);
var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm");
var mainJsPath = Path.Combine(AppContext.BaseDirectory, "AppBundle", "test-main.js");

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

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

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

[TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
[InlineData(MonoAotCompilerMode.mini)]
[InlineData(MonoAotCompilerMode.wasm)]
public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode)
[FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
public void WasmSupportsInProcessDiagnosers()
{
// Test fails on Linux non-x64.
if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64)
Expand All @@ -73,7 +68,7 @@ public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode)
try
{
var diagnoser = new MockInProcessDiagnoser1(BenchmarkDotNet.Diagnosers.RunMode.NoOverhead);
var config = GetConfig(aotCompilerMode).AddDiagnoser(diagnoser);
var config = GetConfig().AddDiagnoser(diagnoser);

CanExecute<WasmBenchmark>(config);

Expand Down