Skip to content

Commit 2ed1738

Browse files
committed
Async refactor WIP
1 parent cb5d392 commit 2ed1738

Some content is hidden

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

47 files changed

+1690
-887
lines changed

BenchmarkDotNet.slnx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Folder Name="/samples/">
77
<File Path="samples/Directory.Build.props" />
88
<Project Path="samples/BenchmarkDotNet.Samples.FSharp/BenchmarkDotNet.Samples.FSharp.fsproj" />
9-
<Project Path="samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj" DefaultStartup="true" />
9+
<Project Path="samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj" />
1010
</Folder>
1111
<Folder Name="/src/">
1212
<Project Path="src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj" />
@@ -16,6 +16,7 @@
1616
<Project Path="src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj" />
1717
<Project Path="src/BenchmarkDotNet.Disassembler/BenchmarkDotNet.Disassembler.csproj" />
1818
<Project Path="src/BenchmarkDotNet.Exporters.Plotting/BenchmarkDotNet.Exporters.Plotting.csproj" />
19+
<Project Path="src/BenchmarkDotNet.InternalWeaver/BenchmarkDotNet.InternalWeaver.csproj" Id="4674adb8-7b45-4cfe-b1f2-935b41d96b65" />
1920
<Project Path="src/BenchmarkDotNet.TestAdapter/BenchmarkDotNet.TestAdapter.csproj" />
2021
<Project Path="src/BenchmarkDotNet.Weaver/BenchmarkDotNet.Weaver.csproj" />
2122
<Project Path="src/BenchmarkDotNet/BenchmarkDotNet.csproj" />
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
This project is used to weave BenchmarkDotNet assemblies for behavior that the C# compiler does not support. See Engine.
3+
-->
4+
5+
<Project Sdk="Microsoft.NET.Sdk">
6+
<Import Project="..\..\build\common.props" />
7+
8+
<PropertyGroup>
9+
<TargetFrameworks>netstandard2.0</TargetFrameworks>
10+
<OutDir>$(MSBuildThisFileDirectory)bin\$(Configuration)</OutDir>
11+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
12+
<IncludeBuildOutput>false</IncludeBuildOutput>
13+
<!-- AsmResolver is not signed. -->
14+
<SignAssembly>false</SignAssembly>
15+
<DelaySign>false</DelaySign>
16+
<IsPackable>false</IsPackable>
17+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
18+
</PropertyGroup>
19+
20+
<ItemGroup>
21+
<PackageReference Include="AsmResolver.DotNet" Version="6.0.0-beta.5" PrivateAssets="all" />
22+
<PackageReference Include="Microsoft.Build.Framework" Version="18.0.2" PrivateAssets="all" />
23+
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="18.0.2" PrivateAssets="all" />
24+
</ItemGroup>
25+
26+
<Import Project="..\..\build\common.targets" />
27+
</Project>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// *****
2+
// This is used to weave BenchmarkDotNet assemblies for behavior that the C# compiler does not support.
3+
// If any changes are made to this file, increment the InternalWeaverVersionSuffix in the common.props file, then run `build.cmd pack-internal-weaver`.
4+
// *****
5+
6+
using AsmResolver.DotNet;
7+
using AsmResolver.PE.DotNet.Metadata.Tables;
8+
using Microsoft.Build.Framework;
9+
using Microsoft.Build.Utilities;
10+
using System;
11+
using System.IO;
12+
13+
namespace BenchmarkDotNet.InternalWeaver;
14+
15+
public sealed class WeaveAssemblyTask : Task
16+
{
17+
[Required]
18+
public string TargetAssembly { get; set; }
19+
20+
public override bool Execute()
21+
{
22+
if (!File.Exists(TargetAssembly))
23+
{
24+
Log.LogError($"Assembly not found: {TargetAssembly}");
25+
return false;
26+
}
27+
28+
try
29+
{
30+
var module = ModuleDefinition.FromFile(TargetAssembly);
31+
32+
foreach (var type in module.GetAllTypes())
33+
{
34+
if (type.FullName == "BenchmarkDotNet.Engines.Engine")
35+
{
36+
WeaveEngine(type);
37+
}
38+
}
39+
40+
module.Write(TargetAssembly);
41+
}
42+
catch (Exception e)
43+
{
44+
Log.LogErrorFromException(e, true, true, null);
45+
return false;
46+
}
47+
return true;
48+
}
49+
50+
private static void WeaveEngine(TypeDefinition engineType)
51+
{
52+
// Apply AggressiveOptimization to all methods in the Engine and nested types (this includes compiler-generated state machines).
53+
WeaveMethods(engineType);
54+
55+
static void WeaveMethods(TypeDefinition type)
56+
{
57+
foreach (var method in type.Methods)
58+
{
59+
method.ImplAttributes |= MethodImplAttributes.AggressiveOptimization;
60+
}
61+
62+
// Recurse into nested types
63+
foreach (var nested in type.NestedTypes)
64+
{
65+
WeaveMethods(nested);
66+
}
67+
}
68+
}
69+
}

