diff --git a/docs/articles/guides/console-args.md b/docs/articles/guides/console-args.md
index 93e4958bf6..542d4edfab 100644
--- a/docs/articles/guides/console-args.md
+++ b/docs/articles/guides/console-args.md
@@ -294,6 +294,7 @@ dotnet run -c Release -- --filter * --runtimes net6.0 net8.0 --statisticalTest 5
* `--maxIterationCount` Maximum number of iterations to run. The default is 100.
* `--invocationCount` Invocation count in a single iteration. By default calculated by the heuristic.
* `--unrollFactor` How many times the benchmark method will be invoked per one iteration of a generated loop. 16 by default
+* `--consumeTasksSynchronously` (Default: false) Specifies whether to consume (Value)Task-returning benchmarks synchronously.
* `--strategy` The RunStrategy that should be used. Throughput/ColdStart/Monitoring.
* `--platform` The Platform that should be used. If not specified, the host process platform is used (default). AnyCpu/X86/X64/Arm/Arm64/LoongArch64.
* `--runOncePerIteration` (Default: false) Run the benchmark exactly once per iteration.
diff --git a/src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs b/src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs
new file mode 100644
index 0000000000..ccca1c9b13
--- /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..e6a2664155 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(RunMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance)
+ && AwaitHelper.IsBuiltInTaskType(method.ReturnType))
+ {
+ return new SyncTaskDeclarationsProvider(benchmark);
+ }
return new AsyncDeclarationsProvider(benchmark, awaitableInfo.ResultType);
}
@@ -406,7 +411,7 @@ private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGen
.GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}")
.GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static)
.Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode }))
- .ConfigureAwait(true);
+ .ConfigureAwait(false);
""";
}
diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
index ba52e0ac4f..107c2bcb6a 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,69 @@ 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.Run.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)
+ {
+ 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};";
- })
- );
+ {{loadArguments}}
+ {{StartClockSyncCode}}
+ while (--invokeCount >= 0)
+ {
+ this.__Overhead({{passArguments}});@Unroll@
+ }
+ {{ReturnSyncCode}}
+ }
- private string GetPassArguments()
- => string.Join(
- ", ",
- Descriptor.WorkloadMethod.GetParameters()
- .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} arg{index}")
- );
+ private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}})
+ {
+ {{loadArguments}}
+ {{StartClockSyncCode}}
+ while (--invokeCount >= 0)
+ {
+ this.__Overhead({{passArguments}});
+ }
+ {{ReturnSyncCode}}
+ }
+
+ private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}})
+ {
+ {{loadArguments}}
+ {{StartClockSyncCode}}
+ while (--invokeCount >= 0)
+ {
+ {{workloadMethodCall}}@Unroll@
+ }
+ {{ReturnSyncCode}}
+ }
+
+ private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}})
+ {
+ {{loadArguments}}
+ {{StartClockSyncCode}}
+ while (--invokeCount >= 0)
+ {
+ {{workloadMethodCall}}
+ }
+ {{ReturnSyncCode}}
+ }
+ """;
+
+ 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/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
index db52b3abc3..8775038b83 100644
--- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
+++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
@@ -459,6 +459,8 @@ private static Job GetBaseJob(CommandLineOptions options, IConfig? globalConfig)
baseJob = baseJob.WithGcForce(false);
if (options.EvaluateOverhead is bool evaluateOverhead)
baseJob = baseJob.WithEvaluateOverhead(evaluateOverhead);
+ if (options.ConsumeTasksSynchronously)
+ baseJob = baseJob.WithConsumeTasksSynchronously(true);
if (options.EnvironmentVariables.Any())
{
diff --git a/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs
index c919f985a1..acc13c3f9f 100644
--- a/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs
+++ b/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs
@@ -2,6 +2,7 @@
using BenchmarkDotNet.Attributes.CompilerServices;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
@@ -36,7 +37,7 @@ public async ValueTask HandleAsync(HostSignal signal, DiagnoserActionParameters
{
foreach (var diagnoser in diagnosers)
{
- await diagnoser.HandleAsync(signal, parameters, cancellationToken).ConfigureAwait(true);
+ await diagnoser.HandleAsync(signal, parameters, cancellationToken).ConfigureAwait();
}
}
@@ -84,7 +85,7 @@ public async ValueTask HandleAsync(BenchmarkSignal signal, CancellationToken can
{
if (router.ShouldHandle(runMode))
{
- await router.handler.HandleAsync(signal, parameters, cancellationToken).ConfigureAwait(true);
+ await router.handler.HandleAsync(signal, parameters, cancellationToken).ConfigureAwait();
}
}
diff --git a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs
index 89e47265ac..0e222adb28 100644
--- a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs
+++ b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs
@@ -1,24 +1,33 @@
+using BenchmarkDotNet.Attributes.CompilerServices;
using JetBrains.Annotations;
using System.ComponentModel;
namespace BenchmarkDotNet.Engines;
-// Used to ensure async continuations are posted back to the same thread that the benchmark process was started on.
+// Used to ensure async continuations are posted back to the thread that started the benchmarks.
[UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Never)]
public readonly ref struct BenchmarkSynchronizationContext : IDisposable
{
- private readonly BenchmarkDotNetSynchronizationContext context;
+ // If this is non-null, we post task continuations to it, otherwise we use task.ConfigureAwait(true) (see AwaitHelper).
+ [ThreadStatic]
+ internal static SingleThreadPumpContext? Current;
- private BenchmarkSynchronizationContext(BenchmarkDotNetSynchronizationContext context)
+ private readonly SingleThreadPumpContext context;
+
+ private BenchmarkSynchronizationContext(SingleThreadPumpContext context)
{
this.context = context;
}
public static BenchmarkSynchronizationContext CreateAndSetCurrent()
{
- var context = new BenchmarkDotNetSynchronizationContext(SynchronizationContext.Current);
- SynchronizationContext.SetSynchronizationContext(context);
+ if (Current is not null)
+ {
+ throw new InvalidOperationException($"{nameof(BenchmarkSynchronizationContext)} is already in use.");
+ }
+ var context = new SingleThreadPumpContext();
+ Current = context;
return new(context);
}
@@ -29,73 +38,97 @@ public T ExecuteUntilComplete(ValueTask valueTask)
=> context.ExecuteUntilComplete(valueTask);
}
-internal sealed class BenchmarkDotNetSynchronizationContext : SynchronizationContext
+// We implement a specialized context that does not inherit from SynchronizationContext, because we never install a SynchronizationContext.Current.
+[AggressivelyOptimizeMethods]
+internal sealed class SingleThreadPumpContext
{
- private readonly SynchronizationContext? previousContext;
+ // Pooled so we don't allocate per await. Carries the continuation to run, and doubles as an intrusive node
+ // for the free list (touched only on the pump thread) and the ready list (written by completing threads).
+ private sealed class Waiter
+ {
+ public readonly Action OnCompleted;
+ public Action? Continuation;
+ public Waiter? Next;
+ private readonly SingleThreadPumpContext owner;
+
+ public Waiter(SingleThreadPumpContext owner)
+ {
+ this.owner = owner;
+ OnCompleted = Complete;
+ }
+
+ private void Complete() => owner.MarkReady(this);
+ }
+
+ private readonly Thread callerThread;
private readonly object syncRoot = new();
- // Use 2 arrays to reduce lock contention while executing. The common case is only 1 callback will be queued at a time.
- private (SendOrPostCallback d, object? state)[]? queue = new (SendOrPostCallback d, object? state)[1];
- private (SendOrPostCallback d, object? state)[]? executing = new (SendOrPostCallback d, object? state)[1];
- private int queueCount = 0;
+ private Waiter? freeWaiters;
+ private Waiter? readyWaiters;
private bool isCompleted;
+ private bool disposed;
+ private int outstanding;
- internal BenchmarkDotNetSynchronizationContext(SynchronizationContext? previousContext)
+ internal SingleThreadPumpContext()
{
- this.previousContext = previousContext;
+ callerThread = Thread.CurrentThread;
}
- public override SynchronizationContext CreateCopy()
- => this;
-
- public override void Post(SendOrPostCallback d, object? state)
+ private void EnsureValid()
{
- if (d is null) throw new ArgumentNullException(nameof(d));
+ if (disposed)
+ throw new ObjectDisposedException(nameof(SingleThreadPumpContext));
+ if (callerThread != Thread.CurrentThread)
+ throw new InvalidOperationException($"{nameof(SingleThreadPumpContext)} can only be used from the thread it was created on.");
+ }
- lock (syncRoot)
+ internal Action GetPassthroughContinuation(Action continuation)
+ {
+ ArgumentNullException.ThrowIfNull(continuation);
+ EnsureValid();
+ var waiter = freeWaiters;
+ if (waiter is null)
{
- ThrowIfDisposed();
-
- int index = queueCount;
- if (++queueCount > queue!.Length)
- {
- Array.Resize(ref queue, queue.Length * 2);
- }
- queue[index] = (d, state);
-
- Monitor.Pulse(syncRoot);
+ waiter = new Waiter(this);
+ }
+ else
+ {
+ freeWaiters = waiter.Next;
}
+ waiter.Continuation = continuation;
+ waiter.Next = null;
+ outstanding++;
+ return waiter.OnCompleted;
}
- private void ThrowIfDisposed() => _ = queue ?? throw new ObjectDisposedException(nameof(BenchmarkDotNetSynchronizationContext));
+ internal void Post(Action continuation) => GetPassthroughContinuation(continuation).Invoke();
- internal void Dispose()
+ private void MarkReady(Waiter waiter)
{
- int count;
- (SendOrPostCallback d, object? state)[] executing;
lock (syncRoot)
{
- ThrowIfDisposed();
-
- // Flush any remaining posted callbacks.
- count = queueCount;
- queueCount = 0;
- executing = queue!;
- queue = null;
+ waiter.Next = readyWaiters;
+ readyWaiters = waiter;
+ Monitor.Pulse(syncRoot);
}
- this.executing = null;
- for (int i = 0; i < count; ++i)
+ }
+
+ internal void Dispose()
+ {
+ EnsureValid();
+ if (outstanding != 0)
{
- executing[i].d(executing[i].state);
- executing[i] = default;
+ throw new InvalidOperationException($"{nameof(SingleThreadPumpContext)} disposed while there are still pending continuations.");
}
- SetSynchronizationContext(previousContext);
+ disposed = true;
+ BenchmarkSynchronizationContext.Current = null;
}
internal T ExecuteUntilComplete(ValueTask valueTask)
{
- ThrowIfDisposed();
+ EnsureValid();
- var awaiter = valueTask.GetAwaiter();
+ // Ensure the continuation is not posted to the current SynchronizationContext.
+ var awaiter = valueTask.ConfigureAwait(false).GetAwaiter();
if (awaiter.IsCompleted)
{
return awaiter.GetResult();
@@ -104,53 +137,29 @@ internal T ExecuteUntilComplete(ValueTask valueTask)
isCompleted = false;
awaiter.UnsafeOnCompleted(OnCompleted);
- var spinner = new SpinWait();
while (true)
{
- int count;
- (SendOrPostCallback d, object? state)[] executing;
+ Waiter waiter;
lock (syncRoot)
{
- if (isCompleted)
+ while (readyWaiters is null && !isCompleted)
{
- return awaiter.GetResult();
+ Monitor.Wait(syncRoot);
}
-
- count = queueCount;
- queueCount = 0;
- executing = queue!;
- queue = this.executing;
-
- if (count == 0)
+ if (readyWaiters is null)
{
- if (spinner.NextSpinWillYield)
- {
- // Yield the thread and wait for completion or for a posted callback.
- // Thread-safety note: isCompleted and queueCount must be checked inside the lock body
- // before calling Monitor.Wait to avoid missing the pulse and waiting forever.
- Monitor.Wait(syncRoot);
- goto ResetAndContinue;
- }
- else
- {
- goto SpinAndContinue;
- }
+ return awaiter.GetResult();
}
+ waiter = readyWaiters;
+ readyWaiters = waiter.Next;
}
- this.executing = executing;
- for (int i = 0; i < count; ++i)
- {
- var (d, state) = executing[i];
- executing[i] = default;
- d(state);
- }
-
- ResetAndContinue:
- spinner = new();
- continue;
- SpinAndContinue:
- spinner.SpinOnce();
+ var continuation = waiter.Continuation!;
+ waiter.Continuation = null;
+ waiter.Next = freeWaiters; // return to the pool before invoking, so a re-suspend can reuse it
+ freeWaiters = waiter;
+ outstanding--;
+ continuation.Invoke();
}
}
diff --git a/src/BenchmarkDotNet/Engines/Engine.cs b/src/BenchmarkDotNet/Engines/Engine.cs
index 489060068c..a2c4e9eb4b 100644
--- a/src/BenchmarkDotNet/Engines/Engine.cs
+++ b/src/BenchmarkDotNet/Engines/Engine.cs
@@ -63,11 +63,11 @@ public async ValueTask RunAsync()
{
Host.CancellationToken.ThrowIfCancellationRequested();
- await Parameters.GlobalSetupAction.Invoke().ConfigureAwait(true);
+ await Parameters.GlobalSetupAction.Invoke().ConfigureAwait();
bool didThrowNonCancelation = false;
try
{
- return await RunCore().ConfigureAwait(true);
+ return await RunCore().ConfigureAwait();
}
catch (Exception e)
{
@@ -78,7 +78,7 @@ public async ValueTask RunAsync()
{
try
{
- await Parameters.GlobalCleanupAction.Invoke().ConfigureAwait(true);
+ await Parameters.GlobalCleanupAction.Invoke().ConfigureAwait();
}
// We only catch if the benchmark threw to not overwrite the exception. #1045
catch (Exception e) when (didThrowNonCancelation && !ExceptionHelper.IsProperCancelation(e, Host.CancellationToken))
@@ -102,12 +102,12 @@ private async Task RunCore()
{
if (stage.Stage == IterationStage.Actual && stage.Mode == IterationMode.Workload)
{
- await Host.BeforeMainRunAsync().ConfigureAwait(true);
- await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeActualRun, Host.CancellationToken).ConfigureAwait(true);
+ await Host.BeforeMainRunAsync().ConfigureAwait();
+ await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeActualRun, Host.CancellationToken).ConfigureAwait();
}
// We need to force an async yield before each stage to ensure each benchmark invocation is called with a constant stack size. #1120
- await Task.Yield();
+ await AwaitHelper.Yield();
Host.CancellationToken.ThrowIfCancellationRequested();
var stageMeasurements = stage.GetMeasurementList();
@@ -122,12 +122,12 @@ private async Task RunCore()
long totalOperations = invokeCount * Parameters.OperationsPerInvoke;
bool randomizeMemory = iterationData.mode == IterationMode.Workload && MemoryRandomization;
- await iterationData.setupAction().ConfigureAwait(true);
+ await iterationData.setupAction().ConfigureAwait();
bool didThrowNonCancelation = false;
ClockSpan clockSpan;
try
{
- await YieldAndThrowIfCancellationRequested().ConfigureAwait(true);
+ await YieldAndThrowIfCancellationRequested().ConfigureAwait();
GcCollect();
@@ -135,10 +135,10 @@ private async Task RunCore()
EngineEventSource.Log.IterationStart(iterationData.mode, iterationData.stage, totalOperations);
clockSpan = randomizeMemory
- ? await MeasureWithRandomStack(iterationData.workloadAction, invokeCount / unrollFactor).ConfigureAwait(true)
- : await iterationData.workloadAction(invokeCount / unrollFactor, Clock).ConfigureAwait(true);
+ ? await MeasureWithRandomStack(iterationData.workloadAction, invokeCount / unrollFactor).ConfigureAwait()
+ : await iterationData.workloadAction(invokeCount / unrollFactor, Clock).ConfigureAwait();
- await YieldAndThrowIfCancellationRequested().ConfigureAwait(true);
+ await YieldAndThrowIfCancellationRequested().ConfigureAwait();
if (EngineEventSource.Log.IsEnabled())
EngineEventSource.Log.IterationStop(iterationData.mode, iterationData.stage, totalOperations);
@@ -152,7 +152,7 @@ private async Task RunCore()
{
try
{
- await iterationData.cleanupAction().ConfigureAwait(true);
+ await iterationData.cleanupAction().ConfigureAwait();
}
// We only catch if the benchmark threw to not overwrite the exception. #1045
catch (Exception e) when (didThrowNonCancelation && !ExceptionHelper.IsProperCancelation(e, Host.CancellationToken))
@@ -160,12 +160,12 @@ private async Task RunCore()
Host.SendError($"Exception during IterationCleanup!{Environment.NewLine}{e}");
}
}
- await YieldAndThrowIfCancellationRequested().ConfigureAwait(true);
+ await YieldAndThrowIfCancellationRequested().ConfigureAwait();
if (randomizeMemory)
{
- await RandomizeManagedHeapMemory().ConfigureAwait(true);
- await YieldAndThrowIfCancellationRequested().ConfigureAwait(true);
+ await RandomizeManagedHeapMemory().ConfigureAwait();
+ await YieldAndThrowIfCancellationRequested().ConfigureAwait();
}
GcCollect();
@@ -183,8 +183,8 @@ private async Task RunCore()
if (stage.Stage == IterationStage.Actual && stage.Mode == IterationMode.Workload)
{
- await Host.AfterMainRunAsync().ConfigureAwait(true);
- await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterActualRun, Host.CancellationToken).ConfigureAwait(true);
+ await Host.AfterMainRunAsync().ConfigureAwait();
+ await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterActualRun, Host.CancellationToken).ConfigureAwait();
}
}
@@ -195,10 +195,10 @@ private async Task RunCore()
DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadInitial());
DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadFinal());
- await extraIterationData.setupAction!().ConfigureAwait(true); // we run iteration setup first, so even if it allocates, it is not included in the results
+ await extraIterationData.setupAction!().ConfigureAwait(); // we run iteration setup first, so even if it allocates, it is not included in the results
- await Host.SendSignalAsync(HostSignal.BeforeExtraIteration).ConfigureAwait(true);
- await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeExtraIteration, Host.CancellationToken).ConfigureAwait(true);
+ await Host.SendSignalAsync(HostSignal.BeforeExtraIteration).ConfigureAwait();
+ await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeExtraIteration, Host.CancellationToken).ConfigureAwait();
// GC collect before measuring allocations.
ForceGcCollect();
@@ -214,15 +214,15 @@ private async Task RunCore()
{
using (FinalizerBlocker.MaybeStart())
{
- (gcStats, clockSpan) = await MeasureWithGc(extraIterationData.workloadAction!, extraIterationData.invokeCount / extraIterationData.unrollFactor).ConfigureAwait(true);
+ (gcStats, clockSpan) = await MeasureWithGc(extraIterationData.workloadAction!, extraIterationData.invokeCount / extraIterationData.unrollFactor).ConfigureAwait();
}
- await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterExtraIteration, Host.CancellationToken).ConfigureAwait(true);
- await Host.SendSignalAsync(HostSignal.AfterExtraIteration).ConfigureAwait(true);
+ await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterExtraIteration, Host.CancellationToken).ConfigureAwait();
+ await Host.SendSignalAsync(HostSignal.AfterExtraIteration).ConfigureAwait();
}
finally
{
- await extraIterationData.cleanupAction!().ConfigureAwait(true); // we run iteration cleanup after diagnosers are complete.
+ await extraIterationData.cleanupAction!().ConfigureAwait(); // we run iteration cleanup after diagnosers are complete.
}
var totalOperations = extraIterationData.invokeCount * Parameters.OperationsPerInvoke;
@@ -283,7 +283,7 @@ internal static void SleepIfPositive(TimeSpan timeSpan)
private async ValueTask RandomizeManagedHeapMemory()
{
// invoke global cleanup before global setup
- await Parameters.GlobalCleanupAction.Invoke().ConfigureAwait(true);
+ await Parameters.GlobalCleanupAction.Invoke().ConfigureAwait();
var gen0object = new byte[random.Next(32)];
var lohObject = new byte[85 * 1024 + random.Next(32)];
diff --git a/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs b/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs
index 675a1f8df8..672041ab05 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(RunMode.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..9f6062af01
--- /dev/null
+++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs
@@ -0,0 +1,332 @@
+using BenchmarkDotNet.Attributes.CompilerServices;
+using BenchmarkDotNet.Engines;
+using JetBrains.Annotations;
+using System.ComponentModel;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace BenchmarkDotNet.Helpers;
+
+[UsedImplicitly]
+[EditorBrowsable(EditorBrowsableState.Never)]
+[AggressivelyOptimizeMethods]
+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<>);
+ }
+
+ internal static YieldAwaiter Yield() => default;
+
+ public static ConfiguredTaskAwaiter ConfigureAwait(this Task task)
+ => new(task);
+
+ public static ConfiguredTaskAwaiter ConfigureAwait(this Task task)
+ => new(task);
+
+ public static ConfiguredValueTaskAwaiter ConfigureAwait(this ValueTask task)
+ => new(task);
+
+ public static ConfiguredValueTaskAwaiter ConfigureAwait(this ValueTask task)
+ => new(task);
+
+ internal static ConfiguredCancelableAsyncEnumerable ConfigureAwait(this IAsyncEnumerable source, CancellationToken cancellationToken = default)
+ => new(source, cancellationToken);
+
+ internal readonly struct YieldAwaiter : ICriticalNotifyCompletion
+ {
+ public YieldAwaiter GetAwaiter() => this;
+ public bool IsCompleted => false;
+ public void GetResult() { }
+
+ public void OnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ context.Post(continuation);
+ }
+ else
+ {
+ Task.Yield().GetAwaiter().OnCompleted(continuation);
+ }
+ }
+
+ public void UnsafeOnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ context.Post(continuation);
+ }
+ else
+ {
+ Task.Yield().GetAwaiter().UnsafeOnCompleted(continuation);
+ }
+ }
+ }
+
+ public readonly struct ConfiguredTaskAwaiter(Task task) : ICriticalNotifyCompletion
+ {
+ public ConfiguredTaskAwaiter GetAwaiter() => this;
+ public bool IsCompleted => task.IsCompleted;
+ public void GetResult() => task.GetAwaiter().GetResult();
+
+ public void OnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ task.ConfigureAwait(false).GetAwaiter().OnCompleted(context.GetPassthroughContinuation(continuation));
+ }
+ else
+ {
+ task.ConfigureAwait(true).GetAwaiter().OnCompleted(continuation);
+ }
+ }
+
+ public void UnsafeOnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ task.ConfigureAwait(false).GetAwaiter().UnsafeOnCompleted(context.GetPassthroughContinuation(continuation));
+ }
+ else
+ {
+ task.ConfigureAwait(true).GetAwaiter().UnsafeOnCompleted(continuation);
+ }
+ }
+ }
+
+ public readonly struct ConfiguredTaskAwaiter(Task task) : ICriticalNotifyCompletion
+ {
+ public ConfiguredTaskAwaiter GetAwaiter() => this;
+ public bool IsCompleted => task.IsCompleted;
+ public TResult GetResult() => task.GetAwaiter().GetResult();
+
+ public void OnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ task.ConfigureAwait(false).GetAwaiter().OnCompleted(context.GetPassthroughContinuation(continuation));
+ }
+ else
+ {
+ task.ConfigureAwait(true).GetAwaiter().OnCompleted(continuation);
+ }
+ }
+
+ public void UnsafeOnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ task.ConfigureAwait(false).GetAwaiter().UnsafeOnCompleted(context.GetPassthroughContinuation(continuation));
+ }
+ else
+ {
+ task.ConfigureAwait(true).GetAwaiter().UnsafeOnCompleted(continuation);
+ }
+ }
+ }
+
+ public readonly struct ConfiguredValueTaskAwaiter(ValueTask valueTask) : ICriticalNotifyCompletion
+ {
+ private readonly ValueTask _valueTask = valueTask;
+ public ConfiguredValueTaskAwaiter GetAwaiter() => this;
+ public bool IsCompleted => _valueTask.IsCompleted;
+ public void GetResult() => _valueTask.GetAwaiter().GetResult();
+
+ public void OnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ _valueTask.ConfigureAwait(false).GetAwaiter().OnCompleted(context.GetPassthroughContinuation(continuation));
+ }
+ else
+ {
+ _valueTask.ConfigureAwait(true).GetAwaiter().OnCompleted(continuation);
+ }
+ }
+
+ public void UnsafeOnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ _valueTask.ConfigureAwait(false).GetAwaiter().UnsafeOnCompleted(context.GetPassthroughContinuation(continuation));
+ }
+ else
+ {
+ _valueTask.ConfigureAwait(true).GetAwaiter().UnsafeOnCompleted(continuation);
+ }
+ }
+ }
+
+ public readonly struct ConfiguredValueTaskAwaiter(ValueTask valueTask) : ICriticalNotifyCompletion
+ {
+ private readonly ValueTask _valueTask = valueTask;
+ public ConfiguredValueTaskAwaiter GetAwaiter() => this;
+ public bool IsCompleted => _valueTask.IsCompleted;
+ public TResult GetResult() => _valueTask.GetAwaiter().GetResult();
+
+ public void OnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ _valueTask.ConfigureAwait(false).GetAwaiter().OnCompleted(context.GetPassthroughContinuation(continuation));
+ }
+ else
+ {
+ _valueTask.ConfigureAwait(true).GetAwaiter().OnCompleted(continuation);
+ }
+ }
+
+ public void UnsafeOnCompleted(Action continuation)
+ {
+ if (BenchmarkSynchronizationContext.Current is { } context)
+ {
+ _valueTask.ConfigureAwait(false).GetAwaiter().UnsafeOnCompleted(context.GetPassthroughContinuation(continuation));
+ }
+ else
+ {
+ _valueTask.ConfigureAwait(true).GetAwaiter().UnsafeOnCompleted(continuation);
+ }
+ }
+ }
+
+ internal readonly struct ConfiguredCancelableAsyncEnumerable
+#if NET9_0_OR_GREATER
+ where T : allows ref struct
+#endif
+ {
+ private readonly IAsyncEnumerable _enumerable;
+ private readonly CancellationToken _cancellationToken;
+
+ internal ConfiguredCancelableAsyncEnumerable(IAsyncEnumerable enumerable, CancellationToken cancellationToken)
+ {
+ _enumerable = enumerable;
+ _cancellationToken = cancellationToken;
+ }
+
+ public Enumerator GetAsyncEnumerator() =>
+ new(_enumerable.GetAsyncEnumerator(_cancellationToken));
+
+ public readonly struct Enumerator
+ {
+ private readonly IAsyncEnumerator _enumerator;
+
+ internal Enumerator(IAsyncEnumerator enumerator)
+ {
+ _enumerator = enumerator;
+ }
+
+ public ConfiguredValueTaskAwaiter MoveNextAsync() =>
+ _enumerator.MoveNextAsync().ConfigureAwait();
+
+ public T Current => _enumerator.Current;
+
+ public ConfiguredValueTaskAwaiter DisposeAsync() =>
+ _enumerator.DisposeAsync().ConfigureAwait();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Jobs/AccuracyMode.cs b/src/BenchmarkDotNet/Jobs/AccuracyMode.cs
index 97b794dbd0..56642b3c65 100644
--- a/src/BenchmarkDotNet/Jobs/AccuracyMode.cs
+++ b/src/BenchmarkDotNet/Jobs/AccuracyMode.cs
@@ -59,8 +59,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
{
diff --git a/src/BenchmarkDotNet/Jobs/JobExtensions.cs b/src/BenchmarkDotNet/Jobs/JobExtensions.cs
index 2842c8d694..058dd63f2e 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.Run.ConsumeTasksSynchronously = value);
+
///
/// Specifies which outliers should be removed from the distribution
///
diff --git a/src/BenchmarkDotNet/Jobs/RunMode.cs b/src/BenchmarkDotNet/Jobs/RunMode.cs
index f15c625907..faeb92b3f4 100644
--- a/src/BenchmarkDotNet/Jobs/RunMode.cs
+++ b/src/BenchmarkDotNet/Jobs/RunMode.cs
@@ -25,6 +25,7 @@ public sealed class RunMode : JobMode
public static readonly Characteristic MinWarmupIterationCountCharacteristic = CreateCharacteristic(nameof(MinWarmupIterationCount));
public static readonly Characteristic MaxWarmupIterationCountCharacteristic = CreateCharacteristic(nameof(MaxWarmupIterationCount));
public static readonly Characteristic MemoryRandomizationCharacteristic = CreateCharacteristic(nameof(MemoryRandomization));
+ public static readonly Characteristic ConsumeTasksSynchronouslyCharacteristic = CreateCharacteristic(nameof(ConsumeTasksSynchronously));
public static readonly RunMode Dry = new RunMode(nameof(Dry))
{
@@ -190,6 +191,19 @@ public bool MemoryRandomization
set => MemoryRandomizationCharacteristic[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;
+ }
+
internal BdnExecution ToPerfonar() => new()
{
LaunchCount = HasValue(LaunchCountCharacteristic) ? LaunchCount : null,
diff --git a/src/BenchmarkDotNet/Loggers/Broker.cs b/src/BenchmarkDotNet/Loggers/Broker.cs
index 00eba964d5..0b8afac93c 100644
--- a/src/BenchmarkDotNet/Loggers/Broker.cs
+++ b/src/BenchmarkDotNet/Loggers/Broker.cs
@@ -1,6 +1,7 @@
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Running;
using System.Diagnostics;
using System.Net.Sockets;
@@ -76,7 +77,7 @@ private async ValueTask ProcessDataCore(CancellationToken cancellationTo
IpcConnection ipcConnection;
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token))
{
- ipcConnection = await ipcListener.AcceptConnection(linkedCts.Token).ConfigureAwait(true);
+ ipcConnection = await ipcListener.AcceptConnection(linkedCts.Token).ConfigureAwait();
}
using var ic = ipcConnection;
@@ -98,7 +99,7 @@ async void Cancel()
while (true)
{
- var line = await ipcConnection.ReadLineAsync(cancellationTokenSource.Token).ConfigureAwait(true);
+ var line = await ipcConnection.ReadLineAsync(cancellationTokenSource.Token).ConfigureAwait();
if (line == null)
return Result.EndOfStream;
@@ -125,7 +126,7 @@ async void Cancel()
var resultsStringBuilder = new StringBuilder();
for (int i = 0; i < resultsLinesCount;)
{
- line = await ipcConnection.ReadLineAsync(cancellationTokenSource.Token).ConfigureAwait(true);
+ line = await ipcConnection.ReadLineAsync(cancellationTokenSource.Token).ConfigureAwait();
if (line == null)
return Result.EndOfStream;
@@ -150,16 +151,16 @@ async void Cancel()
{
try
{
- await Diagnoser.HandleAsync(signal, DiagnoserActionParameters, cancellationToken).ConfigureAwait(true);
+ await Diagnoser.HandleAsync(signal, DiagnoserActionParameters, cancellationToken).ConfigureAwait();
}
// If the benchmark was canceled, the child process may still be waiting for an acknowledgement,
// because it sends the AfterAll signal in a finally block, and we need to allow the process to exit gracefully,
// so we send the acknowledgement always.
finally
{
- using (await writeSemaphore.EnterScopeAsync(CancellationToken.None).ConfigureAwait(true))
+ using (await writeSemaphore.EnterScopeAsync(CancellationToken.None).ConfigureAwait())
{
- await ipcConnection.WriteLineAsync(Engine.Signals.Acknowledgment).ConfigureAwait(true);
+ await ipcConnection.WriteLineAsync(Engine.Signals.Acknowledgment).ConfigureAwait();
}
}
diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
index c306fbec21..fcb16a103e 100644
--- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
+++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
@@ -60,9 +60,9 @@ internal static async ValueTask Run(BenchmarkRunInfo[] benchmarkRunIn
compositeLogger.WriteLineInfo("// Validating benchmarks:");
- var (supportedBenchmarks, validationErrors) = await GetSupportedBenchmarks(benchmarkRunInfos, resolver).ConfigureAwait(true);
+ var (supportedBenchmarks, validationErrors) = await GetSupportedBenchmarks(benchmarkRunInfos, resolver).ConfigureAwait();
- validationErrors.AddRange(await Validate(supportedBenchmarks).ConfigureAwait(true));
+ validationErrors.AddRange(await Validate(supportedBenchmarks).ConfigureAwait());
foreach (var validationError in validationErrors)
eventProcessor.OnValidationError(validationError);
@@ -94,16 +94,18 @@ internal static async ValueTask Run(BenchmarkRunInfo[] benchmarkRunIn
var parallelBuildPartitions = buildPartitions.Except(sequentialBuildPartitions).ToArray();
Dictionary buildResults = parallelBuildPartitions.Length > 0
- ? await BuildInParallel(compositeLogger, rootArtifactsFolderPath, parallelBuildPartitions, globalChronometer, eventProcessor, cancellationToken).ConfigureAwait(true)
+ ? await BuildInParallel(compositeLogger, rootArtifactsFolderPath, parallelBuildPartitions, globalChronometer, eventProcessor, cancellationToken).ConfigureAwait()
: [];
if (sequentialBuildPartitions.Length > 0)
{
+#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
await foreach (var (buildPartition, buildResult) in
- BuildSequential(compositeLogger, rootArtifactsFolderPath, sequentialBuildPartitions, globalChronometer, eventProcessor, cancellationToken).ConfigureAwait(true))
+ BuildSequential(compositeLogger, rootArtifactsFolderPath, sequentialBuildPartitions, globalChronometer, eventProcessor, cancellationToken).ConfigureAwait())
{
buildResults.Add(buildPartition, buildResult);
}
+#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
var allBuildsHaveFailed = buildResults.Values.All(buildResult => !buildResult.IsBuildSuccess);
@@ -137,11 +139,11 @@ internal static async ValueTask Run(BenchmarkRunInfo[] benchmarkRunIn
eventProcessor.OnStartRunBenchmarksInType(benchmarkRunInfo.Type, benchmarkRunInfo.BenchmarksCases);
(var summary, benchmarksToRunCount) = await Run(benchmarkRunInfo, benchmarkToBuildResult, resolver, compositeLogger, eventProcessor, artifactsToCleanup,
resultsFolderPath, logFilePath, totalBenchmarkCount, runsChronometer, benchmarksToRunCount,
- taskbarProgress, cancellationToken).ConfigureAwait(true);
+ taskbarProgress, cancellationToken).ConfigureAwait();
eventProcessor.OnEndRunBenchmarksInType(benchmarkRunInfo.Type, summary);
if (!benchmarkRunInfo.Config.Options.IsSet(ConfigOptions.JoinSummary))
- await PrintSummary(compositeLogger, benchmarkRunInfo.Config, summary, cancellationToken).ConfigureAwait(true);
+ await PrintSummary(compositeLogger, benchmarkRunInfo.Config, summary, cancellationToken).ConfigureAwait();
LogTotalTime(compositeLogger, summary.TotalTime, summary.GetNumberOfExecutedBenchmarks(), message: "Run time");
compositeLogger.WriteLine();
@@ -156,7 +158,7 @@ internal static async ValueTask Run(BenchmarkRunInfo[] benchmarkRunIn
{
var joinedSummary = Summary.Join(results, runsChronometer.GetElapsed());
- await PrintSummary(compositeLogger, supportedBenchmarks.First(b => b.Config.Options.IsSet(ConfigOptions.JoinSummary)).Config, joinedSummary, cancellationToken).ConfigureAwait(true);
+ await PrintSummary(compositeLogger, supportedBenchmarks.First(b => b.Config.Options.IsSet(ConfigOptions.JoinSummary)).Config, joinedSummary, cancellationToken).ConfigureAwait();
results.Clear();
results.Add(joinedSummary);
@@ -236,7 +238,7 @@ internal static async ValueTask Run(BenchmarkRunInfo[] benchmarkRunIn
artifactsToCleanup.AddRange(buildResult.ArtifactsToCleanup);
eventProcessor.OnStartRunBenchmark(benchmark);
- var report = await RunCore(benchmark, info.benchmarkId, logger, resolver, buildResult, benchmarkRunInfo.CompositeInProcessDiagnoser, cancellationToken).ConfigureAwait(true);
+ var report = await RunCore(benchmark, info.benchmarkId, logger, resolver, buildResult, benchmarkRunInfo.CompositeInProcessDiagnoser, cancellationToken).ConfigureAwait();
eventProcessor.OnEndRunBenchmark(benchmark, report);
if (report.AllMeasurements.Any(m => m.Operations == 0))
@@ -324,16 +326,16 @@ private static async ValueTask PrintSummary(ILogger logger, ImmutableConfig conf
logger.WriteLineHeader("// * Export *");
string currentDirectory = Directory.GetCurrentDirectory();
- await config.GetCompositeExporter().ExportAsync(summary, logger, cancellationToken).ConfigureAwait(true);
+ await config.GetCompositeExporter().ExportAsync(summary, logger, cancellationToken).ConfigureAwait();
logger.WriteLine();
logger.WriteLineHeader("// * Detailed results *");
- await BenchmarkReportExporter.ExportToLogAsync(summary, logger, cancellationToken).ConfigureAwait(true);
+ await BenchmarkReportExporter.ExportToLogAsync(summary, logger, cancellationToken).ConfigureAwait();
logger.WriteLineHeader("// * Summary *");
- await ((MarkdownExporter)MarkdownExporter.Console).ExportToLogAsync(summary, logger, cancellationToken).ConfigureAwait(true);
+ await ((MarkdownExporter)MarkdownExporter.Console).ExportToLogAsync(summary, logger, cancellationToken).ConfigureAwait();
// TODO: make exporter
ConclusionHelper.Print(logger, config.GetCompositeAnalyser().Analyse(summary).Distinct().ToList());
@@ -403,7 +405,7 @@ private static async ValueTask> BuildInP
var result = await Task.Run(
async () => await Build(buildPartition, rootArtifactsFolderPath, buildLogger, cancellationToken).ConfigureAwait(false),
cancellationToken
- ).ConfigureAwait(true);
+ ).ConfigureAwait();
// If the generation was successful, but the build was not, we will try building sequentially
// so don't send the OnBuildComplete event yet.
@@ -413,7 +415,7 @@ private static async ValueTask> BuildInP
return (buildPartition, result);
}
- var buildResults = (await Task.WhenAll(buildPartitions.Select(BuildAsync)).ConfigureAwait(true))
+ var buildResults = (await Task.WhenAll(buildPartitions.Select(BuildAsync)).ConfigureAwait())
.ToDictionary(build => build.Partition, build => build.Result);
var afterParallelBuild = globalChronometer.GetElapsed();
@@ -430,7 +432,7 @@ private static async ValueTask> BuildInP
if (buildResults[buildPartition].IsGenerateSuccess && !buildResults[buildPartition].IsBuildSuccess)
{
if (!buildResults[buildPartition].TryToExplainFailureReason(out _))
- buildResults[buildPartition] = await Build(buildPartition, rootArtifactsFolderPath, buildLogger, cancellationToken).ConfigureAwait(true);
+ buildResults[buildPartition] = await Build(buildPartition, rootArtifactsFolderPath, buildLogger, cancellationToken).ConfigureAwait();
eventProcessor.OnBuildComplete(buildPartition, buildResults[buildPartition]);
}
@@ -457,7 +459,7 @@ private static async ValueTask> BuildInP
foreach (var buildPartition in buildPartitions)
{
- var result = await Build(buildPartition, rootArtifactsFolderPath, logger, cancellationToken).ConfigureAwait(true);
+ var result = await Build(buildPartition, rootArtifactsFolderPath, logger, cancellationToken).ConfigureAwait();
eventProcessor.OnBuildComplete(buildPartition, result);
yield return (buildPartition, result);
}
@@ -474,7 +476,7 @@ private static async ValueTask Build(BuildPartition buildPartition,
{
var toolchain = buildPartition.RepresentativeBenchmarkCase.GetToolchain(); // it's guaranteed that all the benchmarks in single partition have same toolchain
- var generateResult = await toolchain.Generator.GenerateProjectAsync(buildPartition, buildLogger, rootArtifactsFolderPath, cancellationToken).ConfigureAwait(true);
+ var generateResult = await toolchain.Generator.GenerateProjectAsync(buildPartition, buildLogger, rootArtifactsFolderPath, cancellationToken).ConfigureAwait();
try
{
@@ -545,7 +547,7 @@ private static async ValueTask RunCore(BenchmarkCase benchmarkC
launchIndex,
useDiagnoser ? Diagnosers.RunMode.NoOverhead : Diagnosers.RunMode.None,
cancellationToken
- ).ConfigureAwait(true);
+ ).ConfigureAwait();
executeResults.Add(executeResult);
@@ -590,7 +592,7 @@ private static async ValueTask RunCore(BenchmarkCase benchmarkC
++launchCount,
Diagnosers.RunMode.ExtraRun,
cancellationToken
- ).ConfigureAwait(true);
+ ).ConfigureAwait();
if (executeResult.IsSuccess)
{
@@ -606,7 +608,7 @@ private static async ValueTask RunCore(BenchmarkCase benchmarkC
logger.WriteLineInfo("// Run, Diagnostic [SeparateLogic]");
await separateLogicCompositeDiagnoser.HandleAsync(HostSignal.SeparateLogic, new DiagnoserActionParameters(null, benchmarkCase, benchmarkId), cancellationToken)
- .ConfigureAwait(true);
+ .ConfigureAwait();
if (compositeInProcessDiagnoser.InProcessDiagnosers.Any(d => d.GetRunMode(benchmarkCase) == Diagnosers.RunMode.SeparateLogic))
{
@@ -622,7 +624,7 @@ private static async ValueTask RunCore(BenchmarkCase benchmarkC
++launchCount,
Diagnosers.RunMode.SeparateLogic,
cancellationToken
- ).ConfigureAwait(true);
+ ).ConfigureAwait();
if (executeResult.IsSuccess)
{
@@ -652,7 +654,7 @@ private static async ValueTask RunExecute(ILogger logger, Benchma
diagnoser,
diagnoserRunMode),
cancellationToken
- ).ConfigureAwait(true);
+ ).ConfigureAwait();
if (!executeResult.IsSuccess)
{
@@ -704,14 +706,14 @@ private static void LogTotalTime(ILogger logger, TimeSpan time, int executedBenc
var errors = await benchmark.GetToolchain()
.ValidateAsync(benchmark, resolver)
.ToArrayAsync()
- .ConfigureAwait(true);
+ .ConfigureAwait();
validationErrors.AddRange(errors);
return !errors.Any(error => error.IsCritical);
})
.ToArrayAsync()
- .ConfigureAwait(true);
+ .ConfigureAwait();
runInfos.Add(
new BenchmarkRunInfo(
diff --git a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
index 26cf3c8e8c..47029d33ac 100644
--- a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
+++ b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
@@ -54,7 +54,7 @@ namespace BenchmarkDotNet.Autogenerated
// the first thing to do is to let diagnosers hook in before anything happens
// so all jit-related diagnosers can catch first jit compilation!
- await global::BenchmarkDotNet.Engines.HostExtensions.BeforeAnythingElseAsync(host).ConfigureAwait(true);
+ await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(global::BenchmarkDotNet.Engines.HostExtensions.BeforeAnythingElseAsync(host));
try
{
diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt
index affe8f2f40..53d97e831b 100644
--- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt
+++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt
@@ -34,7 +34,7 @@
await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.SeparateLogic, host.CancellationToken).ConfigureAwait(false);
return;
}
- await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.BeforeEngine, host.CancellationToken).ConfigureAwait(true);
+ await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.BeforeEngine, host.CancellationToken));
global::BenchmarkDotNet.Engines.EngineParameters engineParameters = new global::BenchmarkDotNet.Engines.EngineParameters()
{
@@ -54,7 +54,7 @@
InProcessDiagnoserHandler = compositeInProcessDiagnoserHandler
};
- global::BenchmarkDotNet.Engines.RunResults results = await new $EngineFactoryType$().Create(engineParameters).RunAsync().ConfigureAwait(true);
+ global::BenchmarkDotNet.Engines.RunResults results = await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(new $EngineFactoryType$().Create(engineParameters).RunAsync());
host.ReportResults(results); // printing costs memory, do this after runs
instance.__TrickTheJIT__(); // compile the method for disassembler, but without actual run of the benchmark ;)
diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs
index 74c2dcee03..e77f93be8f 100644
--- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs
+++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs
@@ -43,7 +43,7 @@ public async ValueTask ExecuteAsync(ExecuteParameters executePara
executeParameters.LaunchIndex,
executeParameters.DiagnoserRunMode,
cancellationToken
- ).ConfigureAwait(true);
+ ).ConfigureAwait();
}
finally
{
@@ -91,11 +91,11 @@ private async ValueTask Execute(BenchmarkCase benchmarkCase,
logger.WriteLineInfo($"// Execute: {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}");
- await diagnoser.HandleAsync(HostSignal.BeforeProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait(true);
+ await diagnoser.HandleAsync(HostSignal.BeforeProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait();
process.Start();
- await diagnoser.HandleAsync(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait(true);
+ await diagnoser.HandleAsync(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait();
processOutputReader.BeginRead();
diff --git a/src/BenchmarkDotNet/Toolchains/Executor.cs b/src/BenchmarkDotNet/Toolchains/Executor.cs
index 98b019e139..3a8d038556 100644
--- a/src/BenchmarkDotNet/Toolchains/Executor.cs
+++ b/src/BenchmarkDotNet/Toolchains/Executor.cs
@@ -37,7 +37,7 @@ private static async ValueTask Execute(BenchmarkCase benchmarkCas
try
{
return await ExecuteCore(benchmarkCase, benchmarkId, logger, artifactsPaths, diagnoser, compositeInProcessDiagnoser, resolver, launchIndex, diagnoserRunMode, cancellationToken)
- .ConfigureAwait(true);
+ .ConfigureAwait();
}
finally
{
@@ -64,7 +64,7 @@ private static async ValueTask ExecuteCore(BenchmarkCase benchmar
{
using Broker broker = new(logger, process, diagnoser, compositeInProcessDiagnoser, benchmarkCase, benchmarkId, tcplistener);
- await diagnoser.HandleAsync(HostSignal.BeforeProcessStart, new DiagnoserActionParameters(process, benchmarkCase, benchmarkId), cancellationToken).ConfigureAwait(true);
+ await diagnoser.HandleAsync(HostSignal.BeforeProcessStart, new DiagnoserActionParameters(process, benchmarkCase, benchmarkId), cancellationToken).ConfigureAwait();
logger.WriteLineInfo($"// Execute: {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}");
@@ -79,7 +79,7 @@ private static async ValueTask ExecuteCore(BenchmarkCase benchmar
return new ExecuteResult(true, null, null, [], [], [], launchIndex);
}
- await broker.Diagnoser.HandleAsync(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait(true);
+ await broker.Diagnoser.HandleAsync(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait();
processOutputReader.BeginRead();
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..7267d909cb 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(RunMode.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..b9a338b07f
--- /dev/null
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs
@@ -0,0 +1,145 @@
+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.Run.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)
+ {
+ // 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 argLocals = argFields.Select(a => ilBuilder.DeclareLocal(a.ArgLocalsType)).ToList();
+ var startedClockLocal = ilBuilder.DeclareLocal(typeof(StartedClock));
+
+ // The workload is consumed synchronously via the blocking AwaitHelper.GetResult. BenchmarkDotNet never
+ // installs a SynchronizationContext, so user awaits resume on the thread pool instead of being posted
+ // back to this (blocked) thread.
+
+ // 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);
+
+ // return 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.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/Emit/InProcessEmitExecutor.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs
index ebccfa2ea0..686c70cbf1 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs
@@ -48,12 +48,12 @@ public async ValueTask ExecuteAsync(ExecuteParameters executePara
runThread.Start();
}
- exitCode = await taskCompletionSource.Task.ConfigureAwait(true);
+ exitCode = await taskCompletionSource.Task.ConfigureAwait();
runThread.Join();
}
else
{
- exitCode = await ExecuteCore(host, executeParameters).ConfigureAwait(true);
+ exitCode = await ExecuteCore(host, executeParameters).ConfigureAwait();
}
host.HandleInProcessDiagnoserResults(executeParameters.BenchmarkCase, executeParameters.CompositeInProcessDiagnoser);
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs
index f44c6da266..4813b111e4 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs
@@ -17,7 +17,7 @@ public static async ValueTask Run(IHost host, ExecuteParameters parameters)
{
// the first thing to do is to let diagnosers hook in before anything happens
// so all jit-related diagnosers can catch first jit compilation!
- await host.BeforeAnythingElseAsync().ConfigureAwait(true);
+ await host.BeforeAnythingElseAsync().ConfigureAwait();
try
{
@@ -25,7 +25,7 @@ public static async ValueTask Run(IHost host, ExecuteParameters parameters)
.GeneratedAssembly
.GetType(EmittedTypePrefix + parameters.BenchmarkId)!;
- await RunCore(runnableType, host, parameters).ConfigureAwait(true);
+ await RunCore(runnableType, host, parameters).ConfigureAwait();
return 0;
}
@@ -87,7 +87,7 @@ private static async ValueTask RunCore(Type runnableType, IHost host, ExecutePar
await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.SeparateLogic, host.CancellationToken).ConfigureAwait(false);
return;
}
- await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeEngine, host.CancellationToken).ConfigureAwait(true);
+ await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeEngine, host.CancellationToken).ConfigureAwait();
var engineParameters = new EngineParameters()
{
@@ -111,7 +111,7 @@ private static async ValueTask RunCore(Type runnableType, IHost host, ExecutePar
.ResolveValue(InfrastructureMode.EngineFactoryCharacteristic, InfrastructureResolver.Instance)!
.Create(engineParameters)
.RunAsync()
- .ConfigureAwait(true);
+ .ConfigureAwait();
host.ReportResults(results);
runnableType.GetMethod(TrickTheJitCoreMethodName)!.Invoke(instance, []);
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs
index a06f11894d..d7a2da9797 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs
@@ -42,6 +42,7 @@ public class InProcessValidator : IValidator
{ RunMode.IterationTimeCharacteristic, DontValidate },
{ RunMode.InvocationCountCharacteristic, DontValidate },
{ RunMode.UnrollFactorCharacteristic, DontValidate },
+ { RunMode.ConsumeTasksSynchronouslyCharacteristic, DontValidate },
{ AccuracyMode.AnalyzeLaunchVarianceCharacteristic, DontValidate },
{ AccuracyMode.EvaluateOverheadCharacteristic, DontValidate },
{ AccuracyMode.MaxRelativeErrorCharacteristic, 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..b1ff1f0f4b 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,207 @@ private async Task WorkloadCore()
public override void Cleanup()
=> workloadValueTaskSource.Complete();
}
+
+// Blocking variants used when Job.Run.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()
+ {
+ blockingCallback();
+ return new();
+ }
+
+ private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock)
+ {
+ // The workload is consumed synchronously via the blocking AwaitHelper.GetResult. BenchmarkDotNet never
+ // installs a SynchronizationContext, so user awaits resume on the thread pool instead of being posted
+ // back to this (blocked) thread.
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ unrolledBlockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+
+ private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock)
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ blockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+}
+
+[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()
+ {
+ blockingCallback();
+ return new();
+ }
+
+ private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock)
+ {
+ // The workload is consumed synchronously via the blocking AwaitHelper.GetResult. BenchmarkDotNet never
+ // installs a SynchronizationContext, so user awaits resume on the thread pool instead of being posted
+ // back to this (blocked) thread.
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ unrolledBlockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+
+ private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock)
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ blockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+}
+
+[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()
+ {
+ blockingCallback();
+ return new();
+ }
+
+ private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock)
+ {
+ // The workload is consumed synchronously via the blocking AwaitHelper.GetResult. BenchmarkDotNet never
+ // installs a SynchronizationContext, so user awaits resume on the thread pool instead of being posted
+ // back to this (blocked) thread.
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ unrolledBlockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+
+ private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock)
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ blockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+}
+
+[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()
+ {
+ blockingCallback();
+ return new();
+ }
+
+ private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock)
+ {
+ // The workload is consumed synchronously via the blocking AwaitHelper.GetResult. BenchmarkDotNet never
+ // installs a SynchronizationContext, so user awaits resume on the thread pool instead of being posted
+ // back to this (blocked) thread.
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ unrolledBlockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+
+ private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock)
+ {
+ var startedClock = clock.Start();
+ while (--invokeCount >= 0)
+ {
+ blockingCallback();
+ }
+ return new ValueTask(startedClock.GetElapsed());
+ }
+}
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs
index 1c32ca2533..0710a6d3ee 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs
@@ -48,12 +48,12 @@ public async ValueTask ExecuteAsync(ExecuteParameters executePara
runThread.Start();
}
- exitCode = await taskCompletionSource.Task.ConfigureAwait(true);
+ exitCode = await taskCompletionSource.Task.ConfigureAwait();
runThread.Join();
}
else
{
- exitCode = await ExecuteCore(host, executeParameters, benchmarkActionFactory).ConfigureAwait(true);
+ exitCode = await ExecuteCore(host, executeParameters, benchmarkActionFactory).ConfigureAwait();
}
host.HandleInProcessDiagnoserResults(executeParameters.BenchmarkCase, executeParameters.CompositeInProcessDiagnoser);
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs
index 28dc146f36..a0f3b1f78d 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs
@@ -22,7 +22,7 @@ public static async ValueTask Run(IHost host, ExecuteParameters parameters,
{
// the first thing to do is to let diagnosers hook in before anything happens
// so all jit-related diagnosers can catch first jit compilation!
- await host.BeforeAnythingElseAsync().ConfigureAwait(true);
+ await host.BeforeAnythingElseAsync().ConfigureAwait();
try
{
@@ -36,7 +36,7 @@ public static async ValueTask Run(IHost host, ExecuteParameters parameters,
var methodInfo = type.GetMethod(nameof(Runnable.RunCore), BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException($"Bug: method {nameof(Runnable.RunCore)} in {inProcessRunnableTypeName} not found.");
- await ((ValueTask)methodInfo.Invoke(null, [host, parameters, benchmarkActionFactory])!).ConfigureAwait(true);
+ await ((ValueTask)methodInfo.Invoke(null, [host, parameters, benchmarkActionFactory])!).ConfigureAwait();
return 0;
}
@@ -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(RunMode.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);
@@ -173,7 +174,7 @@ public static async ValueTask RunCore(IHost host, ExecuteParameters parameters,
await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.SeparateLogic, host.CancellationToken).ConfigureAwait(false);
return;
}
- await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeEngine, host.CancellationToken).ConfigureAwait(true);
+ await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeEngine, host.CancellationToken).ConfigureAwait();
var engineParameters = new EngineParameters
{
@@ -184,7 +185,7 @@ public static async ValueTask RunCore(IHost host, ExecuteParameters parameters,
OverheadActionUnroll = overheadAction.InvokeUnroll,
GlobalSetupAction = async () =>
{
- await globalSetupAction.InvokeSingle().ConfigureAwait(true);
+ await globalSetupAction.InvokeSingle().ConfigureAwait();
workloadAction.Setup();
overheadAction.Setup();
},
@@ -207,7 +208,7 @@ public static async ValueTask RunCore(IHost host, ExecuteParameters parameters,
.ResolveValue(InfrastructureMode.EngineFactoryCharacteristic, InfrastructureResolver.Instance)!
.Create(engineParameters)
.RunAsync()
- .ConfigureAwait(true);
+ .ConfigureAwait();
host.ReportResults(results); // printing costs memory, do this after runs
await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterEngine, host.CancellationToken).ConfigureAwait(false);
diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs
index 848ac60182..ed7fd12c6d 100644
--- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs
+++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs
@@ -135,7 +135,7 @@ private static async ValueTask Execute(BenchmarkCase benchmarkCas
IDiagnoser diagnoser, CompositeInProcessDiagnoser compositeInProcessDiagnoser, IResolver resolver, int launchIndex,
Diagnosers.RunMode diagnoserRunMode, CancellationToken cancellationToken)
{
- using ProcessListener processListener = await CreateProcessListenerAsync(benchmarkCase, benchmarkId, artifactsPaths, resolver, diagnoserRunMode, cancellationToken).ConfigureAwait(true);
+ using ProcessListener processListener = await CreateProcessListenerAsync(benchmarkCase, benchmarkId, artifactsPaths, resolver, diagnoserRunMode, cancellationToken).ConfigureAwait();
try
{
bool isFileBasedIpc = processListener.Listener is FileStdOutListener;
@@ -150,10 +150,10 @@ private static async ValueTask Execute(BenchmarkCase benchmarkCas
((FileStdOutListener)processListener.Listener).AttachProcessOutputReader(processOutputReader);
}
- await diagnoser.HandleAsync(HostSignal.BeforeProcessStart, new DiagnoserActionParameters(processListener.Process, benchmarkCase, benchmarkId), cancellationToken).ConfigureAwait(true);
+ await diagnoser.HandleAsync(HostSignal.BeforeProcessStart, new DiagnoserActionParameters(processListener.Process, benchmarkCase, benchmarkId), cancellationToken).ConfigureAwait();
return await Execute(processListener.Process, benchmarkCase, processOutputReader,
benchmarkId, logger, launchIndex, diagnoser,
- compositeInProcessDiagnoser, processListener.Listener, cancellationToken).ConfigureAwait(true);
+ compositeInProcessDiagnoser, processListener.Listener, cancellationToken).ConfigureAwait();
}
finally
{
@@ -195,11 +195,11 @@ private static async ValueTask Execute(Process process, Benchmark
logger.WriteLineInfo($"// Execute: {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}");
- await diagnoser.HandleAsync(HostSignal.BeforeProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait(true);
+ await diagnoser.HandleAsync(HostSignal.BeforeProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait();
process.Start();
- await diagnoser.HandleAsync(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait(true);
+ await diagnoser.HandleAsync(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters, cancellationToken).ConfigureAwait();
processOutputReader.BeginRead();
diff --git a/src/BenchmarkDotNet/Validators/ExecutionValidator.cs b/src/BenchmarkDotNet/Validators/ExecutionValidator.cs
index fe2ec98cae..2ec6c19e6f 100644
--- a/src/BenchmarkDotNet/Validators/ExecutionValidator.cs
+++ b/src/BenchmarkDotNet/Validators/ExecutionValidator.cs
@@ -28,14 +28,14 @@ protected override async IAsyncEnumerable ValidateAsyncCore(Val
{
continue;
}
- if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalSetupMethod, errors, cancellationToken).ConfigureAwait(true))
+ if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalSetupMethod, errors, cancellationToken).ConfigureAwait())
{
- if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationSetupMethod, errors, cancellationToken).ConfigureAwait(true))
+ if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationSetupMethod, errors, cancellationToken).ConfigureAwait())
{
- await ExecuteBenchmarkAsync(benchmarkTypeInstance, benchmark, args, errors, cancellationToken).ConfigureAwait(true);
- await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationCleanupMethod, errors, cancellationToken).ConfigureAwait(true);
+ await ExecuteBenchmarkAsync(benchmarkTypeInstance, benchmark, args, errors, cancellationToken).ConfigureAwait();
+ await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationCleanupMethod, errors, cancellationToken).ConfigureAwait();
}
- await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalCleanupMethod, errors, cancellationToken).ConfigureAwait(true);
+ await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalCleanupMethod, errors, cancellationToken).ConfigureAwait();
}
}
diff --git a/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs b/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs
index 6401e0f9dc..6d54901dbd 100644
--- a/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs
+++ b/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs
@@ -35,18 +35,18 @@ protected override async IAsyncEnumerable ValidateAsyncCore(Val
{
continue;
}
- if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalSetupMethod, errors, cancellationToken).ConfigureAwait(true))
+ if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalSetupMethod, errors, cancellationToken).ConfigureAwait())
{
- if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationSetupMethod, errors, cancellationToken).ConfigureAwait(true))
+ if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationSetupMethod, errors, cancellationToken).ConfigureAwait())
{
- var (hasResult, result) = await ExecuteBenchmarkAsync(benchmarkTypeInstance, benchmark, args, errors, cancellationToken).ConfigureAwait(true);
+ var (hasResult, result) = await ExecuteBenchmarkAsync(benchmarkTypeInstance, benchmark, args, errors, cancellationToken).ConfigureAwait();
if (hasResult)
{
results.Add((benchmark, result));
}
- await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationCleanupMethod, errors, cancellationToken).ConfigureAwait(true);
+ await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationCleanupMethod, errors, cancellationToken).ConfigureAwait();
}
- await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalCleanupMethod, errors, cancellationToken).ConfigureAwait(true);
+ await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalCleanupMethod, errors, cancellationToken).ConfigureAwait();
}
}
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..09832a6c8d 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.Run.ConsumeTasksSynchronously);
+ }
+ }
+
[Fact(Skip = "This should be handled somehow at CommandLineParser level. See https://github.com/commandlineparser/commandline/pull/892")]
public void UserCanSpecifyWasmArgs()
{