Fix consume ValueTask backed by IValueTaskSource (#2108)

* Added AwaitHelper to properly wait for ValueTasks.

* Adjust `AwaitHelper` to allow multiple threads to use it concurrently.

* Changed AwaitHelper to static.

* Add test case to make sure ValueTasks work properly with a race condition between `IsCompleted` and `OnCompleted`.

Changed AwaitHelper to use `ManualResetEventSlim` instead of `Monitor.Wait`.

* Make `ValueTaskWaiter.Wait` generic.

* Compare types directly.
This commit is contained in:
Tim Cassell 2024-03-19 18:08:45 -04:00 коммит произвёл GitHub
Родитель 0d3099163a
Коммит 7306ee7def
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
10 изменённых файлов: 444 добавлений и 198 удалений

Просмотреть файл

@ -63,7 +63,7 @@ namespace BenchmarkDotNet.Code
(method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) ||
method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))))
{
return $"() => {method.Name}().GetAwaiter().GetResult()";
return $"() => BenchmarkDotNet.Helpers.AwaitHelper.GetResult({method.Name}())";
}
return method.Name;
@ -149,12 +149,10 @@ namespace BenchmarkDotNet.Code
{
public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
// we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
// and will eventually throw actual exception, not aggregated one
public override string WorkloadMethodDelegate(string passArguments)
=> $"({passArguments}) => {{ {Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult(); }}";
=> $"({passArguments}) => {{ BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}";
public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()";
public override string GetWorkloadMethodCall(string passArguments) => $"BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))";
protected override Type WorkloadMethodReturnType => typeof(void);
}
@ -168,11 +166,9 @@ namespace BenchmarkDotNet.Code
protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single();
// we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
// and will eventually throw actual exception, not aggregated one
public override string WorkloadMethodDelegate(string passArguments)
=> $"({passArguments}) => {{ return {Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult(); }}";
=> $"({passArguments}) => {{ return BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}";
public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()";
public override string GetWorkloadMethodCall(string passArguments) => $"BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))";
}
}

Просмотреть файл

@ -0,0 +1,108 @@
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace BenchmarkDotNet.Helpers
{
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>(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<T>(Task<T> 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<T>(ValueTask<T> 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(AwaitHelper.GetResult), BindingFlags.Public | BindingFlags.Static, null, new Type[1] { taskType }, null);
}
Type compareType = taskType.GetGenericTypeDefinition() == typeof(ValueTask<>) ? typeof(ValueTask<>)
: typeof(Task).IsAssignableFrom(taskType.GetGenericTypeDefinition()) ? 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(AwaitHelper.GetResult)) return false;
Type paramType = m.GetParameters().First().ParameterType;
return paramType.IsGenericType && paramType.GetGenericTypeDefinition() == compareType;
})
.MakeGenericMethod(new[] { resultType });
}
}
}

Просмотреть файл

@ -1,5 +1,7 @@
using BenchmarkDotNet.Engines;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
@ -16,28 +18,24 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
OriginMethodReturnType = methodReturnType;
// Please note this code does not support await over extension methods.
var getAwaiterMethod = methodReturnType.GetMethod(nameof(Task<int>.GetAwaiter), BindingFlagsPublicInstance);
if (getAwaiterMethod == null)
// Only support (Value)Task for parity with other toolchains (and so we can use AwaitHelper).
IsAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask)
|| (methodReturnType.GetTypeInfo().IsGenericType
&& (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
|| methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>)));
if (!IsAwaitable)
{
WorkloadMethodReturnType = methodReturnType;
}
else
{
var getResultMethod = getAwaiterMethod
WorkloadMethodReturnType = methodReturnType
.GetMethod(nameof(Task.GetAwaiter), BindingFlagsPublicInstance)
.ReturnType
.GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance);
if (getResultMethod == null)
{
WorkloadMethodReturnType = methodReturnType;
}
else
{
WorkloadMethodReturnType = getResultMethod.ReturnType;
GetAwaiterMethod = getAwaiterMethod;
GetResultMethod = getResultMethod;
}
.GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance)
.ReturnType;
GetResultMethod = Helpers.AwaitHelper.GetGetResultMethod(methodReturnType);
}
if (WorkloadMethodReturnType == null)
@ -74,7 +72,6 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
public Type WorkloadMethodReturnType { get; }
public Type OverheadMethodReturnType { get; }
public MethodInfo? GetAwaiterMethod { get; }
public MethodInfo? GetResultMethod { get; }
public bool IsVoid { get; }
@ -82,6 +79,6 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
public bool IsConsumable { get; }
public FieldInfo? WorkloadConsumableField { get; }
public bool IsAwaitable => GetAwaiterMethod != null && GetResultMethod != null;
public bool IsAwaitable { get; }
}
}