src/BenchmarkDotNet/BenchmarkDotNet.csproj

Lines changed: 20 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,24 @@
4950
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
5051
</PackageReference>
5152
</ItemGroup>
53+
54+
<!-- Weave our own assembly -->
55+
<ItemGroup>
56+
<ProjectReference Include="..\BenchmarkDotNet.InternalWeaver\BenchmarkDotNet.InternalWeaver.csproj" PrivateAssets="all" />
57+
</ItemGroup>
58+
<UsingTask TaskName="BenchmarkDotNet.InternalWeaver.WeaveAssemblyTask" AssemblyFile="$(MSBuildThisFileDirectory)..\BenchmarkDotNet.InternalWeaver\bin\$(Configuration)\BenchmarkDotNet.InternalWeaver.dll" />
59+
<PropertyGroup>
60+
<_WeaveAssemblyPath>$(IntermediateOutputPath)$(TargetName)$(TargetExt)</_WeaveAssemblyPath>
61+
<_WeaveAssembliesStampFile>$(IntermediateOutputPath)BenchmarkDotNetWeaver.stamp</_WeaveAssembliesStampFile>
62+
</PropertyGroup>
63+
<Target Name="InternalWeaveAssemblies" AfterTargets="CoreCompile" BeforeTargets="CopyFilesToOutputDirectory" Inputs="$(_WeaveAssemblyPath)" Outputs="$(_WeaveAssembliesStampFile)">
64+
<WeaveAssemblyTask TargetAssembly="$(_WeaveAssemblyPath)" />
65+
<!-- Create stamp file for incrementality -->
66+
<Touch Files="$(_WeaveAssembliesStampFile)" AlwaysCreate="true" />
67+
<ItemGroup>
68+
<FileWrites Include="$(_WeaveAssembliesStampFile)" />
69+
</ItemGroup>
70+
</Target>
71+
5272
<Import Project="..\..\build\common.targets" />
5373
</Project>

src/BenchmarkDotNet/Code/CodeGenerator.cs

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,17 @@ internal static string Generate(BuildPartition buildPartition)
3131
{
3232
var benchmark = buildInfo.BenchmarkCase;
3333

34-
var provider = GetDeclarationsProvider(benchmark.Descriptor);
35-
36-
string passArguments = GetPassArguments(benchmark);
37-
38-
string benchmarkTypeCode = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkType.txt"))
34+
string benchmarkTypeCode = GetDeclarationsProvider(benchmark)
35+
.ReplaceTemplate(new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkType.txt")))
3936
.Replace("$ID$", buildInfo.Id.ToString())
40-
.Replace("$OperationsPerInvoke$", provider.OperationsPerInvoke)
41-
.Replace("$WorkloadTypeName$", provider.WorkloadTypeName)
42-
.Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName)
43-
.Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName)
44-
.Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName)
45-
.Replace("$IterationCleanupMethodName$", provider.IterationCleanupMethodName)
4637
.Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark))
4738
.Replace("$ParamsContent$", GetParamsContent(benchmark))
4839
.Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark))
4940
.Replace("$DeclareArgumentFields$", GetDeclareArgumentFields(benchmark))
5041
.Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark))
51-
.Replace("$LoadArguments$", GetLoadArguments(benchmark))
52-
.Replace("$PassArguments$", passArguments)
5342
.Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark))
5443
.Replace("$RunExtraIteration$", buildInfo.Config.HasExtraIterationDiagnoser(benchmark) ? "true" : "false")
5544
.Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName)
56-
.Replace("$WorkloadMethodCall$", provider.GetWorkloadMethodCall(passArguments))
5745
.Replace("$InProcessDiagnoserRouters$", GetInProcessDiagnoserRouters(buildInfo))
5846
.ToString();
5947

