Skip to content

Commit 3cebcb9

Browse files
authored
Use DynamicDependency (#2941)
* Use DynamicDependency in generated code. * Add wasm compiler mode tests. * Fix IL emit diff tests.
1 parent 2af26c2 commit 3cebcb9

File tree

7 files changed

+105
-73
lines changed

7 files changed

+105
-73
lines changed

src/BenchmarkDotNet/Code/CodeGenerator.cs

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ internal static string Generate(BuildPartition buildPartition)
2727

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

30-
var extraDefines = new List<string>();
31-
3230
foreach (var buildInfo in buildPartition.Benchmarks)
3331
{
3432
var benchmark = buildInfo.BenchmarkCase;
@@ -64,19 +62,11 @@ internal static string Generate(BuildPartition buildPartition)
6462
benchmarksCode.Add(benchmarkTypeCode);
6563
}
6664

67-
if (buildPartition.IsNativeAot)
68-
extraDefines.Add("#define NATIVEAOT");
69-
else if (buildPartition.IsNetFramework)
70-
extraDefines.Add("#define NETFRAMEWORK");
71-
else if (buildPartition.IsWasm)
72-
extraDefines.Add("#define WASM");
73-
7465
string benchmarkProgramContent = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkProgram.txt"))
7566
.Replace("$ShadowCopyDefines$", useShadowCopy ? "#define SHADOWCOPY" : null).Replace("$ShadowCopyFolderPath$", shadowCopyFolderPath)
76-
.Replace("$ExtraDefines$", string.Join(Environment.NewLine, extraDefines))
67+
.Replace("$ExtraDefines$", buildPartition.IsNetFramework ? "#define NETFRAMEWORK" : string.Empty)
7768
.Replace("$DerivedTypes$", string.Join(Environment.NewLine, benchmarksCode))
78-
.Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition.RepresentativeBenchmarkCase.Descriptor))
79-
.Replace("$NativeAotSwitch$", GetNativeAotSwitch(buildPartition))
69+
.Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition))
8070
.ToString();
8171

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

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

200201
private static string GetEngineFactoryTypeName(BenchmarkCase benchmarkCase)
201202
{
@@ -251,26 +252,6 @@ private static string GetParameterModifier(ParameterInfo parameterInfo)
251252
return "ref";
252253
}
253254

254-
/// <summary>
255-
/// for NativeAOT we can't use reflection to load type and run a method, so we simply generate a switch for all types..
256-
/// </summary>
257-
private static string GetNativeAotSwitch(BuildPartition buildPartition)
258-
{
259-
if (!buildPartition.IsNativeAot)
260-
return default;
261-
262-
var @switch = new StringBuilder(buildPartition.Benchmarks.Length * 30);
263-
@switch.AppendLine("switch (id) {");
264-
265-
foreach (var buildInfo in buildPartition.Benchmarks)
266-
@switch.AppendLine($"case {buildInfo.Id.Value}: BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;");
267-
268-
@switch.AppendLine("default: throw new System.NotSupportedException(\"invalid benchmark id\");");
269-
@switch.AppendLine("}");
270-
271-
return @switch.ToString();
272-
}
273-
274255
private static Type GetFieldType(Type argumentType, ParameterInstance argument)
275256
{
276257
// #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)

src/BenchmarkDotNet/Running/BuildPartition.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using BenchmarkDotNet.Toolchains;
1313
using BenchmarkDotNet.Toolchains.CsProj;
1414
using BenchmarkDotNet.Toolchains.DotNetCli;
15-
using BenchmarkDotNet.Toolchains.MonoWasm;
1615
using BenchmarkDotNet.Toolchains.Roslyn;
1716
using JetBrains.Annotations;
1817

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

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

60-
public bool IsWasm => Runtime is WasmRuntime // given job can have Wasm toolchain set, but Runtime == default ;)
61-
|| (RepresentativeBenchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain) && toolchain is WasmToolchain);
62-
6359
public bool IsNetFramework => Runtime is ClrRuntime
6460
|| (RepresentativeBenchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain) && (toolchain is RoslynToolchain || toolchain is CsProjClassicNetToolchain));
6561

src/BenchmarkDotNet/Templates/BenchmarkProgram.txt

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,81 @@ $ExtraDefines$
44
// this file must not be importing any namespaces
55
// we should use full names everywhere to avoid any potential naming conflicts, example: #1007, #778
66