Просмотреть файл

@ -434,7 +434,7 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
Type argLocalsType;
Type argFieldType;
MethodInfo? opConversion = null;
MethodInfo opConversion = null;
if (parameterType.IsByRef)
{
argLocalsType = parameterType;
@ -582,42 +582,28 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
workloadInvokeMethod.ReturnParameter,
args);
args = methodBuilder.GetEmitParameters(args);
var callResultType = consumableInfo.OriginMethodReturnType;
var awaiterType = consumableInfo.GetAwaiterMethod?.ReturnType
?? throw new InvalidOperationException($"Bug: {nameof(consumableInfo.GetAwaiterMethod)} is null");
var ilBuilder = methodBuilder.GetILGenerator();
/*
.locals init (
[0] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int32>
)
*/
var callResultLocal =
ilBuilder.DeclareOptionalLocalForInstanceCall(callResultType, consumableInfo.GetAwaiterMethod);
var awaiterLocal =
ilBuilder.DeclareOptionalLocalForInstanceCall(awaiterType, consumableInfo.GetResultMethod);
/*
// return TaskSample(arg0). ... ;
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: call instance class [mscorlib]System.Threading.Tasks.Task`1<int32> [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::TaskSample(int64)
*/
IL_0026: ldarg.0
IL_0027: ldloc.0
IL_0028: ldloc.1
IL_0029: ldloc.2
IL_002a: ldloc.3
IL_002b: call instance class [System.Private.CoreLib]System.Threading.Tasks.Task`1<object> BenchmarkDotNet.Helpers.Runnable_0::WorkloadMethod(string, string, string, string)
*/
if (!Descriptor.WorkloadMethod.IsStatic)
ilBuilder.Emit(OpCodes.Ldarg_0);
ilBuilder.EmitLdargs(args);
ilBuilder.Emit(OpCodes.Call, Descriptor.WorkloadMethod);
/*
// ... .GetAwaiter().GetResult();
IL_0007: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<!0> class [mscorlib]System.Threading.Tasks.Task`1<int32>::GetAwaiter()
IL_000c: stloc.0
IL_000d: ldloca.s 0
IL_000f: call instance !0 valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int32>::GetResult()
*/
ilBuilder.EmitInstanceCallThisValueOnStack(callResultLocal, consumableInfo.GetAwaiterMethod);
ilBuilder.EmitInstanceCallThisValueOnStack(awaiterLocal, consumableInfo.GetResultMethod);
// BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...);
IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult<int32>(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1<!!0>)
*/
ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod);
/*
IL_0014: ret
@ -833,19 +819,6 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
var skipFirstArg = workloadMethod.IsStatic;
var argLocals = EmitDeclareArgLocals(ilBuilder, skipFirstArg);
LocalBuilder? callResultLocal = null;
LocalBuilder? awaiterLocal = null;
if (consumableInfo.IsAwaitable)
{
var callResultType = consumableInfo.OriginMethodReturnType;
var awaiterType = consumableInfo.GetAwaiterMethod?.ReturnType
?? throw new InvalidOperationException($"Bug: {nameof(consumableInfo.GetAwaiterMethod)} is null");
callResultLocal =
ilBuilder.DeclareOptionalLocalForInstanceCall(callResultType, consumableInfo.GetAwaiterMethod);
awaiterLocal =
ilBuilder.DeclareOptionalLocalForInstanceCall(awaiterType, consumableInfo.GetResultMethod);
}
consumeEmitter.DeclareDisassemblyDiagnoserLocals(ilBuilder);
var notElevenLabel = ilBuilder.DefineLabel();
@ -870,29 +843,27 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation
EmitLoadArgFieldsToLocals(ilBuilder, argLocals, skipFirstArg);
/*
// return TaskSample(_argField) ... ;
IL_0011: ldarg.0
IL_0012: ldloc.0
IL_0013: call instance class [mscorlib]System.Threading.Tasks.Task`1<int32> [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::TaskSample(int64)
IL_0018: ret
IL_0026: ldarg.0
IL_0027: ldloc.0
IL_0028: ldloc.1
IL_0029: ldloc.2
IL_002a: ldloc.3
IL_002b: call instance class [System.Private.CoreLib]System.Threading.Tasks.Task`1<object> BenchmarkDotNet.Helpers.Runnable_0::WorkloadMethod(string, string, string, string)
*/
if (!workloadMethod.IsStatic)
{
ilBuilder.Emit(OpCodes.Ldarg_0);
}
ilBuilder.EmitLdLocals(argLocals);
ilBuilder.Emit(OpCodes.Call, workloadMethod);
if (consumableInfo.IsAwaitable)
{
/*
// ... .GetAwaiter().GetResult();
IL_0007: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<!0> class [mscorlib]System.Threading.Tasks.Task`1<int32>::GetAwaiter()
IL_000c: stloc.0
IL_000d: ldloca.s 0
IL_000f: call instance !0 valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<int32>::GetResult()
*/
ilBuilder.EmitInstanceCallThisValueOnStack(callResultLocal, consumableInfo.GetAwaiterMethod);
ilBuilder.EmitInstanceCallThisValueOnStack(awaiterLocal, consumableInfo.GetResultMethod);
// BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...);
IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult<int32>(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1<!!0>)
*/
ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod);
}
/*

Просмотреть файл

@ -118,7 +118,7 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit
private void Overhead() { }
// must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate
private void ExecuteBlocking() => startTaskCallback.Invoke().GetAwaiter().GetResult();
private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke());
[MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
private void WorkloadActionUnroll(long repeatCount)
@ -165,7 +165,7 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit
private T Overhead() => default;
// must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate
private T ExecuteBlocking() => startTaskCallback().GetAwaiter().GetResult();
private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke());
private void InvokeSingleHardcoded() => result = callback();
@ -217,7 +217,7 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit
private T Overhead() => default;
// must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate
private T ExecuteBlocking() => startTaskCallback().GetAwaiter().GetResult();
private T ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke());
private void InvokeSingleHardcoded() => result = callback();

Просмотреть файл

@ -5,6 +5,7 @@ using System.Reflection;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Running;
namespace BenchmarkDotNet.Validators
@ -130,21 +131,8 @@ namespace BenchmarkDotNet.Validators
return;
}
var returnType = result.GetType();
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
{
var asTaskMethod = result.GetType().GetMethod("AsTask");
result = asTaskMethod.Invoke(result, null);
}
if (result is Task task)
{
task.GetAwaiter().GetResult();
}
else if (result is ValueTask valueTask)
{
valueTask.GetAwaiter().GetResult();
}
AwaitHelper.GetGetResultMethod(result.GetType())
?.Invoke(null, new[] { result });
}
private bool TryToSetParamsFields(object benchmarkTypeInstance, List<ValidationError> errors)

Просмотреть файл

@ -48,13 +48,20 @@ namespace BenchmarkDotNet.IntegrationTests
private static string[] GetActualLogLines(Summary summary)
=> GetSingleStandardOutput(summary).Where(line => line.StartsWith(Prefix)).ToArray();
[Fact]
public void AllSetupAndCleanupMethodRunsTest()
[Theory]
[InlineData(typeof(AllSetupAndCleanupAttributeBenchmarks))]
[InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksTask))]
[InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksGenericTask))]
[InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksValueTask))]
[InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksGenericValueTask))]
[InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksValueTaskSource))]
[InlineData(typeof(AllSetupAndCleanupAttributeBenchmarksGenericValueTaskSource))]
public void AllSetupAndCleanupMethodRunsTest(Type benchmarkType)
{
var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob");
var config = CreateSimpleConfig(job: miniJob);
var summary = CanExecute<AllSetupAndCleanupAttributeBenchmarks>(config);
var summary = CanExecute(benchmarkType, config);
var actualLogLines = GetActualLogLines(summary);
foreach (string line in actualLogLines)
@ -83,56 +90,7 @@ namespace BenchmarkDotNet.IntegrationTests
public void Benchmark() => Console.WriteLine(BenchmarkCalled);
}
[Fact]
public void AllSetupAndCleanupMethodRunsAsyncTest()
{
var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob");
var config = CreateSimpleConfig(job: miniJob);
var summary = CanExecute<AllSetupAndCleanupAttributeBenchmarksAsync>(config);
var actualLogLines = GetActualLogLines(summary);
foreach (string line in actualLogLines)
Output.WriteLine(line);
Assert.Equal(expectedLogLines, actualLogLines);
}
public class AllSetupAndCleanupAttributeBenchmarksAsync
{
private int setupCounter;
private int cleanupCounter;
[IterationSetup]
public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")");
[IterationCleanup]
public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")");
[GlobalSetup]
public Task GlobalSetup() => Console.Out.WriteLineAsync(GlobalSetupCalled);
[GlobalCleanup]
public Task GlobalCleanup() => Console.Out.WriteLineAsync(GlobalCleanupCalled);
[Benchmark]
public Task Benchmark() => Console.Out.WriteLineAsync(BenchmarkCalled);
}
[Fact]
public void AllSetupAndCleanupMethodRunsAsyncTaskSetupTest()
{
var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob");
var config = CreateSimpleConfig(job: miniJob);
var summary = CanExecute<AllSetupAndCleanupAttributeBenchmarksAsyncTaskSetup>(config);
var actualLogLines = GetActualLogLines(summary);
foreach (string line in actualLogLines)
Output.WriteLine(line);
Assert.Equal(expectedLogLines, actualLogLines);
}
public class AllSetupAndCleanupAttributeBenchmarksAsyncTaskSetup
public class AllSetupAndCleanupAttributeBenchmarksTask
{
private int setupCounter;
private int cleanupCounter;
@ -153,21 +111,7 @@ namespace BenchmarkDotNet.IntegrationTests
public void Benchmark() => Console.WriteLine(BenchmarkCalled);
}
[Fact]
public void AllSetupAndCleanupMethodRunsAsyncGenericTaskSetupTest()
{
var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob");
var config = CreateSimpleConfig(job: miniJob);
var summary = CanExecute<AllSetupAndCleanupAttributeBenchmarksAsyncGenericTaskSetup>(config);
var actualLogLines = GetActualLogLines(summary);
foreach (string line in actualLogLines)
Output.WriteLine(line);
Assert.Equal(expectedLogLines, actualLogLines);
}
public class AllSetupAndCleanupAttributeBenchmarksAsyncGenericTaskSetup
public class AllSetupAndCleanupAttributeBenchmarksGenericTask
{
private int setupCounter;
private int cleanupCounter;
@ -198,21 +142,7 @@ namespace BenchmarkDotNet.IntegrationTests
public void Benchmark() => Console.WriteLine(BenchmarkCalled);
}
[Fact]
public void AllSetupAndCleanupMethodRunsAsyncValueTaskSetupTest()
{
var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob");
var config = CreateSimpleConfig(job: miniJob);
var summary = CanExecute<AllSetupAndCleanupAttributeBenchmarksAsyncValueTaskSetup>(config);
var actualLogLines = GetActualLogLines(summary);
foreach (string line in actualLogLines)
Output.WriteLine(line);
Assert.Equal(expectedLogLines, actualLogLines);
}
public class AllSetupAndCleanupAttributeBenchmarksAsyncValueTaskSetup
public class AllSetupAndCleanupAttributeBenchmarksValueTask
{
private int setupCounter;
private int cleanupCounter;
@ -233,21 +163,7 @@ namespace BenchmarkDotNet.IntegrationTests
public void Benchmark() => Console.WriteLine(BenchmarkCalled);
}
[FactEnvSpecific(EnvRequirement.NonWindows)]
public void AllSetupAndCleanupMethodRunsAsyncGenericValueTaskSetupTest()
{
var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob");
var config = CreateSimpleConfig(job: miniJob);
var summary = CanExecute<AllSetupAndCleanupAttributeBenchmarksAsyncGenericValueTaskSetup>(config);
var actualLogLines = GetActualLogLines(summary);
foreach (string line in actualLogLines)
Output.WriteLine(line);
Assert.Equal(expectedLogLines, actualLogLines);
}
public class AllSetupAndCleanupAttributeBenchmarksAsyncGenericValueTaskSetup
public class AllSetupAndCleanupAttributeBenchmarksGenericValueTask
{
private int setupCounter;
private int cleanupCounter;
@ -277,5 +193,69 @@ namespace BenchmarkDotNet.IntegrationTests
[Benchmark]
public void Benchmark() => Console.WriteLine(BenchmarkCalled);
}
public class AllSetupAndCleanupAttributeBenchmarksValueTaskSource
{
private readonly ValueTaskSource<int> valueTaskSource = new ();
private int setupCounter;
private int cleanupCounter;
[IterationSetup]
public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")");
[IterationCleanup]
public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")");
[GlobalSetup]
public ValueTask GlobalSetup()
{
valueTaskSource.Reset();
Console.Out.WriteLineAsync(GlobalSetupCalled).ContinueWith(_ => valueTaskSource.SetResult(42));
return new ValueTask(valueTaskSource, valueTaskSource.Token);
}
[GlobalCleanup]
public ValueTask GlobalCleanup()
{
valueTaskSource.Reset();
Console.Out.WriteLineAsync(GlobalCleanupCalled).ContinueWith(_ => valueTaskSource.SetResult(42));
return new ValueTask(valueTaskSource, valueTaskSource.Token);
}
[Benchmark]
public void Benchmark() => Console.WriteLine(BenchmarkCalled);
}
public class AllSetupAndCleanupAttributeBenchmarksGenericValueTaskSource
{
private readonly ValueTaskSource<int> valueTaskSource = new ();
private int setupCounter;
private int cleanupCounter;
[IterationSetup]
public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")");
[IterationCleanup]
public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")");
[GlobalSetup]
public ValueTask<int> GlobalSetup()
{
valueTaskSource.Reset();
Console.Out.WriteLineAsync(GlobalSetupCalled).ContinueWith(_ => valueTaskSource.SetResult(42));
return new ValueTask<int>(valueTaskSource, valueTaskSource.Token);
}
[GlobalCleanup]
public ValueTask<int> GlobalCleanup()
{
valueTaskSource.Reset();
Console.Out.WriteLineAsync(GlobalCleanupCalled).ContinueWith(_ => valueTaskSource.SetResult(42));
return new ValueTask<int>(valueTaskSource, valueTaskSource.Token);
}
[Benchmark]
public void Benchmark() => Console.WriteLine(BenchmarkCalled);
}
}
}

Просмотреть файл

@ -1,10 +1,44 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
using BenchmarkDotNet.Attributes;
using Xunit;
using Xunit.Abstractions;
namespace BenchmarkDotNet.IntegrationTests
{
internal class ValueTaskSource<T> : IValueTaskSource<T>, IValueTaskSource
{
private ManualResetValueTaskSourceCore<T> _core;
T IValueTaskSource<T>.GetResult(short token) => _core.GetResult(token);
void IValueTaskSource.GetResult(short token) => _core.GetResult(token);
ValueTaskSourceStatus IValueTaskSource<T>.GetStatus(short token) => _core.GetStatus(token);
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token);
void IValueTaskSource<T>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
void IValueTaskSource.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
public void Reset() => _core.Reset();
public short Token => _core.Version;
public void SetResult(T result) => _core.SetResult(result);
}
// This is used to test the case of ValueTaskAwaiter.IsCompleted returns false, then OnCompleted invokes the callback immediately because it happened to complete between the 2 calls.
internal class ValueTaskSourceCallbackOnly<T> : IValueTaskSource<T>, IValueTaskSource
{
private ManualResetValueTaskSourceCore<T> _core;
T IValueTaskSource<T>.GetResult(short token) => _core.GetResult(token);
void IValueTaskSource.GetResult(short token) => _core.GetResult(token);
// Always return pending state so OnCompleted will be called.
ValueTaskSourceStatus IValueTaskSource<T>.GetStatus(short token) => ValueTaskSourceStatus.Pending;
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => ValueTaskSourceStatus.Pending;
void IValueTaskSource<T>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
void IValueTaskSource.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
public void Reset() => _core.Reset();
public short Token => _core.Version;
public void SetResult(T result) => _core.SetResult(result);
}
public class AsyncBenchmarksTests : BenchmarkTestExecutor
{
public AsyncBenchmarksTests(ITestOutputHelper output) : base(output) { }
@ -24,8 +58,13 @@ namespace BenchmarkDotNet.IntegrationTests
}
}
[Fact]
public void TaskReturningMethodsAreAwaited_AlreadyComplete() => CanExecute<TaskImmediateMethods>();
public class TaskDelayMethods
{
private readonly ValueTaskSource<int> valueTaskSource = new ();
private const int MillisecondsDelay = 100;
internal const double NanosecondsDelay = MillisecondsDelay * 1e+6;
@ -39,6 +78,17 @@ namespace BenchmarkDotNet.IntegrationTests
[Benchmark]
public ValueTask ReturningValueTask() => new ValueTask(Task.Delay(MillisecondsDelay));
[Benchmark]
public ValueTask ReturningValueTaskBackByIValueTaskSource()
{
valueTaskSource.Reset();
Task.Delay(MillisecondsDelay).ContinueWith(_ =>
{
valueTaskSource.SetResult(default);
});
return new ValueTask(valueTaskSource, valueTaskSource.Token);
}
[Benchmark]
public async Task Awaiting() => await Task.Delay(MillisecondsDelay);
@ -47,6 +97,70 @@ namespace BenchmarkDotNet.IntegrationTests
[Benchmark]
public ValueTask<int> ReturningGenericValueTask() => new ValueTask<int>(ReturningGenericTask());
[Benchmark]
public ValueTask<int> ReturningGenericValueTaskBackByIValueTaskSource()
{
valueTaskSource.Reset();
Task.Delay(MillisecondsDelay).ContinueWith(_ =>
{
valueTaskSource.SetResult(default);
});
return new ValueTask<int>(valueTaskSource, valueTaskSource.Token);
}
}
public class TaskImmediateMethods
{
private readonly ValueTaskSource<int> valueTaskSource = new ();
private readonly ValueTaskSourceCallbackOnly<int> valueTaskSourceCallbackOnly = new ();
[Benchmark]
public Task ReturningTask() => Task.CompletedTask;
[Benchmark]
public ValueTask ReturningValueTask() => new ValueTask();
[Benchmark]
public ValueTask ReturningValueTaskBackByIValueTaskSource()
{
valueTaskSource.Reset();
valueTaskSource.SetResult(default);
return new ValueTask(valueTaskSource, valueTaskSource.Token);
}
[Benchmark]
public ValueTask ReturningValueTaskBackByIValueTaskSource_ImmediateCallback()
{
valueTaskSourceCallbackOnly.Reset();
valueTaskSourceCallbackOnly.SetResult(default);
return new ValueTask(valueTaskSourceCallbackOnly, valueTaskSourceCallbackOnly.Token);
}
[Benchmark]
public async Task Awaiting() => await Task.CompletedTask;
[Benchmark]
public Task<int> ReturningGenericTask() => ReturningTask().ContinueWith(_ => default(int));
[Benchmark]
public ValueTask<int> ReturningGenericValueTask() => new ValueTask<int>(ReturningGenericTask());
[Benchmark]
public ValueTask<int> ReturningGenericValueTaskBackByIValueTaskSource()
{
valueTaskSource.Reset();
valueTaskSource.SetResult(default);
return new ValueTask<int>(valueTaskSource, valueTaskSource.Token);
}
[Benchmark]
public ValueTask<int> ReturningGenericValueTaskBackByIValueTaskSource_ImmediateCallback()
{
valueTaskSourceCallbackOnly.Reset();
valueTaskSourceCallbackOnly.SetResult(default);
return new ValueTask<int>(valueTaskSourceCallbackOnly, valueTaskSourceCallbackOnly.Token);
}
}
}
}

Просмотреть файл

@ -153,6 +153,13 @@ namespace BenchmarkDotNet.IntegrationTests
Interlocked.Increment(ref Counter);
}
[Benchmark]
public async ValueTask InvokeOnceValueTaskAsync()
{
await Task.Yield();
Interlocked.Increment(ref Counter);
}
[Benchmark]
public string InvokeOnceRefType()
{
@ -195,6 +202,13 @@ namespace BenchmarkDotNet.IntegrationTests
Interlocked.Increment(ref Counter);
}
[Benchmark]
public static async ValueTask InvokeOnceStaticValueTaskAsync()
{
await Task.Yield();
Interlocked.Increment(ref Counter);
}
[Benchmark]
public static string InvokeOnceStaticRefType()
{

Просмотреть файл

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Validators;
@ -563,5 +564,82 @@ namespace BenchmarkDotNet.Tests.Validators
[Benchmark]
public void NonThrowing() { }
}
private class ValueTaskSource<T> : IValueTaskSource<T>, IValueTaskSource
{
private ManualResetValueTaskSourceCore<T> _core;
T IValueTaskSource<T>.GetResult(short token) => _core.GetResult(token);
void IValueTaskSource.GetResult(short token) => _core.GetResult(token);
ValueTaskSourceStatus IValueTaskSource<T>.GetStatus(short token) => _core.GetStatus(token);
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token);
void IValueTaskSource<T>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
void IValueTaskSource.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
public void Reset() => _core.Reset();
public short Token => _core.Version;
public void SetResult(T result) => _core.SetResult(result);
}
[Fact]
public void AsyncValueTaskBackedByIValueTaskSourceIsAwaitedProperly()
{
var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncValueTaskSource))).ToList();
Assert.True(AsyncValueTaskSource.WasCalled);
Assert.Empty(validationErrors);
}
public class AsyncValueTaskSource
{
private readonly ValueTaskSource<bool> valueTaskSource = new ();
public static bool WasCalled;
[GlobalSetup]
public ValueTask GlobalSetup()
{
valueTaskSource.Reset();
Task.Delay(1).ContinueWith(_ =>
{
WasCalled = true;
valueTaskSource.SetResult(true);
});
return new ValueTask(valueTaskSource, valueTaskSource.Token);
}
[Benchmark]
public void NonThrowing() { }
}
[Fact]
public void AsyncGenericValueTaskBackedByIValueTaskSourceIsAwaitedProperly()
{
var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericValueTaskSource))).ToList();
Assert.True(AsyncGenericValueTaskSource.WasCalled);
Assert.Empty(validationErrors);
}
public class AsyncGenericValueTaskSource
{
private readonly ValueTaskSource<int> valueTaskSource = new ();
public static bool WasCalled;
[GlobalSetup]
public ValueTask<int> GlobalSetup()
{
valueTaskSource.Reset();
Task.Delay(1).ContinueWith(_ =>
{
WasCalled = true;
valueTaskSource.SetResult(1);
});
return new ValueTask<int>(valueTaskSource, valueTaskSource.Token);
}
[Benchmark]
public void NonThrowing() { }
}
}
}