@@ -122,27 +110,21 @@ private static string GetJobsSetDefinition(BenchmarkCase benchmarkCase)
122110
Replace("; ", ";\n ");
123111
}
124112

125-
private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor)
113+
private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchmark)
126114
{
127-
var method = descriptor.WorkloadMethod;
115+
var method = benchmark.Descriptor.WorkloadMethod;
128116

129-
if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask))
130-
{
131-
return new AsyncDeclarationsProvider(descriptor);
132-
}
133-
if (method.ReturnType.GetTypeInfo().IsGenericType
134-
&& (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
135-
|| method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>)))
117+
if (method.ReturnType.IsAwaitable())
136118
{
137-
return new AsyncDeclarationsProvider(descriptor);
119+
return new AsyncDeclarationsProvider(benchmark);
138120
}
139121

140122
if (method.ReturnType == typeof(void) && method.HasAttribute<AsyncStateMachineAttribute>())
141123
{
142124
throw new NotSupportedException("async void is not supported by design");
143125
}
144126

145-
return new SyncDeclarationsProvider(descriptor);
127+
return new SyncDeclarationsProvider(benchmark);
146128
}
147129

148130
// internal for tests
@@ -158,31 +140,19 @@ private static string GetArgumentsDefinition(BenchmarkCase benchmarkCase)
158140
=> string.Join(
159141
", ",
160142
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
161-
.Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}"));
143+
.Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}"));
162144

163145
private static string GetDeclareArgumentFields(BenchmarkCase benchmarkCase)
164146
=> string.Join(
165147
Environment.NewLine,
166148
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
167-
.Select((parameter, index) => $"private {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name)).GetCorrectCSharpTypeName()} __argField{index};"));
149+
.Select((parameter, index) => $"public {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name)).GetCorrectCSharpTypeName()} __argField{index};"));
168150

169151
private static string GetInitializeArgumentFields(BenchmarkCase benchmarkCase)
170152
=> string.Join(
171153
Environment.NewLine,
172154
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
173-
.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
174-
175-
private static string GetLoadArguments(BenchmarkCase benchmarkCase)
176-
=> string.Join(
177-
Environment.NewLine,
178-
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
179-
.Select((parameter, index) => $"{(parameter.ParameterType.IsByRef ? "ref" : string.Empty)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {(parameter.ParameterType.IsByRef ? "ref" : string.Empty)} this.__argField{index};"));
180-
181-
private static string GetPassArguments(BenchmarkCase benchmarkCase)
182-
=> string.Join(
183-
", ",
184-
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
185-
.Select((parameter, index) => $"{GetParameterModifier(parameter)} arg{index}"));
155+
.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
186156

187157
private static string GetExtraAttributes(BuildPartition buildPartition)
188158
{
@@ -236,7 +206,7 @@ private static string GetInProcessDiagnoserRouters(BenchmarkBuildInfo buildInfo)
236206
}
237207
}
238208

239-
private static string GetParameterModifier(ParameterInfo parameterInfo)
209+
internal static string GetParameterModifier(ParameterInfo parameterInfo)
240210
{
241211
if (!parameterInfo.ParameterType.IsByRef)
242212
return string.Empty;
@@ -260,28 +230,21 @@ private static Type GetFieldType(Type argumentType, ParameterInstance argument)
260230

261231
return argumentType;
262232
}
233+
}
263234

264-
private class SmartStringBuilder
265-
{
266-
private readonly string originalText;
267-
private readonly StringBuilder builder;
268-
269-
public SmartStringBuilder(string text)
270-
{
271-
originalText = text;
272-
builder = new StringBuilder(text);
273-
}
274-
275-
public SmartStringBuilder Replace(string oldValue, string? newValue)
276-
{
277-
if (originalText.Contains(oldValue))
278-
builder.Replace(oldValue, newValue);
279-
else
280-
builder.Append($"\n// '{oldValue}' not found");
281-
return this;
282-
}
235+
internal class SmartStringBuilder(string text)
236+
{
237+
private readonly StringBuilder builder = new(text);
283238

284-
public override string ToString() => builder.ToString();
239+
public SmartStringBuilder Replace(string oldValue, string? newValue)
240+
{
241+
if (text.Contains(oldValue))
242+
builder.Replace(oldValue, newValue);
243+
else
244+
builder.Append($"\n// '{oldValue}' not found");
245+
return this;
285246
}
247+
248+
public override string ToString() => builder.ToString();
286249
}
287250
}

0 commit comments

Comments
 (0)