Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom awaitable types #2349

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5720494
Added AwaitHelper to properly wait for ValueTasks.
timcassell Sep 19, 2022
4b3d20a
Adjust `AwaitHelper` to allow multiple threads to use it concurrently.
timcassell Sep 25, 2022
f0acf70
Changed AwaitHelper to static.
timcassell Sep 26, 2022
c4a07b0
Fix async GlobalSetup/GlobalCleanup not being awaited with InProcessE…
timcassell Sep 19, 2022
4d6159c
Added missing ValueTask (non-generic) to InProcess (no emit) toolchains.
timcassell Sep 19, 2022
c43d879
Merge branch 'valuetask-nongeneric-inprocess' into temp
timcassell Sep 26, 2022
9a6c74f
Refactored delegates to pass in IClock and return ValueTask<ClockSpan>.
timcassell Sep 19, 2022
ce3b1c7
Merge branch 'master' into valuetask-nongeneric-inprocess
timcassell Oct 20, 2022
792662e
Merge branch 'valuetask-nongeneric-inprocess' into reduce-async-overh…
timcassell Oct 20, 2022
d3b1607
Merge branch 'master' into reduce-async-overhead-new
timcassell Jan 2, 2023
85a6c47
Clean up duplicated code.
timcassell Jan 21, 2023
299d279
Merge branch 'master' into reduce-async-overhead-new
timcassell Jun 16, 2023
3c398d0
Experimental custom async method builder support.
timcassell Jun 24, 2023
61de07f
Added config option to add async consumers.
timcassell Jun 27, 2023
a4e72c8
Corrected AsyncBenchmarkRunner type in generated code.
timcassell Jun 28, 2023
4242bee
Split IAsyncConsumer to IAsyncVoidConsumer and IAsyncResultConsumer.
timcassell Jun 29, 2023
fbbb467
Pass arguments directly if awaitable.
timcassell Jun 29, 2023
b976a8e
Separated awaitable adapter and async method builder adapter.
timcassell Jul 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions src/BenchmarkDotNet/Code/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text;
using System.Threading.Tasks;
using BenchmarkDotNet.Characteristics;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Disassemblers;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Extensions;
Expand All @@ -33,7 +34,9 @@ internal static string Generate(BuildPartition buildPartition)
{
var benchmark = buildInfo.BenchmarkCase;

var provider = GetDeclarationsProvider(benchmark.Descriptor);
var provider = GetDeclarationsProvider(benchmark.Descriptor, benchmark.Config);

provider.OverrideUnrollFactor(benchmark);

string passArguments = GetPassArguments(benchmark);

Expand All @@ -49,6 +52,7 @@ internal static string Generate(BuildPartition buildPartition)
.Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName)
.Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers)
.Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName)
.Replace("$InitializeAsyncBenchmarkRunnerFields$", provider.GetInitializeAsyncBenchmarkRunnerFields(buildInfo.Id))
.Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName)
.Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName)
.Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName)
Expand All @@ -59,8 +63,10 @@ internal static string Generate(BuildPartition buildPartition)
.Replace("$ParamsContent$", GetParamsContent(benchmark))
.Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark))
.Replace("$DeclareArgumentFields$", GetDeclareArgumentFields(benchmark))
.Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark)).Replace("$LoadArguments$", GetLoadArguments(benchmark))
.Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark))
.Replace("$LoadArguments$", GetLoadArguments(benchmark))
.Replace("$PassArguments$", passArguments)
.Replace("$PassArgumentsDirect$", GetPassArgumentsDirect(benchmark))
.Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark))
.Replace("$MeasureExtraStats$", buildInfo.Config.HasExtraStatsDiagnoser() ? "true" : "false")
.Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName)
Expand Down Expand Up @@ -148,21 +154,10 @@ private static string GetJobsSetDefinition(BenchmarkCase benchmarkCase)
Replace("; ", ";\n ");
}

private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor)
private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor, IConfig config)
{
var method = descriptor.WorkloadMethod;

if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask))
{
return new TaskDeclarationsProvider(descriptor);
}
if (method.ReturnType.GetTypeInfo().IsGenericType
&& (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>)
|| method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>)))
{
return new GenericTaskDeclarationsProvider(descriptor);
}

if (method.ReturnType == typeof(void))
{
bool isUsingAsyncKeyword = method.HasAttribute<AsyncStateMachineAttribute>();
Expand All @@ -183,6 +178,11 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto
return new ByRefDeclarationsProvider(descriptor);
}

if (config.GetIsAwaitable(method.ReturnType, out var adapter))
{
return new AwaitableDeclarationsProvider(descriptor, adapter);
}

return new NonVoidDeclarationsProvider(descriptor);
}

Expand Down Expand Up @@ -224,6 +224,12 @@ private static string GetPassArguments(BenchmarkCase benchmarkCase)
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
.Select((parameter, index) => $"{GetParameterModifier(parameter)} arg{index}"));

private static string GetPassArgumentsDirect(BenchmarkCase benchmarkCase)
=> string.Join(
", ",
benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
.Select((parameter, index) => $"{GetParameterModifier(parameter)} __argField{index}"));

private static string GetExtraAttributes(Descriptor descriptor)
=> descriptor.WorkloadMethod.GetCustomAttributes(false).OfType<STAThreadAttribute>().Any() ? "[System.STAThreadAttribute]" : string.Empty;