7+
#pragma warning disable CS0436 // Type conflicts with imported type
78
#if !NET7_0_OR_GREATER
89
namespace System.Diagnostics.CodeAnalysis
910
{
10-
/// <summary>
11-
/// Specifies that this constructor sets all required members for the current type,
12-
/// and callers do not need to set any required members themselves.
13-
/// </summary>
1411
[global::System.AttributeUsage(global::System.AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
1512
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
1613
internal sealed class SetsRequiredMembersAttribute : global::System.Attribute
1714
{
1815
}
1916
}
2017
#endif
18+
#if !NET5_0_OR_GREATER
19+
namespace System.Diagnostics.CodeAnalysis
20+
{
21+
[global::System.Flags]
22+
internal enum DynamicallyAccessedMemberTypes
23+
{
24+
None = 0,
25+
PublicParameterlessConstructor = 0x0001,
26+
PublicConstructors = 0x0002 | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor,
27+
NonPublicConstructors = 0x0004,
28+
PublicMethods = 0x0008,
29+
NonPublicMethods = 0x0010,
30+
PublicFields = 0x0020,
31+
NonPublicFields = 0x0040,
32+
PublicNestedTypes = 0x0080,
33+
NonPublicNestedTypes = 0x0100,
34+
PublicProperties = 0x0200,
35+
NonPublicProperties = 0x0400,
36+
PublicEvents = 0x0800,
37+
NonPublicEvents = 0x1000,
38+
Interfaces = 0x2000,
39+
All = ~global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None
40+
}
41+
[global::System.AttributeUsage(global::System.AttributeTargets.Constructor | global::System.AttributeTargets.Field | global::System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
42+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
43+
[global::System.Diagnostics.Conditional("MULTI_TARGETING_SUPPORT_ATTRIBUTES")]
44+
internal sealed class DynamicDependencyAttribute : global::System.Attribute
45+
{
46+
public DynamicDependencyAttribute(string memberSignature)
47+
{
48+
MemberSignature = memberSignature;
49+
}
50+
public DynamicDependencyAttribute(string memberSignature, global::System.Type type)
51+
{
52+
MemberSignature = memberSignature;
53+
Type = type;
54+
}
55+
public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName)
56+
{
57+
MemberSignature = memberSignature;
58+
TypeName = typeName;
59+
AssemblyName = assemblyName;
60+
}
61+
public DynamicDependencyAttribute(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes memberTypes, global::System.Type type)
62+
{
63+
MemberTypes = memberTypes;
64+
Type = type;
65+
}
66+
public DynamicDependencyAttribute(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes memberTypes, string typeName, string assemblyName)
67+
{
68+
MemberTypes = memberTypes;
69+
TypeName = typeName;
70+
AssemblyName = assemblyName;
71+
}
72+
public string MemberSignature { get; }
73+
public global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes MemberTypes { get; }
74+
public global::System.Type Type { get; }
75+
public string TypeName { get; }
76+
public string AssemblyName { get; }
77+
public string Condition { get; set; }
78+
}
79+
}
80+
#endif
81+
#pragma warning restore CS0436 // Type conflicts with imported type
2182

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

39100
private static global::System.Int32 AfterAssemblyLoadingAttached(global::System.String[] args)
40101
{
41-
global::BenchmarkDotNet.Engines.IHost host; // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
102+
global::BenchmarkDotNet.Engines.IHost host;
42103
if (global::BenchmarkDotNet.Engines.AnonymousPipesHost.TryGetFileHandles(args, out global::System.String writeHandle, out global::System.String readHandle))
43104
host = new global::BenchmarkDotNet.Engines.AnonymousPipesHost(writeHandle, readHandle);
44105
else
@@ -54,23 +115,18 @@ namespace BenchmarkDotNet.Autogenerated
54115
// which could cause the jitting/assembly loading to happen before we do anything
55116
// we have some jitting diagnosers and we want them to catch all the informations!!
56117

57-
// this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
58118
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";
59119
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");
60120
global::System.Int32 id = args.Length > 0
61-
? global::System.Int32.Parse(args[args.Length - 1]) // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
121+
? global::System.Int32.Parse(args[args.Length - 1])
62122
: 0; // used when re-using generated exe without BDN (typically to repro a bug)
63123

64124
if (args.Length == 0)
65125
{
66126
host.WriteLine("You have not specified benchmark id (an integer) so the first benchmark will be executed.");
67127
}
68-
#if NATIVEAOT
69-
$NativeAotSwitch$
70-
#else
71128
global::System.Type type = typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly.GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}");
72129
type.GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static).Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode });
73-
#endif
74130
return 0;
75131
}
76132
catch (global::System.Exception oom) when (oom is global::System.OutOfMemoryException || oom is global::System.Reflection.TargetInvocationException reflection && reflection.InnerException is global::System.OutOfMemoryException)

src/BenchmarkDotNet/Templates/WasmLinkerDescription.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@
22
<assembly fullname="System.Private.CoreLib">
33
<type fullname="System.IntPtr" />
44
</assembly>
5-
<assembly fullname="$PROGRAMNAME$" preserve="all" />
65
</linker>

