Skip to content

Commit e930ff7

Browse files
committed
Async refactor
1 parent d75bb3b commit e930ff7

70 files changed

Lines changed: 4688 additions & 2030 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build/BenchmarkDotNet.Build/Runners/BuildRunner.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using Cake.Common.Tools.DotNet.Workload.Install;
99
using Cake.Core;
1010
using Cake.Core.IO;
11-
using System;
1211
using System.IO;
1312
using System.Linq;
1413

@@ -51,7 +50,6 @@ public void PackWeaver()
5150
{
5251
MSBuildSettings = context.MsBuildSettingsRestore,
5352
};
54-
MaybeAppendArgument(restoreSettings);
5553
context.DotNetRestore(weaverPath.GetDirectory().FullPath, restoreSettings);
5654

5755
context.Information("BuildSystemProvider: " + context.BuildSystem().Provider);
@@ -63,7 +61,6 @@ public void PackWeaver()
6361
Configuration = context.BuildConfiguration,
6462
Verbosity = context.BuildVerbosity
6563
};
66-
MaybeAppendArgument(buildSettings);
6764
context.DotNetBuild(weaverPath.FullPath, buildSettings);
6865

6966
var packSettings = new DotNetPackSettings
@@ -72,7 +69,6 @@ public void PackWeaver()
7269
MSBuildSettings = context.MsBuildSettingsPack,
7370
Configuration = context.BuildConfiguration
7471
};
75-
MaybeAppendArgument(packSettings);
7672
context.DotNetPack(weaverPath.FullPath, packSettings);
7773
}
7874

build/common.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
<PropertyGroup>
6262
<!-- Increment this when the BenchmarkDotNet.Weaver package needs to be re-packed. -->
63-
<WeaverVersionSuffix>-1</WeaverVersionSuffix>
63+
<WeaverVersionSuffix>-2</WeaverVersionSuffix>
6464
</PropertyGroup>
6565

