Skip to content

Commit 4dd871f

Browse files
committed
Refactored delegates to pass in IClock and return ValueTask<ClockSpan>.
Force async unroll factor to 1. Support async IterationSetup/IterationCleanup.
1 parent 07fccb7 commit 4dd871f

34 files changed

+2519
-647
lines changed

src/BenchmarkDotNet/Code/CodeGenerator.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ internal static string Generate(BuildPartition buildPartition)
3535

3636
var provider = GetDeclarationsProvider(benchmark.Descriptor);
3737

38+
provider.OverrideUnrollFactor(benchmark);
39+
3840
string passArguments = GetPassArguments(benchmark);
3941

4042
string compilationId = $"{provider.ReturnsDefinition}_{buildInfo.Id}";
@@ -49,6 +51,7 @@ internal static string Generate(BuildPartition buildPartition)
4951
.Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName)
5052
.Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers)
5153
.Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName)
54+
.Replace("$AwaiterTypeName$", provider.AwaiterTypeName)
5255
.Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName)
5356
.Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName)
5457
.Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName)
@@ -152,15 +155,12 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto
152155
{
153156
var method = descriptor.WorkloadMethod;
154157

155-
if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask))
156-
{
157-
return new TaskDeclarationsProvider(descriptor);
158-
}
159-
if (method.ReturnType.GetTypeInfo().IsGenericType
160-
&& (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
158+
if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask)
159+
|| method.ReturnType.GetTypeInfo().IsGenericType
160+
&& (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
161161
|| method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>)))
162162
{
163-
return new GenericTaskDeclarationsProvider(descriptor);
163+
return new TaskDeclarationsProvider(descriptor);
164164
}
165165

166166
if (method.ReturnType == typeof(void))

src/BenchmarkDotNet/Code/DeclarationsProvider.cs

+16-27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Linq;
32
using System.Reflection;
43
using System.Threading.Tasks;
54
using BenchmarkDotNet.Engines;
@@ -11,9 +10,6 @@ namespace BenchmarkDotNet.Code
1110
{
1211
internal abstract class DeclarationsProvider
1312
{
14-
// "GlobalSetup" or "GlobalCleanup" methods are optional, so default to an empty delegate, so there is always something that can be invoked
15-
private const string EmptyAction = "() => { }";
16-
1713
protected readonly Descriptor Descriptor;
1814

1915
internal DeclarationsProvider(Descriptor descriptor) => Descriptor = descriptor;
@@ -26,9 +22,9 @@ internal abstract class DeclarationsProvider
2622

2723
public string GlobalCleanupMethodName => GetMethodName(Descriptor.GlobalCleanupMethod);
2824

29-
public string IterationSetupMethodName => Descriptor.IterationSetupMethod?.Name ?? EmptyAction;
25+
public string IterationSetupMethodName => GetMethodName(Descriptor.IterationSetupMethod);
3026

31-
public string IterationCleanupMethodName => Descriptor.IterationCleanupMethod?.Name ?? EmptyAction;
27+
public string IterationCleanupMethodName => GetMethodName(Descriptor.IterationCleanupMethod);
3228

3329
public abstract string ReturnsDefinition { get; }
3430

@@ -48,13 +44,18 @@ internal abstract class DeclarationsProvider
4844

4945
public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName();
5046

47+
public virtual string AwaiterTypeName => string.Empty;
48+
49+
public virtual void OverrideUnrollFactor(BenchmarkCase benchmarkCase) { }
50+
5151
public abstract string OverheadImplementation { get; }
5252

5353
private string GetMethodName(MethodInfo method)
5454
{
55+
// "Setup" or "Cleanup" methods are optional, so default to a simple delegate, so there is always something that can be invoked
5556
if (method == null)
5657
{
57-
return EmptyAction;
58+
return "() => new System.Threading.Tasks.ValueTask()";
5859
}
5960

6061
if (method.ReturnType == typeof(Task) ||
@@ -63,10 +64,10 @@ private string GetMethodName(MethodInfo method)
6364
(method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) ||
6465
method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))))
6566
{
66-
return $"() => awaitHelper.GetResult({method.Name}())";
67+
return $"() => BenchmarkDotNet.Helpers.AwaitHelper.ToValueTaskVoid({method.Name}())";
6768
}
6869

69-
return method.Name;
70+
return $"() => {{ {method.Name}(); return new System.Threading.Tasks.ValueTask(); }}";
7071
}
7172
}
7273

@@ -145,30 +146,18 @@ public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descripto
145146
public override string WorkloadMethodReturnTypeModifiers => "ref readonly";
146147
}
147148