src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts
2929
if (((WasmRuntime)buildPartition.Runtime).Aot)
3030
{
3131
GenerateProjectFile(buildPartition, artifactsPaths, aot: true, logger);
32-
GenerateLinkerDescriptionFile(artifactsPaths);
33-
}
34-
else
32+
33+
var linkDescriptionFileName = "WasmLinkerDescription.xml";
34+
File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath), linkDescriptionFileName), ResourceHelper.LoadTemplate(linkDescriptionFileName));
35+
} else
3536
{
3637
GenerateProjectFile(buildPartition, artifactsPaths, aot: false, logger);
3738
}
@@ -66,16 +67,6 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths
6667
GatherReferences(buildPartition, artifactsPaths, logger);
6768
}
6869

69-
protected void GenerateLinkerDescriptionFile(ArtifactsPaths artifactsPaths)
70-
{
71-
const string linkDescriptionFileName = "WasmLinkerDescription.xml";
72-
73-
var content = ResourceHelper.LoadTemplate(linkDescriptionFileName)
74-
.Replace("$PROGRAMNAME$", artifactsPaths.ProgramName);
75-
76-
File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath)!, linkDescriptionFileName), content);
77-
}
78-
7970
protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "AppBundle", MainJS);
8071

8172
protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration)

tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ public class NaiveRunnableEmitDiff
1313
private static readonly HashSet<string> IgnoredTypeNames = new HashSet<string>()
1414
{
1515
"BenchmarkDotNet.Autogenerated.UniqueProgramName",
16-
"BenchmarkDotNet.Autogenerated.DirtyAssemblyResolveHelper", // not required to be used in the InProcess toolchains (it's already used in the host process)
17-
"System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", // Conditionally added in runtimes older than .Net 7.
16+
// not required to be used in the InProcess toolchains (it's already used in the host process)
17+
"BenchmarkDotNet.Autogenerated.DirtyAssemblyResolveHelper",
18+
// Polyfill types added for older runtimes.
19+
"System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute",
20+
"System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes",
21+
"System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute",
1822
};
1923

2024
private static readonly HashSet<string> IgnoredAttributeTypeNames = new HashSet<string>()

tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using BenchmarkDotNet.Tests.Loggers;
1212
using BenchmarkDotNet.Tests.XUnit;
1313
using BenchmarkDotNet.Toolchains.DotNetCli;
14+
using BenchmarkDotNet.Toolchains.MonoAotLLVM;
1415
using BenchmarkDotNet.Toolchains.MonoWasm;
1516
using Xunit;
1617
using Xunit.Abstractions;
@@ -27,11 +28,11 @@ namespace BenchmarkDotNet.IntegrationTests
2728
/// </summary>
2829
public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output)
2930
{
30-
private ManualConfig GetConfig()
31+
private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode)
3132
{
3233
var dotnetVersion = "net8.0";
3334
var logger = new OutputLogger(Output);
34-
var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm");
35+
var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm", aotCompilerMode: aotCompilerMode);
3536
var mainJsPath = Path.Combine(AppContext.BaseDirectory, "AppBundle", "test-main.js");
3637

3738
return ManualConfig.CreateEmpty()
@@ -44,20 +45,24 @@ private ManualConfig GetConfig()
4445
.WithOption(ConfigOptions.GenerateMSBuildBinLog, true);
4546
}
4647

47-
[FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
48-
public void WasmIsSupported()
48+
[TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
49+
[InlineData(MonoAotCompilerMode.mini)]
50+
[InlineData(MonoAotCompilerMode.wasm)]
51+
public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode)
4952
{
5053
// Test fails on Linux non-x64.
5154
if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64)
5255
{
5356
return;
5457
}
5558

56-
CanExecute<WasmBenchmark>(GetConfig());
59+
CanExecute<WasmBenchmark>(GetConfig(aotCompilerMode));
5760
}
5861

59-
[FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
60-
public void WasmSupportsInProcessDiagnosers()
62+
[TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
63+
[InlineData(MonoAotCompilerMode.mini)]
64+
[InlineData(MonoAotCompilerMode.wasm)]
65+
public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode)
6166
{
6267
// Test fails on Linux non-x64.
6368
if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64)
@@ -68,7 +73,7 @@ public void WasmSupportsInProcessDiagnosers()
6873
try
6974
{
7075
var diagnoser = new MockInProcessDiagnoser1(BenchmarkDotNet.Diagnosers.RunMode.NoOverhead);
71-
var config = GetConfig().AddDiagnoser(diagnoser);
76+
var config = GetConfig(aotCompilerMode).AddDiagnoser(diagnoser);
7277

7378
CanExecute<WasmBenchmark>(config);
7479

0 commit comments

Comments
 (0)