6666
<ItemGroup>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace BenchmarkDotNet.Attributes;
4+
5+
/// <summary>
6+
/// When applied to an async benchmark method, overrides the return type of the async method that calls the benchmark method.
7+
/// </summary>
8+
[AttributeUsage(AttributeTargets.Method)]
9+
public sealed class AsyncCallerTypeAttribute(Type asyncCallerType) : Attribute
10+
{
11+
/// <summary>
12+
/// The return type of the async method that calls the benchmark method.
13+
/// </summary>
14+
public Type AsyncCallerType { get; private set; } = asyncCallerType;
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace BenchmarkDotNet.Attributes.CompilerServices;
5+
6+
// MethodImplOptions.AggressiveOptimization is applied to all methods to force them to go straight to tier1 JIT,
7+
// eliminating tiered JIT as a potential variable in measurements.
8+
// This is necessary because C# does not support any way to apply attributes to compiler-generated state machine methods.
9+
// This is applied both to the core Engine and auto-generated classes.
10+
#pragma warning disable CS1574
11+
/// <summary>
12+
/// Instructs the BenchmarkDotNet assembly weaver to apply <see cref="MethodImplOptions.AggressiveOptimization"/> to all declared
13+
/// methods in the annotated type and nested types that are not already annotated with <see cref="MethodImplOptions.NoOptimization"/>.
14+
/// </summary>
15+
#pragma warning restore CS1574
16+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
17+
public sealed class AggressivelyOptimizeMethodsAttribute : Attribute
18+
{
19+
}

src/BenchmarkDotNet.Weaver/buildTransitive/netstandard2.0/BenchmarkDotNet.Weaver.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
Inputs="$(BenchmarkDotNetWeaveAssemblyPath)"
1818
Outputs="$(BenchmarkDotNetWeaveAssembliesStampFile)">
1919

20-
<WeaveAssemblyTask TargetAssembly="$(BenchmarkDotNetWeaveAssemblyPath)" />
20+
<WeaveAssemblyTask TargetAssembly="$(BenchmarkDotNetWeaveAssemblyPath)" TreatWarningsAsErrors="$(TreatWarningsAsErrors)" />
2121

2222
<!-- Create stamp file for incrementality -->
2323
<Touch Files="$(BenchmarkDotNetWeaveAssembliesStampFile)" AlwaysCreate="true" />
Binary file not shown.
Binary file not shown.

src/BenchmarkDotNet.Weaver/src/WeaveAssemblyTask.cs

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,76 @@ public sealed class WeaveAssemblyTask : Task
2424
[Required]
2525
public string TargetAssembly { get; set; }
2626

27+
/// <summary>
28+
/// Whether to treat warnings as errors.
29+
/// </summary>
30+
public bool TreatWarningsAsErrors { get; set; }
31+
2732
/// <summary>
2833
/// Runs the weave assembly task.
2934
/// </summary>
3035
/// <returns><see langword="true"/> if successful; <see langword="false"/> otherwise.</returns>
3136
public override bool Execute()
32-
{
37+
{
3338
if (!File.Exists(TargetAssembly))
3439
{
3540
Log.LogError($"Assembly not found: {TargetAssembly}");
3641
return false;
3742
}
3843

39-
4044
bool benchmarkMethodsImplAdjusted = false;
4145
try
4246
{
4347
var module = ModuleDefinition.FromFile(TargetAssembly);
4448

49+
bool anyAdjustments = false;
4550
foreach (var type in module.GetAllTypes())
4651
{
47-
// We can skip non-public types as they are not valid for benchmarks.
48-
if (type.IsNotPublic)
52+
if (type.CustomAttributes.Any(attr => attr.Constructor.DeclaringType.FullName == "BenchmarkDotNet.Attributes.CompilerServices.AggressivelyOptimizeMethodsAttribute"))
4953
{
50-
continue;
54+
ApplyAggressiveOptimizationToMethods(type);
55+
56+
void ApplyAggressiveOptimizationToMethods(TypeDefinition type)
57+
{
58+
// Apply AggressiveOptimization to all methods in the type and nested types that
59+
// aren't annotated with NoOptimization (this includes compiler-generated state machines).
60+
foreach (var method in type.Methods)
61+
{
62+
if ((method.ImplAttributes & MethodImplAttributes.NoOptimization) == 0)
63+
{
64+
var oldImpl = method.ImplAttributes;
65+
method.ImplAttributes |= MethodImplAttributes.AggressiveOptimization;
66+
anyAdjustments |= (oldImpl & MethodImplAttributes.AggressiveOptimization) == 0;
67+
}
68+
}
69+
70+
// Recurse into nested types
71+
foreach (var nested in type.NestedTypes)
72+
{
73+
ApplyAggressiveOptimizationToMethods(nested);
74+
}
75+
}
5176
}
5277

53-
foreach (var method in type.Methods)
78+
// We can skip non-public types as they are not valid for benchmarks.
79+
// !type.IsNotPublic handles nested types, while type.IsPublic does not.
80+
if (!type.IsNotPublic)
5481
{
55-
if (method.CustomAttributes.Any(IsBenchmarkAttribute))
82+
foreach (var method in type.Methods)
5683
{
57-
var oldImpl = method.ImplAttributes;
58-
// Remove AggressiveInlining and add NoInlining.
59-
method.ImplAttributes = (oldImpl & ~MethodImplAttributes.AggressiveInlining) | MethodImplAttributes.NoInlining;
60-
benchmarkMethodsImplAdjusted |= (oldImpl & MethodImplAttributes.NoInlining) == 0;
84+
if (method.CustomAttributes.Any(IsBenchmarkAttribute))
85+
{
86+
var oldImpl = method.ImplAttributes;
87+
// Remove AggressiveInlining and add NoInlining.
88+
method.ImplAttributes = (oldImpl & ~MethodImplAttributes.AggressiveInlining) | MethodImplAttributes.NoInlining;
89+
benchmarkMethodsImplAdjusted |= (oldImpl & MethodImplAttributes.NoInlining) == 0;
90+
anyAdjustments |= benchmarkMethodsImplAdjusted;
91+
}
6192
}
6293
}
6394
}
6495

65-
if (benchmarkMethodsImplAdjusted)
96+
if (anyAdjustments)
6697
{
6798
// Write to a memory stream before overwriting the original file in case an exception occurs during the write (like unsupported platform).
6899
// https://github.com/Washi1337/AsmResolver/issues/640
@@ -90,9 +121,17 @@ public override bool Execute()
90121
}
91122
catch (Exception e)
92123
{
93-
Log.LogWarning($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}. Error:{Environment.NewLine}{e}");
124+
if (TreatWarningsAsErrors)
125+
{
126+
Log.LogError($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}.");
127+
Log.LogErrorFromException(e, true, true, null);
128+
}
129+
else
130+
{
131+
Log.LogWarning($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}. Error:{Environment.NewLine}{e}");
132+
}
94133
}
95-
return true;
134+
return !Log.HasLoggedErrors;
96135
}
97136

98137
private static bool IsBenchmarkAttribute(CustomAttribute attribute)

src/BenchmarkDotNet/BenchmarkDotNet.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<Import Project="..\..\build\common.props" />
3+
34
<PropertyGroup>
45
<AssemblyTitle>BenchmarkDotNet</AssemblyTitle>
56
<TargetFrameworks>netstandard2.0;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
@@ -49,5 +50,10 @@
4950
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
5051
</PackageReference>
5152
</ItemGroup>
53+
<!-- The transitive weaver reference is stripped during full pack, so we need to reference it directly. -->
54+
<ItemGroup Condition="'$(IsFullPack)' == 'true'">
55+
<PackageReference Include="BenchmarkDotNet.Weaver" Version="$(Version)$(WeaverVersionSuffix)" PrivateAssets="all" />
56+
</ItemGroup>
57+
5258
<Import Project="..\..\build\common.targets" />
5359
</Project>

src/BenchmarkDotNet/Code/CodeGenerator.cs

Lines changed: 25 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Reflection;
66
using System.Runtime.CompilerServices;
77
using System.Text;
8-
using System.Threading.Tasks;
98
using BenchmarkDotNet.Characteristics;
109
using BenchmarkDotNet.Diagnosers;
1110
using BenchmarkDotNet.Disassemblers;
@@ -33,29 +32,17 @@ internal static string Generate(BuildPartition buildPartition)
3332
{
3433
var benchmark = buildInfo.BenchmarkCase;
3534

36-
var provider = GetDeclarationsProvider(benchmark.Descriptor);
37-
38-
string passArguments = GetPassArguments(benchmark);
39-
40-
string benchmarkTypeCode = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkType.txt"))
35+
string benchmarkTypeCode = GetDeclarationsProvider(benchmark)
36+
.ReplaceTemplate(new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkType.txt")))
4137
.Replace("$ID$", buildInfo.Id.ToString())
42-
.Replace("$OperationsPerInvoke$", provider.OperationsPerInvoke)
43-
.Replace("$WorkloadTypeName$", provider.WorkloadTypeName)
44-
.Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName)
45-
.Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName)
46-
.Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName)
47-
.Replace("$IterationCleanupMethodName$", provider.IterationCleanupMethodName)
4838
.Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark))
4939
.Replace("$ParamsContent$", GetParamsContent(benchmark))
5040
.Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark))
5141
.Replace("$DeclareArgumentFields$", GetDeclareArgumentFields(benchmark))
5242
.Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark))
53-
.Replace("$LoadArguments$", GetLoadArguments(benchmark))
54-
.Replace("$PassArguments$", passArguments)
5543
.Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark))
5644
.Replace("$RunExtraIteration$", buildInfo.Config.HasExtraIterationDiagnoser(benchmark) ? "true" : "false")
5745
.Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName)
58-
.Replace("$WorkloadMethodCall$", provider.GetWorkloadMethodCall(passArguments))
5946
.Replace("$InProcessDiagnoserRouters$", GetInProcessDiagnoserRouters(buildInfo))
6047
.ToString();
6148