148-
internal class TaskDeclarationsProvider : VoidDeclarationsProvider
149+
internal class TaskDeclarationsProvider : DeclarationsProvider
149150
{
150151
public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
151152

152-
public override string WorkloadMethodDelegate(string passArguments)
153-
=> $"({passArguments}) => {{ awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}";
154-
155-
public override string GetWorkloadMethodCall(string passArguments) => $"awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))";
153+
public override string ReturnsDefinition => "RETURNS_AWAITABLE";
156154

157-
protected override Type WorkloadMethodReturnType => typeof(void);
158-
}
159-
160-
/// <summary>
161-
/// declarations provider for <see cref="Task{TResult}" /> and <see cref="ValueTask{TResult}" />
162-
/// </summary>
163-
internal class GenericTaskDeclarationsProvider : NonVoidDeclarationsProvider
164-
{
165-
public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
155+
public override string AwaiterTypeName => WorkloadMethodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance).ReturnType.GetCorrectCSharpTypeName();
166156

167-
protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single();
157+
public override string OverheadImplementation => $"return default({OverheadMethodReturnType.GetCorrectCSharpTypeName()});";
168158

169-
public override string WorkloadMethodDelegate(string passArguments)
170-
=> $"({passArguments}) => {{ return awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}";
159+
protected override Type OverheadMethodReturnType => WorkloadMethodReturnType;
171160

172-
public override string GetWorkloadMethodCall(string passArguments) => $"awaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))";
161+
public override void OverrideUnrollFactor(BenchmarkCase benchmarkCase) => benchmarkCase.ForceUnrollFactorForAsync();
173162
}
174163
}

src/BenchmarkDotNet/Engines/Engine.cs

+23-20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Globalization;
55
using System.Linq;
66
using System.Runtime.CompilerServices;
7+
using System.Threading.Tasks;
78
using BenchmarkDotNet.Characteristics;
89
using BenchmarkDotNet.Jobs;
910
using BenchmarkDotNet.Portability;
@@ -19,17 +20,17 @@ public class Engine : IEngine
1920
public const int MinInvokeCount = 4;
2021

2122
[PublicAPI] public IHost Host { get; }
22-
[PublicAPI] public Action<long> WorkloadAction { get; }
23+
[PublicAPI] public Func<long, IClock, ValueTask<ClockSpan>> WorkloadAction { get; }
2324
[PublicAPI] public Action Dummy1Action { get; }
2425
[PublicAPI] public Action Dummy2Action { get; }
2526
[PublicAPI] public Action Dummy3Action { get; }
26-
[PublicAPI] public Action<long> OverheadAction { get; }
27+
[PublicAPI] public Func<long, IClock, ValueTask<ClockSpan>> OverheadAction { get; }
2728
[PublicAPI] public Job TargetJob { get; }
2829
[PublicAPI] public long OperationsPerInvoke { get; }
29-
[PublicAPI] public Action GlobalSetupAction { get; }
30-
[PublicAPI] public Action GlobalCleanupAction { get; }
31-
[PublicAPI] public Action IterationSetupAction { get; }
32-
[PublicAPI] public Action IterationCleanupAction { get; }
30+
[PublicAPI] public Func<ValueTask> GlobalSetupAction { get; }
31+
[PublicAPI] public Func<ValueTask> GlobalCleanupAction { get; }
32+
[PublicAPI] public Func<ValueTask> IterationSetupAction { get; }
33+
[PublicAPI] public Func<ValueTask> IterationCleanupAction { get; }
3334
[PublicAPI] public IResolver Resolver { get; }
3435
[PublicAPI] public CultureInfo CultureInfo { get; }
3536
[PublicAPI] public string BenchmarkName { get; }
@@ -46,13 +47,14 @@ public class Engine : IEngine
4647
private readonly EngineActualStage actualStage;
4748
private readonly bool includeExtraStats;
4849
private readonly Random random;
50+
private readonly Helpers.AwaitHelper awaitHelper;
4951