Expand Down
69 changes: 40 additions & 29 deletions src/BenchmarkDotNet/Code/DeclarationsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
Expand All @@ -11,9 +12,6 @@ namespace BenchmarkDotNet.Code
{
internal abstract class DeclarationsProvider
{
// "GlobalSetup" or "GlobalCleanup" methods are optional, so default to an empty delegate, so there is always something that can be invoked
private const string EmptyAction = "() => { }";

protected readonly Descriptor Descriptor;

internal DeclarationsProvider(Descriptor descriptor) => Descriptor = descriptor;
Expand All @@ -26,9 +24,9 @@ internal abstract class DeclarationsProvider

public string GlobalCleanupMethodName => GetMethodName(Descriptor.GlobalCleanupMethod);

public string IterationSetupMethodName => Descriptor.IterationSetupMethod?.Name ?? EmptyAction;
public string IterationSetupMethodName => GetMethodName(Descriptor.IterationSetupMethod);

public string IterationCleanupMethodName => Descriptor.IterationCleanupMethod?.Name ?? EmptyAction;
public string IterationCleanupMethodName => GetMethodName(Descriptor.IterationCleanupMethod);

public abstract string ReturnsDefinition { get; }

Expand All @@ -48,13 +46,18 @@ internal abstract class DeclarationsProvider

public string OverheadMethodReturnTypeName => OverheadMethodReturnType.GetCorrectCSharpTypeName();

public virtual string GetInitializeAsyncBenchmarkRunnerFields(BenchmarkId id) => null;

public virtual void OverrideUnrollFactor(BenchmarkCase benchmarkCase) { }

public abstract string OverheadImplementation { get; }

private string GetMethodName(MethodInfo method)
{
// "Setup" or "Cleanup" methods are optional, so default to a simple delegate, so there is always something that can be invoked
if (method == null)
{
return EmptyAction;
return "() => new System.Threading.Tasks.ValueTask()";
}

if (method.ReturnType == typeof(Task) ||
Expand All @@ -63,10 +66,10 @@ private string GetMethodName(MethodInfo method)
(method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) ||
method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))))
{
return $"() => {method.Name}().GetAwaiter().GetResult()";
return $"() => BenchmarkDotNet.Helpers.AwaitHelper.ToValueTaskVoid({method.Name}())";
}

return method.Name;
return $"() => {{ {method.Name}(); return new System.Threading.Tasks.ValueTask(); }}";
}
}

Expand Down Expand Up @@ -145,34 +148,42 @@ internal class ByReadOnlyRefDeclarationsProvider : ByRefDeclarationsProvider
public override string WorkloadMethodReturnTypeModifiers => "ref readonly";
}

internal class TaskDeclarationsProvider : VoidDeclarationsProvider
internal class AwaitableDeclarationsProvider : DeclarationsProvider
{
public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
private readonly ConcreteAsyncAdapter adapter;

// 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(); }}";
public AwaitableDeclarationsProvider(Descriptor descriptor, ConcreteAsyncAdapter adapter) : base(descriptor) => this.adapter = adapter;

public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()";
public override string ReturnsDefinition => "RETURNS_AWAITABLE";

protected override Type WorkloadMethodReturnType => typeof(void);
}
public override string OverheadImplementation => $"return default({OverheadMethodReturnType.GetCorrectCSharpTypeName()});";

/// <summary>
/// declarations provider for <see cref="Task{TResult}" /> and <see cref="ValueTask{TResult}" />
/// </summary>
internal class GenericTaskDeclarationsProvider : NonVoidDeclarationsProvider
{
public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
protected override Type OverheadMethodReturnType => WorkloadMethodReturnType.IsValueType && !WorkloadMethodReturnType.IsPrimitive
? typeof(EmptyAwaiter) // we return this simple type so we don't include the cost of a large struct in the overhead
: WorkloadMethodReturnType;

protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single();
private string GetRunnableName(BenchmarkId id) => $"BenchmarkDotNet.Autogenerated.Runnable_{id}";

// 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(); }}";
public override string GetInitializeAsyncBenchmarkRunnerFields(BenchmarkId id)
{
string awaitableAdapterTypeName = adapter.awaitableAdapterType.GetCorrectCSharpTypeName();
string asyncMethodBuilderAdapterTypeName = adapter.asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName();

string awaiterTypeName = adapter.awaiterType.GetCorrectCSharpTypeName();
string overheadAwaiterTypeName = adapter.awaiterType.IsValueType
? typeof(EmptyAwaiter).GetCorrectCSharpTypeName() // we use this simple type so we don't include the cost of a large struct in the overhead
: awaiterTypeName;
string appendResultType = adapter.resultType == null ? string.Empty : $", {adapter.resultType.GetCorrectCSharpTypeName()}";

string runnableName = GetRunnableName(id);

var workloadRunnerTypeName = $"BenchmarkDotNet.Engines.AsyncWorkloadRunner<{runnableName}.WorkloadFunc, {asyncMethodBuilderAdapterTypeName}, {awaitableAdapterTypeName}, {WorkloadMethodReturnTypeName}, {awaiterTypeName}{appendResultType}>";
var overheadRunnerTypeName = $"BenchmarkDotNet.Engines.AsyncOverheadRunner<{runnableName}.OverheadFunc, {asyncMethodBuilderAdapterTypeName}, {OverheadMethodReturnTypeName}, {overheadAwaiterTypeName}{appendResultType}>";

return $"__asyncWorkloadRunner = new {workloadRunnerTypeName}(new {runnableName}.WorkloadFunc(this));" + Environment.NewLine
+ $" __asyncOverheadRunner = new {overheadRunnerTypeName}(new {runnableName}.OverheadFunc(this));";
}

public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()";
public override void OverrideUnrollFactor(BenchmarkCase benchmarkCase) => benchmarkCase.ForceUnrollFactorForAsync();
}
}
Loading
Loading