@@ -132,27 +119,21 @@ private static string GetJobsSetDefinition(BenchmarkCase benchmarkCase)
132119
Replace("; ", ";\n ");
133120
}
134121

135-
private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor)
122+
private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchmark)
136123
{
137-
var method = descriptor.WorkloadMethod;
124+
var method = benchmark.Descriptor.WorkloadMethod;
138125

139-
if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask))
140-
{
141-
return new AsyncDeclarationsProvider(descriptor);
142-
}
143-
if (method.ReturnType.GetTypeInfo().IsGenericType
144-
&& (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
145-
|| method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>)))
126+
if (method.ReturnType.IsAwaitable())
146127
{
147-
return new AsyncDeclarationsProvider(descriptor);
128+
return new AsyncDeclarationsProvider(benchmark);
148129
}
149130

150131
if (method.ReturnType == typeof(void) && method.HasAttribute<AsyncStateMachineAttribute>())
151132
{
152133
throw new NotSupportedException("async void is not supported by design");
153134
}
154135

155-
return new SyncDeclarationsProvider(descriptor);
136+
return new SyncDeclarationsProvider(benchmark);
156137
}
157138

158139
// internal for tests
@@ -168,31 +149,19 @@ private static string GetArgumentsDefinition(BenchmarkCase benchmarkCase)
168149
=> string.Join(
169150
", ",
170151
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
171-
.Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}"));
152+
.Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}"));
172153

