diff --git a/src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs b/src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs
new file mode 100644
index 0000000000..7e95882608
--- /dev/null
+++ b/src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs
@@ -0,0 +1,8 @@
+using BenchmarkDotNet.Jobs;
+
+namespace BenchmarkDotNet.Attributes;
+
+///
+public class ConsumeTasksSynchronouslyAttribute(bool value) : JobMutatorConfigBaseAttribute(Job.Default.WithConsumeTasksSynchronously(value))
+{
+}
diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs
index 418fcfd637..8cf9f6c784 100644
--- a/src/BenchmarkDotNet/Code/CodeGenerator.cs
+++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs
@@ -115,6 +115,11 @@ private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchm
if (method.ReturnType.IsAwaitable(out var awaitableInfo))
{
+ if (benchmark.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance)
+ && AwaitHelper.IsBuiltInTaskType(method.ReturnType))
+ {
+ return new SyncTaskDeclarationsProvider(benchmark);
+ }
return new AsyncDeclarationsProvider(benchmark, awaitableInfo.ResultType);
}
diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
index ba52e0ac4f..f7b1fe750a 100644
--- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
+++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
@@ -2,6 +2,7 @@
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using Perfolizer.Horology;
@@ -89,6 +90,24 @@ private static string GetMethodPrefix(MethodInfo method)
protected string GetWorkloadMethodCall(string passArguments)
=> $"{GetMethodPrefix(Descriptor.WorkloadMethod)}.{Descriptor.WorkloadMethod.Name}({passArguments});";
+ protected string GetLoadArguments()
+ => string.Join(
+ Environment.NewLine,
+ Descriptor.WorkloadMethod.GetParameters()
+ .Select((parameter, index) =>
+ {
+ var refModifier = parameter.ParameterType.IsByRef ? "ref" : string.Empty;
+ return $"{refModifier} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {refModifier} this.__fieldsContainer.argField{index};";
+ })
+ );
+
+ protected string GetPassArguments()
+ => string.Join(
+ ", ",
+ Descriptor.WorkloadMethod.GetParameters()
+ .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} arg{index}")
+ );
+
protected string GetPassArgumentsDirect()
=> string.Join(
", ",
@@ -167,24 +186,108 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString
return smartStringBuilder
.Replace("$CoreImpl$", coreImpl);
}
+ }
- private string GetLoadArguments()
- => string.Join(
- Environment.NewLine,
- Descriptor.WorkloadMethod.GetParameters()
- .Select((parameter, index) =>
+ // Used when Job.Accuracy.ConsumeTasksSynchronously is enabled for (Value)Task()-returning workloads.
+ // Generates a synchronous loop that blocks on the returned task via AwaitHelper.GetResult, matching the
+ // pre-async-refactor behavior so historical results stay comparable.
+ internal sealed class SyncTaskDeclarationsProvider(BenchmarkCase benchmark) : DeclarationsProvider(benchmark)
+ {
+ public override string[] GetExtraFields() => [];
+
+ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartStringBuilder)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ string synchronizationContextTypeName = typeof(SynchronizationContext).GetCorrectCSharpTypeName();
+ string loadArguments = GetLoadArguments();
+ string passArguments = GetPassArguments();
+ string workloadMethodCall = $"global::{typeof(AwaitHelper).FullName}.{nameof(AwaitHelper.GetResult)}({GetWorkloadMethodCall(passArguments).TrimEnd(';')});";
+ string coreImpl = $$"""
+ private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}})
{
- var refModifier = parameter.ParameterType.IsByRef ? "ref" : string.Empty;
- return $"{refModifier} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {refModifier} this.__fieldsContainer.argField{index};";
- })
- );
+ {{synchronizationContextTypeName}} originalContext = {{synchronizationContextTypeName}}.Current;
+ {{synchronizationContextTypeName}}.SetSynchronizationContext(null);
+ try
+ {
+ {{loadArguments}}
+ {{StartClockSyncCode}}
+ while (--invokeCount >= 0)
+ {
+ this.__Overhead({{passArguments}});@Unroll@
+ }
+ {{ReturnSyncCode}}
+ }
+ finally
+ {
+ {{synchronizationContextTypeName}}.SetSynchronizationContext(originalContext);
+ }
+ }
- private string GetPassArguments()
- => string.Join(
- ", ",
- Descriptor.WorkloadMethod.GetParameters()
- .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} arg{index}")
- );
+ private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}})
+ {
+ {{synchronizationContextTypeName}} originalContext = {{synchronizationContextTypeName}}.Current;
+ {{synchronizationContextTypeName}}.SetSynchronizationContext(null);
+ try
+ {
+ {{loadArguments}}
+ {{StartClockSyncCode}}
+ while (--invokeCount >= 0)
+ {
+ this.__Overhead({{passArguments}});
+ }
+ {{ReturnSyncCode}}
+ }
+ finally
+ {
+ {{synchronizationContextTypeName}}.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}})
+ {
+ {{synchronizationContextTypeName}} originalContext = {{synchronizationContextTypeName}}.Current;
+ {{synchronizationContextTypeName}}.SetSynchronizationContext(null);
+ try
+ {
+ {{loadArguments}}
+ {{StartClockSyncCode}}
+ while (--invokeCount >= 0)
+ {
+ {{workloadMethodCall}}@Unroll@
+ }
+ {{ReturnSyncCode}}
+ }
+ finally
+ {
+ {{synchronizationContextTypeName}}.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}})
+ {
+ {{synchronizationContextTypeName}} originalContext = {{synchronizationContextTypeName}}.Current;
+ {{synchronizationContextTypeName}}.SetSynchronizationContext(null);
+ try
+ {
+ {{loadArguments}}
+ {{StartClockSyncCode}}
+ while (--invokeCount >= 0)
+ {
+ {{workloadMethodCall}}
+ }
+ {{ReturnSyncCode}}
+ }
+ finally
+ {
+ {{synchronizationContextTypeName}}.SetSynchronizationContext(originalContext);
+ }
+ }
+ """;
+
+ return smartStringBuilder
+ .Replace("$CoreImpl$", coreImpl);
+ }
}
internal abstract class AsyncDeclarationsProviderBase(BenchmarkCase benchmark) : DeclarationsProvider(benchmark)
diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs
index 543e1306ac..89279e5faa 100644
--- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs
+++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs
@@ -228,6 +228,9 @@ public bool UseDisassemblyDiagnoser
[Option("evaluateOverhead", Required = false, HelpText = "Specifies whether to run and evaluate overhead iterations.")]
public bool? EvaluateOverhead { get; set; }
+ [Option("consumeTasksSynchronously", Required = false, Default = false, HelpText = "Specifies whether to consume (Value)Task-returning benchmarks synchronously.")]
+ public bool ConsumeTasksSynchronously { get; set; }
+
[Option("resume", Required = false, Default = false, HelpText = "Continue the execution if the last run was stopped.")]
public bool Resume { get; set; }
diff --git a/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs b/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs
index 675a1f8df8..48b96df4a1 100644
--- a/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs
+++ b/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs
@@ -23,6 +23,7 @@ private EnvironmentResolver()
// TODO: find a better place
Register(AccuracyMode.AnalyzeLaunchVarianceCharacteristic, () => false);
+ Register(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, () => false);
Register(RunMode.UnrollFactorCharacteristic, job =>
{
// TODO: move it to another place and use the main resolver
diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs
new file mode 100644
index 0000000000..f7f0f00299
--- /dev/null
+++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs
@@ -0,0 +1,119 @@
+using JetBrains.Annotations;
+using System.ComponentModel;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace BenchmarkDotNet.Helpers;
+
+[UsedImplicitly]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public static class AwaitHelper
+{
+ private class ValueTaskWaiter
+ {
+ // We use thread static field so that each thread uses its own individual callback and reset event.
+ [ThreadStatic]
+ private static ValueTaskWaiter? ts_current;
+ internal static ValueTaskWaiter Current => ts_current ??= new ValueTaskWaiter();
+
+ // We cache the callback to prevent allocations for memory diagnoser.
+ private readonly Action awaiterCallback;
+ private readonly ManualResetEventSlim resetEvent;
+
+ private ValueTaskWaiter()
+ {
+ resetEvent = new();
+ awaiterCallback = resetEvent.Set;
+ }
+
+ internal void Wait(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion
+ {
+ resetEvent.Reset();
+ awaiter.UnsafeOnCompleted(awaiterCallback);
+
+ // The fastest way to wait for completion is to spin a bit before waiting on the event. This is the same logic that Task.GetAwaiter().GetResult() uses.
+ var spinner = new SpinWait();
+ while (!resetEvent.IsSet)
+ {
+ if (spinner.NextSpinWillYield)
+ {
+ resetEvent.Wait();
+ return;
+ }
+ spinner.SpinOnce();
+ }
+ }
+ }
+
+ // 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 static void GetResult(this Task task) => task.GetAwaiter().GetResult();
+
+ public static T GetResult(this Task task) => task.GetAwaiter().GetResult();
+
+ // ValueTask can be backed by an IValueTaskSource that only supports asynchronous awaits,
+ // so we have to hook up a callback instead of calling .GetAwaiter().GetResult() like we do for Task.
+ // The alternative is to convert it to Task using .AsTask(), but that causes allocations which we must avoid for memory diagnoser.
+ public static void GetResult(this ValueTask task)
+ {
+ // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process.
+ var awaiter = task.ConfigureAwait(false).GetAwaiter();
+ if (!awaiter.IsCompleted)
+ {
+ ValueTaskWaiter.Current.Wait(awaiter);
+ }
+ awaiter.GetResult();
+ }
+
+ public static T GetResult(this ValueTask task)
+ {
+ // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process.
+ var awaiter = task.ConfigureAwait(false).GetAwaiter();
+ if (!awaiter.IsCompleted)
+ {
+ ValueTaskWaiter.Current.Wait(awaiter);
+ }
+ return awaiter.GetResult();
+ }
+
+ internal static MethodInfo? GetGetResultMethod(Type taskType)
+ {
+ if (!taskType.IsGenericType)
+ {
+ return typeof(AwaitHelper).GetMethod(nameof(GetResult), BindingFlags.Public | BindingFlags.Static, null, [taskType], null)!;
+ }
+ var genericTypeDefinition = taskType.GetGenericTypeDefinition();
+ Type? compareType = genericTypeDefinition == typeof(ValueTask<>) ? typeof(ValueTask<>)
+ : genericTypeDefinition == typeof(Task<>) ? typeof(Task<>)
+ : null;
+ if (compareType == null)
+ {
+ return null;
+ }
+ var resultType = taskType
+ .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)!
+ .ReturnType
+ .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance)!
+ .ReturnType;
+ return typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .First(m =>
+ {
+ if (m.Name != nameof(GetResult))
+ return false;
+ Type paramType = m.GetParameters().First().ParameterType;
+ return paramType.IsGenericType && paramType.GetGenericTypeDefinition() == compareType;
+ })
+ .MakeGenericMethod([resultType]);
+ }
+
+ internal static bool IsBuiltInTaskType(Type type)
+ {
+ if (!type.IsGenericType)
+ {
+ return type == typeof(ValueTask) || type == typeof(Task);
+ }
+ var genericTypeDefinition = type.GetGenericTypeDefinition();
+ return genericTypeDefinition == typeof(ValueTask<>)
+ || genericTypeDefinition == typeof(Task<>);
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Jobs/AccuracyMode.cs b/src/BenchmarkDotNet/Jobs/AccuracyMode.cs
index 97b794dbd0..3b11150d15 100644
--- a/src/BenchmarkDotNet/Jobs/AccuracyMode.cs
+++ b/src/BenchmarkDotNet/Jobs/AccuracyMode.cs
@@ -13,6 +13,7 @@ public sealed class AccuracyMode : JobMode
public static readonly Characteristic MinIterationTimeCharacteristic = CreateCharacteristic(nameof(MinIterationTime));
public static readonly Characteristic MinInvokeCountCharacteristic = CreateCharacteristic(nameof(MinInvokeCount));
public static readonly Characteristic EvaluateOverheadCharacteristic = CreateCharacteristic(nameof(EvaluateOverhead));
+ public static readonly Characteristic ConsumeTasksSynchronouslyCharacteristic = CreateCharacteristic(nameof(ConsumeTasksSynchronously));
public static readonly Characteristic OutlierModeCharacteristic = CreateCharacteristic(nameof(OutlierMode));
public static readonly Characteristic AnalyzeLaunchVarianceCharacteristic = CreateCharacteristic(nameof(AnalyzeLaunchVariance));
@@ -59,8 +60,8 @@ public int MinInvokeCount
}
///
- /// Specifies if the overhead should be evaluated (Idle runs) and it's average value subtracted from every result.
- /// True by default, very important for nano-benchmarks.
+ /// Specifies if the overhead should be evaluated (Idle runs) and its average value subtracted from every result.
+ /// False by default.
///
public bool EvaluateOverhead
{
@@ -68,6 +69,19 @@ public bool EvaluateOverhead
set => EvaluateOverheadCharacteristic[this] = value;
}
+ ///
+ /// Specifies whether (Value)Task-returning benchmarks should be consumed synchronously.
+ /// False by default.
+ ///
+ ///
+ /// Intended to make async benchmark results comparable to historical results obtained from older BenchmarkDotNet versions. Recommended to leave false for new benchmarks.
+ ///
+ public bool ConsumeTasksSynchronously
+ {
+ get => ConsumeTasksSynchronouslyCharacteristic[this];
+ set => ConsumeTasksSynchronouslyCharacteristic[this] = value;
+ }
+
///
/// Specifies which outliers should be removed from the distribution.
///
diff --git a/src/BenchmarkDotNet/Jobs/JobExtensions.cs b/src/BenchmarkDotNet/Jobs/JobExtensions.cs
index 2842c8d694..9099e15ed5 100644
--- a/src/BenchmarkDotNet/Jobs/JobExtensions.cs
+++ b/src/BenchmarkDotNet/Jobs/JobExtensions.cs
@@ -299,6 +299,9 @@ public static Job WithMsBuildArguments(this Job job, params string[] msBuildArgu
///
public static Job WithEvaluateOverhead(this Job job, bool value) => job.WithCore(j => j.Accuracy.EvaluateOverhead = value);
+ ///
+ public static Job WithConsumeTasksSynchronously(this Job job, bool value) => job.WithCore(j => j.Accuracy.ConsumeTasksSynchronously = value);
+
///
/// Specifies which outliers should be removed from the distribution
///
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs
index bd0528d122..81519664d6 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs
@@ -12,7 +12,7 @@ partial class RunnableEmitter
// Roslyn generates ordinals in declaration order of every member.
// We don't necessarily emit members in the same order (or at all in the case of Runnable_#.Run), so we map it to the expected Roslyn ordinal.
// This doesn't really matter for the runtime, but it helps with the NaiveRunnableEmitDiff tests.
- private readonly Dictionary s_asyncMethodToOrdinalMap = new()
+ protected virtual IReadOnlyDictionary AsyncMethodToOrdinalMap { get; } = new Dictionary
{
{ GlobalSetupMethodName, 4 },
{ GlobalCleanupMethodName, 5 },
@@ -134,7 +134,7 @@ private AsyncStateMachineBuilderInfo BeginAsyncStateMachineTypeBuilder(string ca
[CompilerGenerated]
private struct <__GlobalSetup>d__4 : IAsyncStateMachine
*/
- int ordinal = s_asyncMethodToOrdinalMap[callerMethodName];
+ int ordinal = AsyncMethodToOrdinalMap[callerMethodName];
var asyncStateMachineTypeBuilder = runnableBuilder.DefineNestedType(
$"<{callerMethodName}>d__{ordinal}",
TypeAttributes.NestedPrivate | TypeAttributes.AutoLayout | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit,
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs
index 4c15ba95e6..b2fcd89257 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs
@@ -1,5 +1,6 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Helpers.Reflection.Emit;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
@@ -72,7 +73,17 @@ public static Assembly EmitPartitionAssembly(GenerateResult generateResult, Buil
var returnType = benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType;
RunnableEmitter runnableEmitter;
if (returnType.IsAwaitable(out var awaitableInfo))
- runnableEmitter = new AsyncCoreEmitter(buildPartition, moduleBuilder, benchmark, awaitableInfo);
+ {
+ if (benchmark.BenchmarkCase.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, buildPartition.Resolver)
+ && AwaitHelper.IsBuiltInTaskType(returnType))
+ {
+ runnableEmitter = new SyncTaskCoreEmitter(buildPartition, moduleBuilder, benchmark);
+ }
+ else
+ {
+ runnableEmitter = new AsyncCoreEmitter(buildPartition, moduleBuilder, benchmark, awaitableInfo);
+ }
+ }
else if (returnType.IsAsyncEnumerable(out var asyncEnumerableInfo))
runnableEmitter = new AsyncEnumerableCoreEmitter(buildPartition, moduleBuilder, benchmark, asyncEnumerableInfo);
else
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs
new file mode 100644
index 0000000000..9f4175ce0c
--- /dev/null
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs
@@ -0,0 +1,173 @@
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Helpers.Reflection.Emit;
+using BenchmarkDotNet.Running;
+using Perfolizer.Horology;
+using System.Reflection;
+using System.Reflection.Emit;
+using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants;
+
+namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation;
+
+partial class RunnableEmitter
+{
+ // Used when Job.Accuracy.ConsumeTasksSynchronously is enabled for (Value)Task()-returning workloads.
+ // Emits the same shape as SyncCoreEmitter but routes the workload return value through AwaitHelper.GetResult
+ // so the iteration loop stays synchronous, matching the pre-async-refactor behavior so historical results stay comparable.
+ private sealed class SyncTaskCoreEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder, BenchmarkBuildInfo benchmark) : RunnableEmitter(buildPartition, moduleBuilder, benchmark)
+ {
+ private static readonly MethodInfo SynchronizationContextGetCurrent =
+ typeof(SynchronizationContext).GetProperty(nameof(SynchronizationContext.Current), BindingFlags.Public | BindingFlags.Static)!.GetMethod!;
+ private static readonly MethodInfo SynchronizationContextSetCurrent =
+ typeof(SynchronizationContext).GetMethod(nameof(SynchronizationContext.SetSynchronizationContext), BindingFlags.Public | BindingFlags.Static, null, [typeof(SynchronizationContext)], null)!;
+
+ // The workload is consumed synchronously, so the only async state machines are the setup/cleanup
+ // methods. Without arguments there is no fields container declared before them, so their Roslyn
+ // ordinals are two lower than in the async path (which always declares the fields container).
+ // With arguments the fields container shifts them back to the async ordinals.
+ protected override IReadOnlyDictionary AsyncMethodToOrdinalMap
+ => argFields.Count > 0
+ ? base.AsyncMethodToOrdinalMap
+ : new Dictionary
+ {
+ { GlobalSetupMethodName, 2 },
+ { GlobalCleanupMethodName, 3 },
+ { IterationSetupMethodName, 4 },
+ { IterationCleanupMethodName, 5 },
+ };
+
+ protected override void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { }
+
+ protected override void EmitCoreImpl()
+ {
+ EmitAction(OverheadActionUnrollMethodName, overheadImplementationMethod, jobUnrollFactor, isWorkload: false);
+ EmitAction(OverheadActionNoUnrollMethodName, overheadImplementationMethod, 1, isWorkload: false);
+ EmitAction(WorkloadActionUnrollMethodName, Descriptor.WorkloadMethod, jobUnrollFactor, isWorkload: true);
+ EmitAction(WorkloadActionNoUnrollMethodName, Descriptor.WorkloadMethod, 1, isWorkload: true);
+ }
+
+ private MethodBuilder EmitAction(string methodName, MethodInfo methodToCall, int unrollFactor, bool isWorkload)
+ {
+ MethodInfo? getResultMethod = null;
+ if (isWorkload)
+ {
+ getResultMethod = AwaitHelper.GetGetResultMethod(methodToCall.ReturnType)
+ ?? throw new InvalidOperationException(
+ $"AwaitHelper.GetResult is not available for workload return type {methodToCall.ReturnType.GetDisplayName()}. ConsumeTasksSynchronously only supports (Value)Task().");
+ }
+
+ var invokeCountArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long));
+ var actionMethodBuilder = runnableBuilder
+ .DefineNonVirtualInstanceMethod(
+ methodName,
+ MethodAttributes.Private,
+ EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)),
+ [
+ invokeCountArg,
+ new EmitParameterInfo(1, ClockParamName, typeof(IClock))
+ ]
+ )
+ .SetAggressiveOptimizationImplementationFlag();
+ invokeCountArg.SetMember(actionMethodBuilder);
+
+ var ilBuilder = actionMethodBuilder.GetILGenerator();
+
+ var originalContextLocal = ilBuilder.DeclareLocal(typeof(SynchronizationContext));
+ var argLocals = argFields.Select(a => ilBuilder.DeclareLocal(a.ArgLocalsType)).ToList();
+ var startedClockLocal = ilBuilder.DeclareLocal(typeof(StartedClock));
+ var resultLocal = ilBuilder.DeclareLocal(typeof(ValueTask));
+ var endLabel = ilBuilder.DefineLabel();
+
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ // SynchronizationContext originalContext = SynchronizationContext.Current;
+ ilBuilder.Emit(OpCodes.Call, SynchronizationContextGetCurrent);
+ ilBuilder.EmitStloc(originalContextLocal);
+ // SynchronizationContext.SetSynchronizationContext(null);
+ ilBuilder.Emit(OpCodes.Ldnull);
+ ilBuilder.Emit(OpCodes.Call, SynchronizationContextSetCurrent);
+
+ ilBuilder.BeginExceptionBlock();
+ {
+ // load fields
+ EmitLoadArgFieldsToLocals(ilBuilder, argLocals);
+
+ // StartedClock startedClock = ClockExtensions.Start(clock);
+ ilBuilder.Emit(OpCodes.Ldarg_2);
+ ilBuilder.Emit(OpCodes.Call, GetStartClockMethod());
+ ilBuilder.EmitStloc(startedClockLocal);
+
+ // loop
+ ilBuilder.EmitLoopBeginFromArgToZero(out var loopStartLabel, out var loopHeadLabel);
+ {
+ for (int u = 0; u < unrollFactor; u++)
+ {
+ if (!methodToCall.IsStatic)
+ {
+ ilBuilder.Emit(OpCodes.Ldarg_0);
+ }
+ ilBuilder.EmitLdLocals(argLocals);
+ ilBuilder.Emit(OpCodes.Call, methodToCall);
+
+ if (getResultMethod is not null)
+ {
+ // global::BenchmarkDotNet.Helpers.AwaitHelper.GetResult();
+ ilBuilder.Emit(OpCodes.Call, getResultMethod);
+ // Generic Task/ValueTask overloads return T — discard it, mirroring the pre-refactor
+ // void ExecuteBlocking() => AwaitHelper.GetResult(callback()) implementation.
+ if (getResultMethod.ReturnType != typeof(void))
+ {
+ ilBuilder.Emit(OpCodes.Pop);
+ }
+ }
+ else if (methodToCall.ReturnType != typeof(void))
+ {
+ ilBuilder.Emit(OpCodes.Pop);
+ }
+ }
+ }
+ ilBuilder.EmitLoopEndFromArgToZero(loopStartLabel, loopHeadLabel, invokeCountArg);
+
+ // resultLocal = new ValueTask(startedClock.GetElapsed());
+ ilBuilder.EmitLdloca(startedClockLocal);
+ ilBuilder.Emit(OpCodes.Call, typeof(StartedClock).GetMethod(nameof(StartedClock.GetElapsed), BindingFlags.Public | BindingFlags.Instance)!);
+ ilBuilder.Emit(OpCodes.Newobj, typeof(ValueTask).GetConstructor([typeof(ClockSpan)])!);
+ ilBuilder.EmitStloc(resultLocal);
+ ilBuilder.Emit(OpCodes.Leave, endLabel);
+ }
+ ilBuilder.BeginFinallyBlock();
+ {
+ // SynchronizationContext.Current = originalContext;
+ ilBuilder.EmitLdloc(originalContextLocal);
+ ilBuilder.Emit(OpCodes.Call, SynchronizationContextSetCurrent);
+ }
+ ilBuilder.EndExceptionBlock();
+
+ ilBuilder.MarkLabel(endLabel);
+ ilBuilder.EmitLdloc(resultLocal);
+ ilBuilder.Emit(OpCodes.Ret);
+
+ return actionMethodBuilder;
+ }
+
+ private void EmitLoadArgFieldsToLocals(ILGenerator ilBuilder, List argLocals)
+ {
+ for (int i = 0; i < argFields.Count; i++)
+ {
+ ilBuilder.Emit(OpCodes.Ldarg_0);
+ ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField);
+
+ var argFieldInfo = argFields[i];
+ if (argFieldInfo.ArgLocalsType.IsByRef)
+ ilBuilder.Emit(OpCodes.Ldflda, argFieldInfo.Field);
+ else
+ ilBuilder.Emit(OpCodes.Ldfld, argFieldInfo.Field);
+
+ if (argFieldInfo.OpImplicitMethod != null)
+ ilBuilder.Emit(OpCodes.Call, argFieldInfo.OpImplicitMethod);
+
+ ilBuilder.EmitStloc(argLocals[i]);
+ }
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs
index a06f11894d..ba650ceeb6 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs
@@ -44,6 +44,7 @@ public class InProcessValidator : IValidator
{ RunMode.UnrollFactorCharacteristic, DontValidate },
{ AccuracyMode.AnalyzeLaunchVarianceCharacteristic, DontValidate },
{ AccuracyMode.EvaluateOverheadCharacteristic, DontValidate },
+ { AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, DontValidate },
{ AccuracyMode.MaxRelativeErrorCharacteristic, DontValidate },
{ AccuracyMode.MaxAbsoluteErrorCharacteristic, DontValidate },
{ AccuracyMode.MinInvokeCountCharacteristic, DontValidate },
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs
index b152fc4c6f..4f7fa22609 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs
@@ -7,7 +7,7 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit;
internal static class BenchmarkActionFactory
{
- private static IBenchmarkAction CreateCore(IBenchmarkActionFactory? factory, object instance, MethodInfo targetMethod, int unrollFactor)
+ private static IBenchmarkAction CreateCore(IBenchmarkActionFactory? factory, object instance, MethodInfo targetMethod, int unrollFactor, bool consumeTasksSynchronously = false)
{
if (factory?.TryCreate(instance, targetMethod, unrollFactor, out var benchmarkAction) == true)
{
@@ -41,10 +41,14 @@ private static IBenchmarkAction CreateCore(IBenchmarkActionFactory? factory, obj
}
if (resultType == typeof(Task))
- return new BenchmarkActionTask(resultInstance, targetMethod, unrollFactor);
+ return consumeTasksSynchronously
+ ? new BenchmarkActionBlockingTask(resultInstance, targetMethod, unrollFactor)
+ : new BenchmarkActionTask(resultInstance, targetMethod, unrollFactor);
if (resultType == typeof(ValueTask))
- return new BenchmarkActionValueTask(resultInstance, targetMethod, unrollFactor);
+ return consumeTasksSynchronously
+ ? new BenchmarkActionBlockingValueTask(resultInstance, targetMethod, unrollFactor)
+ : new BenchmarkActionValueTask(resultInstance, targetMethod, unrollFactor);
if (resultType.GetTypeInfo().IsGenericType)
{
@@ -52,14 +56,18 @@ private static IBenchmarkAction CreateCore(IBenchmarkActionFactory? factory, obj
var argType = resultType.GenericTypeArguments[0];
if (typeof(Task<>) == genericType)
return Create(
- typeof(BenchmarkActionTask<>).MakeGenericType(argType),
+ consumeTasksSynchronously
+ ? typeof(BenchmarkActionBlockingTask<>).MakeGenericType(argType)
+ : typeof(BenchmarkActionTask<>).MakeGenericType(argType),
resultInstance,
targetMethod,
unrollFactor);
- if (typeof(ValueTask<>).IsAssignableFrom(genericType))
+ if (typeof(ValueTask<>) == genericType)
return Create(
- typeof(BenchmarkActionValueTask<>).MakeGenericType(argType),
+ consumeTasksSynchronously
+ ? typeof(BenchmarkActionBlockingValueTask<>).MakeGenericType(argType)
+ : typeof(BenchmarkActionValueTask<>).MakeGenericType(argType),
resultInstance,
targetMethod,
unrollFactor);
@@ -125,8 +133,8 @@ private static BenchmarkActionBase Create(Type actionType, object? instance, Met
private static readonly MethodInfo FallbackSignature = new Action(BenchmarkActionBase.OverheadStatic).GetMethodInfo();
- public static IBenchmarkAction CreateWorkload(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, int unrollFactor) =>
- CreateCore(factory, instance, descriptor.WorkloadMethod, unrollFactor);
+ public static IBenchmarkAction CreateWorkload(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, int unrollFactor, bool consumeTasksSynchronously = false) =>
+ CreateCore(factory, instance, descriptor.WorkloadMethod, unrollFactor, consumeTasksSynchronously);
public static IBenchmarkAction CreateOverhead(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, int unrollFactor) =>
CreateCore(factory, instance, FallbackSignature, unrollFactor);
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionImpl.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionImpl.cs
index 51148cb31e..316e65f2ba 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionImpl.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionImpl.cs
@@ -1,5 +1,6 @@
using BenchmarkDotNet.Attributes.CompilerServices;
using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Helpers;
using Perfolizer.Horology;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@@ -702,3 +703,327 @@ private async Task WorkloadCore()
public override void Cleanup()
=> workloadValueTaskSource.Complete();
}
+
+// Blocking variants used when Job.Accuracy.ConsumeTasksSynchronously is enabled. They drive the workload
+// through AwaitHelper.GetResult so the iteration loop stays synchronous, matching the pre-async-refactor behavior
+// so historical results stay comparable.
+
+[AggressivelyOptimizeMethods]
+public sealed class BenchmarkActionBlockingTask : BenchmarkActionBase
+{
+ private readonly Func callback;
+ private readonly Action blockingCallback;
+ private readonly Action unrolledBlockingCallback;
+
+ [SetsRequiredMembers]
+ public BenchmarkActionBlockingTask(object? instance, MethodInfo method, int unrollFactor)
+ {
+ callback = CreateWorkload>(instance, method);
+ blockingCallback = InvokeBlocking;
+ unrolledBlockingCallback = Unroll(blockingCallback, unrollFactor);
+ InvokeSingle = InvokeOnce;
+ InvokeUnroll = WorkloadActionUnroll;
+ InvokeNoUnroll = WorkloadActionNoUnroll;
+ }
+
+ private void InvokeBlocking() => AwaitHelper.GetResult(callback());
+
+ private ValueTask InvokeOnce()
+ {
+ // Setup/cleanup calls go through InvokeSingle → InvokeOnce, so apply the same SynchronizationContext
+ // suppression the workload loop uses to avoid deadlocking on user awaits that capture the context.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ blockingCallback();
+ return new();
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ unrolledBlockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ blockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+}
+
+[AggressivelyOptimizeMethods]
+public sealed class BenchmarkActionBlockingTask : BenchmarkActionBase
+{
+ private readonly Func> callback;
+ private readonly Action blockingCallback;
+ private readonly Action unrolledBlockingCallback;
+
+ [SetsRequiredMembers]
+ public BenchmarkActionBlockingTask(object? instance, MethodInfo method, int unrollFactor)
+ {
+ callback = CreateWorkload>>(instance, method);
+ blockingCallback = InvokeBlocking;
+ unrolledBlockingCallback = Unroll(blockingCallback, unrollFactor);
+ InvokeSingle = InvokeOnce;
+ InvokeUnroll = WorkloadActionUnroll;
+ InvokeNoUnroll = WorkloadActionNoUnroll;
+ }
+
+ private void InvokeBlocking() => AwaitHelper.GetResult(callback());
+
+ private ValueTask InvokeOnce()
+ {
+ // Setup/cleanup calls go through InvokeSingle → InvokeOnce, so apply the same SynchronizationContext
+ // suppression the workload loop uses to avoid deadlocking on user awaits that capture the context.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ blockingCallback();
+ return new();
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ unrolledBlockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ blockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+}
+
+[AggressivelyOptimizeMethods]
+public sealed class BenchmarkActionBlockingValueTask : BenchmarkActionBase
+{
+ private readonly Func callback;
+ private readonly Action blockingCallback;
+ private readonly Action unrolledBlockingCallback;
+
+ [SetsRequiredMembers]
+ public BenchmarkActionBlockingValueTask(object? instance, MethodInfo method, int unrollFactor)
+ {
+ callback = CreateWorkload>(instance, method);
+ blockingCallback = InvokeBlocking;
+ unrolledBlockingCallback = Unroll(blockingCallback, unrollFactor);
+ InvokeSingle = InvokeOnce;
+ InvokeUnroll = WorkloadActionUnroll;
+ InvokeNoUnroll = WorkloadActionNoUnroll;
+ }
+
+ private void InvokeBlocking() => AwaitHelper.GetResult(callback());
+
+ private ValueTask InvokeOnce()
+ {
+ // Setup/cleanup calls go through InvokeSingle → InvokeOnce, so apply the same SynchronizationContext
+ // suppression the workload loop uses to avoid deadlocking on user awaits that capture the context.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ blockingCallback();
+ return new();
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ unrolledBlockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ blockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+}
+
+[AggressivelyOptimizeMethods]
+public sealed class BenchmarkActionBlockingValueTask : BenchmarkActionBase
+{
+ private readonly Func> callback;
+ private readonly Action blockingCallback;
+ private readonly Action unrolledBlockingCallback;
+
+ [SetsRequiredMembers]
+ public BenchmarkActionBlockingValueTask(object? instance, MethodInfo method, int unrollFactor)
+ {
+ callback = CreateWorkload>>(instance, method);
+ blockingCallback = InvokeBlocking;
+ unrolledBlockingCallback = Unroll(blockingCallback, unrollFactor);
+ InvokeSingle = InvokeOnce;
+ InvokeUnroll = WorkloadActionUnroll;
+ InvokeNoUnroll = WorkloadActionNoUnroll;
+ }
+
+ private void InvokeBlocking() => AwaitHelper.GetResult(callback());
+
+ private ValueTask InvokeOnce()
+ {
+ // Setup/cleanup calls go through InvokeSingle → InvokeOnce, so apply the same SynchronizationContext
+ // suppression the workload loop uses to avoid deadlocking on user awaits that capture the context.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ blockingCallback();
+ return new();
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ unrolledBlockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock)
+ {
+ // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to
+ // continue on the captured context (which is the default Task behavior), and restore it after.
+ SynchronizationContext? originalContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(null);
+ try
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ blockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs
index 28dc146f36..d3af6d0942 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs
@@ -137,10 +137,11 @@ public static async ValueTask RunCore(IHost host, ExecuteParameters parameters,
var target = benchmarkCase.Descriptor;
var job = new Job().Apply(benchmarkCase.Job).Freeze();
int unrollFactor = benchmarkCase.Job.ResolveValue(RunMode.UnrollFactorCharacteristic, EnvironmentResolver.Instance);
+ bool consumeTasksSynchronously = benchmarkCase.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance);
// DONTTOUCH: these should be allocated together
var instance = Activator.CreateInstance(benchmarkCase.Descriptor.Type)!;
- var workloadAction = BenchmarkActionFactory.CreateWorkload(benchmarkActionFactory, target, instance, unrollFactor);
+ var workloadAction = BenchmarkActionFactory.CreateWorkload(benchmarkActionFactory, target, instance, unrollFactor, consumeTasksSynchronously);
var overheadAction = BenchmarkActionFactory.CreateOverhead(benchmarkActionFactory, target, instance, unrollFactor);
var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(benchmarkActionFactory, target, instance);
var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(benchmarkActionFactory, target, instance);
diff --git a/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs b/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs
index 03a0fd09b6..566c155c03 100644
--- a/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs
+++ b/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs
@@ -43,10 +43,32 @@ public class AsyncBenchmarksTests : BenchmarkTestExecutor
{
public AsyncBenchmarksTests(ITestOutputHelper output) : base(output) { }
- [Fact]
- public void TaskReturningMethodsAreAwaited()
+ public static IEnumerable GetToolchains() =>
+ [
+ new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = false }),
+ new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = true }),
+ new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = false }),
+ new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = true }),
+ Job.Default.GetToolchain()
+ ];
+
+ public static TheoryData GetToolchainsWithConsumeTasksSynchronously()
{
- var summary = CanExecute();
+ var data = new TheoryData();
+ foreach (var toolchain in GetToolchains())
+ {
+ data.Add(toolchain, false);
+ data.Add(toolchain, true);
+ }
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetToolchainsWithConsumeTasksSynchronously), DisableDiscoveryEnumeration = true)]
+ public void TaskReturningMethodsAreAwaited(IToolchain toolchain, bool consumeTasksSynchronously)
+ {
+ var summary = CanExecute(CreateSimpleConfig(
+ job: Job.Dry.WithToolchain(toolchain).WithConsumeTasksSynchronously(consumeTasksSynchronously)));
foreach (var report in summary.Reports)
foreach (var measurement in report.AllMeasurements)
@@ -58,29 +80,24 @@ public void TaskReturningMethodsAreAwaited()
}
}
- [Fact]
- public void TaskReturningMethodsAreAwaited_AlreadyComplete() => CanExecute();
-
- public static TheoryData GetToolchains() =>
- [
- new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = false }),
- new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = true }),
- new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = false }),
- new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = true }),
- Job.Default.GetToolchain()
- ];
+ [Theory]
+ [MemberData(nameof(GetToolchainsWithConsumeTasksSynchronously), DisableDiscoveryEnumeration = true)]
+ public void TaskReturningMethodsAreAwaited_AlreadyComplete(IToolchain toolchain, bool consumeTasksSynchronously)
+ => CanExecute(CreateSimpleConfig(
+ job: Job.Dry.WithToolchain(toolchain).WithConsumeTasksSynchronously(consumeTasksSynchronously)));
[Theory]
- [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)]
- public void TaskYieldWithNullSyncContext(IToolchain toolchain)
- => CanExecute(CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)));
+ [MemberData(nameof(GetToolchainsWithConsumeTasksSynchronously), DisableDiscoveryEnumeration = true)]
+ public void TaskYieldWithNullSyncContext(IToolchain toolchain, bool consumeTasksSynchronously)
+ => CanExecute(CreateSimpleConfig(
+ job: Job.Dry.WithToolchain(toolchain).WithConsumeTasksSynchronously(consumeTasksSynchronously)));
// #3103
[Theory]
- [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)]
- public void AsyncWorkloadRestartsAfterMemoryRandomization(IToolchain toolchain)
+ [MemberData(nameof(GetToolchainsWithConsumeTasksSynchronously), DisableDiscoveryEnumeration = true)]
+ public void AsyncWorkloadRestartsAfterMemoryRandomization(IToolchain toolchain, bool consumeTasksSynchronously)
=> CanExecute(CreateSimpleConfig(
- job: Job.Dry.WithToolchain(toolchain).WithIterationCount(3).WithMemoryRandomization(true)));
+ job: Job.Dry.WithToolchain(toolchain).WithIterationCount(3).WithMemoryRandomization(true).WithConsumeTasksSynchronously(consumeTasksSynchronously)));
public class RandomMemoryAsyncBenchmarks
{
diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.cs
index ec85224c36..4f1a3a1283 100644
--- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.cs
+++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.cs
@@ -19,6 +19,18 @@ namespace BenchmarkDotNet.IntegrationTests.InProcess.EmitTests
///
public class RunnableTaskCaseBenchmark
{
+ [GlobalSetup]
+ public async ValueTask GlobalSetup() => await Task.Yield();
+
+ [GlobalCleanup]
+ public async Task GlobalCleanup() => await Task.Yield();
+
+ [IterationSetup]
+ public async ValueTask IterationSetup() => await Task.Yield();
+
+ [IterationCleanup]
+ public async Task IterationCleanup() => await Task.Yield();
+
// ---- Begin TaskCase(Task) ----
[Benchmark]
diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.tt b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.tt
index 761aab422b..26cfc6b2bc 100644
--- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.tt
+++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.tt
@@ -22,6 +22,18 @@ namespace BenchmarkDotNet.IntegrationTests.InProcess.EmitTests
///
public class RunnableTaskCaseBenchmark
{
+ [GlobalSetup]
+ public async ValueTask GlobalSetup() => await Task.Yield();
+
+ [GlobalCleanup]
+ public async Task GlobalCleanup() => await Task.Yield();
+
+ [IterationSetup]
+ public async ValueTask IterationSetup() => await Task.Yield();
+
+ [IterationCleanup]
+ public async Task IterationCleanup() => await Task.Yield();
+
<#
int counter = 1;
EmitTaskCaseBenchmark(ref counter, "Task", "Task.CompletedTask");
diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs
index 1f0e342233..7c6b7bdfb8 100644
--- a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs
+++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs
@@ -32,7 +32,7 @@ private IConfig CreateInProcessConfig(OutputLogger? logger)
.AddColumnProvider(DefaultColumnProviders.Instance);
}
- private IConfig CreateInProcessAndRoslynConfig(OutputLogger logger)
+ private IConfig CreateInProcessAndRoslynConfig(OutputLogger logger, bool consumeTasksSynchronously = false)
{
var config = new ManualConfig()
.AddColumnProvider(DefaultConfig.Instance.GetColumnProviders().ToArray())
@@ -44,12 +44,14 @@ private IConfig CreateInProcessAndRoslynConfig(OutputLogger logger)
Job.Dry
.WithToolchain(InProcessEmitToolchain.Default)
.WithInvocationCount(4)
- .WithUnrollFactor(4))
+ .WithUnrollFactor(4)
+ .WithConsumeTasksSynchronously(consumeTasksSynchronously))
.AddJob(
Job.Dry
.WithToolchain(new RoslynToolchain())
.WithInvocationCount(4)
- .WithUnrollFactor(4))
+ .WithUnrollFactor(4)
+ .WithConsumeTasksSynchronously(consumeTasksSynchronously))
.WithOptions(ConfigOptions.KeepBenchmarkFiles)
.AddLogger(logger ?? (Output != null ? new OutputLogger(Output) : ConsoleLogger.Default));
@@ -103,20 +105,21 @@ public void InProcessBenchmarkSimpleCasesReflectionEmitSupported()
}
[TheoryEnvSpecific("We can't use Roslyn toolchain for .NET Core because we don't know which assemblies to reference and .NET Core does not support dynamic assembly saving", EnvRequirement.FullFrameworkOnly)]
- [InlineData(typeof(SampleBenchmark))]
- [InlineData(typeof(RunnableVoidCaseBenchmark))]
- [InlineData(typeof(RunnableRefStructCaseBenchmark))]
- [InlineData(typeof(RunnableStructCaseBenchmark))]
- [InlineData(typeof(RunnableClassCaseBenchmark))]
- [InlineData(typeof(RunnableManyArgsCaseBenchmark))]
- [InlineData(typeof(RunnableTaskCaseBenchmark))]
- [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableBenchmarks))]
- [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableCallerOverride))]
- [InlineData(typeof(AsyncEnumerableBenchmarksTests.CustomAsyncEnumerableBenchmarks))]
- public void InProcessBenchmarkEmitsSameIL(Type benchmarkType)
+ [InlineData(typeof(SampleBenchmark), false)]
+ [InlineData(typeof(RunnableVoidCaseBenchmark), false)]
+ [InlineData(typeof(RunnableRefStructCaseBenchmark), false)]
+ [InlineData(typeof(RunnableStructCaseBenchmark), false)]
+ [InlineData(typeof(RunnableClassCaseBenchmark), false)]
+ [InlineData(typeof(RunnableManyArgsCaseBenchmark), false)]
+ [InlineData(typeof(RunnableTaskCaseBenchmark), false)]
+ [InlineData(typeof(RunnableTaskCaseBenchmark), true)]
+ [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableBenchmarks), false)]
+ [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableCallerOverride), false)]
+ [InlineData(typeof(AsyncEnumerableBenchmarksTests.CustomAsyncEnumerableBenchmarks), false)]
+ public void InProcessBenchmarkEmitsSameIL(Type benchmarkType, bool consumeTasksSynchronously)
{
var logger = new OutputLogger(Output);
- var config = CreateInProcessAndRoslynConfig(logger);
+ var config = CreateInProcessAndRoslynConfig(logger, consumeTasksSynchronously);
var summary = CanExecute(benchmarkType, config);
diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs
index 8a8313c4fc..20e353cdce 100644
--- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs
+++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs
@@ -748,6 +748,19 @@ public void UsersCanSpecifyEvaluateOverhead()
}
}
+ [Fact]
+ public void UsersCanSpecifyConsumeTasksSynchronously()
+ {
+ var parsedConfiguration = ConfigParser.Parse(["--consumeTasksSynchronously", "true"], new OutputLogger(Output));
+ Assert.NotNull(parsedConfiguration.config);
+ Assert.True(parsedConfiguration.isSuccess);
+
+ foreach (var job in parsedConfiguration.config.GetJobs())
+ {
+ Assert.True(job.Accuracy.ConsumeTasksSynchronously);
+ }
+ }
+
[Fact(Skip = "This should be handled somehow at CommandLineParser level. See https://github.com/commandlineparser/commandline/pull/892")]
public void UserCanSpecifyWasmArgs()
{