diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs
index 52ae0e5478..7921d715f8 100644
--- a/src/BenchmarkDotNet/Code/CodeGenerator.cs
+++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs
@@ -35,6 +35,8 @@ internal static string Generate(BuildPartition buildPartition)
var provider = GetDeclarationsProvider(benchmark.Descriptor);
+ provider.OverrideUnrollFactor(benchmark);
+
string passArguments = GetPassArguments(benchmark);
string compilationId = $"{provider.ReturnsDefinition}_{buildInfo.Id}";
@@ -49,6 +51,7 @@ internal static string Generate(BuildPartition buildPartition)
.Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName)
.Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers)
.Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName)
+ .Replace("$AwaiterTypeName$", provider.AwaiterTypeName)
.Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName)
.Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName)
.Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName)
@@ -152,15 +155,12 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto
{
var method = descriptor.WorkloadMethod;
- if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask))
- {
- return new TaskDeclarationsProvider(descriptor);
- }
- if (method.ReturnType.GetTypeInfo().IsGenericType
- && (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
+ if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask)
+ || method.ReturnType.GetTypeInfo().IsGenericType
+ && (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
|| method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>)))
{
- return new GenericTaskDeclarationsProvider(descriptor);
+ return new TaskDeclarationsProvider(descriptor);
}
if (method.ReturnType == typeof(void))
diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
index ddf78eb572..5f2eca39a6 100644
--- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
+++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
@@ -1,5 +1,4 @@
using System;
-using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using BenchmarkDotNet.Engines;
@@ -11,9 +10,6 @@ namespace BenchmarkDotNet.Code
{
internal abstract class DeclarationsProvider
{
- // "GlobalSetup" or "GlobalCleanup" methods are optional, so default to an empty delegate, so there is always something that can be invoked
- private const string EmptyAction = "() => { }";
-
protected readonly Descriptor Descriptor;
internal DeclarationsProvider(Descriptor descriptor) => Descriptor = descriptor;
@@ -26,9 +22,9 @@ internal abstract class DeclarationsProvider
public string GlobalCleanupMethodName => GetMethodName(Descriptor.GlobalCleanupMethod);
- public string IterationSetupMethodName => Descriptor.IterationSetupMethod?.Name ?? EmptyAction;
+ public string IterationSetupMethodName => GetMethodName(Descriptor.IterationSetupMethod);
- public string IterationCleanupMethodName => Descriptor.IterationCleanupMethod?.Name ?? EmptyAction;
+ public string IterationCleanupMethodName => GetMethodName(Descriptor.IterationCleanupMethod);
public abstract string ReturnsDefinition { get; }
@@ -48,13 +44,18 @@ internal abstract class DeclarationsProvider
public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName();
+ public virtual string AwaiterTypeName => string.Empty;
+
+ public virtual void OverrideUnrollFactor(BenchmarkCase benchmarkCase) { }
+
public abstract string OverheadImplementation { get; }
private string GetMethodName(MethodInfo method)
{
+ // "Setup" or "Cleanup" methods are optional, so default to a simple delegate, so there is always something that can be invoked
if (method == null)
{
- return EmptyAction;
+ return "() => new System.Threading.Tasks.ValueTask()";
}
if (method.ReturnType == typeof(Task) ||
@@ -63,10 +64,10 @@ private string GetMethodName(MethodInfo method)
(method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) ||
method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))))
{
- return $"() => {method.Name}().GetAwaiter().GetResult()";
+ return $"() => BenchmarkDotNet.Helpers.AwaitHelper.ToValueTaskVoid({method.Name}())";
}
- return method.Name;
+ return $"() => {{ {method.Name}(); return new System.Threading.Tasks.ValueTask(); }}";
}
}
@@ -145,34 +146,18 @@ public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descripto
public override string WorkloadMethodReturnTypeModifiers => "ref readonly";
}
- internal class TaskDeclarationsProvider : VoidDeclarationsProvider
+ internal class TaskDeclarationsProvider : DeclarationsProvider
{
public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
- // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
- // and will eventually throw actual exception, not aggregated one
- public override string WorkloadMethodDelegate(string passArguments)
- => $"({passArguments}) => {{ {Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult(); }}";
-
- public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()";
+ public override string ReturnsDefinition => "RETURNS_AWAITABLE";
- protected override Type WorkloadMethodReturnType => typeof(void);
- }
-
- ///
- /// declarations provider for and
- ///
- internal class GenericTaskDeclarationsProvider : NonVoidDeclarationsProvider
- {
- public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
+ public override string AwaiterTypeName => WorkloadMethodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance).ReturnType.GetCorrectCSharpTypeName();
- protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single();
+ public override string OverheadImplementation => $"return default({OverheadMethodReturnType.GetCorrectCSharpTypeName()});";
- // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
- // and will eventually throw actual exception, not aggregated one
- public override string WorkloadMethodDelegate(string passArguments)
- => $"({passArguments}) => {{ return {Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult(); }}";
+ protected override Type OverheadMethodReturnType => WorkloadMethodReturnType;
- public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()";
+ public override void OverrideUnrollFactor(BenchmarkCase benchmarkCase) => benchmarkCase.ForceUnrollFactorForAsync();
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Engines/Engine.cs b/src/BenchmarkDotNet/Engines/Engine.cs
index 271c3c44e1..adbf9c0e2e 100644
--- a/src/BenchmarkDotNet/Engines/Engine.cs
+++ b/src/BenchmarkDotNet/Engines/Engine.cs
@@ -4,6 +4,7 @@
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
using BenchmarkDotNet.Characteristics;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Portability;
@@ -19,17 +20,17 @@ public class Engine : IEngine
public const int MinInvokeCount = 4;
[PublicAPI] public IHost Host { get; }
- [PublicAPI] public Action WorkloadAction { get; }
+ [PublicAPI] public Func> WorkloadAction { get; }
[PublicAPI] public Action Dummy1Action { get; }
[PublicAPI] public Action Dummy2Action { get; }
[PublicAPI] public Action Dummy3Action { get; }
- [PublicAPI] public Action OverheadAction { get; }
+ [PublicAPI] public Func> OverheadAction { get; }
[PublicAPI] public Job TargetJob { get; }
[PublicAPI] public long OperationsPerInvoke { get; }
- [PublicAPI] public Action GlobalSetupAction { get; }
- [PublicAPI] public Action GlobalCleanupAction { get; }
- [PublicAPI] public Action IterationSetupAction { get; }
- [PublicAPI] public Action IterationCleanupAction { get; }
+ [PublicAPI] public Func GlobalSetupAction { get; }
+ [PublicAPI] public Func GlobalCleanupAction { get; }
+ [PublicAPI] public Func IterationSetupAction { get; }
+ [PublicAPI] public Func IterationCleanupAction { get; }
[PublicAPI] public IResolver Resolver { get; }
[PublicAPI] public CultureInfo CultureInfo { get; }
[PublicAPI] public string BenchmarkName { get; }
@@ -51,9 +52,9 @@ public class Engine : IEngine
internal Engine(
IHost host,
IResolver resolver,
- Action dummy1Action, Action dummy2Action, Action dummy3Action, Action overheadAction, Action workloadAction, Job targetJob,
- Action globalSetupAction, Action globalCleanupAction, Action iterationSetupAction, Action iterationCleanupAction, long operationsPerInvoke,
- bool includeExtraStats, string benchmarkName)
+ Action dummy1Action, Action dummy2Action, Action dummy3Action, Func> overheadAction, Func> workloadAction,
+ Job targetJob, Func globalSetupAction, Func globalCleanupAction, Func iterationSetupAction, Func iterationCleanupAction,
+ long operationsPerInvoke, bool includeExtraStats, string benchmarkName)
{
Host = host;
@@ -91,7 +92,7 @@ public void Dispose()
{
try
{
- GlobalCleanupAction?.Invoke();
+ Helpers.AwaitHelper.GetResult(GlobalCleanupAction.Invoke());
}
catch (Exception e)
{
@@ -160,7 +161,7 @@ public Measurement RunIteration(IterationData data)
var action = isOverhead ? OverheadAction : WorkloadAction;
if (!isOverhead)
- IterationSetupAction();
+ Helpers.AwaitHelper.GetResult(IterationSetupAction());
GcCollect();
@@ -170,15 +171,14 @@ public Measurement RunIteration(IterationData data)
Span stackMemory = randomizeMemory ? stackalloc byte[random.Next(32)] : Span.Empty;
// Measure
- var clock = Clock.Start();
- action(invokeCount / unrollFactor);
- var clockSpan = clock.GetElapsed();
+ var op = action(invokeCount / unrollFactor, Clock);
+ var clockSpan = Helpers.AwaitHelper.GetResult(op);
if (EngineEventSource.Log.IsEnabled())
EngineEventSource.Log.IterationStop(data.IterationMode, data.IterationStage, totalOperations);
if (!isOverhead)
- IterationCleanupAction();
+ Helpers.AwaitHelper.GetResult(IterationCleanupAction());
if (randomizeMemory)
RandomizeManagedHeapMemory();
@@ -203,20 +203,21 @@ public Measurement RunIteration(IterationData data)
// it does not matter, because we have already obtained the results!
EnableMonitoring();
- IterationSetupAction(); // we run iteration setup first, so even if it allocates, it is not included in the results
+ Helpers.AwaitHelper.GetResult(IterationSetupAction()); // we run iteration setup first, so even if it allocates, it is not included in the results
var initialThreadingStats = ThreadingStats.ReadInitial(); // this method might allocate
var exceptionsStats = new ExceptionsStats(); // allocates
exceptionsStats.StartListening(); // this method might allocate
var initialGcStats = GcStats.ReadInitial();
- WorkloadAction(data.InvokeCount / data.UnrollFactor);
+ var op = WorkloadAction(data.InvokeCount / data.UnrollFactor, Clock);
+ Helpers.AwaitHelper.GetResult(op);
exceptionsStats.Stop();
var finalGcStats = GcStats.ReadFinal();
var finalThreadingStats = ThreadingStats.ReadFinal();
- IterationCleanupAction(); // we run iteration cleanup after collecting GC stats
+ Helpers.AwaitHelper.GetResult(IterationCleanupAction()); // we run iteration cleanup after collecting GC stats
var totalOperationsCount = data.InvokeCount * OperationsPerInvoke;
GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperations(totalOperationsCount);
@@ -231,14 +232,14 @@ private void Consume(in Span _) { }
private void RandomizeManagedHeapMemory()
{
// invoke global cleanup before global setup
- GlobalCleanupAction?.Invoke();
+ Helpers.AwaitHelper.GetResult(GlobalCleanupAction.Invoke());
var gen0object = new byte[random.Next(32)];
var lohObject = new byte[85 * 1024 + random.Next(32)];
// we expect the key allocations to happen in global setup (not ctor)
// so we call it while keeping the random-size objects alive
- GlobalSetupAction?.Invoke();
+ Helpers.AwaitHelper.GetResult(GlobalSetupAction.Invoke());
GC.KeepAlive(gen0object);
GC.KeepAlive(lohObject);
diff --git a/src/BenchmarkDotNet/Engines/EngineFactory.cs b/src/BenchmarkDotNet/Engines/EngineFactory.cs
index 0588218522..e311a927ee 100644
--- a/src/BenchmarkDotNet/Engines/EngineFactory.cs
+++ b/src/BenchmarkDotNet/Engines/EngineFactory.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using BenchmarkDotNet.Jobs;
using Perfolizer.Horology;
@@ -25,7 +26,7 @@ public IEngine CreateReadyToRun(EngineParameters engineParameters)
if (engineParameters.TargetJob == null)
throw new ArgumentNullException(nameof(engineParameters.TargetJob));
- engineParameters.GlobalSetupAction?.Invoke(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose
+ 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
if (!engineParameters.NeedsJitting) // just create the engine, do NOT jit
return CreateMultiActionEngine(engineParameters);
@@ -109,7 +110,7 @@ private static Engine CreateSingleActionEngine(EngineParameters engineParameters
engineParameters.OverheadActionNoUnroll,
engineParameters.WorkloadActionNoUnroll);
- private static Engine CreateEngine(EngineParameters engineParameters, Job job, Action idle, Action main)
+ private static Engine CreateEngine(EngineParameters engineParameters, Job job, Func> idle, Func> main)
=> new Engine(
engineParameters.Host,
EngineParameters.DefaultResolver,
diff --git a/src/BenchmarkDotNet/Engines/EngineParameters.cs b/src/BenchmarkDotNet/Engines/EngineParameters.cs
index ec61582529..c7361b3a07 100644
--- a/src/BenchmarkDotNet/Engines/EngineParameters.cs
+++ b/src/BenchmarkDotNet/Engines/EngineParameters.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using BenchmarkDotNet.Characteristics;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
@@ -12,19 +13,19 @@ public class EngineParameters
public static readonly IResolver DefaultResolver = new CompositeResolver(BenchmarkRunnerClean.DefaultResolver, EngineResolver.Instance);
public IHost Host { get; set; }
- public Action WorkloadActionNoUnroll { get; set; }
- public Action WorkloadActionUnroll { get; set; }
+ public Func> WorkloadActionNoUnroll { get; set; }
+ public Func> WorkloadActionUnroll { get; set; }
public Action Dummy1Action { get; set; }
public Action Dummy2Action { get; set; }
public Action Dummy3Action { get; set; }
- public Action OverheadActionNoUnroll { get; set; }
- public Action OverheadActionUnroll { get; set; }
+ public Func> OverheadActionNoUnroll { get; set; }
+ public Func> OverheadActionUnroll { get; set; }
public Job TargetJob { get; set; } = Job.Default;
public long OperationsPerInvoke { get; set; } = 1;
- public Action GlobalSetupAction { get; set; }
- public Action GlobalCleanupAction { get; set; }
- public Action IterationSetupAction { get; set; }
- public Action IterationCleanupAction { get; set; }
+ public Func GlobalSetupAction { get; set; }
+ public Func GlobalCleanupAction { get; set; }
+ public Func IterationSetupAction { get; set; }
+ public Func IterationCleanupAction { get; set; }
public bool MeasureExtraStats { get; set; }
[PublicAPI] public string BenchmarkName { get; set; }
diff --git a/src/BenchmarkDotNet/Engines/IEngine.cs b/src/BenchmarkDotNet/Engines/IEngine.cs
index e35b870d8b..5b38371c91 100644
--- a/src/BenchmarkDotNet/Engines/IEngine.cs
+++ b/src/BenchmarkDotNet/Engines/IEngine.cs
@@ -1,8 +1,10 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
using BenchmarkDotNet.Characteristics;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
+using Perfolizer.Horology;
namespace BenchmarkDotNet.Engines
{
@@ -19,13 +21,13 @@ public interface IEngine : IDisposable
long OperationsPerInvoke { get; }
- Action? GlobalSetupAction { get; }
+ Func GlobalSetupAction { get; }
- Action? GlobalCleanupAction { get; }
+ Func GlobalCleanupAction { get; }
- Action WorkloadAction { get; }
+ Func> WorkloadAction { get; }
- Action OverheadAction { get; }
+ Func> OverheadAction { get; }
IResolver Resolver { get; }
diff --git a/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs b/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs
new file mode 100644
index 0000000000..7fc093a9ea
--- /dev/null
+++ b/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Sources;
+
+namespace BenchmarkDotNet.Helpers
+{
+ ///
+ /// Implementation for that will reset itself when awaited so that it can be re-used.
+ ///
+ public class AutoResetValueTaskSource : IValueTaskSource, IValueTaskSource
+ {
+ private ManualResetValueTaskSourceCore _sourceCore;
+
+ /// Completes with a successful result.
+ /// The result.
+ public void SetResult(TResult result) => _sourceCore.SetResult(result);
+
+ /// Completes with an error.
+ /// The exception.
+ public void SetException(Exception error) => _sourceCore.SetException(error);
+
+ /// Gets the operation version.
+ public short Version => _sourceCore.Version;
+
+ private TResult GetResult(short token)
+ {
+ // We don't want to reset this if the token is invalid.
+ if (token != Version)
+ {
+ throw new InvalidOperationException();
+ }
+ try
+ {
+ return _sourceCore.GetResult(token);
+ }
+ finally
+ {
+ _sourceCore.Reset();
+ }
+ }
+
+ void IValueTaskSource.GetResult(short token) => GetResult(token);
+ TResult IValueTaskSource.GetResult(short token) => GetResult(token);
+
+ ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _sourceCore.GetStatus(token);
+ ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _sourceCore.GetStatus(token);
+
+ // Don't pass the flags, we don't want to schedule the continuation on the current SynchronizationContext or TaskScheduler if the user runs this in-process, as that may cause a deadlock when this is waited on synchronously.
+ // And we don't want to capture the ExecutionContext (we don't use it, and it causes allocations in the full framework).
+ void IValueTaskSource.OnCompleted(Action