173154
private static string GetDeclareArgumentFields(BenchmarkCase benchmarkCase)
174155
=> string.Join(
175156
Environment.NewLine,
176157
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
177-
.Select((parameter, index) => $"private {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name)).GetCorrectCSharpTypeName()} __argField{index};"));
158+
.Select((parameter, index) => $"public {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name)).GetCorrectCSharpTypeName()} __argField{index};"));
178159

179160
private static string GetInitializeArgumentFields(BenchmarkCase benchmarkCase)
180161
=> string.Join(
181162
Environment.NewLine,
182163
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
183-
.Select((parameter, index) => $"this.__argField{index} = {benchmarkCase.Parameters.GetArgument(parameter.Name).ToSourceCode()};")); // we init the fields in ctor to provoke all possible allocations and overhead of other type
184-
185-
private static string GetLoadArguments(BenchmarkCase benchmarkCase)
186-
=> string.Join(
187-
Environment.NewLine,
188-
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
189-
.Select((parameter, index) => $"{(parameter.ParameterType.IsByRef ? "ref" : string.Empty)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {(parameter.ParameterType.IsByRef ? "ref" : string.Empty)} this.__argField{index};"));
190-
191-
private static string GetPassArguments(BenchmarkCase benchmarkCase)
192-
=> string.Join(
193-
", ",
194-
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
195-
.Select((parameter, index) => $"{GetParameterModifier(parameter)} arg{index}"));
164+
.Select((parameter, index) => $"this.__fieldsContainer.__argField{index} = {benchmarkCase.Parameters.GetArgument(parameter.Name).ToSourceCode()};")); // we init the fields in ctor to provoke all possible allocations and overhead of other type
196165

197166
private static string GetExtraAttributes(Descriptor descriptor)
198167
=> descriptor.WorkloadMethod.GetCustomAttributes(false).OfType<STAThreadAttribute>().Any() ? "[System.STAThreadAttribute]" : string.Empty;
@@ -235,7 +204,7 @@ private static string GetInProcessDiagnoserRouters(BenchmarkBuildInfo buildInfo)
235204
}
236205
}
237206

238-
private static string GetParameterModifier(ParameterInfo parameterInfo)
207+
internal static string GetParameterModifier(ParameterInfo parameterInfo)
239208
{
240209
if (!parameterInfo.ParameterType.IsByRef)
241210
return string.Empty;
@@ -263,7 +232,7 @@ private static string GetNativeAotSwitch(BuildPartition buildPartition)
263232
@switch.AppendLine("switch (id) {");
264233

265234
foreach (var buildInfo in buildPartition.Benchmarks)
266-
@switch.AppendLine($"case {buildInfo.Id.Value}: BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;");
235+
@switch.AppendLine($"case {buildInfo.Id.Value}: runTask = BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;");
267236

268237
@switch.AppendLine("default: throw new System.NotSupportedException(\"invalid benchmark id\");");
269238
@switch.AppendLine("}");
@@ -279,28 +248,21 @@ private static Type GetFieldType(Type argumentType, ParameterInstance argument)
279248

280249
return argumentType;
281250
}
251+
}
282252

283-
private class SmartStringBuilder
284-
{
285-
private readonly string originalText;
286-
private readonly StringBuilder builder;
287-
288-
public SmartStringBuilder(string text)
289-
{
290-
originalText = text;
291-
builder = new StringBuilder(text);
292-
}
293-
294-
public SmartStringBuilder Replace(string oldValue, string? newValue)
295-
{
296-
if (originalText.Contains(oldValue))
297-
builder.Replace(oldValue, newValue);
298-
else
299-
builder.Append($"\n// '{oldValue}' not found");
300-
return this;
301-
}
253+
internal class SmartStringBuilder(string text)
254+
{
255+
private readonly StringBuilder builder = new(text);
302256

303-
public override string ToString() => builder.ToString();
257+
public SmartStringBuilder Replace(string oldValue, string? newValue)
258+
{
259+
if (text.Contains(oldValue))
260+
builder.Replace(oldValue, newValue);
261+
else
262+
builder.Append($"\n// '{oldValue}' not found");
263+
return this;
304264
}
265+
266+
public override string ToString() => builder.ToString();
305267
}
306268
}

0 commit comments

Comments
 (0)