5052
internal Engine(
5153
IHost host,
5254
IResolver resolver,
53-
Action dummy1Action, Action dummy2Action, Action dummy3Action, Action<long> overheadAction, Action<long> workloadAction, Job targetJob,
54-
Action globalSetupAction, Action globalCleanupAction, Action iterationSetupAction, Action iterationCleanupAction, long operationsPerInvoke,
55-
bool includeExtraStats, string benchmarkName)
55+
Action dummy1Action, Action dummy2Action, Action dummy3Action, Func<long, IClock, ValueTask<ClockSpan>> overheadAction, Func<long, IClock, ValueTask<ClockSpan>> workloadAction,
56+
Job targetJob, Func<ValueTask> globalSetupAction, Func<ValueTask> globalCleanupAction, Func<ValueTask> iterationSetupAction, Func<ValueTask> iterationCleanupAction,
57+
long operationsPerInvoke, bool includeExtraStats, string benchmarkName)
5658
{
5759

5860
Host = host;
@@ -84,13 +86,14 @@ internal Engine(
8486
actualStage = new EngineActualStage(this);
8587

8688
random = new Random(12345); // we are using constant seed to try to get repeatable results
89+
awaitHelper = new Helpers.AwaitHelper();
8790
}
8891

8992
public void Dispose()
9093
{
9194
try
9295
{
93-
GlobalCleanupAction?.Invoke();
96+
awaitHelper.GetResult(GlobalCleanupAction.Invoke());
9497
}
9598
catch (Exception e)
9699
{
@@ -155,7 +158,7 @@ public Measurement RunIteration(IterationData data)
155158
var action = isOverhead ? OverheadAction : WorkloadAction;
156159

157160
if (!isOverhead)
158-
IterationSetupAction();
161+
awaitHelper.GetResult(IterationSetupAction());
159162

160163
GcCollect();
161164

@@ -165,15 +168,14 @@ public Measurement RunIteration(IterationData data)
165168
Span<byte> stackMemory = randomizeMemory ? stackalloc byte[random.Next(32)] : Span<byte>.Empty;
166169

167170
// Measure
168-
var clock = Clock.Start();
169-
action(invokeCount / unrollFactor);
170-
var clockSpan = clock.GetElapsed();
171+
var op = action(invokeCount / unrollFactor, Clock);
172+
var clockSpan = awaitHelper.GetResult(op);
171173

172174
if (EngineEventSource.Log.IsEnabled())
173175
EngineEventSource.Log.IterationStop(data.IterationMode, data.IterationStage, totalOperations);
174176

175177
if (!isOverhead)
176-
IterationCleanupAction();
178+
awaitHelper.GetResult(IterationCleanupAction());
177179

178180
if (randomizeMemory)
179181
RandomizeManagedHeapMemory();
@@ -196,17 +198,18 @@ public Measurement RunIteration(IterationData data)
196198
// it does not matter, because we have already obtained the results!
197199
EnableMonitoring();
198200

199-
IterationSetupAction(); // we run iteration setup first, so even if it allocates, it is not included in the results
201+
awaitHelper.GetResult(IterationSetupAction()); // we run iteration setup first, so even if it allocates, it is not included in the results
200202

201203
var initialThreadingStats = ThreadingStats.ReadInitial(); // this method might allocate
202204
var initialGcStats = GcStats.ReadInitial();
203205

204-
WorkloadAction(data.InvokeCount / data.UnrollFactor);
206+
var op = WorkloadAction(data.InvokeCount / data.UnrollFactor, Clock);
207+
awaitHelper.GetResult(op);
205208

206209
var finalGcStats = GcStats.ReadFinal();
207210
var finalThreadingStats = ThreadingStats.ReadFinal();
208211

209-
IterationCleanupAction(); // we run iteration cleanup after collecting GC stats
212+
awaitHelper.GetResult(IterationCleanupAction()); // we run iteration cleanup after collecting GC stats
210213

211214
GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke);
212215
ThreadingStats threadingStats = (finalThreadingStats - initialThreadingStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke);
@@ -220,14 +223,14 @@ private void Consume(in Span<byte> _) { }
220223
private void RandomizeManagedHeapMemory()
221224
{
222225
// invoke global cleanup before global setup
223-
GlobalCleanupAction?.Invoke();
226+
awaitHelper.GetResult(GlobalCleanupAction.Invoke());
224227

225228
var gen0object = new byte[random.Next(32)];
226229
var lohObject = new byte[85 * 1024 + random.Next(32)];
227230

228231
// we expect the key allocations to happen in global setup (not ctor)
229232
// so we call it while keeping the random-size objects alive
230-
GlobalSetupAction?.Invoke();
233+
awaitHelper.GetResult(GlobalSetupAction.Invoke());
231234

232235
GC.KeepAlive(gen0object);
233236
GC.KeepAlive(lohObject);

src/BenchmarkDotNet/Engines/EngineFactory.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading.Tasks;
23
using BenchmarkDotNet.Jobs;
34
using Perfolizer.Horology;
45

@@ -25,7 +26,7 @@ public IEngine CreateReadyToRun(EngineParameters engineParameters)
2526
if (engineParameters.TargetJob == null)
2627
throw new ArgumentNullException(nameof(engineParameters.TargetJob));
2728

28-
engineParameters.GlobalSetupAction?.Invoke(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose
29+
engineParameters.GlobalSetupAction.Invoke().AsTask().GetAwaiter().GetResult(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose
2930

3031
if (!engineParameters.NeedsJitting) // just create the engine, do NOT jit
3132
return CreateMultiActionEngine(engineParameters);
@@ -109,7 +110,7 @@ private static Engine CreateSingleActionEngine(EngineParameters engineParameters
109110
engineParameters.OverheadActionNoUnroll,
110111
engineParameters.WorkloadActionNoUnroll);
111112

112-
private static Engine CreateEngine(EngineParameters engineParameters, Job job, Action<long> idle, Action<long> main)
113+
private static Engine CreateEngine(EngineParameters engineParameters, Job job, Func<long, IClock, ValueTask<ClockSpan>> idle, Func<long, IClock, ValueTask<ClockSpan>> main)
113114
=> new Engine(
114115
engineParameters.Host,
115116
EngineParameters.DefaultResolver,

src/BenchmarkDotNet/Engines/EngineParameters.cs

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading.Tasks;
23
using BenchmarkDotNet.Characteristics;
34
using BenchmarkDotNet.Jobs;
45
using BenchmarkDotNet.Running;
@@ -12,19 +13,19 @@ public class EngineParameters
1213
public static readonly IResolver DefaultResolver = new CompositeResolver(BenchmarkRunnerClean.DefaultResolver, EngineResolver.Instance);
1314

1415
public IHost Host { get; set; }
15-
public Action<long> WorkloadActionNoUnroll { get; set; }
16-
public Action<long> WorkloadActionUnroll { get; set; }
16+
public Func<long, IClock, ValueTask<ClockSpan>> WorkloadActionNoUnroll { get; set; }
17+
public Func<long, IClock, ValueTask<ClockSpan>> WorkloadActionUnroll { get; set; }
1718
public Action Dummy1Action { get; set; }
1819
public Action Dummy2Action { get; set; }
1920
public Action Dummy3Action { get; set; }
20-
public Action<long> OverheadActionNoUnroll { get; set; }
21-
public Action<long> OverheadActionUnroll { get; set; }
21+
public Func<long, IClock, ValueTask<ClockSpan>> OverheadActionNoUnroll { get; set; }
22+
public Func<long, IClock, ValueTask<ClockSpan>> OverheadActionUnroll { get; set; }
2223
public Job TargetJob { get; set; } = Job.Default;
2324
public long OperationsPerInvoke { get; set; } = 1;
24-
public Action GlobalSetupAction { get; set; }
25-
public Action GlobalCleanupAction { get; set; }
26-
public Action IterationSetupAction { get; set; }
27-
public Action IterationCleanupAction { get; set; }
25+
public Func<ValueTask> GlobalSetupAction { get; set; }
26+
public Func<ValueTask> GlobalCleanupAction { get; set; }
27+
public Func<ValueTask> IterationSetupAction { get; set; }
28+
public Func<ValueTask> IterationCleanupAction { get; set; }
2829
public bool MeasureExtraStats { get; set; }
2930

3031
[PublicAPI] public string BenchmarkName { get; set; }

src/BenchmarkDotNet/Engines/IEngine.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System;
22
using System.Diagnostics.CodeAnalysis;
3+
using System.Threading.Tasks;
34
using BenchmarkDotNet.Characteristics;
45
using BenchmarkDotNet.Jobs;
56
using BenchmarkDotNet.Reports;
67
using JetBrains.Annotations;
8+
using Perfolizer.Horology;
79
using NotNullAttribute = JetBrains.Annotations.NotNullAttribute;
810

911
namespace BenchmarkDotNet.Engines
@@ -24,16 +26,16 @@ public interface IEngine : IDisposable
2426
long OperationsPerInvoke { get; }
2527

2628
[CanBeNull]
27-
Action GlobalSetupAction { get; }
29+
Func<ValueTask> GlobalSetupAction { get; }
2830

2931
[CanBeNull]
30-
Action GlobalCleanupAction { get; }
32+
Func<ValueTask> GlobalCleanupAction { get; }
3133

3234
[NotNull]
33-
Action<long> WorkloadAction { get; }
35+
Func<long, IClock, ValueTask<ClockSpan>> WorkloadAction { get; }
3436

3537
[NotNull]
36-
Action<long> OverheadAction { get; }
38+
Func<long, IClock, ValueTask<ClockSpan>> OverheadAction { get; }
3739

3840
IResolver Resolver { get; }
3941

0 commit comments

Comments
 (0)