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() {