From 8561687993308d2997ea5b6f3df5a83a12b5a9f3 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 23 May 2026 05:27:54 -0400 Subject: [PATCH 1/7] Add config option to restore sync-over-async task consumption behavior. --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 5 + .../Code/DeclarationsProvider.cs | 166 ++++++++- .../ConsoleArguments/CommandLineOptions.cs | 3 + .../Environments/EnvironmentResolver.cs | 1 + src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 119 +++++++ src/BenchmarkDotNet/Jobs/AccuracyMode.cs | 18 +- src/BenchmarkDotNet/Jobs/JobExtensions.cs | 3 + .../Emitters/RunnableEmitter.cs | 13 +- .../Emitters/SetupCleanupEmitter.cs | 94 ++++- .../Emitters/SyncTaskCoreEmitter.cs | 158 +++++++++ .../InProcess/InProcessValidator.cs | 1 + .../NoEmit/BenchmarkActionFactory.cs | 40 ++- .../InProcess/NoEmit/BenchmarkActionImpl.cs | 325 ++++++++++++++++++ .../InProcess/NoEmit/InProcessNoEmitRunner.cs | 11 +- .../AsyncBenchmarksTests.cs | 57 +-- .../RunnableTaskCaseBenchmark.cs | 12 + .../RunnableTaskCaseBenchmark.tt | 12 + .../InProcessEmitTest.cs | 33 +- .../ConfigParserTests.cs | 13 + 19 files changed, 1006 insertions(+), 78 deletions(-) create mode 100644 src/BenchmarkDotNet/Helpers/AwaitHelper.cs create mode 100644 src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 418fcfd637..8cf9f6c784 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -115,6 +115,11 @@ private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchm if (method.ReturnType.IsAwaitable(out var awaitableInfo)) { + if (benchmark.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance) + && AwaitHelper.IsBuiltInTaskType(method.ReturnType)) + { + return new SyncTaskDeclarationsProvider(benchmark); + } return new AsyncDeclarationsProvider(benchmark, awaitableInfo.ResultType); } diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index ba52e0ac4f..c495bbe89f 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; @@ -49,9 +50,19 @@ private void Replace(SmartStringBuilder smartStringBuilder, MethodInfo? method, } else if (method.ReturnType.IsAwaitable(out _)) { - modifier = "async"; - userImpl = $"await {GetMethodPrefix(method)}.{method.Name}();"; - needsExplicitReturn = false; + if (Benchmark.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance) + && AwaitHelper.IsBuiltInTaskType(method.ReturnType)) + { + modifier = string.Empty; + userImpl = GetSyncTaskSetupCleanupImpl(method); + needsExplicitReturn = true; + } + else + { + modifier = "async"; + userImpl = $"await {GetMethodPrefix(method)}.{method.Name}();"; + needsExplicitReturn = false; + } } else { @@ -81,6 +92,23 @@ private static string CombineLines(params string[] parts) protected virtual string GetExtraGlobalSetupImpl() => string.Empty; protected virtual string GetExtraGlobalCleanupImpl() => string.Empty; + private string GetSyncTaskSetupCleanupImpl(MethodInfo method) + { + string syncContextType = typeof(SynchronizationContext).GetCorrectCSharpTypeName(); + return $$""" + {{syncContextType}} originalContext = {{syncContextType}}.Current; + {{syncContextType}}.SetSynchronizationContext(null); + try + { + {{typeof(AwaitHelper).GetCorrectCSharpTypeName()}}.{{nameof(AwaitHelper.GetResult)}}({{GetMethodPrefix(method)}}.{{method.Name}}()); + } + finally + { + {{syncContextType}}.SetSynchronizationContext(originalContext); + } + """; + } + protected abstract SmartStringBuilder ReplaceCore(SmartStringBuilder smartStringBuilder); private static string GetMethodPrefix(MethodInfo method) @@ -89,6 +117,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 +213,108 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString return smartStringBuilder .Replace("$CoreImpl$", coreImpl); } + } - private string GetLoadArguments() - => string.Join( - Environment.NewLine, - Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => + // Used when Job.Accuracy.ConsumeTasksSynchronously is enabled for (Value)Task()-returning workloads. + // Generates a synchronous loop that blocks on the returned task via AwaitHelper.GetResult, matching the + // pre-async-refactor behavior so historical results stay comparable. + internal sealed class SyncTaskDeclarationsProvider(BenchmarkCase benchmark) : DeclarationsProvider(benchmark) + { + public override string[] GetExtraFields() => []; + + protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartStringBuilder) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + string synchronizationContextTypeName = typeof(SynchronizationContext).GetCorrectCSharpTypeName(); + string loadArguments = GetLoadArguments(); + string passArguments = GetPassArguments(); + string workloadMethodCall = $"global::{typeof(AwaitHelper).FullName}.{nameof(AwaitHelper.GetResult)}({GetWorkloadMethodCall(passArguments).TrimEnd(';')});"; + string coreImpl = $$""" + private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}}) { - var refModifier = parameter.ParameterType.IsByRef ? "ref" : string.Empty; - return $"{refModifier} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {refModifier} this.__fieldsContainer.argField{index};"; - }) - ); + {{synchronizationContextTypeName}} originalContext = {{synchronizationContextTypeName}}.Current; + {{synchronizationContextTypeName}}.SetSynchronizationContext(null); + try + { + {{loadArguments}} + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + this.__Overhead({{passArguments}});@Unroll@ + } + {{ReturnSyncCode}} + } + finally + { + {{synchronizationContextTypeName}}.SetSynchronizationContext(originalContext); + } + } - private string GetPassArguments() - => string.Join( - ", ", - Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} arg{index}") - ); + private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}}) + { + {{synchronizationContextTypeName}} originalContext = {{synchronizationContextTypeName}}.Current; + {{synchronizationContextTypeName}}.SetSynchronizationContext(null); + try + { + {{loadArguments}} + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + this.__Overhead({{passArguments}}); + } + {{ReturnSyncCode}} + } + finally + { + {{synchronizationContextTypeName}}.SetSynchronizationContext(originalContext); + } + } + + private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}}) + { + {{synchronizationContextTypeName}} originalContext = {{synchronizationContextTypeName}}.Current; + {{synchronizationContextTypeName}}.SetSynchronizationContext(null); + try + { + {{loadArguments}} + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + {{workloadMethodCall}}@Unroll@ + } + {{ReturnSyncCode}} + } + finally + { + {{synchronizationContextTypeName}}.SetSynchronizationContext(originalContext); + } + } + + private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}}) + { + {{synchronizationContextTypeName}} originalContext = {{synchronizationContextTypeName}}.Current; + {{synchronizationContextTypeName}}.SetSynchronizationContext(null); + try + { + {{loadArguments}} + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + {{workloadMethodCall}} + } + {{ReturnSyncCode}} + } + finally + { + {{synchronizationContextTypeName}}.SetSynchronizationContext(originalContext); + } + } + """; + + return smartStringBuilder + .Replace("$CoreImpl$", coreImpl); + } } internal abstract class AsyncDeclarationsProviderBase(BenchmarkCase benchmark) : DeclarationsProvider(benchmark) diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index 543e1306ac..89279e5faa 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -228,6 +228,9 @@ public bool UseDisassemblyDiagnoser [Option("evaluateOverhead", Required = false, HelpText = "Specifies whether to run and evaluate overhead iterations.")] public bool? EvaluateOverhead { get; set; } + [Option("consumeTasksSynchronously", Required = false, Default = false, HelpText = "Specifies whether to consume (Value)Task-returning benchmarks synchronously.")] + public bool ConsumeTasksSynchronously { get; set; } + [Option("resume", Required = false, Default = false, HelpText = "Continue the execution if the last run was stopped.")] public bool Resume { get; set; } diff --git a/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs b/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs index 675a1f8df8..48b96df4a1 100644 --- a/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs +++ b/src/BenchmarkDotNet/Environments/EnvironmentResolver.cs @@ -23,6 +23,7 @@ private EnvironmentResolver() // TODO: find a better place Register(AccuracyMode.AnalyzeLaunchVarianceCharacteristic, () => false); + Register(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, () => false); Register(RunMode.UnrollFactorCharacteristic, job => { // TODO: move it to another place and use the main resolver diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs new file mode 100644 index 0000000000..ea5d9641d7 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -0,0 +1,119 @@ +using JetBrains.Annotations; +using System.ComponentModel; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace BenchmarkDotNet.Helpers; + +[UsedImplicitly] +[EditorBrowsable(EditorBrowsableState.Never)] +public static class AwaitHelper +{ + private class ValueTaskWaiter + { + // We use thread static field so that each thread uses its own individual callback and reset event. + [ThreadStatic] + private static ValueTaskWaiter? ts_current; + internal static ValueTaskWaiter Current => ts_current ??= new ValueTaskWaiter(); + + // We cache the callback to prevent allocations for memory diagnoser. + private readonly Action awaiterCallback; + private readonly ManualResetEventSlim resetEvent; + + private ValueTaskWaiter() + { + resetEvent = new(); + awaiterCallback = resetEvent.Set; + } + + internal void Wait(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion + { + resetEvent.Reset(); + awaiter.UnsafeOnCompleted(awaiterCallback); + + // The fastest way to wait for completion is to spin a bit before waiting on the event. This is the same logic that Task.GetAwaiter().GetResult() uses. + var spinner = new SpinWait(); + while (!resetEvent.IsSet) + { + if (spinner.NextSpinWillYield) + { + resetEvent.Wait(); + return; + } + spinner.SpinOnce(); + } + } + } + + // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way, + // and will eventually throw actual exception, not aggregated one + public static void GetResult(Task task) => task.GetAwaiter().GetResult(); + + public static T GetResult(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(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(ValueTask task) + { + // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + if (!awaiter.IsCompleted) + { + ValueTaskWaiter.Current.Wait(awaiter); + } + return awaiter.GetResult(); + } + + internal static MethodInfo? GetGetResultMethod(Type taskType) + { + if (!taskType.IsGenericType) + { + return typeof(AwaitHelper).GetMethod(nameof(GetResult), BindingFlags.Public | BindingFlags.Static, null, [taskType], null)!; + } + var genericTypeDefinition = taskType.GetGenericTypeDefinition(); + Type? compareType = genericTypeDefinition == typeof(ValueTask<>) ? typeof(ValueTask<>) + : genericTypeDefinition == typeof(Task<>) ? typeof(Task<>) + : null; + if (compareType == null) + { + return null; + } + var resultType = taskType + .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)! + .ReturnType + .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance)! + .ReturnType; + return typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static) + .First(m => + { + if (m.Name != nameof(GetResult)) + return false; + Type paramType = m.GetParameters().First().ParameterType; + return paramType.IsGenericType && paramType.GetGenericTypeDefinition() == compareType; + }) + .MakeGenericMethod([resultType]); + } + + internal static bool IsBuiltInTaskType(Type type) + { + if (!type.IsGenericType) + { + return type == typeof(ValueTask) || type == typeof(Task); + } + var genericTypeDefinition = type.GetGenericTypeDefinition(); + return genericTypeDefinition == typeof(ValueTask<>) + || genericTypeDefinition == typeof(Task<>); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Jobs/AccuracyMode.cs b/src/BenchmarkDotNet/Jobs/AccuracyMode.cs index 97b794dbd0..3b11150d15 100644 --- a/src/BenchmarkDotNet/Jobs/AccuracyMode.cs +++ b/src/BenchmarkDotNet/Jobs/AccuracyMode.cs @@ -13,6 +13,7 @@ public sealed class AccuracyMode : JobMode public static readonly Characteristic MinIterationTimeCharacteristic = CreateCharacteristic(nameof(MinIterationTime)); public static readonly Characteristic MinInvokeCountCharacteristic = CreateCharacteristic(nameof(MinInvokeCount)); public static readonly Characteristic EvaluateOverheadCharacteristic = CreateCharacteristic(nameof(EvaluateOverhead)); + public static readonly Characteristic ConsumeTasksSynchronouslyCharacteristic = CreateCharacteristic(nameof(ConsumeTasksSynchronously)); public static readonly Characteristic OutlierModeCharacteristic = CreateCharacteristic(nameof(OutlierMode)); public static readonly Characteristic AnalyzeLaunchVarianceCharacteristic = CreateCharacteristic(nameof(AnalyzeLaunchVariance)); @@ -59,8 +60,8 @@ public int MinInvokeCount } /// - /// Specifies if the overhead should be evaluated (Idle runs) and it's average value subtracted from every result. - /// True by default, very important for nano-benchmarks. + /// Specifies if the overhead should be evaluated (Idle runs) and its average value subtracted from every result. + /// False by default. /// public bool EvaluateOverhead { @@ -68,6 +69,19 @@ public bool EvaluateOverhead set => EvaluateOverheadCharacteristic[this] = value; } + /// + /// Specifies whether (Value)Task-returning benchmarks should be consumed synchronously. + /// False by default. + /// + /// + /// Intended to make async benchmark results comparable to historical results obtained from older BenchmarkDotNet versions. Recommended to leave false for new benchmarks. + /// + public bool ConsumeTasksSynchronously + { + get => ConsumeTasksSynchronouslyCharacteristic[this]; + set => ConsumeTasksSynchronouslyCharacteristic[this] = value; + } + /// /// Specifies which outliers should be removed from the distribution. /// diff --git a/src/BenchmarkDotNet/Jobs/JobExtensions.cs b/src/BenchmarkDotNet/Jobs/JobExtensions.cs index 2842c8d694..9099e15ed5 100644 --- a/src/BenchmarkDotNet/Jobs/JobExtensions.cs +++ b/src/BenchmarkDotNet/Jobs/JobExtensions.cs @@ -299,6 +299,9 @@ public static Job WithMsBuildArguments(this Job job, params string[] msBuildArgu /// public static Job WithEvaluateOverhead(this Job job, bool value) => job.WithCore(j => j.Accuracy.EvaluateOverhead = value); + /// + public static Job WithConsumeTasksSynchronously(this Job job, bool value) => job.WithCore(j => j.Accuracy.ConsumeTasksSynchronously = value); + /// /// Specifies which outliers should be removed from the distribution /// diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs index 4c15ba95e6..b2fcd89257 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Helpers.Reflection.Emit; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; @@ -72,7 +73,17 @@ public static Assembly EmitPartitionAssembly(GenerateResult generateResult, Buil var returnType = benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType; RunnableEmitter runnableEmitter; if (returnType.IsAwaitable(out var awaitableInfo)) - runnableEmitter = new AsyncCoreEmitter(buildPartition, moduleBuilder, benchmark, awaitableInfo); + { + if (benchmark.BenchmarkCase.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, buildPartition.Resolver) + && AwaitHelper.IsBuiltInTaskType(returnType)) + { + runnableEmitter = new SyncTaskCoreEmitter(buildPartition, moduleBuilder, benchmark); + } + else + { + runnableEmitter = new AsyncCoreEmitter(buildPartition, moduleBuilder, benchmark, awaitableInfo); + } + } else if (returnType.IsAsyncEnumerable(out var asyncEnumerableInfo)) runnableEmitter = new AsyncEnumerableCoreEmitter(buildPartition, moduleBuilder, benchmark, asyncEnumerableInfo); else diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs index 32a549972f..cac066b13f 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs @@ -1,5 +1,8 @@ +using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Helpers.Reflection.Emit; +using BenchmarkDotNet.Jobs; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; @@ -14,7 +17,18 @@ private void EmitSetupCleanup(string methodName, MethodInfo? methodToCall, Setup { if (methodToCall?.ReturnType.IsAwaitable(out _) == true) { - EmitAsyncSetupCleanup(methodName, methodToCall, kind); + // The sync-task-consumption decision is per-method: a built-in (Value)Task() setup/cleanup + // gets the blocking path whenever ConsumeTasksSynchronously is set, regardless of which core + // emitter was chosen for the workload. This mirrors the source-gen base Replace logic. + if (AwaitHelper.IsBuiltInTaskType(methodToCall.ReturnType) + && benchmark.BenchmarkCase.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance)) + { + EmitBlockingSetupCleanup(methodName, methodToCall, kind); + } + else + { + EmitAsyncSetupCleanup(methodName, methodToCall, kind); + } } else { @@ -71,6 +85,84 @@ [0] valuetype [System.Runtime]System.Threading.Tasks.ValueTask private void EmitAsyncSetupCleanup(string methodName, MethodInfo methodToCall, SetupCleanupKind kind) => EmitAsyncSingleCall(methodName, typeof(AsyncValueTaskMethodBuilder), methodToCall, kind); + // Mirrors SyncTaskSetupCleanupImpl in source-gen: capture+null SynchronizationContext, then try { AwaitHelper.GetResult(base.X()) } finally { restore }, then return new ValueTask(). + private void EmitBlockingSetupCleanup(string methodName, MethodInfo methodToCall, SetupCleanupKind kind) + { + var getResultMethod = AwaitHelper.GetGetResultMethod(methodToCall.ReturnType) + ?? throw new InvalidOperationException( + $"AwaitHelper.GetResult is not available for setup/cleanup return type {methodToCall.ReturnType.GetDisplayName()}. ConsumeTasksSynchronously only supports (Value)Task()."); + + var methodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)) + ) + .SetAggressiveOptimizationImplementationFlag(); + var ilBuilder = methodBuilder.GetILGenerator(); + + // Local order matches Roslyn's source-declaration order: originalContext (outer source declaration), + // then the compiler-generated ValueTask temp used for `return new ValueTask();`. + var originalContextLocal = ilBuilder.DeclareLocal(typeof(SynchronizationContext)); + var valueTaskLocal = ilBuilder.DeclareLocal(typeof(ValueTask)); + + if (kind == SetupCleanupKind.GlobalCleanup) + { + EmitExtraGlobalCleanup(ilBuilder, null); + } + + // SynchronizationContext originalContext = SynchronizationContext.Current; + ilBuilder.Emit(OpCodes.Call, SynchronizationContextGetCurrentMethod); + ilBuilder.EmitStloc(originalContextLocal); + // SynchronizationContext.SetSynchronizationContext(null); + ilBuilder.Emit(OpCodes.Ldnull); + ilBuilder.Emit(OpCodes.Call, SynchronizationContextSetCurrentMethod); + + var endLabel = ilBuilder.DefineLabel(); + ilBuilder.BeginExceptionBlock(); + { + if (!methodToCall.IsStatic) + { + ilBuilder.Emit(OpCodes.Ldarg_0); + } + ilBuilder.Emit(OpCodes.Call, methodToCall); + // AwaitHelper.GetResult() + ilBuilder.Emit(OpCodes.Call, getResultMethod); + // Generic Task/ValueTask overloads return T — discard it. + if (getResultMethod.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + ilBuilder.Emit(OpCodes.Leave, endLabel); + } + ilBuilder.BeginFinallyBlock(); + { + // SynchronizationContext.SetSynchronizationContext(originalContext); + ilBuilder.EmitLdloc(originalContextLocal); + ilBuilder.Emit(OpCodes.Call, SynchronizationContextSetCurrentMethod); + } + ilBuilder.EndExceptionBlock(); + + ilBuilder.MarkLabel(endLabel); + + if (kind == SetupCleanupKind.GlobalSetup) + { + EmitExtraGlobalSetup(ilBuilder, null); + } + + // return new ValueTask(); + ilBuilder.EmitLdloca(valueTaskLocal); + ilBuilder.Emit(OpCodes.Initobj, typeof(ValueTask)); + ilBuilder.EmitLdloc(valueTaskLocal); + ilBuilder.Emit(OpCodes.Ret); + } + + private static readonly MethodInfo SynchronizationContextGetCurrentMethod = + typeof(SynchronizationContext).GetProperty(nameof(SynchronizationContext.Current), BindingFlags.Public | BindingFlags.Static)!.GetMethod!; + + private static readonly MethodInfo SynchronizationContextSetCurrentMethod = + typeof(SynchronizationContext).GetMethod(nameof(SynchronizationContext.SetSynchronizationContext), BindingFlags.Public | BindingFlags.Static, null, [typeof(SynchronizationContext)], null)!; + protected virtual void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { } protected virtual void EmitExtraGlobalSetup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { } } \ No newline at end of file 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..e6f6150c5c --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs @@ -0,0 +1,158 @@ +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Helpers.Reflection.Emit; +using BenchmarkDotNet.Running; +using Perfolizer.Horology; +using System.Reflection; +using System.Reflection.Emit; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; + +namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; + +partial class RunnableEmitter +{ + // Used when Job.Accuracy.ConsumeTasksSynchronously is enabled for (Value)Task()-returning workloads. + // Emits the same shape as SyncCoreEmitter but routes the workload return value through AwaitHelper.GetResult + // so the iteration loop stays synchronous, matching the pre-async-refactor behavior so historical results stay comparable. + private sealed class SyncTaskCoreEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder, BenchmarkBuildInfo benchmark) : RunnableEmitter(buildPartition, moduleBuilder, benchmark) + { + private static readonly MethodInfo SynchronizationContextGetCurrent = + typeof(SynchronizationContext).GetProperty(nameof(SynchronizationContext.Current), BindingFlags.Public | BindingFlags.Static)!.GetMethod!; + private static readonly MethodInfo SynchronizationContextSetCurrent = + typeof(SynchronizationContext).GetMethod(nameof(SynchronizationContext.SetSynchronizationContext), BindingFlags.Public | BindingFlags.Static, null, [typeof(SynchronizationContext)], null)!; + + protected override void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { } + + protected override void EmitCoreImpl() + { + EmitAction(OverheadActionUnrollMethodName, overheadImplementationMethod, jobUnrollFactor, isWorkload: false); + EmitAction(OverheadActionNoUnrollMethodName, overheadImplementationMethod, 1, isWorkload: false); + EmitAction(WorkloadActionUnrollMethodName, Descriptor.WorkloadMethod, jobUnrollFactor, isWorkload: true); + EmitAction(WorkloadActionNoUnrollMethodName, Descriptor.WorkloadMethod, 1, isWorkload: true); + } + + private MethodBuilder EmitAction(string methodName, MethodInfo methodToCall, int unrollFactor, bool isWorkload) + { + MethodInfo? getResultMethod = null; + if (isWorkload) + { + getResultMethod = AwaitHelper.GetGetResultMethod(methodToCall.ReturnType) + ?? throw new InvalidOperationException( + $"AwaitHelper.GetResult is not available for workload return type {methodToCall.ReturnType.GetDisplayName()}. ConsumeTasksSynchronously only supports (Value)Task()."); + } + + var invokeCountArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var actionMethodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + [ + invokeCountArg, + new EmitParameterInfo(1, ClockParamName, typeof(IClock)) + ] + ) + .SetAggressiveOptimizationImplementationFlag(); + invokeCountArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + var originalContextLocal = ilBuilder.DeclareLocal(typeof(SynchronizationContext)); + var argLocals = argFields.Select(a => ilBuilder.DeclareLocal(a.ArgLocalsType)).ToList(); + var startedClockLocal = ilBuilder.DeclareLocal(typeof(StartedClock)); + var resultLocal = ilBuilder.DeclareLocal(typeof(ValueTask)); + var endLabel = ilBuilder.DefineLabel(); + + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + // SynchronizationContext originalContext = SynchronizationContext.Current; + ilBuilder.Emit(OpCodes.Call, SynchronizationContextGetCurrent); + ilBuilder.EmitStloc(originalContextLocal); + // SynchronizationContext.SetSynchronizationContext(null); + ilBuilder.Emit(OpCodes.Ldnull); + ilBuilder.Emit(OpCodes.Call, SynchronizationContextSetCurrent); + + ilBuilder.BeginExceptionBlock(); + { + // load fields + EmitLoadArgFieldsToLocals(ilBuilder, argLocals); + + // StartedClock startedClock = ClockExtensions.Start(clock); + ilBuilder.Emit(OpCodes.Ldarg_2); + ilBuilder.Emit(OpCodes.Call, GetStartClockMethod()); + ilBuilder.EmitStloc(startedClockLocal); + + // loop + ilBuilder.EmitLoopBeginFromArgToZero(out var loopStartLabel, out var loopHeadLabel); + { + for (int u = 0; u < unrollFactor; u++) + { + if (!methodToCall.IsStatic) + { + ilBuilder.Emit(OpCodes.Ldarg_0); + } + ilBuilder.EmitLdLocals(argLocals); + ilBuilder.Emit(OpCodes.Call, methodToCall); + + if (getResultMethod is not null) + { + // global::BenchmarkDotNet.Helpers.AwaitHelper.GetResult(); + ilBuilder.Emit(OpCodes.Call, getResultMethod); + // Generic Task/ValueTask overloads return T — discard it, mirroring the pre-refactor + // void ExecuteBlocking() => AwaitHelper.GetResult(callback()) implementation. + if (getResultMethod.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + } + else if (methodToCall.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + } + } + ilBuilder.EmitLoopEndFromArgToZero(loopStartLabel, loopHeadLabel, invokeCountArg); + + // resultLocal = new ValueTask(startedClock.GetElapsed()); + ilBuilder.EmitLdloca(startedClockLocal); + ilBuilder.Emit(OpCodes.Call, typeof(StartedClock).GetMethod(nameof(StartedClock.GetElapsed), BindingFlags.Public | BindingFlags.Instance)!); + ilBuilder.Emit(OpCodes.Newobj, typeof(ValueTask).GetConstructor([typeof(ClockSpan)])!); + ilBuilder.EmitStloc(resultLocal); + ilBuilder.Emit(OpCodes.Leave, endLabel); + } + ilBuilder.BeginFinallyBlock(); + { + // SynchronizationContext.Current = originalContext; + ilBuilder.EmitLdloc(originalContextLocal); + ilBuilder.Emit(OpCodes.Call, SynchronizationContextSetCurrent); + } + ilBuilder.EndExceptionBlock(); + + ilBuilder.MarkLabel(endLabel); + ilBuilder.EmitLdloc(resultLocal); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private void EmitLoadArgFieldsToLocals(ILGenerator ilBuilder, List argLocals) + { + for (int i = 0; i < argFields.Count; i++) + { + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + + var argFieldInfo = argFields[i]; + if (argFieldInfo.ArgLocalsType.IsByRef) + ilBuilder.Emit(OpCodes.Ldflda, argFieldInfo.Field); + else + ilBuilder.Emit(OpCodes.Ldfld, argFieldInfo.Field); + + if (argFieldInfo.OpImplicitMethod != null) + ilBuilder.Emit(OpCodes.Call, argFieldInfo.OpImplicitMethod); + + ilBuilder.EmitStloc(argLocals[i]); + } + } + } +} diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs index a06f11894d..ba650ceeb6 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs @@ -44,6 +44,7 @@ public class InProcessValidator : IValidator { RunMode.UnrollFactorCharacteristic, DontValidate }, { AccuracyMode.AnalyzeLaunchVarianceCharacteristic, DontValidate }, { AccuracyMode.EvaluateOverheadCharacteristic, DontValidate }, + { AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, DontValidate }, { AccuracyMode.MaxRelativeErrorCharacteristic, DontValidate }, { AccuracyMode.MaxAbsoluteErrorCharacteristic, DontValidate }, { AccuracyMode.MinInvokeCountCharacteristic, DontValidate }, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs index b152fc4c6f..03e3ba2b97 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,21 +133,21 @@ 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); - public static IBenchmarkAction CreateGlobalSetup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance) => - CreateCore(factory, instance, descriptor.GlobalSetupMethod ?? FallbackSignature, 1); + public static IBenchmarkAction CreateGlobalSetup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, bool consumeTasksSynchronously = false) => + CreateCore(factory, instance, descriptor.GlobalSetupMethod ?? FallbackSignature, 1, consumeTasksSynchronously); - public static IBenchmarkAction CreateGlobalCleanup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance) => - CreateCore(factory, instance, descriptor.GlobalCleanupMethod ?? FallbackSignature, 1); + public static IBenchmarkAction CreateGlobalCleanup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, bool consumeTasksSynchronously = false) => + CreateCore(factory, instance, descriptor.GlobalCleanupMethod ?? FallbackSignature, 1, consumeTasksSynchronously); - public static IBenchmarkAction CreateIterationSetup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance) => - CreateCore(factory, instance, descriptor.IterationSetupMethod ?? FallbackSignature, 1); + public static IBenchmarkAction CreateIterationSetup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, bool consumeTasksSynchronously = false) => + CreateCore(factory, instance, descriptor.IterationSetupMethod ?? FallbackSignature, 1, consumeTasksSynchronously); - public static IBenchmarkAction CreateIterationCleanup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance) => - CreateCore(factory, instance, descriptor.IterationCleanupMethod ?? FallbackSignature, 1); + public static IBenchmarkAction CreateIterationCleanup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, bool consumeTasksSynchronously = false) => + CreateCore(factory, instance, descriptor.IterationCleanupMethod ?? FallbackSignature, 1, consumeTasksSynchronously); } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionImpl.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionImpl.cs index 51148cb31e..316e65f2ba 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionImpl.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionImpl.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Attributes.CompilerServices; using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Helpers; using Perfolizer.Horology; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -702,3 +703,327 @@ private async Task WorkloadCore() public override void Cleanup() => workloadValueTaskSource.Complete(); } + +// Blocking variants used when Job.Accuracy.ConsumeTasksSynchronously is enabled. They drive the workload +// through AwaitHelper.GetResult so the iteration loop stays synchronous, matching the pre-async-refactor behavior +// so historical results stay comparable. + +[AggressivelyOptimizeMethods] +public sealed class BenchmarkActionBlockingTask : BenchmarkActionBase +{ + private readonly Func callback; + private readonly Action blockingCallback; + private readonly Action unrolledBlockingCallback; + + [SetsRequiredMembers] + public BenchmarkActionBlockingTask(object? instance, MethodInfo method, int unrollFactor) + { + callback = CreateWorkload>(instance, method); + blockingCallback = InvokeBlocking; + unrolledBlockingCallback = Unroll(blockingCallback, unrollFactor); + InvokeSingle = InvokeOnce; + InvokeUnroll = WorkloadActionUnroll; + InvokeNoUnroll = WorkloadActionNoUnroll; + } + + private void InvokeBlocking() => AwaitHelper.GetResult(callback()); + + private ValueTask InvokeOnce() + { + // Setup/cleanup calls go through InvokeSingle → InvokeOnce, so apply the same SynchronizationContext + // suppression the workload loop uses to avoid deadlocking on user awaits that capture the context. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + blockingCallback(); + return new(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) + { + unrolledBlockingCallback(); + } + return new ValueTask(startedClock.GetElapsed()); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) + { + blockingCallback(); + } + return new ValueTask(startedClock.GetElapsed()); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } +} + +[AggressivelyOptimizeMethods] +public sealed class BenchmarkActionBlockingTask : BenchmarkActionBase +{ + private readonly Func> callback; + private readonly Action blockingCallback; + private readonly Action unrolledBlockingCallback; + + [SetsRequiredMembers] + public BenchmarkActionBlockingTask(object? instance, MethodInfo method, int unrollFactor) + { + callback = CreateWorkload>>(instance, method); + blockingCallback = InvokeBlocking; + unrolledBlockingCallback = Unroll(blockingCallback, unrollFactor); + InvokeSingle = InvokeOnce; + InvokeUnroll = WorkloadActionUnroll; + InvokeNoUnroll = WorkloadActionNoUnroll; + } + + private void InvokeBlocking() => AwaitHelper.GetResult(callback()); + + private ValueTask InvokeOnce() + { + // Setup/cleanup calls go through InvokeSingle → InvokeOnce, so apply the same SynchronizationContext + // suppression the workload loop uses to avoid deadlocking on user awaits that capture the context. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + blockingCallback(); + return new(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) + { + unrolledBlockingCallback(); + } + return new ValueTask(startedClock.GetElapsed()); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) + { + blockingCallback(); + } + return new ValueTask(startedClock.GetElapsed()); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } +} + +[AggressivelyOptimizeMethods] +public sealed class BenchmarkActionBlockingValueTask : BenchmarkActionBase +{ + private readonly Func callback; + private readonly Action blockingCallback; + private readonly Action unrolledBlockingCallback; + + [SetsRequiredMembers] + public BenchmarkActionBlockingValueTask(object? instance, MethodInfo method, int unrollFactor) + { + callback = CreateWorkload>(instance, method); + blockingCallback = InvokeBlocking; + unrolledBlockingCallback = Unroll(blockingCallback, unrollFactor); + InvokeSingle = InvokeOnce; + InvokeUnroll = WorkloadActionUnroll; + InvokeNoUnroll = WorkloadActionNoUnroll; + } + + private void InvokeBlocking() => AwaitHelper.GetResult(callback()); + + private ValueTask InvokeOnce() + { + // Setup/cleanup calls go through InvokeSingle → InvokeOnce, so apply the same SynchronizationContext + // suppression the workload loop uses to avoid deadlocking on user awaits that capture the context. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + blockingCallback(); + return new(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) + { + unrolledBlockingCallback(); + } + return new ValueTask(startedClock.GetElapsed()); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) + { + blockingCallback(); + } + return new ValueTask(startedClock.GetElapsed()); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } +} + +[AggressivelyOptimizeMethods] +public sealed class BenchmarkActionBlockingValueTask : BenchmarkActionBase +{ + private readonly Func> callback; + private readonly Action blockingCallback; + private readonly Action unrolledBlockingCallback; + + [SetsRequiredMembers] + public BenchmarkActionBlockingValueTask(object? instance, MethodInfo method, int unrollFactor) + { + callback = CreateWorkload>>(instance, method); + blockingCallback = InvokeBlocking; + unrolledBlockingCallback = Unroll(blockingCallback, unrollFactor); + InvokeSingle = InvokeOnce; + InvokeUnroll = WorkloadActionUnroll; + InvokeNoUnroll = WorkloadActionNoUnroll; + } + + private void InvokeBlocking() => AwaitHelper.GetResult(callback()); + + private ValueTask InvokeOnce() + { + // Setup/cleanup calls go through InvokeSingle → InvokeOnce, so apply the same SynchronizationContext + // suppression the workload loop uses to avoid deadlocking on user awaits that capture the context. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + blockingCallback(); + return new(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) + { + unrolledBlockingCallback(); + } + return new ValueTask(startedClock.GetElapsed()); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) + { + // Suppress the SynchronizationContext to avoid deadlocks when async awaits are configured to + // continue on the captured context (which is the default Task behavior), and restore it after. + SynchronizationContext? originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + try + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) + { + blockingCallback(); + } + return new ValueTask(startedClock.GetElapsed()); + } + finally + { + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } +} diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs index 28dc146f36..a35100ca7e 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs @@ -137,15 +137,16 @@ public static async ValueTask RunCore(IHost host, ExecuteParameters parameters, var target = benchmarkCase.Descriptor; var job = new Job().Apply(benchmarkCase.Job).Freeze(); int unrollFactor = benchmarkCase.Job.ResolveValue(RunMode.UnrollFactorCharacteristic, EnvironmentResolver.Instance); + bool consumeTasksSynchronously = benchmarkCase.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance); // DONTTOUCH: these should be allocated together var instance = Activator.CreateInstance(benchmarkCase.Descriptor.Type)!; - var workloadAction = BenchmarkActionFactory.CreateWorkload(benchmarkActionFactory, target, instance, unrollFactor); + var workloadAction = BenchmarkActionFactory.CreateWorkload(benchmarkActionFactory, target, instance, unrollFactor, consumeTasksSynchronously); var overheadAction = BenchmarkActionFactory.CreateOverhead(benchmarkActionFactory, target, instance, unrollFactor); - var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(benchmarkActionFactory, target, instance); - var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(benchmarkActionFactory, target, instance); - var iterationSetupAction = BenchmarkActionFactory.CreateIterationSetup(benchmarkActionFactory, target, instance); - var iterationCleanupAction = BenchmarkActionFactory.CreateIterationCleanup(benchmarkActionFactory, target, instance); + var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(benchmarkActionFactory, target, instance, consumeTasksSynchronously); + var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(benchmarkActionFactory, target, instance, consumeTasksSynchronously); + var iterationSetupAction = BenchmarkActionFactory.CreateIterationSetup(benchmarkActionFactory, target, instance, consumeTasksSynchronously); + var iterationCleanupAction = BenchmarkActionFactory.CreateIterationCleanup(benchmarkActionFactory, target, instance, consumeTasksSynchronously); FillMembers(instance, benchmarkCase, host.CancellationToken); diff --git a/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs b/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs index 03a0fd09b6..566c155c03 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs @@ -43,10 +43,32 @@ public class AsyncBenchmarksTests : BenchmarkTestExecutor { public AsyncBenchmarksTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void TaskReturningMethodsAreAwaited() + public static IEnumerable GetToolchains() => + [ + new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = false }), + new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = true }), + new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = false }), + new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = true }), + Job.Default.GetToolchain() + ]; + + public static TheoryData GetToolchainsWithConsumeTasksSynchronously() { - var summary = CanExecute(); + var data = new TheoryData(); + foreach (var toolchain in GetToolchains()) + { + data.Add(toolchain, false); + data.Add(toolchain, true); + } + return data; + } + + [Theory] + [MemberData(nameof(GetToolchainsWithConsumeTasksSynchronously), DisableDiscoveryEnumeration = true)] + public void TaskReturningMethodsAreAwaited(IToolchain toolchain, bool consumeTasksSynchronously) + { + var summary = CanExecute(CreateSimpleConfig( + job: Job.Dry.WithToolchain(toolchain).WithConsumeTasksSynchronously(consumeTasksSynchronously))); foreach (var report in summary.Reports) foreach (var measurement in report.AllMeasurements) @@ -58,29 +80,24 @@ public void TaskReturningMethodsAreAwaited() } } - [Fact] - public void TaskReturningMethodsAreAwaited_AlreadyComplete() => CanExecute(); - - public static TheoryData GetToolchains() => - [ - new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = false }), - new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = true }), - new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = false }), - new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = true }), - Job.Default.GetToolchain() - ]; + [Theory] + [MemberData(nameof(GetToolchainsWithConsumeTasksSynchronously), DisableDiscoveryEnumeration = true)] + public void TaskReturningMethodsAreAwaited_AlreadyComplete(IToolchain toolchain, bool consumeTasksSynchronously) + => CanExecute(CreateSimpleConfig( + job: Job.Dry.WithToolchain(toolchain).WithConsumeTasksSynchronously(consumeTasksSynchronously))); [Theory] - [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] - public void TaskYieldWithNullSyncContext(IToolchain toolchain) - => CanExecute(CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain))); + [MemberData(nameof(GetToolchainsWithConsumeTasksSynchronously), DisableDiscoveryEnumeration = true)] + public void TaskYieldWithNullSyncContext(IToolchain toolchain, bool consumeTasksSynchronously) + => CanExecute(CreateSimpleConfig( + job: Job.Dry.WithToolchain(toolchain).WithConsumeTasksSynchronously(consumeTasksSynchronously))); // #3103 [Theory] - [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] - public void AsyncWorkloadRestartsAfterMemoryRandomization(IToolchain toolchain) + [MemberData(nameof(GetToolchainsWithConsumeTasksSynchronously), DisableDiscoveryEnumeration = true)] + public void AsyncWorkloadRestartsAfterMemoryRandomization(IToolchain toolchain, bool consumeTasksSynchronously) => CanExecute(CreateSimpleConfig( - job: Job.Dry.WithToolchain(toolchain).WithIterationCount(3).WithMemoryRandomization(true))); + job: Job.Dry.WithToolchain(toolchain).WithIterationCount(3).WithMemoryRandomization(true).WithConsumeTasksSynchronously(consumeTasksSynchronously))); public class RandomMemoryAsyncBenchmarks { diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.cs index ec85224c36..4f1a3a1283 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.cs @@ -19,6 +19,18 @@ namespace BenchmarkDotNet.IntegrationTests.InProcess.EmitTests /// public class RunnableTaskCaseBenchmark { + [GlobalSetup] + public async ValueTask GlobalSetup() => await Task.Yield(); + + [GlobalCleanup] + public async Task GlobalCleanup() => await Task.Yield(); + + [IterationSetup] + public async ValueTask IterationSetup() => await Task.Yield(); + + [IterationCleanup] + public async Task IterationCleanup() => await Task.Yield(); + // ---- Begin TaskCase(Task) ---- [Benchmark] diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.tt b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.tt index 761aab422b..26cfc6b2bc 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.tt +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests.T4/RunnableTaskCaseBenchmark.tt @@ -22,6 +22,18 @@ namespace BenchmarkDotNet.IntegrationTests.InProcess.EmitTests /// public class RunnableTaskCaseBenchmark { + [GlobalSetup] + public async ValueTask GlobalSetup() => await Task.Yield(); + + [GlobalCleanup] + public async Task GlobalCleanup() => await Task.Yield(); + + [IterationSetup] + public async ValueTask IterationSetup() => await Task.Yield(); + + [IterationCleanup] + public async Task IterationCleanup() => await Task.Yield(); + <# int counter = 1; EmitTaskCaseBenchmark(ref counter, "Task", "Task.CompletedTask"); diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs index 1f0e342233..7c6b7bdfb8 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs @@ -32,7 +32,7 @@ private IConfig CreateInProcessConfig(OutputLogger? logger) .AddColumnProvider(DefaultColumnProviders.Instance); } - private IConfig CreateInProcessAndRoslynConfig(OutputLogger logger) + private IConfig CreateInProcessAndRoslynConfig(OutputLogger logger, bool consumeTasksSynchronously = false) { var config = new ManualConfig() .AddColumnProvider(DefaultConfig.Instance.GetColumnProviders().ToArray()) @@ -44,12 +44,14 @@ private IConfig CreateInProcessAndRoslynConfig(OutputLogger logger) Job.Dry .WithToolchain(InProcessEmitToolchain.Default) .WithInvocationCount(4) - .WithUnrollFactor(4)) + .WithUnrollFactor(4) + .WithConsumeTasksSynchronously(consumeTasksSynchronously)) .AddJob( Job.Dry .WithToolchain(new RoslynToolchain()) .WithInvocationCount(4) - .WithUnrollFactor(4)) + .WithUnrollFactor(4) + .WithConsumeTasksSynchronously(consumeTasksSynchronously)) .WithOptions(ConfigOptions.KeepBenchmarkFiles) .AddLogger(logger ?? (Output != null ? new OutputLogger(Output) : ConsoleLogger.Default)); @@ -103,20 +105,21 @@ public void InProcessBenchmarkSimpleCasesReflectionEmitSupported() } [TheoryEnvSpecific("We can't use Roslyn toolchain for .NET Core because we don't know which assemblies to reference and .NET Core does not support dynamic assembly saving", EnvRequirement.FullFrameworkOnly)] - [InlineData(typeof(SampleBenchmark))] - [InlineData(typeof(RunnableVoidCaseBenchmark))] - [InlineData(typeof(RunnableRefStructCaseBenchmark))] - [InlineData(typeof(RunnableStructCaseBenchmark))] - [InlineData(typeof(RunnableClassCaseBenchmark))] - [InlineData(typeof(RunnableManyArgsCaseBenchmark))] - [InlineData(typeof(RunnableTaskCaseBenchmark))] - [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableBenchmarks))] - [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableCallerOverride))] - [InlineData(typeof(AsyncEnumerableBenchmarksTests.CustomAsyncEnumerableBenchmarks))] - public void InProcessBenchmarkEmitsSameIL(Type benchmarkType) + [InlineData(typeof(SampleBenchmark), false)] + [InlineData(typeof(RunnableVoidCaseBenchmark), false)] + [InlineData(typeof(RunnableRefStructCaseBenchmark), false)] + [InlineData(typeof(RunnableStructCaseBenchmark), false)] + [InlineData(typeof(RunnableClassCaseBenchmark), false)] + [InlineData(typeof(RunnableManyArgsCaseBenchmark), false)] + [InlineData(typeof(RunnableTaskCaseBenchmark), false)] + [InlineData(typeof(RunnableTaskCaseBenchmark), true)] + [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableBenchmarks), false)] + [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableCallerOverride), false)] + [InlineData(typeof(AsyncEnumerableBenchmarksTests.CustomAsyncEnumerableBenchmarks), false)] + public void InProcessBenchmarkEmitsSameIL(Type benchmarkType, bool consumeTasksSynchronously) { var logger = new OutputLogger(Output); - var config = CreateInProcessAndRoslynConfig(logger); + var config = CreateInProcessAndRoslynConfig(logger, consumeTasksSynchronously); var summary = CanExecute(benchmarkType, config); diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index 8a8313c4fc..20e353cdce 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -748,6 +748,19 @@ public void UsersCanSpecifyEvaluateOverhead() } } + [Fact] + public void UsersCanSpecifyConsumeTasksSynchronously() + { + var parsedConfiguration = ConfigParser.Parse(["--consumeTasksSynchronously", "true"], new OutputLogger(Output)); + Assert.NotNull(parsedConfiguration.config); + Assert.True(parsedConfiguration.isSuccess); + + foreach (var job in parsedConfiguration.config.GetJobs()) + { + Assert.True(job.Accuracy.ConsumeTasksSynchronously); + } + } + [Fact(Skip = "This should be handled somehow at CommandLineParser level. See https://github.com/commandlineparser/commandline/pull/892")] public void UserCanSpecifyWasmArgs() { From 3ad4e18651c830b53758b9a4b4dab8374b149037 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 23 May 2026 06:06:04 -0400 Subject: [PATCH 2/7] Only override sync context for synchronous APIs. --- .../BenchmarkSynchronizationContext.cs | 38 +++++++++++++++---- .../Running/BenchmarkRunnerDirty.cs | 14 +++---- .../Running/BenchmarkSwitcher.cs | 6 +-- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs index 89e47265ac..2e2f998de0 100644 --- a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs +++ b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Helpers; using JetBrains.Annotations; using System.ComponentModel; @@ -15,12 +16,16 @@ private BenchmarkSynchronizationContext(BenchmarkDotNetSynchronizationContext co this.context = context; } + // Used for the actual benchmark execution, we install our sync context unconditionally. + // For benchmarks that need it, like jobs configured with ConsumeTasksSynchronously, they uninstall the sync context during execution. public static BenchmarkSynchronizationContext CreateAndSetCurrent() - { - var context = new BenchmarkDotNetSynchronizationContext(SynchronizationContext.Current); - SynchronizationContext.SetSynchronizationContext(context); - return new(context); - } + => new(new BenchmarkDotNetSynchronizationContext(true)); + + // For BenchmarkRunner/Switcher.Run* synchronous methods, to prevent deadlocks from await continuations posting to the sync context, + // we override it so that they will post to our context instead so we can pump it until completion. + // If there is no current context there is no deadlock concern, so we do not install our sync context and use AwaitHelper.GetResult instead of pumping post callbacks. + internal static BenchmarkSynchronizationContext CreateAndMaybeOverrideCurrent() + => new(new BenchmarkDotNetSynchronizationContext(false)); public void Dispose() => context.Dispose(); @@ -38,10 +43,16 @@ internal sealed class BenchmarkDotNetSynchronizationContext : SynchronizationCon private (SendOrPostCallback d, object? state)[]? executing = new (SendOrPostCallback d, object? state)[1]; private int queueCount = 0; private bool isCompleted; + private readonly bool didSetCurrent; - internal BenchmarkDotNetSynchronizationContext(SynchronizationContext? previousContext) + internal BenchmarkDotNetSynchronizationContext(bool setCurrentIfNullPrevious) { - this.previousContext = previousContext; + previousContext = Current; + didSetCurrent = setCurrentIfNullPrevious || previousContext != null; + if (didSetCurrent) + { + SetSynchronizationContext(this); + } } public override SynchronizationContext CreateCopy() @@ -49,7 +60,7 @@ public override SynchronizationContext CreateCopy() public override void Post(SendOrPostCallback d, object? state) { - if (d is null) throw new ArgumentNullException(nameof(d)); + ArgumentNullException.ThrowIfNull(d); lock (syncRoot) { @@ -70,6 +81,12 @@ public override void Post(SendOrPostCallback d, object? state) internal void Dispose() { + if (!didSetCurrent) + { + ThrowIfDisposed(); + return; + } + int count; (SendOrPostCallback d, object? state)[] executing; lock (syncRoot) @@ -95,6 +112,11 @@ internal T ExecuteUntilComplete(ValueTask valueTask) { ThrowIfDisposed(); + if (!didSetCurrent) + { + return AwaitHelper.GetResult(valueTask); + } + var awaiter = valueTask.GetAwaiter(); if (awaiter.IsCompleted) { diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs index 8c2bc2737e..710de508e8 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs @@ -17,49 +17,49 @@ public static class BenchmarkRunner [PublicAPI] public static Summary Run(IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAsync(config, args)); } [PublicAPI] public static Summary Run(Type type, IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAsync(type, config, args)); } [PublicAPI] public static Summary[] Run(Type[] types, IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAsync(types, config, args)); } [PublicAPI] public static Summary Run(Type type, MethodInfo[] methods, IConfig? config = null) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAsync(type, methods, config)); } [PublicAPI] public static Summary[] Run(Assembly assembly, IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAsync(assembly, config, args)); } [PublicAPI] public static Summary Run(BenchmarkRunInfo benchmarkRunInfo) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfo)); } [PublicAPI] public static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfos)); } diff --git a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs index 8697a5086e..02f03edd67 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs @@ -54,7 +54,7 @@ public class BenchmarkSwitcher [PublicAPI] public IEnumerable RunAll(IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAllAsync(config, args)); } @@ -64,14 +64,14 @@ public IEnumerable RunAll(IConfig? config = null, string[]? args = null [PublicAPI] public Summary RunAllJoined(IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAllJoinedAsync(config, args)); } [PublicAPI] public IEnumerable Run(string[]? args = null, IConfig? config = null) { - using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); return context.ExecuteUntilComplete(RunAsync(args, config)); } From 2dcf5fbbb874c1f5356553fc92787029fe6c4834 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 23 May 2026 08:22:52 -0400 Subject: [PATCH 3/7] Add ConsumeTasksSynchronouslyAttribute. --- .../Mutators/ConsumeTasksSynchronouslyAttribute.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs diff --git a/src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs b/src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs new file mode 100644 index 0000000000..7e95882608 --- /dev/null +++ b/src/BenchmarkDotNet/Attributes/Mutators/ConsumeTasksSynchronouslyAttribute.cs @@ -0,0 +1,8 @@ +using BenchmarkDotNet.Jobs; + +namespace BenchmarkDotNet.Attributes; + +/// +public class ConsumeTasksSynchronouslyAttribute(bool value) : JobMutatorConfigBaseAttribute(Job.Default.WithConsumeTasksSynchronously(value)) +{ +} From 6f9148a6e44c5048bf3c7c4e716de9e92c731f12 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 28 May 2026 18:39:55 -0400 Subject: [PATCH 4/7] Don't install BenchmarkSynchronizationContext in synchronous APIs. --- .../BenchmarkSynchronizationContext.cs | 38 ++++--------------- src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 8 ++-- .../Running/BenchmarkRunnerDirty.cs | 37 ++++-------------- .../Running/BenchmarkSwitcher.cs | 16 ++------ 4 files changed, 24 insertions(+), 75 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs index 2e2f998de0..89e47265ac 100644 --- a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs +++ b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs @@ -1,4 +1,3 @@ -using BenchmarkDotNet.Helpers; using JetBrains.Annotations; using System.ComponentModel; @@ -16,16 +15,12 @@ private BenchmarkSynchronizationContext(BenchmarkDotNetSynchronizationContext co this.context = context; } - // Used for the actual benchmark execution, we install our sync context unconditionally. - // For benchmarks that need it, like jobs configured with ConsumeTasksSynchronously, they uninstall the sync context during execution. public static BenchmarkSynchronizationContext CreateAndSetCurrent() - => new(new BenchmarkDotNetSynchronizationContext(true)); - - // For BenchmarkRunner/Switcher.Run* synchronous methods, to prevent deadlocks from await continuations posting to the sync context, - // we override it so that they will post to our context instead so we can pump it until completion. - // If there is no current context there is no deadlock concern, so we do not install our sync context and use AwaitHelper.GetResult instead of pumping post callbacks. - internal static BenchmarkSynchronizationContext CreateAndMaybeOverrideCurrent() - => new(new BenchmarkDotNetSynchronizationContext(false)); + { + var context = new BenchmarkDotNetSynchronizationContext(SynchronizationContext.Current); + SynchronizationContext.SetSynchronizationContext(context); + return new(context); + } public void Dispose() => context.Dispose(); @@ -43,16 +38,10 @@ internal sealed class BenchmarkDotNetSynchronizationContext : SynchronizationCon private (SendOrPostCallback d, object? state)[]? executing = new (SendOrPostCallback d, object? state)[1]; private int queueCount = 0; private bool isCompleted; - private readonly bool didSetCurrent; - internal BenchmarkDotNetSynchronizationContext(bool setCurrentIfNullPrevious) + internal BenchmarkDotNetSynchronizationContext(SynchronizationContext? previousContext) { - previousContext = Current; - didSetCurrent = setCurrentIfNullPrevious || previousContext != null; - if (didSetCurrent) - { - SetSynchronizationContext(this); - } + this.previousContext = previousContext; } public override SynchronizationContext CreateCopy() @@ -60,7 +49,7 @@ public override SynchronizationContext CreateCopy() public override void Post(SendOrPostCallback d, object? state) { - ArgumentNullException.ThrowIfNull(d); + if (d is null) throw new ArgumentNullException(nameof(d)); lock (syncRoot) { @@ -81,12 +70,6 @@ public override void Post(SendOrPostCallback d, object? state) internal void Dispose() { - if (!didSetCurrent) - { - ThrowIfDisposed(); - return; - } - int count; (SendOrPostCallback d, object? state)[] executing; lock (syncRoot) @@ -112,11 +95,6 @@ internal T ExecuteUntilComplete(ValueTask valueTask) { ThrowIfDisposed(); - if (!didSetCurrent) - { - return AwaitHelper.GetResult(valueTask); - } - var awaiter = valueTask.GetAwaiter(); if (awaiter.IsCompleted) { diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs index ea5d9641d7..f7f0f00299 100644 --- a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -47,14 +47,14 @@ internal void Wait(TAwaiter awaiter) where TAwaiter : ICriticalNotifyC // 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(Task task) => task.GetAwaiter().GetResult(); + public static void GetResult(this Task task) => task.GetAwaiter().GetResult(); - public static T GetResult(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(ValueTask task) + 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(); @@ -65,7 +65,7 @@ public static void GetResult(ValueTask task) awaiter.GetResult(); } - public static T GetResult(ValueTask task) + 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(); diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs index 710de508e8..eaf8ed2ab3 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs @@ -1,6 +1,6 @@ using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Reports; using JetBrains.Annotations; @@ -16,52 +16,31 @@ public static class BenchmarkRunner { [PublicAPI] public static Summary Run(IConfig? config = null, string[]? args = null) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAsync(config, args)); - } + => RunAsync(config, args).GetResult(); [PublicAPI] public static Summary Run(Type type, IConfig? config = null, string[]? args = null) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAsync(type, config, args)); - } + => RunAsync(type, config, args).GetResult(); [PublicAPI] public static Summary[] Run(Type[] types, IConfig? config = null, string[]? args = null) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAsync(types, config, args)); - } + => RunAsync(types, config, args).GetResult(); [PublicAPI] public static Summary Run(Type type, MethodInfo[] methods, IConfig? config = null) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAsync(type, methods, config)); - } + => RunAsync(type, methods, config).GetResult(); [PublicAPI] public static Summary[] Run(Assembly assembly, IConfig? config = null, string[]? args = null) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAsync(assembly, config, args)); - } + => RunAsync(assembly, config, args).GetResult(); [PublicAPI] public static Summary Run(BenchmarkRunInfo benchmarkRunInfo) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfo)); - } + => RunAsync(benchmarkRunInfo).GetResult(); [PublicAPI] public static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfos)); - } + => RunAsync(benchmarkRunInfos).GetResult(); [PublicAPI] public static ValueTask RunAsync(IConfig? config = null, string[]? args = null, CancellationToken cancellationToken = default) diff --git a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs index 02f03edd67..fb75847c3d 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs @@ -4,6 +4,7 @@ using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Parameters; @@ -53,27 +54,18 @@ public class BenchmarkSwitcher /// [PublicAPI] public IEnumerable RunAll(IConfig? config = null, string[]? args = null) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAllAsync(config, args)); - } + => RunAllAsync(config, args).GetResult(); /// /// Run all available benchmarks and join them to a single summary /// [PublicAPI] public Summary RunAllJoined(IConfig? config = null, string[]? args = null) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAllJoinedAsync(config, args)); - } + => RunAllJoinedAsync(config, args).GetResult(); [PublicAPI] public IEnumerable Run(string[]? args = null, IConfig? config = null) - { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); - return context.ExecuteUntilComplete(RunAsync(args, config)); - } + => RunAsync(args, config).GetResult(); /// /// Run all available benchmarks asynchronously. From 3a3b5f1c7eae62a74abc355b52a43643342f056b Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 28 May 2026 18:56:25 -0400 Subject: [PATCH 5/7] Revert "Don't install BenchmarkSynchronizationContext in synchronous APIs." This reverts commit b7c62916074ff3fc1182f0374722419ede038204. --- .../BenchmarkSynchronizationContext.cs | 38 +++++++++++++++---- src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 8 ++-- .../Running/BenchmarkRunnerDirty.cs | 37 ++++++++++++++---- .../Running/BenchmarkSwitcher.cs | 16 ++++++-- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs index 89e47265ac..2e2f998de0 100644 --- a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs +++ b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Helpers; using JetBrains.Annotations; using System.ComponentModel; @@ -15,12 +16,16 @@ private BenchmarkSynchronizationContext(BenchmarkDotNetSynchronizationContext co this.context = context; } + // Used for the actual benchmark execution, we install our sync context unconditionally. + // For benchmarks that need it, like jobs configured with ConsumeTasksSynchronously, they uninstall the sync context during execution. public static BenchmarkSynchronizationContext CreateAndSetCurrent() - { - var context = new BenchmarkDotNetSynchronizationContext(SynchronizationContext.Current); - SynchronizationContext.SetSynchronizationContext(context); - return new(context); - } + => new(new BenchmarkDotNetSynchronizationContext(true)); + + // For BenchmarkRunner/Switcher.Run* synchronous methods, to prevent deadlocks from await continuations posting to the sync context, + // we override it so that they will post to our context instead so we can pump it until completion. + // If there is no current context there is no deadlock concern, so we do not install our sync context and use AwaitHelper.GetResult instead of pumping post callbacks. + internal static BenchmarkSynchronizationContext CreateAndMaybeOverrideCurrent() + => new(new BenchmarkDotNetSynchronizationContext(false)); public void Dispose() => context.Dispose(); @@ -38,10 +43,16 @@ internal sealed class BenchmarkDotNetSynchronizationContext : SynchronizationCon private (SendOrPostCallback d, object? state)[]? executing = new (SendOrPostCallback d, object? state)[1]; private int queueCount = 0; private bool isCompleted; + private readonly bool didSetCurrent; - internal BenchmarkDotNetSynchronizationContext(SynchronizationContext? previousContext) + internal BenchmarkDotNetSynchronizationContext(bool setCurrentIfNullPrevious) { - this.previousContext = previousContext; + previousContext = Current; + didSetCurrent = setCurrentIfNullPrevious || previousContext != null; + if (didSetCurrent) + { + SetSynchronizationContext(this); + } } public override SynchronizationContext CreateCopy() @@ -49,7 +60,7 @@ public override SynchronizationContext CreateCopy() public override void Post(SendOrPostCallback d, object? state) { - if (d is null) throw new ArgumentNullException(nameof(d)); + ArgumentNullException.ThrowIfNull(d); lock (syncRoot) { @@ -70,6 +81,12 @@ public override void Post(SendOrPostCallback d, object? state) internal void Dispose() { + if (!didSetCurrent) + { + ThrowIfDisposed(); + return; + } + int count; (SendOrPostCallback d, object? state)[] executing; lock (syncRoot) @@ -95,6 +112,11 @@ internal T ExecuteUntilComplete(ValueTask valueTask) { ThrowIfDisposed(); + if (!didSetCurrent) + { + return AwaitHelper.GetResult(valueTask); + } + var awaiter = valueTask.GetAwaiter(); if (awaiter.IsCompleted) { diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs index f7f0f00299..ea5d9641d7 100644 --- a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -47,14 +47,14 @@ internal void Wait(TAwaiter awaiter) where TAwaiter : ICriticalNotifyC // 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 void GetResult(Task task) => task.GetAwaiter().GetResult(); - public static T GetResult(this Task task) => task.GetAwaiter().GetResult(); + public static T GetResult(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) + public static void GetResult(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(); @@ -65,7 +65,7 @@ public static void GetResult(this ValueTask task) awaiter.GetResult(); } - public static T GetResult(this ValueTask task) + public static T GetResult(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(); diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs index eaf8ed2ab3..710de508e8 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs @@ -1,6 +1,6 @@ using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; -using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Reports; using JetBrains.Annotations; @@ -16,31 +16,52 @@ public static class BenchmarkRunner { [PublicAPI] public static Summary Run(IConfig? config = null, string[]? args = null) - => RunAsync(config, args).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAsync(config, args)); + } [PublicAPI] public static Summary Run(Type type, IConfig? config = null, string[]? args = null) - => RunAsync(type, config, args).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAsync(type, config, args)); + } [PublicAPI] public static Summary[] Run(Type[] types, IConfig? config = null, string[]? args = null) - => RunAsync(types, config, args).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAsync(types, config, args)); + } [PublicAPI] public static Summary Run(Type type, MethodInfo[] methods, IConfig? config = null) - => RunAsync(type, methods, config).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAsync(type, methods, config)); + } [PublicAPI] public static Summary[] Run(Assembly assembly, IConfig? config = null, string[]? args = null) - => RunAsync(assembly, config, args).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAsync(assembly, config, args)); + } [PublicAPI] public static Summary Run(BenchmarkRunInfo benchmarkRunInfo) - => RunAsync(benchmarkRunInfo).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfo)); + } [PublicAPI] public static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) - => RunAsync(benchmarkRunInfos).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfos)); + } [PublicAPI] public static ValueTask RunAsync(IConfig? config = null, string[]? args = null, CancellationToken cancellationToken = default) diff --git a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs index fb75847c3d..02f03edd67 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs @@ -4,7 +4,6 @@ using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; -using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Parameters; @@ -54,18 +53,27 @@ public class BenchmarkSwitcher /// [PublicAPI] public IEnumerable RunAll(IConfig? config = null, string[]? args = null) - => RunAllAsync(config, args).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAllAsync(config, args)); + } /// /// Run all available benchmarks and join them to a single summary /// [PublicAPI] public Summary RunAllJoined(IConfig? config = null, string[]? args = null) - => RunAllJoinedAsync(config, args).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAllJoinedAsync(config, args)); + } [PublicAPI] public IEnumerable Run(string[]? args = null, IConfig? config = null) - => RunAsync(args, config).GetResult(); + { + using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + return context.ExecuteUntilComplete(RunAsync(args, config)); + } /// /// Run all available benchmarks asynchronously. From a4da3550e95295e95f75856d3070ce74fbd96802 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 28 May 2026 18:56:37 -0400 Subject: [PATCH 6/7] Revert "Only override sync context for synchronous APIs." This reverts commit c3cb966d3837d1c0ad216685c562319fcb7335b8. --- .../BenchmarkSynchronizationContext.cs | 38 ++++--------------- .../Running/BenchmarkRunnerDirty.cs | 14 +++---- .../Running/BenchmarkSwitcher.cs | 6 +-- 3 files changed, 18 insertions(+), 40 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs index 2e2f998de0..89e47265ac 100644 --- a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs +++ b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs @@ -1,4 +1,3 @@ -using BenchmarkDotNet.Helpers; using JetBrains.Annotations; using System.ComponentModel; @@ -16,16 +15,12 @@ private BenchmarkSynchronizationContext(BenchmarkDotNetSynchronizationContext co this.context = context; } - // Used for the actual benchmark execution, we install our sync context unconditionally. - // For benchmarks that need it, like jobs configured with ConsumeTasksSynchronously, they uninstall the sync context during execution. public static BenchmarkSynchronizationContext CreateAndSetCurrent() - => new(new BenchmarkDotNetSynchronizationContext(true)); - - // For BenchmarkRunner/Switcher.Run* synchronous methods, to prevent deadlocks from await continuations posting to the sync context, - // we override it so that they will post to our context instead so we can pump it until completion. - // If there is no current context there is no deadlock concern, so we do not install our sync context and use AwaitHelper.GetResult instead of pumping post callbacks. - internal static BenchmarkSynchronizationContext CreateAndMaybeOverrideCurrent() - => new(new BenchmarkDotNetSynchronizationContext(false)); + { + var context = new BenchmarkDotNetSynchronizationContext(SynchronizationContext.Current); + SynchronizationContext.SetSynchronizationContext(context); + return new(context); + } public void Dispose() => context.Dispose(); @@ -43,16 +38,10 @@ internal sealed class BenchmarkDotNetSynchronizationContext : SynchronizationCon private (SendOrPostCallback d, object? state)[]? executing = new (SendOrPostCallback d, object? state)[1]; private int queueCount = 0; private bool isCompleted; - private readonly bool didSetCurrent; - internal BenchmarkDotNetSynchronizationContext(bool setCurrentIfNullPrevious) + internal BenchmarkDotNetSynchronizationContext(SynchronizationContext? previousContext) { - previousContext = Current; - didSetCurrent = setCurrentIfNullPrevious || previousContext != null; - if (didSetCurrent) - { - SetSynchronizationContext(this); - } + this.previousContext = previousContext; } public override SynchronizationContext CreateCopy() @@ -60,7 +49,7 @@ public override SynchronizationContext CreateCopy() public override void Post(SendOrPostCallback d, object? state) { - ArgumentNullException.ThrowIfNull(d); + if (d is null) throw new ArgumentNullException(nameof(d)); lock (syncRoot) { @@ -81,12 +70,6 @@ public override void Post(SendOrPostCallback d, object? state) internal void Dispose() { - if (!didSetCurrent) - { - ThrowIfDisposed(); - return; - } - int count; (SendOrPostCallback d, object? state)[] executing; lock (syncRoot) @@ -112,11 +95,6 @@ internal T ExecuteUntilComplete(ValueTask valueTask) { ThrowIfDisposed(); - if (!didSetCurrent) - { - return AwaitHelper.GetResult(valueTask); - } - var awaiter = valueTask.GetAwaiter(); if (awaiter.IsCompleted) { diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs index 710de508e8..8c2bc2737e 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs @@ -17,49 +17,49 @@ public static class BenchmarkRunner [PublicAPI] public static Summary Run(IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAsync(config, args)); } [PublicAPI] public static Summary Run(Type type, IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAsync(type, config, args)); } [PublicAPI] public static Summary[] Run(Type[] types, IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAsync(types, config, args)); } [PublicAPI] public static Summary Run(Type type, MethodInfo[] methods, IConfig? config = null) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAsync(type, methods, config)); } [PublicAPI] public static Summary[] Run(Assembly assembly, IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAsync(assembly, config, args)); } [PublicAPI] public static Summary Run(BenchmarkRunInfo benchmarkRunInfo) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfo)); } [PublicAPI] public static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfos)); } diff --git a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs index 02f03edd67..8697a5086e 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs @@ -54,7 +54,7 @@ public class BenchmarkSwitcher [PublicAPI] public IEnumerable RunAll(IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAllAsync(config, args)); } @@ -64,14 +64,14 @@ public IEnumerable RunAll(IConfig? config = null, string[]? args = null [PublicAPI] public Summary RunAllJoined(IConfig? config = null, string[]? args = null) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAllJoinedAsync(config, args)); } [PublicAPI] public IEnumerable Run(string[]? args = null, IConfig? config = null) { - using var context = BenchmarkSynchronizationContext.CreateAndMaybeOverrideCurrent(); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); return context.ExecuteUntilComplete(RunAsync(args, config)); } From 16ce0117e0dfb5675cc39944d5e8bf0f57038bd0 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 28 May 2026 20:21:29 -0400 Subject: [PATCH 7/7] Revert synchronous task consumption for setup/cleanup. --- .../Code/DeclarationsProvider.cs | 33 +------ src/BenchmarkDotNet/Helpers/AwaitHelper.cs | 8 +- .../Emitters/AsyncStateMachineEmitter.cs | 4 +- .../Emitters/SetupCleanupEmitter.cs | 94 +------------------ .../Emitters/SyncTaskCoreEmitter.cs | 15 +++ .../NoEmit/BenchmarkActionFactory.cs | 16 ++-- .../InProcess/NoEmit/InProcessNoEmitRunner.cs | 8 +- 7 files changed, 37 insertions(+), 141 deletions(-) diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index c495bbe89f..f7b1fe750a 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -50,19 +50,9 @@ private void Replace(SmartStringBuilder smartStringBuilder, MethodInfo? method, } else if (method.ReturnType.IsAwaitable(out _)) { - if (Benchmark.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance) - && AwaitHelper.IsBuiltInTaskType(method.ReturnType)) - { - modifier = string.Empty; - userImpl = GetSyncTaskSetupCleanupImpl(method); - needsExplicitReturn = true; - } - else - { - modifier = "async"; - userImpl = $"await {GetMethodPrefix(method)}.{method.Name}();"; - needsExplicitReturn = false; - } + modifier = "async"; + userImpl = $"await {GetMethodPrefix(method)}.{method.Name}();"; + needsExplicitReturn = false; } else { @@ -92,23 +82,6 @@ private static string CombineLines(params string[] parts) protected virtual string GetExtraGlobalSetupImpl() => string.Empty; protected virtual string GetExtraGlobalCleanupImpl() => string.Empty; - private string GetSyncTaskSetupCleanupImpl(MethodInfo method) - { - string syncContextType = typeof(SynchronizationContext).GetCorrectCSharpTypeName(); - return $$""" - {{syncContextType}} originalContext = {{syncContextType}}.Current; - {{syncContextType}}.SetSynchronizationContext(null); - try - { - {{typeof(AwaitHelper).GetCorrectCSharpTypeName()}}.{{nameof(AwaitHelper.GetResult)}}({{GetMethodPrefix(method)}}.{{method.Name}}()); - } - finally - { - {{syncContextType}}.SetSynchronizationContext(originalContext); - } - """; - } - protected abstract SmartStringBuilder ReplaceCore(SmartStringBuilder smartStringBuilder); private static string GetMethodPrefix(MethodInfo method) diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs index ea5d9641d7..f7f0f00299 100644 --- a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -47,14 +47,14 @@ internal void Wait(TAwaiter awaiter) where TAwaiter : ICriticalNotifyC // 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(Task task) => task.GetAwaiter().GetResult(); + public static void GetResult(this Task task) => task.GetAwaiter().GetResult(); - public static T GetResult(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(ValueTask task) + 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(); @@ -65,7 +65,7 @@ public static void GetResult(ValueTask task) awaiter.GetResult(); } - public static T GetResult(ValueTask task) + 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(); 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/SetupCleanupEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs index cac066b13f..32a549972f 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs @@ -1,8 +1,5 @@ -using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; -using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Helpers.Reflection.Emit; -using BenchmarkDotNet.Jobs; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; @@ -17,18 +14,7 @@ private void EmitSetupCleanup(string methodName, MethodInfo? methodToCall, Setup { if (methodToCall?.ReturnType.IsAwaitable(out _) == true) { - // The sync-task-consumption decision is per-method: a built-in (Value)Task() setup/cleanup - // gets the blocking path whenever ConsumeTasksSynchronously is set, regardless of which core - // emitter was chosen for the workload. This mirrors the source-gen base Replace logic. - if (AwaitHelper.IsBuiltInTaskType(methodToCall.ReturnType) - && benchmark.BenchmarkCase.Job.ResolveValue(AccuracyMode.ConsumeTasksSynchronouslyCharacteristic, EnvironmentResolver.Instance)) - { - EmitBlockingSetupCleanup(methodName, methodToCall, kind); - } - else - { - EmitAsyncSetupCleanup(methodName, methodToCall, kind); - } + EmitAsyncSetupCleanup(methodName, methodToCall, kind); } else { @@ -85,84 +71,6 @@ [0] valuetype [System.Runtime]System.Threading.Tasks.ValueTask private void EmitAsyncSetupCleanup(string methodName, MethodInfo methodToCall, SetupCleanupKind kind) => EmitAsyncSingleCall(methodName, typeof(AsyncValueTaskMethodBuilder), methodToCall, kind); - // Mirrors SyncTaskSetupCleanupImpl in source-gen: capture+null SynchronizationContext, then try { AwaitHelper.GetResult(base.X()) } finally { restore }, then return new ValueTask(). - private void EmitBlockingSetupCleanup(string methodName, MethodInfo methodToCall, SetupCleanupKind kind) - { - var getResultMethod = AwaitHelper.GetGetResultMethod(methodToCall.ReturnType) - ?? throw new InvalidOperationException( - $"AwaitHelper.GetResult is not available for setup/cleanup return type {methodToCall.ReturnType.GetDisplayName()}. ConsumeTasksSynchronously only supports (Value)Task()."); - - var methodBuilder = runnableBuilder - .DefineNonVirtualInstanceMethod( - methodName, - MethodAttributes.Private, - EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)) - ) - .SetAggressiveOptimizationImplementationFlag(); - var ilBuilder = methodBuilder.GetILGenerator(); - - // Local order matches Roslyn's source-declaration order: originalContext (outer source declaration), - // then the compiler-generated ValueTask temp used for `return new ValueTask();`. - var originalContextLocal = ilBuilder.DeclareLocal(typeof(SynchronizationContext)); - var valueTaskLocal = ilBuilder.DeclareLocal(typeof(ValueTask)); - - if (kind == SetupCleanupKind.GlobalCleanup) - { - EmitExtraGlobalCleanup(ilBuilder, null); - } - - // SynchronizationContext originalContext = SynchronizationContext.Current; - ilBuilder.Emit(OpCodes.Call, SynchronizationContextGetCurrentMethod); - ilBuilder.EmitStloc(originalContextLocal); - // SynchronizationContext.SetSynchronizationContext(null); - ilBuilder.Emit(OpCodes.Ldnull); - ilBuilder.Emit(OpCodes.Call, SynchronizationContextSetCurrentMethod); - - var endLabel = ilBuilder.DefineLabel(); - ilBuilder.BeginExceptionBlock(); - { - if (!methodToCall.IsStatic) - { - ilBuilder.Emit(OpCodes.Ldarg_0); - } - ilBuilder.Emit(OpCodes.Call, methodToCall); - // AwaitHelper.GetResult() - ilBuilder.Emit(OpCodes.Call, getResultMethod); - // Generic Task/ValueTask overloads return T — discard it. - if (getResultMethod.ReturnType != typeof(void)) - { - ilBuilder.Emit(OpCodes.Pop); - } - ilBuilder.Emit(OpCodes.Leave, endLabel); - } - ilBuilder.BeginFinallyBlock(); - { - // SynchronizationContext.SetSynchronizationContext(originalContext); - ilBuilder.EmitLdloc(originalContextLocal); - ilBuilder.Emit(OpCodes.Call, SynchronizationContextSetCurrentMethod); - } - ilBuilder.EndExceptionBlock(); - - ilBuilder.MarkLabel(endLabel); - - if (kind == SetupCleanupKind.GlobalSetup) - { - EmitExtraGlobalSetup(ilBuilder, null); - } - - // return new ValueTask(); - ilBuilder.EmitLdloca(valueTaskLocal); - ilBuilder.Emit(OpCodes.Initobj, typeof(ValueTask)); - ilBuilder.EmitLdloc(valueTaskLocal); - ilBuilder.Emit(OpCodes.Ret); - } - - private static readonly MethodInfo SynchronizationContextGetCurrentMethod = - typeof(SynchronizationContext).GetProperty(nameof(SynchronizationContext.Current), BindingFlags.Public | BindingFlags.Static)!.GetMethod!; - - private static readonly MethodInfo SynchronizationContextSetCurrentMethod = - typeof(SynchronizationContext).GetMethod(nameof(SynchronizationContext.SetSynchronizationContext), BindingFlags.Public | BindingFlags.Static, null, [typeof(SynchronizationContext)], null)!; - protected virtual void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { } protected virtual void EmitExtraGlobalSetup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs index e6f6150c5c..9f4175ce0c 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs @@ -21,6 +21,21 @@ private sealed class SyncTaskCoreEmitter(BuildPartition buildPartition, ModuleBu private static readonly MethodInfo SynchronizationContextSetCurrent = typeof(SynchronizationContext).GetMethod(nameof(SynchronizationContext.SetSynchronizationContext), BindingFlags.Public | BindingFlags.Static, null, [typeof(SynchronizationContext)], null)!; + // The workload is consumed synchronously, so the only async state machines are the setup/cleanup + // methods. Without arguments there is no fields container declared before them, so their Roslyn + // ordinals are two lower than in the async path (which always declares the fields container). + // With arguments the fields container shifts them back to the async ordinals. + protected override IReadOnlyDictionary AsyncMethodToOrdinalMap + => argFields.Count > 0 + ? base.AsyncMethodToOrdinalMap + : new Dictionary + { + { GlobalSetupMethodName, 2 }, + { GlobalCleanupMethodName, 3 }, + { IterationSetupMethodName, 4 }, + { IterationCleanupMethodName, 5 }, + }; + protected override void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { } protected override void EmitCoreImpl() diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs index 03e3ba2b97..4f7fa22609 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs @@ -139,15 +139,15 @@ public static IBenchmarkAction CreateWorkload(IBenchmarkActionFactory? factory, public static IBenchmarkAction CreateOverhead(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, int unrollFactor) => CreateCore(factory, instance, FallbackSignature, unrollFactor); - public static IBenchmarkAction CreateGlobalSetup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, bool consumeTasksSynchronously = false) => - CreateCore(factory, instance, descriptor.GlobalSetupMethod ?? FallbackSignature, 1, consumeTasksSynchronously); + public static IBenchmarkAction CreateGlobalSetup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance) => + CreateCore(factory, instance, descriptor.GlobalSetupMethod ?? FallbackSignature, 1); - public static IBenchmarkAction CreateGlobalCleanup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, bool consumeTasksSynchronously = false) => - CreateCore(factory, instance, descriptor.GlobalCleanupMethod ?? FallbackSignature, 1, consumeTasksSynchronously); + public static IBenchmarkAction CreateGlobalCleanup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance) => + CreateCore(factory, instance, descriptor.GlobalCleanupMethod ?? FallbackSignature, 1); - public static IBenchmarkAction CreateIterationSetup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, bool consumeTasksSynchronously = false) => - CreateCore(factory, instance, descriptor.IterationSetupMethod ?? FallbackSignature, 1, consumeTasksSynchronously); + public static IBenchmarkAction CreateIterationSetup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance) => + CreateCore(factory, instance, descriptor.IterationSetupMethod ?? FallbackSignature, 1); - public static IBenchmarkAction CreateIterationCleanup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance, bool consumeTasksSynchronously = false) => - CreateCore(factory, instance, descriptor.IterationCleanupMethod ?? FallbackSignature, 1, consumeTasksSynchronously); + public static IBenchmarkAction CreateIterationCleanup(IBenchmarkActionFactory? factory, Descriptor descriptor, object instance) => + CreateCore(factory, instance, descriptor.IterationCleanupMethod ?? FallbackSignature, 1); } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs index a35100ca7e..d3af6d0942 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs @@ -143,10 +143,10 @@ public static async ValueTask RunCore(IHost host, ExecuteParameters parameters, var instance = Activator.CreateInstance(benchmarkCase.Descriptor.Type)!; var workloadAction = BenchmarkActionFactory.CreateWorkload(benchmarkActionFactory, target, instance, unrollFactor, consumeTasksSynchronously); var overheadAction = BenchmarkActionFactory.CreateOverhead(benchmarkActionFactory, target, instance, unrollFactor); - var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(benchmarkActionFactory, target, instance, consumeTasksSynchronously); - var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(benchmarkActionFactory, target, instance, consumeTasksSynchronously); - var iterationSetupAction = BenchmarkActionFactory.CreateIterationSetup(benchmarkActionFactory, target, instance, consumeTasksSynchronously); - var iterationCleanupAction = BenchmarkActionFactory.CreateIterationCleanup(benchmarkActionFactory, target, instance, consumeTasksSynchronously); + var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(benchmarkActionFactory, target, instance); + var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(benchmarkActionFactory, target, instance); + var iterationSetupAction = BenchmarkActionFactory.CreateIterationSetup(benchmarkActionFactory, target, instance); + var iterationCleanupAction = BenchmarkActionFactory.CreateIterationCleanup(benchmarkActionFactory, target, instance); FillMembers(instance, benchmarkCase, host.CancellationToken);