diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 52ae0e5478..65a0bd1030 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -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; @@ -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); @@ -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) @@ -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) @@ -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(); @@ -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); } @@ -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().Any() ? "[System.STAThreadAttribute]" : string.Empty; diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index ddf78eb572..cefd2ebef0 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -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; @@ -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; @@ -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; } @@ -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) || @@ -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(); }}"; } } @@ -145,34 +148,42 @@ public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descripto 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()});"; - /// - /// declarations provider for and - /// - 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(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/AsyncAdapterDefinition.cs b/src/BenchmarkDotNet/Configs/AsyncAdapterDefinition.cs new file mode 100644 index 0000000000..22185669e2 --- /dev/null +++ b/src/BenchmarkDotNet/Configs/AsyncAdapterDefinition.cs @@ -0,0 +1,230 @@ +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BenchmarkDotNet.Configs +{ + public sealed class AsyncAdapterDefinition : IComparable, IEquatable + { + public Type AwaitableType { get; private set; } + public Type AwaitableAdapterType { get; private set; } + public Type AsyncMethodBuilderAdapterType { get; private set; } + + private readonly Type[] awaitableAdapterInterfaceGenericArguments; + private readonly Type[] originalAwaitableGenericArguments; + private readonly int openGenericArgumentsCount; + + public AsyncAdapterDefinition(Type awaitableAdapterType, Type? asyncMethodBuilderAdapterType = null) + { + // If asyncMethodBuilderAdapterType is null, we use awaitableAdapterType if it implements IAsyncMethodBuilderAdapter, + // otherwise we fallback to AsyncTaskMethodBuilderAdapter. + asyncMethodBuilderAdapterType ??= awaitableAdapterType.GetInterfaces().Contains(typeof(IAsyncMethodBuilderAdapter)) + ? awaitableAdapterType + : typeof(AsyncTaskMethodBuilderAdapter); + AwaitableAdapterType = awaitableAdapterType; + AsyncMethodBuilderAdapterType = asyncMethodBuilderAdapterType; + + // Validate asyncMethodBuilderAdapterType + bool isPublic = asyncMethodBuilderAdapterType.IsPublic || asyncMethodBuilderAdapterType.IsNestedPublic; + if (!isPublic || (!asyncMethodBuilderAdapterType.IsValueType && asyncMethodBuilderAdapterType.GetConstructor(Array.Empty()) == null)) + { + throw new ArgumentException($"asyncMethodBuilderAdapterType [{asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName()}] is not a public struct, or a public class with a public, parameterless constructor."); + } + if (!asyncMethodBuilderAdapterType.GetInterfaces().Contains(typeof(IAsyncMethodBuilderAdapter))) + { + throw new ArgumentException($"asyncMethodBuilderAdapterType [{asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName()}] does not implement [{typeof(IAsyncMethodBuilderAdapter).GetCorrectCSharpTypeName()}]."); + } + + // Validate awaitableAdapterType + isPublic = asyncMethodBuilderAdapterType.IsPublic || asyncMethodBuilderAdapterType.IsNestedPublic; + if (!isPublic || (!asyncMethodBuilderAdapterType.IsValueType && asyncMethodBuilderAdapterType.GetConstructor(Array.Empty()) == null)) + { + throw new ArgumentException($"awaitableAdapterType [{asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName()}] is not a public struct, or a public class with a public, parameterless constructor."); + } + + // Must implement exactly 1 IAwaitableAdapter + var awaitableAdapterVoidInterfaces = awaitableAdapterType.GetInterfaces().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAwaitableAdapter<,>)).ToArray(); + var awaitableAdapterResultInterfaces = awaitableAdapterType.GetInterfaces().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IAwaitableAdapter<,,>)).ToArray(); + if (awaitableAdapterVoidInterfaces.Length + awaitableAdapterResultInterfaces.Length != 1) + { + string msg = awaitableAdapterVoidInterfaces.Length + awaitableAdapterResultInterfaces.Length == 0 + ? "does not implement" + : "implements more than one of"; + throw new ArgumentException($"awaitableAdapterType [{awaitableAdapterType.GetCorrectCSharpTypeName()}] {msg} [{typeof(IAwaitableAdapter<,>).GetCorrectCSharpTypeName()}] or [{typeof(IAwaitableAdapter<,,>).GetCorrectCSharpTypeName()}]."); + } + + // Retrieve the awaitable type from the IAwaitableAdapter interface + awaitableAdapterInterfaceGenericArguments = awaitableAdapterVoidInterfaces.Length == 1 + ? awaitableAdapterVoidInterfaces[0].GetGenericArguments() + : awaitableAdapterResultInterfaces[0].GetGenericArguments(); + + Type awaitableType = awaitableAdapterInterfaceGenericArguments[0]; + + // Validate open generics + // awaitableType and awaitableAdapterType must contain the exact same open generic types, + // while asyncMethodBuilderAdapterType can have the same or fewer open generic types. + // It doesn't matter if an open generic type is used multiple times (), as long as each distinct type matches. + originalAwaitableGenericArguments = awaitableType.GetGenericArguments(); + var awaitableOpenGenericTypes = new HashSet( + originalAwaitableGenericArguments.Where(t => t.IsGenericParameter) + ); + Type[] builderGenericTypes = asyncMethodBuilderAdapterType.GetGenericArguments(); + var builderOpenGenericTypes = new HashSet( + builderGenericTypes.Where(t => t.IsGenericParameter) + ); + Type[] awaitableAdapterGenericTypes = awaitableAdapterType.GetGenericArguments(); + foreach (var genericType in awaitableAdapterGenericTypes.Where(t => t.IsGenericParameter).Distinct()) + { + ++openGenericArgumentsCount; + if (!awaitableOpenGenericTypes.Remove(genericType)) + { + throw new ArgumentException($"awaitableAdapterType [{awaitableAdapterType.GetCorrectCSharpTypeName()}] has at least 1 open generic argument that is not contained in the awaitable type [{awaitableType.GetCorrectCSharpTypeName()}]."); + } + builderOpenGenericTypes.Remove(genericType); + } + if (awaitableOpenGenericTypes.Count > 0) + { + throw new ArgumentException($"awaitable type [{awaitableType.GetCorrectCSharpTypeName()}] has at least 1 open generic argument that is not defined in awaitableAdapterType [{awaitableAdapterType.GetCorrectCSharpTypeName()}]."); + } + if (builderOpenGenericTypes.Count > 0) + { + throw new ArgumentException($"asyncMethodBuilderAdapterType [{asyncMethodBuilderAdapterType.GetCorrectCSharpTypeName()}] has at least 1 open generic argument that is not defined in awaitableAdapterType [{awaitableAdapterType.GetCorrectCSharpTypeName()}]."); + } + + if (awaitableType.IsGenericType) + { + // Remap open generic types to their generic type definition (MyType becomes MyType) + // Get generic types from the generic type definition, then fill in the non-generic types. + var awaitableGenericTypeDefinition = awaitableType.GetGenericTypeDefinition(); + Type[] usableGenericTypes = awaitableGenericTypeDefinition.GetGenericArguments(); + for (int i = 0; i < usableGenericTypes.Length; ++i) + { + if (!originalAwaitableGenericArguments[i].IsGenericParameter) + { + usableGenericTypes[i] = originalAwaitableGenericArguments[i]; + } + } + AwaitableType = awaitableGenericTypeDefinition.MakeGenericType(usableGenericTypes); + } + else + { + AwaitableType = awaitableType; + } + } + + internal bool TryMatch(Type type, out ConcreteAsyncAdapter constructedAwaitableAdapter) + { + if (type == AwaitableType) + { + constructedAwaitableAdapter = new ConcreteAsyncAdapter() + { + awaitableType = AwaitableType, + awaiterType = awaitableAdapterInterfaceGenericArguments[1], + awaitableAdapterType = AwaitableAdapterType, + asyncMethodBuilderAdapterType = AsyncMethodBuilderAdapterType, + resultType = awaitableAdapterInterfaceGenericArguments.Length == 2 ? null : awaitableAdapterInterfaceGenericArguments[2] + }; + return true; + } + + if (!type.IsGenericType || !AwaitableType.IsGenericType) + { + constructedAwaitableAdapter = null; + return false; + } + if (type.GetGenericTypeDefinition() != AwaitableType.GetGenericTypeDefinition()) + { + constructedAwaitableAdapter = null; + return false; + } + + // Match against closed generic types, and build a map of open generics to concrete types. + Type[] actualGenericTypes = type.GetGenericArguments(); + var openToConcreteMap = new Dictionary(); + for (int i = 0; i < actualGenericTypes.Length; ++i) + { + Type originalGenericArgument = originalAwaitableGenericArguments[i]; + if (originalGenericArgument.IsGenericParameter) + { + openToConcreteMap[originalGenericArgument] = actualGenericTypes[i]; + continue; + } + if (originalGenericArgument != actualGenericTypes[i]) + { + constructedAwaitableAdapter = null; + return false; + } + } + + // Construct the concrete types using the mapped open-to-concrete types. + Type[] concreteAwaitableAdapterGenericArguments = MapToConcreteTypes(originalAwaitableGenericArguments, openToConcreteMap); + Type concreteAwaitableAdapterType = AwaitableAdapterType.GetGenericTypeDefinition().MakeGenericType(concreteAwaitableAdapterGenericArguments); + + Type concreteAsyncMethodBuilderAdapterType = AsyncMethodBuilderAdapterType; + if (AsyncMethodBuilderAdapterType.IsGenericType) + { + Type[] originalAsyncMethodBuilderAdapterGenericArguments = AsyncMethodBuilderAdapterType.GetGenericArguments(); + // Only construct the concrete asyncMethodBuilderAdapterType if it's not already concrete. + if (originalAsyncMethodBuilderAdapterGenericArguments.Any(t => t.IsGenericParameter)) + { + Type[] concreteAsyncMethodBuilderAdapterGenericArguments = MapToConcreteTypes(originalAsyncMethodBuilderAdapterGenericArguments, openToConcreteMap); + concreteAsyncMethodBuilderAdapterType = AsyncMethodBuilderAdapterType.GetGenericTypeDefinition().MakeGenericType(concreteAwaitableAdapterGenericArguments); + } + } + + Type[] concreteAwaitableAdapterInterfaceGenericArguments = concreteAwaitableAdapterType.GetInterfaces() + .Single(t => t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(IAwaitableAdapter<,>) || t.GetGenericTypeDefinition() == typeof(IAwaitableAdapter<,,>))) + .GetGenericArguments(); + constructedAwaitableAdapter = new ConcreteAsyncAdapter() + { + awaitableType = type, + awaiterType = concreteAwaitableAdapterInterfaceGenericArguments[1], + awaitableAdapterType = concreteAwaitableAdapterType, + asyncMethodBuilderAdapterType = concreteAsyncMethodBuilderAdapterType, + resultType = concreteAwaitableAdapterInterfaceGenericArguments.Length == 2 ? null : concreteAwaitableAdapterInterfaceGenericArguments[2] + }; + return true; + } + + private static Type[] MapToConcreteTypes(Type[] originalTypes, Dictionary openToConcreteMap) + { + Type[] concreteTypes = new Type[originalTypes.Length]; + originalTypes.CopyTo(concreteTypes, 0); + for (int i = 0; i < concreteTypes.Length; ++i) + { + Type genericType = concreteTypes[i]; + if (genericType.IsGenericParameter) + { + concreteTypes[i] = openToConcreteMap[genericType]; + } + } + return concreteTypes; + } + + // We override the default reference type hashing and equality algorithms to forward to AwaitableAdapterType. + // This makes AwaitableAdapterType the "key" when this type is added to a hashset. + public override int GetHashCode() + => AwaitableAdapterType.GetHashCode(); + + public override bool Equals(object obj) + => obj is AsyncAdapterDefinition def && Equals(def); + + public bool Equals(AsyncAdapterDefinition other) + => AwaitableAdapterType.Equals(other.AwaitableAdapterType); + + // We compare against types with fewer open generic types first. This prioritizes exact matches over constructed matches (Task is better than Task<>). + public int CompareTo(AsyncAdapterDefinition other) + => openGenericArgumentsCount.CompareTo(other.openGenericArgumentsCount); + } + + internal sealed class ConcreteAsyncAdapter + { + internal Type awaitableType; + internal Type awaiterType; + internal Type awaitableAdapterType; + internal Type asyncMethodBuilderAdapterType; + internal Type? resultType; + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs index fa0920c7f9..f0bba07ca9 100644 --- a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs +++ b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs @@ -1,13 +1,17 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Filters; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; @@ -115,6 +119,19 @@ public static class ConfigExtensions [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumn[] columns) => config.With(c => c.HideColumns(columns)); [PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumnHidingRule[] rules) => config.With(c => c.HideColumns(rules)); + /// + /// Adds an adapter to make a type awaitable. This type must implement or . + /// Optionally provide an async method builder adapter that will be used to consume the awaitable type. This type must implement . + /// If not provided, will be used if it implements , otherwise will be used. + /// + /// + /// If an adapter already exists for the corresponding awaitable type, it will be overridden. + /// + /// + [PublicAPI] + public static ManualConfig AddAsyncAdapter(this IConfig config, Type awaitableAdapterType, Type asyncMethodBuilderAdapterType = null) + => config.With(c => c.AddAsyncAdapter(awaitableAdapterType, asyncMethodBuilderAdapterType)); + public static ImmutableConfig CreateImmutableConfig(this IConfig config) => ImmutableConfigBuilder.Create(config); internal static ILogger GetNonNullCompositeLogger(this IConfig config) @@ -132,5 +149,22 @@ private static ManualConfig With(this IConfig config, Action addAc addAction(manualConfig); return manualConfig; } + + internal static bool GetIsAwaitable(this IConfig config, Type type, out ConcreteAsyncAdapter adapter) + { + var asyncAdapterDefinitions = new HashSet(DefaultConfig.DefaultAsyncAdapterDefinitions); + asyncAdapterDefinitions.AddRange(config.GetAsyncAdapterDefinitions()); + var arr = asyncAdapterDefinitions.ToArray(); + Array.Sort(arr); + foreach (var adapterDefinition in arr) + { + if (adapterDefinition.TryMatch(type, out adapter)) + { + return true; + } + } + adapter = null; + return false; + } } } diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index 2cbdff2461..2fe048ae8b 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Threading.Tasks; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Filters; using BenchmarkDotNet.Jobs; @@ -65,6 +67,7 @@ public abstract class DebugConfig : IConfig public IEnumerable GetHardwareCounters() => Array.Empty(); public IEnumerable GetFilters() => Array.Empty(); public IEnumerable GetColumnHidingRules() => Array.Empty(); + public IEnumerable GetAsyncAdapterDefinitions() => Array.Empty(); public IOrderer Orderer => DefaultOrderer.Instance; public ICategoryDiscoverer? CategoryDiscoverer => DefaultCategoryDiscoverer.Instance; diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index 8d1df6285b..7d8756d3c3 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Exporters.Csv; using BenchmarkDotNet.Filters; @@ -23,6 +25,15 @@ public class DefaultConfig : IConfig public static readonly IConfig Instance = new DefaultConfig(); private readonly static Conclusion[] emptyConclusion = Array.Empty(); + // AsyncAdapterDefinition can be expensive to create, so we cache the defaults. + internal static ImmutableArray DefaultAsyncAdapterDefinitions { get; } = new[] + { + new AsyncAdapterDefinition(typeof(TaskAdapter), typeof(AsyncTaskMethodBuilderAdapter)), + new AsyncAdapterDefinition(typeof(TaskAdapter<>), typeof(AsyncTaskMethodBuilderAdapter)), + new AsyncAdapterDefinition(typeof(ValueTaskAdapter), typeof(AsyncValueTaskMethodBuilderAdapter)), + new AsyncAdapterDefinition(typeof(ValueTaskAdapter<>), typeof(AsyncValueTaskMethodBuilderAdapter)) + }.ToImmutableArray(); + private DefaultConfig() { } @@ -109,5 +120,9 @@ public string ArtifactsPath public IEnumerable GetFilters() => Array.Empty(); public IEnumerable GetColumnHidingRules() => Array.Empty(); + + // We don't expose the default adapters to users, they get combined with user-supplied adapters when the actual benchmark types are constructed. + // This is necessary so the defaults won't override user-supplied adapters when combining configs. + public IEnumerable GetAsyncAdapterDefinitions() => Array.Empty(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/IAsyncMethodBuilderAdapter.cs b/src/BenchmarkDotNet/Configs/IAsyncMethodBuilderAdapter.cs new file mode 100644 index 0000000000..06256100bc --- /dev/null +++ b/src/BenchmarkDotNet/Configs/IAsyncMethodBuilderAdapter.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace BenchmarkDotNet.Configs +{ + public interface IAsyncMethodBuilderAdapter + { + public void CreateAsyncMethodBuilder(); + public void Start(ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine; + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine; + public void SetStateMachine(IAsyncStateMachine stateMachine); + public void SetResult(); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/IAwaitableAdapter.cs b/src/BenchmarkDotNet/Configs/IAwaitableAdapter.cs new file mode 100644 index 0000000000..bfdd6c2941 --- /dev/null +++ b/src/BenchmarkDotNet/Configs/IAwaitableAdapter.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; + +namespace BenchmarkDotNet.Configs +{ + public interface IAwaitableAdapter + where TAwaiter : ICriticalNotifyCompletion + { + public TAwaiter GetAwaiter(ref TAwaitable awaitable); + public bool GetIsCompleted(ref TAwaiter awaiter); + public void GetResult(ref TAwaiter awaiter); + } + + public interface IAwaitableAdapter + where TAwaiter : ICriticalNotifyCompletion + { + public TAwaiter GetAwaiter(ref TAwaitable awaitable); + public bool GetIsCompleted(ref TAwaiter awaiter); + public TResult GetResult(ref TAwaiter awaiter); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index 46a53692c2..9f83ed45c4 100644 --- a/src/BenchmarkDotNet/Configs/IConfig.cs +++ b/src/BenchmarkDotNet/Configs/IConfig.cs @@ -12,7 +12,6 @@ using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; -using JetBrains.Annotations; namespace BenchmarkDotNet.Configs { @@ -29,6 +28,7 @@ public interface IConfig IEnumerable GetFilters(); IEnumerable GetLogicalGroupRules(); IEnumerable GetColumnHidingRules(); + IEnumerable GetAsyncAdapterDefinitions(); IOrderer? Orderer { get; } ICategoryDiscoverer? CategoryDiscoverer { get; } diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index b5d84eaf56..782783e269 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -32,6 +32,7 @@ public sealed class ImmutableConfig : IConfig private readonly ImmutableHashSet filters; private readonly ImmutableArray rules; private readonly ImmutableArray columnHidingRules; + private readonly ImmutableHashSet awaitableAdapters; internal ImmutableConfig( ImmutableArray uniqueColumnProviders, @@ -44,6 +45,7 @@ internal ImmutableConfig( ImmutableHashSet uniqueFilters, ImmutableArray uniqueRules, ImmutableArray uniqueColumnHidingRules, + ImmutableHashSet uniqueAsyncConsumerTypes, ImmutableHashSet uniqueRunnableJobs, ConfigUnionRule unionRule, string artifactsPath, @@ -65,6 +67,7 @@ internal ImmutableConfig( filters = uniqueFilters; rules = uniqueRules; columnHidingRules = uniqueColumnHidingRules; + awaitableAdapters = uniqueAsyncConsumerTypes; jobs = uniqueRunnableJobs; UnionRule = unionRule; ArtifactsPath = artifactsPath; @@ -97,6 +100,7 @@ internal ImmutableConfig( public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => rules; public IEnumerable GetColumnHidingRules() => columnHidingRules; + public IEnumerable GetAsyncAdapterDefinitions() => awaitableAdapters; public ILogger GetCompositeLogger() => new CompositeLogger(loggers); public IExporter GetCompositeExporter() => new CompositeExporter(exporters); diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index e85abf926d..163a23bb69 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -51,6 +51,7 @@ public static ImmutableConfig Create(IConfig source) var uniqueFilters = source.GetFilters().ToImmutableHashSet(); var uniqueRules = source.GetLogicalGroupRules().ToImmutableArray(); var uniqueHidingRules = source.GetColumnHidingRules().ToImmutableArray(); + var uniqueAsyncConsumerTypes = source.GetAsyncAdapterDefinitions().ToImmutableHashSet(); var uniqueRunnableJobs = GetRunnableJobs(source.GetJobs()).ToImmutableHashSet(); @@ -65,6 +66,7 @@ public static ImmutableConfig Create(IConfig source) uniqueFilters, uniqueRules, uniqueHidingRules, + uniqueAsyncConsumerTypes, uniqueRunnableJobs, source.UnionRule, source.ArtifactsPath ?? DefaultConfig.Instance.ArtifactsPath, diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index 27eca81863..f75ee36f4d 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -6,6 +6,7 @@ using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Filters; @@ -34,6 +35,7 @@ public class ManualConfig : IConfig private readonly List filters = new List(); private readonly List logicalGroupRules = new List(); private readonly List columnHidingRules = new List(); + private readonly HashSet awaitableAdapters = new HashSet(); public IEnumerable GetColumnProviders() => columnProviders; public IEnumerable GetExporters() => exporters; @@ -46,6 +48,7 @@ public class ManualConfig : IConfig public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => logicalGroupRules; public IEnumerable GetColumnHidingRules() => columnHidingRules; + public IEnumerable GetAsyncAdapterDefinitions() => awaitableAdapters; [PublicAPI] public ConfigOptions Options { get; set; } [PublicAPI] public ConfigUnionRule UnionRule { get; set; } = ConfigUnionRule.Union; @@ -106,6 +109,23 @@ public ManualConfig WithBuildTimeout(TimeSpan buildTimeout) return this; } + /// + /// Adds an adapter to make a type awaitable. This type must implement or . + /// Optionally provide an async method builder adapter that will be used to consume the awaitable type. This type must implement . + /// If not provided, will be used if it implements , otherwise will be used. + /// + /// + /// If an adapter already exists for the corresponding awaitable type, it will be overridden. + /// + /// The awaitable type adapter. + /// The async method builder adapter. + /// + public ManualConfig AddAsyncAdapter(Type awaitableAdapterType, Type asyncMethodBuilderAdapterType = null) + { + awaitableAdapters.Add(new AsyncAdapterDefinition(awaitableAdapterType, asyncMethodBuilderAdapterType)); + return this; + } + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This method will soon be removed, please start using .AddColumn() instead.")] public void Add(params IColumn[] newColumns) => AddColumn(newColumns); @@ -261,6 +281,7 @@ public void Add(IConfig config) SummaryStyle = config.SummaryStyle ?? SummaryStyle; logicalGroupRules.AddRange(config.GetLogicalGroupRules()); columnHidingRules.AddRange(config.GetColumnHidingRules()); + awaitableAdapters.AddRange(config.GetAsyncAdapterDefinitions()); Options |= config.Options; BuildTimeout = GetBuildTimeout(BuildTimeout, config.BuildTimeout); } diff --git a/src/BenchmarkDotNet/Engines/AsyncBenchmarkRunner.cs b/src/BenchmarkDotNet/Engines/AsyncBenchmarkRunner.cs new file mode 100644 index 0000000000..397a55cedf --- /dev/null +++ b/src/BenchmarkDotNet/Engines/AsyncBenchmarkRunner.cs @@ -0,0 +1,379 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Helpers; +using Perfolizer.Horology; +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines +{ + // Using an interface instead of delegates allows the JIT to inline the call when it's used as a generic struct. + public interface IFunc + { + TResult Invoke(); + } + + public struct EmptyAwaiter : ICriticalNotifyCompletion + { + void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) + => throw new NotImplementedException(); + + void INotifyCompletion.OnCompleted(Action continuation) + => throw new NotImplementedException(); + } + + public abstract class AsyncBenchmarkRunner : IDisposable, ICriticalNotifyCompletion + { + // The continuation callback moves the state machine forward through the builder in the TAsyncMethodBuilderAdapter. + private Action continuation; + + private protected void AdvanceStateMachine() + { + Action action = continuation; + continuation = null; + action(); + } + + void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) + => this.continuation = continuation; + + void INotifyCompletion.OnCompleted(Action continuation) + => this.continuation = continuation; + + public abstract ValueTask Invoke(long repeatCount, IClock clock); + public abstract ValueTask InvokeSingle(); + // TODO: make sure Dispose is called. + public abstract void Dispose(); + } + + public abstract class AsyncBenchmarkRunner : AsyncBenchmarkRunner + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter + where TAwaiter : ICriticalNotifyCompletion + { + private AutoResetValueTaskSource valueTaskSource; + private readonly TAwaitableAdapter awaitableAdapter; + private readonly TFunc func; + private long repeatsRemaining; + private IClock clock; + private bool isDisposed; + + public AsyncBenchmarkRunner(TFunc func, TAwaitableAdapter awaitableAdapter) + { + this.func = func; + this.awaitableAdapter = awaitableAdapter; + } + + public override ValueTask Invoke(long repeatCount, IClock clock) + { + repeatsRemaining = repeatCount; + // The clock is started inside the state machine. + this.clock = clock; + + if (valueTaskSource == null) + { + valueTaskSource = new AutoResetValueTaskSource(); + // Initialize and start the state machine. + StateMachine stateMachine = default; + stateMachine.asyncMethodBuilderAdapter = new (); + stateMachine.asyncMethodBuilderAdapter.CreateAsyncMethodBuilder(); + stateMachine.awaitableAdapter = awaitableAdapter; + stateMachine.owner = this; + stateMachine.func = func; + stateMachine.state = -1; + stateMachine.asyncMethodBuilderAdapter.Start(ref stateMachine); + } + else + { + AdvanceStateMachine(); + } + + return new ValueTask(valueTaskSource, valueTaskSource.Version); + } + + public override void Dispose() + { + // Set the isDisposed flag and advance the state machine to complete the consumer. + isDisposed = true; + AdvanceStateMachine(); + } + + // C# compiler creates struct state machines in Release mode, so we do the same. + private struct StateMachine : IAsyncStateMachine + { + internal AsyncBenchmarkRunner owner; + internal TAsyncMethodBuilderAdapter asyncMethodBuilderAdapter; + internal TAwaitableAdapter awaitableAdapter; + internal TFunc func; + internal int state; + private StartedClock startedClock; + private TAwaiter currentAwaiter; + +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + public void MoveNext() + { + try + { + if (state == -1) + { + if (owner.isDisposed) + { + // The owner has been disposed, we complete the consumer. + asyncMethodBuilderAdapter.SetResult(); + return; + } + + // The benchmark has been started, start the clock. + state = 0; + var clock = owner.clock; + owner.clock = default; + startedClock = clock.Start(); + goto StartLoop; + } + + if (state == 1) + { + state = 0; + awaitableAdapter.GetResult(ref currentAwaiter); + currentAwaiter = default; + } + + StartLoop: + while (--owner.repeatsRemaining >= 0) + { + var awaitable = func.Invoke(); + var awaiter = awaitableAdapter.GetAwaiter(ref awaitable); + if (!awaitableAdapter.GetIsCompleted(ref awaiter)) + { + state = 1; + currentAwaiter = awaiter; + asyncMethodBuilderAdapter.AwaitOnCompleted(ref currentAwaiter, ref this); + return; + } + awaitableAdapter.GetResult(ref awaiter); + } + } + catch (Exception e) + { + currentAwaiter = default; + startedClock = default; + owner.valueTaskSource.SetException(e); + return; + } + var clockspan = startedClock.GetElapsed(); + currentAwaiter = default; + startedClock = default; + state = -1; + { + // We hook up the continuation to the owner so the state machine can be advanced when the next benchmark iteration starts. + asyncMethodBuilderAdapter.AwaitOnCompleted(ref owner, ref this); + } + owner.valueTaskSource.SetResult(clockspan); + } + + void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => asyncMethodBuilderAdapter.SetStateMachine(stateMachine); + } + + public override ValueTask InvokeSingle() + { + var awaitable = func.Invoke(); + + if (null == default(TAwaitable) && awaitable is Task task) + { + return new ValueTask(task); + } + + if (typeof(TAwaitable) == typeof(ValueTask)) + { + return (ValueTask) (object) awaitable; + } + + var awaiter = awaitableAdapter.GetAwaiter(ref awaitable); + if (awaitableAdapter.GetIsCompleted(ref awaiter)) + { + try + { + awaitableAdapter.GetResult(ref awaiter); + } + catch (Exception e) + { + return new ValueTask(Task.FromException(e)); + } + return new ValueTask(); + } + + ToValueTaskVoidStateMachine stateMachine = default; + stateMachine.builder = AsyncValueTaskMethodBuilder.Create(); + stateMachine.awaitableAdapter = awaitableAdapter; + stateMachine.awaiter = awaiter; + stateMachine.builder.Start(ref stateMachine); + return stateMachine.builder.Task; + + // TODO: remove the commented code after the statemachine is verified working. + //var taskCompletionSource = new TaskCompletionSource(); + //awaiter.UnsafeOnCompleted(() => + //{ + // try + // { + // asyncConsumer.GetResult(ref awaiter); + // } + // catch (Exception e) + // { + // taskCompletionSource.SetException(e); + // return; + // } + // taskCompletionSource.SetResult(true); + //}); + //return new ValueTask(taskCompletionSource.Task); + } + + private struct ToValueTaskVoidStateMachine : IAsyncStateMachine + { + internal AsyncValueTaskMethodBuilder builder; + internal TAwaitableAdapter awaitableAdapter; + internal TAwaiter awaiter; + private bool isStarted; + + public void MoveNext() + { + if (!isStarted) + { + isStarted = true; + builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); + return; + } + + try + { + awaitableAdapter.GetResult(ref awaiter); + builder.SetResult(); + } + catch (Exception e) + { + builder.SetException(e); + } + } + + void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) => builder.SetStateMachine(stateMachine); + } + } + + public sealed class AsyncWorkloadRunner + : AsyncBenchmarkRunner.AwaitableAdapter, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncWorkloadRunner(TFunc func) : base(func, new AwaitableAdapter() { userAdapter = new TAwaitableAdapter() }) { } + + public struct AwaitableAdapter : IAwaitableAdapter + { + internal TAwaitableAdapter userAdapter; + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => userAdapter.GetAwaiter(ref awaitable); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => userAdapter.GetIsCompleted(ref awaiter); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public void GetResult(ref TAwaiter awaiter) + => userAdapter.GetResult(ref awaiter); + } + } + + public sealed class AsyncOverheadRunner + : AsyncBenchmarkRunner.AwaitableAdapter, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncOverheadRunner(TFunc func) : base(func, new ()) { } + + public struct AwaitableAdapter : IAwaitableAdapter + { + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => default; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => true; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public void GetResult(ref TAwaiter awaiter) { } + } + } + + public sealed class AsyncWorkloadRunner + : AsyncBenchmarkRunner.AwaitableAdapter, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncWorkloadRunner(TFunc func) : base(func, new AwaitableAdapter() { userAdapter = new TAwaitableAdapter() }) { } + + public struct AwaitableAdapter : IAwaitableAdapter + { + internal TAwaitableAdapter userAdapter; + + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => userAdapter.GetAwaiter(ref awaitable); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => userAdapter.GetIsCompleted(ref awaiter); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetResult(ref TAwaiter awaiter) + => GetResultNoInlining(ref awaiter); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private TResult GetResultNoInlining(ref TAwaiter awaiter) + => userAdapter.GetResult(ref awaiter); + } + } + + public sealed class AsyncOverheadRunner + : AsyncBenchmarkRunner.AwaitableAdapter, TAwaitable, TAwaiter> + where TFunc : struct, IFunc + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaiter : ICriticalNotifyCompletion + { + public AsyncOverheadRunner(TFunc func) : base(func, new ()) { } + + public struct AwaitableAdapter : IAwaitableAdapter + { + // Make sure the methods are called without inlining. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public TAwaiter GetAwaiter(ref TAwaitable awaitable) + => default; + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public bool GetIsCompleted(ref TAwaiter awaiter) + => true; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetResult(ref TAwaiter awaiter) + { + GetResultNoInlining(ref awaiter); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] +#pragma warning disable IDE0060 // Remove unused parameter + private void GetResultNoInlining(ref TAwaiter awaiter) { } +#pragma warning restore IDE0060 // Remove unused parameter + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/AsyncMethodBuilderAdapters.cs b/src/BenchmarkDotNet/Engines/AsyncMethodBuilderAdapters.cs new file mode 100644 index 0000000000..c73afaad72 --- /dev/null +++ b/src/BenchmarkDotNet/Engines/AsyncMethodBuilderAdapters.cs @@ -0,0 +1,66 @@ +using BenchmarkDotNet.Configs; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines +{ + // Using struct rather than class forces the JIT to generate specialized code for direct calls instead of virtual, and avoids an extra allocation. + public struct AsyncTaskMethodBuilderAdapter : IAsyncMethodBuilderAdapter + { + // We use a type that users cannot access to prevent the async method builder from being pre-jitted with the user's type, in case the benchmark is ran with ColdStart. + private struct EmptyStruct { } + + private AsyncTaskMethodBuilder builder; + + public void CreateAsyncMethodBuilder() + => builder = AsyncTaskMethodBuilder.Create(); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => builder.Start(ref stateMachine); + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => builder.SetResult(default); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => builder.SetStateMachine(stateMachine); + } + + public struct AsyncValueTaskMethodBuilderAdapter : IAsyncMethodBuilderAdapter + { + // We use a type that users cannot access to prevent the async method builder from being pre-jitted with the user's type, in case the benchmark is ran with ColdStart. + private struct EmptyStruct { } + + private AsyncValueTaskMethodBuilder builder; + + public void CreateAsyncMethodBuilder() + => builder = AsyncValueTaskMethodBuilder.Create(); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => builder.Start(ref stateMachine); + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + + public void SetResult() + => builder.SetResult(default); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => builder.SetStateMachine(stateMachine); + + public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) + => awaitable.ConfigureAwait(false).GetAwaiter(); + + public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.IsCompleted; + + public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.GetResult(); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/AwaitableAdapters.cs b/src/BenchmarkDotNet/Engines/AwaitableAdapters.cs new file mode 100644 index 0000000000..f8cfda9355 --- /dev/null +++ b/src/BenchmarkDotNet/Engines/AwaitableAdapters.cs @@ -0,0 +1,40 @@ +using BenchmarkDotNet.Configs; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines +{ + // Using struct rather than class forces the JIT to generate specialized code for direct calls instead of virtual, and avoids an extra allocation. + public struct TaskAdapter : IAwaitableAdapter + { + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); + public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.IsCompleted; + public void GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.GetResult(); + } + + public struct TaskAdapter : IAwaitableAdapter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter, TResult> + { + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter(ref Task awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); + public bool GetIsCompleted(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.IsCompleted; + public TResult GetResult(ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.GetResult(); + } + + // Using struct rather than class forces the JIT to generate specialized code that can be inlined, and avoids an extra allocation. + public struct ValueTaskAdapter : IAwaitableAdapter + { + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); + public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.IsCompleted; + public void GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.GetResult(); + } + + public struct ValueTaskAdapter : IAwaitableAdapter, ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter, TResult> + { + // We use ConfigureAwait(false) to prevent dead-locks with InProcess toolchains (it could be ran on a thread with a SynchronizationContext). + public ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter(ref ValueTask awaitable) => awaitable.ConfigureAwait(false).GetAwaiter(); + public bool GetIsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.IsCompleted; + public TResult GetResult(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) => awaiter.GetResult(); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/Engine.cs b/src/BenchmarkDotNet/Engines/Engine.cs index 271c3c44e1..adbf9c0e2e 100644 --- a/src/BenchmarkDotNet/Engines/Engine.cs +++ b/src/BenchmarkDotNet/Engines/Engine.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Portability; @@ -19,17 +20,17 @@ public class Engine : IEngine public const int MinInvokeCount = 4; [PublicAPI] public IHost Host { get; } - [PublicAPI] public Action WorkloadAction { get; } + [PublicAPI] public Func> WorkloadAction { get; } [PublicAPI] public Action Dummy1Action { get; } [PublicAPI] public Action Dummy2Action { get; } [PublicAPI] public Action Dummy3Action { get; } - [PublicAPI] public Action OverheadAction { get; } + [PublicAPI] public Func> OverheadAction { get; } [PublicAPI] public Job TargetJob { get; } [PublicAPI] public long OperationsPerInvoke { get; } - [PublicAPI] public Action GlobalSetupAction { get; } - [PublicAPI] public Action GlobalCleanupAction { get; } - [PublicAPI] public Action IterationSetupAction { get; } - [PublicAPI] public Action IterationCleanupAction { get; } + [PublicAPI] public Func GlobalSetupAction { get; } + [PublicAPI] public Func GlobalCleanupAction { get; } + [PublicAPI] public Func IterationSetupAction { get; } + [PublicAPI] public Func IterationCleanupAction { get; } [PublicAPI] public IResolver Resolver { get; } [PublicAPI] public CultureInfo CultureInfo { get; } [PublicAPI] public string BenchmarkName { get; } @@ -51,9 +52,9 @@ public class Engine : IEngine internal Engine( IHost host, IResolver resolver, - Action dummy1Action, Action dummy2Action, Action dummy3Action, Action overheadAction, Action workloadAction, Job targetJob, - Action globalSetupAction, Action globalCleanupAction, Action iterationSetupAction, Action iterationCleanupAction, long operationsPerInvoke, - bool includeExtraStats, string benchmarkName) + Action dummy1Action, Action dummy2Action, Action dummy3Action, Func> overheadAction, Func> workloadAction, + Job targetJob, Func globalSetupAction, Func globalCleanupAction, Func iterationSetupAction, Func iterationCleanupAction, + long operationsPerInvoke, bool includeExtraStats, string benchmarkName) { Host = host; @@ -91,7 +92,7 @@ public void Dispose() { try { - GlobalCleanupAction?.Invoke(); + Helpers.AwaitHelper.GetResult(GlobalCleanupAction.Invoke()); } catch (Exception e) { @@ -160,7 +161,7 @@ public Measurement RunIteration(IterationData data) var action = isOverhead ? OverheadAction : WorkloadAction; if (!isOverhead) - IterationSetupAction(); + Helpers.AwaitHelper.GetResult(IterationSetupAction()); GcCollect(); @@ -170,15 +171,14 @@ public Measurement RunIteration(IterationData data) Span stackMemory = randomizeMemory ? stackalloc byte[random.Next(32)] : Span.Empty; // Measure - var clock = Clock.Start(); - action(invokeCount / unrollFactor); - var clockSpan = clock.GetElapsed(); + var op = action(invokeCount / unrollFactor, Clock); + var clockSpan = Helpers.AwaitHelper.GetResult(op); if (EngineEventSource.Log.IsEnabled()) EngineEventSource.Log.IterationStop(data.IterationMode, data.IterationStage, totalOperations); if (!isOverhead) - IterationCleanupAction(); + Helpers.AwaitHelper.GetResult(IterationCleanupAction()); if (randomizeMemory) RandomizeManagedHeapMemory(); @@ -203,20 +203,21 @@ public Measurement RunIteration(IterationData data) // it does not matter, because we have already obtained the results! EnableMonitoring(); - IterationSetupAction(); // we run iteration setup first, so even if it allocates, it is not included in the results + Helpers.AwaitHelper.GetResult(IterationSetupAction()); // we run iteration setup first, so even if it allocates, it is not included in the results var initialThreadingStats = ThreadingStats.ReadInitial(); // this method might allocate var exceptionsStats = new ExceptionsStats(); // allocates exceptionsStats.StartListening(); // this method might allocate var initialGcStats = GcStats.ReadInitial(); - WorkloadAction(data.InvokeCount / data.UnrollFactor); + var op = WorkloadAction(data.InvokeCount / data.UnrollFactor, Clock); + Helpers.AwaitHelper.GetResult(op); exceptionsStats.Stop(); var finalGcStats = GcStats.ReadFinal(); var finalThreadingStats = ThreadingStats.ReadFinal(); - IterationCleanupAction(); // we run iteration cleanup after collecting GC stats + Helpers.AwaitHelper.GetResult(IterationCleanupAction()); // we run iteration cleanup after collecting GC stats var totalOperationsCount = data.InvokeCount * OperationsPerInvoke; GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperations(totalOperationsCount); @@ -231,14 +232,14 @@ private void Consume(in Span _) { } private void RandomizeManagedHeapMemory() { // invoke global cleanup before global setup - GlobalCleanupAction?.Invoke(); + Helpers.AwaitHelper.GetResult(GlobalCleanupAction.Invoke()); var gen0object = new byte[random.Next(32)]; var lohObject = new byte[85 * 1024 + random.Next(32)]; // we expect the key allocations to happen in global setup (not ctor) // so we call it while keeping the random-size objects alive - GlobalSetupAction?.Invoke(); + Helpers.AwaitHelper.GetResult(GlobalSetupAction.Invoke()); GC.KeepAlive(gen0object); GC.KeepAlive(lohObject); diff --git a/src/BenchmarkDotNet/Engines/EngineFactory.cs b/src/BenchmarkDotNet/Engines/EngineFactory.cs index 0588218522..e311a927ee 100644 --- a/src/BenchmarkDotNet/Engines/EngineFactory.cs +++ b/src/BenchmarkDotNet/Engines/EngineFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using BenchmarkDotNet.Jobs; using Perfolizer.Horology; @@ -25,7 +26,7 @@ public IEngine CreateReadyToRun(EngineParameters engineParameters) if (engineParameters.TargetJob == null) throw new ArgumentNullException(nameof(engineParameters.TargetJob)); - engineParameters.GlobalSetupAction?.Invoke(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose + engineParameters.GlobalSetupAction.Invoke().AsTask().GetAwaiter().GetResult(); // whatever the settings are, we MUST call global setup here, the global cleanup is part of Engine's Dispose if (!engineParameters.NeedsJitting) // just create the engine, do NOT jit return CreateMultiActionEngine(engineParameters); @@ -109,7 +110,7 @@ private static Engine CreateSingleActionEngine(EngineParameters engineParameters engineParameters.OverheadActionNoUnroll, engineParameters.WorkloadActionNoUnroll); - private static Engine CreateEngine(EngineParameters engineParameters, Job job, Action idle, Action main) + private static Engine CreateEngine(EngineParameters engineParameters, Job job, Func> idle, Func> main) => new Engine( engineParameters.Host, EngineParameters.DefaultResolver, diff --git a/src/BenchmarkDotNet/Engines/EngineParameters.cs b/src/BenchmarkDotNet/Engines/EngineParameters.cs index ec61582529..c7361b3a07 100644 --- a/src/BenchmarkDotNet/Engines/EngineParameters.cs +++ b/src/BenchmarkDotNet/Engines/EngineParameters.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; @@ -12,19 +13,19 @@ public class EngineParameters public static readonly IResolver DefaultResolver = new CompositeResolver(BenchmarkRunnerClean.DefaultResolver, EngineResolver.Instance); public IHost Host { get; set; } - public Action WorkloadActionNoUnroll { get; set; } - public Action WorkloadActionUnroll { get; set; } + public Func> WorkloadActionNoUnroll { get; set; } + public Func> WorkloadActionUnroll { get; set; } public Action Dummy1Action { get; set; } public Action Dummy2Action { get; set; } public Action Dummy3Action { get; set; } - public Action OverheadActionNoUnroll { get; set; } - public Action OverheadActionUnroll { get; set; } + public Func> OverheadActionNoUnroll { get; set; } + public Func> OverheadActionUnroll { get; set; } public Job TargetJob { get; set; } = Job.Default; public long OperationsPerInvoke { get; set; } = 1; - public Action GlobalSetupAction { get; set; } - public Action GlobalCleanupAction { get; set; } - public Action IterationSetupAction { get; set; } - public Action IterationCleanupAction { get; set; } + public Func GlobalSetupAction { get; set; } + public Func GlobalCleanupAction { get; set; } + public Func IterationSetupAction { get; set; } + public Func IterationCleanupAction { get; set; } public bool MeasureExtraStats { get; set; } [PublicAPI] public string BenchmarkName { get; set; } diff --git a/src/BenchmarkDotNet/Engines/IEngine.cs b/src/BenchmarkDotNet/Engines/IEngine.cs index e35b870d8b..5b38371c91 100644 --- a/src/BenchmarkDotNet/Engines/IEngine.cs +++ b/src/BenchmarkDotNet/Engines/IEngine.cs @@ -1,8 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Reports; +using Perfolizer.Horology; namespace BenchmarkDotNet.Engines { @@ -19,13 +21,13 @@ public interface IEngine : IDisposable long OperationsPerInvoke { get; } - Action? GlobalSetupAction { get; } + Func GlobalSetupAction { get; } - Action? GlobalCleanupAction { get; } + Func GlobalCleanupAction { get; } - Action WorkloadAction { get; } + Func> WorkloadAction { get; } - Action OverheadAction { get; } + Func> OverheadAction { get; } IResolver Resolver { get; } diff --git a/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs b/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs new file mode 100644 index 0000000000..7fc093a9ea --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/AutoResetValueTaskSource.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace BenchmarkDotNet.Helpers +{ + /// + /// Implementation for that will reset itself when awaited so that it can be re-used. + /// + public class AutoResetValueTaskSource : IValueTaskSource, IValueTaskSource + { + private ManualResetValueTaskSourceCore _sourceCore; + + /// Completes with a successful result. + /// The result. + public void SetResult(TResult result) => _sourceCore.SetResult(result); + + /// Completes with an error. + /// The exception. + public void SetException(Exception error) => _sourceCore.SetException(error); + + /// Gets the operation version. + public short Version => _sourceCore.Version; + + private TResult GetResult(short token) + { + // We don't want to reset this if the token is invalid. + if (token != Version) + { + throw new InvalidOperationException(); + } + try + { + return _sourceCore.GetResult(token); + } + finally + { + _sourceCore.Reset(); + } + } + + void IValueTaskSource.GetResult(short token) => GetResult(token); + TResult IValueTaskSource.GetResult(short token) => GetResult(token); + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _sourceCore.GetStatus(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _sourceCore.GetStatus(token); + + // Don't pass the flags, we don't want to schedule the continuation on the current SynchronizationContext or TaskScheduler if the user runs this in-process, as that may cause a deadlock when this is waited on synchronously. + // And we don't want to capture the ExecutionContext (we don't use it, and it causes allocations in the full framework). + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _sourceCore.OnCompleted(continuation, state, token, ValueTaskSourceOnCompletedFlags.None); + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _sourceCore.OnCompleted(continuation, state, token, ValueTaskSourceOnCompletedFlags.None); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs new file mode 100644 index 0000000000..12c86bd595 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Helpers +{ + public static class AwaitHelper + { + private class ValueTaskWaiter + { + // We use thread static field so that multiple threads can use individual lock object and callback. + [ThreadStatic] + private static ValueTaskWaiter ts_current; + internal static ValueTaskWaiter Current => ts_current ??= new ValueTaskWaiter(); + + private readonly Action awaiterCallback; + private bool awaiterCompleted; + + private ValueTaskWaiter() + { + awaiterCallback = AwaiterCallback; + } + + private void AwaiterCallback() + { + lock (this) + { + awaiterCompleted = true; + System.Threading.Monitor.Pulse(this); + } + } + + // Hook up a callback instead of converting to Task to prevent extra allocations on each benchmark run. + internal void Wait(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + { + lock (this) + { + awaiterCompleted = false; + awaiter.UnsafeOnCompleted(awaiterCallback); + // Check if the callback executed synchronously before blocking. + if (!awaiterCompleted) + { + System.Threading.Monitor.Wait(this); + } + } + } + + internal void Wait(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + { + lock (this) + { + awaiterCompleted = false; + awaiter.UnsafeOnCompleted(awaiterCallback); + // Check if the callback executed synchronously before blocking. + if (!awaiterCompleted) + { + System.Threading.Monitor.Wait(this); + } + } + } + } + + // 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(); + } + + public static ValueTask ToValueTaskVoid(Task task) + { + return new ValueTask(task); + } + + public static ValueTask ToValueTaskVoid(Task task) + { + return new ValueTask(task); + } + + public static ValueTask ToValueTaskVoid(ValueTask task) + { + return task; + } + + // ValueTask unfortunately can't be converted to a ValueTask for free, so we must create a state machine. + // It's not a big deal though, as this is only used for Setup/Cleanup where allocations aren't measured. + // And in practice, this should never be used, as (Value)Task Setup/Cleanup methods have no utility. + public static async ValueTask ToValueTaskVoid(ValueTask task) + { + _ = await task.ConfigureAwait(false); + } + + internal static MethodInfo GetGetResultMethod(Type taskType) => GetMethod(taskType, nameof(AwaitHelper.GetResult)); + + internal static MethodInfo GetToValueTaskMethod(Type taskType) => GetMethod(taskType, nameof(AwaitHelper.ToValueTaskVoid)); + + private static MethodInfo GetMethod(Type taskType, string methodName) + { + if (!taskType.IsGenericType) + { + return typeof(AwaitHelper).GetMethod(methodName, BindingFlags.Public | BindingFlags.Static, null, new Type[1] { taskType }, null); + } + + Type compareType = taskType.GetGenericTypeDefinition() == typeof(ValueTask<>) ? typeof(ValueTask<>) + : typeof(Task).IsAssignableFrom(taskType) ? typeof(Task<>) + : null; + return compareType == null + ? null + : typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static) + .First(m => + { + if (m.Name != methodName) return false; + Type paramType = m.GetParameters().First().ParameterType; + // We have to compare the types indirectly, == check doesn't work. + return paramType.Assembly == compareType.Assembly && paramType.Namespace == compareType.Namespace && paramType.Name == compareType.Name; + }) + .MakeGenericMethod(new[] + { + taskType + .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance).ReturnType + .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance).ReturnType + }); + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs index 6d601e6495..50d4fd86af 100644 --- a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs +++ b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorStatementExtensions.cs @@ -143,5 +143,58 @@ public static void EmitLoopEndFromLocToArg( ilBuilder.EmitLdarg(toArg); ilBuilder.Emit(OpCodes.Blt, loopStartLabel); } + + public static void EmitLoopBeginFromFldTo0( + this ILGenerator ilBuilder, + Label loopStartLabel, + Label loopHeadLabel) + { + // IL_001b: br.s IL_0029 // loop start (head: IL_0029) + ilBuilder.Emit(OpCodes.Br, loopHeadLabel); + + // loop start (head: IL_0036) + ilBuilder.MarkLabel(loopStartLabel); + } + + public static void EmitLoopEndFromFldTo0( + this ILGenerator ilBuilder, + Label loopStartLabel, + Label loopHeadLabel, + FieldBuilder counterField, + LocalBuilder counterLocal) + { + // loop counter stored as loc0, loop max passed as arg1 + /* + // while (--repeatsRemaining >= 0) + IL_0029: ldarg.0 + IL_002a: ldarg.0 + IL_002b: ldfld int64 BenchmarkRunner_0::repeatsRemaining + IL_0030: ldc.i4.1 + IL_0031: conv.i8 + IL_0032: sub + IL_0033: stloc.1 + IL_0034: ldloc.1 + IL_0035: stfld int64 BenchmarkRunner_0::repeatsRemaining + IL_003a: ldloc.1 + IL_003b: ldc.i4.0 + IL_003c: conv.i8 + IL_003d: bge.s IL_001d + // end loop + */ + ilBuilder.MarkLabel(loopHeadLabel); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, counterField); + ilBuilder.Emit(OpCodes.Ldc_I4_1); + ilBuilder.Emit(OpCodes.Conv_I8); + ilBuilder.Emit(OpCodes.Sub); + ilBuilder.EmitStloc(counterLocal); + ilBuilder.EmitLdloc(counterLocal); + ilBuilder.Emit(OpCodes.Stfld, counterField); + ilBuilder.EmitLdloc(counterLocal); + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Conv_I8); + ilBuilder.Emit(OpCodes.Bge, loopStartLabel); + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Running/BenchmarkCase.cs b/src/BenchmarkDotNet/Running/BenchmarkCase.cs index b517ab5676..410446c00d 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkCase.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkCase.cs @@ -11,7 +11,7 @@ namespace BenchmarkDotNet.Running public class BenchmarkCase : IComparable, IDisposable { public Descriptor Descriptor { get; } - public Job Job { get; } + public Job Job { get; private set; } public ParameterInstances Parameters { get; } public ImmutableConfig Config { get; } @@ -32,6 +32,11 @@ public Runtime GetRuntime() => Job.Environment.HasValue(EnvironmentMode.RuntimeC ? Job.Environment.Runtime : RuntimeInformation.GetCurrentRuntime(); + internal void ForceUnrollFactorForAsync() + { + Job = Job.WithUnrollFactor(1); + } + public void Dispose() => Parameters.Dispose(); public int CompareTo(BenchmarkCase other) => string.Compare(FolderInfo, other.FolderInfo, StringComparison.Ordinal); diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index d8f15f9138..530a5381bf 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -63,13 +63,14 @@ iterationCleanupAction = $IterationCleanupMethodName$; overheadDelegate = __Overhead; workloadDelegate = $WorkloadMethodDelegate$; + $InitializeAsyncBenchmarkRunnerFields$ $InitializeArgumentFields$ } - private System.Action globalSetupAction; - private System.Action globalCleanupAction; - private System.Action iterationSetupAction; - private System.Action iterationCleanupAction; + private System.Func globalSetupAction; + private System.Func globalCleanupAction; + private System.Func iterationSetupAction; + private System.Func iterationCleanupAction; private BenchmarkDotNet.Autogenerated.Runnable_$ID$.OverheadDelegate overheadDelegate; private BenchmarkDotNet.Autogenerated.Runnable_$ID$.WorkloadDelegate workloadDelegate; $DeclareArgumentFields$ @@ -109,56 +110,146 @@ $OverheadImplementation$ } -#if RETURNS_CONSUMABLE_$ID$ +#if RETURNS_AWAITABLE_$ID$ + + private struct WorkloadFunc : BenchmarkDotNet.Engines.IFunc<$WorkloadMethodReturnType$> + { + private readonly BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance; + + public WorkloadFunc(BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance) + { + this.instance = instance; + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public $WorkloadMethodReturnType$ Invoke() + { + return instance.__InvokeWorkload(); + } + } + + private struct OverheadFunc : BenchmarkDotNet.Engines.IFunc<$OverheadMethodReturnTypeName$> + { + private readonly BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance; + + public OverheadFunc(BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance) + { + this.instance = instance; + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public $OverheadMethodReturnTypeName$ Invoke() + { + return instance.__InvokeOverhead(); + } + } + + private readonly BenchmarkDotNet.Engines.AsyncBenchmarkRunner __asyncWorkloadRunner; + private readonly BenchmarkDotNet.Engines.AsyncBenchmarkRunner __asyncOverheadRunner; + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private $WorkloadMethodReturnType$ __InvokeWorkload() + { + return workloadDelegate($PassArgumentsDirect$); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private $OverheadMethodReturnTypeName$ __InvokeOverhead() + { + return overheadDelegate($PassArgumentsDirect$); + } + + // Awaits are not unrolled. + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + return __asyncOverheadRunner.Invoke(invokeCount, clock); + } + + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + return __asyncOverheadRunner.Invoke(invokeCount, clock); + } + + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + return __asyncWorkloadRunner.Invoke(invokeCount, clock); + } + + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) + { + return __asyncWorkloadRunner.Invoke(invokeCount, clock); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + public $WorkloadMethodReturnType$ $DisassemblerEntryMethodName$() + { + if (NotEleven == 11) + { + $LoadArguments$ + return $WorkloadMethodCall$; + } + + return default($WorkloadMethodReturnType$); + } + +#elif RETURNS_CONSUMABLE_$ID$ private BenchmarkDotNet.Engines.Consumer consumer = new BenchmarkDotNet.Engines.Consumer(); #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { consumer.Consume(overheadDelegate($PassArguments$));@Unroll@ } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { consumer.Consume(overheadDelegate($PassArguments$)); } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { consumer.Consume(workloadDelegate($PassArguments$)$ConsumeField$);@Unroll@ } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { consumer.Consume(workloadDelegate($PassArguments$)$ConsumeField$); } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] @@ -178,57 +269,65 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ result = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { result = overheadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ result = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { result = overheadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $WorkloadMethodReturnType$ result = default($WorkloadMethodReturnType$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { result = workloadDelegate($PassArguments$);@Unroll@ } NonGenericKeepAliveWithoutBoxing(result); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $WorkloadMethodReturnType$ result = default($WorkloadMethodReturnType$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { result = workloadDelegate($PassArguments$); } NonGenericKeepAliveWithoutBoxing(result); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } // we must not simply use DeadCodeEliminationHelper.KeepAliveWithoutBoxing because it's generic method @@ -253,29 +352,33 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { value = overheadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { value = overheadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } private $WorkloadMethodReturnType$ workloadDefaultValueHolder = default($WorkloadMethodReturnType$); @@ -283,29 +386,33 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { alias = workloadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref alias); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { alias = workloadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(ref alias); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] @@ -324,29 +431,33 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { value = overheadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ $OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$); + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { value = overheadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } private $WorkloadMethodReturnType$ workloadDefaultValueHolder = default($WorkloadMethodReturnType$); @@ -354,29 +465,33 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { alias = workloadDelegate($PassArguments$);@Unroll@ } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder; + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { alias = workloadDelegate($PassArguments$); } BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias); + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] @@ -395,49 +510,57 @@ #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { overheadDelegate($PassArguments$);@Unroll@ } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void OverheadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask OverheadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { overheadDelegate($PassArguments$); } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { workloadDelegate($PassArguments$);@Unroll@ } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } #if NETCOREAPP3_0_OR_GREATER [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveOptimization)] #endif - private void WorkloadActionNoUnroll(System.Int64 invokeCount) + private System.Threading.Tasks.ValueTask WorkloadActionNoUnroll(System.Int64 invokeCount, Perfolizer.Horology.IClock clock) { $LoadArguments$ + var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); for (System.Int64 i = 0; i < invokeCount; i++) { workloadDelegate($PassArguments$); } + return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs index 0fde3ac1ee..4e32eb2da3 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs @@ -1,6 +1,8 @@ using BenchmarkDotNet.Engines; using JetBrains.Annotations; using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -15,37 +17,23 @@ public ConsumableTypeInfo(Type methodReturnType) if (methodReturnType == null) throw new ArgumentNullException(nameof(methodReturnType)); - OriginMethodReturnType = methodReturnType; + WorkloadMethodReturnType = methodReturnType; - // Please note this code does not support await over extension methods. - var getAwaiterMethod = methodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlagsPublicInstance); - if (getAwaiterMethod == null) - { - WorkloadMethodReturnType = methodReturnType; - } - else - { - var getResultMethod = getAwaiterMethod - .ReturnType - .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance); - - if (getResultMethod == null) - { - WorkloadMethodReturnType = methodReturnType; - } - else - { - WorkloadMethodReturnType = getResultMethod.ReturnType; - GetAwaiterMethod = getAwaiterMethod; - GetResultMethod = getResultMethod; - } - } + // 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 (WorkloadMethodReturnType == null) throw new InvalidOperationException("Bug: (WorkloadMethodReturnType == null"); var consumableField = default(FieldInfo); - if (WorkloadMethodReturnType == typeof(void)) + if (IsAwaitable) + { + OverheadMethodReturnType = WorkloadMethodReturnType; + } + else if (WorkloadMethodReturnType == typeof(void)) { IsVoid = true; OverheadMethodReturnType = WorkloadMethodReturnType; @@ -75,14 +63,11 @@ public ConsumableTypeInfo(Type methodReturnType) public Type WorkloadMethodReturnType { get; } public Type OverheadMethodReturnType { get; } - public MethodInfo? GetAwaiterMethod { get; } - public MethodInfo? GetResultMethod { get; } - public bool IsVoid { get; } public bool IsByRef { get; } public bool IsConsumable { get; } public FieldInfo? WorkloadConsumableField { get; } - public bool IsAwaitable => GetAwaiterMethod != null && GetResultMethod != null; + public bool IsAwaitable { get; } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs index 76a2a5f505..b92c0228aa 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumableConsumeEmitter.cs @@ -70,7 +70,7 @@ protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerato ilBuilder.EmitReturnDefault(ConsumableInfo.WorkloadMethodReturnType, disassemblyDiagnoserLocal); } - protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder) + protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder, RunnableEmitter runnableEmitter) { var ctor = typeof(Consumer).GetConstructor(Array.Empty()); if (ctor == null) diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumeEmitter.cs index 62fe06c649..7a8529dc74 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumeEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/ConsumeEmitter.cs @@ -1,7 +1,12 @@ using System; using System.Reflection; using System.Reflection.Emit; +using System.Threading.Tasks; +using BenchmarkDotNet.Helpers.Reflection.Emit; using JetBrains.Annotations; +using Perfolizer.Horology; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation { @@ -12,6 +17,8 @@ public static ConsumeEmitter GetConsumeEmitter(ConsumableTypeInfo consumableType if (consumableTypeInfo == null) throw new ArgumentNullException(nameof(consumableTypeInfo)); + if (consumableTypeInfo.IsAwaitable) + return new TaskConsumeEmitter(consumableTypeInfo); if (consumableTypeInfo.IsVoid) return new VoidConsumeEmitter(consumableTypeInfo); if (consumableTypeInfo.IsByRef) @@ -97,14 +104,14 @@ protected virtual void OnEmitMembersOverride(TypeBuilder runnableBuilder) { } - public void OnEmitCtorBody(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder) + public void OnEmitCtorBody(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder, RunnableEmitter runnableEmitter) { AssertNoBuilder(); - OnEmitCtorBodyOverride(constructorBuilder, ilBuilder); + OnEmitCtorBodyOverride(constructorBuilder, ilBuilder, runnableEmitter); } - protected virtual void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder) + protected virtual void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder, RunnableEmitter runnableEmitter) { } @@ -223,5 +230,117 @@ public void EmitActionAfterCall(ILGenerator ilBuilder) protected virtual void EmitActionAfterCallOverride(ILGenerator ilBuilder) { } + + public virtual MethodBuilder EmitActionImpl(RunnableEmitter runnableEmitter, string methodName, RunnableActionKind actionKind, int unrollFactor) + { + FieldInfo actionDelegateField; + MethodInfo actionInvokeMethod; + switch (actionKind) + { + case RunnableActionKind.Overhead: + actionDelegateField = runnableEmitter.overheadDelegateField; + actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(runnableEmitter.overheadDelegateType); + break; + case RunnableActionKind.Workload: + actionDelegateField = runnableEmitter.workloadDelegateField; + actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(runnableEmitter.workloadDelegateType); + break; + default: + throw new ArgumentOutOfRangeException(nameof(actionKind), actionKind, null); + } + + /* + .method private hidebysig + instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 WorkloadActionNoUnroll ( + int64 invokeCount, + class Perfolizer.Horology.IClock clock + ) cil managed + */ + var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var clockArg = new EmitParameterInfo(1, ClockParamName, typeof(IClock)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + toArg, clockArg); + toArg.SetMember(actionMethodBuilder); + clockArg.SetMember(actionMethodBuilder); + + // Emit impl + var ilBuilder = actionMethodBuilder.GetILGenerator(); + BeginEmitAction(actionMethodBuilder, ilBuilder, actionInvokeMethod, actionKind); + + // init locals + var argLocals = runnableEmitter.EmitDeclareArgLocals(ilBuilder); + DeclareActionLocals(ilBuilder); + var startedClockLocal = ilBuilder.DeclareLocal(typeof(StartedClock)); + var indexLocal = ilBuilder.DeclareLocal(typeof(long)); + + // load fields + runnableEmitter.EmitLoadArgFieldsToLocals(ilBuilder, argLocals); + EmitActionBeforeLoop(ilBuilder); + + // start clock + /* + // var startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); + IL_0000: ldarg.2 + IL_0001: call valuetype Perfolizer.Horology.StartedClock Perfolizer.Horology.ClockExtensions::Start(class Perfolizer.Horology.IClock) + IL_0006: stloc.0 + */ + ilBuilder.EmitLdarg(clockArg); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.startClockMethod); + ilBuilder.EmitStloc(startedClockLocal); + + // loop + var loopStartLabel = ilBuilder.DefineLabel(); + var loopHeadLabel = ilBuilder.DefineLabel(); + ilBuilder.EmitLoopBeginFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); + { + /* + // overheadDelegate(); + IL_0005: ldarg.0 + IL_0006: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate + IL_000b: callvirt instance void BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke() + // -or- + // consumer.Consume(overheadDelegate(_argField)); + IL_000c: ldarg.0 + IL_000d: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer BenchmarkDotNet.Autogenerated.Runnable_0::consumer + IL_0012: ldarg.0 + IL_0013: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate + IL_0018: ldloc.0 + IL_0019: callvirt instance int32 BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke(int64) + IL_001e: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer::Consume(int32) + */ + for (int u = 0; u < unrollFactor; u++) + { + EmitActionBeforeCall(ilBuilder); + + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); + ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); + + EmitActionAfterCall(ilBuilder); + } + } + ilBuilder.EmitLoopEndFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); + + EmitActionAfterLoop(ilBuilder); + CompleteEmitAction(ilBuilder); + + /* + // return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); + IL_0021: ldloca.s 0 + IL_0023: call instance valuetype Perfolizer.Horology.ClockSpan Perfolizer.Horology.StartedClock::GetElapsed() + IL_0028: newobj instance void valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1::.ctor(!0) + IL_002d: ret + */ + ilBuilder.EmitLdloca(startedClockLocal); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.getElapsedMethod); + var ctor = typeof(ValueTask).GetConstructor(new[] { typeof(ClockSpan) }); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs index 7f9d47c62f..5b8f27bbaf 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs @@ -7,6 +7,7 @@ using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Security; +using System.Threading.Tasks; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Helpers.Reflection.Emit; @@ -15,6 +16,7 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.Results; using JetBrains.Annotations; +using Perfolizer.Horology; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; @@ -240,18 +242,22 @@ private static void EmitNoArgsMethodCallPopReturn( private int jobUnrollFactor; private int dummyUnrollFactor; - private Type overheadDelegateType; - private Type workloadDelegateType; - private TypeBuilder runnableBuilder; + public Type overheadDelegateType; + public Type workloadDelegateType; + public TypeBuilder runnableBuilder; private ConsumableTypeInfo consumableInfo; private ConsumeEmitter consumeEmitter; + private ConsumableTypeInfo globalSetupReturnInfo; + private ConsumableTypeInfo globalCleanupReturnInfo; + private ConsumableTypeInfo iterationSetupReturnInfo; + private ConsumableTypeInfo iterationCleanupReturnInfo; private FieldBuilder globalSetupActionField; private FieldBuilder globalCleanupActionField; private FieldBuilder iterationSetupActionField; private FieldBuilder iterationCleanupActionField; - private FieldBuilder overheadDelegateField; - private FieldBuilder workloadDelegateField; + public FieldBuilder overheadDelegateField; + public FieldBuilder workloadDelegateField; private FieldBuilder notElevenField; private FieldBuilder dummyVarField; @@ -261,7 +267,6 @@ private static void EmitNoArgsMethodCallPopReturn( private MethodBuilder dummy1Method; private MethodBuilder dummy2Method; private MethodBuilder dummy3Method; - private MethodInfo workloadImplementationMethod; private MethodBuilder overheadImplementationMethod; private MethodBuilder overheadActionUnrollMethod; private MethodBuilder overheadActionNoUnrollMethod; @@ -277,6 +282,9 @@ private static void EmitNoArgsMethodCallPopReturn( private MethodBuilder runMethod; // ReSharper restore NotAccessedField.Local + public readonly MethodInfo getElapsedMethod; + public readonly MethodInfo startClockMethod; + private RunnableEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder) { if (buildPartition == null) @@ -286,6 +294,8 @@ private RunnableEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuild this.buildPartition = buildPartition; this.moduleBuilder = moduleBuilder; + getElapsedMethod = typeof(StartedClock).GetMethod(nameof(StartedClock.GetElapsed), BindingFlagsPublicInstance); + startClockMethod = typeof(ClockExtensions).GetMethod(nameof(ClockExtensions.Start), BindingFlagsPublicStatic); } private Descriptor Descriptor => benchmark.BenchmarkCase.Descriptor; @@ -319,7 +329,6 @@ private Type EmitRunnableCore(BenchmarkBuildInfo newBenchmark) overheadActionNoUnrollMethod = EmitOverheadAction(OverheadActionNoUnrollMethodName, 1); // Workload impl - workloadImplementationMethod = EmitWorkloadImplementation(WorkloadImplementationMethodName); workloadActionUnrollMethod = EmitWorkloadAction(WorkloadActionUnrollMethodName, jobUnrollFactor); workloadActionNoUnrollMethod = EmitWorkloadAction(WorkloadActionNoUnrollMethodName, 1); @@ -349,13 +358,23 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark) // Init current state argFields = new List(); benchmark = newBenchmark; + dummyUnrollFactor = DummyUnrollFactor; + + consumableInfo = new ConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType); + if (consumableInfo.IsAwaitable) + { + benchmark.BenchmarkCase.ForceUnrollFactorForAsync(); + } + jobUnrollFactor = benchmark.BenchmarkCase.Job.ResolveValue( RunMode.UnrollFactorCharacteristic, buildPartition.Resolver); - dummyUnrollFactor = DummyUnrollFactor; - consumableInfo = new ConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType); consumeEmitter = ConsumeEmitter.GetConsumeEmitter(consumableInfo); + globalSetupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.GlobalSetupMethod?.ReturnType); + globalCleanupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.GlobalCleanupMethod?.ReturnType); + iterationSetupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.IterationSetupMethod?.ReturnType); + iterationCleanupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.IterationCleanupMethod?.ReturnType); // Init types runnableBuilder = DefineRunnableTypeBuilder(benchmark, moduleBuilder); @@ -363,6 +382,11 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark) workloadDelegateType = EmitWorkloadDelegateType(); } + private static ConsumableTypeInfo GetConsumableTypeInfo(Type methodReturnType) + { + return methodReturnType == null ? null : new ConsumableTypeInfo(methodReturnType); + } + private Type EmitOverheadDelegateType() { // .class public auto ansi sealed BenchmarkDotNet.Autogenerated.Runnable_0OverheadDelegate @@ -412,13 +436,13 @@ private Type EmitWorkloadDelegateType() private void DefineFields() { globalSetupActionField = - runnableBuilder.DefineField(GlobalSetupActionFieldName, typeof(Action), FieldAttributes.Private); + runnableBuilder.DefineField(GlobalSetupActionFieldName, typeof(Func), FieldAttributes.Private); globalCleanupActionField = - runnableBuilder.DefineField(GlobalCleanupActionFieldName, typeof(Action), FieldAttributes.Private); + runnableBuilder.DefineField(GlobalCleanupActionFieldName, typeof(Func), FieldAttributes.Private); iterationSetupActionField = - runnableBuilder.DefineField(IterationSetupActionFieldName, typeof(Action), FieldAttributes.Private); + runnableBuilder.DefineField(IterationSetupActionFieldName, typeof(Func), FieldAttributes.Private); iterationCleanupActionField = - runnableBuilder.DefineField(IterationCleanupActionFieldName, typeof(Action), FieldAttributes.Private); + runnableBuilder.DefineField(IterationCleanupActionFieldName, typeof(Func), FieldAttributes.Private); overheadDelegateField = runnableBuilder.DefineField(OverheadDelegateFieldName, overheadDelegateType, FieldAttributes.Private); workloadDelegateField = @@ -563,162 +587,17 @@ private MethodBuilder EmitOverheadImplementation(string methodName) return methodBuilder; } - private MethodInfo EmitWorkloadImplementation(string methodName) - { - // Shortcut: DO NOT emit method if the result type is not awaitable - if (!consumableInfo.IsAwaitable) - return Descriptor.WorkloadMethod; - - var workloadInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(workloadDelegateType); - - //.method private hidebysig - // instance int32 __Workload(int64 arg0) cil managed - var args = workloadInvokeMethod.GetParameters(); - var methodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( - methodName, - MethodAttributes.Private, - 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 - ) - */ - 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 [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::TaskSample(int64) - */ - 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 class [mscorlib]System.Threading.Tasks.Task`1::GetAwaiter() - IL_000c: stloc.0 - IL_000d: ldloca.s 0 - IL_000f: call instance !0 valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1::GetResult() - */ - ilBuilder.EmitInstanceCallThisValueOnStack(callResultLocal, consumableInfo.GetAwaiterMethod); - ilBuilder.EmitInstanceCallThisValueOnStack(awaiterLocal, consumableInfo.GetResultMethod); - - /* - IL_0014: ret - */ - ilBuilder.Emit(OpCodes.Ret); - - return methodBuilder; - } - private MethodBuilder EmitOverheadAction(string methodName, int unrollFactor) { - return EmitActionImpl(methodName, RunnableActionKind.Overhead, unrollFactor); + return consumeEmitter.EmitActionImpl(this, methodName, RunnableActionKind.Overhead, unrollFactor); } private MethodBuilder EmitWorkloadAction(string methodName, int unrollFactor) { - return EmitActionImpl(methodName, RunnableActionKind.Workload, unrollFactor); + return consumeEmitter.EmitActionImpl(this, methodName, RunnableActionKind.Workload, unrollFactor); } - private MethodBuilder EmitActionImpl(string methodName, RunnableActionKind actionKind, int unrollFactor) - { - FieldInfo actionDelegateField; - MethodInfo actionInvokeMethod; - switch (actionKind) - { - case RunnableActionKind.Overhead: - actionDelegateField = overheadDelegateField; - actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(overheadDelegateType); - break; - case RunnableActionKind.Workload: - actionDelegateField = workloadDelegateField; - actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(workloadDelegateType); - break; - default: - throw new ArgumentOutOfRangeException(nameof(actionKind), actionKind, null); - } - - // .method private hidebysig - // instance void OverheadActionUnroll(int64 invokeCount) cil managed - var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); - var actionMethodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( - methodName, - MethodAttributes.Private, - EmitParameterInfo.CreateReturnVoidParameter(), - toArg); - toArg.SetMember(actionMethodBuilder); - - // Emit impl - var ilBuilder = actionMethodBuilder.GetILGenerator(); - consumeEmitter.BeginEmitAction(actionMethodBuilder, ilBuilder, actionInvokeMethod, actionKind); - - // init locals - var argLocals = EmitDeclareArgLocals(ilBuilder); - consumeEmitter.DeclareActionLocals(ilBuilder); - var indexLocal = ilBuilder.DeclareLocal(typeof(long)); - - // load fields - EmitLoadArgFieldsToLocals(ilBuilder, argLocals); - consumeEmitter.EmitActionBeforeLoop(ilBuilder); - - // loop - var loopStartLabel = ilBuilder.DefineLabel(); - var loopHeadLabel = ilBuilder.DefineLabel(); - ilBuilder.EmitLoopBeginFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); - { - /* - // overheadDelegate(); - IL_0005: ldarg.0 - IL_0006: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate - IL_000b: callvirt instance void BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke() - // -or- - // consumer.Consume(overheadDelegate(_argField)); - IL_000c: ldarg.0 - IL_000d: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer BenchmarkDotNet.Autogenerated.Runnable_0::consumer - IL_0012: ldarg.0 - IL_0013: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate - IL_0018: ldloc.0 - IL_0019: callvirt instance int32 BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke(int64) - IL_001e: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.Consumer::Consume(int32) - */ - for (int u = 0; u < unrollFactor; u++) - { - consumeEmitter.EmitActionBeforeCall(ilBuilder); - - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); - ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); - - consumeEmitter.EmitActionAfterCall(ilBuilder); - } - } - ilBuilder.EmitLoopEndFromLocToArg(loopStartLabel, loopHeadLabel, indexLocal, toArg); - - consumeEmitter.EmitActionAfterLoop(ilBuilder); - consumeEmitter.CompleteEmitAction(ilBuilder); - - // IL_003a: ret - ilBuilder.EmitVoidReturn(actionMethodBuilder); - - return actionMethodBuilder; - } - - private IReadOnlyList EmitDeclareArgLocals(ILGenerator ilBuilder, bool skipFirst = false) + public IReadOnlyList EmitDeclareArgLocals(ILGenerator ilBuilder, bool skipFirst = false) { // NB: c# compiler does not store first arg in locals for static calls /* @@ -746,7 +625,7 @@ .locals init ( return argLocals; } - private void EmitLoadArgFieldsToLocals(ILGenerator ilBuilder, IReadOnlyList argLocals, bool skipFirstArg = false) + public void EmitLoadArgFieldsToLocals(ILGenerator ilBuilder, IReadOnlyList argLocals, bool skipFirstArg = false) { // NB: c# compiler does not store first arg in locals for static calls int localsOffset = argFields.Count > 0 && skipFirstArg ? -1 : 0; @@ -830,19 +709,6 @@ .locals init ( 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(); @@ -867,31 +733,20 @@ .locals init ( 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 [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 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 class [mscorlib]System.Threading.Tasks.Task`1::GetAwaiter() - IL_000c: stloc.0 - IL_000d: ldloca.s 0 - IL_000f: call instance !0 valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1::GetResult() - */ - ilBuilder.EmitInstanceCallThisValueOnStack(callResultLocal, consumableInfo.GetAwaiterMethod); - ilBuilder.EmitInstanceCallThisValueOnStack(awaiterLocal, consumableInfo.GetResultMethod); - } - /* IL_0018: ret */ @@ -915,53 +770,105 @@ .locals init ( private void EmitSetupCleanupMethods() { // Emit Setup/Cleanup methods - // We emit empty method instead of EmptyAction = "() => { }" - globalSetupMethod = EmitWrapperMethod( - GlobalSetupMethodName, - Descriptor.GlobalSetupMethod); - globalCleanupMethod = EmitWrapperMethod( - GlobalCleanupMethodName, - Descriptor.GlobalCleanupMethod); - iterationSetupMethod = EmitWrapperMethod( - IterationSetupMethodName, - Descriptor.IterationSetupMethod); - iterationCleanupMethod = EmitWrapperMethod( - IterationCleanupMethodName, - Descriptor.IterationCleanupMethod); + // We emit simple method instead of simple Action = "() => new System.Threading.Tasks.ValueTask()" + globalSetupMethod = EmitWrapperMethod(GlobalSetupMethodName, Descriptor.GlobalSetupMethod, globalSetupReturnInfo); + globalCleanupMethod = EmitWrapperMethod(GlobalCleanupMethodName, Descriptor.GlobalCleanupMethod, globalCleanupReturnInfo); + iterationSetupMethod = EmitWrapperMethod(IterationSetupMethodName, Descriptor.IterationSetupMethod, iterationSetupReturnInfo); + iterationCleanupMethod = EmitWrapperMethod(IterationCleanupMethodName, Descriptor.IterationCleanupMethod, iterationCleanupReturnInfo); } - private MethodBuilder EmitWrapperMethod(string methodName, MethodInfo optionalTargetMethod) + private MethodBuilder EmitWrapperMethod(string methodName, MethodInfo optionalTargetMethod, ConsumableTypeInfo returnTypeInfo) { - var methodBuilder = runnableBuilder.DefinePrivateVoidInstanceMethod(methodName); + var methodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask))); var ilBuilder = methodBuilder.GetILGenerator(); - if (optionalTargetMethod != null) - EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true); + if (returnTypeInfo?.IsAwaitable == true) + { + EmitAwaitableSetupTeardown(methodBuilder, optionalTargetMethod, ilBuilder, returnTypeInfo); + } + else + { + var valueTaskLocal = ilBuilder.DeclareLocal(typeof(ValueTask)); - ilBuilder.EmitVoidReturn(methodBuilder); + if (optionalTargetMethod != null) + { + EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true); + } + /* + // return new ValueTask(); + IL_0000: ldloca.s 0 + IL_0002: initobj [System.Private.CoreLib]System.Threading.Tasks.ValueTask + IL_0008: ldloc.0 + */ + ilBuilder.EmitLdloca(valueTaskLocal); + ilBuilder.Emit(OpCodes.Initobj, typeof(ValueTask)); + ilBuilder.EmitLdloc(valueTaskLocal); + } + + ilBuilder.Emit(OpCodes.Ret); return methodBuilder; } + private void EmitAwaitableSetupTeardown( + MethodBuilder methodBuilder, + MethodInfo targetMethod, + ILGenerator ilBuilder, + ConsumableTypeInfo returnTypeInfo) + { + if (targetMethod == null) + throw new ArgumentNullException(nameof(targetMethod)); + + // BenchmarkDotNet.Helpers.AwaitHelper.ToValueTaskVoid(workloadDelegate()); + /* + // call for instance + // GlobalSetup(); + IL_0000: ldarg.0 + IL_0001: call instance class [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalSetup() + IL_0006: call valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask BenchmarkDotNet.Helpers.AwaitHelper::ToValueTaskVoid(class [System.Private.CoreLib]System.Threading.Tasks.Task) + */ + /* + // call for static + // GlobalSetup(); + IL_0000: call class [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalCleanup() + IL_0005: call valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask BenchmarkDotNet.Helpers.AwaitHelper::ToValueTaskVoid(class [System.Private.CoreLib]System.Threading.Tasks.Task) + */ + if (targetMethod.IsStatic) + { + ilBuilder.Emit(OpCodes.Call, targetMethod); + + } + else if (methodBuilder.IsStatic) + { + throw new InvalidOperationException( + $"[BUG] Static method {methodBuilder.Name} tries to call instance member {targetMethod.Name}"); + } + else + { + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, targetMethod); + } + ilBuilder.Emit(OpCodes.Call, Helpers.AwaitHelper.GetToValueTaskMethod(returnTypeInfo.WorkloadMethodReturnType)); + } + private void EmitCtorBody() { var ilBuilder = ctorMethod.GetILGenerator(); ilBuilder.EmitCallBaseParameterlessCtor(ctorMethod); - consumeEmitter.OnEmitCtorBody(ctorMethod, ilBuilder); + consumeEmitter.OnEmitCtorBody(ctorMethod, ilBuilder, this); ilBuilder.EmitSetDelegateToThisField(globalSetupActionField, globalSetupMethod); ilBuilder.EmitSetDelegateToThisField(globalCleanupActionField, globalCleanupMethod); ilBuilder.EmitSetDelegateToThisField(iterationSetupActionField, iterationSetupMethod); ilBuilder.EmitSetDelegateToThisField(iterationCleanupActionField, iterationCleanupMethod); ilBuilder.EmitSetDelegateToThisField(overheadDelegateField, overheadImplementationMethod); - - if (workloadImplementationMethod == null) - ilBuilder.EmitSetDelegateToThisField(workloadDelegateField, Descriptor.WorkloadMethod); - else - ilBuilder.EmitSetDelegateToThisField(workloadDelegateField, workloadImplementationMethod); + ilBuilder.EmitSetDelegateToThisField(workloadDelegateField, Descriptor.WorkloadMethod); ilBuilder.EmitCtorReturn(ctorMethod); } diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/TaskConsumeEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/TaskConsumeEmitter.cs new file mode 100644 index 0000000000..355dc353f4 --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/TaskConsumeEmitter.cs @@ -0,0 +1,658 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Helpers.Reflection.Emit; +using Perfolizer.Horology; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; + +namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation +{ + internal class TaskConsumeEmitter : ConsumeEmitter + { + private MethodInfo overheadKeepAliveWithoutBoxingMethod; + private MethodInfo getResultMethod; + + private LocalBuilder disassemblyDiagnoserLocal; + /* + private readonly BenchmarkDotNet.Helpers.AutoResetValueTaskSource valueTaskSource = new BenchmarkDotNet.Helpers.AutoResetValueTaskSource(); + private System.Int64 repeatsRemaining; + private readonly System.Action continuation; + private Perfolizer.Horology.StartedClock startedClock; + private $AwaiterTypeName$ currentAwaiter; + */ + private FieldBuilder valueTaskSourceField; + private FieldBuilder repeatsRemainingField; + private FieldBuilder continuationField; + private FieldBuilder startedClockField; + private FieldBuilder currentAwaiterField; + + private MethodBuilder overheadActionImplMethod; + private MethodBuilder workloadActionImplMethod; + private MethodBuilder setContinuationMethod; + private MethodBuilder runTaskMethod; + private MethodBuilder continuationMethod; + private MethodBuilder setExceptionMethod; + + public TaskConsumeEmitter(ConsumableTypeInfo consumableTypeInfo) : base(consumableTypeInfo) + { + } + + protected override void OnDefineFieldsOverride(TypeBuilder runnableBuilder) + { + overheadKeepAliveWithoutBoxingMethod = typeof(DeadCodeEliminationHelper).GetMethods() + .First(m => m.Name == nameof(DeadCodeEliminationHelper.KeepAliveWithoutBoxing) + && !m.GetParameterTypes().First().IsByRef) + .MakeGenericMethod(ConsumableInfo.OverheadMethodReturnType); + + valueTaskSourceField = runnableBuilder.DefineField(ValueTaskSourceFieldName, typeof(Helpers.AutoResetValueTaskSource), FieldAttributes.Private | FieldAttributes.InitOnly); + repeatsRemainingField = runnableBuilder.DefineField(RepeatsRemainingFieldName, typeof(long), FieldAttributes.Private); + continuationField = runnableBuilder.DefineField(ContinuationFieldName, typeof(Action), FieldAttributes.Private); + startedClockField = runnableBuilder.DefineField(StartedClockFieldName, typeof(StartedClock), FieldAttributes.Private); + // (Value)TaskAwaiter() + currentAwaiterField = runnableBuilder.DefineField(CurrentAwaiterFieldName, + ConsumableInfo.WorkloadMethodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance).ReturnType, + FieldAttributes.Private); + getResultMethod = currentAwaiterField.FieldType.GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsAllInstance); + } + + protected override void DeclareDisassemblyDiagnoserLocalsOverride(ILGenerator ilBuilder) + { + // optional local if default(T) uses .initobj + disassemblyDiagnoserLocal = ilBuilder.DeclareOptionalLocalForReturnDefault(ConsumableInfo.WorkloadMethodReturnType); + } + + protected override void EmitDisassemblyDiagnoserReturnDefaultOverride(ILGenerator ilBuilder) + { + ilBuilder.EmitReturnDefault(ConsumableInfo.WorkloadMethodReturnType, disassemblyDiagnoserLocal); + } + + protected override void OnEmitCtorBodyOverride(ConstructorBuilder constructorBuilder, ILGenerator ilBuilder, RunnableEmitter runnableEmitter) + { + var ctor = typeof(Helpers.AutoResetValueTaskSource).GetConstructor(Array.Empty()); + if (ctor == null) + throw new InvalidOperationException($"Cannot get default .ctor for {typeof(Helpers.AutoResetValueTaskSource)}"); + + /* + // valueTaskSourceField = new BenchmarkDotNet.Helpers.AutoResetValueTaskSource(); + IL_0000: ldarg.0 + IL_0001: newobj instance void class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1::.ctor() + IL_0006: stfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Stfld, valueTaskSourceField); + /* + // __SetContinuation(); + IL_0006: ldarg.0 + IL_0007: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__SetContinuation() + */ + setContinuationMethod = EmitSetContinuationImpl(runnableEmitter); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, setContinuationMethod); + } + + public override MethodBuilder EmitActionImpl(RunnableEmitter runnableEmitter, string methodName, RunnableActionKind actionKind, int unrollFactor) + { + MethodBuilder actionImpl = actionKind switch + { + RunnableActionKind.Overhead => EmitOverheadActionImpl(runnableEmitter), + RunnableActionKind.Workload => EmitWorkloadActionImpl(runnableEmitter), + _ => throw new ArgumentOutOfRangeException(nameof(actionKind), actionKind, null), + }; + + /* + .method private hidebysig + instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 WorkloadActionNoUnroll ( + int64 invokeCount, + class Perfolizer.Horology.IClock clock + ) cil managed + */ + var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var clockArg = new EmitParameterInfo(1, ClockParamName, typeof(IClock)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + toArg, clockArg); + toArg.SetMember(actionMethodBuilder); + clockArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + /* + // return WorkloadActionImpl(invokeCount, clock); + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: ldarg.2 + IL_0003: call instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 BenchmarkDotNet.Autogenerated.Runnable_0::WorkloadActionImpl(int64, class Perfolizer.Horology.IClock) + IL_0008: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(toArg); + ilBuilder.EmitLdarg(clockArg); + ilBuilder.Emit(OpCodes.Call, actionImpl); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private MethodBuilder EmitOverheadActionImpl(RunnableEmitter runnableEmitter) + { + if (overheadActionImplMethod != null) + { + return overheadActionImplMethod; + } + + FieldInfo actionDelegateField = runnableEmitter.overheadDelegateField; + MethodInfo actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(runnableEmitter.overheadDelegateType); + + /* + .method private hidebysig + instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 OverheadActionImpl ( + int64 invokeCount, + class Perfolizer.Horology.IClock clock + ) cil managed + */ + var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var clockArg = new EmitParameterInfo(1, ClockParamName, typeof(IClock)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + OverheadActionImplMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + toArg, clockArg); + toArg.SetMember(actionMethodBuilder); + clockArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + // init locals + var valueLocal = ilBuilder.DeclareLocal(ConsumableInfo.OverheadMethodReturnType); + var argLocals = runnableEmitter.EmitDeclareArgLocals(ilBuilder); + var indexLocal = ilBuilder.DeclareLocal(typeof(long)); + + /* + // repeatsRemaining = invokeCount; + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: stfld int64 BenchmarkDotNet.Autogenerated.Runnable_0::repeatsRemaining + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(toArg); + ilBuilder.Emit(OpCodes.Stfld, repeatsRemainingField); + /* + // Task value = default; + IL_0007: ldnull + IL_0008: stloc.0 + */ + ilBuilder.EmitSetLocalToDefault(valueLocal); + /* + // startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); + IL_0009: ldarg.0 + IL_000a: ldarg.2 + IL_000b: call valuetype Perfolizer.Horology.StartedClock Perfolizer.Horology.ClockExtensions::Start(class Perfolizer.Horology.IClock) + IL_0010: stfld valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(clockArg); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.startClockMethod); + ilBuilder.Emit(OpCodes.Stfld, startedClockField); + + // try { ... } + ilBuilder.BeginExceptionBlock(); + { + // load fields + runnableEmitter.EmitLoadArgFieldsToLocals(ilBuilder, argLocals); + + // while (--repeatsRemaining >= 0) { ... } + var loopStartLabel = ilBuilder.DefineLabel(); + var loopHeadLabel = ilBuilder.DefineLabel(); + ilBuilder.EmitLoopBeginFromFldTo0(loopStartLabel, loopHeadLabel); + { + /* + // value = overheadDelegate(); + IL_0017: ldarg.0 + IL_0018: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate BenchmarkDotNet.Autogenerated.Runnable_0::overheadDelegate + IL_001d: callvirt instance void BenchmarkDotNet.Autogenerated.Runnable_0/OverheadDelegate::Invoke() + IL_0022: stloc.0 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); + ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); + ilBuilder.EmitStloc(valueLocal); + } + ilBuilder.EmitLoopEndFromFldTo0(loopStartLabel, loopHeadLabel, repeatsRemainingField, indexLocal); + } + // catch (System.Exception) { ... } + ilBuilder.BeginCatchBlock(typeof(Exception)); + { + // IL_003b: pop + ilBuilder.Emit(OpCodes.Pop); + /* + // BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value); + IL_003c: ldloc.0 + IL_003d: call void BenchmarkDotNet.Engines.DeadCodeEliminationHelper::KeepAliveWithoutBoxing>(!!0) + */ + ilBuilder.EmitStaticCall(overheadKeepAliveWithoutBoxingMethod, valueLocal); + // IL_0042: rethrow + ilBuilder.Emit(OpCodes.Rethrow); + } + ilBuilder.EndExceptionBlock(); + + /* + // return new System.Threading.Tasks.ValueTask(startedClock.GetElapsed()); + IL_0044: ldarg.0 + IL_0045: ldflda valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + IL_004a: call instance valuetype Perfolizer.Horology.ClockSpan Perfolizer.Horology.StartedClock::GetElapsed() + IL_004f: newobj instance void valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1::.ctor(!0) + IL_0054: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.getElapsedMethod); + var ctor = typeof(ValueTask).GetConstructor(new[] { typeof(ClockSpan) }); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Ret); + + return overheadActionImplMethod = actionMethodBuilder; + } + + private MethodBuilder EmitWorkloadActionImpl(RunnableEmitter runnableEmitter) + { + if (workloadActionImplMethod != null) + { + return workloadActionImplMethod; + } + + setExceptionMethod = EmitSetExceptionImpl(runnableEmitter); + runTaskMethod = EmitRunTaskImpl(runnableEmitter); + continuationMethod = EmitContinuationImpl(runnableEmitter); + + /* + .method private hidebysig + instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1 WorkloadActionImpl ( + int64 invokeCount, + class Perfolizer.Horology.IClock clock + ) cil managed + */ + var toArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var clockArg = new EmitParameterInfo(1, ClockParamName, typeof(IClock)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + WorkloadActionImplMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + toArg, clockArg); + toArg.SetMember(actionMethodBuilder); + clockArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + /* + // repeatsRemaining = invokeCount; + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: stfld int64 BenchmarkDotNet.Autogenerated.Runnable_0::repeatsRemaining + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(toArg); + ilBuilder.Emit(OpCodes.Stfld, repeatsRemainingField); + /* + // startedClock = Perfolizer.Horology.ClockExtensions.Start(clock); + IL_0012: ldarg.0 + IL_0013: ldarg.2 + IL_0014: call valuetype Perfolizer.Horology.StartedClock Perfolizer.Horology.ClockExtensions::Start(class Perfolizer.Horology.IClock) + IL_0019: stfld valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdarg(clockArg); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.startClockMethod); + ilBuilder.Emit(OpCodes.Stfld, startedClockField); + /* + // __RunTask(); + IL_001e: ldarg.0 + IL_001f: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__RunTask() + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, runTaskMethod); + /* + // return new System.Threading.Tasks.ValueTask(valueTaskSource, valueTaskSource.Version); + IL_0024: ldarg.0 + IL_0025: ldfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + IL_002a: ldarg.0 + IL_002b: ldfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + IL_0030: callvirt instance int16 class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1::get_Version() + IL_0035: newobj instance void valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1::.ctor(class [System.Private.CoreLib]System.Threading.Tasks.Sources.IValueTaskSource`1, int16) + IL_003a: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, valueTaskSourceField); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, valueTaskSourceField); + var getVersionMethod = valueTaskSourceField.FieldType.GetProperty(nameof(Helpers.AutoResetValueTaskSource.Version), BindingFlagsPublicInstance).GetGetMethod(true); + ilBuilder.Emit(OpCodes.Callvirt, getVersionMethod); + var ctor = actionMethodBuilder.ReturnType.GetConstructor(new[] { valueTaskSourceField.FieldType, getVersionMethod.ReturnType }); + ilBuilder.Emit(OpCodes.Newobj, ctor); + ilBuilder.Emit(OpCodes.Ret); + + return workloadActionImplMethod = actionMethodBuilder; + } + + private MethodBuilder EmitSetContinuationImpl(RunnableEmitter runnableEmitter) + { + /* + .method private hidebysig + instance void __SetContinuation () cil managed + */ + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefinePrivateVoidInstanceMethod(SetContinuationMethodName); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + // continuation = __Continuation; + ilBuilder.EmitSetDelegateToThisField(continuationField, continuationMethod); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private MethodBuilder EmitRunTaskImpl(RunnableEmitter runnableEmitter) + { + /* + .method private hidebysig + instance void __RunTask () cil managed + */ + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + RunTaskMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnVoidParameter()); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + FieldInfo actionDelegateField = runnableEmitter.workloadDelegateField; + MethodInfo actionInvokeMethod = TypeBuilderExtensions.GetDelegateInvokeMethod(runnableEmitter.workloadDelegateType); + + // init locals + //.locals init ( + // [0] valuetype Perfolizer.Horology.ClockSpan clockspan, + // // [1] valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1, // If ValueTask + // [1] int64, + // [2] class [System.Private.CoreLib]System.Exception e + //) + var clockspanLocal = ilBuilder.DeclareLocal(typeof(ClockSpan)); + var argLocals = runnableEmitter.EmitDeclareArgLocals(ilBuilder); + LocalBuilder maybeValueTaskLocal = actionInvokeMethod.ReturnType.IsValueType + ? ilBuilder.DeclareLocal(actionInvokeMethod.ReturnType) + : null; + var indexLocal = ilBuilder.DeclareLocal(typeof(long)); + var exceptionLocal = ilBuilder.DeclareLocal(typeof(Exception)); + + var returnLabel = ilBuilder.DefineLabel(); + + // try { ... } + ilBuilder.BeginExceptionBlock(); + { + // load fields + runnableEmitter.EmitLoadArgFieldsToLocals(ilBuilder, argLocals); + + // while (--repeatsRemaining >= 0) { ... } + var loopStartLabel = ilBuilder.DefineLabel(); + var loopHeadLabel = ilBuilder.DefineLabel(); + ilBuilder.EmitLoopBeginFromFldTo0(loopStartLabel, loopHeadLabel); + { + /* + // currentAwaiter = workloadDelegate().GetAwaiter(); + IL_0002: ldarg.0 + IL_0003: ldarg.0 + IL_0004: ldfld class [System.Private.CoreLib]System.Func`1> BenchmarkDotNet.Autogenerated.Runnable_0::workloadDelegate + IL_0009: callvirt instance !0 class [System.Private.CoreLib]System.Func`1>::Invoke() + IL_000e: stloc.1 + IL_000f: ldloca.s 1 + IL_0011: call instance valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask`1::GetAwaiter() + IL_0016: stfld valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, actionDelegateField); + ilBuilder.EmitInstanceCallThisValueOnStack(null, actionInvokeMethod, argLocals); + ilBuilder.EmitInstanceCallThisValueOnStack(maybeValueTaskLocal, ConsumableInfo.WorkloadMethodReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlagsAllInstance)); + ilBuilder.Emit(OpCodes.Stfld, currentAwaiterField); + /* + // if (!currentAwaiter.IsCompleted) + IL_001b: ldarg.0 + IL_001c: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_0021: call instance bool valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::get_IsCompleted() + IL_0026: brtrue.s IL_003b + */ + var isCompletedLabel = ilBuilder.DefineLabel(); + + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Call, currentAwaiterField.FieldType.GetProperty(nameof(TaskAwaiter.IsCompleted), BindingFlagsAllInstance).GetGetMethod(true)); + ilBuilder.Emit(OpCodes.Brtrue, isCompletedLabel); + { + /* + // currentAwaiter.UnsafeOnCompleted(continuation); + IL_0028: ldarg.0 + IL_0029: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_002e: ldarg.0 + IL_002f: ldfld class [System.Private.CoreLib]System.Action BenchmarkDotNet.Autogenerated.Runnable_0::continuation + IL_0034: call instance void valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::UnsafeOnCompleted(class [System.Private.CoreLib]System.Action) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, continuationField); + ilBuilder.Emit(OpCodes.Call, currentAwaiterField.FieldType.GetMethod(nameof(TaskAwaiter.UnsafeOnCompleted), BindingFlagsAllInstance)); + // return; + ilBuilder.Emit(OpCodes.Leave, returnLabel); + } + ilBuilder.MarkLabel(isCompletedLabel); + /* + // currentAwaiter.GetResult(); + IL_003b: ldarg.0 + IL_003c: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_0041: call instance !0 valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::GetResult() + IL_0046: pop + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Call, getResultMethod); + if (getResultMethod.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + } + ilBuilder.EmitLoopEndFromFldTo0(loopStartLabel, loopHeadLabel, repeatsRemainingField, indexLocal); + } + // catch (System.Exception) { ... } + ilBuilder.BeginCatchBlock(typeof(Exception)); + { + /* + // __SetException(e); + IL_005f: stloc.3 + IL_0060: ldarg.0 + IL_0061: ldloc.3 + IL_0062: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__SetException(class [System.Private.CoreLib]System.Exception) + */ + ilBuilder.EmitStloc(exceptionLocal); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdloc(exceptionLocal); + ilBuilder.Emit(OpCodes.Call, setExceptionMethod); + // return; + ilBuilder.Emit(OpCodes.Leave, returnLabel); + } + ilBuilder.EndExceptionBlock(); + + /* + // var clockspan = startedClock.GetElapsed(); + IL_0069: ldarg.0 + IL_006a: ldflda valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + IL_006f: call instance valuetype Perfolizer.Horology.ClockSpan Perfolizer.Horology.StartedClock::GetElapsed() + IL_0074: stloc.0 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Call, runnableEmitter.getElapsedMethod); + ilBuilder.EmitStloc(clockspanLocal); + /* + // currentAwaiter = default; + IL_0075: ldarg.0 + IL_0076: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_007b: initobj valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Initobj, currentAwaiterField.FieldType); + /* + // startedClock = default(Perfolizer.Horology.StartedClock); + IL_0081: ldarg.0 + IL_0082: ldflda valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + IL_0087: initobj Perfolizer.Horology.StartedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Initobj, startedClockField.FieldType); + /* + // valueTaskSource.SetResult(clockspan); + IL_008d: ldarg.0 + IL_008e: ldfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + IL_0093: ldloc.0 + IL_0094: callvirt instance void class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1::SetResult(!0) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, valueTaskSourceField); + ilBuilder.EmitLdloc(clockspanLocal); + ilBuilder.Emit(OpCodes.Callvirt, valueTaskSourceField.FieldType.GetMethod(nameof(Helpers.AutoResetValueTaskSource.SetResult), BindingFlagsPublicInstance)); + + ilBuilder.MarkLabel(returnLabel); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private MethodBuilder EmitContinuationImpl(RunnableEmitter runnableEmitter) + { + /* + .method private hidebysig + instance void __Continuation () cil managed + */ + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + ContinuationMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnVoidParameter()); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + // init locals + var exceptionLocal = ilBuilder.DeclareLocal(typeof(Exception)); + + var returnLabel = ilBuilder.DefineLabel(); + + // try { ... } + ilBuilder.BeginExceptionBlock(); + { + /* + // currentAwaiter.GetResult(); + IL_0000: ldarg.0 + IL_0001: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_0006: call instance !0 valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::GetResult() + IL_000b: pop + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Call, getResultMethod); + if (getResultMethod.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + } + // catch (System.Exception e) { ... } + ilBuilder.BeginCatchBlock(typeof(Exception)); + { + // IL_000e: stloc.0 + ilBuilder.EmitStloc(exceptionLocal); + /* + // __SetException(e); + IL_000f: ldarg.0 + IL_0010: ldloc.0 + IL_0011: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__SetException(class [System.Private.CoreLib]System.Exception) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdloc(exceptionLocal); + ilBuilder.Emit(OpCodes.Call, setExceptionMethod); + // return; + ilBuilder.Emit(OpCodes.Leave, returnLabel); + } + ilBuilder.EndExceptionBlock(); + + /* + // __RunTask(); + IL_0018: ldarg.0 + IL_0019: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__RunTask() + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, runTaskMethod); + + // return; + ilBuilder.MarkLabel(returnLabel); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private MethodBuilder EmitSetExceptionImpl(RunnableEmitter runnableEmitter) + { + /* + .method private hidebysig + instance void __SetException ( + class [System.Private.CoreLib]System.Exception e + ) cil managed + */ + var exceptionArg = new EmitParameterInfo(0, "e", typeof(Exception)); + var actionMethodBuilder = runnableEmitter.runnableBuilder.DefineNonVirtualInstanceMethod( + SetExceptionMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnVoidParameter(), + exceptionArg); + exceptionArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + /* + // currentAwaiter = default; + IL_0000: ldarg.0 + IL_0001: ldflda valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_0::currentAwaiter + IL_0006: initobj valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, currentAwaiterField); + ilBuilder.Emit(OpCodes.Initobj, currentAwaiterField.FieldType); + /* + // startedClock = default(Perfolizer.Horology.StartedClock); + IL_000c: ldarg.0 + IL_000d: ldflda valuetype Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_0::startedClock + IL_0012: initobj Perfolizer.Horology.StartedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Initobj, startedClockField.FieldType); + /* + // valueTaskSource.SetException(e); + IL_0018: ldarg.0 + IL_0019: ldfld class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1 BenchmarkDotNet.Autogenerated.Runnable_0::valueTaskSource + IL_001e: ldarg.1 + IL_001f: callvirt instance void class BenchmarkDotNet.Helpers.AutoResetValueTaskSource`1::SetException(class [System.Private.CoreLib]System.Exception) + IL_0024: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, valueTaskSourceField); + ilBuilder.EmitLdarg(exceptionArg); + var setExceptionMethod = valueTaskSourceField.FieldType.GetMethod(nameof(Helpers.AutoResetValueTaskSource.SetException), BindingFlagsPublicInstance); + ilBuilder.Emit(OpCodes.Callvirt, setExceptionMethod); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs index c6e8cd8ae1..63237ee10b 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs @@ -37,12 +37,25 @@ public class RunnableConstants public const string WorkloadActionNoUnrollMethodName = "WorkloadActionNoUnroll"; public const string ForDisassemblyDiagnoserMethodName = "__ForDisassemblyDiagnoser__"; public const string InvokeCountParamName = "invokeCount"; + public const string ClockParamName = "clock"; public const string ConsumerFieldName = "consumer"; public const string NonGenericKeepAliveWithoutBoxingMethodName = "NonGenericKeepAliveWithoutBoxing"; public const string DummyParamName = "_"; public const string WorkloadDefaultValueHolderFieldName = "workloadDefaultValueHolder"; + public const string ValueTaskSourceFieldName = "valueTaskSource"; + public const string RepeatsRemainingFieldName = "repeatsRemaining"; + public const string ContinuationFieldName = "continuation"; + public const string StartedClockFieldName = "startedClock"; + public const string CurrentAwaiterFieldName = "currentAwaiter"; + public const string OverheadActionImplMethodName = "OverheadActionImpl"; + public const string WorkloadActionImplMethodName = "WorkloadActionImpl"; + public const string SetContinuationMethodName = "__SetContinuation"; + public const string RunTaskMethodName = "__RunTask"; + public const string ContinuationMethodName = "__Continuation"; + public const string SetExceptionMethodName = "__SetException"; + public const string GlobalSetupMethodName = "GlobalSetup"; public const string GlobalCleanupMethodName = "GlobalCleanup"; public const string IterationSetupMethodName = "IterationSetup"; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs index f8cfd02ad1..d1fa515d00 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs @@ -1,8 +1,10 @@ using System; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using BenchmarkDotNet.Parameters; using BenchmarkDotNet.Running; +using Perfolizer.Horology; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation @@ -104,9 +106,9 @@ public static void SetParameter( } } - public static Action CallbackFromField(T instance, string memberName) + public static Func CallbackFromField(T instance, string memberName) { - return GetFieldValueCore(instance, memberName); + return GetFieldValueCore>(instance, memberName); } public static Action CallbackFromMethod(T instance, string memberName) @@ -114,9 +116,9 @@ public static Action CallbackFromMethod(T instance, string memberName) return GetDelegateCore(instance, memberName); } - public static Action LoopCallbackFromMethod(T instance, string memberName) + public static Func> LoopCallbackFromMethod(T instance, string memberName) { - return GetDelegateCore>(instance, memberName); + return GetDelegateCore>>(instance, memberName); } private static TResult GetFieldValueCore(T instance, string memberName) diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkAction.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkAction.cs index df1911d0b0..01b69fccae 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkAction.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkAction.cs @@ -1,6 +1,7 @@ using System; - +using System.Threading.Tasks; using JetBrains.Annotations; +using Perfolizer.Horology; namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit { @@ -8,16 +9,9 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit [PublicAPI] public abstract class BenchmarkAction { - /// Gets or sets invoke single callback. - /// Invoke single callback. - public Action InvokeSingle { get; protected set; } - - /// Gets or sets invoke multiple times callback. - /// Invoke multiple times callback. - public Action InvokeMultiple { get; protected set; } - - /// Gets the last run result. - /// The last run result. + public Func InvokeSingle { get; protected set; } + public Func> InvokeUnroll { get; protected set; } + public Func> InvokeNoUnroll { get; protected set; } public virtual object LastRunResult => null; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs index 6b0f2468d8..9b28a275d4 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs @@ -1,13 +1,10 @@ using System; using System.Reflection; using System.Runtime.CompilerServices; -using System.Threading.Tasks; - +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Running; -using JetBrains.Annotations; - namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit { /// Helper class that creates instances. @@ -22,34 +19,16 @@ private static BenchmarkAction CreateCore( object instance, MethodInfo? targetMethod, MethodInfo? fallbackIdleSignature, - int unrollFactor) + int unrollFactor, + IConfig config) { PrepareInstanceAndResultType(instance, targetMethod, fallbackIdleSignature, out var resultInstance, out var resultType); if (resultType == typeof(void)) return new BenchmarkActionVoid(resultInstance, targetMethod, unrollFactor); - if (resultType == typeof(Task)) - return new BenchmarkActionTask(resultInstance, targetMethod, unrollFactor); - - if (resultType.GetTypeInfo().IsGenericType) - { - var genericType = resultType.GetGenericTypeDefinition(); - var argType = resultType.GenericTypeArguments[0]; - if (typeof(Task<>) == genericType) - return Create( - typeof(BenchmarkActionTask<>).MakeGenericType(argType), - resultInstance, - targetMethod, - unrollFactor); - - if (typeof(ValueTask<>).IsAssignableFrom(genericType)) - return Create( - typeof(BenchmarkActionValueTask<>).MakeGenericType(argType), - resultInstance, - targetMethod, - unrollFactor); - } + if (config.GetIsAwaitable(resultType, out var adapter)) + return CreateBenchmarkActionAwaitable(adapter, resultInstance, targetMethod, unrollFactor); if (targetMethod == null && resultType.GetTypeInfo().IsValueType) // for Idle: we return int because creating bigger ValueType could take longer than benchmarked method itself. @@ -62,6 +41,23 @@ private static BenchmarkAction CreateCore( unrollFactor); } + private static BenchmarkActionBase CreateBenchmarkActionAwaitable(ConcreteAsyncAdapter adapter, object instance, MethodInfo method, int unrollFactor) + { + if (adapter.resultType == null) + { + return (BenchmarkActionBase) Activator.CreateInstance( + typeof(BenchmarkActionAwaitable<,,,>).MakeGenericType(adapter.asyncMethodBuilderAdapterType, adapter.awaitableAdapterType, adapter.awaitableType, adapter.awaiterType), + instance, + method, + unrollFactor); + } + return (BenchmarkActionBase) Activator.CreateInstance( + typeof(BenchmarkActionAwaitable<,,,,>).MakeGenericType(adapter.asyncMethodBuilderAdapterType, adapter.awaitableAdapterType, adapter.awaitableType, adapter.awaiterType, adapter.resultType), + instance, + method, + unrollFactor); + } + private static void PrepareInstanceAndResultType( object instance, MethodInfo targetMethod, MethodInfo fallbackIdleSignature, out object resultInstance, out Type resultType) @@ -98,53 +94,68 @@ private static void FallbackMethod() { } private static readonly MethodInfo FallbackSignature = new Action(FallbackMethod).GetMethodInfo(); private static readonly MethodInfo DummyMethod = typeof(DummyInstance).GetMethod(nameof(DummyInstance.Dummy)); + internal static int GetUnrollFactor(BenchmarkCase benchmarkCase) + { + if (benchmarkCase.Config.GetIsAwaitable(benchmarkCase.Descriptor.WorkloadMethod.ReturnType, out _)) + { + benchmarkCase.ForceUnrollFactorForAsync(); + } + return benchmarkCase.Job.ResolveValue(Jobs.RunMode.UnrollFactorCharacteristic, Environments.EnvironmentResolver.Instance); + } + /// Creates run benchmark action. /// Descriptor info. /// Instance of target. /// Unroll factor. + /// Config. /// Run benchmark action. - public static BenchmarkAction CreateWorkload(Descriptor descriptor, object instance, int unrollFactor) => - CreateCore(instance, descriptor.WorkloadMethod, null, unrollFactor); + public static BenchmarkAction CreateWorkload(Descriptor descriptor, object instance, int unrollFactor, IConfig config) => + CreateCore(instance, descriptor.WorkloadMethod, null, unrollFactor, config); /// Creates idle benchmark action. /// Descriptor info. /// Instance of target. /// Unroll factor. + /// Config. /// Idle benchmark action. - public static BenchmarkAction CreateOverhead(Descriptor descriptor, object instance, int unrollFactor) => - CreateCore(instance, null, descriptor.WorkloadMethod, unrollFactor); + public static BenchmarkAction CreateOverhead(Descriptor descriptor, object instance, int unrollFactor, IConfig config) => + CreateCore(instance, null, descriptor.WorkloadMethod, unrollFactor, config); /// Creates global setup benchmark action. /// Descriptor info. /// Instance of target. + /// Config. /// Setup benchmark action. - public static BenchmarkAction CreateGlobalSetup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.GlobalSetupMethod, FallbackSignature, 1); + public static BenchmarkAction CreateGlobalSetup(Descriptor descriptor, object instance, IConfig config) => + CreateCore(instance, descriptor.GlobalSetupMethod, FallbackSignature, 1, config); /// Creates global cleanup benchmark action. /// Descriptor info. /// Instance of target. + /// Config. /// Cleanup benchmark action. - public static BenchmarkAction CreateGlobalCleanup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.GlobalCleanupMethod, FallbackSignature, 1); + public static BenchmarkAction CreateGlobalCleanup(Descriptor descriptor, object instance, IConfig config) => + CreateCore(instance, descriptor.GlobalCleanupMethod, FallbackSignature, 1, config); /// Creates global setup benchmark action. /// Descriptor info. /// Instance of target. + /// Config. /// Setup benchmark action. - public static BenchmarkAction CreateIterationSetup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.IterationSetupMethod, FallbackSignature, 1); + public static BenchmarkAction CreateIterationSetup(Descriptor descriptor, object instance, IConfig config) => + CreateCore(instance, descriptor.IterationSetupMethod, FallbackSignature, 1, config); /// Creates global cleanup benchmark action. /// Descriptor info. /// Instance of target. + /// Config. /// Cleanup benchmark action. - public static BenchmarkAction CreateIterationCleanup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.IterationCleanupMethod, FallbackSignature, 1); + public static BenchmarkAction CreateIterationCleanup(Descriptor descriptor, object instance, IConfig config) => + CreateCore(instance, descriptor.IterationCleanupMethod, FallbackSignature, 1, config); /// Creates a dummy benchmark action. /// Dummy benchmark action. public static BenchmarkAction CreateDummy() => - CreateCore(new DummyInstance(), DummyMethod, null, 1); + CreateCore(new DummyInstance(), DummyMethod, null, 1, DefaultConfig.Instance); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs index eef3ce8997..d86d40b19f 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs @@ -1,5 +1,9 @@ -using System; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; +using Perfolizer.Horology; +using System; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit @@ -22,19 +26,36 @@ internal class BenchmarkActionVoid : BenchmarkActionBase public BenchmarkActionVoid(object instance, MethodInfo method, int unrollFactor) { callback = CreateWorkloadOrOverhead(instance, method, OverheadStatic, OverheadInstance); - InvokeSingle = callback; + InvokeSingle = InvokeSingleHardcoded; unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + InvokeUnroll = InvokeUnrollHardcoded; + InvokeNoUnroll = InvokeNoUnrollHardcoded; } private static void OverheadStatic() { } private void OverheadInstance() { } - private void InvokeMultipleHardcoded(long repeatCount) + private ValueTask InvokeSingleHardcoded() { + callback(); + return new ValueTask(); + } + + private ValueTask InvokeUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); for (long i = 0; i < repeatCount; i++) unrolledCallback(); + return new ValueTask(startedClock.GetElapsed()); + } + + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + { + var startedClock = clock.Start(); + for (long i = 0; i < repeatCount; i++) + callback(); + return new ValueTask(startedClock.GetElapsed()); } } @@ -50,145 +71,108 @@ public BenchmarkAction(object instance, MethodInfo method, int unrollFactor) InvokeSingle = InvokeSingleHardcoded; unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + InvokeUnroll = InvokeUnrollHardcoded; + InvokeNoUnroll = InvokeNoUnrollHardcoded; } private static T OverheadStatic() => default; private T OverheadInstance() => default; - private void InvokeSingleHardcoded() => result = callback(); + private ValueTask InvokeSingleHardcoded() + { + result = callback(); + return new ValueTask(); + } - private void InvokeMultipleHardcoded(long repeatCount) + private ValueTask InvokeUnrollHardcoded(long repeatCount, IClock clock) { + var startedClock = clock.Start(); for (long i = 0; i < repeatCount; i++) result = unrolledCallback(); + return new ValueTask(startedClock.GetElapsed()); } - public override object LastRunResult => result; - } - - internal class BenchmarkActionTask : BenchmarkActionBase - { - private readonly Func startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; - - public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) { - bool isIdle = method == null; - if (!isIdle) - { - startTaskCallback = CreateWorkload>(instance, method); - callback = ExecuteBlocking; - } - else - { - callback = Overhead; - } - - InvokeSingle = callback; - - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; - + var startedClock = clock.Start(); + for (long i = 0; i < repeatCount; i++) + result = callback(); + return new ValueTask(startedClock.GetElapsed()); } - // must be kept in sync with VoidDeclarationsProvider.IdleImplementation - private void Overhead() { } + public override object LastRunResult => result; + } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => startTaskCallback.Invoke().GetAwaiter().GetResult(); + private readonly struct AwaitableFunc : IFunc + { + private readonly Func callback; - private void InvokeMultipleHardcoded(long repeatCount) - { - for (long i = 0; i < repeatCount; i++) - unrolledCallback(); - } + internal AwaitableFunc(Func callback) => this.callback = callback; + public TAwaitable Invoke() => callback(); } - internal class BenchmarkActionTask : BenchmarkActionBase + internal class BenchmarkActionAwaitable : BenchmarkActionBase + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter, new() + where TAwaiter : ICriticalNotifyCompletion { - private readonly Func> startTaskCallback; - private readonly Func callback; - private readonly Func unrolledCallback; - private T result; + private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; - public BenchmarkActionTask(object instance, MethodInfo method, int unrollFactor) + public BenchmarkActionAwaitable(object instance, MethodInfo method, int unrollFactor) { - bool isOverhead = method == null; - if (!isOverhead) + bool isIdle = method == null; + if (!isIdle) { - startTaskCallback = CreateWorkload>>(instance, method); - callback = ExecuteBlocking; + var callback = CreateWorkload>(instance, method); + asyncBenchmarkRunner = new AsyncWorkloadRunner, TAsyncMethodBuilderAdapter, TAwaitableAdapter, TAwaitable, TAwaiter>(new (callback)); } else { - callback = Overhead; + asyncBenchmarkRunner = new AsyncOverheadRunner, TAsyncMethodBuilderAdapter, TAwaitable, TAwaiter>(new (Overhead)); } - InvokeSingle = InvokeSingleHardcoded; - - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } - private T Overhead() => default; - - // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => startTaskCallback().GetAwaiter().GetResult(); + private TAwaitable Overhead() => default; - private void InvokeSingleHardcoded() => result = callback(); - - private void InvokeMultipleHardcoded(long repeatCount) - { - for (long i = 0; i < repeatCount; i++) - result = unrolledCallback(); - } + protected virtual ValueTask InvokeSingleHardcoded() + => asyncBenchmarkRunner.InvokeSingle(); - public override object LastRunResult => result; + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + => asyncBenchmarkRunner.Invoke(repeatCount, clock); } - internal class BenchmarkActionValueTask : BenchmarkActionBase + internal class BenchmarkActionAwaitable : BenchmarkActionBase + where TAsyncMethodBuilderAdapter : IAsyncMethodBuilderAdapter, new() + where TAwaitableAdapter : IAwaitableAdapter, new() + where TAwaiter : ICriticalNotifyCompletion { - private readonly Func> startTaskCallback; - private readonly Func callback; - private readonly Func unrolledCallback; - private T result; + private readonly AsyncBenchmarkRunner asyncBenchmarkRunner; - public BenchmarkActionValueTask(object instance, MethodInfo method, int unrollFactor) + public BenchmarkActionAwaitable(object instance, MethodInfo method, int unrollFactor) { - bool isOverhead = method == null; - if (!isOverhead) + bool isIdle = method == null; + if (!isIdle) { - startTaskCallback = CreateWorkload>>(instance, method); - callback = ExecuteBlocking; + var callback = CreateWorkload>(instance, method); + asyncBenchmarkRunner = new AsyncWorkloadRunner, TAsyncMethodBuilderAdapter, TAwaitableAdapter, TAwaitable, TAwaiter, TResult>(new (callback)); } else { - callback = Overhead; + asyncBenchmarkRunner = new AsyncOverheadRunner, TAsyncMethodBuilderAdapter, TAwaitable, TAwaiter, TResult>(new (Overhead)); } - InvokeSingle = InvokeSingleHardcoded; - - - unrolledCallback = Unroll(callback, unrollFactor); - InvokeMultiple = InvokeMultipleHardcoded; + InvokeUnroll = InvokeNoUnroll = InvokeNoUnrollHardcoded; } - private T Overhead() => default; - - // must be kept in sync with GenericTaskDeclarationsProvider.TargetMethodDelegate - private T ExecuteBlocking() => startTaskCallback().GetAwaiter().GetResult(); + private TAwaitable Overhead() => default; - private void InvokeSingleHardcoded() => result = callback(); - - private void InvokeMultipleHardcoded(long repeatCount) - { - for (long i = 0; i < repeatCount; i++) - result = unrolledCallback(); - } + protected virtual ValueTask InvokeSingleHardcoded() + => asyncBenchmarkRunner.InvokeSingle(); - public override object LastRunResult => result; + private ValueTask InvokeNoUnrollHardcoded(long repeatCount, IClock clock) + => asyncBenchmarkRunner.Invoke(repeatCount, clock); } } } \ 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 e890000683..972e187c48 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using System.Threading.Tasks; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Exporters; @@ -7,6 +8,7 @@ using BenchmarkDotNet.Running; using JetBrains.Annotations; +using Perfolizer.Horology; namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit { @@ -102,18 +104,18 @@ private static class Runnable { public static void RunCore(IHost host, BenchmarkCase benchmarkCase) { + int unrollFactor = BenchmarkActionFactory.GetUnrollFactor(benchmarkCase); var target = benchmarkCase.Descriptor; var job = benchmarkCase.Job; // TODO: filter job (same as SourceCodePresenter does)? - int unrollFactor = benchmarkCase.Job.ResolveValue(RunMode.UnrollFactorCharacteristic, EnvironmentResolver.Instance); // DONTTOUCH: these should be allocated together var instance = Activator.CreateInstance(benchmarkCase.Descriptor.Type); - var workloadAction = BenchmarkActionFactory.CreateWorkload(target, instance, unrollFactor); - var overheadAction = BenchmarkActionFactory.CreateOverhead(target, instance, unrollFactor); - var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(target, instance); - var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(target, instance); - var iterationSetupAction = BenchmarkActionFactory.CreateIterationSetup(target, instance); - var iterationCleanupAction = BenchmarkActionFactory.CreateIterationCleanup(target, instance); + var workloadAction = BenchmarkActionFactory.CreateWorkload(target, instance, unrollFactor, benchmarkCase.Config); + var overheadAction = BenchmarkActionFactory.CreateOverhead(target, instance, unrollFactor, benchmarkCase.Config); + var globalSetupAction = BenchmarkActionFactory.CreateGlobalSetup(target, instance, benchmarkCase.Config); + var globalCleanupAction = BenchmarkActionFactory.CreateGlobalCleanup(target, instance, benchmarkCase.Config); + var iterationSetupAction = BenchmarkActionFactory.CreateIterationSetup(target, instance, benchmarkCase.Config); + var iterationCleanupAction = BenchmarkActionFactory.CreateIterationCleanup(target, instance, benchmarkCase.Config); var dummy1 = BenchmarkActionFactory.CreateDummy(); var dummy2 = BenchmarkActionFactory.CreateDummy(); var dummy3 = BenchmarkActionFactory.CreateDummy(); @@ -129,21 +131,13 @@ public static void RunCore(IHost host, BenchmarkCase benchmarkCase) var engineParameters = new EngineParameters { Host = host, - WorkloadActionNoUnroll = invocationCount => - { - for (int i = 0; i < invocationCount; i++) - workloadAction.InvokeSingle(); - }, - WorkloadActionUnroll = workloadAction.InvokeMultiple, - Dummy1Action = dummy1.InvokeSingle, - Dummy2Action = dummy2.InvokeSingle, - Dummy3Action = dummy3.InvokeSingle, - OverheadActionNoUnroll = invocationCount => - { - for (int i = 0; i < invocationCount; i++) - overheadAction.InvokeSingle(); - }, - OverheadActionUnroll = overheadAction.InvokeMultiple, + WorkloadActionNoUnroll = workloadAction.InvokeNoUnroll, + WorkloadActionUnroll = workloadAction.InvokeUnroll, + Dummy1Action = () => dummy1.InvokeSingle(), + Dummy2Action = () => dummy2.InvokeSingle(), + Dummy3Action = () => dummy3.InvokeSingle(), + OverheadActionNoUnroll = overheadAction.InvokeNoUnroll, + OverheadActionUnroll = overheadAction.InvokeUnroll, GlobalSetupAction = globalSetupAction.InvokeSingle, GlobalCleanupAction = globalCleanupAction.InvokeSingle, IterationSetupAction = iterationSetupAction.InvokeSingle, diff --git a/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs b/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs index 0718d6b8cb..68082ef0dc 100644 --- a/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs @@ -55,8 +55,10 @@ internal static IEnumerable GetAllReferences(BenchmarkCase benchmarkCa .Concat( new[] { - benchmarkCase.Descriptor.Type.GetTypeInfo().Assembly, // this assembly does not has to have a reference to BenchmarkDotNet (e.g. custom framework for benchmarking that internally uses BenchmarkDotNet - typeof(BenchmarkCase).Assembly // BenchmarkDotNet + benchmarkCase.Descriptor.Type.GetTypeInfo().Assembly, // this assembly does not have to have a reference to BenchmarkDotNet (e.g. custom framework for benchmarking that internally uses BenchmarkDotNet + typeof(BenchmarkCase).Assembly, // BenchmarkDotNet + typeof(System.Threading.Tasks.ValueTask).Assembly, // TaskExtensions + typeof(Perfolizer.Horology.IClock).Assembly // Perfolizer }) .Distinct(); } diff --git a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs index 92d7422ae2..b495e4d87f 100644 --- a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs +++ b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Running; namespace BenchmarkDotNet.Validators @@ -130,21 +131,8 @@ private void TryToGetTaskResult(object result) 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 errors) diff --git a/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs b/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs index 7ddb73025c..fc31054f38 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs @@ -49,13 +49,20 @@ public AllSetupAndCleanupTest(ITestOutputHelper output) : base(output) { } 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(config); + var summary = CanExecute(benchmarkType, config); var actualLogLines = GetActualLogLines(summary); foreach (string line in actualLogLines) @@ -84,30 +91,16 @@ public class AllSetupAndCleanupAttributeBenchmarks 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(config); - - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); - } - - public class AllSetupAndCleanupAttributeBenchmarksAsync + public class AllSetupAndCleanupAttributeBenchmarksTask { private int setupCounter; private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public Task IterationSetup() => Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")"); [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public Task IterationCleanup() => Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); [GlobalSetup] public Task GlobalSetup() => Console.Out.WriteLineAsync(GlobalSetupCalled); @@ -116,71 +109,94 @@ public class AllSetupAndCleanupAttributeBenchmarksAsync 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(config); - - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); + public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - public class AllSetupAndCleanupAttributeBenchmarksAsyncTaskSetup + public class AllSetupAndCleanupAttributeBenchmarksGenericTask { private int setupCounter; private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public async Task IterationSetup() + { + await Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")"); + + return 42; + } [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public async Task IterationCleanup() + { + await Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + + return 42; + } [GlobalSetup] - public Task GlobalSetup() => Console.Out.WriteLineAsync(GlobalSetupCalled); + public async Task GlobalSetup() + { + await Console.Out.WriteLineAsync(GlobalSetupCalled); + + return 42; + } [GlobalCleanup] - public Task GlobalCleanup() => Console.Out.WriteLineAsync(GlobalCleanupCalled); + public async Task GlobalCleanup() + { + await Console.Out.WriteLineAsync(GlobalCleanupCalled); + + return 42; + } [Benchmark] public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - [Fact] - public void AllSetupAndCleanupMethodRunsAsyncGenericTaskSetupTest() + public class AllSetupAndCleanupAttributeBenchmarksValueTask { - var miniJob = Job.Default.WithStrategy(RunStrategy.Monitoring).WithWarmupCount(2).WithIterationCount(3).WithInvocationCount(1).WithUnrollFactor(1).WithId("MiniJob"); - var config = CreateSimpleConfig(job: miniJob); + private int setupCounter; + private int cleanupCounter; - var summary = CanExecute(config); + [IterationSetup] + public ValueTask IterationSetup() => new ValueTask(Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")")); - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); + [IterationCleanup] + public ValueTask IterationCleanup() => new ValueTask(Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")")); + + [GlobalSetup] + public ValueTask GlobalSetup() => new ValueTask(Console.Out.WriteLineAsync(GlobalSetupCalled)); + + [GlobalCleanup] + public ValueTask GlobalCleanup() => new ValueTask(Console.Out.WriteLineAsync(GlobalCleanupCalled)); + + [Benchmark] + public void Benchmark() => Console.WriteLine(BenchmarkCalled); } - public class AllSetupAndCleanupAttributeBenchmarksAsyncGenericTaskSetup + public class AllSetupAndCleanupAttributeBenchmarksGenericValueTask { private int setupCounter; private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public async ValueTask IterationSetup() + { + await Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")"); + + return 42; + } [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public async ValueTask IterationCleanup() + { + await Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + + return 42; + } [GlobalSetup] - public async Task GlobalSetup() + public async ValueTask GlobalSetup() { await Console.Out.WriteLineAsync(GlobalSetupCalled); @@ -188,7 +204,7 @@ public async Task GlobalSetup() } [GlobalCleanup] - public async Task GlobalCleanup() + public async ValueTask GlobalCleanup() { await Console.Out.WriteLineAsync(GlobalCleanupCalled); @@ -199,80 +215,84 @@ public async Task GlobalCleanup() 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(config); - - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); - } - - public class AllSetupAndCleanupAttributeBenchmarksAsyncValueTaskSetup + public class AllSetupAndCleanupAttributeBenchmarksValueTaskSource { + private readonly ValueTaskSource valueTaskSource = new (); private int setupCounter; private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public ValueTask IterationSetup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")").ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public ValueTask IterationCleanup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")").ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [GlobalSetup] - public ValueTask GlobalSetup() => new ValueTask(Console.Out.WriteLineAsync(GlobalSetupCalled)); + public ValueTask GlobalSetup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(GlobalSetupCalled).ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [GlobalCleanup] - public ValueTask GlobalCleanup() => new ValueTask(Console.Out.WriteLineAsync(GlobalCleanupCalled)); + 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); } - [FactNotGitHubActionsWindows] - 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(config); - - var actualLogLines = GetActualLogLines(summary); - foreach (string line in actualLogLines) - Output.WriteLine(line); - Assert.Equal(expectedLogLines, actualLogLines); - } - - public class AllSetupAndCleanupAttributeBenchmarksAsyncGenericValueTaskSetup + public class AllSetupAndCleanupAttributeBenchmarksGenericValueTaskSource { + private readonly ValueTaskSource valueTaskSource = new (); private int setupCounter; private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public ValueTask IterationSetup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")").ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public ValueTask IterationCleanup() + { + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")").ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [GlobalSetup] - public async ValueTask GlobalSetup() + public ValueTask GlobalSetup() { - await Console.Out.WriteLineAsync(GlobalSetupCalled); - - return 42; + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(GlobalSetupCalled).ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); } [GlobalCleanup] - public async ValueTask GlobalCleanup() + public ValueTask GlobalCleanup() { - await Console.Out.WriteLineAsync(GlobalCleanupCalled); - - return 42; + valueTaskSource.Reset(); + Console.Out.WriteLineAsync(GlobalCleanupCalled).ContinueWith(_ => valueTaskSource.SetResult(42)); + return new ValueTask(valueTaskSource, valueTaskSource.Token); } [Benchmark] diff --git a/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs b/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs index 8eb149b5dc..e9d1f3785e 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs @@ -1,10 +1,27 @@ -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 : IValueTaskSource, IValueTaskSource + { + private ManualResetValueTaskSourceCore _core; + + T IValueTaskSource.GetResult(short token) => _core.GetResult(token); + void IValueTaskSource.GetResult(short token) => _core.GetResult(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags); + void IValueTaskSource.OnCompleted(Action 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) { } @@ -26,6 +43,8 @@ public void TaskReturningMethodsAreAwaited() public class TaskDelayMethods { + private readonly ValueTaskSource valueTaskSource = new (); + private const int MillisecondsDelay = 100; internal const double NanosecondsDelay = MillisecondsDelay * 1e+6; @@ -39,6 +58,17 @@ public class TaskDelayMethods [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 +77,17 @@ public class TaskDelayMethods [Benchmark] public ValueTask ReturningGenericValueTask() => new ValueTask(ReturningGenericTask()); + + [Benchmark] + public ValueTask ReturningGenericValueTaskBackByIValueTaskSource() + { + valueTaskSource.Reset(); + Task.Delay(MillisecondsDelay).ContinueWith(_ => + { + valueTaskSource.SetResult(default); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs index 032e2643d8..1171b0f014 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs @@ -50,7 +50,7 @@ public Reports.Summary CanExecute(IConfig config = null, bool fullVa /// Optional custom config to be used instead of the default /// Optional: disable validation (default = true/enabled) /// The summary from the benchmark run - protected Reports.Summary CanExecute(Type type, IConfig config = null, bool fullValidation = true) + public Reports.Summary CanExecute(Type type, IConfig config = null, bool fullValidation = true) { // Add logging, so the Benchmark execution is in the TestRunner output (makes Debugging easier) if (config == null) diff --git a/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs b/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs index 43a7bac9ee..73beb58203 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs @@ -9,6 +9,8 @@ using BenchmarkDotNet.Reports; using BenchmarkDotNet.Characteristics; using Perfolizer.Mathematics.OutlierDetection; +using System.Threading.Tasks; +using Perfolizer.Horology; namespace BenchmarkDotNet.IntegrationTests { @@ -90,10 +92,10 @@ public void WriteLine() { } public void WriteLine(string line) { } public Job TargetJob { get; } public long OperationsPerInvoke { get; } - public Action GlobalSetupAction { get; set; } - public Action GlobalCleanupAction { get; set; } - public Action WorkloadAction { get; } - public Action OverheadAction { get; } + public Func GlobalSetupAction { get; set; } + public Func GlobalCleanupAction { get; set; } + public Func> WorkloadAction { get; } + public Func> OverheadAction { get; } public IResolver Resolver { get; } public Measurement RunIteration(IterationData data) { throw new NotImplementedException(); } diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs index 65cd9c752e..1cc4f8b033 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; @@ -31,7 +32,9 @@ public class NaiveRunnableEmitDiff { { OpCodes.Br_S, OpCodes.Br }, { OpCodes.Blt_S, OpCodes.Blt }, - { OpCodes.Bne_Un_S, OpCodes.Bne_Un } + { OpCodes.Bne_Un_S, OpCodes.Bne_Un }, + { OpCodes.Bge_S, OpCodes.Bge }, + { OpCodes.Brtrue_S, OpCodes.Brtrue }, }; public static void RunDiff(string roslynAssemblyPath, string emittedAssemblyPath, ILogger logger) @@ -63,7 +66,15 @@ private static bool AreSameTypeIgnoreNested(TypeReference left, TypeReference ri private static bool AreSameSignature(MethodReference left, MethodReference right) { - return (left.Name == right.Name || (left.Name.StartsWith("<.ctor>") && right.Name == "__Workload")) + var lookup = new HashSet() + { + RunnableConstants.WorkloadImplementationMethodName, + RunnableConstants.GlobalSetupMethodName, + RunnableConstants.GlobalCleanupMethodName, + RunnableConstants.IterationSetupMethodName, + RunnableConstants.IterationCleanupMethodName + }; + return (left.Name == right.Name || (left.Name.StartsWith("<.ctor>") && lookup.Contains(right.Name))) && AreSameTypeIgnoreNested(left.ReturnType, right.ReturnType) && left.Parameters.Count == right.Parameters.Count && left.Parameters @@ -80,7 +91,9 @@ private static List GetOpInstructions(MethodDefinition method) var result = new List(bodyInstructions.Count); foreach (var instruction in bodyInstructions) { - if (compareNops || instruction.OpCode != OpCodes.Nop) + // Skip leave instructions since the IlBuilder forces them differently than Roslyn. + if (instruction.OpCode != OpCodes.Leave && instruction.OpCode != OpCodes.Leave_S + && (compareNops || instruction.OpCode != OpCodes.Nop)) result.Add(instruction); } @@ -289,14 +302,17 @@ private static void DiffMembers(TypeDefinition type1, TypeDefinition type2, ILog } var methods2ByName = type2.Methods.ToLookup(f => f.Name); + var methods2ByComparison = new HashSet(type2.Methods); foreach (var method1 in type1.Methods) { logger.Write($" method {method1.FullName}"); var method2 = methods2ByName[method1.Name].SingleOrDefault(m => AreSameSignature(method1, m)); if (method2 == null) - method2 = type2.Methods.SingleOrDefault(m => AreSameSignature(method1, m)); - if (method2 == null) + method2 = methods2ByComparison.FirstOrDefault(m => AreSameSignature(method1, m)); + if (method2 != null) + methods2ByComparison.Remove(method2); + else method2 = methods2ByName[method1.Name].SingleOrDefault(); if (Diff(method1, method2)) diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs index 1d5619a27c..d8f1f8a5cc 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs @@ -153,6 +153,13 @@ public async Task InvokeOnceTaskAsync() Interlocked.Increment(ref Counter); } + [Benchmark] + public async ValueTask InvokeOnceValueTaskAsync() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + [Benchmark] public string InvokeOnceRefType() { @@ -195,6 +202,13 @@ public static async Task InvokeOnceStaticTaskAsync() Interlocked.Increment(ref Counter); } + [Benchmark] + public static async ValueTask InvokeOnceStaticValueTaskAsync() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + [Benchmark] public static string InvokeOnceStaticRefType() { @@ -225,35 +239,229 @@ public static ValueTask InvokeOnceStaticValueTaskOfT() } } - [Fact] - public void InProcessEmitToolchainSupportsIterationSetupAndCleanup() + [Theory] + [InlineData(typeof(IterationSetupCleanup))] + [InlineData(typeof(IterationSetupCleanupTask))] + [InlineData(typeof(IterationSetupCleanupValueTask))] + [InlineData(typeof(IterationSetupCleanupValueTaskSource))] + [InlineData(typeof(GlobalSetupCleanup))] + [InlineData(typeof(GlobalSetupCleanupTask))] + [InlineData(typeof(GlobalSetupCleanupValueTask))] + [InlineData(typeof(GlobalSetupCleanupValueTaskSource))] + public void InProcessEmitToolchainSupportsSetupAndCleanup(Type benchmarkType) { var logger = new OutputLogger(Output); var config = CreateInProcessConfig(logger); - WithIterationSetupAndCleanup.SetupCounter = 0; - WithIterationSetupAndCleanup.BenchmarkCounter = 0; - WithIterationSetupAndCleanup.CleanupCounter = 0; + Counters.SetupCounter = 0; + Counters.BenchmarkCounter = 0; + Counters.CleanupCounter = 0; - var summary = CanExecute(config); + var summary = CanExecute(benchmarkType, config); - Assert.Equal(1, WithIterationSetupAndCleanup.SetupCounter); - Assert.Equal(16, WithIterationSetupAndCleanup.BenchmarkCounter); - Assert.Equal(1, WithIterationSetupAndCleanup.CleanupCounter); + Assert.Equal(1, Counters.SetupCounter); + Assert.Equal(16, Counters.BenchmarkCounter); + Assert.Equal(1, Counters.CleanupCounter); } - public class WithIterationSetupAndCleanup + private static class Counters { public static int SetupCounter, BenchmarkCounter, CleanupCounter; + } + + public class IterationSetupCleanup + { + [IterationSetup] + public void Setup() => Interlocked.Increment(ref Counters.SetupCounter); + + [Benchmark] + public void Benchmark() => Interlocked.Increment(ref Counters.BenchmarkCounter); + + [IterationCleanup] + public void Cleanup() => Interlocked.Increment(ref Counters.CleanupCounter); + } + + public class IterationSetupCleanupTask + { + [IterationSetup] + public static async Task IterationSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.SetupCounter); + } + + [IterationCleanup] + public async Task IterationCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.CleanupCounter); + } + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class IterationSetupCleanupValueTask + { [IterationSetup] - public void Setup() => Interlocked.Increment(ref SetupCounter); + public static async ValueTask IterationSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.SetupCounter); + } + + [IterationCleanup] + public async ValueTask IterationCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.CleanupCounter); + } [Benchmark] - public void Benchmark() => Interlocked.Increment(ref BenchmarkCounter); + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class IterationSetupCleanupValueTaskSource + { + private readonly static ValueTaskSource valueTaskSource = new (); + + [IterationSetup] + public static ValueTask IterationSetup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + Interlocked.Increment(ref Counters.SetupCounter); + valueTaskSource.SetResult(42); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } [IterationCleanup] - public void Cleanup() => Interlocked.Increment(ref CleanupCounter); + public ValueTask IterationCleanup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + Interlocked.Increment(ref Counters.CleanupCounter); + valueTaskSource.SetResult(42); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class GlobalSetupCleanup + { + [GlobalSetup] + public static void GlobalSetup() + { + Interlocked.Increment(ref Counters.SetupCounter); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + Interlocked.Increment(ref Counters.CleanupCounter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class GlobalSetupCleanupTask + { + [GlobalSetup] + public static async Task GlobalSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.SetupCounter); + } + + [GlobalCleanup] + public async Task GlobalCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.CleanupCounter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class GlobalSetupCleanupValueTask + { + [GlobalSetup] + public static async ValueTask GlobalSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.SetupCounter); + } + + [GlobalCleanup] + public async ValueTask GlobalCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counters.CleanupCounter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + + public class GlobalSetupCleanupValueTaskSource + { + private readonly static ValueTaskSource valueTaskSource = new (); + + [GlobalSetup] + public static ValueTask GlobalSetup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + Interlocked.Increment(ref Counters.SetupCounter); + valueTaskSource.SetResult(42); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [GlobalCleanup] + public ValueTask GlobalCleanup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + Interlocked.Increment(ref Counters.CleanupCounter); + valueTaskSource.SetResult(42); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs index a06870ad5e..148b3501af 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs @@ -13,8 +13,10 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; using BenchmarkDotNet.Tests.Loggers; +using BenchmarkDotNet.Tests.Mocks; using BenchmarkDotNet.Toolchains.InProcess.NoEmit; using JetBrains.Annotations; +using Perfolizer.Horology; using Xunit; using Xunit.Abstractions; @@ -32,6 +34,7 @@ public InProcessTest(ITestOutputHelper output) : base(output) private const string StringResult = "42"; private const int UnrollFactor = 16; + private readonly IClock clock = new MockClock(TimeInterval.Millisecond.ToFrequency()); [Fact] public void BenchmarkActionGlobalSetupSupported() => TestInvoke(x => BenchmarkAllCases.GlobalSetup(), UnrollFactor); @@ -43,19 +46,46 @@ public InProcessTest(ITestOutputHelper output) : base(output) public void BenchmarkActionVoidSupported() => TestInvoke(x => x.InvokeOnceVoid(), UnrollFactor); [Fact] - public void BenchmarkActionTaskSupported() => TestInvoke(x => x.InvokeOnceTaskAsync(), UnrollFactor, null); + public void BenchmarkActionTaskSupported() => TestInvoke(x => x.InvokeOnceTaskAsync(), UnrollFactor); [Fact] - public void BenchmarkActionRefTypeSupported() => TestInvoke(x => x.InvokeOnceRefType(), UnrollFactor, StringResult); + public void BenchmarkActionValueTaskSupported() => TestInvoke(x => x.InvokeOnceValueTaskAsync(), UnrollFactor); [Fact] - public void BenchmarkActionValueTypeSupported() => TestInvoke(x => x.InvokeOnceValueType(), UnrollFactor, DecimalResult); + public void BenchmarkActionRefTypeSupported() => TestInvoke(x => x.InvokeOnceRefType(), UnrollFactor); [Fact] - public void BenchmarkActionTaskOfTSupported() => TestInvoke(x => x.InvokeOnceTaskOfTAsync(), UnrollFactor, StringResult); + public void BenchmarkActionValueTypeSupported() => TestInvoke(x => x.InvokeOnceValueType(), UnrollFactor); [Fact] - public void BenchmarkActionValueTaskOfTSupported() => TestInvoke(x => x.InvokeOnceValueTaskOfT(), UnrollFactor, DecimalResult); + public void BenchmarkActionTaskOfTSupported() => TestInvoke(x => x.InvokeOnceTaskOfTAsync(), UnrollFactor); + + [Fact] + public void BenchmarkActionValueTaskOfTSupported() => TestInvoke(x => x.InvokeOnceValueTaskOfT(), UnrollFactor); + + [Fact] + public void BenchmarkActionGlobalSetupTaskSupported() => TestInvokeSetupCleanupTask(x => BenchmarkSetupCleanupTask.GlobalSetup()); + + [Fact] + public void BenchmarkActionGlobalCleanupTaskSupported() => TestInvokeSetupCleanupTask(x => x.GlobalCleanup()); + + [Fact] + public void BenchmarkActionIterationSetupTaskSupported() => TestInvokeSetupCleanupTask(x => BenchmarkSetupCleanupTask.GlobalSetup()); + + [Fact] + public void BenchmarkActionIterationCleanupTaskSupported() => TestInvokeSetupCleanupTask(x => x.GlobalCleanup()); + + [Fact] + public void BenchmarkActionGlobalSetupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => BenchmarkSetupCleanupValueTask.GlobalSetup()); + + [Fact] + public void BenchmarkActionGlobalCleanupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => x.GlobalCleanup()); + + [Fact] + public void BenchmarkActionIterationSetupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => BenchmarkSetupCleanupValueTask.GlobalSetup()); + + [Fact] + public void BenchmarkActionIterationCleanupValueTaskSupported() => TestInvokeSetupCleanupValueTask(x => x.GlobalCleanup()); [Fact] public void BenchmarkDifferentPlatformReturnsValidationError() @@ -82,59 +112,120 @@ private void TestInvoke(Expression> methodCall, int un var descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod, targetMethod, targetMethod); // Run mode - var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor); - TestInvoke(action, unrollFactor, false, null); + var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor, DefaultConfig.Instance); + TestInvoke(action, unrollFactor, false, ref BenchmarkAllCases.Counter); // Idle mode - action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor); - TestInvoke(action, unrollFactor, true, null); + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor, DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); // GlobalSetup/GlobalCleanup - action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, 1, false, null); - action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, 1, false, null); + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases(), DefaultConfig.Instance); + TestInvoke(action, 1, false, ref BenchmarkAllCases.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases(), DefaultConfig.Instance); + TestInvoke(action, 1, false, ref BenchmarkAllCases.Counter); // GlobalSetup/GlobalCleanup (empty) descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod); - action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, unrollFactor, true, null); - action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, unrollFactor, true, null); + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases(), DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases(), DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); // Dummy (just in case something may broke) action = BenchmarkActionFactory.CreateDummy(); - TestInvoke(action, unrollFactor, true, null); - action = BenchmarkActionFactory.CreateDummy(); - TestInvoke(action, unrollFactor, true, null); + TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); } [AssertionMethod] - private void TestInvoke(Expression> methodCall, int unrollFactor, object expectedResult) + private void TestInvoke(Expression> methodCall, int unrollFactor) { var targetMethod = ((MethodCallExpression)methodCall.Body).Method; var descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod); + var methodReturnType = typeof(T); + bool isAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask) + || (methodReturnType.GetTypeInfo().IsGenericType + && (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) + || methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))); + if (isAwaitable) + { + unrollFactor = 1; + } + + // Run mode + var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor, DefaultConfig.Instance); + TestInvoke(action, unrollFactor, false, ref BenchmarkAllCases.Counter); + + // Idle mode + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor, DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkAllCases.Counter); + } + + [AssertionMethod] + private void TestInvokeSetupCleanupTask(Expression> methodCall) + { + var targetMethod = ((MethodCallExpression) methodCall.Body).Method; + var descriptor = new Descriptor(typeof(BenchmarkSetupCleanupTask), targetMethod, targetMethod, targetMethod, targetMethod, targetMethod); + int unrollFactor = 1; + + // Run mode + var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkSetupCleanupTask(), unrollFactor, DefaultConfig.Instance); + TestInvoke(action, unrollFactor, false, ref BenchmarkSetupCleanupTask.Counter); + + // Idle mode + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkSetupCleanupTask(), unrollFactor, DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkSetupCleanupTask.Counter); + + // GlobalSetup/GlobalCleanup + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkSetupCleanupTask(), DefaultConfig.Instance); + TestInvoke(action, 1, false, ref BenchmarkSetupCleanupTask.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkSetupCleanupTask(), DefaultConfig.Instance); + TestInvoke(action, 1, false, ref BenchmarkSetupCleanupTask.Counter); + + // GlobalSetup/GlobalCleanup (empty) + descriptor = new Descriptor(typeof(BenchmarkSetupCleanupTask), targetMethod); + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkSetupCleanupTask(), DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkSetupCleanupTask.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkSetupCleanupTask(), DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkSetupCleanupTask.Counter); + + // Dummy (just in case something may broke) + action = BenchmarkActionFactory.CreateDummy(); + TestInvoke(action, unrollFactor, true, ref BenchmarkSetupCleanupTask.Counter); + } + + [AssertionMethod] + private void TestInvokeSetupCleanupValueTask(Expression> methodCall) + { + var targetMethod = ((MethodCallExpression) methodCall.Body).Method; + var descriptor = new Descriptor(typeof(BenchmarkSetupCleanupValueTask), targetMethod, targetMethod, targetMethod, targetMethod, targetMethod); + int unrollFactor = 1; + // Run mode - var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor); - TestInvoke(action, unrollFactor, false, expectedResult); + var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkSetupCleanupValueTask(), unrollFactor, DefaultConfig.Instance); + TestInvoke(action, unrollFactor, false, ref BenchmarkSetupCleanupValueTask.Counter); // Idle mode + action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkSetupCleanupValueTask(), unrollFactor, DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkSetupCleanupValueTask.Counter); - bool isValueTask = typeof(T).IsConstructedGenericType && typeof(T).GetGenericTypeDefinition() == typeof(ValueTask<>); + // GlobalSetup/GlobalCleanup + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkSetupCleanupValueTask(), DefaultConfig.Instance); + TestInvoke(action, 1, false, ref BenchmarkSetupCleanupValueTask.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkSetupCleanupValueTask(), DefaultConfig.Instance); + TestInvoke(action, 1, false, ref BenchmarkSetupCleanupValueTask.Counter); - object idleExpected; - if (isValueTask) - idleExpected = GetDefault(typeof(T).GetGenericArguments()[0]); - else if (typeof(T).GetTypeInfo().IsValueType) - idleExpected = 0; - else if (expectedResult == null || typeof(T) == typeof(Task)) - idleExpected = null; - else - idleExpected = GetDefault(expectedResult.GetType()); + // GlobalSetup/GlobalCleanup (empty) + descriptor = new Descriptor(typeof(BenchmarkSetupCleanupValueTask), targetMethod); + action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkSetupCleanupValueTask(), DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkSetupCleanupValueTask.Counter); + action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkSetupCleanupValueTask(), DefaultConfig.Instance); + TestInvoke(action, unrollFactor, true, ref BenchmarkSetupCleanupValueTask.Counter); - action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor); - TestInvoke(action, unrollFactor, true, idleExpected); + // Dummy (just in case something may broke) + action = BenchmarkActionFactory.CreateDummy(); + TestInvoke(action, unrollFactor, true, ref BenchmarkSetupCleanupValueTask.Counter); } private static object GetDefault(Type type) @@ -147,36 +238,34 @@ private static object GetDefault(Type type) } [AssertionMethod] - private void TestInvoke(BenchmarkAction benchmarkAction, int unrollFactor, bool isIdle, object expectedResult) + private void TestInvoke(BenchmarkAction benchmarkAction, int unrollFactor, bool isIdle, ref int counter) { try { - BenchmarkAllCases.Counter = 0; + counter = 0; if (isIdle) { - benchmarkAction.InvokeSingle(); - Assert.Equal(0, BenchmarkAllCases.Counter); - benchmarkAction.InvokeMultiple(0); - Assert.Equal(0, BenchmarkAllCases.Counter); - benchmarkAction.InvokeMultiple(11); - Assert.Equal(0, BenchmarkAllCases.Counter); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeSingle()); + Assert.Equal(0, counter); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeUnroll(0, clock)); + Assert.Equal(0, counter); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeUnroll(11, clock)); + Assert.Equal(0, counter); } else { - benchmarkAction.InvokeSingle(); - Assert.Equal(1, BenchmarkAllCases.Counter); - benchmarkAction.InvokeMultiple(0); - Assert.Equal(1, BenchmarkAllCases.Counter); - benchmarkAction.InvokeMultiple(11); - Assert.Equal(BenchmarkAllCases.Counter, 1 + unrollFactor * 11); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeSingle()); + Assert.Equal(1, counter); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeUnroll(0, clock)); + Assert.Equal(1, counter); + Helpers.AwaitHelper.GetResult(benchmarkAction.InvokeUnroll(11, clock)); + Assert.Equal(1 + unrollFactor * 11, counter); } - - Assert.Equal(benchmarkAction.LastRunResult, expectedResult); } finally { - BenchmarkAllCases.Counter = 0; + counter = 0; } } @@ -244,6 +333,13 @@ public async Task InvokeOnceTaskAsync() Interlocked.Increment(ref Counter); } + [Benchmark] + public async ValueTask InvokeOnceValueTaskAsync() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + [Benchmark] public string InvokeOnceRefType() { @@ -273,5 +369,85 @@ public ValueTask InvokeOnceValueTaskOfT() return new ValueTask(DecimalResult); } } + + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + public class BenchmarkSetupCleanupTask + { + public static int Counter; + + [GlobalSetup] + public static async Task GlobalSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [GlobalCleanup] + public async Task GlobalCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [IterationSetup] + public static async Task IterationSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [IterationCleanup] + public async Task IterationCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counter); + } + } + + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + public class BenchmarkSetupCleanupValueTask + { + public static int Counter; + + [GlobalSetup] + public static async ValueTask GlobalSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [GlobalCleanup] + public async ValueTask GlobalCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [IterationSetup] + public static async ValueTask IterationSetup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [IterationCleanup] + public async ValueTask IterationCleanup() + { + await Task.Yield(); + Interlocked.Increment(ref Counter); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counter); + } + } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs b/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs index 19a0e0583f..e3845613d1 100644 --- a/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs +++ b/tests/BenchmarkDotNet.Tests/Engine/EngineFactoryTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Jobs; @@ -21,32 +22,72 @@ public class EngineFactoryTests private IResolver DefaultResolver => BenchmarkRunnerClean.DefaultResolver; - private void GlobalSetup() => timesGlobalSetupCalled++; - private void IterationSetup() => timesIterationSetupCalled++; - private void IterationCleanup() => timesIterationCleanupCalled++; - private void GlobalCleanup() => timesGlobalCleanupCalled++; + private ValueTask GlobalSetup() + { + timesGlobalSetupCalled++; + return new ValueTask(); + } + private ValueTask IterationSetup() + { + timesIterationSetupCalled++; + return new ValueTask(); + } + private ValueTask IterationCleanup() + { + timesIterationCleanupCalled++; + return new ValueTask(); + } + private ValueTask GlobalCleanup() + { + timesGlobalCleanupCalled++; + return new ValueTask(); + } - private void Throwing(long _) => throw new InvalidOperationException("must NOT be called"); + private ValueTask Throwing(long _, IClock __) => throw new InvalidOperationException("must NOT be called"); - private void VeryTimeConsumingSingle(long _) + private ValueTask VeryTimeConsumingSingle(long _, IClock clock) { + var startedClock = clock.Start(); timesBenchmarkCalled++; Thread.Sleep(IterationTime); + return new ValueTask(startedClock.GetElapsed()); } - private void TimeConsumingOnlyForTheFirstCall(long _) + private ValueTask TimeConsumingOnlyForTheFirstCall(long _, IClock clock) { + var startedClock = clock.Start(); if (timesBenchmarkCalled++ == 0) { Thread.Sleep(IterationTime); } + return new ValueTask(startedClock.GetElapsed()); } - private void InstantNoUnroll(long invocationCount) => timesBenchmarkCalled += (int) invocationCount; - private void InstantUnroll(long _) => timesBenchmarkCalled += 16; + private ValueTask InstantNoUnroll(long invocationCount, IClock clock) + { + var startedClock = clock.Start(); + timesBenchmarkCalled += (int) invocationCount; + return new ValueTask(startedClock.GetElapsed()); + } + private ValueTask InstantUnroll(long _, IClock clock) + { + var startedClock = clock.Start(); + timesBenchmarkCalled += 16; + return new ValueTask(startedClock.GetElapsed()); + } - private void OverheadNoUnroll(long invocationCount) => timesOverheadCalled += (int) invocationCount; - private void OverheadUnroll(long _) => timesOverheadCalled += 16; + private ValueTask OverheadNoUnroll(long invocationCount, IClock clock) + { + var startedClock = clock.Start(); + timesOverheadCalled += (int) invocationCount; + return new ValueTask(startedClock.GetElapsed()); + } + private ValueTask OverheadUnroll(long _, IClock clock) + { + var startedClock = clock.Start(); + timesOverheadCalled += 16; + return new ValueTask(startedClock.GetElapsed()); + } private static readonly Dictionary JobsWhichDontRequireJitting = new Dictionary { @@ -197,22 +238,26 @@ public void MediumTimeConsumingBenchmarksShouldStartPilotFrom2AndIncrementItWith var mediumTime = TimeSpan.FromMilliseconds(IterationTime.TotalMilliseconds / times); - void MediumNoUnroll(long invocationCount) + ValueTask MediumNoUnroll(long invocationCount, IClock clock) { + var startedClock = clock.Start(); for (int i = 0; i < invocationCount; i++) { timesBenchmarkCalled++; Thread.Sleep(mediumTime); } + return new ValueTask(startedClock.GetElapsed()); } - void MediumUnroll(long _) + ValueTask MediumUnroll(long _, IClock clock) { + var startedClock = clock.Start(); timesBenchmarkCalled += unrollFactor; for (int i = 0; i < unrollFactor; i++) // the real unroll factor obviously does not use loop ;) Thread.Sleep(mediumTime); + return new ValueTask(startedClock.GetElapsed()); } var engineParameters = CreateEngineParameters(mainNoUnroll: MediumNoUnroll, mainUnroll: MediumUnroll, job: Job.Default); @@ -239,7 +284,7 @@ void MediumUnroll(long _) Assert.Equal(1, timesGlobalCleanupCalled); } - private EngineParameters CreateEngineParameters(Action mainNoUnroll, Action mainUnroll, Job job) + private EngineParameters CreateEngineParameters(Func> mainNoUnroll, Func> mainUnroll, Job job) => new EngineParameters { Dummy1Action = () => { }, diff --git a/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs b/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs index 6b1fad64a6..5b784a2691 100644 --- a/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs +++ b/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Jobs; @@ -32,16 +33,16 @@ public MockEngine(ITestOutputHelper output, Job job, Func GlobalSetupAction { get; set; } [UsedImplicitly] - public Action GlobalCleanupAction { get; set; } + public Func GlobalCleanupAction { get; set; } [UsedImplicitly] public bool IsDiagnoserAttached { get; set; } - public Action WorkloadAction { get; } = _ => { }; - public Action OverheadAction { get; } = _ => { }; + public Func> WorkloadAction { get; } = (invokeCount, clock) => default; + public Func> OverheadAction { get; } = (invokeCount, clock) => default; [UsedImplicitly] public IEngineFactory Factory => null; diff --git a/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs index 343bdc53dc..fece3b3939 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs @@ -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 @@ public async ValueTask GlobalCleanup() [Benchmark] public void NonThrowing() { } } + + private class ValueTaskSource : IValueTaskSource, IValueTaskSource + { + private ManualResetValueTaskSourceCore _core; + + T IValueTaskSource.GetResult(short token) => _core.GetResult(token); + void IValueTaskSource.GetResult(short token) => _core.GetResult(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _core.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags); + void IValueTaskSource.OnCompleted(Action 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 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 valueTaskSource = new (); + + public static bool WasCalled; + + [GlobalSetup] + public ValueTask GlobalSetup() + { + valueTaskSource.Reset(); + Task.Delay(1).ContinueWith(_ => + { + WasCalled = true; + valueTaskSource.SetResult(1); + }); + return new ValueTask(valueTaskSource, valueTaskSource.Token); + } + + [Benchmark] + public void NonThrowing() { } + } } } \ No newline at end of file