diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..6b553286b8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS1998: Async method lacks 'await' operators and will run synchronously +dotnet_diagnostic.CS1998.severity = suggestion diff --git a/BenchmarkDotNet.slnx b/BenchmarkDotNet.slnx index 1d2b3755ad..332c23c455 100644 --- a/BenchmarkDotNet.slnx +++ b/BenchmarkDotNet.slnx @@ -8,6 +8,9 @@ + + + diff --git a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs index a6c371cedb..c58761d2d9 100644 --- a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs +++ b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs @@ -8,7 +8,6 @@ using Cake.Common.Tools.DotNet.Workload.Install; using Cake.Core; using Cake.Core.IO; -using System; using System.IO; using System.Linq; @@ -51,7 +50,6 @@ public void PackWeaver() { MSBuildSettings = context.MsBuildSettingsRestore, }; - MaybeAppendArgument(restoreSettings); context.DotNetRestore(weaverPath.GetDirectory().FullPath, restoreSettings); context.Information("BuildSystemProvider: " + context.BuildSystem().Provider); @@ -63,7 +61,6 @@ public void PackWeaver() Configuration = context.BuildConfiguration, Verbosity = context.BuildVerbosity }; - MaybeAppendArgument(buildSettings); context.DotNetBuild(weaverPath.FullPath, buildSettings); var packSettings = new DotNetPackSettings @@ -72,7 +69,6 @@ public void PackWeaver() MSBuildSettings = context.MsBuildSettingsPack, Configuration = context.BuildConfiguration }; - MaybeAppendArgument(packSettings); context.DotNetPack(weaverPath.FullPath, packSettings); } diff --git a/build/common.props b/build/common.props index 6fe9ca8e92..928642e9bc 100644 --- a/build/common.props +++ b/build/common.props @@ -60,7 +60,7 @@ - -1 + -2 diff --git a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj index 9df0af8dc9..2411c721a6 100644 --- a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj +++ b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj @@ -26,6 +26,7 @@ + diff --git a/src/BenchmarkDotNet.Annotations/Attributes/AsyncCallerTypeAttribute.cs b/src/BenchmarkDotNet.Annotations/Attributes/AsyncCallerTypeAttribute.cs new file mode 100644 index 0000000000..d59ec46d2a --- /dev/null +++ b/src/BenchmarkDotNet.Annotations/Attributes/AsyncCallerTypeAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace BenchmarkDotNet.Attributes; + +/// +/// When applied to an async benchmark method, overrides the return type of the async method that calls the benchmark method. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class AsyncCallerTypeAttribute(Type asyncCallerType) : Attribute +{ + /// + /// The return type of the async method that calls the benchmark method. + /// + public Type AsyncCallerType { get; private set; } = asyncCallerType; +} diff --git a/src/BenchmarkDotNet.Annotations/Attributes/CompilerServices/AggressivelyOptimizeMethods.cs b/src/BenchmarkDotNet.Annotations/Attributes/CompilerServices/AggressivelyOptimizeMethods.cs new file mode 100644 index 0000000000..17b0ec3c6a --- /dev/null +++ b/src/BenchmarkDotNet.Annotations/Attributes/CompilerServices/AggressivelyOptimizeMethods.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BenchmarkDotNet.Attributes.CompilerServices; + +// MethodImplOptions.AggressiveOptimization is applied to all methods to force them to go straight to tier1 JIT, +// eliminating tiered JIT as a potential variable in measurements. +// This is necessary because C# does not support any way to apply attributes to compiler-generated state machine methods. +// This is applied both to the core Engine and auto-generated classes. +#pragma warning disable CS1574 +/// +/// Instructs the BenchmarkDotNet assembly weaver to apply to all declared +/// methods in the annotated type and nested types that are not already annotated with . +/// +#pragma warning restore CS1574 +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public sealed class AggressivelyOptimizeMethodsAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/ConcurrencyVisualizerProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/ConcurrencyVisualizerProfiler.cs index acb6280162..f414df0151 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/ConcurrencyVisualizerProfiler.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/ConcurrencyVisualizerProfiler.cs @@ -75,7 +75,7 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) public IEnumerable ProcessResults(DiagnoserResults results) => etwProfiler.ProcessResults(results); - public IEnumerable Validate(ValidationParameters validationParameters) => etwProfiler.Validate(validationParameters); + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => etwProfiler.ValidateAsync(validationParameters); private static EtwProfilerConfig CreateDefaultConfig() { diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs index 57f57d9e1a..8e0f905d3f 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs @@ -57,8 +57,8 @@ public EtwProfiler(EtwProfilerConfig config) public RunMode GetRunMode(BenchmarkCase benchmarkCase) => runMode; - public IEnumerable Validate(ValidationParameters validationParameters) - => HardwareCounters.Validate(validationParameters, mandatory: false); + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) + => HardwareCounters.Validate(validationParameters, mandatory: false).ToAsyncEnumerable(); public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/JitDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/JitDiagnoser.cs index 0d06faf75b..eb72a6e723 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/JitDiagnoser.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/JitDiagnoser.cs @@ -32,7 +32,7 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) public virtual IEnumerable ProcessResults(DiagnoserResults results) => Array.Empty(); - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { if (!OsDetector.IsWindows()) { diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/NativeMemoryProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/NativeMemoryProfiler.cs index ece842c36f..8ee6e04077 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/NativeMemoryProfiler.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/NativeMemoryProfiler.cs @@ -56,7 +56,7 @@ public IEnumerable ProcessResults(DiagnoserResults results) return new NativeMemoryLogParser(traceFilePath, results.BenchmarkCase, logger, results.BuildResult.ArtifactsPaths.ProgramName).Parse(); } - public IEnumerable Validate(ValidationParameters validationParameters) => etwProfiler.Validate(validationParameters); + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => etwProfiler.ValidateAsync(validationParameters); private static EtwProfilerConfig CreateDefaultConfig() { diff --git a/src/BenchmarkDotNet.Weaver/buildTransitive/netstandard2.0/BenchmarkDotNet.Weaver.targets b/src/BenchmarkDotNet.Weaver/buildTransitive/netstandard2.0/BenchmarkDotNet.Weaver.targets index c10ef0575f..7fc664d8a7 100644 --- a/src/BenchmarkDotNet.Weaver/buildTransitive/netstandard2.0/BenchmarkDotNet.Weaver.targets +++ b/src/BenchmarkDotNet.Weaver/buildTransitive/netstandard2.0/BenchmarkDotNet.Weaver.targets @@ -17,7 +17,7 @@ Inputs="$(BenchmarkDotNetWeaveAssemblyPath)" Outputs="$(BenchmarkDotNetWeaveAssembliesStampFile)"> - + diff --git a/src/BenchmarkDotNet.Weaver/packages/BenchmarkDotNet.Weaver.0.16.0-develop-1.nupkg b/src/BenchmarkDotNet.Weaver/packages/BenchmarkDotNet.Weaver.0.16.0-develop-1.nupkg deleted file mode 100644 index 8e3f508ff0..0000000000 Binary files a/src/BenchmarkDotNet.Weaver/packages/BenchmarkDotNet.Weaver.0.16.0-develop-1.nupkg and /dev/null differ diff --git a/src/BenchmarkDotNet.Weaver/packages/BenchmarkDotNet.Weaver.0.16.0-develop-2.nupkg b/src/BenchmarkDotNet.Weaver/packages/BenchmarkDotNet.Weaver.0.16.0-develop-2.nupkg new file mode 100644 index 0000000000..97ef4cdaf8 Binary files /dev/null and b/src/BenchmarkDotNet.Weaver/packages/BenchmarkDotNet.Weaver.0.16.0-develop-2.nupkg differ diff --git a/src/BenchmarkDotNet.Weaver/src/WeaveAssemblyTask.cs b/src/BenchmarkDotNet.Weaver/src/WeaveAssemblyTask.cs index 9fdff81cac..f19c70d3f2 100644 --- a/src/BenchmarkDotNet.Weaver/src/WeaveAssemblyTask.cs +++ b/src/BenchmarkDotNet.Weaver/src/WeaveAssemblyTask.cs @@ -24,45 +24,76 @@ public sealed class WeaveAssemblyTask : Task [Required] public string TargetAssembly { get; set; } + /// + /// Whether to treat warnings as errors. + /// + public bool TreatWarningsAsErrors { get; set; } + /// /// Runs the weave assembly task. /// /// if successful; otherwise. public override bool Execute() - { + { if (!File.Exists(TargetAssembly)) { Log.LogError($"Assembly not found: {TargetAssembly}"); return false; } - bool benchmarkMethodsImplAdjusted = false; try { var module = ModuleDefinition.FromFile(TargetAssembly); + bool anyAdjustments = false; foreach (var type in module.GetAllTypes()) { - // We can skip non-public types as they are not valid for benchmarks. - if (type.IsNotPublic) + if (type.CustomAttributes.Any(attr => attr.Constructor.DeclaringType.FullName == "BenchmarkDotNet.Attributes.CompilerServices.AggressivelyOptimizeMethodsAttribute")) { - continue; + ApplyAggressiveOptimizationToMethods(type); + + void ApplyAggressiveOptimizationToMethods(TypeDefinition type) + { + // Apply AggressiveOptimization to all methods in the type and nested types that + // aren't annotated with NoOptimization (this includes compiler-generated state machines). + foreach (var method in type.Methods) + { + if ((method.ImplAttributes & MethodImplAttributes.NoOptimization) == 0) + { + var oldImpl = method.ImplAttributes; + method.ImplAttributes |= MethodImplAttributes.AggressiveOptimization; + anyAdjustments |= (oldImpl & MethodImplAttributes.AggressiveOptimization) == 0; + } + } + + // Recurse into nested types + foreach (var nested in type.NestedTypes) + { + ApplyAggressiveOptimizationToMethods(nested); + } + } } - foreach (var method in type.Methods) + // We can skip non-public types as they are not valid for benchmarks. + // !type.IsNotPublic handles nested types, while type.IsPublic does not. + if (!type.IsNotPublic) { - if (method.CustomAttributes.Any(IsBenchmarkAttribute)) + foreach (var method in type.Methods) { - var oldImpl = method.ImplAttributes; - // Remove AggressiveInlining and add NoInlining. - method.ImplAttributes = (oldImpl & ~MethodImplAttributes.AggressiveInlining) | MethodImplAttributes.NoInlining; - benchmarkMethodsImplAdjusted |= (oldImpl & MethodImplAttributes.NoInlining) == 0; + if (method.CustomAttributes.Any(IsBenchmarkAttribute)) + { + var oldImpl = method.ImplAttributes; + // Remove AggressiveInlining and add NoInlining. + method.ImplAttributes = (oldImpl & ~MethodImplAttributes.AggressiveInlining) | MethodImplAttributes.NoInlining; + benchmarkMethodsImplAdjusted |= (oldImpl & MethodImplAttributes.NoInlining) == 0; + anyAdjustments |= benchmarkMethodsImplAdjusted; + } } } } - if (benchmarkMethodsImplAdjusted) + if (anyAdjustments) { // Write to a memory stream before overwriting the original file in case an exception occurs during the write (like unsupported platform). // https://github.com/Washi1337/AsmResolver/issues/640 @@ -90,9 +121,17 @@ public override bool Execute() } catch (Exception e) { - Log.LogWarning($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}. Error:{Environment.NewLine}{e}"); + if (TreatWarningsAsErrors) + { + Log.LogError($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}."); + Log.LogErrorFromException(e, true, true, null); + } + else + { + Log.LogWarning($"Assembly weaving failed. Benchmark methods found requiring NoInlining: {benchmarkMethodsImplAdjusted}. Error:{Environment.NewLine}{e}"); + } } - return true; + return !Log.HasLoggedErrors; } private static bool IsBenchmarkAttribute(CustomAttribute attribute) diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index c4d8b56639..e8d6d684f6 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -1,10 +1,11 @@  + BenchmarkDotNet netstandard2.0;net6.0;net8.0;net9.0;net10.0 true - $(NoWarn);1701;1702;1705;1591;3005;NU1702;CS3001;CS3003 + $(NoWarn);1701;1702;1705;1591;3005;NU1510;NU1702;CS3001;CS3003 BenchmarkDotNet BenchmarkDotNet BenchmarkDotNet @@ -28,6 +29,7 @@ + @@ -51,5 +53,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/BenchmarkDotNet/Code/CodeGenEntryPointType.cs b/src/BenchmarkDotNet/Code/CodeGenEntryPointType.cs new file mode 100644 index 0000000000..a049cc008f --- /dev/null +++ b/src/BenchmarkDotNet/Code/CodeGenEntryPointType.cs @@ -0,0 +1,10 @@ +namespace BenchmarkDotNet.Code; + +/// +/// Specifies how to generate the entry-point for the benchmark process. +/// +public enum CodeGenEntryPointType +{ + Synchronous, + Asynchronous +} diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index fa0f4c4567..9d8b945df3 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -5,7 +5,6 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; -using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Disassemblers; @@ -21,7 +20,7 @@ namespace BenchmarkDotNet.Code { internal static class CodeGenerator { - internal static string Generate(BuildPartition buildPartition, CodeGenBenchmarkRunCallType benchmarkRunCallType) + internal static string Generate(BuildPartition buildPartition, CodeGenEntryPointType entryPointType, CodeGenBenchmarkRunCallType benchmarkRunCallType) { (bool useShadowCopy, string shadowCopyFolderPath) = GetShadowCopySettings(); @@ -31,29 +30,20 @@ internal static string Generate(BuildPartition buildPartition, CodeGenBenchmarkR { var benchmark = buildInfo.BenchmarkCase; - var provider = GetDeclarationsProvider(benchmark.Descriptor); + var declarationsProvider = GetDeclarationsProvider(benchmark); + var extraFields = declarationsProvider.GetExtraFields(); - string passArguments = GetPassArguments(benchmark); - - string benchmarkTypeCode = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkType.txt")) + string benchmarkTypeCode = declarationsProvider + .ReplaceTemplate(new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkType.txt"))) .Replace("$ID$", buildInfo.Id.ToString()) - .Replace("$OperationsPerInvoke$", provider.OperationsPerInvoke) - .Replace("$WorkloadTypeName$", provider.WorkloadTypeName) - .Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName) - .Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName) - .Replace("$IterationSetupMethodName$", provider.IterationSetupMethodName) - .Replace("$IterationCleanupMethodName$", provider.IterationCleanupMethodName) .Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark)) .Replace("$ParamsContent$", GetParamsContent(benchmark)) .Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark)) - .Replace("$DeclareArgumentFields$", GetDeclareArgumentFields(benchmark)) + .Replace("$DeclareFieldsContainer$", GetDeclareFieldsContainer(benchmark, buildInfo.Id, extraFields)) .Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark)) - .Replace("$LoadArguments$", GetLoadArguments(benchmark)) - .Replace("$PassArguments$", passArguments) .Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark)) .Replace("$RunExtraIteration$", buildInfo.Config.HasExtraIterationDiagnoser(benchmark) ? "true" : "false") .Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName) - .Replace("$WorkloadMethodCall$", provider.GetWorkloadMethodCall(passArguments)) .Replace("$InProcessDiagnoserRouters$", GetInProcessDiagnoserRouters(buildInfo)) .ToString(); @@ -63,11 +53,9 @@ internal static string Generate(BuildPartition buildPartition, CodeGenBenchmarkR } string benchmarkProgramContent = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkProgram.txt")) - .Replace("$ShadowCopyDefines$", useShadowCopy ? "#define SHADOWCOPY" : null).Replace("$ShadowCopyFolderPath$", shadowCopyFolderPath) - .Replace("$ExtraDefines$", buildPartition.IsNetFramework ? "#define NETFRAMEWORK" : string.Empty) - .Replace("$DerivedTypes$", string.Join(Environment.NewLine, benchmarksCode)) - .Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition.RepresentativeBenchmarkCase.Descriptor)) + .Replace("$EntryPoint$", GetEntryPoint(buildPartition, entryPointType, useShadowCopy, shadowCopyFolderPath)) .Replace("$BenchmarkRunCall$", GetBenchmarkRunCall(buildPartition, benchmarkRunCallType)) + .Replace("$DerivedTypes$", string.Join(Environment.NewLine, benchmarksCode)) .ToString(); return benchmarkProgramContent; @@ -123,19 +111,13 @@ private static string GetJobsSetDefinition(BenchmarkCase benchmarkCase) Replace("; ", ";\n "); } - private static DeclarationsProvider GetDeclarationsProvider(Descriptor descriptor) + private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchmark) { - var method = descriptor.WorkloadMethod; + var method = benchmark.Descriptor.WorkloadMethod; - if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(ValueTask)) - { - return new AsyncDeclarationsProvider(descriptor); - } - if (method.ReturnType.GetTypeInfo().IsGenericType - && (method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) - || method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))) + if (method.ReturnType.IsAwaitable()) { - return new AsyncDeclarationsProvider(descriptor); + return new AsyncDeclarationsProvider(benchmark); } if (method.ReturnType == typeof(void) && method.HasAttribute()) @@ -143,7 +125,7 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto throw new NotSupportedException("async void is not supported by design"); } - return new SyncDeclarationsProvider(descriptor); + return new SyncDeclarationsProvider(benchmark); } // internal for tests @@ -159,34 +141,56 @@ private static string GetArgumentsDefinition(BenchmarkCase benchmarkCase) => string.Join( ", ", benchmarkCase.Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}")); + .Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}")); - private static string GetDeclareArgumentFields(BenchmarkCase benchmarkCase) - => string.Join( - Environment.NewLine, - benchmarkCase.Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"private {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name!)).GetCorrectCSharpTypeName()} __argField{index};")); + private static string GetDeclareFieldsContainer(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, string[] extraFields) + { + var fields = benchmarkCase.Descriptor.WorkloadMethod.GetParameters() + .Select((parameter, index) => $"public {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name!)).GetCorrectCSharpTypeName()} argField{index};") + .Concat(extraFields) + .ToArray(); - private static string GetInitializeArgumentFields(BenchmarkCase benchmarkCase) - => string.Join( - Environment.NewLine, - benchmarkCase.Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"this.__argField{index} = {benchmarkCase.Parameters.GetArgument(parameter.Name!).ToSourceCode()};")); // we init the fields in ctor to provoke all possible allocations and overhead of other type + // Prevent CS0169 + if (fields.Length == 0) + { + return string.Empty; + } - private static string GetLoadArguments(BenchmarkCase benchmarkCase) - => string.Join( - Environment.NewLine, - benchmarkCase.Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"{(parameter.ParameterType.IsByRef ? "ref" : string.Empty)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {(parameter.ParameterType.IsByRef ? "ref" : string.Empty)} this.__argField{index};")); + // Wrapper struct is necessary because of error CS4004: Cannot await in an unsafe context + var sb = new StringBuilder(); + sb.AppendLine(""" + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Auto)] + private unsafe struct FieldsContainer + { + """); + foreach (var field in fields) + { + sb.AppendLine($" {field}"); + } + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine($" private global::BenchmarkDotNet.Autogenerated.Runnable_{benchmarkId.Value}.FieldsContainer __fieldsContainer;"); + return sb.ToString(); + } + + /* + + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Auto)] + private unsafe struct FieldsContainer + { + $DeclareArgumentFields$ + $ExtraFields$ + } + + private global::BenchmarkDotNet.Autogenerated.Runnable_$ID$.FieldsContainer __fieldsContainer; + + */ - private static string GetPassArguments(BenchmarkCase benchmarkCase) + private static string GetInitializeArgumentFields(BenchmarkCase benchmarkCase) => string.Join( - ", ", + Environment.NewLine, benchmarkCase.Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"{GetParameterModifier(parameter)} arg{index}")); - - private static string GetExtraAttributes(Descriptor descriptor) - => descriptor.WorkloadMethod.GetCustomAttributes(false).OfType().Any() ? "[System.STAThreadAttribute]" : string.Empty; + .Select((parameter, index) => $"this.__fieldsContainer.argField{index} = {benchmarkCase.Parameters.GetArgument(parameter.Name!).ToSourceCode()};")); // we init the fields in ctor to provoke all possible allocations and overhead of other type private static string GetEngineFactoryTypeName(BenchmarkCase benchmarkCase) { @@ -226,7 +230,7 @@ private static string GetInProcessDiagnoserRouters(BenchmarkBuildInfo buildInfo) } } - private static string GetParameterModifier(ParameterInfo parameterInfo) + internal static string GetParameterModifier(ParameterInfo parameterInfo) { if (!parameterInfo.ParameterType.IsByRef) return string.Empty; @@ -242,13 +246,126 @@ private static string GetParameterModifier(ParameterInfo parameterInfo) return "ref"; } + private static string GetEntryPoint(BuildPartition buildPartition, CodeGenEntryPointType entryPointType, bool useShadowCopy, string shadowCopyFolderPath) + { + if (entryPointType == CodeGenEntryPointType.Asynchronous) + { + // Only wasm uses async entry-point, we don't need to worry about .Net Framework assembly resolve helper. + // Async entry-points also cannot participate in STAThread, so we ignore that as well. + return """ + public static async global::System.Threading.Tasks.Task Main(global::System.String[] args) + { + return await MainCore(args); + } + """; + } + + string mainImpl = """ + global::BenchmarkDotNet.Engines.BenchmarkSynchronizationContext benchmarkSynchronizationContext = global::BenchmarkDotNet.Engines.BenchmarkSynchronizationContext.CreateAndSetCurrent(); + try + { + global::System.Threading.Tasks.ValueTask task = MainCore(args); + return benchmarkSynchronizationContext.ExecuteUntilComplete(task); + } + finally + { + benchmarkSynchronizationContext.Dispose(); + } + """; + + if (!buildPartition.IsNetFramework) + { + return $$""" + {{GetSTAThreadAttribute()}} + public static global::System.Int32 Main(global::System.String[] args) + { + {{mainImpl}} + } + """; + } + + return $$""" + {{GetAssemblyResolveHelperClass()}} + + {{GetSTAThreadAttribute()}} + public static global::System.Int32 Main(global::System.String[] args) + { + // this method MUST NOT have any dependencies to BenchmarkDotNet and any other external dlls! + // otherwise if LINQPad's shadow copy is enabled, we will not register for AssemblyLoading event + // before .NET Framework tries to load it for this method + using(new BenchmarkDotNet.Autogenerated.UniqueProgramName.DirtyAssemblyResolveHelper()) + return AfterAssemblyLoadingAttached(args); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static global::System.Int32 AfterAssemblyLoadingAttached(global::System.String[] args) + { + {{mainImpl}} + } + """; + + string GetSTAThreadAttribute() + => buildPartition.RepresentativeBenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes(false).OfType().Any() + ? "[global::System.STAThread]" + : string.Empty; + + string GetAssemblyResolveHelperClass() + { + string impl = useShadowCopy + // used for LINQPad + ? $$""" + global::System.String guessedPath = global::System.IO.Path.Combine(@"{{shadowCopyFolderPath}}", $"{new global::System.Reflection.AssemblyName(args.Name).Name}.dll"); + return global::System.IO.File.Exists(guessedPath) ? global::System.Reflection.Assembly.LoadFrom(guessedPath) : null; + """ + : """ + global::System.Reflection.AssemblyName fullName = new global::System.Reflection.AssemblyName(args.Name); + global::System.String simpleName = fullName.Name; + + global::System.String guessedPath = global::System.IO.Path.Combine(global::System.AppDomain.CurrentDomain.BaseDirectory, $"{simpleName}.dll"); + + if (!global::System.IO.File.Exists(guessedPath)) + { + global::System.Console.WriteLine($"// Wrong assembly binding redirects for {args.Name}."); + return null; // we can't help, and we also don't call Assembly.Load which if fails comes back here, creates endless loop and causes StackOverflow + } + + // the file is right there, but has most probably different version and there is no assembly binding redirect or there is a wrong one... + // so we just load it and ignore the version mismatch + + // we warn the user about that, in case some Super User want to be aware of that + global::System.Console.WriteLine($"// Wrong assembly binding redirects for {simpleName}, loading it from disk anyway."); + + return global::System.Reflection.Assembly.LoadFrom(guessedPath); + """; + + return $$""" + private sealed class DirtyAssemblyResolveHelper : global::System.IDisposable + { + internal DirtyAssemblyResolveHelper() => global::System.AppDomain.CurrentDomain.AssemblyResolve += HelpTheFrameworkToResolveTheAssembly; + + public void Dispose() => global::System.AppDomain.CurrentDomain.AssemblyResolve -= HelpTheFrameworkToResolveTheAssembly; + + /// + /// according to https://msdn.microsoft.com/en-us/library/ff527268(v=vs.110).aspx + /// "the handler is invoked whenever the runtime fails to bind to an assembly by name." + /// + /// not null when we find it manually, null when we can't help + private global::System.Reflection.Assembly HelpTheFrameworkToResolveTheAssembly(global::System.Object sender, global::System.ResolveEventArgs args) + { + {{impl}} + } + } + """; + } + } + private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGenBenchmarkRunCallType runCallType) { if (runCallType == CodeGenBenchmarkRunCallType.Reflection) { // Use reflection to call benchmark's Run method indirectly. return """ - typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly + await (global::System.Threading.Tasks.ValueTask) typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly .GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}") .GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static) .Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode }); @@ -260,7 +377,9 @@ private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGen @switch.AppendLine("switch (id) {"); foreach (var buildInfo in buildPartition.Benchmarks) - @switch.AppendLine($"case {buildInfo.Id.Value}: BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;"); + { + @switch.AppendLine($"case {buildInfo.Id.Value}: await BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;"); + } @switch.AppendLine("default: throw new System.NotSupportedException(\"invalid benchmark id\");"); @switch.AppendLine("}"); @@ -276,28 +395,21 @@ private static Type GetFieldType(Type argumentType, ParameterInstance argument) return argumentType; } + } - private class SmartStringBuilder - { - private readonly string originalText; - private readonly StringBuilder builder; - - public SmartStringBuilder(string text) - { - originalText = text; - builder = new StringBuilder(text); - } - - public SmartStringBuilder Replace(string oldValue, string? newValue) - { - if (originalText.Contains(oldValue)) - builder.Replace(oldValue, newValue); - else - builder.Append($"\n// '{oldValue}' not found"); - return this; - } + internal class SmartStringBuilder(string text) + { + private readonly StringBuilder builder = new(text); - public override string ToString() => builder.ToString(); + public SmartStringBuilder Replace(string oldValue, string? newValue) + { + if (text.Contains(oldValue)) + builder.Replace(oldValue, newValue); + else + builder.Append($"\n// '{oldValue}' not found"); + return this; } + + public override string ToString() => builder.ToString(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index 7bb3dedd1c..e6cd279c21 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -1,65 +1,326 @@ -using System.Reflection; +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; +using Perfolizer.Horology; namespace BenchmarkDotNet.Code { - internal abstract class DeclarationsProvider + internal abstract class DeclarationsProvider(BenchmarkCase benchmark) { - // "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 static readonly string CoreReturnType = typeof(ValueTask).GetCorrectCSharpTypeName(); + protected static readonly string CoreParameters = $"long invokeCount, {typeof(IClock).GetCorrectCSharpTypeName()} clock"; + protected static readonly string StartClockSyncCode = $"{typeof(StartedClock).GetCorrectCSharpTypeName()} startedClock = {typeof(ClockExtensions).GetCorrectCSharpTypeName()}.Start(clock);"; + protected static readonly string ReturnSyncCode = $"return new {CoreReturnType}(startedClock.GetElapsed());"; + private static readonly string ReturnCompletedValueTask = $"return new {typeof(ValueTask).GetCorrectCSharpTypeName()}();"; - protected readonly Descriptor Descriptor; + protected BenchmarkCase Benchmark { get; } = benchmark; + protected Descriptor Descriptor => Benchmark.Descriptor; - internal DeclarationsProvider(Descriptor descriptor) => Descriptor = descriptor; + public abstract string[] GetExtraFields(); - public string OperationsPerInvoke => Descriptor.OperationsPerInvoke.ToString(); + public SmartStringBuilder ReplaceTemplate(SmartStringBuilder smartStringBuilder) + { + Replace(smartStringBuilder, Descriptor.GlobalSetupMethod, "$GlobalSetupModifiers$", "$GlobalSetupImpl$", false); + Replace(smartStringBuilder, Descriptor.GlobalCleanupMethod, "$GlobalCleanupModifiers$", "$GlobalCleanupImpl$", true); + Replace(smartStringBuilder, Descriptor.IterationSetupMethod, "$IterationSetupModifiers$", "$IterationSetupImpl$", false); + Replace(smartStringBuilder, Descriptor.IterationCleanupMethod, "$IterationCleanupModifiers$", "$IterationCleanupImpl$", false); + return ReplaceCore(smartStringBuilder) + .Replace("$DisassemblerEntryMethodImpl$", GetWorkloadMethodCall(GetPassArgumentsDirect())) + .Replace("$OperationsPerInvoke$", Descriptor.OperationsPerInvoke.ToString()) + .Replace("$WorkloadTypeName$", Descriptor.Type.GetCorrectCSharpTypeName()); + } - public string WorkloadTypeName => Descriptor.Type.GetCorrectCSharpTypeName(); + private void Replace(SmartStringBuilder smartStringBuilder, MethodInfo? method, string replaceModifiers, string replaceImpl, bool isGlobalCleanup) + { + string modifier; + string impl; + if (method == null) + { + modifier = string.Empty; + impl = ReturnCompletedValueTask; + if (isGlobalCleanup) + { + impl = PrependExtraGlobalCleanupImpl(impl); + } + smartStringBuilder + .Replace(replaceModifiers, modifier) + .Replace(replaceImpl, impl); + return; + } - public string GlobalSetupMethodName => GetMethodName(Descriptor.GlobalSetupMethod); + if (method.ReturnType.IsAwaitable()) + { + modifier = "async"; + impl = $"await {GetMethodPrefix(method)}.{method.Name}();"; + } + else + { + modifier = string.Empty; + impl = $""" + {GetMethodPrefix(method)}.{method.Name}(); + {ReturnCompletedValueTask} + """; + } + if (isGlobalCleanup) + { + impl = PrependExtraGlobalCleanupImpl(impl); + } + smartStringBuilder + .Replace(replaceModifiers, modifier) + .Replace(replaceImpl, impl); + } - public string GlobalCleanupMethodName => GetMethodName(Descriptor.GlobalCleanupMethod); + protected abstract string PrependExtraGlobalCleanupImpl(string impl); - public string IterationSetupMethodName => Descriptor.IterationSetupMethod?.Name ?? EmptyAction; + protected abstract SmartStringBuilder ReplaceCore(SmartStringBuilder smartStringBuilder); - public string IterationCleanupMethodName => Descriptor.IterationCleanupMethod?.Name ?? EmptyAction; + private static string GetMethodPrefix(MethodInfo method) + => method.IsStatic ? method.DeclaringType!.GetCorrectCSharpTypeName() : "base"; - public abstract string GetWorkloadMethodCall(string passArguments); + protected string GetWorkloadMethodCall(string passArguments) + => $"{GetMethodPrefix(Descriptor.WorkloadMethod)}.{Descriptor.WorkloadMethod.Name}({passArguments});"; - protected static string GetMethodPrefix(MethodInfo method) - => method.IsStatic ? method.DeclaringType!.GetCorrectCSharpTypeName() : "base"; + protected string GetPassArgumentsDirect() + => string.Join( + ", ", + Descriptor.WorkloadMethod.GetParameters() + .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} this.__fieldsContainer.argField{index}") + ); + } + + internal class SyncDeclarationsProvider(BenchmarkCase benchmark) : DeclarationsProvider(benchmark) + { + public override string[] GetExtraFields() => []; + + protected override string PrependExtraGlobalCleanupImpl(string impl) => impl; - private string GetMethodName(MethodInfo? method) + protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartStringBuilder) { - if (method == null) - { - return EmptyAction; - } + string loadArguments = GetLoadArguments(); + string passArguments = GetPassArguments(); + string workloadMethodCall = GetWorkloadMethodCall(passArguments); + string coreImpl = $$""" + private unsafe {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}}) + { + {{loadArguments}} + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + this.__Overhead({{passArguments}});@Unroll@ + } + {{ReturnSyncCode}} + } - if (method.ReturnType == typeof(Task) || - method.ReturnType == typeof(ValueTask) || - (method.ReturnType.IsGenericType && - (method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) || - method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)))) - { - return $"() => global::BenchmarkDotNet.Helpers.AwaitHelper.GetResult({GetMethodPrefix(Descriptor.WorkloadMethod)}.{method.Name}())"; - } + private unsafe {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}}) + { + {{loadArguments}} + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + this.__Overhead({{passArguments}}); + } + {{ReturnSyncCode}} + } + + private unsafe {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}}) + { + {{loadArguments}} + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + {{workloadMethodCall}}@Unroll@ + } + {{ReturnSyncCode}} + } + + private unsafe {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}}) + { + {{loadArguments}} + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + {{workloadMethodCall}} + } + {{ReturnSyncCode}} + } + """; - return $"{GetMethodPrefix(Descriptor.WorkloadMethod)}.{method.Name}"; + return smartStringBuilder + .Replace("$CoreImpl$", coreImpl); } - } - internal class SyncDeclarationsProvider(Descriptor descriptor) : DeclarationsProvider(descriptor) - { - public override string GetWorkloadMethodCall(string passArguments) - => $"{GetMethodPrefix(Descriptor.WorkloadMethod)}.{Descriptor.WorkloadMethod.Name}({passArguments})"; + private string GetLoadArguments() + => string.Join( + Environment.NewLine, + Descriptor.WorkloadMethod.GetParameters() + .Select((parameter, index) => + { + var refModifier = parameter.ParameterType.IsByRef ? "ref" : string.Empty; + return $"{refModifier} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {refModifier} this.__fieldsContainer.argField{index};"; + }) + ); + + private string GetPassArguments() + => string.Join( + ", ", + Descriptor.WorkloadMethod.GetParameters() + .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} arg{index}") + ); } - internal class AsyncDeclarationsProvider(Descriptor descriptor) : DeclarationsProvider(descriptor) + internal class AsyncDeclarationsProvider(BenchmarkCase benchmark) : DeclarationsProvider(benchmark) { - public override string GetWorkloadMethodCall(string passArguments) - => $"global::BenchmarkDotNet.Helpers.AwaitHelper.GetResult({GetMethodPrefix(Descriptor.WorkloadMethod)}.{Descriptor.WorkloadMethod.Name}({passArguments}))"; + public override string[] GetExtraFields() => + [ + $"public {typeof(WorkloadContinuerAndValueTaskSource).GetCorrectCSharpTypeName()} workloadContinuerAndValueTaskSource;", + $"public {typeof(IClock).GetCorrectCSharpTypeName()} clock;", + "public long invokeCount;" + ]; + + protected override string PrependExtraGlobalCleanupImpl(string impl) + => $""" + this.__fieldsContainer.workloadContinuerAndValueTaskSource?.Complete(); + {impl} + """; + + protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartStringBuilder) + { + // Unlike sync calls, async calls suffer from unrolling, so we multiply the invokeCount by the unroll factor and delegate the implementation to *NoUnroll methods. + int unrollFactor = Benchmark.Job.ResolveValue(RunMode.UnrollFactorCharacteristic, EnvironmentResolver.Instance); + string passArguments = GetPassArgumentsDirect(); + string workloadMethodCall = GetWorkloadMethodCall(passArguments); + bool hasAsyncMethodBuilderAttribute = TryGetAsyncMethodBuilderAttribute(out var asyncMethodBuilderAttribute); + Type workloadCoreReturnType = GetWorkloadCoreReturnType(hasAsyncMethodBuilderAttribute); + string finalReturn = GetFinalReturn(workloadCoreReturnType); + string coreImpl = $$""" + private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}}) + { + return this.OverheadActionNoUnroll(invokeCount * {{unrollFactor}}, clock); + } + + private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}}) + { + {{StartClockSyncCode}} + while (--invokeCount >= 0) + { + this.__Overhead({{passArguments}}); + } + {{ReturnSyncCode}} + } + + private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}}) + { + return this.WorkloadActionNoUnroll(invokeCount * {{unrollFactor}}, clock); + } + + private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}}) + { + this.__fieldsContainer.invokeCount = invokeCount; + this.__fieldsContainer.clock = clock; + if (this.__fieldsContainer.workloadContinuerAndValueTaskSource == null) + { + this.__fieldsContainer.workloadContinuerAndValueTaskSource = new {{typeof(WorkloadContinuerAndValueTaskSource).GetCorrectCSharpTypeName()}}(); + this.__StartWorkload(); + } + return this.__fieldsContainer.workloadContinuerAndValueTaskSource.Continue(); + } + + private async void __StartWorkload() + { + await __WorkloadCore(); + } + + {{asyncMethodBuilderAttribute}} + private async {{workloadCoreReturnType.GetCorrectCSharpTypeName()}} __WorkloadCore() + { + try + { + while (true) + { + await this.__fieldsContainer.workloadContinuerAndValueTaskSource; + if (this.__fieldsContainer.workloadContinuerAndValueTaskSource.IsCompleted) + { + {{finalReturn}} + } + + {{typeof(StartedClock).GetCorrectCSharpTypeName()}} startedClock = {{typeof(ClockExtensions).GetCorrectCSharpTypeName()}}.Start(this.__fieldsContainer.clock); + while (--this.__fieldsContainer.invokeCount >= 0) + { + // Necessary because of error CS4004: Cannot await in an unsafe context + {{Descriptor.WorkloadMethod.ReturnType.GetCorrectCSharpTypeName()}} awaitable; + unsafe { awaitable = {{workloadMethodCall}} } + await awaitable; + } + this.__fieldsContainer.workloadContinuerAndValueTaskSource.SetResult(startedClock.GetElapsed()); + } + } + catch (global::System.Exception e) + { + __fieldsContainer.workloadContinuerAndValueTaskSource.SetException(e); + {{finalReturn}} + } + } + """; + + return smartStringBuilder + .Replace("$CoreImpl$", coreImpl); + } + + private bool TryGetAsyncMethodBuilderAttribute(out string asyncMethodBuilderAttribute) + { + asyncMethodBuilderAttribute = string.Empty; + if (Descriptor.WorkloadMethod.HasAttribute()) + { + return false; + } + if (Descriptor.WorkloadMethod.GetAsyncMethodBuilderAttribute() is not { } attr) + { + return false; + } + if (attr.GetType().GetProperty(nameof(AsyncMethodBuilderAttribute.BuilderType), BindingFlags.Public | BindingFlags.Instance)?.GetValue(attr) is not Type builderType) + { + return false; + } + asyncMethodBuilderAttribute = $"[{typeof(AsyncMethodBuilderAttribute).GetCorrectCSharpTypeName()}(typeof({builderType.GetCorrectCSharpTypeName()}))]"; + return true; + } + + private Type GetWorkloadCoreReturnType(bool hasAsyncMethodBuilderAttribute) + { + if (Descriptor.WorkloadMethod.ResolveAttribute() is { } asyncCallerTypeAttribute) + { + return asyncCallerTypeAttribute.AsyncCallerType; + } + if (hasAsyncMethodBuilderAttribute + || Descriptor.WorkloadMethod.ReturnType.HasAsyncMethodBuilderAttribute() + // Task and Task are not annotated with their builder type, the C# compiler special-cases them. + || (Descriptor.WorkloadMethod.ReturnType.IsGenericType && Descriptor.WorkloadMethod.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) + ) + { + return Descriptor.WorkloadMethod.ReturnType; + } + // Fallback to Task if the benchmark return type is Task or any awaitable type that is not a custom task-like type. + return typeof(Task); + } + + private static string GetFinalReturn(Type workloadCoreReturnType) + { + var finalReturnType = workloadCoreReturnType + .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)! + .ReturnType! + .GetMethod(nameof(TaskAwaiter.GetResult))! + .ReturnType; + return finalReturnType == typeof(void) + ? "return;" + : $"return default({finalReturnType.GetCorrectCSharpTypeName()});"; + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs index e1b49a6ff3..68b9b693d3 100644 --- a/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs +++ b/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs @@ -4,7 +4,9 @@ using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Attributes.CompilerServices; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Loggers; @@ -55,8 +57,8 @@ public void DisplayResults(ILogger logger) } } - public IEnumerable Validate(ValidationParameters validationParameters) - => diagnosers.SelectMany(diagnoser => diagnoser.Validate(validationParameters)); + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) + => diagnosers.ToAsyncEnumerable().SelectMany(diagnoser => diagnoser.ValidateAsync(validationParameters)); } public sealed class CompositeInProcessDiagnoser(IReadOnlyList inProcessDiagnosers) @@ -70,12 +72,12 @@ public void DeserializeResults(int index, BenchmarkCase benchmarkCase, string re => InProcessDiagnosers[index].DeserializeResults(benchmarkCase, results); } + [AggressivelyOptimizeMethods] [UsedImplicitly] [EditorBrowsable(EditorBrowsableState.Never)] public sealed class CompositeInProcessDiagnoserHandler(IReadOnlyList routers, IHost host, RunMode runMode, InProcessDiagnoserActionArgs parameters) { - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - public void Handle(BenchmarkSignal signal) + public async ValueTask HandleAsync(BenchmarkSignal signal) { if (runMode == RunMode.None) { diff --git a/src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs b/src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs index cad872292a..c478805970 100644 --- a/src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs @@ -56,7 +56,7 @@ public EventPipeProfiler(EventPipeProfile profile = EventPipeProfile.CpuSampling public RunMode GetRunMode(BenchmarkCase benchmarkCase) => performExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead; - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { foreach (var benchmark in validationParameters.Benchmarks) { diff --git a/src/BenchmarkDotNet/Diagnosers/ExceptionDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/ExceptionDiagnoser.cs index 589584899d..008a53fa4d 100644 --- a/src/BenchmarkDotNet/Diagnosers/ExceptionDiagnoser.cs +++ b/src/BenchmarkDotNet/Diagnosers/ExceptionDiagnoser.cs @@ -49,7 +49,7 @@ public IEnumerable ProcessResults(DiagnoserResults diagnoserResults) } } - public IEnumerable Validate(ValidationParameters validationParameters) => []; + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => AsyncEnumerable.Empty(); void IInProcessDiagnoser.DeserializeResults(BenchmarkCase benchmarkCase, string serializedResults) => results.Add(benchmarkCase, long.Parse(serializedResults)); diff --git a/src/BenchmarkDotNet/Diagnosers/IDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/IDiagnoser.cs index d28c63f9ee..475720ede3 100644 --- a/src/BenchmarkDotNet/Diagnosers/IDiagnoser.cs +++ b/src/BenchmarkDotNet/Diagnosers/IDiagnoser.cs @@ -26,7 +26,7 @@ public interface IDiagnoser void DisplayResults(ILogger logger); - IEnumerable Validate(ValidationParameters validationParameters); + IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters); } public interface IConfigurableDiagnoser : IDiagnoser diff --git a/src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs index e9c9fd7ae5..73d7ee16fd 100644 --- a/src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs +++ b/src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Engines; @@ -27,7 +28,7 @@ public class MemoryDiagnoser : IDiagnoser public IEnumerable Exporters => Array.Empty(); public IEnumerable Analysers => Array.Empty(); public void DisplayResults(ILogger logger) { } - public IEnumerable Validate(ValidationParameters validationParameters) => Array.Empty(); + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => AsyncEnumerable.Empty(); // the action takes places in other process, and the values are gathered by Engine public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { } diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index 3f7bb89ca1..efbc66d40d 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -51,7 +51,7 @@ public class PerfCollectProfiler : IProfiler public RunMode GetRunMode(BenchmarkCase benchmarkCase) => config.RunMode; - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { if (!OsDetector.IsLinux()) { diff --git a/src/BenchmarkDotNet/Diagnosers/SnapshotProfilerBase.cs b/src/BenchmarkDotNet/Diagnosers/SnapshotProfilerBase.cs index d6eb1db611..a9abd8107d 100644 --- a/src/BenchmarkDotNet/Diagnosers/SnapshotProfilerBase.cs +++ b/src/BenchmarkDotNet/Diagnosers/SnapshotProfilerBase.cs @@ -66,7 +66,7 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) } } - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { var runtimeMonikers = validationParameters.Benchmarks.Select(b => b.Job.Environment.GetRuntime().RuntimeMoniker).Distinct(); foreach (var runtimeMoniker in runtimeMonikers) diff --git a/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoser.cs index 4b2cb75f73..409c3da0f1 100644 --- a/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoser.cs +++ b/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoser.cs @@ -50,7 +50,7 @@ public IEnumerable ProcessResults(DiagnoserResults diagnoserResults) } } - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { foreach (var benchmark in validationParameters.Benchmarks) { diff --git a/src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs index 43cc910559..e7a44ea785 100644 --- a/src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs +++ b/src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs @@ -28,8 +28,10 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { } public void DisplayResults(ILogger logger) => logger.WriteLineError(GetErrorMessage()); - public IEnumerable Validate(ValidationParameters validationParameters) - => new[] { new ValidationError(false, GetErrorMessage()) }; + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) + { + yield return new ValidationError(false, GetErrorMessage()); + } private string GetErrorMessage() => $@"Unable to resolve {unresolved.Name} diagnoser using dynamic assembly loading. {(RuntimeInformation.IsFullFramework || OsDetector.IsWindows() diff --git a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs index fa75e9f170..8967877b94 100644 --- a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs +++ b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs @@ -115,7 +115,7 @@ public void DisplayResults(ILogger logger) ? "Disassembled benchmarks got exported to \".\\BenchmarkDotNet.Artifacts\\results\\*asm.md\"" : "No benchmarks were disassembled"); - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { var currentPlatform = RuntimeInformation.GetCurrentPlatform(); if (!(currentPlatform is Platform.X64 or Platform.X86 or Platform.Arm64)) diff --git a/src/BenchmarkDotNet/Engines/AnonymousPipesHost.cs b/src/BenchmarkDotNet/Engines/AnonymousPipesHost.cs deleted file mode 100644 index 7707d2d1cf..0000000000 --- a/src/BenchmarkDotNet/Engines/AnonymousPipesHost.cs +++ /dev/null @@ -1,86 +0,0 @@ -using BenchmarkDotNet.Validators; -using System; -using System.IO; -using System.Text; -using Microsoft.Win32.SafeHandles; -using JetBrains.Annotations; -using BenchmarkDotNet.Portability; -using System.Runtime.CompilerServices; - -namespace BenchmarkDotNet.Engines -{ - public class AnonymousPipesHost : IHost - { - internal const string AnonymousPipesDescriptors = "--anonymousPipes"; - internal static readonly Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - - private readonly StreamWriter outWriter; - private readonly StreamReader inReader; - - public AnonymousPipesHost(string writHandle, string readHandle) - { - outWriter = new StreamWriter(OpenAnonymousPipe(writHandle, FileAccess.Write), UTF8NoBOM); - // Flush the data to the Stream after each write, otherwise the host process will wait for input endlessly! - outWriter.AutoFlush = true; - inReader = new StreamReader(OpenAnonymousPipe(readHandle, FileAccess.Read), UTF8NoBOM, detectEncodingFromByteOrderMarks: false); - } - - private Stream OpenAnonymousPipe(string fileHandle, FileAccess access) - => new FileStream(new SafeFileHandle(new IntPtr(int.Parse(fileHandle)), ownsHandle: true), access, bufferSize: 1); - - public void Dispose() - { - outWriter.Dispose(); - inReader.Dispose(); - } - - public void Write(string message) => outWriter.Write(message); - - public void WriteLine() => outWriter.WriteLine(); - - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - public void WriteLine(string message) => outWriter.WriteLine(message); - - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - public void SendSignal(HostSignal hostSignal) - { - if (hostSignal == HostSignal.AfterAll) - { - // Before the last signal is reported and the benchmark process exits, - // add an artificial sleep to increase the chance of host process reading all std output. - System.Threading.Thread.Sleep(1); - } - - WriteLine(Engine.Signals.ToMessage(hostSignal)); - - // read the response from Parent process, make the communication blocking - string? acknowledgment = inReader.ReadLine(); - if (acknowledgment != Engine.Signals.Acknowledgment - && !(acknowledgment is null && hostSignal == HostSignal.AfterAll)) // an early EOF, but still valid - { - throw new NotSupportedException($"Unknown Acknowledgment: {acknowledgment}"); - } - } - - public void SendError(string message) => outWriter.WriteLine($"{ValidationErrorReporter.ConsoleErrorPrefix} {message}"); - - public void ReportResults(RunResults runResults) => runResults.Print(outWriter); - - [PublicAPI] // called from generated code - public static bool TryGetFileHandles(string[] args, out string? writeHandle, out string? readHandle) - { - for (int i = 0; i < args.Length; i++) - { - if (args[i] == AnonymousPipesDescriptors) - { - writeHandle = args[i + 1]; // IndexOutOfRangeException means a bug (incomplete data) - readHandle = args[i + 2]; - return true; - } - } - - writeHandle = readHandle = null; - return false; - } - } -} diff --git a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs new file mode 100644 index 0000000000..077674b2a0 --- /dev/null +++ b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs @@ -0,0 +1,168 @@ +using JetBrains.Annotations; +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines; + +// Used to ensure async continuations are posted back to the same thread that the benchmark process was started on. +[UsedImplicitly] +[EditorBrowsable(EditorBrowsableState.Never)] +public readonly ref struct BenchmarkSynchronizationContext : IDisposable +{ + private readonly BenchmarkDotNetSynchronizationContext context; + + private BenchmarkSynchronizationContext(BenchmarkDotNetSynchronizationContext context) + { + this.context = context; + } + + public static BenchmarkSynchronizationContext CreateAndSetCurrent() + { + var context = new BenchmarkDotNetSynchronizationContext(SynchronizationContext.Current); + SynchronizationContext.SetSynchronizationContext(context); + return new(context); + } + + public void Dispose() + => context.Dispose(); + + public T ExecuteUntilComplete(ValueTask valueTask) + => context.ExecuteUntilComplete(valueTask); +} + +internal sealed class BenchmarkDotNetSynchronizationContext : SynchronizationContext +{ + private readonly SynchronizationContext? previousContext; + private readonly object syncRoot = new(); + // Use 2 arrays to reduce lock contention while executing. The common case is only 1 callback will be queued at a time. + private (SendOrPostCallback d, object? state)[]? queue = new (SendOrPostCallback d, object? state)[1]; + private (SendOrPostCallback d, object? state)[]? executing = new (SendOrPostCallback d, object? state)[1]; + private int queueCount = 0; + private bool isCompleted; + + internal BenchmarkDotNetSynchronizationContext(SynchronizationContext? previousContext) + { + this.previousContext = previousContext; + } + + public override SynchronizationContext CreateCopy() + => this; + + public override void Post(SendOrPostCallback d, object? state) + { + if (d is null) throw new ArgumentNullException(nameof(d)); + + lock (syncRoot) + { + ThrowIfDisposed(); + + int index = queueCount; + if (++queueCount > queue!.Length) + { + Array.Resize(ref queue, queue.Length * 2); + } + queue[index] = (d, state); + + Monitor.Pulse(syncRoot); + } + } + + private void ThrowIfDisposed() => _ = queue ?? throw new ObjectDisposedException(nameof(BenchmarkDotNetSynchronizationContext)); + + internal void Dispose() + { + int count; + (SendOrPostCallback d, object? state)[] executing; + lock (syncRoot) + { + ThrowIfDisposed(); + + // Flush any remaining posted callbacks. + count = queueCount; + queueCount = 0; + executing = queue!; + queue = null; + } + this.executing = null; + for (int i = 0; i < count; ++i) + { + executing[i].d(executing[i].state); + executing[i] = default; + } + SetSynchronizationContext(previousContext); + } + + internal T ExecuteUntilComplete(ValueTask valueTask) + { + ThrowIfDisposed(); + + var awaiter = valueTask.GetAwaiter(); + if (awaiter.IsCompleted) + { + return awaiter.GetResult(); + } + + isCompleted = false; + awaiter.UnsafeOnCompleted(OnCompleted); + + var spinner = new SpinWait(); + while (true) + { + int count; + (SendOrPostCallback d, object? state)[] executing; + lock (syncRoot) + { + if (isCompleted) + { + return awaiter.GetResult(); + } + + count = queueCount; + queueCount = 0; + executing = queue!; + queue = this.executing; + + if (count == 0) + { + if (spinner.NextSpinWillYield) + { + // Yield the thread and wait for completion or for a posted callback. + // Thread-safety note: isCompleted and queueCount must be checked inside the lock body + // before calling Monitor.Wait to avoid missing the pulse and waiting forever. + Monitor.Wait(syncRoot); + goto ResetAndContinue; + } + else + { + goto SpinAndContinue; + } + } + } + this.executing = executing; + for (int i = 0; i < count; ++i) + { + var (d, state) = executing[i]; + executing[i] = default; + d(state); + } + + ResetAndContinue: + spinner = new(); + continue; + + SpinAndContinue: + spinner.SpinOnce(); + } + } + + private void OnCompleted() + { + lock (syncRoot) + { + isCompleted = true; + Monitor.Pulse(syncRoot); + } + } +} diff --git a/src/BenchmarkDotNet/Engines/Engine.cs b/src/BenchmarkDotNet/Engines/Engine.cs index 334fe40b90..2b5119f758 100644 --- a/src/BenchmarkDotNet/Engines/Engine.cs +++ b/src/BenchmarkDotNet/Engines/Engine.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes.CompilerServices; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Portability; @@ -12,6 +14,9 @@ namespace BenchmarkDotNet.Engines { + // MethodImplOptions.AggressiveOptimization is applied to all methods to force them to go straight to tier1 JIT, + // eliminating tiered JIT as a potential variable in measurements. + [AggressivelyOptimizeMethods] [UsedImplicitly] public class Engine : IEngine { @@ -59,13 +64,13 @@ internal Engine(EngineParameters engineParameters) random = new Random(12345); // we are using constant seed to try to get repeatable results } - public RunResults Run() + public async ValueTask RunAsync() { - Parameters.GlobalSetupAction.Invoke(); + await Parameters.GlobalSetupAction.Invoke(); bool didThrow = false; try { - return RunCore(); + return await RunCore(); } catch { @@ -76,7 +81,7 @@ public RunResults Run() { try { - Parameters.GlobalCleanupAction.Invoke(); + await Parameters.GlobalCleanupAction.Invoke(); } // We only catch if the benchmark threw to not overwrite the exception. #1045 catch (Exception e) when (didThrow) @@ -86,17 +91,15 @@ public RunResults Run() } } - // AggressiveOptimization forces the method to go straight to tier1 JIT, and will never be re-jitted, - // eliminating tiered JIT as a potential variable in measurements. - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private RunResults RunCore() + // This method is extra long because the helper methods were inlined in order to prevent extra async allocations on each iteration. + private async ValueTask RunCore() { var measurements = new List(); if (EngineEventSource.Log.IsEnabled()) EngineEventSource.Log.BenchmarkStart(Parameters.BenchmarkName); - IterationData extraStatsIterationData = default; + IterationData extraIterationData = default; // Enumerate the stages and run iterations in a loop to ensure each benchmark invocation is called with a constant stack size. // #1120 foreach (var stage in EngineStage.EnumerateStages(Parameters)) @@ -104,16 +107,48 @@ private RunResults RunCore() if (stage.Stage == IterationStage.Actual && stage.Mode == IterationMode.Workload) { Host.BeforeMainRun(); - Parameters.InProcessDiagnoserHandler.Handle(BenchmarkSignal.BeforeActualRun); + await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeActualRun); } var stageMeasurements = stage.GetMeasurementList(); while (stage.GetShouldRunIteration(stageMeasurements, out var iterationData)) { - var measurement = RunIteration(iterationData); + // Initialization + long invokeCount = iterationData.invokeCount; + int unrollFactor = iterationData.unrollFactor; + if (invokeCount % unrollFactor != 0) + throw new ArgumentOutOfRangeException(nameof(iterationData), $"InvokeCount({invokeCount}) should be a multiple of UnrollFactor({unrollFactor})."); + + long totalOperations = invokeCount * Parameters.OperationsPerInvoke; + bool randomizeMemory = iterationData.mode == IterationMode.Workload && MemoryRandomization; + + await iterationData.setupAction(); + + GcCollect(); + + if (EngineEventSource.Log.IsEnabled()) + EngineEventSource.Log.IterationStart(iterationData.mode, iterationData.stage, totalOperations); + + var clockSpan = randomizeMemory + ? await MeasureWithRandomStack(iterationData.workloadAction, invokeCount / unrollFactor) + : await iterationData.workloadAction(invokeCount / unrollFactor, Clock); + + if (EngineEventSource.Log.IsEnabled()) + EngineEventSource.Log.IterationStop(iterationData.mode, iterationData.stage, totalOperations); + + await iterationData.cleanupAction(); + + if (randomizeMemory) + await RandomizeManagedHeapMemory(); + + GcCollect(); + + // Results + var measurement = new Measurement(0, iterationData.mode, iterationData.stage, iterationData.index, totalOperations, clockSpan.GetNanoseconds()); + Host.WriteLine(measurement.ToString()); stageMeasurements.Add(measurement); // Actual Workload is always the last stage, so we use the same data to run extra stats. - extraStatsIterationData = iterationData; + extraIterationData = iterationData; } measurements.AddRange(stageMeasurements); @@ -122,126 +157,72 @@ private RunResults RunCore() if (stage.Stage == IterationStage.Actual && stage.Mode == IterationMode.Workload) { Host.AfterMainRun(); - Parameters.InProcessDiagnoserHandler.Handle(BenchmarkSignal.AfterActualRun); + await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterActualRun); } } GcStats workGcHasDone = default; if (Parameters.RunExtraIteration) { - (workGcHasDone, var extraMeasurement) = RunExtraIteration(extraStatsIterationData); - measurements.Add(extraMeasurement); - } + // Warm up the GC measurement functions before starting the actual measurement. + DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadInitial()); + DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadFinal()); - if (EngineEventSource.Log.IsEnabled()) - EngineEventSource.Log.BenchmarkStop(Parameters.BenchmarkName); + await extraIterationData.setupAction!(); // we run iteration setup first, so even if it allocates, it is not included in the results - var outlierMode = TargetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, Resolver); + Host.SendSignal(HostSignal.BeforeExtraIteration); + await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeExtraIteration); - return new RunResults(measurements, outlierMode, workGcHasDone); - } + // GC collect before measuring allocations. + ForceGcCollect(); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private Measurement RunIteration(IterationData data) - { - // Initialization - long invokeCount = data.invokeCount; - int unrollFactor = data.unrollFactor; - if (invokeCount % unrollFactor != 0) - throw new ArgumentOutOfRangeException(nameof(data), $"InvokeCount({invokeCount}) should be a multiple of UnrollFactor({unrollFactor})."); + // #1542 + // If the jit is tiered, we put the current thread to sleep so it can kick in, compile its stuff, + // and NOT allocate anything on the background thread when we are measuring allocations. + SleepIfPositive(JitInfo.BackgroundCompilationDelay); - long totalOperations = invokeCount * Parameters.OperationsPerInvoke; - bool randomizeMemory = data.mode == IterationMode.Workload && MemoryRandomization; + GcStats gcStats; + ClockSpan clockSpan; + using (FinalizerBlocker.MaybeStart()) + { + (gcStats, clockSpan) = await MeasureWithGc(extraIterationData.workloadAction!, extraIterationData.invokeCount / extraIterationData.unrollFactor); + } - data.setupAction(); + await Parameters.InProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterExtraIteration); + Host.SendSignal(HostSignal.AfterExtraIteration); - GcCollect(); + await extraIterationData.cleanupAction!(); // we run iteration cleanup after diagnosers are complete. - if (EngineEventSource.Log.IsEnabled()) - EngineEventSource.Log.IterationStart(data.mode, data.stage, totalOperations); - - var clockSpan = randomizeMemory - ? MeasureWithRandomStack(data.workloadAction, invokeCount / unrollFactor) - : Measure(data.workloadAction, invokeCount / unrollFactor); + var totalOperations = extraIterationData.invokeCount * Parameters.OperationsPerInvoke; + var measurement = new Measurement(0, IterationMode.Workload, IterationStage.Extra, 1, totalOperations, clockSpan.GetNanoseconds()); + Host.WriteLine(measurement.ToString()); + workGcHasDone = gcStats.WithTotalOperations(totalOperations); + measurements.Add(measurement); + } if (EngineEventSource.Log.IsEnabled()) - EngineEventSource.Log.IterationStop(data.mode, data.stage, totalOperations); - - data.cleanupAction(); - - if (randomizeMemory) - RandomizeManagedHeapMemory(); + EngineEventSource.Log.BenchmarkStop(Parameters.BenchmarkName); - GcCollect(); + var outlierMode = TargetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, Resolver); - // Results - var measurement = new Measurement(0, data.mode, data.stage, data.index, totalOperations, clockSpan.GetNanoseconds()); - Host.WriteLine(measurement.ToString()); - return measurement; + return new RunResults(measurements, outlierMode, workGcHasDone); } // This is in a separate method, because stackalloc can affect code alignment, // resulting in unexpected measurements on some AMD cpus, // even if the stackalloc branch isn't executed. (#2366) - [MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)] - private unsafe ClockSpan MeasureWithRandomStack(Action action, long invokeCount) + [MethodImpl(MethodImplOptions.NoInlining)] + private unsafe ValueTask MeasureWithRandomStack(Func> action, long invokeCount) { byte* stackMemory = stackalloc byte[random.Next(32)]; - var clockSpan = Measure(action, invokeCount); + var task = action(invokeCount, Clock); Consume(stackMemory); - return clockSpan; + return task; } - [MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)] + [MethodImpl(MethodImplOptions.NoInlining)] private unsafe void Consume(byte* _) { } - [MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)] - private ClockSpan Measure(Action action, long invokeCount) - { - var clock = Clock.Start(); - action(invokeCount); - return clock.GetElapsed(); - } - - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private (GcStats, Measurement) RunExtraIteration(IterationData data) - { - // Warm up the GC measurement functions before starting the actual measurement. - DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadInitial()); - DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadFinal()); - - data.setupAction(); // we run iteration setup first, so even if it allocates, it is not included in the results - - Host.SendSignal(HostSignal.BeforeExtraIteration); - Parameters.InProcessDiagnoserHandler.Handle(BenchmarkSignal.BeforeExtraIteration); - - // GC collect before measuring allocations. - ForceGcCollect(); - - // #1542 - // If the jit is tiered, we put the current thread to sleep so it can kick in, compile its stuff, - // and NOT allocate anything on the background thread when we are measuring allocations. - SleepIfPositive(JitInfo.BackgroundCompilationDelay); - - GcStats gcStats; - ClockSpan clockSpan; - using (FinalizerBlocker.MaybeStart()) - { - (gcStats, clockSpan) = MeasureWithGc(data.workloadAction, data.invokeCount / data.unrollFactor); - } - - Parameters.InProcessDiagnoserHandler.Handle(BenchmarkSignal.AfterExtraIteration); - Host.SendSignal(HostSignal.AfterExtraIteration); - - data.cleanupAction(); // we run iteration cleanup after diagnosers are complete. - - var totalOperations = data.invokeCount * Parameters.OperationsPerInvoke; - var measurement = new Measurement(0, IterationMode.Workload, IterationStage.Extra, 1, totalOperations, clockSpan.GetNanoseconds()); - Host.WriteLine(measurement.ToString()); - return (gcStats.WithTotalOperations(totalOperations), measurement); - } - - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] internal static void SleepIfPositive(TimeSpan timeSpan) { if (timeSpan > TimeSpan.Zero) @@ -250,28 +231,27 @@ internal static void SleepIfPositive(TimeSpan timeSpan) } } - // Isolate the allocation measurement and skip tier0 jit to make sure we don't get any unexpected allocations. - [MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)] - private (GcStats, ClockSpan) MeasureWithGc(Action action, long invokeCount) + // Isolate the allocation measurement to make sure we don't get any unexpected allocations. + [MethodImpl(MethodImplOptions.NoInlining)] + private async ValueTask<(GcStats, ClockSpan)> MeasureWithGc(Func> action, long invokeCount) { var initialGcStats = GcStats.ReadInitial(); - var clockSpan = Measure(action, invokeCount); + var clockSpan = await action(invokeCount, Clock); var finalGcStats = GcStats.ReadFinal(); return (finalGcStats - initialGcStats, clockSpan); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void RandomizeManagedHeapMemory() + private async ValueTask RandomizeManagedHeapMemory() { // invoke global cleanup before global setup - Parameters.GlobalCleanupAction.Invoke(); + await Parameters.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 - Parameters.GlobalSetupAction.Invoke(); + await Parameters.GlobalSetupAction.Invoke(); GC.KeepAlive(gen0object); GC.KeepAlive(lohObject); @@ -279,7 +259,6 @@ private void RandomizeManagedHeapMemory() // we don't enforce GC.Collects here as engine does it later anyway } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] private void GcCollect() { if (!ForceGcCleanups) @@ -288,7 +267,6 @@ private void GcCollect() ForceGcCollect(); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] internal static void ForceGcCollect() { GC.Collect(); @@ -314,10 +292,8 @@ public static class Signals private static readonly Dictionary MessagesToSignals = SignalsToMessages.ToDictionary(p => p.Value, p => p.Key); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] public static string ToMessage(HostSignal signal) => SignalsToMessages[signal]; - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] public static bool TryGetSignal(string message, out HostSignal signal) => MessagesToSignals.TryGetValue(message, out signal); } @@ -343,7 +319,6 @@ private sealed class Impl private readonly object hangLock = new(); private readonly ManualResetEventSlim enteredFinalizerEvent = new(false); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] ~Impl() { lock (hangLock) @@ -353,7 +328,7 @@ private sealed class Impl } } - [MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)] + [MethodImpl(MethodImplOptions.NoInlining)] internal static (object hangLock, ManualResetEventSlim enteredFinalizerEvent) CreateWeakly() { var impl = new Impl(); @@ -361,7 +336,6 @@ internal static (object hangLock, ManualResetEventSlim enteredFinalizerEvent) Cr } } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] internal static FinalizerBlocker MaybeStart() { if (Environment.GetEnvironmentVariable(UnitTestBlockFinalizerEnvKey) != UnitTestBlockFinalizerEnvValue) @@ -378,7 +352,6 @@ internal static FinalizerBlocker MaybeStart() return new FinalizerBlocker(hangLock); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] public void Dispose() { if (hangLock is not null) diff --git a/src/BenchmarkDotNet/Engines/EngineJitStage.cs b/src/BenchmarkDotNet/Engines/EngineJitStage.cs index a774696417..a7e87c5691 100644 --- a/src/BenchmarkDotNet/Engines/EngineJitStage.cs +++ b/src/BenchmarkDotNet/Engines/EngineJitStage.cs @@ -7,15 +7,11 @@ namespace BenchmarkDotNet.Engines; -internal abstract class EngineJitStage(EngineParameters parameters) : EngineStage(IterationStage.Jitting, IterationMode.Workload, parameters) -{ -} - // We do our best to encourage the jit to fully promote methods to tier1, but tiered jit relies on heuristics, // and we purposefully don't spend too much time in this stage, so we can't guarantee it. // This should succeed for 99%+ of microbenchmarks. For any sufficiently short benchmarks where this fails, // the following stages (Pilot and Warmup) will likely take it the rest of the way. Long-running benchmarks may never fully reach tier1. -internal sealed class EngineFirstJitStage : EngineJitStage +internal sealed class EngineJitStage : EngineStage { // It is not worth spending a long time in jit stage for macro-benchmarks. private static readonly TimeInterval MaxTieringTime = TimeInterval.FromSeconds(10); @@ -29,7 +25,7 @@ internal sealed class EngineFirstJitStage : EngineJitStage private readonly IEnumerator enumerator; private readonly bool evaluateOverhead; - internal EngineFirstJitStage(bool evaluateOverhead, EngineParameters parameters) : base(parameters) + internal EngineJitStage(bool evaluateOverhead, EngineParameters parameters) : base(IterationStage.Jitting, IterationMode.Workload, parameters) { enumerator = EnumerateIterations(); this.evaluateOverhead = evaluateOverhead; @@ -44,7 +40,7 @@ private int GetMaxMeasurementCount() : 1; if (evaluateOverhead) { - count *= 2; + count += 1; } return count; } @@ -111,10 +107,7 @@ private IEnumerator EnumerateIterations() remainingCalls -= invokeCount; ++iterationIndex; - if (evaluateOverhead) - { - yield return GetOverheadIterationData(invokeCount); - } + // The generated __Overhead method is aggressively optimized, so we don't need to run it again. yield return GetWorkloadIterationData(invokeCount); if ((remainingTiers + remainingCalls) > 0 @@ -131,44 +124,12 @@ private IEnumerator EnumerateIterations() // Empirical evidence shows that the first call after the method is tiered up may take longer, // so we run an extra iteration to ensure the next stage gets a stable measurement. ++iterationIndex; - if (evaluateOverhead) - { - yield return GetOverheadIterationData(1); - } yield return GetWorkloadIterationData(1); } private IterationData GetOverheadIterationData(long invokeCount) - => new(IterationMode.Overhead, IterationStage.Jitting, iterationIndex, invokeCount, 1, () => { }, () => { }, parameters.OverheadActionNoUnroll); + => new(IterationMode.Overhead, IterationStage.Jitting, iterationIndex, invokeCount, 1, () => new(), () => new(), parameters.OverheadActionNoUnroll); private IterationData GetWorkloadIterationData(long invokeCount) => new(IterationMode.Workload, IterationStage.Jitting, iterationIndex, invokeCount, 1, parameters.IterationSetupAction, parameters.IterationCleanupAction, parameters.WorkloadActionNoUnroll); -} - -internal sealed class EngineSecondJitStage : EngineJitStage -{ - private readonly int unrollFactor; - private readonly bool evaluateOverhead; - - public EngineSecondJitStage(int unrollFactor, bool evaluateOverhead, EngineParameters parameters) : base(parameters) - { - this.unrollFactor = unrollFactor; - this.evaluateOverhead = evaluateOverhead; - iterationIndex = evaluateOverhead ? 0 : 2; - } - - internal override List GetMeasurementList() => new(evaluateOverhead ? 2 : 1); - - // The benchmark method has already been jitted via *NoUnroll, we only need to jit the *Unroll methods here, which aren't tiered. - internal override bool GetShouldRunIteration(List measurements, out IterationData iterationData) - { - iterationData = ++iterationIndex switch - { - 1 => new(IterationMode.Overhead, IterationStage.Jitting, 1, unrollFactor, unrollFactor, () => { }, () => { }, parameters.OverheadActionUnroll), - // IterationSetup/Cleanup are only used for *NoUnroll benchmarks - 2 => new(IterationMode.Workload, IterationStage.Jitting, 1, unrollFactor, unrollFactor, () => { }, () => { }, parameters.WorkloadActionUnroll), - _ => default - }; - return iterationIndex <= 2; - } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/EngineParameters.cs b/src/BenchmarkDotNet/Engines/EngineParameters.cs index 7add0414bb..dda417062f 100644 --- a/src/BenchmarkDotNet/Engines/EngineParameters.cs +++ b/src/BenchmarkDotNet/Engines/EngineParameters.cs @@ -1,7 +1,9 @@ using System; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; +using Perfolizer.Horology; namespace BenchmarkDotNet.Engines { @@ -11,16 +13,16 @@ public class EngineParameters public IResolver Resolver { get; set; } = DefaultResolver; public required IHost Host { get; set; } - public required Action WorkloadActionNoUnroll { get; set; } - public required Action WorkloadActionUnroll { get; set; } - public required Action OverheadActionNoUnroll { get; set; } - public required Action OverheadActionUnroll { get; set; } + public required Func> WorkloadActionNoUnroll { get; set; } + public required Func> WorkloadActionUnroll { get; set; } + public required Func> OverheadActionNoUnroll { get; set; } + public required Func> OverheadActionUnroll { get; set; } public Job TargetJob { get; set; } = Job.Default; public long OperationsPerInvoke { get; set; } = 1; - public required Action GlobalSetupAction { get; set; } - public required Action GlobalCleanupAction { get; set; } - public required Action IterationSetupAction { get; set; } - public required Action IterationCleanupAction { get; set; } + public required Func GlobalSetupAction { get; set; } + public required Func GlobalCleanupAction { get; set; } + public required Func IterationSetupAction { get; set; } + public required Func IterationCleanupAction { get; set; } public bool RunExtraIteration { get; set; } public required string BenchmarkName { get; set; } public required Diagnosers.CompositeInProcessDiagnoserHandler InProcessDiagnoserHandler { get; set; } diff --git a/src/BenchmarkDotNet/Engines/EngineStage.cs b/src/BenchmarkDotNet/Engines/EngineStage.cs index 8bd031112f..7598297ec3 100644 --- a/src/BenchmarkDotNet/Engines/EngineStage.cs +++ b/src/BenchmarkDotNet/Engines/EngineStage.cs @@ -33,7 +33,7 @@ internal static IEnumerable EnumerateStages(EngineParameters parame int minInvokeCount = parameters.TargetJob.ResolveValue(AccuracyMode.MinInvokeCountCharacteristic, parameters.Resolver); // AOT technically doesn't have a JIT, but we run jit stage regardless because of static constructors. #2004 - var jitStage = new EngineFirstJitStage(evaluateOverhead, parameters); + var jitStage = new EngineJitStage(evaluateOverhead, parameters); yield return jitStage; bool hasUnrollFactor = parameters.TargetJob.HasValue(RunMode.UnrollFactorCharacteristic); @@ -58,13 +58,6 @@ internal static IEnumerable EnumerateStages(EngineParameters parame skipPilotStage = !pilotStage.needsFurtherPilot; } - // The first jit stage only jitted *NoUnroll methods, now we need to jit *Unroll methods if they're going to be used. - // TODO: This stage can be removed after we refactor the engine/codegen to pass the clock into the delegates. - if (!RuntimeInformation.IsAot && unrollFactor != 1) - { - yield return new EngineSecondJitStage(unrollFactor, evaluateOverhead, parameters); - } - if (!skipPilotStage) { var pilotStage = EnginePilotStage.GetStage(invokeCount, unrollFactor, minInvokeCount, parameters); diff --git a/src/BenchmarkDotNet/Engines/IEngine.cs b/src/BenchmarkDotNet/Engines/IEngine.cs index 9754d58354..a877c82d38 100644 --- a/src/BenchmarkDotNet/Engines/IEngine.cs +++ b/src/BenchmarkDotNet/Engines/IEngine.cs @@ -1,6 +1,8 @@ -namespace BenchmarkDotNet.Engines; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Engines; public interface IEngine { - RunResults Run(); + ValueTask RunAsync(); } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/IHost.cs b/src/BenchmarkDotNet/Engines/IHost.cs index e57ee2172b..25f0b4b957 100644 --- a/src/BenchmarkDotNet/Engines/IHost.cs +++ b/src/BenchmarkDotNet/Engines/IHost.cs @@ -1,18 +1,18 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +using System; +using System.ComponentModel; -namespace BenchmarkDotNet.Engines +namespace BenchmarkDotNet.Engines; + +[UsedImplicitly] +[EditorBrowsable(EditorBrowsableState.Never)] +public interface IHost : IDisposable { - [SuppressMessage("ReSharper", "UnusedMember.Global")] - public interface IHost : IDisposable - { - void Write(string message); - void WriteLine(); - void WriteLine(string message); + void WriteLine(); + void WriteLine(string message); - void SendSignal(HostSignal hostSignal); - void SendError(string message); + void SendSignal(HostSignal hostSignal); + void SendError(string message); - void ReportResults(RunResults runResults); - } + void ReportResults(RunResults runResults); } diff --git a/src/BenchmarkDotNet/Engines/IterationData.cs b/src/BenchmarkDotNet/Engines/IterationData.cs index f95f686922..de6efdc759 100644 --- a/src/BenchmarkDotNet/Engines/IterationData.cs +++ b/src/BenchmarkDotNet/Engines/IterationData.cs @@ -1,17 +1,18 @@ -using System; +using Perfolizer.Horology; +using System; +using System.Threading.Tasks; -namespace BenchmarkDotNet.Engines +namespace BenchmarkDotNet.Engines; + +internal readonly struct IterationData(IterationMode iterationMode, IterationStage iterationStage, int index, long invokeCount, int unrollFactor, + Func setupAction, Func cleanupAction, Func> workloadAction) { - internal readonly struct IterationData(IterationMode iterationMode, IterationStage iterationStage, int index, long invokeCount, int unrollFactor, - Action setupAction, Action cleanupAction, Action workloadAction) - { - public readonly IterationMode mode = iterationMode; - public readonly IterationStage stage = iterationStage; - public readonly int index = index; - public readonly long invokeCount = invokeCount; - public readonly int unrollFactor = unrollFactor; - public readonly Action setupAction = setupAction; - public readonly Action cleanupAction = cleanupAction; - public readonly Action workloadAction = workloadAction; - } + public readonly IterationMode mode = iterationMode; + public readonly IterationStage stage = iterationStage; + public readonly int index = index; + public readonly long invokeCount = invokeCount; + public readonly int unrollFactor = unrollFactor; + public readonly Func setupAction = setupAction; + public readonly Func cleanupAction = cleanupAction; + public readonly Func> workloadAction = workloadAction; } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Engines/NoAcknowledgementConsoleHost.cs b/src/BenchmarkDotNet/Engines/NoAcknowledgementConsoleHost.cs index bcd0281e0d..7a88851644 100644 --- a/src/BenchmarkDotNet/Engines/NoAcknowledgementConsoleHost.cs +++ b/src/BenchmarkDotNet/Engines/NoAcknowledgementConsoleHost.cs @@ -1,35 +1,38 @@ using System; using System.IO; -using System.Runtime.CompilerServices; -using BenchmarkDotNet.Portability; +using BenchmarkDotNet.Attributes.CompilerServices; using BenchmarkDotNet.Validators; -namespace BenchmarkDotNet.Engines +namespace BenchmarkDotNet.Engines; + +// This class is used when somebody manually launches benchmarking .exe without providing pipe name, or for wasm that doesn't support pipes. +[AggressivelyOptimizeMethods] +internal sealed class NoAcknowledgementConsoleHost : IHost { - // this class is used only when somebody manually launches benchmarking .exe without providing anonymous pipes file descriptors - public sealed class NoAcknowledgementConsoleHost : IHost - { - private readonly TextWriter outWriter; + private readonly TextWriter outWriter; - public NoAcknowledgementConsoleHost() => outWriter = Console.Out; + public NoAcknowledgementConsoleHost() => outWriter = Console.Out; - public void Write(string message) => outWriter.Write(message); + public void WriteAsync(string message) + => outWriter.Write(message); - public void WriteLine() => outWriter.WriteLine(); + public void WriteLine() + => outWriter.WriteLine(); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - public void WriteLine(string message) => outWriter.WriteLine(message); + public void WriteLine(string message) + => outWriter.WriteLine(message); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - public void SendSignal(HostSignal hostSignal) => WriteLine(Engine.Signals.ToMessage(hostSignal)); + public void SendSignal(HostSignal hostSignal) + => WriteLine(Engine.Signals.ToMessage(hostSignal)); - public void SendError(string message) => outWriter.WriteLine($"{ValidationErrorReporter.ConsoleErrorPrefix} {message}"); + public void SendError(string message) + => WriteLine($"{ValidationErrorReporter.ConsoleErrorPrefix} {message}"); - public void ReportResults(RunResults runResults) => runResults.Print(outWriter); + public void ReportResults(RunResults runResults) + => runResults.Print(outWriter); - public void Dispose() - { - // do nothing on purpose - there is no point in closing STD OUT - } + public void Dispose() + { + // do nothing on purpose - there is no point in closing STD OUT } } diff --git a/src/BenchmarkDotNet/Engines/TcpHost.cs b/src/BenchmarkDotNet/Engines/TcpHost.cs new file mode 100644 index 0000000000..caa531a8c2 --- /dev/null +++ b/src/BenchmarkDotNet/Engines/TcpHost.cs @@ -0,0 +1,86 @@ +using BenchmarkDotNet.Attributes.CompilerServices; +using BenchmarkDotNet.Validators; +using JetBrains.Annotations; +using System; +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace BenchmarkDotNet.Engines; + +[AggressivelyOptimizeMethods] +[UsedImplicitly] +[EditorBrowsable(EditorBrowsableState.Never)] +public class TcpHost : IHost +{ + internal const string TcpPortDescriptor = "--tcpPort"; + internal static readonly Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + public static readonly TimeSpan ConnectionTimeout = TimeSpan.FromMinutes(1); + + private readonly StreamWriter outWriter; + private readonly StreamReader inReader; + + public TcpHost(TcpClient client) + { + var stream = client.GetStream(); + // Flush the data to the Stream after each write, otherwise the host process will wait for input endlessly! + outWriter = new(stream, UTF8NoBOM) { AutoFlush = true }; + inReader = new(stream, UTF8NoBOM, detectEncodingFromByteOrderMarks: false); + } + + public void Dispose() + { + outWriter.Dispose(); + inReader.Dispose(); + } + + public void WriteLine() + => outWriter.WriteLine(); + + public void WriteLine(string message) + => outWriter.WriteLine(message); + + public void SendSignal(HostSignal hostSignal) + { + if (hostSignal == HostSignal.AfterAll) + { + // Before the last signal is reported and the benchmark process exits, + // add an artificial sleep to increase the chance of host process reading all std output. + Thread.Sleep(1); + } + + outWriter.WriteLine(Engine.Signals.ToMessage(hostSignal)); + + // Read the response from Parent process. + string? acknowledgment = inReader.ReadLine(); + if (acknowledgment != Engine.Signals.Acknowledgment + && !(acknowledgment is null && hostSignal == HostSignal.AfterAll)) // an early EOF, but still valid + { + throw new NotSupportedException($"Unknown Acknowledgment: {acknowledgment}"); + } + } + + public void SendError(string message) + => outWriter.WriteLine($"{ValidationErrorReporter.ConsoleErrorPrefix} {message}"); + + public void ReportResults(RunResults runResults) + => runResults.Print(outWriter); + + public static IHost GetHost(string[] args) + { + for (int i = 0; i < args.Length; i++) + { + if (args[i] == TcpPortDescriptor) + { + int port = int.Parse(args[i + 1]); + var client = new TcpClient(); + client.Connect(IPAddress.Loopback, port); + return new TcpHost(client); + } + } + return new NoAcknowledgementConsoleHost(); + } +} diff --git a/src/BenchmarkDotNet/Engines/WorkloadContinuerAndValueTaskSource.cs b/src/BenchmarkDotNet/Engines/WorkloadContinuerAndValueTaskSource.cs new file mode 100644 index 0000000000..e1749501df --- /dev/null +++ b/src/BenchmarkDotNet/Engines/WorkloadContinuerAndValueTaskSource.cs @@ -0,0 +1,67 @@ +using JetBrains.Annotations; +using Perfolizer.Horology; +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace BenchmarkDotNet.Engines; + +// This is used to prevent allocating a new async state machine on every benchmark iteration. +[UsedImplicitly] +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class WorkloadContinuerAndValueTaskSource : ICriticalNotifyCompletion, IValueTaskSource +{ + private static readonly Action s_completedSentinel = CompletedMethod; + private static void CompletedMethod() => throw new InvalidOperationException(); + + private Action? continuation; + private ManualResetValueTaskSourceCore valueTaskSourceCore; + + public ValueTask Continue() + { + valueTaskSourceCore.Reset(); + var callback = continuation; + continuation = null; + callback?.Invoke(); + return new(this, valueTaskSourceCore.Version); + } + + public void Complete() + { + var callback = continuation; + continuation = s_completedSentinel; + callback?.Invoke(); + } + + public void SetResult(ClockSpan result) + => valueTaskSourceCore.SetResult(result); + + public void SetException(Exception exception) + => valueTaskSourceCore.SetException(exception); + + // Await infrastructure + public WorkloadContinuerAndValueTaskSource GetAwaiter() + => this; + + public void OnCompleted(Action continuation) + => UnsafeOnCompleted(continuation); + + public void UnsafeOnCompleted(Action continuation) + => this.continuation = continuation; + + public bool IsCompleted + => continuation == s_completedSentinel; + + public void GetResult() { } + + ClockSpan IValueTaskSource.GetResult(short token) + => valueTaskSourceCore.GetResult(token); + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) + => valueTaskSourceCore.GetStatus(token); + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + => valueTaskSourceCore.OnCompleted(continuation, state, token, flags); +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs index a5b1839d13..f40dc2b9f9 100644 --- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs @@ -3,6 +3,8 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; namespace BenchmarkDotNet.Extensions @@ -253,5 +255,32 @@ internal static bool IsByRefLike(this Type type) #else => type.IsByRefLike; #endif + + internal static bool IsAwaitable(this Type type) + { + // This does not handle await extension. + var awaiterType = type.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)?.ReturnType; + if (awaiterType is null) + { + return false; + } + if (awaiterType.GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance) is null) + { + return false; + } + var isCompletedProperty = awaiterType.GetProperty(nameof(TaskAwaiter.IsCompleted), BindingFlags.Public | BindingFlags.Instance); + if (isCompletedProperty?.PropertyType != typeof(bool)) + { + return false; + } + return awaiterType.GetInterfaces().Any(type => typeof(INotifyCompletion).IsAssignableFrom(type)); + } + + internal static Attribute? GetAsyncMethodBuilderAttribute(this MemberInfo memberInfo) + // AsyncMethodBuilderAttribute can come from any assembly, so we need to use reflection by name instead of searching for the exact type. + => memberInfo.GetCustomAttributes(false).FirstOrDefault(attr => attr.GetType().FullName == typeof(AsyncMethodBuilderAttribute).FullName) as Attribute; + + internal static bool HasAsyncMethodBuilderAttribute(this MemberInfo memberInfo) + => memberInfo.GetAsyncMethodBuilderAttribute() != null; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Extensions/TaskExtensions.cs b/src/BenchmarkDotNet/Extensions/TaskExtensions.cs new file mode 100644 index 0000000000..a11da51954 --- /dev/null +++ b/src/BenchmarkDotNet/Extensions/TaskExtensions.cs @@ -0,0 +1,95 @@ +#if NETSTANDARD2_0 +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Extensions; + +internal static class TaskExtensions +{ + public static async Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + if (task.IsCompleted) + { + await task; + return; + } + cancellationToken.ThrowIfCancellationRequested(); + + var timeoutTaskSource = new TaskCompletionSource(); + using var _ = cancellationToken.Register(() => timeoutTaskSource.TrySetCanceled(cancellationToken), false); + await await Task.WhenAny(task, timeoutTaskSource.Task); + } + + public static async Task WaitAsync(this Task task, TimeSpan timeout) + { + if (task.IsCompleted) + { + await task; + return; + } + + var timeoutTaskSource = new TaskCompletionSource(); + using var cts = new CancellationTokenSource(timeout); + using var _ = cts.Token.Register(() => timeoutTaskSource.SetException(new TimeoutException()), false); + await await Task.WhenAny(task, timeoutTaskSource.Task); + } + + public static async Task WaitAsync(this Task task, TimeSpan timeout, CancellationToken cancellationToken) + { + if (task.IsCompleted) + { + await task; + return; + } + cancellationToken.ThrowIfCancellationRequested(); + + var timeoutTaskSource = new TaskCompletionSource(); + using var timeoutCts = new CancellationTokenSource(timeout); + using var _ = cancellationToken.Register(() => timeoutTaskSource.TrySetCanceled(cancellationToken), false); + using var __ = timeoutCts.Token.Register(() => timeoutTaskSource.TrySetException(new TimeoutException()), false); + await await Task.WhenAny(task, timeoutTaskSource.Task); + } + + public static async Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + if (task.IsCompleted) + { + return await task; + } + cancellationToken.ThrowIfCancellationRequested(); + + var timeoutTaskSource = new TaskCompletionSource(); + using var _ = cancellationToken.Register(() => timeoutTaskSource.TrySetCanceled(cancellationToken), false); + return await await Task.WhenAny(task, timeoutTaskSource.Task); + } + + public static async Task WaitAsync(this Task task, TimeSpan timeout) + { + if (task.IsCompleted) + { + return await task; + } + + var timeoutTaskSource = new TaskCompletionSource(); + using var cts = new CancellationTokenSource(timeout); + using var _ = cts.Token.Register(() => timeoutTaskSource.SetException(new TimeoutException()), false); + return await await Task.WhenAny(task, timeoutTaskSource.Task); + } + + public static async Task WaitAsync(this Task task, TimeSpan timeout, CancellationToken cancellationToken) + { + if (task.IsCompleted) + { + return await task; + } + cancellationToken.ThrowIfCancellationRequested(); + + var timeoutTaskSource = new TaskCompletionSource(); + using var timeoutCts = new CancellationTokenSource(timeout); + using var _ = cancellationToken.Register(() => timeoutTaskSource.TrySetCanceled(cancellationToken), false); + using var __ = timeoutCts.Token.Register(() => timeoutTaskSource.TrySetException(new TimeoutException()), false); + return await await Task.WhenAny(task, timeoutTaskSource.Task); + } +} +#endif diff --git a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs b/src/BenchmarkDotNet/Helpers/AwaitHelper.cs deleted file mode 100644 index e11e25b015..0000000000 --- a/src/BenchmarkDotNet/Helpers/AwaitHelper.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace BenchmarkDotNet.Helpers -{ - public static class AwaitHelper - { - private class ValueTaskWaiter - { - // We use thread static field so that each thread uses its own individual callback and reset event. - [ThreadStatic] - private static ValueTaskWaiter? ts_current; - internal static ValueTaskWaiter Current => ts_current ??= new ValueTaskWaiter(); - - // We cache the callback to prevent allocations for memory diagnoser. - private readonly Action awaiterCallback; - private readonly ManualResetEventSlim resetEvent; - - private ValueTaskWaiter() - { - resetEvent = new(); - awaiterCallback = resetEvent.Set; - } - - internal void Wait(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion - { - resetEvent.Reset(); - awaiter.UnsafeOnCompleted(awaiterCallback); - - // The fastest way to wait for completion is to spin a bit before waiting on the event. This is the same logic that Task.GetAwaiter().GetResult() uses. - var spinner = new SpinWait(); - while (!resetEvent.IsSet) - { - if (spinner.NextSpinWillYield) - { - resetEvent.Wait(); - return; - } - spinner.SpinOnce(); - } - } - } - - // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way, - // and will eventually throw actual exception, not aggregated one - public static void GetResult(Task task) => task.GetAwaiter().GetResult(); - - public static T GetResult(Task task) => task.GetAwaiter().GetResult(); - - // ValueTask can be backed by an IValueTaskSource that only supports asynchronous awaits, - // so we have to hook up a callback instead of calling .GetAwaiter().GetResult() like we do for Task. - // The alternative is to convert it to Task using .AsTask(), but that causes allocations which we must avoid for memory diagnoser. - public static void GetResult(ValueTask task) - { - // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. - var awaiter = task.ConfigureAwait(false).GetAwaiter(); - if (!awaiter.IsCompleted) - { - ValueTaskWaiter.Current.Wait(awaiter); - } - awaiter.GetResult(); - } - - public static T GetResult(ValueTask task) - { - // Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process. - var awaiter = task.ConfigureAwait(false).GetAwaiter(); - if (!awaiter.IsCompleted) - { - ValueTaskWaiter.Current.Wait(awaiter); - } - return awaiter.GetResult(); - } - - internal static MethodInfo? GetGetResultMethod(Type taskType) - { - if (!taskType.IsGenericType) - { - return typeof(AwaitHelper).GetMethod(nameof(AwaitHelper.GetResult), BindingFlags.Public | BindingFlags.Static, null, new Type[1] { taskType }, null)!; - } - - Type? compareType = taskType.GetGenericTypeDefinition() == typeof(ValueTask<>) ? typeof(ValueTask<>) - : typeof(Task).IsAssignableFrom(taskType.GetGenericTypeDefinition()) ? typeof(Task<>) - : null; - if (compareType == null) - { - return null; - } - var resultType = taskType - .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)! - .ReturnType - .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance)! - .ReturnType; - return typeof(AwaitHelper).GetMethods(BindingFlags.Public | BindingFlags.Static) - .First(m => - { - if (m.Name != nameof(AwaitHelper.GetResult)) return false; - Type paramType = m.GetParameters().First().ParameterType; - return paramType.IsGenericType && paramType.GetGenericTypeDefinition() == compareType; - }) - .MakeGenericMethod(new[] { resultType }); - } - } -} diff --git a/src/BenchmarkDotNet/Helpers/CancelableStreamReader.cs b/src/BenchmarkDotNet/Helpers/CancelableStreamReader.cs new file mode 100644 index 0000000000..16ccab7561 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/CancelableStreamReader.cs @@ -0,0 +1,369 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Helpers; + +internal sealed class CancelableStreamReader : IDisposable +{ +#if NET7_0_OR_GREATER + private readonly StreamReader _defaultReader; + + public CancelableStreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize = -1, bool leaveOpen = false) + => _defaultReader = new(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, leaveOpen); + + public ValueTask ReadLineAsync(CancellationToken cancellationToken) + => _defaultReader.ReadLineAsync(cancellationToken); + + public void Dispose() + => _defaultReader.Dispose(); +#else + // Impl copied from https://github.com/dotnet/runtime/blob/407f9f1476709c3e5aea25511b330e5c1df13fb8/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs + // slightly adjusted to work in netstandard2.0. + + private const int DefaultBufferSize = 1024; + private const int MinBufferSize = 128; + + private readonly Stream _stream; + private Encoding _encoding; + private Decoder _decoder; + private readonly byte[] _byteBuffer; + private char[] _charBuffer; + private int _charPos; + private int _charLen; + private int _byteLen; + private int _bytePos; + private int _maxCharsPerBuffer; + private bool _disposed; + private bool _detectEncoding; + private bool _checkPreamble; + private readonly bool _closable; + + public CancelableStreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize = -1, bool leaveOpen = false) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (!stream.CanRead) throw new ArgumentException("Stream is not readable", nameof(stream)); + + if (bufferSize == -1) + { + bufferSize = DefaultBufferSize; + } + if (bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize)); + + _stream = stream; + _encoding = encoding ??= Encoding.UTF8; + _decoder = encoding.GetDecoder(); + if (bufferSize < MinBufferSize) + { + bufferSize = MinBufferSize; + } + + _byteBuffer = new byte[bufferSize]; + _maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize); + _charBuffer = new char[_maxCharsPerBuffer]; + _detectEncoding = detectEncodingFromByteOrderMarks; + + int preambleLength = encoding.GetPreamble().Length; + _checkPreamble = preambleLength > 0 && preambleLength <= bufferSize; + + _closable = !leaveOpen; + } + + ~CancelableStreamReader() + => Dispose(false); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + _disposed = true; + + if (_closable) + { + try + { + if (disposing) + { + _stream.Close(); + } + } + finally + { + _charPos = 0; + _charLen = 0; + } + } + } + + public async ValueTask ReadLineAsync(CancellationToken cancellationToken) + { + if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0) + { + return null; + } + + string retVal; + char[]? arrayPoolBuffer = null; + int arrayPoolBufferPos = 0; + + do + { + char[] charBuffer = _charBuffer; + int charLen = _charLen; + int charPos = _charPos; + + Debug.Assert(charPos < charLen, "ReadBuffer returned > 0 but didn't bump _charLen?"); + + int idxOfNewline = charBuffer.AsSpan(charPos, charLen - charPos).IndexOfAny('\r', '\n'); + if (idxOfNewline >= 0) + { + if (arrayPoolBuffer is null) + { + retVal = new string(charBuffer, charPos, idxOfNewline); + } + else + { + retVal = new string(arrayPoolBuffer, 0, arrayPoolBufferPos) + new string(charBuffer, charPos, idxOfNewline); + ArrayPool.Shared.Return(arrayPoolBuffer); + } + + charPos += idxOfNewline; + char matchedChar = charBuffer[charPos++]; + _charPos = charPos; + + if (matchedChar == '\r') + { + if (charPos < charLen || (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) > 0) + { + if (_charBuffer[_charPos] == '\n') + { + _charPos++; + } + } + } + + return retVal; + } + + if (arrayPoolBuffer is null) + { + arrayPoolBuffer = ArrayPool.Shared.Rent(charLen - charPos + 80); + } + else if ((arrayPoolBuffer.Length - arrayPoolBufferPos) < (charLen - charPos)) + { + char[] newBuffer = ArrayPool.Shared.Rent(checked(arrayPoolBufferPos + charLen - charPos)); + arrayPoolBuffer.AsSpan(0, arrayPoolBufferPos).CopyTo(newBuffer); + ArrayPool.Shared.Return(arrayPoolBuffer); + arrayPoolBuffer = newBuffer; + } + charBuffer.AsSpan(charPos, charLen - charPos).CopyTo(arrayPoolBuffer.AsSpan(arrayPoolBufferPos)); + arrayPoolBufferPos += charLen - charPos; + } + while (await ReadBufferAsync(cancellationToken).ConfigureAwait(false) > 0); + + if (arrayPoolBuffer is not null) + { + retVal = new string(arrayPoolBuffer, 0, arrayPoolBufferPos); + ArrayPool.Shared.Return(arrayPoolBuffer); + } + else + { + retVal = string.Empty; + } + + return retVal; + } + + private async ValueTask ReadBufferAsync(CancellationToken cancellationToken) + { + _charLen = 0; + _charPos = 0; + byte[] tmpByteBuffer = _byteBuffer; + Stream tmpStream = _stream; + + if (!_checkPreamble) + { + _byteLen = 0; + } + + bool eofReached = false; + + do + { + if (_checkPreamble) + { + Debug.Assert(_bytePos <= _encoding.GetPreamble().Length, "possible bug in _compressPreamble. Are two threads using this StreamReader at the same time?"); + int tmpBytePos = _bytePos; + int len = await tmpStream.ReadAsync(tmpByteBuffer, tmpBytePos, tmpByteBuffer.Length - tmpBytePos, cancellationToken).ConfigureAwait(false); + Debug.Assert(len >= 0, "Stream.Read returned a negative number! This is a bug in your stream class."); + + if (len == 0) + { + eofReached = true; + break; + } + + _byteLen += len; + } + else + { + Debug.Assert(_bytePos == 0, "_bytePos can be non zero only when we are trying to _checkPreamble. Are two threads using this StreamReader at the same time?"); + _byteLen = await tmpStream.ReadAsync(tmpByteBuffer, 0, tmpByteBuffer.Length, cancellationToken).ConfigureAwait(false); + Debug.Assert(_byteLen >= 0, "Stream.Read returned a negative number! Bug in stream class."); + + if (_byteLen == 0) + { + eofReached = true; + break; + } + } + + if (IsPreamble()) + { + continue; + } + + if (_detectEncoding && _byteLen >= 2) + { + DetectEncoding(); + } + + Debug.Assert(_charPos == 0 && _charLen == 0, "We shouldn't be trying to decode more data if we made progress in an earlier iteration."); + _charLen = _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, 0, flush: false); + } while (_charLen == 0); + + if (eofReached) + { + Debug.Assert(_charPos == 0 && _charLen == 0, "We shouldn't be looking for EOF unless we have an empty char buffer."); + _charLen = _decoder.GetChars(_byteBuffer, 0, _byteLen, _charBuffer, 0, flush: true); + _bytePos = 0; + _byteLen = 0; + } + + return _charLen; + } + + private bool IsPreamble() + { + if (!_checkPreamble) + { + return false; + } + + return IsPreambleWorker(); + + bool IsPreambleWorker() + { + Debug.Assert(_checkPreamble); + ReadOnlySpan preamble = _encoding.GetPreamble(); + + Debug.Assert(_bytePos < preamble.Length, "_compressPreamble was called with the current bytePos greater than the preamble buffer length. Are two threads using this StreamReader at the same time?"); + int len = Math.Min(_byteLen, preamble.Length); + + for (int i = _bytePos; i < len; i++) + { + if (_byteBuffer[i] != preamble[i]) + { + _bytePos = 0; + _checkPreamble = false; + return false; + } + } + _bytePos = len; + + Debug.Assert(_bytePos <= preamble.Length, "possible bug in _compressPreamble. Are two threads using this StreamReader at the same time?"); + + if (_bytePos == preamble.Length) + { + CompressBuffer(preamble.Length); + _bytePos = 0; + _checkPreamble = false; + _detectEncoding = false; + } + + return _checkPreamble; + } + } + + private void DetectEncoding() + { + Debug.Assert(_byteLen >= 2, "Caller should've validated that at least 2 bytes were available."); + + byte[] byteBuffer = _byteBuffer; + _detectEncoding = false; + bool changedEncoding = false; + + ushort firstTwoBytes = BinaryPrimitives.ReadUInt16LittleEndian(byteBuffer); + if (firstTwoBytes == 0xFFFE) + { + _encoding = Encoding.BigEndianUnicode; + CompressBuffer(2); + changedEncoding = true; + } + else if (firstTwoBytes == 0xFEFF) + { + if (_byteLen < 4 || byteBuffer[2] != 0 || byteBuffer[3] != 0) + { + _encoding = Encoding.Unicode; + CompressBuffer(2); + changedEncoding = true; + } + else + { + _encoding = Encoding.UTF32; + CompressBuffer(4); + changedEncoding = true; + } + } + else if (_byteLen >= 3 && firstTwoBytes == 0xBBEF && byteBuffer[2] == 0xBF) + { + _encoding = Encoding.UTF8; + CompressBuffer(3); + changedEncoding = true; + } + else if (_byteLen >= 4 && firstTwoBytes == 0 && byteBuffer[2] == 0xFE && byteBuffer[3] == 0xFF) + { + _encoding = new UTF32Encoding(bigEndian: true, byteOrderMark: true); + CompressBuffer(4); + changedEncoding = true; + } + else if (_byteLen == 2) + { + _detectEncoding = true; + } + + if (changedEncoding) + { + _decoder = _encoding.GetDecoder(); + int newMaxCharsPerBuffer = _encoding.GetMaxCharCount(byteBuffer.Length); + if (newMaxCharsPerBuffer > _maxCharsPerBuffer) + { + _charBuffer = new char[newMaxCharsPerBuffer]; + } + _maxCharsPerBuffer = newMaxCharsPerBuffer; + } + } + + private void CompressBuffer(int n) + { + Debug.Assert(_byteLen >= n, "CompressBuffer was called with a number of bytes greater than the current buffer length. Are two threads using this StreamReader at the same time?"); + byte[] byteBuffer = _byteBuffer; + _ = byteBuffer.Length; + new ReadOnlySpan(byteBuffer, n, _byteLen - n).CopyTo(byteBuffer); + _byteLen -= n; + } +#endif +} diff --git a/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs b/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs new file mode 100644 index 0000000000..98997e03af --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Extensions; +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Helpers; + +internal static class DynamicAwaitHelper +{ + internal static async ValueTask<(bool hasResult, object? result)> GetOrAwaitResult(object? value) + { + var valueType = value?.GetType(); + if (valueType?.IsAwaitable() != true) + { + return (true, value); + } + + var getAwaiterMethod = valueType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)!; + var awaiterType = getAwaiterMethod.ReturnType; + var getResultMethod = awaiterType.GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance)!; + var result = await new DynamicAwaitable(getAwaiterMethod, awaiterType, getResultMethod, value!); + return (getResultMethod.ReturnType != typeof(void), result); + } + + private readonly struct DynamicAwaitable(MethodInfo getAwaiterMethod, Type awaiterType, MethodInfo getResultMethod, object awaitable) + { + public DynamicAwaiter GetAwaiter() + => new(awaiterType, getResultMethod, getAwaiterMethod.Invoke(awaitable, null)); + } + + private readonly struct DynamicAwaiter(Type awaiterType, MethodInfo getResultMethod, object? awaiter) : ICriticalNotifyCompletion + { + public bool IsCompleted + => awaiterType.GetProperty(nameof(TaskAwaiter.IsCompleted), BindingFlags.Public | BindingFlags.Instance)!.GetMethod!.Invoke(awaiter, null) is true; + + public object? GetResult() + => getResultMethod.Invoke(awaiter, null); + + public void OnCompleted(Action continuation) + => OnCompletedCore(typeof(INotifyCompletion), nameof(INotifyCompletion.OnCompleted), continuation); + + public void UnsafeOnCompleted(Action continuation) + => OnCompletedCore(typeof(ICriticalNotifyCompletion), nameof(ICriticalNotifyCompletion.UnsafeOnCompleted), continuation); + + private void OnCompletedCore(Type interfaceType, string methodName, Action continuation) + { + var onCompletedMethod = interfaceType.GetMethod(methodName); + var map = awaiterType.GetInterfaceMap(interfaceType); + + for (int i = 0; i < map.InterfaceMethods.Length; i++) + { + if (map.InterfaceMethods[i] == onCompletedMethod) + { + map.TargetMethods[i].Invoke(awaiter, [continuation]); + return; + } + } + } + } +} diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorDefaultValueExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorDefaultValueExtensions.cs new file mode 100644 index 0000000000..2b2dcc3f07 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorDefaultValueExtensions.cs @@ -0,0 +1,152 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace BenchmarkDotNet.Helpers.Reflection.Emit; + +internal static class IlGeneratorDefaultValueExtensions +{ + public static void EmitSetFieldToDefault(this ILGenerator ilBuilder, FieldInfo fieldInfo) + { + var resultType = fieldInfo.FieldType; + switch (resultType) + { + case null: + case Type t when t == typeof(void): + throw new ArgumentException(); + case Type t when t.IsValueType: + ilBuilder.Emit(OpCodes.Ldflda, fieldInfo); + ilBuilder.Emit(OpCodes.Initobj, resultType); + break; + default: + ilBuilder.Emit(OpCodes.Ldnull); + ilBuilder.Emit(OpCodes.Stfld, fieldInfo); + break; + } + } + + public static void MaybeEmitSetLocalToDefault(this ILGenerator ilBuilder, LocalBuilder? optionalLocal) + { + var resultType = optionalLocal?.LocalType; + switch (resultType) + { + case null: + case Type t when t == typeof(void): + break; + case Type t when t.IsClass || t.IsInterface: + ilBuilder.Emit(OpCodes.Ldnull); + ilBuilder.EmitStloc(optionalLocal!); + break; + case Type t when t.UseInitObjForInitLocal(): + EmitInitObj(ilBuilder, resultType, optionalLocal!); + break; + default: + EmitLoadDefaultPrimitive(ilBuilder, resultType); + ilBuilder.EmitStloc(optionalLocal!); + break; + } + } + + private static bool IsInitLocalPrimitive(this Type t) + { + // var x = default(T): + // C# compiler uses special logic for enum defaults and primitive defaults + // On init local case this logic does not apply for IntPtr & UIntPtr. + + if (t == typeof(void)) + return true; + + if (t.IsEnum) + return true; + + return t.IsPrimitive + && t != typeof(IntPtr) + && t != typeof(UIntPtr); + } + + private static bool UseInitObjForInitLocal(this Type resultType) + { + return resultType.IsValueType && !resultType.IsInitLocalPrimitive(); + } + + private static void EmitInitObj(ILGenerator ilBuilder, Type resultType, LocalBuilder optionalLocalForInitobj) + { + if (optionalLocalForInitobj == null) + throw new ArgumentNullException(nameof(optionalLocalForInitobj)); + + /* + IL_0000: ldloca.s 0 + IL_0002: initobj [mscorlib]System.DateTime + */ + ilBuilder.EmitLdloca(optionalLocalForInitobj); + ilBuilder.Emit(OpCodes.Initobj, resultType); + } + + private static void EmitLoadDefaultPrimitive(this ILGenerator ilBuilder, Type resultType) + { + var valueType = resultType; + if (valueType.IsEnum) + valueType = resultType.GetEnumUnderlyingType(); + + // The primitive types are Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single + // + custom logic for decimal + switch (valueType) + { + case Type t0 when t0 == typeof(bool): + case Type t1 when t1 == typeof(byte): + case Type t2 when t2 == typeof(sbyte): + case Type t3 when t3 == typeof(short): + case Type t4 when t4 == typeof(ushort): + case Type t5 when t5 == typeof(int): + case Type t6 when t6 == typeof(uint): + case Type t7 when t7 == typeof(char): + ilBuilder.Emit(OpCodes.Ldc_I4_0); + break; + case Type t1 when t1 == typeof(ulong): + case Type t2 when t2 == typeof(long): + /* + // return 0L; + IL_0000: ldc.i4.0 + IL_0001: conv.i8 + // return 0uL; + IL_0000: ldc.i4.0 + IL_0001: conv.i8 + */ + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Conv_I8); + break; + case Type t when t == typeof(IntPtr): + /* + IL_0000: ldc.i4.0 + IL_0001: conv.i + */ + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Conv_I); + break; + case Type t when t == typeof(UIntPtr): + /* + IL_0000: ldc.i4.0 + IL_0001: conv.u + */ + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Conv_U); + break; + case Type t when t == typeof(double): + ilBuilder.Emit(OpCodes.Ldc_R8, 0.0d); + break; + case Type t when t == typeof(float): + ilBuilder.Emit(OpCodes.Ldc_R4, 0.0f); + break; + case Type t when t == typeof(decimal): + /* + // return decimal.Zero; + IL_0011: ldsfld valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::Zero + */ + var zeroField = typeof(decimal).GetField(nameof(decimal.Zero))!; + ilBuilder.Emit(OpCodes.Ldsfld, zeroField); + break; + default: + throw new NotSupportedException($"Cannot emit default for {resultType}."); + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorEmitOpExtensions.cs b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorEmitOpExtensions.cs index 7763d59e91..1611e0010a 100644 --- a/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorEmitOpExtensions.cs +++ b/src/BenchmarkDotNet/Helpers/Reflection.Emit/IlGeneratorEmitOpExtensions.cs @@ -131,5 +131,48 @@ public static void EmitStarg(this ILGenerator ilBuilder, ParameterInfo argument) ilBuilder.Emit(OpCodes.Starg, checked((short) position)); } } + + public static void EmitLdc_I4(this ILGenerator ilBuilder, int value) + { + switch (value) + { + case -1: + ilBuilder.Emit(OpCodes.Ldc_I4_M1); + break; + case 0: + ilBuilder.Emit(OpCodes.Ldc_I4_0); + break; + case 1: + ilBuilder.Emit(OpCodes.Ldc_I4_1); + break; + case 2: + ilBuilder.Emit(OpCodes.Ldc_I4_2); + break; + case 3: + ilBuilder.Emit(OpCodes.Ldc_I4_3); + break; + case 4: + ilBuilder.Emit(OpCodes.Ldc_I4_4); + break; + case 5: + ilBuilder.Emit(OpCodes.Ldc_I4_5); + break; + case 6: + ilBuilder.Emit(OpCodes.Ldc_I4_6); + break; + case 7: + ilBuilder.Emit(OpCodes.Ldc_I4_7); + break; + case 8: + ilBuilder.Emit(OpCodes.Ldc_I4_8); + break; + case var i when sbyte.MinValue <= i && i <= sbyte.MaxValue: + ilBuilder.Emit(OpCodes.Ldc_I4_S, (sbyte) value); + break; + default: + ilBuilder.Emit(OpCodes.Ldc_I4, value); + break; + } + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Loggers/Broker.cs b/src/BenchmarkDotNet/Loggers/Broker.cs index 08c4cea717..a673df6e43 100644 --- a/src/BenchmarkDotNet/Loggers/Broker.cs +++ b/src/BenchmarkDotNet/Loggers/Broker.cs @@ -2,10 +2,14 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.IO.Pipes; +using System.Net.Sockets; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Running; namespace BenchmarkDotNet.Loggers @@ -15,24 +19,25 @@ internal class Broker : IDisposable private readonly ILogger logger; private readonly Process process; private readonly CompositeInProcessDiagnoser compositeInProcessDiagnoser; - private readonly AnonymousPipeServerStream inputFromBenchmark, acknowledgments; + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly TcpListener tcpListener; private enum Result { Success, EndOfStream, InvalidData, + EarlyProcessExit, } public Broker(ILogger logger, Process process, IDiagnoser? diagnoser, CompositeInProcessDiagnoser compositeInProcessDiagnoser, - BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, AnonymousPipeServerStream inputFromBenchmark, AnonymousPipeServerStream acknowledgments) + BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, TcpListener tcpListener) { this.logger = logger; this.process = process; this.Diagnoser = diagnoser; this.compositeInProcessDiagnoser = compositeInProcessDiagnoser; - this.inputFromBenchmark = inputFromBenchmark; - this.acknowledgments = acknowledgments; + this.tcpListener = tcpListener; DiagnoserActionParameters = new DiagnoserActionParameters(process, benchmarkCase, benchmarkId); process.EnableRaisingEvents = true; @@ -51,119 +56,136 @@ public void Dispose() { process.Exited -= OnProcessExited; - // Dispose all the pipes to let reading from pipe finish with EOF and avoid a resource leak. - DisposeLocalCopyOfClientHandles(); - inputFromBenchmark.Dispose(); - acknowledgments.Dispose(); + cancellationTokenSource.Cancel(throwOnFirstException: false); } private void OnProcessExited(object? sender, EventArgs e) - { - DisposeLocalCopyOfClientHandles(); - } - - private void DisposeLocalCopyOfClientHandles() - { - inputFromBenchmark.DisposeLocalCopyOfClientHandle(); - acknowledgments.DisposeLocalCopyOfClientHandle(); - } + => Dispose(); - internal void ProcessData() + internal async ValueTask ProcessData() { - // When the process fails to start, there is no pipe to read from. - // If we try to read from such pipe, the read blocks and BDN hangs. - // We can't use async methods with cancellation tokens because Anonymous Pipes don't support async IO. - - // Usually, this property is not set yet. - if (process.HasExited) - return; - - var result = ProcessDataBlocking(); + var result = await ProcessDataCore(); if (result != Result.Success) + { logger.WriteLineError($"ProcessData operation is interrupted by {result}."); + } } - private Result ProcessDataBlocking() + private async ValueTask ProcessDataCore() { - using StreamReader reader = new(inputFromBenchmark, AnonymousPipesHost.UTF8NoBOM, detectEncodingFromByteOrderMarks: false); - using StreamWriter writer = new(acknowledgments, AnonymousPipesHost.UTF8NoBOM, bufferSize: 1); - // Flush the data to the Stream after each write, otherwise the client will wait for input endlessly! - writer.AutoFlush = true; + if (process.HasExited || cancellationTokenSource.IsCancellationRequested) + return Result.EarlyProcessExit; - while (true) + try { - var line = reader.ReadLine(); - if (line == null) - return Result.EndOfStream; - - // TODO: implement Silent mode here - logger.WriteLine(LogKind.Default, line); - - // Handle normal log. - if (!line.StartsWith("//")) +#if NET6_0_OR_GREATER + TcpClient client; + using var timeoutCts = new CancellationTokenSource(TcpHost.ConnectionTimeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, timeoutCts.Token); + try + { + client = await tcpListener.AcceptTcpClientAsync(linkedCts.Token); + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) { - Results.Add(line); - continue; + throw new TimeoutException($"The connection to the benchmark process timed out after {TcpHost.ConnectionTimeout}."); } + using var _ = client; +#else + using var client = await tcpListener.AcceptTcpClientAsync().WaitAsync(TcpHost.ConnectionTimeout, cancellationTokenSource.Token); +#endif + using var stream = client.GetStream(); + using CancelableStreamReader reader = new(stream, TcpHost.UTF8NoBOM, detectEncodingFromByteOrderMarks: false); + // Flush the data to the Stream after each write, otherwise the client will wait for input endlessly! + using StreamWriter writer = new(stream, TcpHost.UTF8NoBOM, bufferSize: 1) { AutoFlush = true }; + + while (true) + { + var line = await reader.ReadLineAsync(cancellationTokenSource.Token); - // Keep in sync with WasmExecutor and InProcessHost. + if (line == null) + return Result.EndOfStream; - // Handle line prefixed with "// InProcessDiagnoser " - if (line.StartsWith(CompositeInProcessDiagnoser.HeaderKey)) - { - // Something like "// InProcessDiagnoser 0 1" - string[] lineItems = line.Split(' '); - int diagnoserIndex = int.Parse(lineItems[2]); - int resultsLinesCount = int.Parse(lineItems[3]); - var resultsStringBuilder = new StringBuilder(); - for (int i = 0; i < resultsLinesCount;) + // TODO: implement Silent mode here + logger.WriteLine(LogKind.Default, line); + + // Handle normal log. + if (!line.StartsWith("//")) { - line = reader.ReadLine(); - if (line == null) - return Result.EndOfStream; + Results.Add(line); + continue; + } - if (!line.StartsWith($"{CompositeInProcessDiagnoser.ResultsKey} ")) - return Result.InvalidData; + // Keep in sync with WasmExecutor and InProcessHost. - // Strip the prepended "// InProcessDiagnoserResults ". - line = line.Substring(CompositeInProcessDiagnoser.ResultsKey.Length + 1); - resultsStringBuilder.Append(line); - if (++i < resultsLinesCount) + // Handle line prefixed with "// InProcessDiagnoser " + if (line.StartsWith(CompositeInProcessDiagnoser.HeaderKey)) + { + // Something like "// InProcessDiagnoser 0 1" + string[] lineItems = line.Split(' '); + int diagnoserIndex = int.Parse(lineItems[2]); + int resultsLinesCount = int.Parse(lineItems[3]); + var resultsStringBuilder = new StringBuilder(); + for (int i = 0; i < resultsLinesCount;) { - resultsStringBuilder.AppendLine(); - } - } - compositeInProcessDiagnoser.DeserializeResults(diagnoserIndex, DiagnoserActionParameters.BenchmarkCase, resultsStringBuilder.ToString()); - continue; - } + line = await reader.ReadLineAsync(cancellationTokenSource.Token); - // Handle HostSignal data - if (Engine.Signals.TryGetSignal(line, out var signal)) - { - Diagnoser?.Handle(signal, DiagnoserActionParameters); + if (line == null) + return Result.EndOfStream; - writer.WriteLine(Engine.Signals.Acknowledgment); + if (!line.StartsWith($"{CompositeInProcessDiagnoser.ResultsKey} ")) + return Result.InvalidData; - if (signal == HostSignal.BeforeAnythingElse) - { - // The client has connected, we no longer need to keep the local copy of client handle alive. - // This allows server to detect that child process is done and hence avoid resource leak. - // Full explanation: https://stackoverflow.com/a/39700027 - DisposeLocalCopyOfClientHandles(); + // Strip the prepended "// InProcessDiagnoserResults ". + line = line.Substring(CompositeInProcessDiagnoser.ResultsKey.Length + 1); + resultsStringBuilder.Append(line); + if (++i < resultsLinesCount) + { + resultsStringBuilder.AppendLine(); + } + } + compositeInProcessDiagnoser.DeserializeResults(diagnoserIndex, DiagnoserActionParameters.BenchmarkCase, resultsStringBuilder.ToString()); + continue; } - else if (signal == HostSignal.AfterAll) + + // Handle HostSignal data + if (Engine.Signals.TryGetSignal(line, out var signal)) { - // we have received the last signal so we can stop reading from the pipe - // if the process won't exit after this, its hung and needs to be killed - return Result.Success; + Diagnoser?.Handle(signal, DiagnoserActionParameters); + + writer.WriteLine(Engine.Signals.Acknowledgment); + + if (signal == HostSignal.AfterAll) + { + // we have received the last signal so we can stop reading from the pipe + // if the process won't exit after this, its hung and needs to be killed + return Result.Success; + } + + continue; } - continue; + // Other line that have "//" prefix. + PrefixedOutput.Add(line); } - - // Other line that have "//" prefix. - PrefixedOutput.Add(line); } + catch (IOException e) when (e.InnerException is SocketException se && IsEarlyExitCode(se.SocketErrorCode)) + { + return Result.EarlyProcessExit; + } + catch (SocketException e) when (IsEarlyExitCode(e.SocketErrorCode)) + { + return Result.EarlyProcessExit; + } + catch (OperationCanceledException) + { + return Result.EarlyProcessExit; + } + + static bool IsEarlyExitCode(SocketError error) + => error is SocketError.ConnectionReset + or SocketError.Shutdown + or SocketError.OperationAborted; } } } diff --git a/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs b/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs index af0d274ac9..073427fd73 100644 --- a/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs +++ b/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs @@ -94,10 +94,11 @@ public SmartArgument(ParameterDefinition[] parameterDefinitions, object value, M public string ToSourceCode() { Type paramType = parameterDefinitions[argumentIndex].ParameterType; - bool isParamRefLike = RunnableReflectionHelpers.IsRefLikeType(paramType); - string cast = isParamRefLike ? $"({Value.GetType().GetCorrectCSharpTypeName()})" - : $"({paramType.GetCorrectCSharpTypeName()})"; // it's an object so we need to cast it to the right type + // it's an object so we need to cast it to the right type + string cast = paramType.IsByRefLike() + ? $"({Value.GetType().GetCorrectCSharpTypeName()})" + : $"({paramType.GetCorrectCSharpTypeName()})"; string callPostfix = source is PropertyInfo ? string.Empty : "()"; diff --git a/src/BenchmarkDotNet/Running/BenchmarkCase.cs b/src/BenchmarkDotNet/Running/BenchmarkCase.cs index f6ca11a456..08a6ac14e3 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkCase.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkCase.cs @@ -30,7 +30,7 @@ internal BenchmarkCase(Descriptor descriptor, Job job, ParameterInstances parame public Runtime GetRuntime() => Job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic) ? Job.Environment.Runtime! - : RuntimeInformation.GetTargetOrCurrentRuntime(Descriptor.WorkloadMethod.DeclaringType!.Assembly); + : RuntimeInformation.GetTargetOrCurrentRuntime(Descriptor.Type.Assembly); public void Dispose() => Parameters.Dispose(); diff --git a/src/BenchmarkDotNet/Running/BenchmarkId.cs b/src/BenchmarkDotNet/Running/BenchmarkId.cs index abb16c8924..4dd4ca3c27 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkId.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkId.cs @@ -33,8 +33,8 @@ public BenchmarkId(int value, BenchmarkCase benchmarkCase) public string ToArguments(Diagnosers.RunMode diagnoserRunMode) => $"--benchmarkName {FullBenchmarkName.EscapeCommandLine()} --job {JobId.EscapeCommandLine()} --diagnoserRunMode {(int) diagnoserRunMode} --benchmarkId {Value}"; - public string ToArguments(string fromBenchmark, string toBenchmark, Diagnosers.RunMode diagnoserRunMode) - => $"{AnonymousPipesHost.AnonymousPipesDescriptors} {fromBenchmark} {toBenchmark} {ToArguments(diagnoserRunMode)}"; + public string ToArguments(int port, Diagnosers.RunMode diagnoserRunMode) + => $"{TcpHost.TcpPortDescriptor} {port} {ToArguments(diagnoserRunMode)}"; public override string ToString() => Value.ToString(); diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index a49a746898..f1d7633a86 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Columns; @@ -36,7 +37,7 @@ internal static class BenchmarkRunnerClean internal static readonly IResolver DefaultResolver = new CompositeResolver(EnvironmentResolver.Instance, InfrastructureResolver.Instance); - internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) + internal static async ValueTask Run(BenchmarkRunInfo[] benchmarkRunInfos) { using var taskbarProgress = new TaskbarProgress(TaskbarProgressState.Indeterminate); @@ -62,9 +63,9 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) compositeLogger.WriteLineInfo("// Validating benchmarks:"); - var (supportedBenchmarks, validationErrors) = GetSupportedBenchmarks(benchmarkRunInfos, resolver); + var (supportedBenchmarks, validationErrors) = await GetSupportedBenchmarks(benchmarkRunInfos, resolver); - validationErrors.AddRange(Validate(supportedBenchmarks)); + validationErrors.AddRange(await Validate(supportedBenchmarks)); foreach (var validationError in validationErrors) eventProcessor.OnValidationError(validationError); @@ -74,7 +75,7 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) eventProcessor.OnEndValidationStage(); // Ensure that OnEndValidationStage() is called when a critical validation error exists. if (validationErrors.Any(validationError => validationError.IsCritical)) - return new[] { Summary.ValidationFailed(title, resultsFolderPath, logFilePath, validationErrors.ToImmutableArray()) }; + return [Summary.ValidationFailed(title, resultsFolderPath, logFilePath, [..validationErrors])]; int totalBenchmarkCount = supportedBenchmarks.Sum(benchmarkInfo => benchmarkInfo.BenchmarksCases.Length); int benchmarksToRunCount = totalBenchmarkCount - (idToResume + 1); // ids are indexed from 0 @@ -143,8 +144,8 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) } eventProcessor.OnStartRunBenchmarksInType(benchmarkRunInfo.Type, benchmarkRunInfo.BenchmarksCases); - var summary = Run(benchmarkRunInfo, benchmarkToBuildResult, resolver, compositeLogger, eventProcessor, artifactsToCleanup, - resultsFolderPath, logFilePath, totalBenchmarkCount, in runsChronometer, ref benchmarksToRunCount, + (var summary, benchmarksToRunCount) = await Run(benchmarkRunInfo, benchmarkToBuildResult, resolver, compositeLogger, eventProcessor, artifactsToCleanup, + resultsFolderPath, logFilePath, totalBenchmarkCount, runsChronometer, benchmarksToRunCount, taskbarProgress); eventProcessor.OnEndRunBenchmarksInType(benchmarkRunInfo.Type, summary); @@ -196,18 +197,18 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) } } - private static Summary Run(BenchmarkRunInfo benchmarkRunInfo, - Dictionary buildResults, - IResolver resolver, - ILogger logger, - EventProcessor eventProcessor, - List artifactsToCleanup, - string resultsFolderPath, - string logFilePath, - int totalBenchmarkCount, - in StartedClock runsChronometer, - ref int benchmarksToRunCount, - TaskbarProgress taskbarProgress) + private static async ValueTask<(Summary summary, int benchmarksToRunCount)> Run(BenchmarkRunInfo benchmarkRunInfo, + Dictionary buildResults, + IResolver resolver, + ILogger logger, + EventProcessor eventProcessor, + List artifactsToCleanup, + string resultsFolderPath, + string logFilePath, + int totalBenchmarkCount, + StartedClock runsChronometer, + int benchmarksToRunCount, + TaskbarProgress taskbarProgress) { var runStart = runsChronometer.GetElapsed(); @@ -216,7 +217,7 @@ private static Summary Run(BenchmarkRunInfo benchmarkRunInfo, var config = benchmarkRunInfo.Config; var cultureInfo = config.CultureInfo ?? DefaultCultureInfo.Instance; var reports = new List(); - string title = GetTitle(new[] { benchmarkRunInfo }); + string title = GetTitle([benchmarkRunInfo]); using var consoleTitler = new ConsoleTitler($"{benchmarksToRunCount}/{totalBenchmarkCount} Remaining"); logger.WriteLineInfo($"// Found {benchmarks.Length} benchmarks:"); @@ -244,7 +245,7 @@ private static Summary Run(BenchmarkRunInfo benchmarkRunInfo, artifactsToCleanup.AddRange(buildResult.ArtifactsToCleanup); eventProcessor.OnStartRunBenchmark(benchmark); - var report = RunCore(benchmark, info.benchmarkId, logger, resolver, buildResult, benchmarkRunInfo.CompositeInProcessDiagnoser); + var report = await RunCore(benchmark, info.benchmarkId, logger, resolver, buildResult, benchmarkRunInfo.CompositeInProcessDiagnoser); eventProcessor.OnEndRunBenchmark(benchmark, report); if (report.AllMeasurements.Any(m => m.Operations == 0)) @@ -297,15 +298,20 @@ private static Summary Run(BenchmarkRunInfo benchmarkRunInfo, var runEnd = runsChronometer.GetElapsed(); - return new Summary(title, - reports.ToImmutableArray(), - HostEnvironmentInfo.GetCurrent(), - resultsFolderPath, - logFilePath, - runEnd.GetTimeSpan() - runStart.GetTimeSpan(), - cultureInfo, - Validate(benchmarkRunInfo), // validate them once again, but don't print the output - config.GetColumnHidingRules().ToImmutableArray()); + return ( + new Summary( + title, + [.. reports], + HostEnvironmentInfo.GetCurrent(), + resultsFolderPath, + logFilePath, + runEnd.GetTimeSpan() - runStart.GetTimeSpan(), + cultureInfo, + [.. await Validate(benchmarkRunInfo)], // validate them once again, but don't print the output + [.. config.GetColumnHidingRules()] + ), + benchmarksToRunCount + ); } private static void PrintSummary(ILogger logger, ImmutableConfig config, Summary summary) @@ -374,15 +380,11 @@ private static void PrintSummary(ILogger logger, ImmutableConfig config, Summary logger.WriteLineHeader("// ***** BenchmarkRunner: End *****"); } - private static ImmutableArray Validate(params BenchmarkRunInfo[] benchmarks) - { - var validationErrors = new List(); - - foreach (var benchmarkRunInfo in benchmarks) - validationErrors.AddRange(benchmarkRunInfo.Config.GetCompositeValidator().Validate(new ValidationParameters(benchmarkRunInfo.BenchmarksCases, benchmarkRunInfo.Config))); - - return validationErrors.ToImmutableArray(); - } + private static async ValueTask> Validate(params BenchmarkRunInfo[] benchmarks) + => await benchmarks + .ToAsyncEnumerable() + .SelectMany(benchmark => benchmark.Config.GetCompositeValidator().ValidateAsync(new ValidationParameters(benchmark.BenchmarksCases, benchmark.Config))) + .ToArrayAsync(); private static Dictionary BuildInParallel(ILogger logger, string rootArtifactsFolderPath, BuildPartition[] buildPartitions, in StartedClock globalChronometer, EventProcessor eventProcessor) { @@ -480,7 +482,7 @@ private static BuildResult Build(BuildPartition buildPartition, string rootArtif } } - private static BenchmarkReport RunCore(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, ILogger logger, IResolver resolver, BuildResult buildResult, + private static async ValueTask RunCore(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, ILogger logger, IResolver resolver, BuildResult buildResult, CompositeInProcessDiagnoser compositeInProcessDiagnoser) { var toolchain = benchmarkCase.GetToolchain(); @@ -488,12 +490,12 @@ private static BenchmarkReport RunCore(BenchmarkCase benchmarkCase, BenchmarkId logger.WriteLineHeader("// **************************"); logger.WriteLineHeader("// Benchmark: " + benchmarkCase.DisplayInfo); - var (success, executeResults, metrics) = Execute(logger, benchmarkCase, benchmarkId, toolchain, buildResult, resolver, compositeInProcessDiagnoser); + var (success, executeResults, metrics) = await Execute(logger, benchmarkCase, benchmarkId, toolchain, buildResult, resolver, compositeInProcessDiagnoser); return new BenchmarkReport(success, benchmarkCase, buildResult, buildResult, executeResults, metrics); } - private static (bool success, List executeResults, List metrics) Execute( + private static async ValueTask<(bool success, List executeResults, List metrics)> Execute( ILogger logger, BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, IToolchain toolchain, BuildResult buildResult, IResolver resolver, CompositeInProcessDiagnoser compositeInProcessDiagnoser) { @@ -520,7 +522,7 @@ private static (bool success, List executeResults, List m // use diagnoser only for the last run (we need single result, not many) bool useDiagnoser = launchIndex == launchCount && noOverheadCompositeDiagnoser != null; - var executeResult = RunExecute( + var executeResult = await RunExecute( logger, benchmarkCase, benchmarkId, @@ -563,7 +565,7 @@ private static (bool success, List executeResults, List m { logger.WriteLineInfo("// Run, Diagnostic"); - var executeResult = RunExecute( + var executeResult = await RunExecute( logger, benchmarkCase, benchmarkId, @@ -592,7 +594,7 @@ private static (bool success, List executeResults, List m if (compositeInProcessDiagnoser.InProcessDiagnosers.Any(d => d.GetRunMode(benchmarkCase) == Diagnosers.RunMode.SeparateLogic)) { - var executeResult = RunExecute( + var executeResult = await RunExecute( logger, benchmarkCase, benchmarkId, @@ -616,10 +618,10 @@ private static (bool success, List executeResults, List m return (true, executeResults, metrics); } - private static ExecuteResult RunExecute(ILogger logger, BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, IToolchain toolchain, + private static async ValueTask RunExecute(ILogger logger, BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, IToolchain toolchain, BuildResult buildResult, IResolver resolver, IDiagnoser? diagnoser, CompositeInProcessDiagnoser compositeInProcessDiagnoser, int launchIndex, Diagnosers.RunMode diagnoserRunMode) { - var executeResult = toolchain.Executor.Execute( + var executeResult = await toolchain.Executor.ExecuteAsync( new ExecuteParameters( buildResult, benchmarkCase, @@ -654,15 +656,15 @@ private static ExecuteResult RunExecute(ILogger logger, BenchmarkCase benchmarkC private static void LogTotalTime(ILogger logger, TimeSpan time, int executedBenchmarksCount, string message = "Total time") => logger.WriteLineStatistic($"{message}: {time.ToFormattedTotalTime(DefaultCultureInfo.Instance)}, executed benchmarks: {executedBenchmarksCount}"); - private static (BenchmarkRunInfo[], List) GetSupportedBenchmarks(BenchmarkRunInfo[] benchmarkRunInfos, IResolver resolver) + private static async ValueTask<(BenchmarkRunInfo[], List)> GetSupportedBenchmarks(BenchmarkRunInfo[] benchmarkRunInfos, IResolver resolver) { - List validationErrors = new(); + List validationErrors = []; List runInfos = new(benchmarkRunInfos.Length); if (benchmarkRunInfos.Length == 0) { validationErrors.Add(new ValidationError(true, $"No benchmarks were found.")); - return (Array.Empty(), validationErrors); + return ([], validationErrors); } foreach (var benchmarkRunInfo in benchmarkRunInfos) @@ -673,19 +675,20 @@ private static (BenchmarkRunInfo[], List) GetSupportedBenchmark continue; } - var validBenchmarks = benchmarkRunInfo.BenchmarksCases - .Where(benchmark => + var validBenchmarks = await benchmarkRunInfo.BenchmarksCases + .ToAsyncEnumerable() + .Where(async (benchmark, _) => { - var errors = benchmark.GetToolchain() - .Validate(benchmark, resolver) - .ToArray(); + var errors = await benchmark.GetToolchain() + .ValidateAsync(benchmark, resolver) + .ToArrayAsync(); validationErrors.AddRange(errors); return !errors.Any(error => error.IsCritical); }) - .ToArray(); + .ToArrayAsync(); runInfos.Add( new BenchmarkRunInfo( diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs index 1e170f7503..7bff744578 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs @@ -1,12 +1,12 @@ using System; -using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Loggers; -using BenchmarkDotNet.Portability; using BenchmarkDotNet.Reports; using JetBrains.Annotations; @@ -20,93 +20,146 @@ public static class BenchmarkRunner [PublicAPI] public static Summary Run(IConfig? config = null, string[]? args = null) { - using (DirtyAssemblyResolveHelper.Create()) - return RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(typeof(T), config, args)); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAsync(config, args)); } [PublicAPI] public static Summary Run(Type type, IConfig? config = null, string[]? args = null) { - using (DirtyAssemblyResolveHelper.Create()) - return RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(type, config, args)); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAsync(type, config, args)); } [PublicAPI] public static Summary[] Run(Type[] types, IConfig? config = null, string[]? args = null) { - using (DirtyAssemblyResolveHelper.Create()) - return RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(types, config, args)); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAsync(types, config, args)); } [PublicAPI] public static Summary Run(Type type, MethodInfo[] methods, IConfig? config = null) { - using (DirtyAssemblyResolveHelper.Create()) - return RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(type, methods, config)); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAsync(type, methods, config)); } [PublicAPI] public static Summary[] Run(Assembly assembly, IConfig? config = null, string[]? args = null) { - using (DirtyAssemblyResolveHelper.Create()) - return RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(assembly, config, args)); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAsync(assembly, config, args)); } [PublicAPI] public static Summary Run(BenchmarkRunInfo benchmarkRunInfo) { - using (DirtyAssemblyResolveHelper.Create()) - return RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(new[] { benchmarkRunInfo }).Single()); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfo)); } [PublicAPI] public static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) + { + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAsync(benchmarkRunInfos)); + } + + [PublicAPI] + public static ValueTask RunAsync(IConfig? config = null, string[]? args = null) + => RunAsync(typeof(T), config, args); + + [PublicAPI] + public static async ValueTask RunAsync(Type type, IConfig? config = null, string[]? args = null) + { + using (DirtyAssemblyResolveHelper.Create()) + { + return await RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(type, config, args)); + } + } + + [PublicAPI] + public static async ValueTask RunAsync(Type[] types, IConfig? config = null, string[]? args = null) + { + using (DirtyAssemblyResolveHelper.Create()) + { + return await RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(types, config, args)); + } + } + + [PublicAPI] + public static async ValueTask RunAsync(Type type, MethodInfo[] methods, IConfig? config = null) + { + using (DirtyAssemblyResolveHelper.Create()) + { + return await RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(type, methods, config)); + } + } + + [PublicAPI] + public static async ValueTask RunAsync(Assembly assembly, IConfig? config = null, string[]? args = null) { using (DirtyAssemblyResolveHelper.Create()) - return RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(benchmarkRunInfos)); + { + return await RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(assembly, config, args)); + } + } + + [PublicAPI] + public static async ValueTask RunAsync(BenchmarkRunInfo benchmarkRunInfo) + => (await RunAsync([benchmarkRunInfo])).Single(); + + [PublicAPI] + public static async ValueTask RunAsync(BenchmarkRunInfo[] benchmarkRunInfos) + { + using (DirtyAssemblyResolveHelper.Create()) + { + return await RunWithExceptionHandling(() => RunWithDirtyAssemblyResolveHelper(benchmarkRunInfos)); + } } [MethodImpl(MethodImplOptions.NoInlining)] - private static Summary RunWithDirtyAssemblyResolveHelper(Type type, IConfig? config, string[]? args) + private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type type, IConfig? config, string[]? args) { var summaries = args == null - ? BenchmarkRunnerClean.Run(new[] { BenchmarkConverter.TypeToBenchmarks(type, config) }) - : new BenchmarkSwitcher(new[] { type }).RunWithDirtyAssemblyResolveHelper(args, config, false); + ? await BenchmarkRunnerClean.Run([BenchmarkConverter.TypeToBenchmarks(type, config)]) + : await new BenchmarkSwitcher([type]).RunWithDirtyAssemblyResolveHelper(args, config, false); return summaries.SingleOrDefault() ?? Summary.ValidationFailed($"No benchmarks found in type '{type.Name}'", string.Empty, string.Empty); } [MethodImpl(MethodImplOptions.NoInlining)] - private static Summary RunWithDirtyAssemblyResolveHelper(Type type, MethodInfo[] methods, IConfig? config = null) + private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type type, MethodInfo[] methods, IConfig? config = null) { - var summaries = BenchmarkRunnerClean.Run(new[] { BenchmarkConverter.MethodsToBenchmarks(type, methods, config) }); + var summaries = await BenchmarkRunnerClean.Run([BenchmarkConverter.MethodsToBenchmarks(type, methods, config)]); return summaries.SingleOrDefault() ?? Summary.ValidationFailed($"No benchmarks found in type '{type.Name}'", string.Empty, string.Empty); } [MethodImpl(MethodImplOptions.NoInlining)] - private static Summary[] RunWithDirtyAssemblyResolveHelper(Assembly assembly, IConfig? config, string[]? args) + private static async ValueTask RunWithDirtyAssemblyResolveHelper(Assembly assembly, IConfig? config, string[]? args) => args == null - ? BenchmarkRunnerClean.Run(assembly.GetRunnableBenchmarks().Select(type => BenchmarkConverter.TypeToBenchmarks(type, config)).ToArray()) - : new BenchmarkSwitcher(assembly).RunWithDirtyAssemblyResolveHelper(args, config, false).ToArray(); + ? await BenchmarkRunnerClean.Run(assembly.GetRunnableBenchmarks().Select(type => BenchmarkConverter.TypeToBenchmarks(type, config)).ToArray()) + : (await new BenchmarkSwitcher(assembly).RunWithDirtyAssemblyResolveHelper(args, config, false)).ToArray(); [MethodImpl(MethodImplOptions.NoInlining)] - private static Summary[] RunWithDirtyAssemblyResolveHelper(Type[] types, IConfig? config, string[]? args) + private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type[] types, IConfig? config, string[]? args) => args == null - ? BenchmarkRunnerClean.Run(types.Select(type => BenchmarkConverter.TypeToBenchmarks(type, config)).ToArray()) - : new BenchmarkSwitcher(types).RunWithDirtyAssemblyResolveHelper(args, config, false).ToArray(); + ? await BenchmarkRunnerClean.Run(types.Select(type => BenchmarkConverter.TypeToBenchmarks(type, config)).ToArray()) + : (await new BenchmarkSwitcher(types).RunWithDirtyAssemblyResolveHelper(args, config, false)).ToArray(); [MethodImpl(MethodImplOptions.NoInlining)] - private static Summary[] RunWithDirtyAssemblyResolveHelper(BenchmarkRunInfo[] benchmarkRunInfos) + private static ValueTask RunWithDirtyAssemblyResolveHelper(BenchmarkRunInfo[] benchmarkRunInfos) => BenchmarkRunnerClean.Run(benchmarkRunInfos); - private static Summary RunWithExceptionHandling(Func run) + private static async ValueTask RunWithExceptionHandling(Func> run) { try { - return run(); + return await run(); } catch (InvalidBenchmarkDeclarationException e) { @@ -115,16 +168,16 @@ private static Summary RunWithExceptionHandling(Func run) } } - private static Summary[] RunWithExceptionHandling(Func run) + private static async ValueTask RunWithExceptionHandling(Func> run) { try { - return run(); + return await run(); } catch (InvalidBenchmarkDeclarationException e) { ConsoleLogger.Default.WriteLineError(e.Message); - return new[] { Summary.ValidationFailed(e.Message, string.Empty, string.Empty) }; + return [Summary.ValidationFailed(e.Message, string.Empty, string.Empty)]; } } } diff --git a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs index 69f75c7e77..8e1c458706 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs @@ -4,9 +4,11 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using BenchmarkDotNet.Configs; using BenchmarkDotNet.ConsoleArguments; using BenchmarkDotNet.ConsoleArguments.ListBenchmarks; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Jobs; @@ -21,8 +23,8 @@ namespace BenchmarkDotNet.Running public class BenchmarkSwitcher { private readonly IUserInteraction userInteraction = new UserInteraction(); - private readonly List types = new List(); - private readonly List assemblies = new List(); + private readonly List types = []; + private readonly List assemblies = []; internal BenchmarkSwitcher(IUserInteraction userInteraction) => this.userInteraction = userInteraction; @@ -44,22 +46,19 @@ public class BenchmarkSwitcher [SuppressMessage("ReSharper", "ParameterHidesMember")] public BenchmarkSwitcher With(Assembly[] assemblies) { this.assemblies.AddRange(assemblies); return this; } - [PublicAPI] public static BenchmarkSwitcher FromTypes(Type[] types) => new BenchmarkSwitcher(types); + [PublicAPI] public static BenchmarkSwitcher FromTypes(Type[] types) => new(types); - [PublicAPI] public static BenchmarkSwitcher FromAssembly(Assembly assembly) => new BenchmarkSwitcher(assembly); + [PublicAPI] public static BenchmarkSwitcher FromAssembly(Assembly assembly) => new(assembly); - [PublicAPI] public static BenchmarkSwitcher FromAssemblies(Assembly[] assemblies) => new BenchmarkSwitcher(assemblies); + [PublicAPI] public static BenchmarkSwitcher FromAssemblies(Assembly[] assemblies) => new(assemblies); /// /// Run all available benchmarks. /// [PublicAPI] public IEnumerable RunAll(IConfig? config = null, string[]? args = null) { - args ??= Array.Empty(); - if (ConfigParser.TryUpdateArgs(args, out var updatedArgs, options => options.Filters = new[] { "*" })) - args = updatedArgs; - - return Run(args, config); + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAllAsync(config, args)); } /// @@ -67,33 +66,66 @@ [PublicAPI] public IEnumerable RunAll(IConfig? config = null, string[]? /// [PublicAPI] public Summary RunAllJoined(IConfig? config = null, string[]? args = null) { - args ??= Array.Empty(); - if (ConfigParser.TryUpdateArgs(args, out var updatedArgs, options => (options.Join, options.Filters) = (true, new[] { "*" }))) + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAllJoinedAsync(config, args)); + } + + [PublicAPI] + public IEnumerable Run(string[]? args = null, IConfig? config = null) + { + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(RunAsync(args, config)); + } + + /// + /// Run all available benchmarks asynchronously. + /// + [PublicAPI] + public ValueTask> RunAllAsync(IConfig? config = null, string[]? args = null) + { + args ??= []; + if (ConfigParser.TryUpdateArgs(args, out var updatedArgs, options => options.Filters = ["*"])) args = updatedArgs; - return Run(args, config).Single(); + return RunAsync(args, config); } + /// + /// Run all available benchmarks and join them to a single summary asynchronously. + /// [PublicAPI] - public IEnumerable Run(string[]? args = null, IConfig? config = null) + public async ValueTask RunAllJoinedAsync(IConfig? config = null, string[]? args = null) + { + args ??= []; + if (ConfigParser.TryUpdateArgs(args, out var updatedArgs, options => (options.Join, options.Filters) = (true, ["*"]))) + args = updatedArgs; + + var results = await RunAsync(args, config); + return results.Single(); + } + + [PublicAPI] + public async ValueTask> RunAsync(string[]? args = null, IConfig? config = null) { // VS generates bad assembly binding redirects for ValueTuple for Full .NET Framework // we need to keep the logic that uses it in a separate method and create DirtyAssemblyResolveHelper first // so it can ignore the version mismatch ;) using (DirtyAssemblyResolveHelper.Create()) - return RunWithDirtyAssemblyResolveHelper(args, config, true); + { + return await RunWithDirtyAssemblyResolveHelper(args, config, true); + } } [MethodImpl(MethodImplOptions.NoInlining)] - internal IEnumerable RunWithDirtyAssemblyResolveHelper(string[]? args, IConfig? config, bool askUserForInput) + internal async ValueTask> RunWithDirtyAssemblyResolveHelper(string[]? args, IConfig? config, bool askUserForInput) { - var notNullArgs = args ?? Array.Empty(); + var notNullArgs = args ?? []; var notNullConfig = config ?? DefaultConfig.Instance; var logger = notNullConfig.GetNonNullCompositeLogger(); var (isParsingSuccess, parsedConfig, options) = ConfigParser.Parse(notNullArgs, logger, notNullConfig); if (!isParsingSuccess) // invalid console args, the ConfigParser printed the error - return Array.Empty(); + return []; if (args == null && Environment.GetCommandLineArgs().Length > 1) // The first element is the executable file name logger.WriteLineHint("You haven't passed command line arguments to BenchmarkSwitcher.Run method. Running with default configuration."); @@ -101,25 +133,25 @@ internal IEnumerable RunWithDirtyAssemblyResolveHelper(string[]? args, if (options!.PrintInformation) { logger.WriteLine(HostEnvironmentInfo.GetInformation()); - return Array.Empty(); + return []; } var effectiveConfig = ManualConfig.Union(notNullConfig, parsedConfig!); var (allTypesValid, allAvailableTypesWithRunnableBenchmarks) = TypeFilter.GetTypesWithRunnableBenchmarks(types, assemblies, logger); if (!allTypesValid) // there were some invalid and TypeFilter printed errors - return Array.Empty(); + return []; if (allAvailableTypesWithRunnableBenchmarks.IsEmpty()) { userInteraction.PrintNoBenchmarksError(logger); - return Array.Empty(); + return []; } if (options.ListBenchmarkCaseMode != ListBenchmarkCaseMode.Disabled) { PrintList(logger, effectiveConfig, allAvailableTypesWithRunnableBenchmarks, options); - return Array.Empty(); + return []; } var benchmarksToFilter = options.UserProvidedFilters || !askUserForInput @@ -128,18 +160,18 @@ internal IEnumerable RunWithDirtyAssemblyResolveHelper(string[]? args, if (effectiveConfig.Options.HasFlag(ConfigOptions.ApplesToApples)) { - return ApplesToApples(ImmutableConfigBuilder.Create(effectiveConfig), benchmarksToFilter, logger, options); + return await ApplesToApples(ImmutableConfigBuilder.Create(effectiveConfig), benchmarksToFilter, logger, options); } var filteredBenchmarks = TypeFilter.Filter(effectiveConfig, benchmarksToFilter); if (filteredBenchmarks.IsEmpty()) { - userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, options.Filters.ToArray()); - return Array.Empty(); + userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, [.. options.Filters]); + return []; } - return BenchmarkRunnerClean.Run(filteredBenchmarks); + return await BenchmarkRunnerClean.Run(filteredBenchmarks); } private static void PrintList(ILogger nonNullLogger, IConfig effectiveConfig, IReadOnlyList allAvailableTypesWithRunnableBenchmarks, CommandLineOptions options) @@ -154,24 +186,24 @@ private static void PrintList(ILogger nonNullLogger, IConfig effectiveConfig, IR printer.Print(testNames, nonNullLogger); } - private IEnumerable ApplesToApples(ImmutableConfig effectiveConfig, IReadOnlyList benchmarksToFilter, ILogger logger, CommandLineOptions options) + private async ValueTask> ApplesToApples(ImmutableConfig effectiveConfig, IReadOnlyList benchmarksToFilter, ILogger logger, CommandLineOptions options) { var jobs = effectiveConfig.GetJobs().ToArray(); if (jobs.Length <= 1) { logger.WriteError("To use apples-to-apples comparison you must specify at least two Job objects."); - return Array.Empty(); + return []; } var baselineJob = jobs.SingleOrDefault(job => job.Meta.Baseline); if (baselineJob == default) { logger.WriteError("To use apples-to-apples comparison you must specify exactly ONE baseline Job object."); - return Array.Empty(); + return []; } else if (jobs.Any(job => !job.Run.HasValue(RunMode.IterationCountCharacteristic))) { logger.WriteError("To use apples-to-apples comparison you must specify the number of iterations in explicit way."); - return Array.Empty(); + return []; } #pragma warning disable CS0618 // WithEvaluateOverhead is obsolete @@ -189,12 +221,12 @@ private IEnumerable ApplesToApples(ImmutableConfig effectiveConfig, IRe var invocationCountBenchmarks = TypeFilter.Filter(invocationCountConfig, benchmarksToFilter); if (invocationCountBenchmarks.IsEmpty()) { - userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, options.Filters.ToArray()); - return Array.Empty(); + userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, [.. options.Filters]); + return []; } logger.WriteLineHeader("Each benchmark is going to be executed just once to get invocation counts."); - Summary[] invocationCountSummaries = BenchmarkRunnerClean.Run(invocationCountBenchmarks); + Summary[] invocationCountSummaries = await BenchmarkRunnerClean.Run(invocationCountBenchmarks); Dictionary<(Descriptor Descriptor, ParameterInstances Parameters), Measurement> dictionary = invocationCountSummaries .SelectMany(summary => summary.Reports) @@ -224,7 +256,7 @@ private IEnumerable ApplesToApples(ImmutableConfig effectiveConfig, IRe #pragma warning restore CS0618 // WithEvaluateOverhead is obsolete logger.WriteLineHeader("Actual benchmarking is going to happen now!"); - return BenchmarkRunnerClean.Run(benchmarksWithInvocationCount); + return await BenchmarkRunnerClean.Run(benchmarksWithInvocationCount); } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt index b4566457f6..0d08de2cbc 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt @@ -1,5 +1,3 @@ -$ShadowCopyDefines$ -$ExtraDefines$ // // this file must not be importing any namespaces // we should use full names everywhere to avoid any potential naming conflicts, example: #1007, #778 @@ -22,25 +20,12 @@ namespace BenchmarkDotNet.Autogenerated { public class UniqueProgramName // we need different name than typical "Program" to avoid problems with referencing "Program" types from benchmarked code, #691 { - $ExtraAttribute$ - public static global::System.Int32 Main(global::System.String[] args) - { - // this method MUST NOT have any dependencies to BenchmarkDotNet and any other external dlls! (CoreRT is exception from this rule) - // otherwise if LINQPad's shadow copy is enabled, we will not register for AssemblyLoading event - // before .NET Framework tries to load it for this method -#if NETFRAMEWORK - using(new DirtyAssemblyResolveHelper()) -#endif - return AfterAssemblyLoadingAttached(args); - } + $EntryPoint$ - private static global::System.Int32 AfterAssemblyLoadingAttached(global::System.String[] args) + private static async global::System.Threading.Tasks.ValueTask MainCore(global::System.String[] args) { - global::BenchmarkDotNet.Engines.IHost host; // this variable name is used by CodeGenerator.GetBenchmarkRunCall, do NOT change it - if (global::BenchmarkDotNet.Engines.AnonymousPipesHost.TryGetFileHandles(args, out global::System.String writeHandle, out global::System.String readHandle)) - host = new global::BenchmarkDotNet.Engines.AnonymousPipesHost(writeHandle, readHandle); - else - host = new global::BenchmarkDotNet.Engines.NoAcknowledgementConsoleHost(); + // This variable name is used by CodeGenerator.GetBenchmarkRunCall, do NOT change it! + global::BenchmarkDotNet.Engines.IHost host = global::BenchmarkDotNet.Engines.TcpHost.GetHost(args); // the first thing to do is to let diagnosers hook in before anything happens // so all jit-related diagnosers can catch first jit compilation! @@ -89,49 +74,5 @@ namespace BenchmarkDotNet.Autogenerated } } -#if NETFRAMEWORK - internal class DirtyAssemblyResolveHelper : global::System.IDisposable - { - internal DirtyAssemblyResolveHelper() => global::System.AppDomain.CurrentDomain.AssemblyResolve += HelpTheFrameworkToResolveTheAssembly; - - public void Dispose() => global::System.AppDomain.CurrentDomain.AssemblyResolve -= HelpTheFrameworkToResolveTheAssembly; - - /// - /// according to https://msdn.microsoft.com/en-us/library/ff527268(v=vs.110).aspx - /// "the handler is invoked whenever the runtime fails to bind to an assembly by name." - /// - /// not null when we find it manually, null when can't help - private global::System.Reflection.Assembly HelpTheFrameworkToResolveTheAssembly(global::System.Object sender, global::System.ResolveEventArgs args) - { -#if SHADOWCOPY // used for LINQPad - const global::System.String shadowCopyFolderPath = @"$ShadowCopyFolderPath$"; - - global::System.String guessedPath = global::System.IO.Path.Combine(shadowCopyFolderPath, $"{new global::System.Reflection.AssemblyName(args.Name).Name}.dll"); - - return global::System.IO.File.Exists(guessedPath) ? global::System.Reflection.Assembly.LoadFrom(guessedPath) : null; -#else - global::System.Reflection.AssemblyName fullName = new global::System.Reflection.AssemblyName(args.Name); - global::System.String simpleName = fullName.Name; - - global::System.String guessedPath = global::System.IO.Path.Combine(global::System.AppDomain.CurrentDomain.BaseDirectory, $"{simpleName}.dll"); - - if (!global::System.IO.File.Exists(guessedPath)) - { - global::System.Console.WriteLine($"// Wrong assembly binding redirects for {args.Name}."); - return null; // we can't help, and we also don't call Assembly.Load which if fails comes back here, creates endless loop and causes StackOverflow - } - - // the file is right there, but has most probably different version and there is no assembly binding redirect or there is a wrong one... - // so we just load it and ignore the version mismatch - - // we warn the user about that, in case some Super User want to be aware of that - global::System.Console.WriteLine($"// Wrong assembly binding redirects for {simpleName}, loading it from disk anyway."); - - return global::System.Reflection.Assembly.LoadFrom(guessedPath); -#endif // SHADOWCOPY - } - } -#endif // NETFRAMEWORK - $DerivedTypes$ } diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index a6a8a1d69b..9535d3c01f 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -1,7 +1,8 @@ // Type name must be in sync with DisassemblyDiagnoser.BuildClrMdArgs. - public unsafe sealed class Runnable_$ID$ : $WorkloadTypeName$ + [global::BenchmarkDotNet.Attributes.CompilerServices.AggressivelyOptimizeMethods] + public sealed partial class Runnable_$ID$ : $WorkloadTypeName$ { - public static void Run(global::BenchmarkDotNet.Engines.IHost host, global::System.String benchmarkName, global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode) + public static async global::System.Threading.Tasks.ValueTask Run(global::BenchmarkDotNet.Engines.IHost host, global::System.String benchmarkName, global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode) { global::BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance = new global::BenchmarkDotNet.Autogenerated.Runnable_$ID$(); @@ -17,7 +18,7 @@ host.WriteLine(); global::System.Collections.Generic.IEnumerable errors = global::BenchmarkDotNet.Validators.BenchmarkProcessValidator.Validate(job, instance); - if (global::BenchmarkDotNet.Validators.ValidationErrorReporter.ReportIfAny(errors, host)) + if (await global::BenchmarkDotNet.Validators.ValidationErrorReporter.ReportIfAnyAsync(errors, host)) return; global::BenchmarkDotNet.Diagnosers.CompositeInProcessDiagnoserHandler compositeInProcessDiagnoserHandler = new global::BenchmarkDotNet.Diagnosers.CompositeInProcessDiagnoserHandler( @@ -30,10 +31,10 @@ ); if (diagnoserRunMode == global::BenchmarkDotNet.Diagnosers.RunMode.SeparateLogic) { - compositeInProcessDiagnoserHandler.Handle(global::BenchmarkDotNet.Engines.BenchmarkSignal.SeparateLogic); + await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.SeparateLogic); return; } - compositeInProcessDiagnoserHandler.Handle(global::BenchmarkDotNet.Engines.BenchmarkSignal.BeforeEngine); + await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.BeforeEngine); global::BenchmarkDotNet.Engines.EngineParameters engineParameters = new global::BenchmarkDotNet.Engines.EngineParameters() { @@ -42,10 +43,10 @@ WorkloadActionNoUnroll = instance.WorkloadActionNoUnroll, OverheadActionNoUnroll = instance.OverheadActionNoUnroll, OverheadActionUnroll = instance.OverheadActionUnroll, - GlobalSetupAction = instance.globalSetupAction, - GlobalCleanupAction = instance.globalCleanupAction, - IterationSetupAction = instance.iterationSetupAction, - IterationCleanupAction = instance.iterationCleanupAction, + GlobalSetupAction = instance.__GlobalSetup, + GlobalCleanupAction = instance.__GlobalCleanup, + IterationSetupAction = instance.__IterationSetup, + IterationCleanupAction = instance.__IterationCleanup, TargetJob = job, OperationsPerInvoke = $OperationsPerInvoke$, RunExtraIteration = $RunExtraIteration$, @@ -53,92 +54,65 @@ InProcessDiagnoserHandler = compositeInProcessDiagnoserHandler }; - global::BenchmarkDotNet.Engines.RunResults results = new $EngineFactoryType$().Create(engineParameters).Run(); + global::BenchmarkDotNet.Engines.RunResults results = await new $EngineFactoryType$().Create(engineParameters).RunAsync(); host.ReportResults(results); // printing costs memory, do this after runs instance.__TrickTheJIT__(); // compile the method for disassembler, but without actual run of the benchmark ;) - compositeInProcessDiagnoserHandler.Handle(global::BenchmarkDotNet.Engines.BenchmarkSignal.AfterEngine); + await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.AfterEngine); } [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public Runnable_$ID$() + public unsafe Runnable_$ID$() { - this.globalSetupAction = $GlobalSetupMethodName$; - this.globalCleanupAction = $GlobalCleanupMethodName$; - this.iterationSetupAction = $IterationSetupMethodName$; - this.iterationCleanupAction = $IterationCleanupMethodName$; $InitializeArgumentFields$ $ParamsContent$ } - private global::System.Action globalSetupAction; - private global::System.Action globalCleanupAction; - private global::System.Action iterationSetupAction; - private global::System.Action iterationCleanupAction; - $DeclareArgumentFields$ + $DeclareFieldsContainer$ - // this method is used only for the disassembly diagnoser purposes - // the goal is to get this and the benchmarked method jitted, but without executing the benchmarked method itself - public global::System.Int32 NotEleven; - [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - public void __TrickTheJIT__() + private $GlobalSetupModifiers$ global::System.Threading.Tasks.ValueTask __GlobalSetup() { - this.NotEleven = new global::System.Random(123).Next(0, 10); - $DisassemblerEntryMethodName$(); + $GlobalSetupImpl$ } - [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - public void $DisassemblerEntryMethodName$() + private $GlobalCleanupModifiers$ global::System.Threading.Tasks.ValueTask __GlobalCleanup() { - if (this.NotEleven == 11) - { - $LoadArguments$ - $WorkloadMethodCall$; - } + $GlobalCleanupImpl$ } - [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private void __Overhead($ArgumentsDefinition$) + private $IterationSetupModifiers$ global::System.Threading.Tasks.ValueTask __IterationSetup() { + $IterationSetupImpl$ } - [global::System.Runtime.CompilerServices.MethodImpl(global::BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] - private void OverheadActionUnroll(global::System.Int64 invokeCount) + private $IterationCleanupModifiers$ global::System.Threading.Tasks.ValueTask __IterationCleanup() { - $LoadArguments$ - while (--invokeCount >= 0) - { - this.__Overhead($PassArguments$);@Unroll@ - } + $IterationCleanupImpl$ } - [global::System.Runtime.CompilerServices.MethodImpl(global::BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] - private void OverheadActionNoUnroll(global::System.Int64 invokeCount) + // this method is used only for the disassembly diagnoser purposes + // the goal is to get this and the benchmarked method jitted, but without executing the benchmarked method itself + public global::System.Int32 NotEleven; + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + public void __TrickTheJIT__() { - $LoadArguments$ - while (--invokeCount >= 0) - { - this.__Overhead($PassArguments$); - } + this.NotEleven = new global::System.Random(123).Next(0, 10); + $DisassemblerEntryMethodName$(); } - [global::System.Runtime.CompilerServices.MethodImpl(global::BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(global::System.Int64 invokeCount) + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + public unsafe void $DisassemblerEntryMethodName$() { - $LoadArguments$ - while (--invokeCount >= 0) + if (this.NotEleven == 11) { - $WorkloadMethodCall$;@Unroll@ + $DisassemblerEntryMethodImpl$ } } - [global::System.Runtime.CompilerServices.MethodImpl(global::BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(global::System.Int64 invokeCount) + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private unsafe void __Overhead($ArgumentsDefinition$) { - $LoadArguments$ - while (--invokeCount >= 0) - { - $WorkloadMethodCall$; - } } + + $CoreImpl$ } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs b/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs index 70ffb09cb6..c981451bfd 100644 --- a/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs @@ -59,7 +59,7 @@ public CoreRunToolchain(FileInfo coreRun, bool createCopy = true, public override string ToString() => Name; - public IEnumerable Validate(BenchmarkCase benchmark, IResolver resolver) + public async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmark, IResolver resolver) { if (!SourceCoreRun.Exists) { diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjClassicNetToolchain.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjClassicNetToolchain.cs index dbadfb79ba..7a43d3670a 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjClassicNetToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjClassicNetToolchain.cs @@ -47,9 +47,9 @@ public static IToolchain From(string targetFrameworkMoniker, string packagesPath packagesPath.EnsureNotNull(), customDotNetCliPath.EnsureNotNull()); - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { - foreach (var validationError in base.Validate(benchmarkCase, resolver)) + await foreach (var validationError in base.ValidateAsync(benchmarkCase, resolver)) { yield return validationError; } diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs index 74f7d004f0..26d7052f53 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs @@ -44,9 +44,9 @@ public static IToolchain From(NetCoreAppSettings settings) new DotNetCliExecutor(settings.CustomDotNetCliPath), settings.CustomDotNetCliPath); - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { - foreach (var validationError in base.Validate(benchmarkCase, resolver)) + await foreach (var validationError in base.ValidateAsync(benchmarkCase, resolver)) { yield return validationError; } diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs index 47a708868e..f0b347e2ff 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs @@ -2,6 +2,9 @@ using System.Diagnostics; using System.IO; using System.IO.Pipes; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Engines; @@ -17,26 +20,24 @@ namespace BenchmarkDotNet.Toolchains.DotNetCli { [PublicAPI] - public class DotNetCliExecutor : IExecutor + public class DotNetCliExecutor(string customDotNetCliPath) : IExecutor { - public DotNetCliExecutor(string customDotNetCliPath) => CustomDotNetCliPath = customDotNetCliPath; - - private string CustomDotNetCliPath { get; } - - public ExecuteResult Execute(ExecuteParameters executeParameters) + public async ValueTask ExecuteAsync(ExecuteParameters executeParameters) { if (!File.Exists(executeParameters.BuildResult.ArtifactsPaths.ExecutablePath)) { executeParameters.Logger.WriteLineError($"Did not find {executeParameters.BuildResult.ArtifactsPaths.ExecutablePath}, but the folder contained:"); foreach (var file in new DirectoryInfo(executeParameters.BuildResult.ArtifactsPaths.BinariesDirectoryPath).GetFiles("*.*")) + { executeParameters.Logger.WriteLineError(file.Name); + } return ExecuteResult.CreateFailed(); } try { - return Execute( + return await Execute( executeParameters.BenchmarkCase, executeParameters.BenchmarkId, executeParameters.Logger, @@ -56,79 +57,86 @@ public ExecuteResult Execute(ExecuteParameters executeParameters) } } - private ExecuteResult Execute(BenchmarkCase benchmarkCase, - BenchmarkId benchmarkId, - ILogger logger, - ArtifactsPaths artifactsPaths, - IDiagnoser? diagnoser, - CompositeInProcessDiagnoser compositeInProcessDiagnoser, - string executableName, - IResolver resolver, - int launchIndex, - Diagnosers.RunMode diagnoserRunMode) + private async ValueTask Execute(BenchmarkCase benchmarkCase, + BenchmarkId benchmarkId, + ILogger logger, + ArtifactsPaths artifactsPaths, + IDiagnoser? diagnoser, + CompositeInProcessDiagnoser compositeInProcessDiagnoser, + string executableName, + IResolver resolver, + int launchIndex, + Diagnosers.RunMode diagnoserRunMode) { - using AnonymousPipeServerStream inputFromBenchmark = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable); - using AnonymousPipeServerStream acknowledgments = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable); - - var startInfo = DotNetCliCommandExecutor.BuildStartInfo( - CustomDotNetCliPath, - artifactsPaths.BinariesDirectoryPath, - $"{executableName.EscapeCommandLine()} {benchmarkId.ToArguments(inputFromBenchmark.GetClientHandleAsString(), acknowledgments.GetClientHandleAsString(), diagnoserRunMode)}", - redirectStandardOutput: true, - redirectStandardInput: false, - redirectStandardError: false); // #1629 - - startInfo.SetEnvironmentVariables(benchmarkCase, resolver); - - using Process process = new() { StartInfo = startInfo }; - using ConsoleExitHandler consoleExitHandler = new(process, logger); - using AsyncProcessOutputReader processOutputReader = new(process, logOutput: true, logger, readStandardError: false); - - List results; - List prefixedOutput; - using (Broker broker = new(logger, process, diagnoser, compositeInProcessDiagnoser, benchmarkCase, benchmarkId, inputFromBenchmark, acknowledgments)) + var tcplistener = new TcpListener(IPAddress.Loopback, port: 0); + try { - logger.WriteLineInfo($"// Execute: {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}"); - - diagnoser?.Handle(HostSignal.BeforeProcessStart, broker.DiagnoserActionParameters); + tcplistener.Start(1); - process.Start(); + var startInfo = DotNetCliCommandExecutor.BuildStartInfo( + customDotNetCliPath, + artifactsPaths.BinariesDirectoryPath, + $"{executableName.EscapeCommandLine()} {benchmarkId.ToArguments(((IPEndPoint) tcplistener.LocalEndpoint).Port, diagnoserRunMode)}", + redirectStandardOutput: true, + redirectStandardInput: false, + redirectStandardError: false); // #1629 - diagnoser?.Handle(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters); + startInfo.SetEnvironmentVariables(benchmarkCase, resolver); - processOutputReader.BeginRead(); + using Process process = new() { StartInfo = startInfo }; + using ConsoleExitHandler consoleExitHandler = new(process, logger); + using AsyncProcessOutputReader processOutputReader = new(process, logOutput: true, logger, readStandardError: false); - process.EnsureHighPriority(logger); - if (benchmarkCase.Job.Environment.HasValue(EnvironmentMode.AffinityCharacteristic)) + List results; + List prefixedOutput; + using (Broker broker = new(logger, process, diagnoser, compositeInProcessDiagnoser, benchmarkCase, benchmarkId, tcplistener)) { - process.TrySetAffinity(benchmarkCase.Job.Environment.Affinity, logger); - } + logger.WriteLineInfo($"// Execute: {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}"); - broker.ProcessData(); + diagnoser?.Handle(HostSignal.BeforeProcessStart, broker.DiagnoserActionParameters); - results = broker.Results; - prefixedOutput = broker.PrefixedOutput; - } + process.Start(); - if (!process.WaitForExit(milliseconds: (int) ExecuteParameters.ProcessExitTimeout.TotalMilliseconds)) - { - logger.WriteLineInfo($"// The benchmarking process did not quit within {ExecuteParameters.ProcessExitTimeout.TotalSeconds} seconds, it's going to get force killed now."); + diagnoser?.Handle(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters); + + processOutputReader.BeginRead(); + + process.EnsureHighPriority(logger); + if (benchmarkCase.Job.Environment.HasValue(EnvironmentMode.AffinityCharacteristic)) + { + process.TrySetAffinity(benchmarkCase.Job.Environment.Affinity, logger); + } - processOutputReader.CancelRead(); - consoleExitHandler.KillProcessTree(); + await broker.ProcessData(); + + results = broker.Results; + prefixedOutput = broker.PrefixedOutput; + } + + if (!process.WaitForExit(milliseconds: (int) ExecuteParameters.ProcessExitTimeout.TotalMilliseconds)) + { + logger.WriteLineInfo($"// The benchmarking process did not quit within {ExecuteParameters.ProcessExitTimeout.TotalSeconds} seconds, it's going to get force killed now."); + + processOutputReader.CancelRead(); + consoleExitHandler.KillProcessTree(); + } + else + { + processOutputReader.StopRead(); + } + + return new ExecuteResult(true, + process.HasExited ? process.ExitCode : null, + process.Id, + results, + prefixedOutput, + processOutputReader.GetOutputLines(), + launchIndex); } - else + finally { - processOutputReader.StopRead(); + tcplistener.Stop(); } - - return new ExecuteResult(true, - process.HasExited ? process.ExitCode : null, - process.Id, - results, - prefixedOutput, - processOutputReader.GetOutputLines(), - launchIndex); } } } diff --git a/src/BenchmarkDotNet/Toolchains/Executor.cs b/src/BenchmarkDotNet/Toolchains/Executor.cs index d0caa3d29c..95f4ca12c5 100644 --- a/src/BenchmarkDotNet/Toolchains/Executor.cs +++ b/src/BenchmarkDotNet/Toolchains/Executor.cs @@ -5,7 +5,10 @@ using System.IO; using System.IO.Pipes; using System.Linq; +using System.Net; +using System.Net.Sockets; using System.Text; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Engines; @@ -24,27 +27,27 @@ namespace BenchmarkDotNet.Toolchains [PublicAPI("Used by some of our Superusers that implement their own Toolchains (e.g. Kestrel team)")] public class Executor : IExecutor { - public ExecuteResult Execute(ExecuteParameters executeParameters) + /// + /// Out-of-process executor executes synchronously. + /// + public async ValueTask ExecuteAsync(ExecuteParameters executeParameters) { string exePath = executeParameters.BuildResult.ArtifactsPaths.ExecutablePath; - if (!File.Exists(exePath)) - { - return ExecuteResult.CreateFailed(); - } - - return Execute(executeParameters.BenchmarkCase, executeParameters.BenchmarkId, executeParameters.Logger, executeParameters.BuildResult.ArtifactsPaths, - executeParameters.Diagnoser, executeParameters.CompositeInProcessDiagnoser, executeParameters.Resolver, executeParameters.LaunchIndex, - executeParameters.DiagnoserRunMode); + return !File.Exists(exePath) + ? ExecuteResult.CreateFailed() + : await Execute(executeParameters.BenchmarkCase, executeParameters.BenchmarkId, executeParameters.Logger, executeParameters.BuildResult.ArtifactsPaths, + executeParameters.Diagnoser, executeParameters.CompositeInProcessDiagnoser, executeParameters.Resolver, executeParameters.LaunchIndex, + executeParameters.DiagnoserRunMode); } - private static ExecuteResult Execute(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, ILogger logger, ArtifactsPaths artifactsPaths, + private static async ValueTask Execute(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, ILogger logger, ArtifactsPaths artifactsPaths, IDiagnoser? diagnoser, CompositeInProcessDiagnoser compositeInProcessDiagnoser, IResolver resolver, int launchIndex, Diagnosers.RunMode diagnoserRunMode) { try { - return ExecuteCore(benchmarkCase, benchmarkId, logger, artifactsPaths, diagnoser, compositeInProcessDiagnoser, resolver, launchIndex, diagnoserRunMode); + return await ExecuteCore(benchmarkCase, benchmarkId, logger, artifactsPaths, diagnoser, compositeInProcessDiagnoser, resolver, launchIndex, diagnoserRunMode); } finally { @@ -52,76 +55,83 @@ private static ExecuteResult Execute(BenchmarkCase benchmarkCase, BenchmarkId be } } - private static ExecuteResult ExecuteCore(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, ILogger logger, ArtifactsPaths artifactsPaths, + private static async ValueTask ExecuteCore(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, ILogger logger, ArtifactsPaths artifactsPaths, IDiagnoser? diagnoser, CompositeInProcessDiagnoser compositeInProcessDiagnoser, IResolver resolver, int launchIndex, Diagnosers.RunMode diagnoserRunMode) { - using AnonymousPipeServerStream inputFromBenchmark = new(PipeDirection.In, HandleInheritability.Inheritable); - using AnonymousPipeServerStream acknowledgments = new(PipeDirection.Out, HandleInheritability.Inheritable); + var tcplistener = new TcpListener(IPAddress.Loopback, port: 0); + try + { + tcplistener.Start(1); - string args = benchmarkId.ToArguments(inputFromBenchmark.GetClientHandleAsString(), acknowledgments.GetClientHandleAsString(), diagnoserRunMode); + string args = benchmarkId.ToArguments(((IPEndPoint) tcplistener.LocalEndpoint).Port, diagnoserRunMode); - using Process process = new() { StartInfo = CreateStartInfo(benchmarkCase, artifactsPaths, args, resolver) }; - using ConsoleExitHandler consoleExitHandler = new(process, logger); - using AsyncProcessOutputReader processOutputReader = new(process, logOutput: true, logger, readStandardError: false); + using Process process = new() { StartInfo = CreateStartInfo(benchmarkCase, artifactsPaths, args, resolver) }; + using ConsoleExitHandler consoleExitHandler = new(process, logger); + using AsyncProcessOutputReader processOutputReader = new(process, logOutput: true, logger, readStandardError: false); - List results; - List prefixedOutput; - using (Broker broker = new(logger, process, diagnoser, compositeInProcessDiagnoser, benchmarkCase, benchmarkId, inputFromBenchmark, acknowledgments)) - { - diagnoser?.Handle(HostSignal.BeforeProcessStart, new DiagnoserActionParameters(process, benchmarkCase, benchmarkId)); + List results; + List prefixedOutput; + using (Broker broker = new(logger, process, diagnoser, compositeInProcessDiagnoser, benchmarkCase, benchmarkId, tcplistener)) + { + diagnoser?.Handle(HostSignal.BeforeProcessStart, new DiagnoserActionParameters(process, benchmarkCase, benchmarkId)); - logger.WriteLineInfo($"// Execute: {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}"); + logger.WriteLineInfo($"// Execute: {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}"); - try - { - process.Start(); - } - catch (Win32Exception ex) - { - logger.WriteLineError($"// Failed to start the benchmark process: {ex}"); + try + { + process.Start(); + } + catch (Win32Exception ex) + { + logger.WriteLineError($"// Failed to start the benchmark process: {ex}"); - return new ExecuteResult(true, null, null, [], [], [], launchIndex); - } + return new ExecuteResult(true, null, null, [], [], [], launchIndex); + } - broker.Diagnoser?.Handle(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters); + broker.Diagnoser?.Handle(HostSignal.AfterProcessStart, broker.DiagnoserActionParameters); - processOutputReader.BeginRead(); + processOutputReader.BeginRead(); - process.EnsureHighPriority(logger); - if (benchmarkCase.Job.Environment.HasValue(EnvironmentMode.AffinityCharacteristic)) - { - process.TrySetAffinity(benchmarkCase.Job.Environment.Affinity, logger); + process.EnsureHighPriority(logger); + if (benchmarkCase.Job.Environment.HasValue(EnvironmentMode.AffinityCharacteristic)) + { + process.TrySetAffinity(benchmarkCase.Job.Environment.Affinity, logger); + } + + await broker.ProcessData(); + + results = broker.Results; + prefixedOutput = broker.PrefixedOutput; } - broker.ProcessData(); + if (!process.WaitForExit(milliseconds: (int) ExecuteParameters.ProcessExitTimeout.TotalMilliseconds)) + { + logger.WriteLineInfo("// The benchmarking process did not quit on time, it's going to get force killed now."); - results = broker.Results; - prefixedOutput = broker.PrefixedOutput; - } + processOutputReader.CancelRead(); + consoleExitHandler.KillProcessTree(); + } + else + { + processOutputReader.StopRead(); + } - if (!process.WaitForExit(milliseconds: (int)ExecuteParameters.ProcessExitTimeout.TotalMilliseconds)) - { - logger.WriteLineInfo("// The benchmarking process did not quit on time, it's going to get force killed now."); + if (results.Any(line => line.Contains("BadImageFormatException"))) + logger.WriteLineError("You are probably missing AnyCPU in your .csproj file."); - processOutputReader.CancelRead(); - consoleExitHandler.KillProcessTree(); + return new ExecuteResult(true, + process.HasExited ? process.ExitCode : null, + process.Id, + results, + prefixedOutput, + processOutputReader.GetOutputLines(), + launchIndex); } - else + finally { - processOutputReader.StopRead(); + tcplistener.Stop(); } - - if (results.Any(line => line.Contains("BadImageFormatException"))) - logger.WriteLineError("You are probably missing AnyCPU in your .csproj file."); - - return new ExecuteResult(true, - process.HasExited ? process.ExitCode : null, - process.Id, - results, - prefixedOutput, - processOutputReader.GetOutputLines(), - launchIndex); } private static ProcessStartInfo CreateStartInfo(BenchmarkCase benchmarkCase, ArtifactsPaths artifactsPaths, string args, IResolver resolver) @@ -174,7 +184,7 @@ private static string GetMonoArguments(Job job, string exePath, string args, IRe { var arguments = job.HasValue(InfrastructureMode.ArgumentsCharacteristic) ? job.ResolveValue(InfrastructureMode.ArgumentsCharacteristic, resolver)!.OfType().ToArray() - : Array.Empty(); + : []; // from mono --help: "Usage is: mono [options] program [program-options]" var builder = new StringBuilder(30); @@ -182,7 +192,9 @@ private static string GetMonoArguments(Job job, string exePath, string args, IRe builder.Append(job.ResolveValue(EnvironmentMode.JitCharacteristic, resolver) == Jit.Llvm ? "--llvm" : "--nollvm"); foreach (var argument in arguments) + { builder.Append($" {argument.TextRepresentation}"); + } builder.Append($" \"{exePath}\" "); builder.Append(args); diff --git a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs index eb97178ae0..d3215947a5 100644 --- a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs +++ b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs @@ -15,6 +15,9 @@ namespace BenchmarkDotNet.Toolchains [PublicAPI] public abstract class GeneratorBase : IGenerator { + /// + public CodeGenEntryPointType EntryPointType { get; init; } + /// public CodeGenBenchmarkRunCallType BenchmarkRunCallType { get; init; } @@ -121,7 +124,7 @@ [PublicAPI] protected virtual void GenerateAppConfig(BuildPartition buildPartiti /// You most probably do NOT need to override this method!! /// [PublicAPI] protected virtual void GenerateCode(BuildPartition buildPartition, ArtifactsPaths artifactsPaths) - => File.WriteAllText(artifactsPaths.ProgramCodePath, CodeGenerator.Generate(buildPartition, BenchmarkRunCallType)); + => File.WriteAllText(artifactsPaths.ProgramCodePath, CodeGenerator.Generate(buildPartition, EntryPointType, BenchmarkRunCallType)); protected virtual string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, $"{programName}{GetExecutableExtension()}"); diff --git a/src/BenchmarkDotNet/Toolchains/IExecutor.cs b/src/BenchmarkDotNet/Toolchains/IExecutor.cs index 85604ef369..1ffd2aad52 100644 --- a/src/BenchmarkDotNet/Toolchains/IExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/IExecutor.cs @@ -1,10 +1,10 @@ using BenchmarkDotNet.Toolchains.Parameters; using BenchmarkDotNet.Toolchains.Results; +using System.Threading.Tasks; -namespace BenchmarkDotNet.Toolchains +namespace BenchmarkDotNet.Toolchains; + +public interface IExecutor { - public interface IExecutor - { - ExecuteResult Execute(ExecuteParameters executeParameters); - } + ValueTask ExecuteAsync(ExecuteParameters executeParameters); } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/IToolchain.cs b/src/BenchmarkDotNet/Toolchains/IToolchain.cs index 7085b820fe..5f8e3da7b2 100644 --- a/src/BenchmarkDotNet/Toolchains/IToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/IToolchain.cs @@ -14,6 +14,6 @@ public interface IToolchain IExecutor Executor { get; } bool IsInProcess { get; } - IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver); + IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs deleted file mode 100644 index 1978751396..0000000000 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/ConsumableTypeInfo.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; - -namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation -{ - public class ConsumableTypeInfo - { - public ConsumableTypeInfo(Type methodReturnType) - { - if (methodReturnType == null) - throw new ArgumentNullException(nameof(methodReturnType)); - - OriginMethodReturnType = methodReturnType; - - // Only support (Value)Task for parity with other toolchains (and so we can use AwaitHelper). - IsAwaitable = methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask) - || (methodReturnType.GetTypeInfo().IsGenericType - && (methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) - || methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ValueTask<>))); - - if (!IsAwaitable) - { - WorkloadMethodReturnType = methodReturnType; - } - else - { - WorkloadMethodReturnType = methodReturnType - .GetMethod(nameof(Task.GetAwaiter), BindingFlagsPublicInstance)! - .ReturnType - .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlagsPublicInstance)! - .ReturnType; - GetResultMethod = Helpers.AwaitHelper.GetGetResultMethod(methodReturnType); - } - - if (WorkloadMethodReturnType == null) - throw new InvalidOperationException("Bug: (WorkloadMethodReturnType == null"); - - if (WorkloadMethodReturnType == typeof(void)) - { - IsVoid = true; - } - else if (WorkloadMethodReturnType.IsByRef) - { - IsByRef = true; - } - } - - public Type OriginMethodReturnType { get; } - public Type WorkloadMethodReturnType { get; } - - public MethodInfo? GetResultMethod { get; } - - public bool IsVoid { get; } - public bool IsByRef { get; } - - public bool IsAwaitable { get; } - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs new file mode 100644 index 0000000000..1943b88990 --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs @@ -0,0 +1,906 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers.Reflection.Emit; +using BenchmarkDotNet.Running; +using Perfolizer.Horology; +using System; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; + +namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; + +partial class RunnableEmitter +{ + // TODO: update this to support runtime-async. + private sealed class AsyncCoreEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder, BenchmarkBuildInfo benchmark) : RunnableEmitter(buildPartition, moduleBuilder, benchmark) + { + private FieldInfo workloadContinuerAndValueTaskSourceField = null!; + private FieldInfo clockField = null!; + private FieldInfo invokeCountField = null!; + + protected override int GetExtraFieldsCount() => 3; + + protected override void EmitExtraFields(TypeBuilder fieldsContainerBuilder) + { + base.EmitExtraFields(fieldsContainerBuilder); + + workloadContinuerAndValueTaskSourceField = fieldsContainerBuilder.DefineField( + WorkloadContinuerAndValueTaskSourceFieldName, + typeof(WorkloadContinuerAndValueTaskSource), + FieldAttributes.Public); + clockField = fieldsContainerBuilder.DefineField( + ClockFieldName, + typeof(IClock), + FieldAttributes.Public); + invokeCountField = fieldsContainerBuilder.DefineField( + InvokeCountFieldName, + typeof(long), + FieldAttributes.Public); + } + + protected override void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal) + { + // __fieldsContainer.workloadContinuerAndValueTaskSource?.Complete(); + + var callCompleteLabel = ilBuilder.DefineLabel(); // IL_0022 + var skipCompleteLabel = ilBuilder.DefineLabel(); // IL_0027 + + /* + // WorkloadContinuerAndValueTaskSource workloadContinuerAndValueTaskSource = runnable_.__fieldsContainer.workloadContinuerAndValueTaskSource; + IL_0011: ldloc.1 + IL_0012: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_0::__fieldsContainer + IL_0017: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer::workloadContinuerAndValueTaskSource + */ + if (thisLocal != null) + { + ilBuilder.EmitLdloc(thisLocal); + } + else + { + ilBuilder.Emit(OpCodes.Ldarg_0); + } + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldfld, workloadContinuerAndValueTaskSourceField); + /* + // if (workloadContinuerAndValueTaskSource != null) + IL_001c: dup + IL_001d: brtrue.s IL_0022 + */ + ilBuilder.Emit(OpCodes.Dup); + ilBuilder.Emit(OpCodes.Brtrue_S, callCompleteLabel); + + /* + IL_001f: pop + IL_0020: br.s IL_0027 + */ + ilBuilder.Emit(OpCodes.Pop); + ilBuilder.Emit(OpCodes.Br, skipCompleteLabel); + + /* + // workloadContinuerAndValueTaskSource.Complete(); + IL_0022: call instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::Complete() + */ + ilBuilder.MarkLabel(callCompleteLabel); + ilBuilder.Emit(OpCodes.Call, typeof(WorkloadContinuerAndValueTaskSource).GetMethod(nameof(WorkloadContinuerAndValueTaskSource.Complete), BindingFlags.Public | BindingFlags.Instance)!); + + ilBuilder.MarkLabel(skipCompleteLabel); + } + + protected override void EmitCoreImpl() + { + EmitOverhead(); + EmitWorkload(); + } + + private void EmitOverhead() + { + var noUnrollMethod = EmitNoUnrollMethod(); + EmitUnrollMethod(); + + MethodInfo EmitNoUnrollMethod() + { + /* + // private ValueTask OverheadActionNoUnroll(long invokeCount, IClock clock) + .method private hidebysig + instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1 OverheadActionNoUnroll ( + int64 invokeCount, + class [Perfolizer]Perfolizer.Horology.IClock clock + ) cil managed flags(0200) + */ + var invokeCountArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var methodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + OverheadActionNoUnrollMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + [ + invokeCountArg, + new EmitParameterInfo(1, ClockParamName, typeof(IClock)) + ] + ) + .SetAggressiveOptimizationImplementationFlag(); + invokeCountArg.SetMember(methodBuilder); + + var ilBuilder = methodBuilder.GetILGenerator(); + + /* + .locals init ( + [0] valuetype [Perfolizer]Perfolizer.Horology.StartedClock startedClock + ) + */ + var startedClockLocal = ilBuilder.DeclareLocal(typeof(StartedClock)); + + var startLoopLabel = ilBuilder.DefineLabel(); // IL_000f + + /* + // StartedClock startedClock = ClockExtensions.Start(clock); + IL_0000: ldarg.2 + IL_0001: call valuetype [Perfolizer]Perfolizer.Horology.StartedClock [Perfolizer]Perfolizer.Horology.ClockExtensions::Start(class [Perfolizer]Perfolizer.Horology.IClock) + IL_0006: stloc.0 + */ + ilBuilder.Emit(OpCodes.Ldarg_2); + ilBuilder.Emit(OpCodes.Call, GetStartClockMethod()); + ilBuilder.EmitStloc(startedClockLocal); + + // loop + ilBuilder.EmitLoopBeginFromArgToZero(out var loopStartLabel, out var loopHeadLabel); + { + /* + // __Overhead(); + IL_0009: ldarg.0 + IL_000a: call instance void BenchmarkDotNet.Autogenerated.Runnable_1::__Overhead() + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + EmitLoadArgFieldsForCall(ilBuilder, null); + ilBuilder.Emit(OpCodes.Call, overheadImplementationMethod); + } + ilBuilder.EmitLoopEndFromArgToZero(loopStartLabel, loopHeadLabel, invokeCountArg); + + /* + // return new ValueTask(startedClock.GetElapsed()); + IL_001a: ldloca.s 0 + IL_001c: call instance valuetype [Perfolizer]Perfolizer.Horology.ClockSpan [Perfolizer]Perfolizer.Horology.StartedClock::GetElapsed() + IL_0021: newobj instance void valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1::.ctor(!0) + IL_0026: ret + */ + ilBuilder.EmitLdloca(startedClockLocal); + ilBuilder.Emit(OpCodes.Call, typeof(StartedClock).GetMethod(nameof(StartedClock.GetElapsed), BindingFlags.Public | BindingFlags.Instance)!); + ilBuilder.Emit(OpCodes.Newobj, typeof(ValueTask).GetConstructor([typeof(ClockSpan)])!); + ilBuilder.Emit(OpCodes.Ret); + + return methodBuilder; + } + + void EmitUnrollMethod() + { + /* + // private ValueTask OverheadActionUnroll(long invokeCount, IClock clock) + .method private hidebysig + instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1 OverheadActionUnroll ( + int64 invokeCount, + class [Perfolizer]Perfolizer.Horology.IClock clock + ) cil managed flags(0200) + */ + var methodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + OverheadActionUnrollMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + [ + new EmitParameterInfo(0, InvokeCountParamName, typeof(long)), + new EmitParameterInfo(1, ClockParamName, typeof(IClock)) + ] + ) + .SetAggressiveOptimizationImplementationFlag(); + + var ilBuilder = methodBuilder.GetILGenerator(); + + /* + // return OverheadActionNoUnroll(invokeCount * 16, clock); + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: ldc.i4.s 16 + IL_0004: conv.i8 + IL_0005: mul + IL_0006: ldarg.2 + IL_0007: call instance valuetype [System.Threading.Tasks.Extensions]System.Threading.Tasks.ValueTask`1 BenchmarkDotNet.Autogenerated.Runnable_0::OverheadActionNoUnroll(int64, class [Perfolizer]Perfolizer.Horology.IClock) + IL_000c: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldarg_1); + ilBuilder.EmitLdc_I4(jobUnrollFactor); + ilBuilder.Emit(OpCodes.Conv_I8); + ilBuilder.Emit(OpCodes.Mul); + ilBuilder.Emit(OpCodes.Ldarg_2); + ilBuilder.Emit(OpCodes.Call, noUnrollMethod); + ilBuilder.Emit(OpCodes.Ret); + } + } + + private Type GetWorkloadCoreAsyncMethodBuilderType() + { + // If the benchmark method overrode the caller type, use that type to get the builder type. + if (Descriptor.WorkloadMethod.ResolveAttribute() is AsyncCallerTypeAttribute asyncCallerTypeAttribute) + { + return GetBuilderTypeFromUserSpecifiedAsyncType(asyncCallerTypeAttribute.AsyncCallerType); + } + // If the benchmark method overrode the builder, use the same builder. + if (Descriptor.WorkloadMethod.GetAsyncMethodBuilderAttribute() is { } methodAttr + && methodAttr.GetType().GetProperty(nameof(AsyncMethodBuilderAttribute.BuilderType), BindingFlags.Public | BindingFlags.Instance)?.GetValue(methodAttr) is Type methodBuilderType) + { + return methodBuilderType; + } + if (Descriptor.WorkloadMethod.ReturnType.GetAsyncMethodBuilderAttribute() is { } typeAttr + && typeAttr.GetType().GetProperty(nameof(AsyncMethodBuilderAttribute.BuilderType), BindingFlags.Public | BindingFlags.Instance)?.GetValue(typeAttr) is Type typeBuilderType) + { + return GetConcreteBuilderType(typeBuilderType); + } + // Task and Task are not annotated with their builder type, the C# compiler special-cases them. + if (Descriptor.WorkloadMethod.ReturnType.IsGenericType && Descriptor.WorkloadMethod.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + return GetConcreteBuilderType(typeof(AsyncTaskMethodBuilder<>)); + } + // Fallback to AsyncTaskMethodBuilder if the benchmark return type is Task or any awaitable type that is not a custom task-like type. + return typeof(AsyncTaskMethodBuilder); + + Type GetBuilderTypeFromUserSpecifiedAsyncType(Type asyncType) + { + if (asyncType.GetAsyncMethodBuilderAttribute() is { } typeAttr + && typeAttr.GetType().GetProperty(nameof(AsyncMethodBuilderAttribute.BuilderType), BindingFlags.Public | BindingFlags.Instance)?.GetValue(typeAttr) is Type typeBuilderType) + { + return GetConcreteBuilderType(typeBuilderType); + } + // Task and Task are not annotated with their builder type, the C# compiler special-cases them. + if (asyncType == typeof(Task)) + { + return typeof(AsyncTaskMethodBuilder); + } + if (asyncType.IsGenericType && asyncType.GetGenericTypeDefinition() == typeof(Task<>)) + { + return typeof(AsyncTaskMethodBuilder<>).MakeGenericType([asyncType.GetGenericArguments()[0]]); + } + throw new NotSupportedException($"AsyncMethodBuilderAttribute not found on type {asyncType.GetDisplayName()} from {Descriptor.DisplayInfo}"); + } + + Type GetConcreteBuilderType(Type builderType) + { + if (!builderType.IsGenericTypeDefinition) + { + return builderType; + } + if (builderType.GetGenericArguments().Length != 1) + { + throw new NotSupportedException($"AsyncMethodBuilder {builderType.GetDisplayName()} has generic arity greater than 1."); + } + var resultType = Descriptor.WorkloadMethod.ReturnType + .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)! + .ReturnType + .GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance)! + .ReturnType; + return builderType.MakeGenericType([resultType]); + } + } + + private void EmitWorkload() + { + var asyncMethodBuilderType = GetWorkloadCoreAsyncMethodBuilderType(); + var builderInfo = BeginAsyncStateMachineTypeBuilder(WorkloadCoreMethodName, asyncMethodBuilderType, runnableBuilder); + var (asyncStateMachineTypeBuilder, publicFields, (ilBuilder, endTryLabel, returnLabel, stateLocal, thisLocal, returnDefaultLocal)) = builderInfo; + var (stateField, builderField, _) = publicFields; + var startedClockField = asyncStateMachineTypeBuilder.DefineField( + "5__2", + typeof(StartedClock), + FieldAttributes.Private + ); + var workloadContinuerAwaiterField = asyncStateMachineTypeBuilder.DefineField( + "<>u__1", + typeof(object), + FieldAttributes.Private + ); + var benchmarkAwaiterField = asyncStateMachineTypeBuilder.DefineField( + "<>u__2", + Descriptor.WorkloadMethod.ReturnType + .GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)! + .ReturnType, + FieldAttributes.Private + ); + EmitMoveNextImpl(); + var asyncStateMachineType = CompleteAsyncStateMachineType(asyncMethodBuilderType, builderInfo); + + var workloadCoreMethod = EmitAsyncCallerStub(WorkloadCoreMethodName, asyncStateMachineType, publicFields); + + var startWorkloadMethod = EmitAsyncSingleCall(StartWorkloadMethodName, typeof(AsyncVoidMethodBuilder), workloadCoreMethod, false); + + var noUnrollMethod = EmitNoUnrollMethod(); + EmitUnrollMethod(); + + void EmitMoveNextImpl() + { + /* + .locals init ( + [3] class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource, + [4] valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1 awaitable, + [5] valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1, + [6] int64, + [7] class [System.Runtime]System.Exception e, + ) + */ + var workloadContinuerLocal = ilBuilder.DeclareLocal(typeof(WorkloadContinuerAndValueTaskSource)); + var benchmarkAwaitableLocal = Descriptor.WorkloadMethod.ReturnType.IsValueType + ? ilBuilder.DeclareLocal(Descriptor.WorkloadMethod.ReturnType) + : null; + var benchmarkAwaiterLocal = ilBuilder.DeclareLocal(benchmarkAwaiterField.FieldType); + var invokeCountLocal = ilBuilder.DeclareLocal(typeof(long)); + var exceptionLocal = ilBuilder.DeclareLocal(typeof(Exception)); + + var whileTrueLabel = ilBuilder.DefineLabel(); // IL_001d + var workloadContinuationLabel = ilBuilder.DefineLabel(); // IL_0059 + var workloadContinuationGetResultLabel = ilBuilder.DefineLabel(); // IL_0075 + var benchmarkContinuationLabel = ilBuilder.DefineLabel(); // IL_00f0 + var benchmarkContinuationGetResultLabel = ilBuilder.DefineLabel(); // IL_010d + var startClockLabel = ilBuilder.DefineLabel(); // IL_009a + var callBenchmarkLoopLabel = ilBuilder.DefineLabel(); // IL_0115 + var callBenchmarkLabel = ilBuilder.DefineLabel(); // IL_00b2 + + // Not sure why Roslyn emits this, but we match it exactly. + /* + IL_000e: ldloc.0 + // _ = 1; + IL_000f: ldc.i4.1 + IL_0010: pop + IL_0011: pop + IL_0012: nop + */ + ilBuilder.EmitLdloc(stateLocal); + ilBuilder.Emit(OpCodes.Ldc_I4_1); + ilBuilder.Emit(OpCodes.Pop); + ilBuilder.Emit(OpCodes.Pop); + ilBuilder.Emit(OpCodes.Nop); + + ilBuilder.BeginExceptionBlock(); + { + /* + // if (num != 0) + IL_0013: ldloc.0 + IL_0014: brfalse.s IL_0059 + */ + ilBuilder.EmitLdloc(stateLocal); + ilBuilder.Emit(OpCodes.Brfalse_S, workloadContinuationLabel); + + /* + // if (num != 1) + IL_0016: ldloc.0 + IL_0017: ldc.i4.1 + IL_0018: beq IL_00f0 + */ + ilBuilder.EmitLdloc(stateLocal); + ilBuilder.Emit(OpCodes.Ldc_I4_1); + ilBuilder.Emit(OpCodes.Beq, benchmarkContinuationLabel); + + /* + // awaiter2 = runnable_.__fieldsContainer.workloadContinuerAndValueTaskSource.GetAwaiter(); + IL_001d: ldloc.1 + IL_001e: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0023: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::workloadContinuerAndValueTaskSource + IL_0028: callvirt instance class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::GetAwaiter() + IL_002d: stloc.3 + */ + ilBuilder.MarkLabel(whileTrueLabel); + ilBuilder.EmitLdloc(thisLocal!); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldfld, workloadContinuerAndValueTaskSourceField); + ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadContinuerAndValueTaskSource).GetMethod(nameof(WorkloadContinuerAndValueTaskSource.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)!); + ilBuilder.EmitStloc(workloadContinuerLocal); + /* + // if (!awaiter2.IsCompleted) + IL_002e: ldloc.3 + IL_002f: callvirt instance bool [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::get_IsCompleted() + IL_0034: brtrue.s IL_0075 + */ + ilBuilder.EmitLdloc(workloadContinuerLocal); + ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadContinuerAndValueTaskSource).GetProperty(nameof(WorkloadContinuerAndValueTaskSource.IsCompleted), BindingFlags.Public | BindingFlags.Instance)!.GetMethod!); + ilBuilder.Emit(OpCodes.Brtrue_S, workloadContinuationGetResultLabel); + /* + // num = (<>1__state = 0); + IL_0036: ldarg.0 + IL_0037: ldc.i4.0 + IL_0038: dup + IL_0039: stloc.0 + IL_003a: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>1__state' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Dup); + ilBuilder.EmitStloc(stateLocal); + ilBuilder.Emit(OpCodes.Stfld, stateField); + /* + // <>u__1 = awaiter2; + IL_003f: ldarg.0 + IL_0040: ldloc.3 + IL_0041: stfld object BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>u__1' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdloc(workloadContinuerLocal); + ilBuilder.Emit(OpCodes.Stfld, workloadContinuerAwaiterField); + /* + // <>t__builder.AwaitUnsafeOnCompletedd__17>(ref awaiter2, ref this); + IL_0046: ldarg.0 + IL_0047: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>t__builder' + IL_004c: ldloca.s 3 + IL_004e: ldarg.0 + IL_004f: call instance void valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1::AwaitUnsafeOnCompletedd__17'>(!!0&, !!1&) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, builderField); + ilBuilder.EmitLdloca(workloadContinuerLocal); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, GetAwaitOnCompletedMethod(asyncMethodBuilderType, typeof(WorkloadContinuerAndValueTaskSource), asyncStateMachineTypeBuilder)); + /* + // return; + IL_0054: leave IL_01a7 + */ + ilBuilder.Emit(OpCodes.Leave, returnLabel); + + /* + // WorkloadContinuerAndValueTaskSource awaiter2 = (WorkloadContinuerAndValueTaskSource)<>u__1; + IL_0059: ldarg.0 + IL_005a: ldfld object BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>u__1' + IL_005f: castclass [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource + IL_0064: stloc.3 + */ + ilBuilder.MarkLabel(workloadContinuationLabel); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, workloadContinuerAwaiterField); + ilBuilder.Emit(OpCodes.Castclass, typeof(WorkloadContinuerAndValueTaskSource)); + ilBuilder.EmitStloc(workloadContinuerLocal); + /* + // <>u__1 = null; + IL_0065: ldarg.0 + IL_0066: ldnull + IL_0067: stfld object BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>u__1' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitSetFieldToDefault(workloadContinuerAwaiterField); + /* + // num = (<>1__state = -1); + IL_006c: ldarg.0 + IL_006d: ldc.i4.m1 + IL_006e: dup + IL_006f: stloc.0 + IL_0070: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>1__state' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_M1); + ilBuilder.Emit(OpCodes.Dup); + ilBuilder.EmitStloc(stateLocal); + ilBuilder.Emit(OpCodes.Stfld, stateField); + + /* + // awaiter2.GetResult(); + IL_0075: ldloc.3 + IL_0076: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::GetResult() + */ + ilBuilder.MarkLabel(workloadContinuationGetResultLabel); + ilBuilder.EmitLdloc(workloadContinuerLocal); + ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadContinuerAndValueTaskSource).GetMethod(nameof(WorkloadContinuerAndValueTaskSource.GetResult), BindingFlags.Public | BindingFlags.Instance)!); + /* + // if (!runnable_.__fieldsContainer.workloadContinuerAndValueTaskSource.IsCompleted) + IL_007b: ldloc.1 + IL_007c: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0081: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::workloadContinuerAndValueTaskSource + IL_0086: callvirt instance bool [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::get_IsCompleted() + IL_008b: brfalse.s IL_009a + */ + ilBuilder.EmitLdloc(thisLocal!); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldfld, workloadContinuerAndValueTaskSourceField); + ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadContinuerAndValueTaskSource).GetProperty(nameof(WorkloadContinuerAndValueTaskSource.IsCompleted), BindingFlags.Public | BindingFlags.Instance)!.GetMethod!); + ilBuilder.Emit(OpCodes.Brfalse_S, startClockLabel); + + /* + // result = default(GcStats); + IL_008d: ldloca.s 2 + IL_008f: initobj [BenchmarkDotNet]BenchmarkDotNet.Engines.GcStats + */ + ilBuilder.MaybeEmitSetLocalToDefault(returnDefaultLocal); + /* + IL_0095: leave IL_0193 + */ + ilBuilder.Emit(OpCodes.Leave, endTryLabel); + + /* + // 5__2 = ClockExtensions.Start(runnable_.__fieldsContainer.clock); + IL_009a: ldarg.0 + IL_009b: ldloc.1 + IL_009c: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_00a1: ldfld class [Perfolizer]Perfolizer.Horology.IClock BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::clock + IL_00a6: call valuetype [Perfolizer]Perfolizer.Horology.StartedClock [Perfolizer]Perfolizer.Horology.ClockExtensions::Start(class [Perfolizer]Perfolizer.Horology.IClock) + IL_00ab: stfld valuetype [Perfolizer]Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'5__2' + */ + ilBuilder.MarkLabel(startClockLabel); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdloc(thisLocal!); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldfld, clockField); + ilBuilder.Emit(OpCodes.Call, GetStartClockMethod()); + ilBuilder.Emit(OpCodes.Stfld, startedClockField); + /* + // goto IL_0115; + IL_00b0: br.s IL_0115 + */ + ilBuilder.Emit(OpCodes.Br, callBenchmarkLoopLabel); + + /* + // awaiter = ((AllSetupAndCleanupAttributeBenchmarksTask)runnable_).Benchmark2().GetAwaiter(); + IL_00b2: ldloc.1 + IL_00b3: call instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1 [BenchmarkDotNet.IntegrationTests]BenchmarkDotNet.IntegrationTests.AllSetupAndCleanupTest/AllSetupAndCleanupAttributeBenchmarksTask::Benchmark2() + IL_00b8: stloc.s 4 + IL_00ba: ldloca.s 4 + */ + ilBuilder.MarkLabel(callBenchmarkLabel); + if (!Descriptor.WorkloadMethod.IsStatic) + { + ilBuilder.EmitLdloc(thisLocal!); + } + EmitLoadArgFieldsForCall(ilBuilder, thisLocal); + ilBuilder.Emit(OpCodes.Call, Descriptor.WorkloadMethod); + if (benchmarkAwaitableLocal == null) + { + ilBuilder.Emit(OpCodes.Callvirt, Descriptor.WorkloadMethod.ReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)!); + } + else + { + ilBuilder.EmitStloc(benchmarkAwaitableLocal); + ilBuilder.EmitLdloca(benchmarkAwaitableLocal); + ilBuilder.Emit(OpCodes.Call, Descriptor.WorkloadMethod.ReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)!); + } + /* + // if (!awaiter.IsCompleted) + IL_00bc: call instance valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1 valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1::GetAwaiter() + IL_00c1: stloc.s 5 + IL_00c3: ldloca.s 5 + IL_00c5: call instance bool valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1::get_IsCompleted() + IL_00ca: brtrue.s IL_010d + */ + ilBuilder.EmitStloc(benchmarkAwaiterLocal); + ilBuilder.EmitLdloca(benchmarkAwaiterLocal); + ilBuilder.Emit(OpCodes.Call, benchmarkAwaiterField.FieldType.GetProperty(nameof(TaskAwaiter.IsCompleted), BindingFlags.Public | BindingFlags.Instance)!.GetMethod!); + ilBuilder.Emit(OpCodes.Brtrue_S, benchmarkContinuationGetResultLabel); + + /* + // num = (<>1__state = 1); + IL_00cc: ldarg.0 + IL_00cd: ldc.i4.1 + IL_00ce: dup + IL_00cf: stloc.0 + IL_00d0: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>1__state' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_1); + ilBuilder.Emit(OpCodes.Dup); + ilBuilder.EmitStloc(stateLocal); + ilBuilder.Emit(OpCodes.Stfld, stateField); + /* + // <>u__2 = awaiter; + IL_00d5: ldarg.0 + IL_00d6: ldloc.s 5 + IL_00d8: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>u__2' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdloc(benchmarkAwaiterLocal); + ilBuilder.Emit(OpCodes.Stfld, benchmarkAwaiterField); + /* + // <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); + IL_00dd: ldarg.0 + IL_00de: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>t__builder' + IL_00e3: ldloca.s 5 + IL_00e5: ldarg.0 + IL_00e6: call instance void valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1::AwaitUnsafeOnCompleted, valuetype BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'>(!!0&, !!1&) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, builderField); + ilBuilder.EmitLdloca(benchmarkAwaiterLocal); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, GetAwaitOnCompletedMethod(asyncMethodBuilderType, benchmarkAwaiterField.FieldType, asyncStateMachineTypeBuilder)); + /* + // return; + IL_00eb: leave IL_01a7 + */ + ilBuilder.Emit(OpCodes.Leave, returnLabel); + + /* + // awaiter = <>u__2; + IL_00f0: ldarg.0 + IL_00f1: ldfld valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>u__2' + IL_00f6: stloc.s 5 + */ + ilBuilder.MarkLabel(benchmarkContinuationLabel); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, benchmarkAwaiterField); + ilBuilder.EmitStloc(benchmarkAwaiterLocal); + /* + // <>u__2 = default(ValueTaskAwaiter); + IL_00f8: ldarg.0 + IL_00f9: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>u__2' + IL_00fe: initobj valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitSetFieldToDefault(benchmarkAwaiterField); + /* + // num = (<>1__state = -1); + IL_0104: ldarg.0 + IL_0105: ldc.i4.m1 + IL_0106: dup + IL_0107: stloc.0 + IL_0108: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'<>1__state' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_M1); + ilBuilder.Emit(OpCodes.Dup); + ilBuilder.EmitStloc(stateLocal); + ilBuilder.Emit(OpCodes.Stfld, stateField); + + /* + // awaiter.GetResult(); + IL_010d: ldloca.s 5 + IL_010f: call instance !0 valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1::GetResult() + IL_0114: pop + */ + ilBuilder.MarkLabel(benchmarkContinuationGetResultLabel); + ilBuilder.EmitLdloca(benchmarkAwaiterLocal); + var benchmarkAwaiterGetResultMethod = benchmarkAwaiterField.FieldType.GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance)!; + ilBuilder.Emit(OpCodes.Call, benchmarkAwaiterGetResultMethod); + if (benchmarkAwaiterGetResultMethod.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + + /* + // if (--runnable_.__fieldsContainer.invokeCount >= 0) + IL_0115: ldloc.1 + IL_0116: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_011b: ldflda int64 BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::invokeCount + IL_0120: dup + IL_0121: ldind.i8 + IL_0122: ldc.i4.1 + IL_0123: conv.i8 + IL_0124: sub + IL_0125: stloc.s 6 + IL_0127: ldloc.s 6 + IL_0129: stind.i8 + */ + ilBuilder.MarkLabel(callBenchmarkLoopLabel); + ilBuilder.EmitLdloc(thisLocal!); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldflda, invokeCountField); + ilBuilder.Emit(OpCodes.Dup); + ilBuilder.Emit(OpCodes.Ldind_I8); + ilBuilder.Emit(OpCodes.Ldc_I4_1); + ilBuilder.Emit(OpCodes.Conv_I8); + ilBuilder.Emit(OpCodes.Sub); + ilBuilder.EmitStloc(invokeCountLocal); + ilBuilder.EmitLdloc(invokeCountLocal); + ilBuilder.Emit(OpCodes.Stind_I8); + /* + // runnable_.__fieldsContainer.workloadContinuerAndValueTaskSource.SetResult(5__2.GetElapsed()); + IL_012a: ldloc.s 6 + IL_012c: ldc.i4.0 + IL_012d: conv.i8 + IL_012e: bge.s IL_00b2 + */ + ilBuilder.EmitLdloc(invokeCountLocal); + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Conv_I8); + ilBuilder.Emit(OpCodes.Bge, callBenchmarkLabel); + + /* + IL_0130: ldloc.1 + IL_0131: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0136: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::workloadContinuerAndValueTaskSource + IL_013b: ldarg.0 + IL_013c: ldflda valuetype [Perfolizer]Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'5__2' + IL_0141: call instance valuetype [Perfolizer]Perfolizer.Horology.ClockSpan [Perfolizer]Perfolizer.Horology.StartedClock::GetElapsed() + IL_0146: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::SetResult(valuetype [Perfolizer]Perfolizer.Horology.ClockSpan) + */ + ilBuilder.EmitLdloc(thisLocal!); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldfld, workloadContinuerAndValueTaskSourceField); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, startedClockField); + ilBuilder.Emit(OpCodes.Call, typeof(StartedClock).GetMethod(nameof(StartedClock.GetElapsed), BindingFlags.Public | BindingFlags.Instance)!); + ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadContinuerAndValueTaskSource).GetMethod(nameof(WorkloadContinuerAndValueTaskSource.SetResult), [typeof(ClockSpan)])!); + /* + // 5__2 = default(StartedClock); + IL_014b: ldarg.0 + IL_014c: ldflda valuetype [Perfolizer]Perfolizer.Horology.StartedClock BenchmarkDotNet.Autogenerated.Runnable_1/'<__WorkloadCore>d__17'::'5__2' + IL_0151: initobj [Perfolizer]Perfolizer.Horology.StartedClock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitSetFieldToDefault(startedClockField); + /* + // goto IL_001d; + IL_0157: br IL_001d + */ + ilBuilder.Emit(OpCodes.Br, whileTrueLabel); + } + // end .try + ilBuilder.BeginCatchBlock(typeof(Exception)); + { + /* + // catch (Exception exception) + IL_015c: stloc.s 7 + */ + ilBuilder.EmitStloc(exceptionLocal); + /* + // runnable_.__fieldsContainer.workloadContinuerAndValueTaskSource.SetException(exception); + IL_015e: ldloc.1 + IL_015f: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0164: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::workloadContinuerAndValueTaskSource + IL_0169: ldloc.s 7 + IL_016b: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::SetException(class [System.Runtime]System.Exception) + */ + ilBuilder.EmitLdloc(thisLocal!); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldfld, workloadContinuerAndValueTaskSourceField); + ilBuilder.EmitLdloc(exceptionLocal); + ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadContinuerAndValueTaskSource).GetMethod(nameof(WorkloadContinuerAndValueTaskSource.SetException), [typeof(Exception)])!); + /* + // result = default(GcStats); + IL_0170: ldloca.s 2 + IL_0172: initobj [BenchmarkDotNet]BenchmarkDotNet.Engines.GcStats + IL_0178: leave.s IL_0193 + */ + ilBuilder.MaybeEmitSetLocalToDefault(returnDefaultLocal); + + ilBuilder.EndExceptionBlock(); + } // end handler + } + + MethodInfo EmitNoUnrollMethod() + { + /* + // private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) + .method private hidebysig + instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1 WorkloadActionNoUnroll ( + int64 invokeCount, + class [Perfolizer]Perfolizer.Horology.IClock clock + ) cil managed flags(0200) + */ + var methodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + WorkloadActionNoUnrollMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + [ + new EmitParameterInfo(0, InvokeCountParamName, typeof(long)), + new EmitParameterInfo(1, ClockParamName, typeof(IClock)) + ] + ) + .SetAggressiveOptimizationImplementationFlag(); + + var ilBuilder = methodBuilder.GetILGenerator(); + + var callContinueLabel = ilBuilder.DefineLabel(); // IL_003b + + /* + // __fieldsContainer.invokeCount = invokeCount; + IL_0000: ldarg.0 + IL_0001: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0006: ldarg.1 + IL_0007: stfld int64 BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::invokeCount + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldarg_1); + ilBuilder.Emit(OpCodes.Stfld, invokeCountField); + /* + // __fieldsContainer.clock = clock; + IL_000c: ldarg.0 + IL_000d: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0012: ldarg.2 + IL_0013: stfld class [Perfolizer]Perfolizer.Horology.IClock BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::clock + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldarg_2); + ilBuilder.Emit(OpCodes.Stfld, clockField); + /* + // if (__fieldsContainer.workloadContinuerAndValueTaskSource == null) + IL_0018: ldarg.0 + IL_0019: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_001e: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::workloadContinuerAndValueTaskSource + IL_0023: brtrue.s IL_003b + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldfld, workloadContinuerAndValueTaskSourceField); + ilBuilder.Emit(OpCodes.Brtrue_S, callContinueLabel); + + /* + // __fieldsContainer.workloadContinuerAndValueTaskSource = new WorkloadContinuerAndValueTaskSource(); + IL_0025: ldarg.0 + IL_0026: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_002b: newobj instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::.ctor() + IL_0030: stfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::workloadContinuerAndValueTaskSource + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Newobj, typeof(WorkloadContinuerAndValueTaskSource).GetConstructor([])!); + ilBuilder.Emit(OpCodes.Stfld, workloadContinuerAndValueTaskSourceField); + /* + // __StartWorkload(); + IL_0035: ldarg.0 + IL_0036: call instance void BenchmarkDotNet.Autogenerated.Runnable_1::__StartWorkload() + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, startWorkloadMethod); + + /* + // return __fieldsContainer.workloadContinuerAndValueTaskSource.Continue(); + IL_003b: ldarg.0 + IL_003c: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0041: ldfld class [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::workloadContinuerAndValueTaskSource + IL_0046: callvirt instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1 [BenchmarkDotNet]BenchmarkDotNet.Engines.WorkloadContinuerAndValueTaskSource::Continue() + IL_004b: ret + */ + ilBuilder.MarkLabel(callContinueLabel); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + ilBuilder.Emit(OpCodes.Ldfld, workloadContinuerAndValueTaskSourceField); + ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadContinuerAndValueTaskSource).GetMethod(nameof(WorkloadContinuerAndValueTaskSource.Continue), BindingFlags.Public | BindingFlags.Instance)!); + ilBuilder.Emit(OpCodes.Ret); + + return methodBuilder; + } + + void EmitUnrollMethod() + { + /* + // private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + .method private hidebysig + instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1 WorkloadActionUnroll ( + int64 invokeCount, + class [Perfolizer]Perfolizer.Horology.IClock clock + ) cil managed flags(0200) + */ + var methodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + WorkloadActionUnrollMethodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + [ + new EmitParameterInfo(0, InvokeCountParamName, typeof(long)), + new EmitParameterInfo(1, ClockParamName, typeof(IClock)) + ] + ) + .SetAggressiveOptimizationImplementationFlag(); + + var ilBuilder = methodBuilder.GetILGenerator(); + + /* + // return WorkloadActionNoUnroll(invokeCount * 16, clock); + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: ldc.i4.s 16 + IL_0004: conv.i8 + IL_0005: mul + IL_0006: ldarg.2 + IL_0007: call instance valuetype [System.Threading.Tasks.Extensions]System.Threading.Tasks.ValueTask`1 BenchmarkDotNet.Autogenerated.Runnable_0::WorkloadActionNoUnroll(int64, class [Perfolizer]Perfolizer.Horology.IClock) + IL_000c: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldarg_1); + ilBuilder.EmitLdc_I4(jobUnrollFactor); + ilBuilder.Emit(OpCodes.Conv_I8); + ilBuilder.Emit(OpCodes.Mul); + ilBuilder.Emit(OpCodes.Ldarg_2); + ilBuilder.Emit(OpCodes.Call, noUnrollMethod); + ilBuilder.Emit(OpCodes.Ret); + } + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs new file mode 100644 index 0000000000..020d38c024 --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs @@ -0,0 +1,570 @@ +using BenchmarkDotNet.Helpers.Reflection.Emit; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; + +namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; + +partial class RunnableEmitter +{ + // Roslyn generates ordinals in declaration order of every member. + // We don't necessarily emit members in the same order (or at all in the case of Runnable_#.Run), so we map it to the expected Roslyn ordinal. + // This doesn't really matter for the runtime, but it helps with the NaiveRunnableEmitDiff tests. + private readonly Dictionary s_asyncMethodToOrdinalMap = new() + { + { GlobalSetupMethodName, 4 }, + { GlobalCleanupMethodName, 5 }, + { IterationSetupMethodName, 6 }, + { IterationCleanupMethodName, 7 }, + { OverheadActionUnrollMethodName, 12 }, + { OverheadActionNoUnrollMethodName, 13 }, + { WorkloadActionUnrollMethodName, 14 }, + { WorkloadActionNoUnrollMethodName, 15 }, + { StartWorkloadMethodName, 16 }, + { WorkloadCoreMethodName, 17 }, + }; + + private record struct AsyncStateMachineFields(FieldInfo StateField, FieldInfo BuilderField, FieldInfo? ThisField); + private record struct AsyncStateMachineMoveNextInfo(ILGenerator IlBuilder, Label EndTryLabel, Label ReturnLabel, LocalBuilder StateLocal, LocalBuilder? ThisLocal, LocalBuilder? ReturnDefaultLocal); + private record struct AsyncStateMachineBuilderInfo(TypeBuilder TypeBuilder, AsyncStateMachineFields PublicFields, AsyncStateMachineMoveNextInfo MoveNextInfo); + + private MethodInfo EmitAsyncCallerStub(string methodName, Type asyncStateMachineType, AsyncStateMachineFields asyncStateMachineFields) + { + var (stateField, builderField, thisField) = asyncStateMachineFields; + var asyncMethodBuilderType = builderField.FieldType; + + // AsyncVoidMethodBuilder for `async void` has no Task property. + var taskGetMethod = asyncMethodBuilderType.GetProperty(nameof(AsyncTaskMethodBuilder.Task), BindingFlags.Public | BindingFlags.Instance)?.GetMethod; + /* + .method private hidebysig + instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask __GlobalSetup () cil managed flags(0200) + */ + var methodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + taskGetMethod?.ReturnParameter ?? EmitParameterInfo.CreateReturnVoidParameter() + ) + .SetAggressiveOptimizationImplementationFlag(); + + // [AsyncStateMachine(typeof(<__GlobalSetup>d__4))] + var attrCtor = typeof(AsyncStateMachineAttribute).GetConstructor([typeof(Type)]) + ?? throw new MissingMemberException(nameof(AsyncStateMachineAttribute)); + methodBuilder.SetCustomAttribute(new CustomAttributeBuilder(attrCtor, [asyncStateMachineType])); + + ILGenerator ilBuilder = methodBuilder.GetILGenerator(); + /* + .locals init ( + [0] valuetype BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4' + ) + */ + var asyncStateMachineLocal = ilBuilder.DeclareLocal(asyncStateMachineType); + /* + // stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create(); + IL_0000: ldloca.s 0 + IL_0002: call valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create() + IL_0007: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + */ + ilBuilder.EmitLdloca(asyncStateMachineLocal); + ilBuilder.Emit(OpCodes.Call, asyncMethodBuilderType.GetMethod(nameof(AsyncTaskMethodBuilder.Create), BindingFlags.Public | BindingFlags.Static)!); + ilBuilder.Emit(OpCodes.Stfld, builderField); + if (thisField is not null) + { + /* + // stateMachine.<>4__this = this; + IL_000c: ldloca.s 0 + IL_000e: ldarg.0 + IL_000f: stfld class BenchmarkDotNet.Autogenerated.Runnable_0 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>4__this' + */ + ilBuilder.EmitLdloca(asyncStateMachineLocal); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Stfld, thisField); + } + /* + // stateMachine.<>1__state = -1; + IL_0014: ldloca.s 0 + IL_0016: ldc.i4.m1 + IL_0017: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + */ + ilBuilder.EmitLdloca(asyncStateMachineLocal); + ilBuilder.Emit(OpCodes.Ldc_I4_M1); + ilBuilder.Emit(OpCodes.Stfld, stateField); + /* + // stateMachine.<>t__builder.Start(ref stateMachine); + IL_001c: ldloca.s 0 + IL_001e: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_0023: ldloca.s 0 + IL_0025: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Startd__4'>(!!0&) + */ + ilBuilder.EmitLdloca(asyncStateMachineLocal); + ilBuilder.Emit(OpCodes.Ldflda, builderField); + ilBuilder.EmitLdloca(asyncStateMachineLocal); + var startMethod = asyncMethodBuilderType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Single(method => method.IsGenericMethod && method.Name == nameof(AsyncTaskMethodBuilder.Start)) + .MakeGenericMethod(asyncStateMachineType); + ilBuilder.Emit(OpCodes.Call, startMethod); + + if (taskGetMethod != null) + { + /* + // return stateMachine.<>t__builder.Task; + IL_002a: ldloca.s 0 + IL_002c: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_0031: call instance class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task() + */ + ilBuilder.EmitLdloca(asyncStateMachineLocal); + ilBuilder.Emit(OpCodes.Ldflda, builderField); + ilBuilder.Emit(OpCodes.Call, taskGetMethod); + } + /* + IL_0036: ret + */ + ilBuilder.Emit(OpCodes.Ret); + + return methodBuilder; + } + + private AsyncStateMachineBuilderInfo BeginAsyncStateMachineTypeBuilder(string callerMethodName, Type asyncMethodBuilderType, Type? thisType) + { + /* + [StructLayout(LayoutKind.Auto)] + [CompilerGenerated] + private struct <__GlobalSetup>d__4 : IAsyncStateMachine + */ + int ordinal = s_asyncMethodToOrdinalMap[callerMethodName]; + var asyncStateMachineTypeBuilder = runnableBuilder.DefineNestedType( + $"<{callerMethodName}>d__{ordinal}", + TypeAttributes.NestedPrivate | TypeAttributes.AutoLayout | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + typeof(ValueType)); + nestedTypeBuilders.Add(asyncStateMachineTypeBuilder); + + asyncStateMachineTypeBuilder.AddInterfaceImplementation(typeof(IAsyncStateMachine)); + + var attrCtor = typeof(CompilerGeneratedAttribute).GetConstructor([]) + ?? throw new MissingMemberException(nameof(CompilerGeneratedAttribute)); + asyncStateMachineTypeBuilder.SetCustomAttribute(new CustomAttributeBuilder(attrCtor, [])); + + /* + .field public int32 '<>1__state' + .field public valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder '<>t__builder' + .field public class BenchmarkDotNet.Autogenerated.Runnable_0 '<>4__this' + */ + var stateField = asyncStateMachineTypeBuilder.DefineField("<>1__state", typeof(int), FieldAttributes.Public); + var builderField = asyncStateMachineTypeBuilder.DefineField("<>t__builder", asyncMethodBuilderType, FieldAttributes.Public); + var thisField = thisType is not null + ? asyncStateMachineTypeBuilder.DefineField("<>4__this", thisType, FieldAttributes.Public) + : null; + + var moveNextInfo = BeginEmitMoveNext(); + EmitSetStateMachine(); + + return new(asyncStateMachineTypeBuilder, new(stateField, builderField, thisField), moveNextInfo); + + AsyncStateMachineMoveNextInfo BeginEmitMoveNext() + { + /* + .method private final hidebysig newslot virtual + instance void MoveNext () cil managed flags(0200) + */ + var methodBuilder = asyncStateMachineTypeBuilder + .DefineMethod( + nameof(IAsyncStateMachine.MoveNext), + MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, + typeof(void), + [] + ) + .SetAggressiveOptimizationImplementationFlag(); + + /* + .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext() + */ + asyncStateMachineTypeBuilder.DefineMethodOverride( + methodBuilder, + typeof(IAsyncStateMachine).GetMethod(nameof(IAsyncStateMachine.MoveNext))! + ); + + var ilBuilder = methodBuilder.GetILGenerator(); + + var stateLocal = ilBuilder.DeclareLocal(typeof(int)); + var thisLocal = thisType is not null + ? ilBuilder.DeclareLocal(thisType) + : null; + var returnDefaultLocal = asyncMethodBuilderType.GetMethod(nameof(AsyncTaskMethodBuilder.SetResult), BindingFlags.Public | BindingFlags.Instance)!.GetParameters() is [var setResultParam] + ? ilBuilder.DeclareLocal(setResultParam.ParameterType) + : null; + + var endTryLabel = ilBuilder.DefineLabel(); // IL_0082 + var returnLabel = ilBuilder.DefineLabel(); // IL_0095 + + /* + // int num = <>1__state; + IL_0000: ldarg.0 + IL_0001: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + IL_0006: stloc.0 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, stateField); + ilBuilder.Emit(OpCodes.Stloc, stateLocal); + if (thisField is not null) + { + /* + // Runnable_0 runnable_ = <>4__this; + IL_0007: ldarg.0 + IL_0008: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>4__this' + IL_000d: stloc.1 + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, thisField); + ilBuilder.Emit(OpCodes.Stloc, thisLocal!); + } + + ilBuilder.BeginExceptionBlock(); + { + // Core impl emitted by caller. + } + // Catch block and the rest emitted by CompleteAsyncStateMachineTypeBuilder. + + return new(ilBuilder, endTryLabel, returnLabel, stateLocal, thisLocal, returnDefaultLocal); + } + + void EmitSetStateMachine() + { + /* + .method private final hidebysig newslot virtual + instance void SetStateMachine ( + class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine + ) cil managed flags(0200) + */ + var methodBuilder = asyncStateMachineTypeBuilder + .DefineMethod( + nameof(IAsyncStateMachine.SetStateMachine), + MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, + typeof(void), + [typeof(IAsyncStateMachine)] + ) + .SetAggressiveOptimizationImplementationFlag(); + + // [DebuggerHidden] + var attrCtor = typeof(DebuggerHiddenAttribute).GetConstructor([]) + ?? throw new MissingMemberException(nameof(DebuggerHiddenAttribute)); + methodBuilder.SetCustomAttribute(new CustomAttributeBuilder(attrCtor, [])); + + /* + .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine) + */ + asyncStateMachineTypeBuilder.DefineMethodOverride( + methodBuilder, + typeof(IAsyncStateMachine).GetMethod(nameof(IAsyncStateMachine.SetStateMachine))! + ); + + var ilBuilder = methodBuilder.GetILGenerator(); + + /* + // <>t__builder.SetStateMachine(stateMachine); + IL_0000: ldarg.0 + IL_0001: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_0006: ldarg.1 + IL_0007: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine) + IL_000c: ret + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, builderField); + ilBuilder.Emit(OpCodes.Ldarg_1); + ilBuilder.Emit(OpCodes.Call, asyncMethodBuilderType.GetMethod(nameof(AsyncTaskMethodBuilder.SetStateMachine), BindingFlags.Public | BindingFlags.Instance)!); + ilBuilder.Emit(OpCodes.Ret); + } + } + + private Type CompleteAsyncStateMachineType(Type asyncMethodBuilderType, AsyncStateMachineBuilderInfo asyncStateMachineBuilderInfo) + { + var (asyncStateMachineTypeBuilder, (stateField, builderField, _), (ilBuilder, endTryLabel, returnLabel, _, _, returnDefaultLocal)) = asyncStateMachineBuilderInfo; + + var setResultMethod = asyncMethodBuilderType.GetMethod(nameof(AsyncTaskMethodBuilder.SetResult), BindingFlags.Public | BindingFlags.Instance)!; + + // end .try + ilBuilder.BeginCatchBlock(typeof(Exception)); + { + /* + // catch (Exception exception) + IL_006b: stloc.3 + */ + var exLocal = ilBuilder.DeclareLocal(typeof(Exception)); + ilBuilder.Emit(OpCodes.Stloc, exLocal); + /* + // <>1__state = -2; + IL_006c: ldarg.0 + IL_006d: ldc.i4.s -2 + IL_006f: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_S, (sbyte) -2); + ilBuilder.Emit(OpCodes.Stfld, stateField); + /* + // <>t__builder.SetException(exception); + IL_0074: ldarg.0 + IL_0075: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_007a: ldloc.3 + IL_007b: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::SetException(class [System.Runtime]System.Exception) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, builderField); + ilBuilder.EmitLdloc(exLocal); + ilBuilder.Emit(OpCodes.Call, asyncMethodBuilderType.GetMethod(nameof(AsyncTaskMethodBuilder.SetException), BindingFlags.Public | BindingFlags.Instance)!); + + ilBuilder.EndExceptionBlock(); + } // end handler + /* + // <>1__state = -2; + IL_0082: ldarg.0 + IL_0083: ldc.i4.s -2 + IL_0085: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + */ + // IL_0082: + ilBuilder.MarkLabel(endTryLabel); + + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_S, (sbyte) -2); + ilBuilder.Emit(OpCodes.Stfld, stateField); + /* + // <>t__builder.SetResult(); + IL_008a: ldarg.0 + IL_008b: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_0090: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::SetResult() + + -or- + + // <>t__builder.SetResult(result); + IL_018f: ldarg.0 + IL_0190: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1 BenchmarkDotNet.Autogenerated.Runnable_0/'d__17'::'<>t__builder' + IL_0195: ldloc.2 + IL_0196: call instance void valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1::SetResult(!0) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, builderField); + if (returnDefaultLocal != null) + { + ilBuilder.EmitLdloc(returnDefaultLocal); + } + ilBuilder.Emit(OpCodes.Call, setResultMethod); + + // IL_0095: + ilBuilder.MarkLabel(returnLabel); + /* + IL_0095: ret + */ + ilBuilder.Emit(OpCodes.Ret); + + return asyncStateMachineTypeBuilder; + } + + /* + private async ValueTask __GlobalSetup() + { + await base.GlobalSetup(); + } + */ + private MethodInfo EmitAsyncSingleCall(string methodName, Type asyncMethodBuilderType, MethodInfo methodToCall, bool isGlobalCleanup) + { + bool needsThisLocal = !methodToCall.IsStatic + || (isGlobalCleanup && this is AsyncCoreEmitter); + var builderInfo = BeginAsyncStateMachineTypeBuilder(methodName, asyncMethodBuilderType, needsThisLocal ? runnableBuilder : null); + var (asyncStateMachineTypeBuilder, publicFields, (ilBuilder, _, returnLabel, stateLocal, thisLocal, _)) = builderInfo; + var (stateField, builderField, _) = publicFields; + EmitMoveNextImpl(); + var asyncStateMachineType = CompleteAsyncStateMachineType(asyncMethodBuilderType, builderInfo); + + return EmitAsyncCallerStub(methodName, asyncStateMachineType, publicFields); + + void EmitMoveNextImpl() + { + var getAwaiterMethod = methodToCall.ReturnType.GetMethod(nameof(Task.GetAwaiter), BindingFlags.Public | BindingFlags.Instance)!; + var awaiterType = getAwaiterMethod.ReturnType; + + // .field private valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter '<>u__1' + var awaiterField = asyncStateMachineTypeBuilder.DefineField( + "<>u__1", + awaiterType.IsValueType ? awaiterType : typeof(object), + FieldAttributes.Private); + + var awaiterLocal = ilBuilder.DeclareLocal(awaiterType); + var awaitableLocal = methodToCall.ReturnType.IsValueType + ? ilBuilder.DeclareLocal(methodToCall.ReturnType) + : null; + + var state0Label = ilBuilder.DefineLabel(); // IL_0046 + var completedLabel = ilBuilder.DefineLabel(); // IL_0062 + + /* + // if (num != 0) + IL_000e: ldloc.0 + IL_000f: brfalse.s IL_0046 + */ + ilBuilder.EmitLdloc(stateLocal); + ilBuilder.Emit(OpCodes.Brfalse_S, state0Label); + + if (isGlobalCleanup) + { + EmitExtraGlobalCleanup(ilBuilder, thisLocal); + } + + /* + // awaiter = runnable_.GlobalSetup().GetAwaiter(); + IL_0011: ldloc.1 + IL_0012: call instance class [System.Runtime]System.Threading.Tasks.Task [BenchmarkDotNet.IntegrationTests]BenchmarkDotNet.IntegrationTests.AllSetupAndCleanupTest/AllSetupAndCleanupAttributeBenchmarksTask::GlobalSetup() + IL_0017: callvirt instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter() + IL_001c: stloc.2 + */ + if (!methodToCall.IsStatic) + { + ilBuilder.EmitLdloc(thisLocal!); + } + ilBuilder.Emit(OpCodes.Call, methodToCall); + if (awaitableLocal is null) + { + ilBuilder.Emit(OpCodes.Callvirt, getAwaiterMethod); + } + else + { + ilBuilder.EmitStloc(awaitableLocal); + ilBuilder.EmitLdloca(awaitableLocal); + ilBuilder.Emit(OpCodes.Call, getAwaiterMethod); + } + ilBuilder.Emit(OpCodes.Stloc, awaiterLocal); + /* + // if (!awaiter.IsCompleted) + IL_001d: ldloca.s 2 + IL_001f: call instance bool [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted() + IL_0024: brtrue.s IL_0062 + */ + if (awaiterType.IsValueType) + { + ilBuilder.EmitLdloca(awaiterLocal); + ilBuilder.Emit(OpCodes.Call, awaiterType.GetProperty(nameof(TaskAwaiter.IsCompleted), BindingFlags.Public | BindingFlags.Instance)!.GetMethod!); + } + else + { + ilBuilder.EmitLdloc(awaiterLocal); + ilBuilder.Emit(OpCodes.Callvirt, awaiterType.GetProperty(nameof(TaskAwaiter.IsCompleted), BindingFlags.Public | BindingFlags.Instance)!.GetMethod!); + } + ilBuilder.Emit(OpCodes.Brtrue_S, completedLabel); + + /* + // num = (<>1__state = 0); + IL_0026: ldarg.0 + IL_0027: ldc.i4.0 + IL_0028: dup + IL_0029: stloc.0 + IL_002a: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Dup); + ilBuilder.Emit(OpCodes.Stloc, stateLocal); + ilBuilder.Emit(OpCodes.Stfld, stateField); + /* + // <>u__1 = awaiter; + IL_002f: ldarg.0 + IL_0030: ldloc.2 + IL_0031: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>u__1' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitLdloc(awaiterLocal); + ilBuilder.Emit(OpCodes.Stfld, awaiterField); + /* + // <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); + IL_0036: ldarg.0 + IL_0037: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_003c: ldloca.s 2 + IL_003e: ldarg.0 + IL_003f: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::AwaitUnsafeOnCompletedd__4'>(!!0&, !!1&) + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, builderField); + ilBuilder.EmitLdloca(awaiterLocal); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Call, GetAwaitOnCompletedMethod(asyncMethodBuilderType, awaiterType, asyncStateMachineTypeBuilder)); + /* + // return; + IL_0044: leave.s IL_0095 + */ + ilBuilder.Emit(OpCodes.Leave_S, returnLabel); + + /* + // awaiter = <>u__1; + IL_0046: ldarg.0 + IL_0047: ldfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>u__1' + IL_004c: stloc.2 + */ + // IL_0046: + ilBuilder.MarkLabel(state0Label); + + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldfld, awaiterField); + ilBuilder.Emit(OpCodes.Stloc, awaiterLocal); + /* + // <>u__1 = default(TaskAwaiter); + IL_004d: ldarg.0 + IL_004e: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>u__1' + IL_0053: initobj [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.EmitSetFieldToDefault(awaiterField); + /* + // num = (<>1__state = -1); + IL_0059: ldarg.0 + IL_005a: ldc.i4.m1 + IL_005b: dup + IL_005c: stloc.0 + IL_005d: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + */ + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_M1); + ilBuilder.Emit(OpCodes.Dup); + ilBuilder.Emit(OpCodes.Stloc, stateLocal); + ilBuilder.Emit(OpCodes.Stfld, stateField); + + /* + // awaiter.GetResult(); + IL_0062: ldloca.s 2 + IL_0064: call instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult() + IL_0069: leave.s IL_0082 + */ + // IL_0062: + ilBuilder.MarkLabel(completedLabel); + + var getResultMethod = awaiterType.GetMethod(nameof(TaskAwaiter.GetResult), BindingFlags.Public | BindingFlags.Instance)!; + if (awaiterType.IsValueType) + { + ilBuilder.EmitLdloca(awaiterLocal); + ilBuilder.Emit(OpCodes.Call, getResultMethod); + } + else + { + ilBuilder.EmitLdloc(awaiterLocal); + ilBuilder.Emit(OpCodes.Callvirt, getResultMethod); + } + if (getResultMethod.ReturnType != typeof(void)) + { + ilBuilder.Emit(OpCodes.Pop); + } + } + } + + private static MethodInfo GetAwaitOnCompletedMethod(Type asyncMethodBuilderType, Type awaiterType, Type asyncStateMachineType) + { + var awaitOnCompletedMethodName = typeof(ICriticalNotifyCompletion).IsAssignableFrom(awaiterType) + ? nameof(AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted) + : nameof(AsyncTaskMethodBuilder.AwaitOnCompleted); + return asyncMethodBuilderType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Single(method => method.IsGenericMethod && method.Name == awaitOnCompletedMethodName) + .MakeGenericMethod(awaiterType, asyncStateMachineType); + } +} \ 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 ee9623a914..86408569d4 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs @@ -8,13 +8,14 @@ using System.Runtime.CompilerServices; using System.Security; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers.Reflection.Emit; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Properties; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.Results; +using Perfolizer.Horology; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; @@ -24,35 +25,46 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation /// A helper type that emits code that matches BenchmarkType.txt template. /// IMPORTANT: this type IS NOT thread safe. /// - internal class RunnableEmitter + internal abstract partial class RunnableEmitter { /// /// Maps action args to fields that store arg values. /// - private class ArgFieldInfo - { - public ArgFieldInfo(FieldInfo field, Type argLocalsType, MethodInfo opImplicitMethod) - { - Field = field; - ArgLocalsType = argLocalsType; - OpImplicitMethod = opImplicitMethod; - } + private record struct ArgFieldInfo(FieldInfo Field, Type ArgLocalsType, MethodInfo? OpImplicitMethod); + + private readonly BuildPartition buildPartition; + private readonly ModuleBuilder moduleBuilder; + private readonly BenchmarkBuildInfo benchmark; + private readonly int jobUnrollFactor; - public FieldInfo Field { get; } + private readonly TypeBuilder runnableBuilder; + private readonly List nestedTypeBuilders = []; - public Type ArgLocalsType { get; } + private FieldBuilder fieldsContainerField = null!; + private readonly List argFields = []; + private FieldBuilder notElevenField = null!; - public MethodInfo OpImplicitMethod { get; } + private MethodBuilder overheadImplementationMethod = null!; + + private Descriptor Descriptor => benchmark.BenchmarkCase.Descriptor; + private Type BenchmarkReturnType => Descriptor.WorkloadMethod.ReturnType; + + private RunnableEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder, BenchmarkBuildInfo benchmark) + { + this.buildPartition = buildPartition; + this.moduleBuilder = moduleBuilder; + this.benchmark = benchmark; + jobUnrollFactor = benchmark.BenchmarkCase.Job.ResolveValue(RunMode.UnrollFactorCharacteristic, buildPartition.Resolver); + runnableBuilder = DefineRunnableTypeBuilder(); } /// - /// Emits assembly with runnables from current build partition.. + /// Emits assembly with runnables from current build partition. /// - public static Assembly EmitPartitionAssembly( - GenerateResult generateResult, - BuildPartition buildPartition, - ILogger logger) + public static Assembly EmitPartitionAssembly(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) { + if (buildPartition is null) throw new ArgumentNullException(nameof(buildPartition)); + var assemblyResultPath = generateResult.ArtifactsPaths.ExecutablePath; var assemblyFileName = Path.GetFileName(assemblyResultPath); var config = buildPartition.Benchmarks.First().Config; @@ -62,8 +74,11 @@ public static Assembly EmitPartitionAssembly( var moduleBuilder = DefineModuleBuilder(assemblyBuilder, assemblyFileName, saveToDisk); foreach (var benchmark in buildPartition.Benchmarks) { - var runnableEmitter = new RunnableEmitter(buildPartition, moduleBuilder); - runnableEmitter.EmitRunnableCore(benchmark); + var returnType = benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType; + RunnableEmitter runnableEmitter = returnType.IsAwaitable() + ? new AsyncCoreEmitter(buildPartition, moduleBuilder, benchmark) + : new SyncCoreEmitter(buildPartition, moduleBuilder, benchmark); + runnableEmitter.EmitRunnableCore(); } if (saveToDisk) @@ -75,7 +90,6 @@ public static Assembly EmitPartitionAssembly( return assemblyBuilder; } - private static bool ShouldSaveToDisk(IConfig config) { if (!BenchmarkDotNetInfo.Instance.IsRelease) @@ -127,12 +141,12 @@ private static void DefineAssemblyAttributes(AssemblyBuilder assemblyBuilder) ?? throw new MissingMemberException(nameof(RuntimeCompatibilityAttribute)); var attributeProp = typeof(RuntimeCompatibilityAttribute) - .GetProperty(nameof(RuntimeCompatibilityAttribute.WrapNonExceptionThrows))!; + .GetProperty(nameof(RuntimeCompatibilityAttribute.WrapNonExceptionThrows)); attBuilder = new CustomAttributeBuilder( attributeCtor, - constructorArgs: [], - namedProperties: [attributeProp], - propertyValues: [true]); + Array.Empty(), + new[] { attributeProp! }, + new object[] { true }); assemblyBuilder.SetCustomAttribute(attBuilder); // [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] @@ -166,15 +180,11 @@ private static ModuleBuilder DefineModuleBuilder(AssemblyBuilder assemblyBuilder return moduleBuilder; } - private static TypeBuilder DefineRunnableTypeBuilder( - BenchmarkBuildInfo benchmark, - ModuleBuilder moduleBuilder) + private TypeBuilder DefineRunnableTypeBuilder() { // .class public auto ansi sealed beforefieldinit BenchmarkDotNet.Autogenerated.Runnable_0 // extends [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark - var benchmarkDescriptor = benchmark.BenchmarkCase.Descriptor; - - var workloadType = benchmarkDescriptor.Type.GetTypeInfo(); + var workloadType = Descriptor.Type.GetTypeInfo(); var workloadTypeAttributes = workloadType.Attributes; if (workloadTypeAttributes.HasFlag(TypeAttributes.NestedPublic)) { @@ -190,11 +200,7 @@ private static TypeBuilder DefineRunnableTypeBuilder( return result; } - private static void EmitNoArgsMethodCallPopReturn( - MethodBuilder methodBuilder, - MethodInfo targetMethod, - ILGenerator ilBuilder, - bool forceDirectCall) + private static void EmitNoArgsMethodCallPopReturn(MethodBuilder methodBuilder, MethodInfo targetMethod, ILGenerator ilBuilder) { if (targetMethod == null) throw new ArgumentNullException(nameof(targetMethod)); @@ -213,7 +219,7 @@ private static void EmitNoArgsMethodCallPopReturn( */ if (targetMethod.IsStatic) { - ilBuilder.EmitStaticCall(targetMethod, Array.Empty()); + ilBuilder.EmitStaticCall(targetMethod, []); } else if (methodBuilder.IsStatic) { @@ -223,422 +229,225 @@ private static void EmitNoArgsMethodCallPopReturn( else { ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.EmitInstanceCallThisValueOnStack( - null, - targetMethod, - Array.Empty(), - forceDirectCall); + ilBuilder.EmitInstanceCallThisValueOnStack(null, targetMethod, [], true); } if (targetMethod.ReturnType != typeof(void)) ilBuilder.Emit(OpCodes.Pop); } - private readonly BuildPartition buildPartition; - private readonly ModuleBuilder moduleBuilder; - - private BenchmarkBuildInfo benchmark = default!; - private List argFields = default!; - private int jobUnrollFactor; - - private TypeBuilder runnableBuilder = default!; - private ConsumableTypeInfo consumableInfo = default!; - private ConsumableTypeInfo? globalSetupReturnInfo; - private ConsumableTypeInfo? globalCleanupReturnInfo; - private ConsumableTypeInfo? iterationSetupReturnInfo; - private ConsumableTypeInfo? iterationCleanupReturnInfo; - - private FieldBuilder globalSetupActionField = default!; - private FieldBuilder globalCleanupActionField = default!; - private FieldBuilder iterationSetupActionField = default!; - private FieldBuilder iterationCleanupActionField = default!; - private FieldBuilder notElevenField = default!; - - // ReSharper disable NotAccessedField.Local - private ConstructorBuilder ctorMethod = default!; - private MethodBuilder trickTheJitMethod = default!; - private MethodBuilder overheadImplementationMethod = default!; - private MethodBuilder overheadActionUnrollMethod = default!; - private MethodBuilder overheadActionNoUnrollMethod = default!; - private MethodBuilder workloadActionUnrollMethod = default!; - private MethodBuilder workloadActionNoUnrollMethod = default!; - private MethodBuilder forDisassemblyDiagnoserMethod = default!; - - private MethodBuilder globalSetupMethod = default!; - private MethodBuilder globalCleanupMethod = default!; - private MethodBuilder iterationSetupMethod = default!; - private MethodBuilder iterationCleanupMethod = default!; - - private MethodBuilder runMethod = default!; - // ReSharper restore NotAccessedField.Local - - private RunnableEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder) - { - if (buildPartition == null) - throw new ArgumentNullException(nameof(buildPartition)); - if (moduleBuilder == null) - throw new ArgumentNullException(nameof(moduleBuilder)); - - this.buildPartition = buildPartition; - this.moduleBuilder = moduleBuilder; - } - - private Descriptor Descriptor => benchmark.BenchmarkCase.Descriptor; - - // ReSharper disable once UnusedMethodReturnValue.Local - private Type EmitRunnableCore(BenchmarkBuildInfo newBenchmark) + private void EmitRunnableCore() { - if (newBenchmark == null) - throw new ArgumentNullException(nameof(newBenchmark)); - - InitForEmitRunnable(newBenchmark); - - // 1. Emit fields - DefineFields(); - - // 2. Define members - ctorMethod = DefineCtor(); - trickTheJitMethod = DefineTrickTheJitMethod(); - - // Overhead impl - overheadImplementationMethod = EmitOverheadImplementation(OverheadImplementationMethodName); - overheadActionUnrollMethod = EmitOverheadAction(OverheadActionUnrollMethodName, jobUnrollFactor); - overheadActionNoUnrollMethod = EmitOverheadAction(OverheadActionNoUnrollMethodName, 1); - - // Workload impl - workloadActionUnrollMethod = EmitWorkloadAction(WorkloadActionUnrollMethodName, jobUnrollFactor); - workloadActionNoUnrollMethod = EmitWorkloadAction(WorkloadActionNoUnrollMethodName, 1); - - // __ForDisassemblyDiagnoser__ impl - forDisassemblyDiagnoserMethod = EmitForDisassemblyDiagnoser(ForDisassemblyDiagnoserMethodName); - - // 4. Instance completion - // Emit wrappers for setup/cleanup callbacks + EmitFields(); + EmitCtor(); EmitSetupCleanupMethods(); + EmitTrickTheJit(); + overheadImplementationMethod = EmitOverheadImplementation(OverheadImplementationMethodName); + EmitCoreImpl(); - // Emit methods that depend on others - EmitTrickTheJitBody(); - EmitCtorBody(); - - // 5. Emit Run() logic - runMethod = EmitRunMethod(); - -#if NETFRAMEWORK - return runnableBuilder.CreateType(); -#else - return runnableBuilder.CreateTypeInfo()!; -#endif - } - - private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark) - { - // Init current state - argFields = new List(); - benchmark = newBenchmark; - jobUnrollFactor = benchmark.BenchmarkCase.Job.ResolveValue( - RunMode.UnrollFactorCharacteristic, - buildPartition.Resolver); - - consumableInfo = new ConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType); - 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 type - runnableBuilder = DefineRunnableTypeBuilder(benchmark, moduleBuilder); + foreach (var nestedTypeBuilder in nestedTypeBuilders) + { + nestedTypeBuilder.CreateTypeInfo(); + } + runnableBuilder.CreateTypeInfo(); } - private static ConsumableTypeInfo? GetConsumableTypeInfo(Type? methodReturnType) - { - return methodReturnType == null ? null : new ConsumableTypeInfo(methodReturnType); - } + protected abstract void EmitCoreImpl(); - private void DefineFields() + private void EmitFields() { - globalSetupActionField = - runnableBuilder.DefineField(GlobalSetupActionFieldName, typeof(Action), FieldAttributes.Private); - globalCleanupActionField = - runnableBuilder.DefineField(GlobalCleanupActionFieldName, typeof(Action), FieldAttributes.Private); - iterationSetupActionField = - runnableBuilder.DefineField(IterationSetupActionFieldName, typeof(Action), FieldAttributes.Private); - iterationCleanupActionField = - runnableBuilder.DefineField(IterationCleanupActionFieldName, typeof(Action), FieldAttributes.Private); - - // Define arg fields - foreach (var parameter in Descriptor.WorkloadMethod.GetParameters()) + var parameters = Descriptor.WorkloadMethod.GetParameters(); + if (parameters.Length + GetExtraFieldsCount() > 0) { - var argValue = benchmark.BenchmarkCase.Parameters.GetArgument(parameter.Name!); - var parameterType = parameter.ParameterType; - - Type argLocalsType; - Type argFieldType; - MethodInfo? opConversion = null; - if (parameterType.IsByRef) - { - argLocalsType = parameterType; - argFieldType = argLocalsType.GetElementType() - ?? throw new InvalidOperationException($"Bug: cannot get field type from {argLocalsType}"); - } - else if (IsRefLikeType(parameterType) && argValue.Value != null) - { - argLocalsType = parameterType; - - // Use conversion on load; store passed value - var passedArgType = argValue.Value.GetType(); - opConversion = GetImplicitConversionOpFromTo(passedArgType, argLocalsType) ?? - throw new InvalidOperationException($"Bug: No conversion from {passedArgType} to {argLocalsType}."); - argFieldType = passedArgType; - } - else + /* + private unsafe struct FieldsContainer + { + } + */ + var fieldsContainerBuilder = runnableBuilder.DefineNestedType( + "FieldsContainer", + TypeAttributes.NestedPrivate | TypeAttributes.AutoLayout | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + typeof(ValueType)); + nestedTypeBuilders.Add(fieldsContainerBuilder); + + // Define arg fields + foreach (var parameter in parameters) { - // No conversion; load ref to arg field; - argLocalsType = parameterType; - argFieldType = parameterType; + var argValue = benchmark.BenchmarkCase.Parameters.GetArgument(parameter.Name!); + var parameterType = parameter.ParameterType; + + Type argLocalsType; + Type argFieldType; + MethodInfo? opConversion = null; + if (parameterType.IsByRef) + { + argLocalsType = parameterType; + argFieldType = argLocalsType.GetElementType() + ?? throw new InvalidOperationException($"Bug: cannot get field type from {argLocalsType}"); + } + else if (parameterType.IsByRefLike() && argValue.Value != null) + { + argLocalsType = parameterType; + + // Use conversion on load; store passed value + var passedArgType = argValue.Value.GetType(); + opConversion = GetImplicitConversionOpFromTo(passedArgType, argLocalsType) + ?? throw new InvalidOperationException($"Bug: No conversion from {passedArgType} to {argLocalsType}."); + argFieldType = passedArgType; + } + else + { + // No conversion; load ref to arg field; + argLocalsType = parameterType; + argFieldType = parameterType; + } + + if (argFieldType.IsByRefLike()) + throw new NotSupportedException($"Passing ref readonly structs by ref is not supported (cannot store {argFieldType} as a class field)."); + + var argField = fieldsContainerBuilder.DefineField( + ArgFieldPrefix + parameter.Position, + argFieldType, + FieldAttributes.Public); + + argFields.Add(new(argField, argLocalsType, opConversion)); } - if (IsRefLikeType(argFieldType)) - throw new NotSupportedException( - $"Passing ref readonly structs by ref is not supported (cannot store {argFieldType} as a class field)."); + EmitExtraFields(fieldsContainerBuilder); - var argField = runnableBuilder.DefineField( - ArgFieldPrefix + parameter.Position, - argFieldType, + // private FieldsContainer __fieldsContainer; + fieldsContainerField = runnableBuilder.DefineField( + FieldsContainerName, + fieldsContainerBuilder, FieldAttributes.Private); - - argFields.Add(new ArgFieldInfo(argField, argLocalsType, opConversion!)); } notElevenField = runnableBuilder.DefineField(NotElevenFieldName, typeof(int), FieldAttributes.Public); } - private ConstructorBuilder DefineCtor() + protected virtual int GetExtraFieldsCount() => 0; + + protected virtual void EmitExtraFields(TypeBuilder fieldsContainerBuilder) { } + + private void EmitCtor() { // .method public hidebysig specialname rtspecialname // instance void.ctor() cil managed - return runnableBuilder.DefinePublicInstanceCtor(); - } - - private MethodBuilder DefineTrickTheJitMethod() - { - // .method public hidebysig - // instance void __TrickTheJIT__() cil managed noinlining nooptimization - var result = runnableBuilder - .DefinePublicNonVirtualVoidInstanceMethod(TrickTheJitCoreMethodName) - .SetNoInliningImplementationFlag() - .SetNoOptimizationImplementationFlag(); - - return result; + var ctorMethod = runnableBuilder.DefinePublicInstanceCtor(); + var ilBuilder = ctorMethod.GetILGenerator(); + ilBuilder.EmitCallBaseParameterlessCtor(ctorMethod); + ilBuilder.EmitCtorReturn(ctorMethod); } - private MethodBuilder EmitOverheadImplementation(string methodName) + protected void EmitLoadArgFieldsForCall(ILGenerator ilBuilder, LocalBuilder? runnableLocal) { - //.method private hidebysig - // instance void __Overhead(int64 arg0) cil managed - - // Replace arg names - var parameters = Descriptor.WorkloadMethod.GetParameters() - .Select(p => - (ParameterInfo)new EmitParameterInfo( - p.Position, - ArgParamPrefix + p.Position, - p.ParameterType, - p.Attributes, - null)) - .ToArray(); - - var methodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( - methodName, - MethodAttributes.Private, - EmitParameterInfo.CreateReturnVoidParameter(), - parameters) - .SetNoInliningImplementationFlag(); - - var ilBuilder = methodBuilder.GetILGenerator(); /* - // return; - IL_0001: ret - */ - ilBuilder.EmitVoidReturn(methodBuilder); + // base.InvokeOnceVoid(__fieldsContainer.argField0, __fieldsContainer.argField1); + IL_000b: ldarg.0 + IL_000c: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0011: ldfld bool BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::argField0 + IL_0016: ldarg.0 + IL_0017: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_001c: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::argField1 - return methodBuilder; - } - - private MethodBuilder EmitOverheadAction(string methodName, int unrollFactor) - => EmitActionImpl(methodName, overheadImplementationMethod, EmitCallOverhead, unrollFactor); + // -or- - private MethodBuilder EmitWorkloadAction(string methodName, int unrollFactor) - => EmitActionImpl(methodName, Descriptor.WorkloadMethod, EmitCallWorkload, unrollFactor); + // base.InvokeOnceVoid(ref __fieldsContainer.argField0, ref __fieldsContainer.argField1); + IL_000b: ldarg.0 + IL_000c: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_2/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_2::__fieldsContainer + IL_0011: ldflda bool BenchmarkDotNet.Autogenerated.Runnable_2/FieldsContainer::argField0 + IL_0016: ldarg.0 + IL_0017: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_2/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_2::__fieldsContainer + IL_001c: ldflda int32 BenchmarkDotNet.Autogenerated.Runnable_2/FieldsContainer::argField1 - private void EmitCallOverhead(ILGenerator ilBuilder, IReadOnlyList argLocals) - { - /* - // __Overhead(); - IL_0008: ldarg.0 - IL_0009: call instance void BenchmarkDotNet.Autogenerated.Runnable_0::__Overhead() - */ - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.EmitLdLocals(argLocals); - ilBuilder.Emit(OpCodes.Call, overheadImplementationMethod); - } + // -or- (ref struct arg call) - private void EmitCallWorkload(ILGenerator ilBuilder, IReadOnlyList argLocals) - { - /* - // InvokeOnceVoid(); - IL_0008: ldarg.0 - IL_0009: call instance void [BenchmarkDotNet.IntegrationTests]BenchmarkDotNet.IntegrationTests.InProcessEmitTest/BenchmarkAllCases::InvokeOnceVoid() + // base.InvokeOnceVoid((Span)__fieldsContainer.argField0, __fieldsContainer.argField1); + IL_000b: ldarg.0 + IL_000c: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_0::__fieldsContainer + IL_0011: ldfld bool[] BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer::argField0 + IL_0016: call valuetype [System.Runtime]System.Span`1 valuetype [System.Runtime]System.Span`1::op_Implicit(!0[]) + IL_001b: ldarg.0 + IL_001c: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_0::__fieldsContainer + IL_0021: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer::argField1 */ - MethodInfo invokeMethod = Descriptor.WorkloadMethod; - if (!invokeMethod.IsStatic) + foreach (var argFieldInfo in argFields) { - ilBuilder.Emit(OpCodes.Ldarg_0); - } - ilBuilder.EmitLdLocals(argLocals); - ilBuilder.Emit(OpCodes.Call, invokeMethod); + if (runnableLocal is not null) + ilBuilder.EmitLdloc(runnableLocal); + else + ilBuilder.Emit(OpCodes.Ldarg_0); - if (consumableInfo.IsAwaitable) - { - /* - // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); - IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) - */ - ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod!); - } + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); - if (consumableInfo.WorkloadMethodReturnType != typeof(void)) - { - // IL_000b: pop - ilBuilder.Emit(OpCodes.Pop); + if (argFieldInfo.ArgLocalsType.IsByRef) + ilBuilder.Emit(OpCodes.Ldflda, argFieldInfo.Field); + else + ilBuilder.Emit(OpCodes.Ldfld, argFieldInfo.Field); + + if (argFieldInfo.OpImplicitMethod != null) + ilBuilder.Emit(OpCodes.Call, argFieldInfo.OpImplicitMethod); } } - private MethodBuilder EmitActionImpl(string methodName, MethodInfo invokeMethod, Action> callMethodEmitter, int unrollFactor) + private void EmitSetupCleanupMethods() { - // .method private hidebysig - // instance void OverheadActionUnroll(int64 invokeCount) cil managed aggressiveoptimization - var invokeCountArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); - var actionMethodBuilder = runnableBuilder.DefineNonVirtualInstanceMethod( - methodName, - MethodAttributes.Private, - EmitParameterInfo.CreateReturnVoidParameter(), - invokeCountArg) - .SetAggressiveOptimizationImplementationFlag(); - invokeCountArg.SetMember(actionMethodBuilder); - - // Emit impl - var ilBuilder = actionMethodBuilder.GetILGenerator(); - - // init locals - var argLocals = EmitDeclareArgLocals(ilBuilder); - - // load fields - EmitLoadArgFieldsToLocals(ilBuilder, argLocals); - - // loop - ilBuilder.EmitLoopBeginFromArgToZero(out var loopStartLabel, out var loopHeadLabel); - { - for (int u = 0; u < unrollFactor; u++) - { - callMethodEmitter(ilBuilder, argLocals); - } - } - ilBuilder.EmitLoopEndFromArgToZero(loopStartLabel, loopHeadLabel, invokeCountArg); - - // IL_003a: ret - ilBuilder.EmitVoidReturn(actionMethodBuilder); - - return actionMethodBuilder; + EmitSetupCleanup(GlobalSetupMethodName, Descriptor.GlobalSetupMethod, false); + EmitSetupCleanup(GlobalCleanupMethodName, Descriptor.GlobalCleanupMethod, true); + EmitSetupCleanup(IterationSetupMethodName, Descriptor.IterationSetupMethod, false); + EmitSetupCleanup(IterationCleanupMethodName, Descriptor.IterationCleanupMethod, false); } - private IReadOnlyList EmitDeclareArgLocals(ILGenerator ilBuilder, bool skipFirst = false) + private void EmitTrickTheJit() { - // NB: c# compiler does not store first arg in locals for static calls - /* - .locals init ( - [0] int64, // argFields[0] - [1] int32, // argFields[1] - ) - // -or- (static calls) - .locals init ( - [0] int32, // argFields[1] - ) - */ - bool first = true; - var argLocals = new List(argFields.Count); - foreach (var argField in argFields) - { - if (!first || !skipFirst) - { - argLocals.Add(ilBuilder.DeclareLocal(argField.ArgLocalsType)); - } + var forDisassemblyDiagnoserMethod = EmitForDisassemblyDiagnoserMethod(); - first = false; - } - - return argLocals; - } + // .method public hidebysig + // instance void __TrickTheJIT__() cil managed noinlining nooptimization + var trickTheJitMethod = runnableBuilder + .DefinePublicNonVirtualVoidInstanceMethod(TrickTheJitCoreMethodName) + .SetNoInliningImplementationFlag() + .SetNoOptimizationImplementationFlag(); - private 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; - if (argLocals.Count != argFields.Count + localsOffset) - throw new InvalidOperationException("Bug: argLocals.Count != _argFields.Count + localsOffset"); + var ilBuilder = trickTheJitMethod.GetILGenerator(); /* - // long _argField = __argField0; - IL_0000: ldarg.0 - IL_0001: ldfld int64 BenchmarkDotNet.Autogenerated.Runnable_0::__argField0 - IL_0006: stloc.0 - IL_0007: ldarg.1 - IL_0008: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0::__argField1 - IL_000c: stloc.1 - // -or- - // ref int _argField = ref __argField0; - IL_0000: ldarg.0 - IL_0001: ldflda int64 BenchmarkDotNet.Autogenerated.Runnable_0::__argField0 - IL_0006: stloc.0 - IL_0007: ldarg.1 - IL_000b: ldflda int32 BenchmarkDotNet.Autogenerated.Runnable_0::__argField1 - IL_000c: stloc.1 - // -or- (static call) - // long _argField = __argField0; - IL_0000: ldarg.0 - IL_0001: ldfld int64 BenchmarkDotNet.Autogenerated.Runnable_0::__argField0 - IL_0006: ldarg.1 - IL_0007: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0::__argField1 - IL_000b: stloc.0 // offset by -1 - // -or- (ref struct arg call) + // NotEleven = new Random(123).Next(0, 10); IL_0000: ldarg.0 - IL_0001: ldfld int32[] BenchmarkDotNet.Autogenerated.Runnable_0::__argField0 - IL_0006: call valuetype [System.Memory]System.Span`1 valuetype [System.Memory]System.Span`1::op_Implicit(!0[]) - IL_000b: stloc.0 - IL_000c: ldarg.1 - IL_000d: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0::__argField1 - IL_0012: stloc.1 + IL_0001: ldc.i4.s 123 + IL_0003: newobj instance void [mscorlib]System.Random::.ctor(int32) + IL_0008: ldc.i4.0 + IL_0009: ldc.i4.s 10 + IL_000b: callvirt instance int32 [mscorlib]System.Random::Next(int32, int32) + IL_0010: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0::NotEleven */ - for (int i = 0; i < argFields.Count; i++) - { - ilBuilder.Emit(OpCodes.Ldarg_0); - var argFieldInfo = argFields[i]; + var randomCtor = typeof(Random).GetConstructor(new[] { typeof(int) }) + ?? throw new MissingMemberException(nameof(Random)); + var randomNextMethod = typeof(Random).GetMethod(nameof(Random.Next), new[] { typeof(int), typeof(int) }) + ?? throw new MissingMemberException(nameof(Random.Next)); - if (argFieldInfo.ArgLocalsType.IsByRef) - ilBuilder.Emit(OpCodes.Ldflda, argFieldInfo.Field); - else - ilBuilder.Emit(OpCodes.Ldfld, argFieldInfo.Field); + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldc_I4_S, (byte)123); + ilBuilder.Emit(OpCodes.Newobj, randomCtor); + ilBuilder.Emit(OpCodes.Ldc_I4_0); + ilBuilder.Emit(OpCodes.Ldc_I4_S, (byte)10); + ilBuilder.Emit(OpCodes.Callvirt, randomNextMethod); + ilBuilder.Emit(OpCodes.Stfld, notElevenField); - if (argFieldInfo.OpImplicitMethod != null) - ilBuilder.Emit(OpCodes.Call, argFieldInfo.OpImplicitMethod); + /* + // __ForDisassemblyDiagnoser__(); + IL_0015: ldarg.0 + IL_0016: call instance int32 BenchmarkDotNet.Autogenerated.Runnable_0::__ForDisassemblyDiagnoser__() + IL_001b: pop + */ + EmitNoArgsMethodCallPopReturn(trickTheJitMethod, forDisassemblyDiagnoserMethod, ilBuilder); - var localsIndex = i + localsOffset; - if (localsIndex >= 0) - ilBuilder.EmitStloc(argLocals[localsIndex]); - } + // IL_001b: ret + ilBuilder.EmitVoidReturn(trickTheJitMethod); } - private MethodBuilder EmitForDisassemblyDiagnoser(string methodName) + private MethodBuilder EmitForDisassemblyDiagnoserMethod() { // .method public hidebysig // instance void __ForDisassemblyDiagnoser__() cil managed noinlining nooptimization @@ -646,23 +455,15 @@ private MethodBuilder EmitForDisassemblyDiagnoser(string methodName) var workloadReturnParameter = EmitParameterInfo.CreateReturnParameter(typeof(void)); var methodBuilder = runnableBuilder .DefineNonVirtualInstanceMethod( - methodName, + ForDisassemblyDiagnoserMethodName, MethodAttributes.Public, - workloadReturnParameter) + workloadReturnParameter + ) .SetNoInliningImplementationFlag() .SetNoOptimizationImplementationFlag(); var ilBuilder = methodBuilder.GetILGenerator(); - /* - .locals init ( - [0] int64, - ) - */ - // NB: c# compiler does not store first arg in locals for static calls - var skipFirstArg = workloadMethod.IsStatic; - var argLocals = EmitDeclareArgLocals(ilBuilder, skipFirstArg); - var notElevenLabel = ilBuilder.DefineLabel(); /* // if (NotEleven == 11) @@ -673,42 +474,27 @@ .locals init ( */ ilBuilder.Emit(OpCodes.Ldarg_0); ilBuilder.Emit(OpCodes.Ldfld, notElevenField); - ilBuilder.Emit(OpCodes.Ldc_I4_S, (byte)11); + ilBuilder.Emit(OpCodes.Ldc_I4_S, (byte) 11); ilBuilder.Emit(OpCodes.Bne_Un, notElevenLabel); { /* - // long _argField = __argField0; - IL_000a: ldarg.0 - IL_000b: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0::__argField0 - IL_0010: stloc.0 - */ - EmitLoadArgFieldsToLocals(ilBuilder, argLocals, skipFirstArg); - - /* - 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) + // base.Simple(__fieldsContainer.argField0, __fieldsContainer.argField1); + IL_000a: ldarg.0 + IL_000b: ldarg.0 + IL_000c: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_0011: ldfld bool BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::argField0 + IL_0016: ldarg.0 + IL_0017: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_1::__fieldsContainer + IL_001c: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_1/FieldsContainer::argField1 + IL_0021: call instance void [BenchmarkDotNet.IntegrationTests]BenchmarkDotNet.IntegrationTests.ArgumentsTests/WithArguments::Simple(bool, int32) */ if (!workloadMethod.IsStatic) { ilBuilder.Emit(OpCodes.Ldarg_0); } - ilBuilder.EmitLdLocals(argLocals); + EmitLoadArgFieldsForCall(ilBuilder, null); ilBuilder.Emit(OpCodes.Call, workloadMethod); - - if (consumableInfo.IsAwaitable) - { - /* - // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); - IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) - */ - ilBuilder.Emit(OpCodes.Call, consumableInfo.GetResultMethod!); - } - - if (consumableInfo.WorkloadMethodReturnType != typeof(void)) + if (BenchmarkReturnType != typeof(void)) { ilBuilder.Emit(OpCodes.Pop); } @@ -722,294 +508,49 @@ .locals init ( return methodBuilder; } - private void EmitSetupCleanupMethods() - { - // Emit Setup/Cleanup methods - // We emit empty method instead of EmptyAction = "() => { }" - 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, ConsumableTypeInfo? returnTypeInfo) - { - var methodBuilder = runnableBuilder.DefinePrivateVoidInstanceMethod(methodName); - - var ilBuilder = methodBuilder.GetILGenerator(); - - if (optionalTargetMethod != null) - { - if (returnTypeInfo?.IsAwaitable == true) - { - EmitAwaitableSetupTeardown(methodBuilder, optionalTargetMethod, ilBuilder, returnTypeInfo); - } - else - { - EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true); - } - } - - ilBuilder.EmitVoidReturn(methodBuilder); - - return methodBuilder; - } - - private void EmitAwaitableSetupTeardown( - MethodBuilder methodBuilder, - MethodInfo targetMethod, - ILGenerator ilBuilder, - ConsumableTypeInfo returnTypeInfo) - { - if (targetMethod == null) - throw new ArgumentNullException(nameof(targetMethod)); - - if (returnTypeInfo.WorkloadMethodReturnType == typeof(void)) - { - ilBuilder.Emit(OpCodes.Ldarg_0); - } - /* - // call for instance - // GlobalSetup(); - IL_0006: ldarg.0 - IL_0007: call instance void [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalSetup() - */ - /* - // call for static - // GlobalSetup(); - IL_0006: call string [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalCleanup() - */ - 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); - } - - /* - // BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...); - IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1) - */ - - ilBuilder.Emit(OpCodes.Call, returnTypeInfo.GetResultMethod!); - ilBuilder.Emit(OpCodes.Pop); - } - - private void EmitCtorBody() - { - var ilBuilder = ctorMethod.GetILGenerator(); - - ilBuilder.EmitCallBaseParameterlessCtor(ctorMethod); - - ilBuilder.EmitSetDelegateToThisField(globalSetupActionField, globalSetupMethod); - ilBuilder.EmitSetDelegateToThisField(globalCleanupActionField, globalCleanupMethod); - ilBuilder.EmitSetDelegateToThisField(iterationSetupActionField, iterationSetupMethod); - ilBuilder.EmitSetDelegateToThisField(iterationCleanupActionField, iterationCleanupMethod); - - ilBuilder.EmitCtorReturn(ctorMethod); - } - - private void EmitTrickTheJitBody() - { - var ilBuilder = trickTheJitMethod.GetILGenerator(); - - /* - // NotEleven = new Random(123).Next(0, 10); - IL_0000: ldarg.0 - IL_0001: ldc.i4.s 123 - IL_0003: newobj instance void [mscorlib]System.Random::.ctor(int32) - IL_0008: ldc.i4.0 - IL_0009: ldc.i4.s 10 - IL_000b: callvirt instance int32 [mscorlib]System.Random::Next(int32, int32) - IL_0010: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0::NotEleven - */ - var randomCtor = typeof(Random).GetConstructor(new[] { typeof(int) }) - ?? throw new MissingMemberException(nameof(Random)); - var randomNextMethod = typeof(Random).GetMethod(nameof(Random.Next), new[] { typeof(int), typeof(int) }) - ?? throw new MissingMemberException(nameof(Random.Next)); - - ilBuilder.Emit(OpCodes.Ldarg_0); - ilBuilder.Emit(OpCodes.Ldc_I4_S, (byte)123); - ilBuilder.Emit(OpCodes.Newobj, randomCtor); - ilBuilder.Emit(OpCodes.Ldc_I4_0); - ilBuilder.Emit(OpCodes.Ldc_I4_S, (byte)10); - ilBuilder.Emit(OpCodes.Callvirt, randomNextMethod); - ilBuilder.Emit(OpCodes.Stfld, notElevenField); - - /* - // __ForDisassemblyDiagnoser__(); - IL_0015: ldarg.0 - IL_0016: call instance int32 BenchmarkDotNet.Autogenerated.Runnable_0::__ForDisassemblyDiagnoser__() - IL_001b: pop - */ - EmitNoArgsMethodCallPopReturn(trickTheJitMethod, forDisassemblyDiagnoserMethod, ilBuilder, forceDirectCall: true); - - // IL_001b: ret - ilBuilder.EmitVoidReturn(trickTheJitMethod); - } - - private MethodBuilder EmitRunMethod() + private MethodBuilder EmitOverheadImplementation(string methodName) { - var prepareForRunMethodTemplate = typeof(RunnableReuse).GetMethod(nameof(RunnableReuse.PrepareForRun)) - ?? throw new MissingMemberException(nameof(RunnableReuse.PrepareForRun)); - (Job, EngineParameters, IEngineFactory) resultTuple = new(); - /* - .method public hidebysig static - void Run ( - class [BenchmarkDotNet]BenchmarkDotNet.Engines.IHost host, - class [BenchmarkDotNet]BenchmarkDotNet.Toolchains.Parameters.ExecuteParameters parameters, - ) cil managed + .method private hidebysig + instance void __Overhead (int64 arg0) cil managed noinlining flags(0200) */ - var argsExceptInstance = prepareForRunMethodTemplate - .GetParameters() - .Skip(1) - .Select(p => (ParameterInfo)new EmitParameterInfo(p.Position - 1, p.Name, p.ParameterType, p.Attributes, null)) + // Replace arg names + var parameters = Descriptor.WorkloadMethod.GetParameters() + .Select(p => + (ParameterInfo) new EmitParameterInfo( + p.Position, + ArgParamPrefix + p.Position, + p.ParameterType, + p.Attributes, + null)) .ToArray(); - var methodBuilder = runnableBuilder.DefineStaticMethod( - RunMethodName, - MethodAttributes.Public, - EmitParameterInfo.CreateReturnVoidParameter(), - argsExceptInstance); - argsExceptInstance = methodBuilder.GetEmitParameters(argsExceptInstance); - var hostArg = argsExceptInstance[0]; - var parametersArg = argsExceptInstance[1]; - - var ilBuilder = methodBuilder.GetILGenerator(); - - /* - .locals init ( - [0] class BenchmarkDotNet.Autogenerated.Runnable_0, - [1] class [BenchmarkDotNet]BenchmarkDotNet.Jobs.Job, - [2] class [BenchmarkDotNet]BenchmarkDotNet.Engines.EngineParameters, - [3] class [BenchmarkDotNet]BenchmarkDotNet.Engines.IEngineFactory, - [4] valuetype [BenchmarkDotNet]BenchmarkDotNet.Engines.RunResults + var methodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnVoidParameter(), + parameters ) - */ - var instanceLocal = ilBuilder.DeclareLocal(runnableBuilder); - var jobLocal = ilBuilder.DeclareLocal(typeof(Job)); - var engineParametersLocal = ilBuilder.DeclareLocal(typeof(EngineParameters)); - var engineFactoryLocal = ilBuilder.DeclareLocal(typeof(IEngineFactory)); - var runResultsLocal = ilBuilder.DeclareLocal(typeof(RunResults)); - - /* - // Runnable_0 instance = new Runnable_0(); - IL_0000: newobj instance void BenchmarkDotNet.Autogenerated.Runnable_0::.ctor() - IL_0005: stloc.0 - */ - ilBuilder.Emit(OpCodes.Newobj, ctorMethod); - ilBuilder.EmitStloc(instanceLocal); - - /* - // (Job, EngineParameters, IEngineFactory) valueTuple = RunnableReuse.PrepareForRun(instance, host, parameters); - IL_0006: ldloc.0 - IL_0007: ldarg.0 - IL_0008: ldarg.1 - IL_0009: call valuetype [mscorlib]System.ValueTuple`3 [BenchmarkDotNet]BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReuse::PrepareForRun(!!0, class [BenchmarkDotNet]BenchmarkDotNet.Engines.IHost, class [BenchmarkDotNet]BenchmarkDotNet.Toolchains.Parameters.ExecuteParameters) - */ - ilBuilder.EmitLdloc(instanceLocal); - ilBuilder.EmitLdarg(hostArg); - ilBuilder.EmitLdarg(parametersArg); - ilBuilder.Emit(OpCodes.Call, prepareForRunMethodTemplate.MakeGenericMethod(runnableBuilder)); - - /* - // Job job = valueTuple.Item1; - IL_000e: dup - IL_000f: ldfld !0 valuetype [mscorlib]System.ValueTuple`3::Item1 - IL_0014: stloc.1 - */ - ilBuilder.Emit(OpCodes.Dup); - ilBuilder.Emit(OpCodes.Ldfld, resultTuple.GetType().GetField(nameof(resultTuple.Item1))!); - ilBuilder.EmitStloc(jobLocal); - /* - // EngineParameters engineParameters = valueTuple.Item2; - IL_0015: dup - IL_0016: ldfld !1 valuetype [mscorlib]System.ValueTuple`3::Item2 - IL_001b: stloc.2 - */ - ilBuilder.Emit(OpCodes.Dup); - ilBuilder.Emit(OpCodes.Ldfld, resultTuple.GetType().GetField(nameof(resultTuple.Item2))!); - ilBuilder.EmitStloc(engineParametersLocal); - /* - // IEngineFactory engineFactory = valueTuple.Item3; - IL_001c: ldfld !2 valuetype [mscorlib]System.ValueTuple`3::Item3 - IL_0021: stloc.3 - */ - ilBuilder.Emit(OpCodes.Ldfld, resultTuple.GetType().GetField(nameof(resultTuple.Item3))!); - ilBuilder.EmitStloc(engineFactoryLocal); - - var notNullLabel = ilBuilder.DefineLabel(); - /* - // if (job != null) { ... } // translates to "if null: return; else: ..." - IL_0022: ldloc.1 - IL_0023: brtrue.s IL_0026 - IL_0025: ret - */ - ilBuilder.EmitLdloc(jobLocal); - ilBuilder.Emit(OpCodes.Brtrue_S, notNullLabel); - ilBuilder.EmitVoidReturn(methodBuilder); - - /* - // RunResults results = engineFactory.Create(engineParameters).Run(); - IL_0026: ldloc.3 - IL_0027: ldloc.2 - IL_0028: callvirt instance class [BenchmarkDotNet]BenchmarkDotNet.Engines.IEngine [BenchmarkDotNet]BenchmarkDotNet.Engines.IEngineFactory::Create(class [BenchmarkDotNet]BenchmarkDotNet.Engines.EngineParameters) - IL_002d: callvirt instance valuetype [BenchmarkDotNet]BenchmarkDotNet.Engines.RunResults [BenchmarkDotNet]BenchmarkDotNet.Engines.IEngine::Run() - IL_0032: stloc.s 4 - */ - var createReadyToRunMethod = typeof(IEngineFactory).GetMethod(nameof(IEngineFactory.Create)) - ?? throw new MissingMemberException(nameof(IEngineFactory.Create)); - var runMethodImpl = typeof(IEngine).GetMethod(nameof(IEngine.Run)) - ?? throw new MissingMemberException(nameof(IEngine.Run)); - ilBuilder.MarkLabel(notNullLabel); - ilBuilder.EmitLdloc(engineFactoryLocal); - ilBuilder.EmitLdloc(engineParametersLocal); - ilBuilder.Emit(OpCodes.Callvirt, createReadyToRunMethod); - ilBuilder.Emit(OpCodes.Callvirt, runMethodImpl); - ilBuilder.EmitStloc(runResultsLocal); - /* - // host.ReportResults(runResults); - IL_0034: ldarg.0 - IL_0035: ldloc.s 4 - IL_0037: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Engines.IHost::ReportResults(valuetype [BenchmarkDotNet]BenchmarkDotNet.Engines.RunResults) - */ + .SetNoInliningImplementationFlag() + .SetAggressiveOptimizationImplementationFlag(); - var reportResultsMethod = typeof(IHost).GetMethod(nameof(IHost.ReportResults)) - ?? throw new MissingMemberException(nameof(IHost.ReportResults)); - ilBuilder.EmitLdarg(hostArg); - ilBuilder.EmitLdloc(runResultsLocal); - ilBuilder.Emit(OpCodes.Callvirt, reportResultsMethod); + var ilBuilder = methodBuilder.GetILGenerator(); /* - // runnable_.__TrickTheJIT__(); - IL_003c: ldloc.0 - IL_003d: callvirt instance void BenchmarkDotNet.Autogenerated.ReplaceMe.Runnable_0::__TrickTheJIT__() + // return; + IL_0001: ret */ - ilBuilder.Emit(OpCodes.Ldloc_0); - ilBuilder.Emit(OpCodes.Callvirt, trickTheJitMethod); - /* - // engineParameters.InProcessDiagnoserHandler.Handle(BenchmarkSignal.AfterEngine); - IL_0042: ldloc.2 - IL_0043: callvirt instance class [BenchmarkDotNet]BenchmarkDotNet.Diagnosers.CompositeInProcessDiagnoserHandler [BenchmarkDotNet]BenchmarkDotNet.Engines.EngineParameters::get_InProcessDiagnoserHandler() - IL_0048: ldc.i4.5 - IL_0049: callvirt instance void [BenchmarkDotNet]BenchmarkDotNet.Diagnosers.CompositeInProcessDiagnoserHandler::Handle(valuetype [BenchmarkDotNet]BenchmarkDotNet.Engines.BenchmarkSignal) - */ - ilBuilder.EmitLdloc(engineParametersLocal); - ilBuilder.Emit(OpCodes.Callvirt, typeof(EngineParameters).GetProperty(nameof(EngineParameters.InProcessDiagnoserHandler))!.GetGetMethod()!); - ilBuilder.Emit(OpCodes.Ldc_I4_5); - ilBuilder.Emit(OpCodes.Callvirt, typeof(Diagnosers.CompositeInProcessDiagnoserHandler).GetMethod(nameof(Diagnosers.CompositeInProcessDiagnoserHandler.Handle))!); - ilBuilder.EmitVoidReturn(methodBuilder); return methodBuilder; } + + private MethodInfo GetStartClockMethod() + => typeof(ClockExtensions).GetMethod( + nameof(ClockExtensions.Start), + BindingFlags.Public | BindingFlags.Static, + null, + [typeof(IClock)], + null + )!; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs new file mode 100644 index 0000000000..1bcd7c06f4 --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SetupCleanupEmitter.cs @@ -0,0 +1,71 @@ +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers.Reflection.Emit; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; + +partial class RunnableEmitter +{ + private void EmitSetupCleanup(string methodName, MethodInfo? methodToCall, bool isGlobalCleanup) + { + if (methodToCall?.ReturnType.IsAwaitable() == true) + { + EmitAsyncSetupCleanup(methodName, methodToCall, isGlobalCleanup); + } + else + { + EmitSyncSetupCleanup(methodName, methodToCall, isGlobalCleanup); + } + } + + private void EmitSyncSetupCleanup(string methodName, MethodInfo? methodToCall, bool isGlobalCleanup) + { + /* + .method private hidebysig + instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask __GlobalSetup () cil managed flags(0200) + */ + var methodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)) + ) + .SetAggressiveOptimizationImplementationFlag(); + var ilBuilder = methodBuilder.GetILGenerator(); + /* + .locals init ( + [0] valuetype [System.Runtime]System.Threading.Tasks.ValueTask + ) + */ + var valueTaskLocal = ilBuilder.DeclareLocal(typeof(ValueTask)); + if (isGlobalCleanup) + { + EmitExtraGlobalCleanup(ilBuilder, null); + } + if (methodToCall != null) + { + EmitNoArgsMethodCallPopReturn(methodBuilder, methodToCall, ilBuilder); + } + /* + // return new ValueTask(); + IL_0000: ldloca.s 0 + IL_0002: initobj [System.Runtime]System.Threading.Tasks.ValueTask + IL_0008: ldloc.0 + IL_0009: ret + */ + ilBuilder.EmitLdloca(valueTaskLocal); + ilBuilder.Emit(OpCodes.Initobj, typeof(ValueTask)); + ilBuilder.EmitLdloc(valueTaskLocal); + ilBuilder.Emit(OpCodes.Ret); + } + + + private void EmitAsyncSetupCleanup(string methodName, MethodInfo methodToCall, bool isGlobalCleanup) + => EmitAsyncSingleCall(methodName, typeof(AsyncValueTaskMethodBuilder), methodToCall, isGlobalCleanup); + + // this.__fieldsContainer.workloadContinuerAndValueTaskSource?.Complete(); + protected abstract void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal); +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncCoreEmitter.cs new file mode 100644 index 0000000000..d4e94005dc --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncCoreEmitter.cs @@ -0,0 +1,164 @@ +using BenchmarkDotNet.Helpers.Reflection.Emit; +using BenchmarkDotNet.Running; +using Perfolizer.Horology; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading.Tasks; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; + +namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; + +partial class RunnableEmitter +{ + private sealed class SyncCoreEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder, BenchmarkBuildInfo benchmark) : RunnableEmitter(buildPartition, moduleBuilder, benchmark) + { + protected override void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { } + + protected override void EmitCoreImpl() + { + EmitAction(OverheadActionUnrollMethodName, overheadImplementationMethod, jobUnrollFactor); + EmitAction(OverheadActionNoUnrollMethodName, overheadImplementationMethod, 1); + EmitAction(WorkloadActionUnrollMethodName, Descriptor.WorkloadMethod, jobUnrollFactor); + EmitAction(WorkloadActionNoUnrollMethodName, Descriptor.WorkloadMethod, 1); + } + + private MethodBuilder EmitAction(string methodName, MethodInfo methodToCall, int unrollFactor) + { + /* + .method private hidebysig + instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1 OverheadActionNoUnroll ( + int64 invokeCount, + class [Perfolizer]Perfolizer.Horology.IClock clock + ) cil managed flags(0200) + */ + var invokeCountArg = new EmitParameterInfo(0, InvokeCountParamName, typeof(long)); + var actionMethodBuilder = runnableBuilder + .DefineNonVirtualInstanceMethod( + methodName, + MethodAttributes.Private, + EmitParameterInfo.CreateReturnParameter(typeof(ValueTask)), + [ + invokeCountArg, + new EmitParameterInfo(1, ClockParamName, typeof(IClock)) + ] + ) + .SetAggressiveOptimizationImplementationFlag(); + invokeCountArg.SetMember(actionMethodBuilder); + + var ilBuilder = actionMethodBuilder.GetILGenerator(); + + // init locals + var argLocals = argFields.Select(a => ilBuilder.DeclareLocal(a.ArgLocalsType)).ToList(); + var startedClockLocal = ilBuilder.DeclareLocal(typeof(StartedClock)); + + // load fields + EmitLoadArgFieldsToLocals(ilBuilder, argLocals); + + /* + // StartedClock startedClock = ClockExtensions.Start(clock); + IL_0000: ldarg.2 + IL_0001: call valuetype [Perfolizer]Perfolizer.Horology.StartedClock [Perfolizer]Perfolizer.Horology.ClockExtensions::Start(class [Perfolizer]Perfolizer.Horology.IClock) + IL_0006: stloc.0 + */ + ilBuilder.Emit(OpCodes.Ldarg_2); + ilBuilder.Emit(OpCodes.Call, GetStartClockMethod()); + ilBuilder.EmitStloc(startedClockLocal); + + // loop + ilBuilder.EmitLoopBeginFromArgToZero(out var loopStartLabel, out var loopHeadLabel); + { + for (int u = 0; u < unrollFactor; u++) + { + /* + // InvokeOnceVoid(); + IL_0008: ldarg.0 + IL_0009: call instance void [BenchmarkDotNet.IntegrationTests]BenchmarkDotNet.IntegrationTests.InProcessEmitTest/BenchmarkAllCases::InvokeOnceVoid() + */ + if (!methodToCall.IsStatic) + { + ilBuilder.Emit(OpCodes.Ldarg_0); + } + ilBuilder.EmitLdLocals(argLocals); + ilBuilder.Emit(OpCodes.Call, methodToCall); + + if (methodToCall.ReturnType != typeof(void)) + { + // IL_000b: pop + ilBuilder.Emit(OpCodes.Pop); + } + } + } + ilBuilder.EmitLoopEndFromArgToZero(loopStartLabel, loopHeadLabel, invokeCountArg); + + /* + // return new ValueTask(startedClock.GetElapsed()); + IL_0034: ldloca.s 2 + IL_0036: call instance valuetype [Perfolizer]Perfolizer.Horology.ClockSpan [Perfolizer]Perfolizer.Horology.StartedClock::GetElapsed() + IL_003b: newobj instance void valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1::.ctor(!0) + IL_0040: ret + */ + ilBuilder.EmitLdloca(startedClockLocal); + ilBuilder.Emit(OpCodes.Call, typeof(StartedClock).GetMethod(nameof(StartedClock.GetElapsed), BindingFlags.Public | BindingFlags.Instance)!); + ilBuilder.Emit(OpCodes.Newobj, typeof(ValueTask).GetConstructor([typeof(ClockSpan)])!); + ilBuilder.Emit(OpCodes.Ret); + + return actionMethodBuilder; + } + + private void EmitLoadArgFieldsToLocals(ILGenerator ilBuilder, List argLocals) + { + /* + // bool _argField = __fieldsContainer.argField0; + IL_0000: ldarg.0 + IL_0001: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_0::__fieldsContainer + IL_0006: ldfld bool BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer::argField0 + IL_000b: stloc.0 + // int _argField2 = __fieldsContainer.argField1; + IL_000c: ldarg.0 + IL_000d: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_0::__fieldsContainer + IL_0012: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer::argField1 + IL_0017: stloc.1 + + // -or- + + // ref bool _argField = ref __fieldsContainer.argField0; + IL_0000: ldarg.0 + IL_0001: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_0::__fieldsContainer + IL_0006: ldflda bool BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer::argField0 + IL_000b: stloc.0 + // ref int _argField2 = ref __fieldsContainer.argField1; + IL_000c: ldarg.0 + IL_000d: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_0::__fieldsContainer + IL_0012: ldflda int32 BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer::argField1 + IL_0017: stloc.1 + + // -or- (ref struct arg call) + + // Span arg = __fieldsContainer.argField0; + IL_0000: ldarg.0 + IL_0001: ldflda valuetype BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer BenchmarkDotNet.Autogenerated.Runnable_0::__fieldsContainer + IL_0006: ldfld int32[] BenchmarkDotNet.Autogenerated.Runnable_0/FieldsContainer::argField0 + IL_000b: call valuetype [System.Runtime]System.Span`1 valuetype [System.Runtime]System.Span`1::op_Implicit(!0[]) + IL_0010: stloc.0 + */ + for (int i = 0; i < argFields.Count; i++) + { + ilBuilder.Emit(OpCodes.Ldarg_0); + ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField); + + var argFieldInfo = argFields[i]; + if (argFieldInfo.ArgLocalsType.IsByRef) + ilBuilder.Emit(OpCodes.Ldflda, argFieldInfo.Field); + else + ilBuilder.Emit(OpCodes.Ldfld, argFieldInfo.Field); + + if (argFieldInfo.OpImplicitMethod != null) + ilBuilder.Emit(OpCodes.Call, argFieldInfo.OpImplicitMethod); + + ilBuilder.EmitStloc(argLocals[i]); + } + } + } +} \ 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 0a0ac33313..6d4be8161a 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs @@ -1,27 +1,18 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation { - /// - /// A helper type that emits code that matches BenchmarkType.txt template. - /// IMPORTANT: this type IS NOT thread safe. - /// - public class RunnableConstants + internal class RunnableConstants { - public const string IsByRefLikeAttributeTypeName = "System.Runtime.CompilerServices.IsByRefLikeAttribute"; public const string OpImplicitMethodName = "op_Implicit"; public const string DynamicAssemblySuffix = "Emitted"; public const string EmittedTypePrefix = "BenchmarkDotNet.Autogenerated.Runnable_"; - public const string ArgFieldPrefix = "__argField"; + public const string ArgFieldPrefix = "argField"; public const string ArgParamPrefix = "arg"; + public const string FieldsContainerName = "__fieldsContainer"; - public const string GlobalSetupActionFieldName = "globalSetupAction"; - public const string GlobalCleanupActionFieldName = "globalCleanupAction"; - public const string IterationSetupActionFieldName = "iterationSetupAction"; - public const string IterationCleanupActionFieldName = "iterationCleanupAction"; public const string NotElevenFieldName = "NotEleven"; public const string TrickTheJitCoreMethodName = "__TrickTheJIT__"; - public const string WorkloadImplementationMethodName = "__Workload"; public const string OverheadImplementationMethodName = "__Overhead"; public const string OverheadActionUnrollMethodName = "OverheadActionUnroll"; public const string OverheadActionNoUnrollMethodName = "OverheadActionNoUnroll"; @@ -29,14 +20,17 @@ 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 DummyParamName = "_"; + public const string GlobalSetupMethodName = "__GlobalSetup"; + public const string GlobalCleanupMethodName = "__GlobalCleanup"; + public const string IterationSetupMethodName = "__IterationSetup"; + public const string IterationCleanupMethodName = "__IterationCleanup"; - public const string GlobalSetupMethodName = "GlobalSetup"; - public const string GlobalCleanupMethodName = "GlobalCleanup"; - public const string IterationSetupMethodName = "IterationSetup"; - public const string IterationCleanupMethodName = "IterationCleanup"; - - public const string RunMethodName = "Run"; + public const string WorkloadContinuerAndValueTaskSourceFieldName = "workloadContinuerAndValueTaskSource"; + public const string ClockFieldName = "clock"; + public const string InvokeCountFieldName = "invokeCount"; + public const string StartWorkloadMethodName = "__StartWorkload"; + public const string WorkloadCoreMethodName = "__WorkloadCore"; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableProgram.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableProgram.cs deleted file mode 100644 index 294555415d..0000000000 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableProgram.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Reflection; -using BenchmarkDotNet.Engines; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Toolchains.Parameters; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; - -namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation -{ - internal class RunnableProgram - { - internal static int Run(Assembly partitionAssembly, IHost host, ExecuteParameters parameters) - { - // the first thing to do is to let diagnosers hook in before anything happens - // so all jit-related diagnosers can catch first jit compilation! - host.BeforeAnythingElse(); - - try - { - // we are not using Runnable here in any direct way in order to avoid strong dependency Main<=>Runnable - // which could cause the jitting/assembly loading to happen before we do anything - // we have some jitting diagnosers and we want them to catch all the informations!! - - var runCallback = GetRunCallback(parameters.BenchmarkId, partitionAssembly); - - runCallback.Invoke(null, [host, parameters]); - return 0; - } - catch (Exception oom) when ( - oom is OutOfMemoryException || - oom is TargetInvocationException reflection && reflection.InnerException is OutOfMemoryException) - { - DumpOutOfMemory(host, oom); - return -1; - } - catch (Exception ex) - { - DumpError(host, ex); - return -1; - } - finally - { - host.AfterAll(); - } - } - - private static MethodInfo GetRunCallback( - BenchmarkId benchmarkId, Assembly partitionAssembly) - { - var runnableType = partitionAssembly.GetType(GetRunnableTypeName(benchmarkId))!; - - var runnableMethod = runnableType.GetMethod(RunMethodName, BindingFlagsPublicStatic)!; - - return runnableMethod; - } - - private static string GetRunnableTypeName(BenchmarkId benchmarkId) - { - return EmittedTypePrefix + benchmarkId; - } - - private static void DumpOutOfMemory(IHost host, Exception oom) - { - host.WriteLine(); - host.WriteLine("OutOfMemoryException!"); - host.WriteLine( - "BenchmarkDotNet continues to run additional iterations until desired accuracy level is achieved. It's possible only if the benchmark method doesn't have any side-effects."); - host.WriteLine( - "If your benchmark allocates memory and keeps it alive, you are creating a memory leak."); - host.WriteLine( - "You should redesign your benchmark and remove the side-effects. You can use `OperationsPerInvoke`, `IterationSetup` and `IterationCleanup` to do that."); - host.WriteLine(); - host.WriteLine(oom.ToString()); - } - - private static void DumpError(IHost host, Exception ex) - { - host.WriteLine(); - host.WriteLine(ex.ToString()); - } - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs index f10eaf52a8..4085056ce3 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 @@ -37,12 +39,6 @@ internal static class RunnableReflectionHelpers return value; } - public static bool IsRefLikeType(Type t) - { - return t.IsValueType - && t.GetCustomAttributes().Any(a => a.GetType().FullName == IsByRefLikeAttributeTypeName); - } - public static MethodInfo? GetImplicitConversionOpFromTo(Type from, Type to) { return GetImplicitConversionOpCore(to, from, to) @@ -58,34 +54,28 @@ public static bool IsRefLikeType(Type t) && m.GetParameters().Single().ParameterType == from); } - public static void SetArgumentField( - T instance, - BenchmarkCase benchmarkCase, - ParameterInfo argInfo, - int argIndex - ) - where T : notnull + public static void SetArgumentField(object instance, BenchmarkCase benchmarkCase, ParameterInfo argInfo, int argIndex) { - var argValue = benchmarkCase.Parameters.GetArgument(argInfo.Name!); - var type = instance.GetType(); + var argValue = benchmarkCase.Parameters.GetArgument(argInfo.Name!) + ?? throw new InvalidOperationException($"Can't find arg member for {argInfo.Name}."); + + var containerField = instance.GetType().GetField(FieldsContainerName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + ?? throw new InvalidOperationException("FieldsContainer field not found on runnable instance."); + + var container = containerField.GetValue(instance)!; + var argName = ArgFieldPrefix + argIndex; - if (type.GetField(argName, BindingFlagsNonPublicInstance) is var f && f != null) - { - f.SetValue(instance, TryChangeType(argValue.Value, f.FieldType)); - } - else - { - throw new InvalidOperationException($"Can't find arg member for {argInfo.Name}."); - } + + var argField = container.GetType().GetField(argName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + ?? throw new InvalidOperationException($"Can't find arg member {argName} inside FieldsContainer."); + + argField.SetValue(container, TryChangeType(argValue.Value, argField.FieldType)); + containerField.SetValue(instance, container); } - public static void SetParameter( - T instance, - ParameterInstance paramInfo - ) - where T : notnull + public static void SetParameter(object instance, ParameterInstance paramInfo) { - var instanceArg = paramInfo.IsStatic ? null : (object)instance; + var instanceArg = paramInfo.IsStatic ? null : instance; var bindingFlags = paramInfo.IsStatic ? BindingFlagsAllStatic : BindingFlagsAllInstance; var type = instance.GetType(); @@ -103,32 +93,17 @@ ParameterInstance paramInfo } } - public static Action CallbackFromField(T instance, string memberName) - where T : notnull - { - return GetFieldValueCore(instance, memberName); - } - - public static Action LoopCallbackFromMethod(T instance, string memberName) - where T : notnull + public static Func SetupOrCleanupCallbackFromMethod(object instance, string memberName) { - return GetDelegateCore>(instance, memberName); + return GetDelegateCore>(instance, memberName); } - private static TResult GetFieldValueCore(T instance, string memberName) - where T : notnull + public static Func> LoopCallbackFromMethod(object instance, string memberName) { - var result = instance.GetType().GetField( - memberName, - BindingFlagsAllInstance); - if (result == null) - throw new InvalidOperationException($"Can't find a member {memberName}."); - - return (TResult)result.GetValue(instance)!; // TODO: Currently this method is used to get Action field and assume it's not null. + return GetDelegateCore>>(instance, memberName); } - private static TDelegate GetDelegateCore(T instance, string memberName) - where T : notnull + private static TDelegate GetDelegateCore(object instance, string memberName) { var result = instance.GetType().GetMethod( memberName, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReuse.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReuse.cs deleted file mode 100644 index 97971cca64..0000000000 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReuse.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Linq; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Engines; -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Exporters; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Toolchains.Parameters; -using BenchmarkDotNet.Validators; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; - -namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation -{ - public static class RunnableReuse - { - public static (Job, EngineParameters, IEngineFactory) PrepareForRun(T instance, IHost host, ExecuteParameters parameters) - where T : notnull - { - var benchmarkCase = parameters.BenchmarkCase; - FillObjectMembers(instance, benchmarkCase); - - DumpEnvironment(host); - - var job = CreateJob(benchmarkCase); - DumpJob(host, job); - - var errors = BenchmarkProcessValidator.Validate(job, instance); - if (ValidationErrorReporter.ReportIfAny(errors, host)) - return default; - - var compositeInProcessDiagnoserHandler = new CompositeInProcessDiagnoserHandler( - parameters.CompositeInProcessDiagnoser.InProcessDiagnosers - .Select((d, i) => InProcessDiagnoserRouter.Create(d, benchmarkCase, i)) - .Where(r => r.handler != null) - .ToArray(), - host, - parameters.DiagnoserRunMode, - new InProcessDiagnoserActionArgs(instance) - ); - if (parameters.DiagnoserRunMode == Diagnosers.RunMode.SeparateLogic) - { - compositeInProcessDiagnoserHandler.Handle(BenchmarkSignal.SeparateLogic); - return default; - } - compositeInProcessDiagnoserHandler.Handle(BenchmarkSignal.BeforeEngine); - - var engineParameters = CreateEngineParameters(instance, benchmarkCase, host, compositeInProcessDiagnoserHandler); - var engineFactory = GetEngineFactory(benchmarkCase); - - return (job, engineParameters, engineFactory); - } - - public static void FillObjectMembers(T instance, BenchmarkCase benchmarkCase) - where T : notnull - { - var argIndex = 0; - foreach (var argInfo in benchmarkCase.Descriptor.WorkloadMethod.GetParameters()) - { - SetArgumentField(instance, benchmarkCase, argInfo, argIndex); - argIndex++; - } - - foreach (var paramInfo in benchmarkCase.Parameters.Items - .Where(parameter => !parameter.IsArgument)) - { - SetParameter(instance, paramInfo); - } - } - - private static void DumpEnvironment(IHost host) - { - host.WriteLine(); - foreach (var infoLine in BenchmarkEnvironmentInfo.GetCurrent().ToFormattedString()) - { - host.WriteLine("// {0}", infoLine); - } - } - - private static Job CreateJob(BenchmarkCase benchmarkCase) - { - var job = new Job(); - job.Apply(benchmarkCase.Job); - job.Freeze(); - return job; - } - - private static void DumpJob(IHost host, Job job) - { - host.WriteLine("// Job: {0}", job.DisplayInfo); - host.WriteLine(); - } - - private static IEngineFactory GetEngineFactory(BenchmarkCase benchmarkCase) - { - return benchmarkCase.Job.ResolveValue( - InfrastructureMode.EngineFactoryCharacteristic, - InfrastructureResolver.Instance)!; - } - - private static EngineParameters CreateEngineParameters(T instance, BenchmarkCase benchmarkCase, IHost host, CompositeInProcessDiagnoserHandler inProcessDiagnoserHandler) - where T : notnull - => new() - { - Host = host, - WorkloadActionUnroll = LoopCallbackFromMethod(instance, WorkloadActionUnrollMethodName), - WorkloadActionNoUnroll = LoopCallbackFromMethod(instance, WorkloadActionNoUnrollMethodName), - OverheadActionNoUnroll = LoopCallbackFromMethod(instance, OverheadActionNoUnrollMethodName), - OverheadActionUnroll = LoopCallbackFromMethod(instance, OverheadActionUnrollMethodName), - GlobalSetupAction = CallbackFromField(instance, GlobalSetupActionFieldName), - GlobalCleanupAction = CallbackFromField(instance, GlobalCleanupActionFieldName), - IterationSetupAction = CallbackFromField(instance, IterationSetupActionFieldName), - IterationCleanupAction = CallbackFromField(instance, IterationCleanupActionFieldName), - TargetJob = benchmarkCase.Job, - OperationsPerInvoke = benchmarkCase.Descriptor.OperationsPerInvoke, - RunExtraIteration = benchmarkCase.Config.HasExtraIterationDiagnoser(benchmarkCase), - BenchmarkName = FullNameProvider.GetBenchmarkName(benchmarkCase), - InProcessDiagnoserHandler = inProcessDiagnoserHandler - }; - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs index 25a1271666..f0f59a2b6b 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs @@ -3,12 +3,12 @@ using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; -using BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; using BenchmarkDotNet.Toolchains.Parameters; using BenchmarkDotNet.Toolchains.Results; @@ -16,14 +16,15 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit { internal class InProcessEmitExecutor(bool executeOnSeparateThread) : IExecutor { - public ExecuteResult Execute(ExecuteParameters executeParameters) + public async ValueTask ExecuteAsync(ExecuteParameters executeParameters) { var host = new InProcessHost(executeParameters.BenchmarkCase, executeParameters.Logger, executeParameters.Diagnoser); int exitCode = -1; if (executeOnSeparateThread) { - var runThread = new Thread(() => exitCode = ExecuteCore(host, executeParameters)); + var taskCompletionSource = new TaskCompletionSource(); + var runThread = new Thread(async () => taskCompletionSource.SetResult(await ExecuteCore(host, executeParameters))); if (executeParameters.BenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes(false).Any() && OsDetector.IsWindows()) @@ -34,18 +35,19 @@ public ExecuteResult Execute(ExecuteParameters executeParameters) runThread.IsBackground = true; runThread.Start(); - runThread.Join(); + + exitCode = await taskCompletionSource.Task; } else { - exitCode = ExecuteCore(host, executeParameters); + exitCode = await ExecuteCore(host, executeParameters); } host.HandleInProcessDiagnoserResults(executeParameters.BenchmarkCase, executeParameters.CompositeInProcessDiagnoser); return ExecuteResult.FromRunResults(host.RunResults, exitCode); } - private int ExecuteCore(IHost host, ExecuteParameters parameters) + private async ValueTask ExecuteCore(IHost host, ExecuteParameters parameters) { int exitCode = -1; var process = Process.GetCurrentProcess(); @@ -65,10 +67,7 @@ private int ExecuteCore(IHost host, ExecuteParameters parameters) process.TrySetAffinity(affinity.Value, parameters.Logger); } - var generatedAssembly = ((InProcessEmitArtifactsPath)parameters.BuildResult.ArtifactsPaths) - .GeneratedAssembly; - - exitCode = RunnableProgram.Run(generatedAssembly, host, parameters); + exitCode = await InProcessEmitRunner.Run(host, parameters); } catch (Exception ex) { diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs new file mode 100644 index 0000000000..d3d8ef4f12 --- /dev/null +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs @@ -0,0 +1,141 @@ +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.Parameters; +using BenchmarkDotNet.Validators; +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; + +namespace BenchmarkDotNet.Toolchains.InProcess.Emit; + +internal static class InProcessEmitRunner +{ + public static async ValueTask Run(IHost host, ExecuteParameters parameters) + { + // the first thing to do is to let diagnosers hook in before anything happens + // so all jit-related diagnosers can catch first jit compilation! + host.BeforeAnythingElse(); + + try + { + var runnableType = ((InProcessEmitArtifactsPath) parameters.BuildResult.ArtifactsPaths) + .GeneratedAssembly + .GetType(EmittedTypePrefix + parameters.BenchmarkId)!; + + await RunCore(runnableType, host, parameters); + + return 0; + } + catch (Exception oom) when (oom is OutOfMemoryException || oom is TargetInvocationException reflection && reflection.InnerException is OutOfMemoryException) + { + host.WriteLine(); + host.WriteLine("OutOfMemoryException!"); + host.WriteLine("BenchmarkDotNet continues to run additional iterations until desired accuracy level is achieved. It's possible only if the benchmark method doesn't have any side-effects."); + host.WriteLine("If your benchmark allocates memory and keeps it alive, you are creating a memory leak."); + host.WriteLine("You should redesign your benchmark and remove the side-effects. You can use `OperationsPerInvoke`, `IterationSetup` and `IterationCleanup` to do that."); + host.WriteLine(); + host.WriteLine(oom.ToString()); + + return -1; + } + catch (Exception ex) + { + host.WriteLine(); + host.WriteLine(ex.ToString()); + return -1; + } + finally + { + host.AfterAll(); + } + } + + private static async ValueTask RunCore(Type runnableType, IHost host, ExecuteParameters parameters) + { + var benchmarkCase = parameters.BenchmarkCase; + + var instance = Activator.CreateInstance(runnableType)!; + FillMembers(instance, benchmarkCase); + + host.WriteLine(); + foreach (string infoLine in BenchmarkEnvironmentInfo.GetCurrent().ToFormattedString()) + { + host.WriteLine($"// {infoLine}"); + } + var job = new Job().Apply(benchmarkCase.Job).Freeze(); + host.WriteLine($"// Job: {job.DisplayInfo}"); + host.WriteLine(); + + var errors = BenchmarkProcessValidator.Validate(job, instance); + if (await ValidationErrorReporter.ReportIfAnyAsync(errors, host)) + return; + + var compositeInProcessDiagnoserHandler = new Diagnosers.CompositeInProcessDiagnoserHandler( + parameters.CompositeInProcessDiagnoser.InProcessDiagnosers + .Select((d, i) => Diagnosers.InProcessDiagnoserRouter.Create(d, benchmarkCase, i)) + .Where(r => r.handler != null) + .ToArray(), + host, + parameters.DiagnoserRunMode, + new Diagnosers.InProcessDiagnoserActionArgs(instance) + ); + if (parameters.DiagnoserRunMode == Diagnosers.RunMode.SeparateLogic) + { + await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.SeparateLogic); + return; + } + await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeEngine); + + var engineParameters = new EngineParameters() + { + Host = host, + WorkloadActionUnroll = LoopCallbackFromMethod(instance, WorkloadActionUnrollMethodName), + WorkloadActionNoUnroll = LoopCallbackFromMethod(instance, WorkloadActionNoUnrollMethodName), + OverheadActionNoUnroll = LoopCallbackFromMethod(instance, OverheadActionNoUnrollMethodName), + OverheadActionUnroll = LoopCallbackFromMethod(instance, OverheadActionUnrollMethodName), + GlobalSetupAction = SetupOrCleanupCallbackFromMethod(instance, GlobalSetupMethodName), + GlobalCleanupAction = SetupOrCleanupCallbackFromMethod(instance, GlobalCleanupMethodName), + IterationSetupAction = SetupOrCleanupCallbackFromMethod(instance, IterationSetupMethodName), + IterationCleanupAction = SetupOrCleanupCallbackFromMethod(instance, IterationCleanupMethodName), + TargetJob = benchmarkCase.Job, + OperationsPerInvoke = benchmarkCase.Descriptor.OperationsPerInvoke, + RunExtraIteration = benchmarkCase.Config.HasExtraIterationDiagnoser(benchmarkCase), + BenchmarkName = FullNameProvider.GetBenchmarkName(benchmarkCase), + InProcessDiagnoserHandler = compositeInProcessDiagnoserHandler + }; + + var results = await job + .ResolveValue(InfrastructureMode.EngineFactoryCharacteristic, InfrastructureResolver.Instance)! + .Create(engineParameters) + .RunAsync(); + host.ReportResults(results); + + runnableType.GetMethod(TrickTheJitCoreMethodName)!.Invoke(instance, []); + + await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterEngine); + } + + private static void FillMembers(object instance, BenchmarkCase benchmarkCase) + { + var argIndex = 0; + foreach (var argInfo in benchmarkCase.Descriptor.WorkloadMethod.GetParameters()) + { + SetArgumentField(instance, benchmarkCase, argInfo, argIndex); + argIndex++; + } + + foreach (var paramInfo in benchmarkCase.Parameters.Items) + { + if (!paramInfo.IsArgument) + { + SetParameter(instance, paramInfo); + } + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessHost.cs b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessHost.cs index 4c4b950253..4d4351e9cd 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessHost.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessHost.cs @@ -2,13 +2,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Runtime.CompilerServices; using System.Text; +using BenchmarkDotNet.Attributes.CompilerServices; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Loggers; -using BenchmarkDotNet.Portability; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; @@ -16,6 +15,7 @@ namespace BenchmarkDotNet.Toolchains.InProcess { /// Host API for in-process benchmarks. /// + [AggressivelyOptimizeMethods] internal sealed class InProcessHost : IHost { private readonly ILogger logger; @@ -52,14 +52,13 @@ public InProcessHost(BenchmarkCase benchmarkCase, ILogger logger, IDiagnoser? di /// Passes text to the host. /// Text to write. - public void Write(string message) => logger.Write(message); + public void WriteAsync(string message) => logger.Write(message); /// Passes new line to the host. public void WriteLine() => logger.WriteLine(); /// Passes text (new line appended) to the host. /// Text to write. - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] public void WriteLine(string message) { logger.WriteLine(message); @@ -71,7 +70,6 @@ public void WriteLine(string message) /// Sends notification signal to the host. /// The signal to send. - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] public void SendSignal(HostSignal hostSignal) => diagnoser?.Handle(hostSignal, diagnoserActionParameters!); public void SendError(string message) => logger.WriteLine(LogKind.Error, $"{ValidationErrorReporter.ConsoleErrorPrefix} {message}"); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs index b3ac4a5ee6..215813f0f4 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs @@ -91,7 +91,7 @@ job.Infrastructure.Toolchain is InProcessEmitToolchain /// The instance of validator that DOES fail on error. public static readonly IValidator FailOnError = new InProcessValidator(true); - public static IEnumerable Validate(BenchmarkCase benchmarkCase) + public static async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase) { foreach (var validationError in ValidateJob(benchmarkCase.Job, true)) { @@ -142,7 +142,7 @@ private InProcessValidator(bool failOnErrors) /// Proofs that benchmarks' jobs match the environment. /// The validation parameters. /// Enumerable of validation errors. - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { foreach (var benchmarkWithArguments in validationParameters.Benchmarks.Where(benchmark => benchmark.HasArguments && benchmark.GetToolchain() is InProcessNoEmitToolchain)) yield return new ValidationError(true, "Arguments are not supported by the InProcessNoEmitToolchain, see #687 for more details", benchmarkWithArguments); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkAction.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkAction.cs index 31f7250f99..cc4fe0b882 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkAction.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkAction.cs @@ -1,18 +1,13 @@ -using System; +using Perfolizer.Horology; +using System; +using System.Threading.Tasks; -namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit -{ - /// Common API to run the Setup/Clean/Idle/Run methods - internal abstract class BenchmarkAction - { - /// Gets or sets invoke single callback. - /// Invoke single callback. - public Action InvokeSingle { get; protected set; } = default!; +namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit; - /// Gets or sets invoke multiple times callback. - /// Invoke multiple times callback. - public Action InvokeUnroll { get; protected set; } = default!; - - public Action InvokeNoUnroll { get; protected set; } = default!; - } +internal abstract class BenchmarkAction +{ + public Func InvokeSingle { get; protected set; } = default!; + public Func> InvokeUnroll { get; protected set; } = default!; + public Func> InvokeNoUnroll { get; protected set; } = default!; + public abstract void Complete(); } \ 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 de4d5979ba..acde9bb5b7 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory.cs @@ -3,169 +3,140 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; - using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Running; -namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit +namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit; + +internal static partial class BenchmarkActionFactory { - /// Helper class that creates instances. - internal static partial class BenchmarkActionFactory + /// + /// Dispatch method that creates using + /// or to find correct implementation. + /// Either or should be not null. + /// + private static BenchmarkAction CreateCore(object instance, MethodInfo? targetMethod, MethodInfo? fallbackIdleSignature, int unrollFactor) { - /// - /// Dispatch method that creates using - /// or to find correct implementation. - /// Either or should be not null. - /// - private static BenchmarkAction CreateCore(object instance, MethodInfo? targetMethod, MethodInfo? fallbackIdleSignature, int unrollFactor) - { - PrepareInstanceAndResultType(instance, targetMethod, fallbackIdleSignature, out var resultInstance, out var resultType); - - if (resultType == typeof(void)) - return new BenchmarkActionVoid(resultInstance, targetMethod, unrollFactor); - - // targetMethod must not be null here. Because it's checked by PrepareInstanceAndResultType. - // Following null check is added to suppress nullable annotation errors. - if (targetMethod == null) - throw new ArgumentNullException(nameof(targetMethod)); - - if (resultType == typeof(void*)) - return new BenchmarkActionVoidPointer(resultInstance, targetMethod, unrollFactor); - - if (resultType.IsByRef) - { - var returnParameter = targetMethod.ReturnParameter; - // IsReadOnlyAttribute is not part of netstandard2.0, so we need to check the attribute name as usual. - if (returnParameter.GetCustomAttributes().Any(attribute => attribute.GetType().FullName == "System.Runtime.CompilerServices.IsReadOnlyAttribute")) - return Create( - typeof(BenchmarkActionByRefReadonly<>).MakeGenericType(resultType.GetElementType()!), - resultInstance, - targetMethod, - unrollFactor); + PrepareInstanceAndResultType(instance, targetMethod, fallbackIdleSignature, out var resultInstance, out var resultType); + + if (resultType == typeof(void)) + return new BenchmarkActionVoid(resultInstance, targetMethod, unrollFactor); + + // targetMethod must not be null here. Because it's checked by PrepareInstanceAndResultType. + // Following null check is added to suppress nullable annotation errors. + if (targetMethod == null) + throw new ArgumentNullException(nameof(targetMethod)); + if (resultType == typeof(void*)) + return new BenchmarkActionVoidPointer(resultInstance, targetMethod, unrollFactor); + + if (resultType.IsByRef) + { + var returnParameter = targetMethod.ReturnParameter; + // IsReadOnlyAttribute is not part of netstandard2.0, so we need to check the attribute name as usual. + if (returnParameter.GetCustomAttributes().Any(attribute => attribute.GetType().FullName == "System.Runtime.CompilerServices.IsReadOnlyAttribute")) return Create( - typeof(BenchmarkActionByRef<>).MakeGenericType(resultType.GetElementType()!), + typeof(BenchmarkActionByRefReadonly<>).MakeGenericType(resultType.GetElementType()!), resultInstance, targetMethod, unrollFactor); - } - - if (resultType == typeof(Task)) - return new BenchmarkActionTask(resultInstance, targetMethod, unrollFactor); - - if (resultType == typeof(ValueTask)) - return new BenchmarkActionValueTask(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); - } return Create( - typeof(BenchmarkAction<>).MakeGenericType(resultType), + typeof(BenchmarkActionByRef<>).MakeGenericType(resultType.GetElementType()!), resultInstance, targetMethod, unrollFactor); } - private static void PrepareInstanceAndResultType( - object? instance, - MethodInfo? targetMethod, - MethodInfo? fallbackIdleSignature, - out object? resultInstance, - out Type resultType) + if (resultType == typeof(Task)) + return new BenchmarkActionTask(resultInstance, targetMethod, unrollFactor); + + if (resultType == typeof(ValueTask)) + return new BenchmarkActionValueTask(resultInstance, targetMethod, unrollFactor); + + if (resultType.GetTypeInfo().IsGenericType) { - var signature = targetMethod ?? fallbackIdleSignature; - if (signature == null) - throw new ArgumentNullException( - nameof(fallbackIdleSignature), - $"Either {nameof(targetMethod)} or {nameof(fallbackIdleSignature)} should be not null."); - - if (!signature.IsStatic && instance == null) - throw new ArgumentNullException( - nameof(instance), - $"The {nameof(instance)} parameter should be not null as invocation method is instance method."); - - resultInstance = signature.IsStatic ? null : instance!; - resultType = signature.ReturnType; - - if (resultType == typeof(void)) - { - // DONTTOUCH: async should be checked for target method - // as fallbackIdleSignature used for result type detection only. - bool isUsingAsyncKeyword = targetMethod?.HasAttribute() ?? false; - if (isUsingAsyncKeyword) - throw new NotSupportedException("Async void is not supported by design."); - } - else if (resultType.IsPointer && resultType != typeof(void*)) - { - throw new NotSupportedException("InProcessNoEmitToolchain only supports void* return, not T*"); - } + 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); } - /// Helper to enforce .ctor signature. - private static BenchmarkActionBase Create(Type actionType, object? instance, MethodInfo? method, int unrollFactor) => - (BenchmarkActionBase)Activator.CreateInstance(actionType, instance, method, unrollFactor)!; - - private static void FallbackMethod() { } - private static readonly MethodInfo FallbackSignature = new Action(FallbackMethod).GetMethodInfo(); - - /// Creates run benchmark action. - /// Descriptor info. - /// Instance of target. - /// Unroll factor. - /// Run benchmark action. - public static BenchmarkAction CreateWorkload(Descriptor descriptor, object instance, int unrollFactor) => - CreateCore(instance, descriptor.WorkloadMethod, null, unrollFactor); - - /// Creates idle benchmark action. - /// Descriptor info. - /// Instance of target. - /// Unroll factor. - /// Idle benchmark action. - public static BenchmarkAction CreateOverhead(Descriptor descriptor, object instance, int unrollFactor) => - CreateCore(instance, null, FallbackSignature, unrollFactor); - - /// Creates global setup benchmark action. - /// Descriptor info. - /// Instance of target. - /// Setup benchmark action. - public static BenchmarkAction CreateGlobalSetup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.GlobalSetupMethod, FallbackSignature, 1); - - /// Creates global cleanup benchmark action. - /// Descriptor info. - /// Instance of target. - /// Cleanup benchmark action. - public static BenchmarkAction CreateGlobalCleanup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.GlobalCleanupMethod, FallbackSignature, 1); - - /// Creates global setup benchmark action. - /// Descriptor info. - /// Instance of target. - /// Setup benchmark action. - public static BenchmarkAction CreateIterationSetup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.IterationSetupMethod, FallbackSignature, 1); - - /// Creates global cleanup benchmark action. - /// Descriptor info. - /// Instance of target. - /// Cleanup benchmark action. - public static BenchmarkAction CreateIterationCleanup(Descriptor descriptor, object instance) => - CreateCore(instance, descriptor.IterationCleanupMethod, FallbackSignature, 1); + if (resultType.IsAwaitable()) + { + throw new NotSupportedException($"{nameof(InProcessNoEmitToolchain)} does not support returning awaitable types except (Value)Task()."); + } + + return Create( + typeof(BenchmarkAction<>).MakeGenericType(resultType), + resultInstance, + targetMethod, + unrollFactor); } + + private static void PrepareInstanceAndResultType(object instance, MethodInfo? targetMethod, MethodInfo? fallbackIdleSignature, out object? resultInstance, out Type resultType) + { + var signature = targetMethod ?? fallbackIdleSignature; + if (signature == null) + throw new ArgumentNullException( + nameof(fallbackIdleSignature), + $"Either {nameof(targetMethod)} or {nameof(fallbackIdleSignature)} should be not null."); + + if (!signature.IsStatic && instance == null) + throw new ArgumentNullException( + nameof(instance), + $"The {nameof(instance)} parameter should be not null as invocation method is instance method."); + + resultInstance = signature.IsStatic ? null : instance; + resultType = signature.ReturnType; + + if (resultType == typeof(void)) + { + // DONTTOUCH: async should be checked for target method + // as fallbackIdleSignature used for result type detection only. + bool isUsingAsyncKeyword = targetMethod?.HasAttribute() ?? false; + if (isUsingAsyncKeyword) + throw new NotSupportedException("Async void is not supported by design."); + } + else if (resultType.IsPointer && resultType != typeof(void*)) + { + throw new NotSupportedException("InProcessNoEmitToolchain only supports void* return, not T*"); + } + } + + /// Helper to enforce .ctor signature. + private static BenchmarkActionBase Create(Type actionType, object? instance, MethodInfo method, int unrollFactor) => + (BenchmarkActionBase)Activator.CreateInstance(actionType, instance, method, unrollFactor)!; + + private static void FallbackMethod() { } + private static readonly MethodInfo FallbackSignature = new Action(FallbackMethod).GetMethodInfo(); + + public static BenchmarkAction CreateWorkload(Descriptor descriptor, object instance, int unrollFactor) => + CreateCore(instance, descriptor.WorkloadMethod, null, unrollFactor); + + public static BenchmarkAction CreateOverhead(Descriptor descriptor, object instance, int unrollFactor) => + CreateCore(instance, null, FallbackSignature, unrollFactor); + + public static BenchmarkAction CreateGlobalSetup(Descriptor descriptor, object instance) => + CreateCore(instance, descriptor.GlobalSetupMethod, FallbackSignature, 1); + + public static BenchmarkAction CreateGlobalCleanup(Descriptor descriptor, object instance) => + CreateCore(instance, descriptor.GlobalCleanupMethod, FallbackSignature, 1); + + public static BenchmarkAction CreateIterationSetup(Descriptor descriptor, object instance) => + CreateCore(instance, descriptor.IterationSetupMethod, FallbackSignature, 1); + + public static BenchmarkAction CreateIterationCleanup(Descriptor descriptor, object instance) => + CreateCore(instance, descriptor.IterationCleanupMethod, FallbackSignature, 1); } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Base.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Base.cs index 36f85f2606..dfd23111e1 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Base.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Base.cs @@ -11,14 +11,14 @@ 1. Overhead signature should match to the benchmark method signature (including 2. Should work under .Net native. Uses Delegate.Combine instead of emitting the code. 3. High data locality and no additional allocations / JIT where possible. This means NO closures allowed, no allocations but in .ctor and for LastCallResult boxing, - all state should be stored explicitly as BenchmarkAction's fields. + all state should be stored explicitly as BenchmarkFunc's fields. 4. There can be multiple benchmark actions per single target instance (workload, globalSetup, globalCleanup methods), so target instantiation is not a responsibility of the benchmark action. 5. Implementation should match to the code in BenchmarkProgram.txt. */ // DONTTOUCH: Be VERY CAREFUL when changing the code. - // Please, ensure that the implementation is in sync with content of BenchmarkProgram.txt + // Please, ensure that the implementation is in sync with content of BenchmarkType.txt internal static partial class BenchmarkActionFactory { /// Base class that provides reusable API for final implementations. diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs index c98da9a4a7..7f53c1db89 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/BenchmarkActionFactory_Implementations.cs @@ -1,7 +1,8 @@ -using BenchmarkDotNet.Portability; +using BenchmarkDotNet.Attributes.CompilerServices; +using BenchmarkDotNet.Engines; +using Perfolizer.Horology; using System; using System.Reflection; -using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit @@ -11,9 +12,10 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit */ // DONTTOUCH: Be VERY CAREFUL when changing the code. - // Please, ensure that the implementation is in sync with content of BenchmarkProgram.txt + // Please, ensure that the implementation is in sync with content of BenchmarkType.txt internal static partial class BenchmarkActionFactory { + [AggressivelyOptimizeMethods] internal sealed class BenchmarkActionVoid : BenchmarkActionBase { private readonly Action callback; @@ -23,30 +25,41 @@ public BenchmarkActionVoid(object? instance, MethodInfo? method, int unrollFacto { callback = CreateWorkloadOrOverhead(instance, method); unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = callback; + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask InvokeOnce() { - for (long i = 0; i < repeatCount; i++) + callback(); + return new(); + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) { unrolledCallback(); } + return new ValueTask(startedClock.GetElapsed()); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + var startedClock = clock.Start(); + while (--invokeCount >= 0) { callback(); } + return new ValueTask(startedClock.GetElapsed()); } + + public override void Complete() { } } + [AggressivelyOptimizeMethods] internal unsafe class BenchmarkActionVoidPointer : BenchmarkActionBase { private delegate void* PointerFunc(); @@ -58,30 +71,41 @@ public BenchmarkActionVoidPointer(object? instance, MethodInfo method, int unrol { callback = CreateWorkload(instance, method); unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = () => callback(); + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask InvokeOnce() { - for (long i = 0; i < repeatCount; i++) + callback(); + return new(); + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) { unrolledCallback(); } + return new ValueTask(startedClock.GetElapsed()); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + var startedClock = clock.Start(); + while (--invokeCount >= 0) { callback(); } + return new ValueTask(startedClock.GetElapsed()); } + + public override void Complete() { } } + [AggressivelyOptimizeMethods] internal unsafe class BenchmarkActionByRef : BenchmarkActionBase #if NET9_0_OR_GREATER where T : allows ref struct @@ -96,30 +120,41 @@ public BenchmarkActionByRef(object? instance, MethodInfo method, int unrollFacto { callback = CreateWorkload(instance, method); unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = () => callback(); + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask InvokeOnce() + { + callback(); + return new(); + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + var startedClock = clock.Start(); + while (--invokeCount >= 0) { unrolledCallback(); } + return new ValueTask(startedClock.GetElapsed()); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + var startedClock = clock.Start(); + while (--invokeCount >= 0) { callback(); } + return new ValueTask(startedClock.GetElapsed()); } + + public override void Complete() { } } + [AggressivelyOptimizeMethods] internal unsafe class BenchmarkActionByRefReadonly : BenchmarkActionBase #if NET9_0_OR_GREATER where T : allows ref struct @@ -134,30 +169,41 @@ public BenchmarkActionByRefReadonly(object? instance, MethodInfo method, int unr { callback = CreateWorkload(instance, method); unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = () => callback(); + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask InvokeOnce() { - for (long i = 0; i < repeatCount; i++) + callback(); + return new(); + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) { unrolledCallback(); } + return new ValueTask(startedClock.GetElapsed()); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + var startedClock = clock.Start(); + while (--invokeCount >= 0) { callback(); } + return new ValueTask(startedClock.GetElapsed()); } + + public override void Complete() { } } + [AggressivelyOptimizeMethods] internal class BenchmarkAction : BenchmarkActionBase #if NET9_0_OR_GREATER where T : allows ref struct @@ -170,212 +216,324 @@ public BenchmarkAction(object? instance, MethodInfo method, int unrollFactor) { callback = CreateWorkload>(instance, method); unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = () => callback(); + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask InvokeOnce() { - for (long i = 0; i < repeatCount; i++) + callback(); + return new(); + } + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + { + var startedClock = clock.Start(); + while (--invokeCount >= 0) { unrolledCallback(); } + return new ValueTask(startedClock.GetElapsed()); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + var startedClock = clock.Start(); + while (--invokeCount >= 0) { callback(); } + return new ValueTask(startedClock.GetElapsed()); } + + public override void Complete() { } } + [AggressivelyOptimizeMethods] internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Func startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; + private readonly Func callback; + private readonly int unrollFactor; + private WorkloadContinuerAndValueTaskSource? workloadContinuerAndValueTaskSource; + private IClock? clock; + private long invokeCount; public BenchmarkActionTask(object? instance, MethodInfo method, int unrollFactor) { - if (method == null) - { - startTaskCallback = default!; - callback = CreateWorkloadOrOverhead(instance, method); - } - else - { - startTaskCallback = CreateWorkload>(instance, method); - callback = ExecuteBlocking; - } - unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = callback; + callback = CreateWorkload>(instance, method); + this.unrollFactor = unrollFactor; + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private async ValueTask InvokeOnce() + => await callback(); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + => WorkloadActionNoUnroll(invokeCount * unrollFactor, clock); + + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + this.invokeCount = invokeCount; + this.clock = clock; + if (workloadContinuerAndValueTaskSource == null) { - unrolledCallback(); + workloadContinuerAndValueTaskSource = new(); + StartWorkload(); } + return workloadContinuerAndValueTaskSource.Continue(); + } + + private async void StartWorkload() + { + await WorkloadCore(); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private async Task WorkloadCore() { - for (long i = 0; i < repeatCount; i++) + try { - callback(); + while (true) + { + await workloadContinuerAndValueTaskSource!; + if (workloadContinuerAndValueTaskSource.IsCompleted) + { + return; + } + + var startedClock = clock!.Start(); + while (--invokeCount >= 0) + { + await callback(); + } + workloadContinuerAndValueTaskSource.SetResult(startedClock.GetElapsed()); + } + } + catch (Exception e) + { + workloadContinuerAndValueTaskSource!.SetException(e); } } + + public override void Complete() + => workloadContinuerAndValueTaskSource?.Complete(); } + [AggressivelyOptimizeMethods] internal class BenchmarkActionTask : BenchmarkActionBase { - private readonly Func> startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; + private readonly Func> callback; + private readonly int unrollFactor; + private WorkloadContinuerAndValueTaskSource? workloadContinuerAndValueTaskSource; + private IClock? clock; + private long invokeCount; public BenchmarkActionTask(object? instance, MethodInfo method, int unrollFactor) { - if (method == null) - { - startTaskCallback = default!; - callback = CreateWorkloadOrOverhead(instance, method); - } - else - { - startTaskCallback = CreateWorkload>>(instance, method); - callback = ExecuteBlocking; - } - unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = callback; + callback = CreateWorkload>>(instance, method); + this.unrollFactor = unrollFactor; + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private async ValueTask InvokeOnce() + => await callback(); + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + => WorkloadActionNoUnroll(invokeCount * unrollFactor, clock); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + this.invokeCount = invokeCount; + this.clock = clock; + if (workloadContinuerAndValueTaskSource == null) { - unrolledCallback(); + workloadContinuerAndValueTaskSource = new(); + StartWorkload(); } + return workloadContinuerAndValueTaskSource.Continue(); + } + + private async void StartWorkload() + { + await WorkloadCore(); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private async Task WorkloadCore() { - for (long i = 0; i < repeatCount; i++) + try { - callback(); + while (true) + { + await workloadContinuerAndValueTaskSource!; + if (workloadContinuerAndValueTaskSource.IsCompleted) + { + return default!; + } + + var startedClock = clock!.Start(); + while (--invokeCount >= 0) + { + await callback(); + } + workloadContinuerAndValueTaskSource.SetResult(startedClock.GetElapsed()); + } + } + catch (Exception e) + { + workloadContinuerAndValueTaskSource!.SetException(e); + return default!; } } + + public override void Complete() + => workloadContinuerAndValueTaskSource?.Complete(); } + [AggressivelyOptimizeMethods] internal class BenchmarkActionValueTask : BenchmarkActionBase { - private readonly Func startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; + private readonly Func callback; + private readonly int unrollFactor; + private WorkloadContinuerAndValueTaskSource? workloadContinuerAndValueTaskSource; + private IClock? clock; + private long invokeCount; public BenchmarkActionValueTask(object? instance, MethodInfo method, int unrollFactor) { - if (method == null) - { - startTaskCallback = default!; - callback = CreateWorkloadOrOverhead(instance, method); - } - else - { - startTaskCallback = CreateWorkload>(instance, method); - callback = ExecuteBlocking; - } - unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = callback; + callback = CreateWorkload>(instance, method); + this.unrollFactor = unrollFactor; + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private async ValueTask InvokeOnce() + => await callback(); + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + => WorkloadActionNoUnroll(invokeCount * unrollFactor, clock); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + this.invokeCount = invokeCount; + this.clock = clock; + if (workloadContinuerAndValueTaskSource == null) { - unrolledCallback(); + workloadContinuerAndValueTaskSource = new(); + StartWorkload(); } + return workloadContinuerAndValueTaskSource.Continue(); + } + + private async void StartWorkload() + { + await WorkloadCore(); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private async ValueTask WorkloadCore() { - for (long i = 0; i < repeatCount; i++) + try { - callback(); + while (true) + { + await workloadContinuerAndValueTaskSource!; + if (workloadContinuerAndValueTaskSource.IsCompleted) + { + return; + } + + var startedClock = clock!.Start(); + while (--invokeCount >= 0) + { + await callback(); + } + workloadContinuerAndValueTaskSource.SetResult(startedClock.GetElapsed()); + } + } + catch (Exception e) + { + workloadContinuerAndValueTaskSource!.SetException(e); } } + + public override void Complete() + => workloadContinuerAndValueTaskSource?.Complete(); } + [AggressivelyOptimizeMethods] internal class BenchmarkActionValueTask : BenchmarkActionBase { - private readonly Func> startTaskCallback; - private readonly Action callback; - private readonly Action unrolledCallback; + private readonly Func> callback; + private readonly int unrollFactor; + private WorkloadContinuerAndValueTaskSource? workloadContinuerAndValueTaskSource; + private IClock? clock; + private long invokeCount; public BenchmarkActionValueTask(object? instance, MethodInfo method, int unrollFactor) { - if (method == null) - { - startTaskCallback = default!; - callback = CreateWorkloadOrOverhead(instance, method); - } - else - { - startTaskCallback = CreateWorkload>>(instance, method); - callback = ExecuteBlocking; - } - unrolledCallback = Unroll(callback, unrollFactor); - InvokeSingle = callback; + callback = CreateWorkload>>(instance, method); + this.unrollFactor = unrollFactor; + InvokeSingle = InvokeOnce; InvokeUnroll = WorkloadActionUnroll; InvokeNoUnroll = WorkloadActionNoUnroll; } - // must be kept in sync with TaskDeclarationsProvider.TargetMethodDelegate - private void ExecuteBlocking() => Helpers.AwaitHelper.GetResult(startTaskCallback.Invoke()); + private async ValueTask InvokeOnce() + => await callback(); + + private ValueTask WorkloadActionUnroll(long invokeCount, IClock clock) + => WorkloadActionNoUnroll(invokeCount * unrollFactor, clock); - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionUnroll(long repeatCount) + private ValueTask WorkloadActionNoUnroll(long invokeCount, IClock clock) { - for (long i = 0; i < repeatCount; i++) + this.invokeCount = invokeCount; + this.clock = clock; + if (workloadContinuerAndValueTaskSource == null) { - unrolledCallback(); + workloadContinuerAndValueTaskSource = new(); + StartWorkload(); } + return workloadContinuerAndValueTaskSource.Continue(); + } + + private async void StartWorkload() + { + await WorkloadCore(); } - [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)] - private void WorkloadActionNoUnroll(long repeatCount) + private async ValueTask WorkloadCore() { - for (long i = 0; i < repeatCount; i++) + try { - callback(); + while (true) + { + await workloadContinuerAndValueTaskSource!; + if (workloadContinuerAndValueTaskSource.IsCompleted) + { + return default!; + } + + var startedClock = clock!.Start(); + while (--invokeCount >= 0) + { + await callback(); + } + workloadContinuerAndValueTaskSource.SetResult(startedClock.GetElapsed()); + } + } + catch (Exception e) + { + workloadContinuerAndValueTaskSource!.SetException(e); + return default!; } } + + public override void Complete() + => workloadContinuerAndValueTaskSource?.Complete(); } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs index 10eaa4287b..a225ace0c9 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; @@ -15,14 +16,15 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit { internal class InProcessNoEmitExecutor(bool executeOnSeparateThread) : IExecutor { - public ExecuteResult Execute(ExecuteParameters executeParameters) + public async ValueTask ExecuteAsync(ExecuteParameters executeParameters) { var host = new InProcessHost(executeParameters.BenchmarkCase, executeParameters.Logger, executeParameters.Diagnoser); int exitCode = -1; if (executeOnSeparateThread) { - var runThread = new Thread(() => exitCode = ExecuteCore(host, executeParameters)); + var taskCompletionSource = new TaskCompletionSource(); + var runThread = new Thread(async () => taskCompletionSource.SetResult(await ExecuteCore(host, executeParameters))); if (executeParameters.BenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes(false).Any() && OsDetector.IsWindows()) @@ -33,11 +35,12 @@ public ExecuteResult Execute(ExecuteParameters executeParameters) runThread.IsBackground = true; runThread.Start(); - runThread.Join(); + + exitCode = await taskCompletionSource.Task; } else { - exitCode = ExecuteCore(host, executeParameters); + exitCode = await ExecuteCore(host, executeParameters); } host.HandleInProcessDiagnoserResults(executeParameters.BenchmarkCase, executeParameters.CompositeInProcessDiagnoser); @@ -45,7 +48,7 @@ public ExecuteResult Execute(ExecuteParameters executeParameters) return ExecuteResult.FromRunResults(host.RunResults, exitCode); } - private int ExecuteCore(IHost host, ExecuteParameters parameters) + private async ValueTask ExecuteCore(IHost host, ExecuteParameters parameters) { int exitCode = -1; var process = Process.GetCurrentProcess(); @@ -65,7 +68,7 @@ private int ExecuteCore(IHost host, ExecuteParameters parameters) process.TrySetAffinity(affinity.Value, parameters.Logger); } - exitCode = InProcessNoEmitRunner.Run(host, parameters); + exitCode = await InProcessNoEmitRunner.Run(host, parameters); } catch (Exception ex) { diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs index ad42066350..5f37f2427e 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Exporters; @@ -19,7 +20,7 @@ namespace BenchmarkDotNet.Toolchains.InProcess.NoEmit internal class InProcessNoEmitRunner { [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Runnable))] - public static int Run(IHost host, ExecuteParameters parameters) + public static async ValueTask Run(IHost host, ExecuteParameters parameters) { // the first thing to do is to let diagnosers hook in before anything happens // so all jit-related diagnosers can catch first jit compilation! @@ -37,7 +38,7 @@ public static int Run(IHost host, ExecuteParameters parameters) var methodInfo = type.GetMethod(nameof(Runnable.RunCore), BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException($"Bug: method {nameof(Runnable.RunCore)} in {inProcessRunnableTypeName} not found."); - methodInfo.Invoke(null, [host, parameters]); + await (ValueTask) methodInfo.Invoke(null, [host, parameters])!; return 0; } @@ -104,11 +105,11 @@ internal static void FillMembers(object instance, BenchmarkCase benchmarkCase) [UsedImplicitly] private static class Runnable { - public static void RunCore(IHost host, ExecuteParameters parameters) + public static async ValueTask RunCore(IHost host, ExecuteParameters parameters) { var benchmarkCase = parameters.BenchmarkCase; var target = benchmarkCase.Descriptor; - var job = benchmarkCase.Job; // TODO: filter job (same as SourceCodePresenter does)? + var job = new Job().Apply(benchmarkCase.Job).Freeze(); int unrollFactor = benchmarkCase.Job.ResolveValue(RunMode.UnrollFactorCharacteristic, EnvironmentResolver.Instance); // DONTTOUCH: these should be allocated together @@ -129,7 +130,7 @@ public static void RunCore(IHost host, ExecuteParameters parameters) host.WriteLine(); var errors = BenchmarkProcessValidator.Validate(job, instance); - if (ValidationErrorReporter.ReportIfAny(errors, host)) + if (await ValidationErrorReporter.ReportIfAnyAsync(errors, host)) return; var compositeInProcessDiagnoserHandler = new Diagnosers.CompositeInProcessDiagnoserHandler( @@ -143,10 +144,10 @@ public static void RunCore(IHost host, ExecuteParameters parameters) ); if (parameters.DiagnoserRunMode == Diagnosers.RunMode.SeparateLogic) { - compositeInProcessDiagnoserHandler.Handle(BenchmarkSignal.SeparateLogic); + await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.SeparateLogic); return; } - compositeInProcessDiagnoserHandler.Handle(BenchmarkSignal.BeforeEngine); + await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.BeforeEngine); var engineParameters = new EngineParameters { @@ -156,7 +157,12 @@ public static void RunCore(IHost host, ExecuteParameters parameters) OverheadActionNoUnroll = overheadAction.InvokeNoUnroll, OverheadActionUnroll = overheadAction.InvokeUnroll, GlobalSetupAction = globalSetupAction.InvokeSingle, - GlobalCleanupAction = globalCleanupAction.InvokeSingle, + GlobalCleanupAction = () => + { + workloadAction.Complete(); + overheadAction.Complete(); + return globalCleanupAction.InvokeSingle(); + }, IterationSetupAction = iterationSetupAction.InvokeSingle, IterationCleanupAction = iterationCleanupAction.InvokeSingle, TargetJob = job, @@ -166,13 +172,13 @@ public static void RunCore(IHost host, ExecuteParameters parameters) InProcessDiagnoserHandler = compositeInProcessDiagnoserHandler }; - var results = job + var results = await job .ResolveValue(InfrastructureMode.EngineFactoryCharacteristic, InfrastructureResolver.Instance)! .Create(engineParameters) - .Run(); + .RunAsync(); host.ReportResults(results); // printing costs memory, do this after runs - compositeInProcessDiagnoserHandler.Handle(BenchmarkSignal.AfterEngine); + await compositeInProcessDiagnoserHandler.HandleAsync(BenchmarkSignal.AfterEngine); } } } diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitToolchain.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitToolchain.cs index 94fd847f02..e8887d9ec7 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitToolchain.cs @@ -1,6 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Characteristics; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; using JetBrains.Annotations; @@ -25,8 +26,21 @@ public InProcessNoEmitToolchain(InProcessNoEmitSettings settings) Executor = new InProcessNoEmitExecutor(settings.ExecuteOnSeparateThread); } - public IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) => - InProcessValidator.Validate(benchmarkCase); + public async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) + { + await foreach (var error in InProcessValidator.ValidateAsync(benchmarkCase)) + { + yield return error; + } + + if (benchmarkCase.Descriptor.WorkloadMethod.ReturnType.HasAttribute() == true) + { + yield return new ValidationError(false, + $"{nameof(InProcessNoEmitToolchain)} does not support overriding the async caller type via [AsyncCallerType]. It will be ignored.", + benchmarkCase); + } + + } /// Name of the toolchain. /// The name of the toolchain. diff --git a/src/BenchmarkDotNet/Toolchains/Mono/MonoAotToolchain.cs b/src/BenchmarkDotNet/Toolchains/Mono/MonoAotToolchain.cs index b1e4d44624..154a555c07 100644 --- a/src/BenchmarkDotNet/Toolchains/Mono/MonoAotToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/Mono/MonoAotToolchain.cs @@ -20,9 +20,9 @@ public class MonoAotToolchain : Toolchain { } - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { - foreach (var validationError in base.Validate(benchmarkCase, resolver)) + await foreach (var validationError in base.ValidateAsync(benchmarkCase, resolver)) { yield return validationError; } diff --git a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMToolChain.cs b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMToolChain.cs index 06ef8ea8c6..f565a4f922 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMToolChain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMToolChain.cs @@ -29,9 +29,9 @@ public static IToolchain From(NetCoreAppSettings netCoreAppSettings) new Executor(), netCoreAppSettings.CustomDotNetCliPath); - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { - foreach (var validationError in base.Validate(benchmarkCase, resolver)) + await foreach (var validationError in base.ValidateAsync(benchmarkCase, resolver)) { yield return validationError; } diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs index 4534670f50..c05ff45d6d 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmExecutor.cs @@ -15,23 +15,22 @@ using System.Diagnostics; using System.IO; using System.Text; +using System.Threading.Tasks; namespace BenchmarkDotNet.Toolchains.MonoWasm { internal class WasmExecutor : IExecutor { - public ExecuteResult Execute(ExecuteParameters executeParameters) + public ValueTask ExecuteAsync(ExecuteParameters executeParameters) { string exePath = executeParameters.BuildResult.ArtifactsPaths.ExecutablePath; - if (!File.Exists(exePath)) - { - return ExecuteResult.CreateFailed(); - } - - return Execute(executeParameters.BenchmarkCase, executeParameters.BenchmarkId, executeParameters.Logger, executeParameters.BuildResult.ArtifactsPaths, + var executeResult = !File.Exists(exePath) + ? ExecuteResult.CreateFailed() + : Execute(executeParameters.BenchmarkCase, executeParameters.BenchmarkId, executeParameters.Logger, executeParameters.BuildResult.ArtifactsPaths, executeParameters.Diagnoser, executeParameters.CompositeInProcessDiagnoser, executeParameters.Resolver, executeParameters.LaunchIndex, executeParameters.DiagnoserRunMode); + return new ValueTask(executeResult); } private static ExecuteResult Execute(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, ILogger logger, ArtifactsPaths artifactsPaths, diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index b42aef9e4b..856d22c658 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -19,6 +19,7 @@ public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packa : base(targetFrameworkMoniker, cliPath, packagesPath) { CustomRuntimePack = customRuntimePack; + EntryPointType = Code.CodeGenEntryPointType.Asynchronous; BenchmarkRunCallType = aot ? Code.CodeGenBenchmarkRunCallType.Direct : Code.CodeGenBenchmarkRunCallType.Reflection; } diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs index 6a546714ce..7dff1e2d2f 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmToolchain.cs @@ -20,9 +20,9 @@ private WasmToolchain(string name, IGenerator generator, IBuilder builder, IExec CustomDotNetCliPath = customDotNetCliPath; } - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { - foreach (var validationError in base.Validate(benchmarkCase, resolver)) + await foreach (var validationError in base.ValidateAsync(benchmarkCase, resolver)) { yield return validationError; } diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs index da92e9951c..6096f2d938 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/NativeAotToolchain.cs @@ -80,9 +80,9 @@ internal NativeAotToolchain(string displayName, public static string GetExtraArguments(string runtimeIdentifier) => $"-r {runtimeIdentifier}"; - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { - foreach (var error in base.Validate(benchmarkCase, resolver)) + await foreach (var error in base.ValidateAsync(benchmarkCase, resolver)) { yield return error; } diff --git a/src/BenchmarkDotNet/Toolchains/R2R/R2RToolchain.cs b/src/BenchmarkDotNet/Toolchains/R2R/R2RToolchain.cs index 84c37b214b..a3554abb2f 100644 --- a/src/BenchmarkDotNet/Toolchains/R2R/R2RToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/R2R/R2RToolchain.cs @@ -32,7 +32,7 @@ private R2RToolchain(string name, IGenerator generator, IBuilder builder, IExecu new Executor(), settings.CustomDotNetCliPath); - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { foreach (var validationError in DotNetSdkValidator.ValidateCoreSdks(_customDotNetCliPath, benchmarkCase)) { diff --git a/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs b/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs index d88521cae1..4842290130 100644 --- a/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/Roslyn/Generator.cs @@ -4,7 +4,6 @@ using System.Reflection; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Extensions; -using BenchmarkDotNet.Portability; using BenchmarkDotNet.Running; using JetBrains.Annotations; @@ -17,14 +16,13 @@ protected override string GetBuildArtifactsDirectoryPath(BuildPartition buildPar => Path.GetDirectoryName(buildPartition.AssemblyLocation)!; [PublicAPI] - protected override string[] GetArtifactsToCleanup(ArtifactsPaths artifactsPaths) - => new[] - { - artifactsPaths.ProgramCodePath, - artifactsPaths.AppConfigPath, - artifactsPaths.BuildScriptFilePath, - artifactsPaths.ExecutablePath - }; + protected override string[] GetArtifactsToCleanup(ArtifactsPaths artifactsPaths) => + [ + artifactsPaths.ProgramCodePath, + artifactsPaths.AppConfigPath, + artifactsPaths.BuildScriptFilePath, + artifactsPaths.ExecutablePath + ]; protected override void GenerateBuildScript(BuildPartition buildPartition, ArtifactsPaths artifactsPaths) { @@ -54,11 +52,12 @@ internal static IEnumerable GetAllReferences(BenchmarkCase benchmarkCa .GetReferencedAssemblies() .Select(Assembly.Load) .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 has 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(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/Roslyn/RoslynToolchain.cs b/src/BenchmarkDotNet/Toolchains/Roslyn/RoslynToolchain.cs index b52caab5d9..18c24ffc82 100644 --- a/src/BenchmarkDotNet/Toolchains/Roslyn/RoslynToolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/Roslyn/RoslynToolchain.cs @@ -22,17 +22,17 @@ public class RoslynToolchain : Toolchain } [PublicAPI] - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { - foreach (var validationError in base.Validate(benchmarkCase, resolver)) + await foreach (var validationError in base.ValidateAsync(benchmarkCase, resolver)) { yield return validationError; } - if (!RuntimeInformation.IsFullFramework) + if (!(RuntimeInformation.IsFullFramework || RuntimeInformation.IsOldMono)) { yield return new ValidationError(true, - "The Roslyn toolchain is only supported on .NET Framework", + "The Roslyn toolchain is only supported on .NET Framework and legacy Mono", benchmarkCase); } diff --git a/src/BenchmarkDotNet/Toolchains/Toolchain.cs b/src/BenchmarkDotNet/Toolchains/Toolchain.cs index 1427a9e1b8..5edf9caccf 100644 --- a/src/BenchmarkDotNet/Toolchains/Toolchain.cs +++ b/src/BenchmarkDotNet/Toolchains/Toolchain.cs @@ -29,7 +29,7 @@ public Toolchain(string name, IGenerator generator, IBuilder builder, IExecutor Executor = executor; } - public virtual IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public virtual async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { var runtime = benchmarkCase.Job.ResolveValue(EnvironmentMode.RuntimeCharacteristic, resolver); var jit = benchmarkCase.Job.ResolveValue(EnvironmentMode.JitCharacteristic, resolver); diff --git a/src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs b/src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs index 6ebe351a81..ec25f20805 100644 --- a/src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs +++ b/src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs @@ -46,7 +46,7 @@ internal static IToolchain GetToolchain(this Runtime runtime, Descriptor? descri case ClrRuntime clrRuntime: bool UseRoslyn() => !isRuntimeExplicit - || runtime.MsBuildMoniker == ClrRuntime.GetTargetOrCurrentVersion(descriptor?.WorkloadMethod.DeclaringType?.Assembly).MsBuildMoniker; + || runtime.MsBuildMoniker == ClrRuntime.GetTargetOrCurrentVersion(descriptor?.Type.Assembly).MsBuildMoniker; if (!preferMsBuildToolchains && RuntimeInformation.IsFullFramework && UseRoslyn()) return RoslynToolchain.Instance; diff --git a/src/BenchmarkDotNet/Validators/BaselineValidator.cs b/src/BenchmarkDotNet/Validators/BaselineValidator.cs index 88c2d47d3e..4bf01ec438 100644 --- a/src/BenchmarkDotNet/Validators/BaselineValidator.cs +++ b/src/BenchmarkDotNet/Validators/BaselineValidator.cs @@ -14,7 +14,7 @@ private BaselineValidator() { } public bool TreatsWarningsAsErrors => true; // it is a must! - public IEnumerable Validate(ValidationParameters input) + public async IAsyncEnumerable ValidateAsync(ValidationParameters input) { var allBenchmarks = input.Benchmarks.ToImmutableArray(); var orderProvider = input.Config.Orderer ?? DefaultOrderer.Instance; diff --git a/src/BenchmarkDotNet/Validators/CompilationValidator.cs b/src/BenchmarkDotNet/Validators/CompilationValidator.cs index fa24803dca..f0af9cc42d 100644 --- a/src/BenchmarkDotNet/Validators/CompilationValidator.cs +++ b/src/BenchmarkDotNet/Validators/CompilationValidator.cs @@ -7,7 +7,6 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains; using Microsoft.CodeAnalysis.CSharp; -using BenchmarkDotNet.Attributes; namespace BenchmarkDotNet.Validators { @@ -23,13 +22,13 @@ private CompilationValidator() { } public bool TreatsWarningsAsErrors => true; - public IEnumerable Validate(ValidationParameters validationParameters) + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => ValidateCSharpNaming(validationParameters.Benchmarks) - .Union(ValidateClassModifiers((validationParameters.Benchmarks)) - .Union(ValidateAccessModifiers(validationParameters.Benchmarks)) - .Union(ValidateBindingModifiers(validationParameters.Benchmarks)) - .Union(ValidateMethodImpl(validationParameters.Benchmarks)) - ); + .Union(ValidateClassModifiers(validationParameters.Benchmarks)) + .Union(ValidateAccessModifiers(validationParameters.Benchmarks)) + .Union(ValidateBindingModifiers(validationParameters.Benchmarks)) + .Union(ValidateMethodImpl(validationParameters.Benchmarks)) + .ToAsyncEnumerable(); private static IEnumerable ValidateClassModifiers(IEnumerable benchmarks) { diff --git a/src/BenchmarkDotNet/Validators/CompositeValidator.cs b/src/BenchmarkDotNet/Validators/CompositeValidator.cs index 89b25a68dc..a5017a6ddf 100644 --- a/src/BenchmarkDotNet/Validators/CompositeValidator.cs +++ b/src/BenchmarkDotNet/Validators/CompositeValidator.cs @@ -16,7 +16,7 @@ internal class CompositeValidator : IValidator public bool TreatsWarningsAsErrors => validators.Any(validator => validator.TreatsWarningsAsErrors); - public IEnumerable Validate(ValidationParameters validationParameters) - => validators.SelectMany(validator => validator.Validate(validationParameters)).Distinct(); + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) + => validators.ToAsyncEnumerable().SelectMany(validator => validator.ValidateAsync(validationParameters)).Distinct(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/ConfigValidator.cs b/src/BenchmarkDotNet/Validators/ConfigValidator.cs index 808d349bf4..5ea0fd2b5d 100644 --- a/src/BenchmarkDotNet/Validators/ConfigValidator.cs +++ b/src/BenchmarkDotNet/Validators/ConfigValidator.cs @@ -16,7 +16,7 @@ private ConfigValidator() { } public bool TreatsWarningsAsErrors => false; - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { if (validationParameters.Config.GetLoggers().IsEmpty()) { diff --git a/src/BenchmarkDotNet/Validators/DeferredExecutionValidator.cs b/src/BenchmarkDotNet/Validators/DeferredExecutionValidator.cs index 14da5fdf14..0376a7e75d 100644 --- a/src/BenchmarkDotNet/Validators/DeferredExecutionValidator.cs +++ b/src/BenchmarkDotNet/Validators/DeferredExecutionValidator.cs @@ -16,7 +16,7 @@ public class DeferredExecutionValidator : IValidator public bool TreatsWarningsAsErrors { get; } - public IEnumerable Validate(ValidationParameters validationParameters) + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => validationParameters.Benchmarks .Where(benchmark => IsDeferredExecution(benchmark.Descriptor.WorkloadMethod.ReturnType)) .Select(benchmark => @@ -24,7 +24,9 @@ public IEnumerable Validate(ValidationParameters validationPara TreatsWarningsAsErrors, $"Benchmark {benchmark.Descriptor.Type.Name}.{benchmark.Descriptor.WorkloadMethod.Name} returns a deferred execution result ({benchmark.Descriptor.WorkloadMethod.ReturnType.GetCorrectCSharpTypeName(false, false)}). " + "You need to either change the method declaration to return a materialized result or consume it on your own. You can use .Consume() extension method to do that.", - benchmark)); + benchmark) + ) + .ToAsyncEnumerable(); private bool IsDeferredExecution(Type returnType) { diff --git a/src/BenchmarkDotNet/Validators/DiagnosersValidator.cs b/src/BenchmarkDotNet/Validators/DiagnosersValidator.cs index 7c6f9abb40..ce38f98cca 100644 --- a/src/BenchmarkDotNet/Validators/DiagnosersValidator.cs +++ b/src/BenchmarkDotNet/Validators/DiagnosersValidator.cs @@ -13,10 +13,11 @@ private DiagnosersValidator() public bool TreatsWarningsAsErrors => true; - public IEnumerable Validate(ValidationParameters validationParameters) + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => validationParameters .Config .GetDiagnosers() - .SelectMany(diagnoser => diagnoser.Validate(validationParameters)); + .ToAsyncEnumerable() + .SelectMany(diagnoser => diagnoser.ValidateAsync(validationParameters)); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs b/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs index c78c86b079..7fe92ae71b 100644 --- a/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs +++ b/src/BenchmarkDotNet/Validators/DotNetSdkValidator.cs @@ -35,7 +35,7 @@ public static IEnumerable ValidateFrameworkSdks(BenchmarkCase b { var targetRuntime = benchmark.Job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic) ? benchmark.Job.Environment.Runtime! - : ClrRuntime.GetTargetOrCurrentVersion(benchmark.Descriptor.WorkloadMethod.DeclaringType!.Assembly); + : ClrRuntime.GetTargetOrCurrentVersion(benchmark.Descriptor.Type.Assembly); var requiredSdkVersion = targetRuntime.RuntimeMoniker.GetRuntimeVersion(); var installedVersionString = cachedFrameworkSdks.Value.FirstOrDefault(); diff --git a/src/BenchmarkDotNet/Validators/ExecutionValidator.cs b/src/BenchmarkDotNet/Validators/ExecutionValidator.cs index 79f3037126..10db465f57 100644 --- a/src/BenchmarkDotNet/Validators/ExecutionValidator.cs +++ b/src/BenchmarkDotNet/Validators/ExecutionValidator.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Running; namespace BenchmarkDotNet.Validators @@ -12,13 +14,14 @@ public class ExecutionValidator : ExecutionValidatorBase private ExecutionValidator(bool failOnError) : base(failOnError) { } - protected override void ExecuteBenchmarks(object benchmarkTypeInstance, IEnumerable benchmarks, List errors) + protected override async ValueTask ExecuteBenchmarksAsync(object benchmarkTypeInstance, IEnumerable benchmarks, List errors) { foreach (var benchmark in benchmarks) { try { - benchmark.Descriptor.WorkloadMethod.Invoke(benchmarkTypeInstance, null); + var result = benchmark.Descriptor.WorkloadMethod.Invoke(benchmarkTypeInstance, null); + await DynamicAwaitHelper.GetOrAwaitResult(result); } catch (Exception ex) { diff --git a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs index aa58076e96..6d1ac89387 100644 --- a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs +++ b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs @@ -20,7 +20,7 @@ protected ExecutionValidatorBase(bool failOnError) public bool TreatsWarningsAsErrors { get; } - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { var errors = new List(); @@ -41,17 +41,20 @@ public IEnumerable Validate(ValidationParameters validationPara continue; } - if (!TryToCallGlobalSetup(benchmarkTypeInstance, errors)) + if (!await TryToCallGlobalSetup(benchmarkTypeInstance, errors)) { continue; } - ExecuteBenchmarks(benchmarkTypeInstance, typeGroup, errors); + await ExecuteBenchmarksAsync(benchmarkTypeInstance, typeGroup, errors); - TryToCallGlobalCleanup(benchmarkTypeInstance, errors); + await TryToCallGlobalCleanup(benchmarkTypeInstance, errors); } - return errors; + foreach (var error in errors) + { + yield return error; + } } private bool TryCreateBenchmarkTypeInstance(Type type, List errors, [NotNullWhen(true)] out object? instance) @@ -73,17 +76,17 @@ private bool TryCreateBenchmarkTypeInstance(Type type, List err } } - private bool TryToCallGlobalSetup(object benchmarkTypeInstance, List errors) + private async ValueTask TryToCallGlobalSetup(object benchmarkTypeInstance, List errors) { - return TryToCallGlobalMethod(benchmarkTypeInstance, errors); + return await TryToCallGlobalMethod(benchmarkTypeInstance, errors); } - private void TryToCallGlobalCleanup(object benchmarkTypeInstance, List errors) + private async ValueTask TryToCallGlobalCleanup(object benchmarkTypeInstance, List errors) { - TryToCallGlobalMethod(benchmarkTypeInstance, errors); + await TryToCallGlobalMethod(benchmarkTypeInstance, errors); } - private bool TryToCallGlobalMethod(object benchmarkTypeInstance, List errors) + private async ValueTask TryToCallGlobalMethod(object benchmarkTypeInstance, List errors) { var methods = benchmarkTypeInstance .GetType() @@ -91,7 +94,7 @@ private bool TryToCallGlobalMethod(object benchmarkTypeInstance, List methodInfo.GetCustomAttributes(false).OfType().Any()) .ToArray(); - if (!methods.Any()) + if (methods.Length == 0) { return true; } @@ -107,9 +110,9 @@ private bool TryToCallGlobalMethod(object benchmarkTypeInstance, List(object benchmarkTypeInstance, List type.Name.Replace("Attribute", string.Empty); - private void TryToGetTaskResult(object? result) - { - if (result == null) - { - return; - } - - AwaitHelper.GetGetResultMethod(result.GetType()) - ?.Invoke(null, new[] { result }); - } - private bool TryToSetParamsFields(object benchmarkTypeInstance, List errors) { var paramFields = benchmarkTypeInstance @@ -247,6 +239,6 @@ protected static string GetDisplayExceptionMessage(Exception ex) return ex?.Message ?? "Unknown error"; } - protected abstract void ExecuteBenchmarks(object benchmarkTypeInstance, IEnumerable benchmarks, List errors); + protected abstract ValueTask ExecuteBenchmarksAsync(object benchmarkTypeInstance, IEnumerable benchmarks, List errors); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/GenericBenchmarksValidator.cs b/src/BenchmarkDotNet/Validators/GenericBenchmarksValidator.cs index 45ef7b5cc5..4ff667e63c 100644 --- a/src/BenchmarkDotNet/Validators/GenericBenchmarksValidator.cs +++ b/src/BenchmarkDotNet/Validators/GenericBenchmarksValidator.cs @@ -11,7 +11,7 @@ public class GenericBenchmarksValidator : IValidator public bool TreatsWarningsAsErrors => false; - public IEnumerable Validate(ValidationParameters validationParameters) + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => validationParameters .Benchmarks .Select(benchmark => benchmark.Descriptor.Type.Assembly) @@ -19,6 +19,7 @@ public IEnumerable Validate(ValidationParameters validationPara .SelectMany(assembly => assembly.GetRunnableBenchmarks()) .SelectMany(GenericBenchmarksBuilder.BuildGenericsIfNeeded) .Where(result => !result.isSuccess) - .Select(result => new ValidationError(false, $"Generic type {result.result.Name} failed to build due to wrong type argument or arguments count, ignoring.")); + .Select(result => new ValidationError(false, $"Generic type {result.result.Name} failed to build due to wrong type argument or arguments count, ignoring.")) + .ToAsyncEnumerable(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/IValidator.cs b/src/BenchmarkDotNet/Validators/IValidator.cs index a179cb3bbf..f904cc0c42 100644 --- a/src/BenchmarkDotNet/Validators/IValidator.cs +++ b/src/BenchmarkDotNet/Validators/IValidator.cs @@ -6,6 +6,6 @@ public interface IValidator { bool TreatsWarningsAsErrors { get; } - IEnumerable Validate(ValidationParameters validationParameters); + IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/JitOptimizationsValidator.cs b/src/BenchmarkDotNet/Validators/JitOptimizationsValidator.cs index 89ee75b6de..ad9cb2b707 100644 --- a/src/BenchmarkDotNet/Validators/JitOptimizationsValidator.cs +++ b/src/BenchmarkDotNet/Validators/JitOptimizationsValidator.cs @@ -15,7 +15,7 @@ public class JitOptimizationsValidator : IValidator public bool TreatsWarningsAsErrors { get; } - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { foreach (var group in validationParameters.Benchmarks.GroupBy(benchmark => benchmark.Descriptor.Type.GetTypeInfo().Assembly)) { diff --git a/src/BenchmarkDotNet/Validators/ParamsAllValuesValidator.cs b/src/BenchmarkDotNet/Validators/ParamsAllValuesValidator.cs index 8f283f91be..777349e093 100644 --- a/src/BenchmarkDotNet/Validators/ParamsAllValuesValidator.cs +++ b/src/BenchmarkDotNet/Validators/ParamsAllValuesValidator.cs @@ -18,14 +18,15 @@ private ParamsAllValuesValidator() { } private const BindingFlags ReflectionFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - public IEnumerable Validate(ValidationParameters input) => + public IAsyncEnumerable ValidateAsync(ValidationParameters input) => input.Benchmarks .Select(benchmark => benchmark.Descriptor.Type) .Distinct() .SelectMany(type => type.GetTypeMembersWithGivenAttribute(ReflectionFlags)) .Distinct() .Select(member => GetErrorOrDefault(member.ParameterType)) - .WhereNotNull(); + .WhereNotNull() + .ToAsyncEnumerable(); private bool IsBool(Type paramType) => paramType == typeof(bool); private bool IsEnum(Type paramType) => paramType.GetTypeInfo().IsEnum; diff --git a/src/BenchmarkDotNet/Validators/ParamsValidator.cs b/src/BenchmarkDotNet/Validators/ParamsValidator.cs index 37dc645af5..95981601f5 100644 --- a/src/BenchmarkDotNet/Validators/ParamsValidator.cs +++ b/src/BenchmarkDotNet/Validators/ParamsValidator.cs @@ -13,12 +13,13 @@ public class ParamsValidator : IValidator public bool TreatsWarningsAsErrors => true; - public IEnumerable Validate(ValidationParameters input) => input.Benchmarks + public IAsyncEnumerable ValidateAsync(ValidationParameters input) => input.Benchmarks .Select(benchmark => benchmark.Descriptor.Type) .Distinct() - .SelectMany(Validate); + .ToAsyncEnumerable() + .SelectMany(ValidateAsync); - private IEnumerable Validate(Type type) + private async IAsyncEnumerable ValidateAsync(Type type) { const BindingFlags reflectionFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.FlattenHierarchy; diff --git a/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs b/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs index 65d80d6c5a..3e60d32a9d 100644 --- a/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs +++ b/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs @@ -6,6 +6,7 @@ using System.Reflection; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Parameters; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.InProcess.NoEmit; @@ -20,7 +21,7 @@ public class ReturnValueValidator : ExecutionValidatorBase private ReturnValueValidator(bool failOnError) : base(failOnError) { } - protected override void ExecuteBenchmarks(object benchmarkTypeInstance, IEnumerable benchmarks, List errors) + protected override async System.Threading.Tasks.ValueTask ExecuteBenchmarksAsync(object benchmarkTypeInstance, IEnumerable benchmarks, List errors) { foreach (var parameterGroup in benchmarks.GroupBy(i => i.Parameters, ParameterInstancesEqualityComparer.Instance)) { @@ -32,10 +33,16 @@ protected override void ExecuteBenchmarks(object benchmarkTypeInstance, IEnumera try { InProcessNoEmitRunner.FillMembers(benchmarkTypeInstance, benchmark); - var result = benchmark.Descriptor.WorkloadMethod.Invoke(benchmarkTypeInstance, null)!; + var result = benchmark.Descriptor.WorkloadMethod.Invoke(benchmarkTypeInstance, null); if (benchmark.Descriptor.WorkloadMethod.ReturnType != typeof(void)) - results.Add((benchmark, result)); + { + (var hasResult, result) = await DynamicAwaitHelper.GetOrAwaitResult(result); + if (hasResult) + { + results.Add((benchmark, result!)); + } + } } catch (Exception ex) { diff --git a/src/BenchmarkDotNet/Validators/RunModeValidator.cs b/src/BenchmarkDotNet/Validators/RunModeValidator.cs index ca11c9732c..a4dd3fd352 100644 --- a/src/BenchmarkDotNet/Validators/RunModeValidator.cs +++ b/src/BenchmarkDotNet/Validators/RunModeValidator.cs @@ -15,7 +15,7 @@ private RunModeValidator() { } public bool TreatsWarningsAsErrors => true; - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { var resolver = new CompositeResolver(EnvironmentResolver.Instance, EngineResolver.Instance); // TODO: use specified resolver. foreach (var benchmark in validationParameters.Benchmarks) diff --git a/src/BenchmarkDotNet/Validators/RuntimeValidator.cs b/src/BenchmarkDotNet/Validators/RuntimeValidator.cs index debadf880e..c32d2b2663 100644 --- a/src/BenchmarkDotNet/Validators/RuntimeValidator.cs +++ b/src/BenchmarkDotNet/Validators/RuntimeValidator.cs @@ -17,7 +17,7 @@ private RuntimeValidator() { } public bool TreatsWarningsAsErrors => false; - public IEnumerable Validate(ValidationParameters input) + public async IAsyncEnumerable ValidateAsync(ValidationParameters input) { var allBenchmarks = input.Benchmarks.ToArray(); var nullRuntimeBenchmarks = allBenchmarks.Where(x => x.Job.Environment.Runtime == null).ToArray(); @@ -25,10 +25,9 @@ public IEnumerable Validate(ValidationParameters input) // There is no validation error if all the runtimes are set or if all the runtimes are null. if (allBenchmarks.Length == nullRuntimeBenchmarks.Length) { - return []; + yield break; } - var errors = new List(); foreach (var benchmark in nullRuntimeBenchmarks.Where(x=> !x.GetToolchain().IsInProcess)) { var job = benchmark.Job; @@ -37,8 +36,7 @@ public IEnumerable Validate(ValidationParameters input) : CharacteristicSetPresenter.Display.ToPresentation(job); // Use job text representation instead for auto generated JobId. var message = $"Job({jobText}) doesn't have a Runtime characteristic. It's recommended to specify runtime by using WithRuntime explicitly."; - errors.Add(new ValidationError(false, message)); + yield return new ValidationError(false, message); } - return errors; } } diff --git a/src/BenchmarkDotNet/Validators/SetupCleanupValidator.cs b/src/BenchmarkDotNet/Validators/SetupCleanupValidator.cs index 5617251a66..60c7871170 100644 --- a/src/BenchmarkDotNet/Validators/SetupCleanupValidator.cs +++ b/src/BenchmarkDotNet/Validators/SetupCleanupValidator.cs @@ -14,7 +14,7 @@ private SetupCleanupValidator() { } public bool TreatsWarningsAsErrors => true; // it is a must! - public IEnumerable Validate(ValidationParameters input) + public IAsyncEnumerable ValidateAsync(ValidationParameters input) { var validationErrors = new List(); @@ -28,7 +28,7 @@ public IEnumerable Validate(ValidationParameters input) validationErrors.AddRange(ValidateAttributes(groupByType.Key.Name, allMethods)); } - return validationErrors; + return validationErrors.ToAsyncEnumerable(); } private IEnumerable ValidateAttributes(string benchmarkClassName, IEnumerable allMethods) where T : TargetedAttribute diff --git a/src/BenchmarkDotNet/Validators/ShadowCopyValidator.cs b/src/BenchmarkDotNet/Validators/ShadowCopyValidator.cs index 4d0db84d6c..d1f0927f91 100644 --- a/src/BenchmarkDotNet/Validators/ShadowCopyValidator.cs +++ b/src/BenchmarkDotNet/Validators/ShadowCopyValidator.cs @@ -13,7 +13,7 @@ private ShadowCopyValidator() { } public bool TreatsWarningsAsErrors => false; - public IEnumerable Validate(ValidationParameters validationParameters) + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => validationParameters .Benchmarks .Select(benchmark => benchmark.Descriptor.Type.GetTypeInfo().Assembly) @@ -22,6 +22,8 @@ public IEnumerable Validate(ValidationParameters validationPara .Select( assembly => new ValidationError( false, - $"Assembly {assembly} is located in temp. If you are running benchmarks from xUnit you need to disable shadow copy. It's not supported by design.")); + $"Assembly {assembly} is located in temp. If you are running benchmarks from xUnit you need to disable shadow copy. It's not supported by design.") + ) + .ToAsyncEnumerable(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/ValidationErrorReporter.cs b/src/BenchmarkDotNet/Validators/ValidationErrorReporter.cs index 7afc00765c..f5c122f1af 100644 --- a/src/BenchmarkDotNet/Validators/ValidationErrorReporter.cs +++ b/src/BenchmarkDotNet/Validators/ValidationErrorReporter.cs @@ -1,23 +1,23 @@ using System.Collections.Generic; +using System.Threading.Tasks; using BenchmarkDotNet.Engines; using JetBrains.Annotations; -namespace BenchmarkDotNet.Validators +namespace BenchmarkDotNet.Validators; + +[UsedImplicitly] +public static class ValidationErrorReporter { - public static class ValidationErrorReporter - { - public const string ConsoleErrorPrefix = "// ERROR: "; + public const string ConsoleErrorPrefix = "// ERROR: "; - [UsedImplicitly] // Generated benchmarks - public static bool ReportIfAny(IEnumerable validationErrors, IHost host) + public static async ValueTask ReportIfAnyAsync(IEnumerable validationErrors, IHost host) + { + bool hasErrors = false; + foreach (var validationError in validationErrors) { - bool hasErrors = false; - foreach (var validationError in validationErrors) - { - host.SendError(validationError.Message); - hasErrors = true; - } - return hasErrors; + host.SendError(validationError.Message); + hasErrors = true; } + return hasErrors; } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs b/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs index 36ea5688ad..73db064aea 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/AllSetupAndCleanupTest.cs @@ -96,10 +96,10 @@ public class AllSetupAndCleanupAttributeBenchmarksTask 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); @@ -117,10 +117,20 @@ public class AllSetupAndCleanupAttributeBenchmarksGenericTask 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 async Task GlobalSetup() @@ -148,10 +158,10 @@ public class AllSetupAndCleanupAttributeBenchmarksValueTask private int cleanupCounter; [IterationSetup] - public void IterationSetup() => Console.WriteLine(IterationSetupCalled + " (" + ++setupCounter + ")"); + public ValueTask IterationSetup() => new ValueTask(Console.Out.WriteLineAsync(IterationSetupCalled + " (" + ++setupCounter + ")")); [IterationCleanup] - public void IterationCleanup() => Console.WriteLine(IterationCleanupCalled + " (" + ++cleanupCounter + ")"); + public ValueTask IterationCleanup() => new ValueTask(Console.Out.WriteLineAsync(IterationCleanupCalled + " (" + ++cleanupCounter + ")")); [GlobalSetup] public ValueTask GlobalSetup() => new ValueTask(Console.Out.WriteLineAsync(GlobalSetupCalled)); @@ -169,10 +179,20 @@ public class AllSetupAndCleanupAttributeBenchmarksGenericValueTask 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 ValueTask GlobalSetup() @@ -201,10 +221,20 @@ public class AllSetupAndCleanupAttributeBenchmarksValueTaskSource 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() @@ -233,10 +263,20 @@ public class AllSetupAndCleanupAttributeBenchmarksGenericValueTaskSource 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() diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs index e835065b27..57bd930114 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using BenchmarkDotNet.IntegrationTests.Xunit; using BenchmarkDotNet.Reports; +using System.Threading.Tasks; namespace BenchmarkDotNet.IntegrationTests { @@ -36,10 +37,8 @@ protected BenchmarkTestExecutor(ITestOutputHelper output) /// Optional custom config to be used instead of the default /// Optional: disable validation (default = true/enabled) /// The summary from the benchmark run - public Reports.Summary CanExecute(IConfig? config = null, bool fullValidation = true) - { - return CanExecute(typeof(TBenchmark), config, fullValidation); - } + public Summary CanExecute(IConfig? config = null, bool fullValidation = true) + => CanExecute(typeof(TBenchmark), config, fullValidation); /// /// Runs Benchmarks with the most simple config (SingleRunFastConfig) @@ -50,7 +49,36 @@ public Reports.Summary CanExecute(IConfig? config = null, bool fullV /// Optional custom config to be used instead of the default /// Optional: disable validation (default = true/enabled) /// The summary from the benchmark run - public Reports.Summary CanExecute(Type type, IConfig? config = null, bool fullValidation = true) + public Summary CanExecute(Type type, IConfig? config = null, bool fullValidation = true) + { + // Make sure we ALWAYS combine the Config (default or passed in) with any Config applied to the Type/Class + var summary = BenchmarkRunner.Run(type, GetMinimalConfig(config)); + + if (fullValidation) + { + ValidateSummary(summary); + } + + return summary; + } + + public ValueTask CanExecuteAsync(IConfig? config = null, bool fullValidation = true) + => CanExecuteAsync(typeof(TBenchmark), config, fullValidation); + + public async ValueTask CanExecuteAsync(Type type, IConfig? config = null, bool fullValidation = true) + { + // Make sure we ALWAYS combine the Config (default or passed in) with any Config applied to the Type/Class + var summary = await BenchmarkRunner.RunAsync(type, GetMinimalConfig(config)); + + if (fullValidation) + { + ValidateSummary(summary); + } + + return summary; + } + + private IConfig GetMinimalConfig(IConfig? config) { // Add logging, so the Benchmark execution is in the TestRunner output (makes Debugging easier) if (config == null) @@ -64,30 +92,27 @@ public Reports.Summary CanExecute(Type type, IConfig? config = null, bool fullVa if (!config.GetColumnProviders().Any()) config = config.AddColumnProvider(DefaultColumnProviders.Instance); - // Make sure we ALWAYS combine the Config (default or passed in) with any Config applied to the Type/Class - var summary = BenchmarkRunner.Run(type, config); - - if (fullValidation) - { - Assert.False(summary.HasCriticalValidationErrors, "The \"Summary\" should have NOT \"HasCriticalValidationErrors\""); + return config!; + } - Assert.True(summary.Reports.Any(), "The \"Summary\" should contain at least one \"BenchmarkReport\" in the \"Reports\" collection"); + private static void ValidateSummary(Summary summary) + { + Assert.False(summary.HasCriticalValidationErrors, "The \"Summary\" should have NOT \"HasCriticalValidationErrors\""); - summary.CheckPlatformLinkerIssues(); + Assert.True(summary.Reports.Any(), "The \"Summary\" should contain at least one \"BenchmarkReport\" in the \"Reports\" collection"); - Assert.True(summary.Reports.All(r => r.BuildResult.IsBuildSuccess), - "The following benchmarks have failed to build: " + - string.Join(", ", summary.Reports.Where(r => !r.BuildResult.IsBuildSuccess).Select(r => r.BenchmarkCase.DisplayInfo))); + summary.CheckPlatformLinkerIssues(); - Assert.True(summary.Reports.All(r => r.ExecuteResults != null), - "The following benchmarks don't have any execution results: " + - string.Join(", ", summary.Reports.Where(r => r.ExecuteResults == null).Select(r => r.BenchmarkCase.DisplayInfo))); + Assert.True(summary.Reports.All(r => r.BuildResult.IsBuildSuccess), + "The following benchmarks have failed to build: " + + string.Join(", ", summary.Reports.Where(r => !r.BuildResult.IsBuildSuccess).Select(r => r.BenchmarkCase.DisplayInfo))); - Assert.True(summary.Reports.All(r => r.ExecuteResults.All(er => er.IsSuccess)), - "All reports should have succeeded to execute"); - } + Assert.True(summary.Reports.All(r => r.ExecuteResults != null), + "The following benchmarks don't have any execution results: " + + string.Join(", ", summary.Reports.Where(r => r.ExecuteResults == null).Select(r => r.BenchmarkCase.DisplayInfo))); - return summary; + Assert.True(summary.Reports.All(r => r.ExecuteResults.All(er => er.IsSuccess)), + "All reports should have succeeded to execute"); } protected IConfig CreateSimpleConfig(OutputLogger? logger = null, Job? job = null) diff --git a/tests/BenchmarkDotNet.IntegrationTests/CodeGenTests.cs b/tests/BenchmarkDotNet.IntegrationTests/CodeGenTests.cs new file mode 100644 index 0000000000..d8b1fdff1c --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/CodeGenTests.cs @@ -0,0 +1,148 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Tests.XUnit; +using BenchmarkDotNet.Toolchains; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace BenchmarkDotNet.IntegrationTests; + +public class CodeGenTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) +{ + public static IEnumerable GetToolchains() => + [ + [Job.Default.GetToolchain()], + [InProcessEmitToolchain.Default] + ]; + + [TheoryEnvSpecific(".Net Framework JIT is not tiered", EnvRequirement.DotNetCoreOnly)] + [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void GeneratedBenchmarkTypeMethodsAreAggressivelyOptimized(IToolchain toolchain) + { + var config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + CanExecute(config); + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public class BenchmarkManyTypes + { + private static void AssertAggressiveOptimization() + { + var runnableType = GetRunnableType(); + AssertMethodsAggressivelyOptimized(runnableType); + + static Type GetRunnableType() + { + var stacktrace = new StackTrace(false); + for (int i = 0; i < stacktrace.FrameCount; i++) + { + var benchmarkType = stacktrace.GetFrame(i).GetMethod().DeclaringType; + do + { + if (benchmarkType.Name.StartsWith("Runnable_")) + { + return benchmarkType; + } + benchmarkType = benchmarkType.DeclaringType; + } + while (benchmarkType != null); + } + Assert.Fail("Runnable type not found"); + return null; // unreachable + } + + static void AssertMethodsAggressivelyOptimized(Type type) + { + foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) + { + if (method.MethodImplementationFlags.HasFlag(MethodImplAttributes.NoOptimization)) + { + Assert.False( + method.MethodImplementationFlags.HasFlag(Portability.CodeGenHelper.AggressiveOptimizationOptionForEmit), + $"Method is aggressively optimized: {method}" + ); + } + else + { + Assert.True( + method.MethodImplementationFlags.HasFlag(Portability.CodeGenHelper.AggressiveOptimizationOptionForEmit), + $"Method is not aggressively optimized: {method}" + ); + } + } + + foreach (var nestedType in type.GetNestedTypes()) + { + AssertMethodsAggressivelyOptimized(nestedType); + } + } + } + + [Benchmark] + public void ReturnVoid() => AssertAggressiveOptimization(); + + [Benchmark] + public async Task ReturnTaskAsync() => AssertAggressiveOptimization(); + + [Benchmark] + public async ValueTask ReturnValueTaskAsync() => AssertAggressiveOptimization(); + + [Benchmark] + public string ReturnRefType() + { + AssertAggressiveOptimization(); + return default; + } + + [Benchmark] + public decimal ReturnValueType() + { + AssertAggressiveOptimization(); + return default; + } + + [Benchmark] + public async Task ReturnTaskOfTAsync() + { + AssertAggressiveOptimization(); + return default; + } + + [Benchmark] + public ValueTask ReturnValueTaskOfT() + { + AssertAggressiveOptimization(); + return default; + } + + private int intField; + + [Benchmark] + public ref int ReturnByRefType() + { + AssertAggressiveOptimization(); + return ref intField; + } + + [Benchmark] + public ref readonly int ReturnByRefReadonlyType() + { + AssertAggressiveOptimization(); + return ref intField; + } + + [Benchmark] + public unsafe void* ReturnVoidPointerType() + { + AssertAggressiveOptimization(); + return default; + } + } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/CustomAwaitable.cs b/tests/BenchmarkDotNet.IntegrationTests/CustomAwaitable.cs new file mode 100644 index 0000000000..b7902e100a --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/CustomAwaitable.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BenchmarkDotNet.IntegrationTests; + +public sealed class CustomAwaitable : ICriticalNotifyCompletion +{ + public CustomAwaitable GetAwaiter() => this; + + public void OnCompleted(Action continuation) => continuation(); + + public void UnsafeOnCompleted(Action continuation) => continuation(); + + public bool IsCompleted => true; + + public void GetResult() { } +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs b/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs index e5afe4e0df..161ee7932d 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/CustomEngineTests.cs @@ -6,8 +6,8 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Engines; -using BenchmarkDotNet.Reports; using Perfolizer.Mathematics.OutlierDetection; +using System.Threading.Tasks; namespace BenchmarkDotNet.IntegrationTests { @@ -56,9 +56,9 @@ public IEngine Create(EngineParameters engineParameters) public class CustomEngine(EngineParameters engineParameters) : IEngine { - public RunResults Run() + public async ValueTask RunAsync() { - engineParameters.GlobalSetupAction.Invoke(); + await engineParameters.GlobalSetupAction.Invoke(); Console.WriteLine(EngineRunMessage); try { @@ -73,7 +73,7 @@ public RunResults Run() } finally { - engineParameters.GlobalCleanupAction.Invoke(); + await engineParameters.GlobalCleanupAction.Invoke(); } } } diff --git a/tests/BenchmarkDotNet.IntegrationTests/CustomTask.cs b/tests/BenchmarkDotNet.IntegrationTests/CustomTask.cs new file mode 100644 index 0000000000..d496bfb928 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/CustomTask.cs @@ -0,0 +1,109 @@ +using System; +using System.Runtime.CompilerServices; +using System.Security; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.IntegrationTests; + +[AsyncMethodBuilder(typeof(AsyncCustomTaskMethodBuilder))] +public readonly struct CustomTask(ValueTask valueTask) +{ + public CustomTaskAwaiter GetAwaiter() => new(valueTask.GetAwaiter()); +} + +public readonly struct CustomTaskAwaiter(ValueTaskAwaiter valueTaskAwaiter) : ICriticalNotifyCompletion +{ + public bool IsCompleted => valueTaskAwaiter.IsCompleted; + + public void GetResult() => valueTaskAwaiter.GetResult(); + + public void OnCompleted(Action continuation) => valueTaskAwaiter.OnCompleted(continuation); + + public void UnsafeOnCompleted(Action continuation) => valueTaskAwaiter.UnsafeOnCompleted(continuation); +} + +public struct AsyncCustomTaskMethodBuilder +{ + public static int InUseCounter { get; private set; } + + private AsyncValueTaskMethodBuilder _methodBuilder; + + public static AsyncCustomTaskMethodBuilder Create() + { + ++InUseCounter; + return default; + } + + public CustomTask Task => new(_methodBuilder.Task); + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => _methodBuilder.Start(ref stateMachine); + + public void SetStateMachine(IAsyncStateMachine stateMachine) => _methodBuilder.SetStateMachine(stateMachine); + + public void SetResult() + { + --InUseCounter; + _methodBuilder.SetResult(); + } + + public void SetException(Exception exception) + { + --InUseCounter; + _methodBuilder.SetException(exception); + } + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + => _methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); + + [SecuritySafeCritical] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => _methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); +} + +public struct AsyncWrapperTaskMethodBuilder +{ + public static int InUseCounter { get; private set; } + + private AsyncTaskMethodBuilder _methodBuilder; + + public static AsyncWrapperTaskMethodBuilder Create() + { + ++InUseCounter; + return default; + } + + public Task Task => _methodBuilder.Task; + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => _methodBuilder.Start(ref stateMachine); + + public void SetStateMachine(IAsyncStateMachine stateMachine) => _methodBuilder.SetStateMachine(stateMachine); + + public void SetResult() + { + --InUseCounter; + _methodBuilder.SetResult(); + } + + public void SetException(Exception exception) + { + --InUseCounter; + _methodBuilder.SetException(exception); + } + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + => _methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); + + [SecuritySafeCritical] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + => _methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/CustomTaskAndAwaitableTests.cs b/tests/BenchmarkDotNet.IntegrationTests/CustomTaskAndAwaitableTests.cs new file mode 100644 index 0000000000..fe2045a1f7 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/CustomTaskAndAwaitableTests.cs @@ -0,0 +1,233 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace BenchmarkDotNet.IntegrationTests; + +public class CustomTaskAndAwaitableTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) +{ + public static TheoryData GetToolchains() => new( + [ + Job.Default.GetToolchain(), + InProcessEmitToolchain.Default + ]); + + public class BenchmarkCustomAwaitableSimple + { + [GlobalSetup] + public CustomAwaitable GlobalSetup() => new(); + + [GlobalCleanup] + public CustomAwaitable GlobalCleanup() => new(); + + [IterationSetup] + public CustomAwaitable IterationSetup() => new(); + + [IterationCleanup] + public CustomAwaitable IterationCleanup() => new(); + + [Benchmark] + public CustomAwaitable Benchmark() => new(); + } + + [Theory] + [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void ReturnCustomAwaitableType(IToolchain toolchain) + { + var config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + CanExecute(config); + } + + public class BenchmarkCustomAwaitableOverrideCaller + { + [GlobalSetup] + public CustomAwaitable GlobalSetup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [GlobalCleanup] + public CustomAwaitable GlobalCleanup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [IterationSetup] + public CustomAwaitable IterationSetup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [IterationCleanup] + public CustomAwaitable IterationCleanup() + { + Assert.Equal(1, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [Benchmark] + [AsyncCallerType(typeof(CustomTask))] + public CustomAwaitable Benchmark() + { + Assert.Equal(1, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + } + + [Theory] + [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void ReturnCustomAwaitableTypeAndOverrideCaller(IToolchain toolchain) + { + var config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + CanExecute(config); + } + + public class BenchmarkCustomTask + { + [GlobalSetup] + public CustomTask GlobalSetup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [GlobalCleanup] + public CustomTask GlobalCleanup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [IterationSetup] + public CustomTask IterationSetup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [IterationCleanup] + public CustomTask IterationCleanup() + { + Assert.Equal(1, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [Benchmark] + public async CustomTask Benchmark() + { + Assert.Equal(2, AsyncCustomTaskMethodBuilder.InUseCounter); + await new ValueTask(); + } + } + + [Theory] + [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void ReturnCustomTask(IToolchain toolchain) + { + var config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + CanExecute(config); + } + + public class BenchmarkCustomTaskOverrideCaller + { + [GlobalSetup] + public CustomTask GlobalSetup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [GlobalCleanup] + public CustomTask GlobalCleanup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [IterationSetup] + public CustomTask IterationSetup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [IterationCleanup] + public CustomTask IterationCleanup() + { + Assert.Equal(0, AsyncCustomTaskMethodBuilder.InUseCounter); + return new(); + } + + [Benchmark] + [AsyncCallerType(typeof(ValueTask))] + public async CustomTask Benchmark() + { + Assert.Equal(1, AsyncCustomTaskMethodBuilder.InUseCounter); + await new ValueTask(); + } + } + + [Theory] + [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void ReturnCustomTaskAndOverrideCaller(IToolchain toolchain) + { + var config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + CanExecute(config); + } + + // [AsyncMethodBuilder] on methods is C# 10 feature, but it also requires the attribute be allowed on methods, which only works in .Net 5+. + // It can technically be poly-filled to work, but it's not worth the extra complexity to make it work for the generated project. +#if NET5_0_OR_GREATER + public class BenchmarkCustomBuilder + { + [GlobalSetup] + public void GlobalSetup() + { + Assert.Equal(0, AsyncWrapperTaskMethodBuilder.InUseCounter); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + Assert.Equal(0, AsyncWrapperTaskMethodBuilder.InUseCounter); + } + + [IterationSetup] + public void IterationSetup() + { + Assert.Equal(0, AsyncWrapperTaskMethodBuilder.InUseCounter); + } + + [IterationCleanup] + public void IterationCleanup() + { + Assert.Equal(1, AsyncWrapperTaskMethodBuilder.InUseCounter); + } + + [Benchmark] + [AsyncMethodBuilder(typeof(AsyncWrapperTaskMethodBuilder))] + public async Task Benchmark() + { + Assert.Equal(2, AsyncWrapperTaskMethodBuilder.InUseCounter); + await new ValueTask(); + } + } + + [Theory] + [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void ReturnCustomBuilder(IToolchain toolchain) + { + var config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + CanExecute(config); + } +#endif +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/Diagnosers/MockInProcessDiagnoser.cs b/tests/BenchmarkDotNet.IntegrationTests/Diagnosers/MockInProcessDiagnoser.cs index 938d0cbdf5..3766335d25 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/Diagnosers/MockInProcessDiagnoser.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/Diagnosers/MockInProcessDiagnoser.cs @@ -8,6 +8,7 @@ using BenchmarkDotNet.Validators; using System; using System.Collections.Generic; +using System.Linq; using System.Threading; namespace BenchmarkDotNet.IntegrationTests.Diagnosers; @@ -35,7 +36,7 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { } public IEnumerable ProcessResults(DiagnoserResults results) => []; - public IEnumerable Validate(ValidationParameters validationParameters) => []; + public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => AsyncEnumerable.Empty(); InProcessDiagnoserHandlerData IInProcessDiagnoser.GetHandlerData(BenchmarkCase benchmarkCase) => RunMode == RunMode.None diff --git a/tests/BenchmarkDotNet.IntegrationTests/EventProcessorTests.cs b/tests/BenchmarkDotNet.IntegrationTests/EventProcessorTests.cs index d188be01d4..5a4dd45739 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/EventProcessorTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/EventProcessorTests.cs @@ -208,7 +208,7 @@ public class ErrorAllCasesValidator : IValidator { public bool TreatsWarningsAsErrors => true; - public IEnumerable Validate(ValidationParameters validationParameters) + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) { foreach (var benchmark in validationParameters.Benchmarks) yield return new ValidationError(false, "Mock Validation", benchmark); @@ -221,7 +221,7 @@ public AllUnsupportedToolchain() : base("AllUnsupported", null, null, null) { } - public override IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) + public override async IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) { yield return new ValidationError(true, "Unsupported Benchmark", benchmarkCase); } diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs index ea0ea4a6e2..810d7cbd44 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes.CompilerServices; using BenchmarkDotNet.Loggers; using Mono.Cecil; using Mono.Cecil.Cil; @@ -10,23 +12,23 @@ namespace BenchmarkDotNet.IntegrationTests.InProcess.EmitTests { public class NaiveRunnableEmitDiff { - private static readonly HashSet IgnoredTypeNames = new HashSet() - { + private static readonly HashSet IgnoredTypeNames = + [ "BenchmarkDotNet.Autogenerated.UniqueProgramName", "BenchmarkDotNet.Autogenerated.DirtyAssemblyResolveHelper", // not required to be used in the InProcess toolchains (it's already used in the host process) - "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", // Conditionally added in runtimes older than .Net 7. - }; + "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", // Poly-filled type added in old runtimes. + ]; - private static readonly HashSet IgnoredAttributeTypeNames = new HashSet() - { - "System.Runtime.CompilerServices.CompilerGeneratedAttribute" - }; + private static readonly HashSet IgnoredAttributeTypeNames = + [ + typeof(AggressivelyOptimizeMethodsAttribute).FullName + ]; - private static readonly HashSet IgnoredRunnableMethodNames = new HashSet() - { + private static readonly HashSet IgnoredRunnableMethodNames = + [ "Run", ".ctor" - }; + ]; private static readonly IReadOnlyDictionary AltOpCodes = new Dictionary() { @@ -200,7 +202,16 @@ private static void Diff(CustomAttribute left, CustomAttribute right, ICustomAtt if (!AreSameTypeIgnoreNested(attArg1.Type, attArg2.Type)) throw new InvalidOperationException($"No matching attribute for {left.AttributeType} ({owner})"); - if (!Equals(attArg1.Value, attArg2.Value)) + object leftValue = attArg1.Value; + object rightValue = attArg2.Value; + if (left.AttributeType.FullName == typeof(AsyncStateMachineAttribute).FullName) + { + // We can't compare type arguments of AsyncStateMachineAttribute because the type is generated, + // we just have to compare their ToString. + leftValue = attArg1.Value.ToString(); + rightValue = attArg2.Value.ToString(); + } + if (!Equals(leftValue, rightValue)) throw new InvalidOperationException($"No matching attribute for {left.AttributeType} ({owner})"); } } @@ -341,7 +352,9 @@ private static void DiffDefinition(MethodDefinition method1, MethodDefinition me if (method1.Attributes != method2.Attributes) throw new InvalidOperationException($"No matching method for {method1}"); - if (method1.ImplAttributes != method2.ImplAttributes) + // Roslyn toolchain doesn't apply AggressiveOptimization, and doesn't need to because .Net Framework doesn't have tiered JIT. + // TODO: Don't exclude AggressiveOptimization if/when we compare IL in .Net Core. + if ((method1.ImplAttributes & ~MethodImplAttributes.AggressiveOptimization) != (method2.ImplAttributes & ~MethodImplAttributes.AggressiveOptimization)) throw new InvalidOperationException($"No matching method for {method1}"); if (method1.Parameters.Count != method2.Parameters.Count) diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/Runnable_0.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/Runnable_0.cs deleted file mode 100644 index 83eb22ec54..0000000000 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/Runnable_0.cs +++ /dev/null @@ -1,33 +0,0 @@ -using BenchmarkDotNet.Engines; -using BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; -using BenchmarkDotNet.Toolchains.Parameters; - -// ReSharper disable once CheckNamespace -namespace BenchmarkDotNet.Autogenerated.ReplaceMe -{ - // Stub for diff for RunMethod - // Used as a template for EmitRunMethod() - internal class Runnable_0 - { - public static void Run(IHost host, ExecuteParameters parameters) - { - var instance = new Runnable_0(); - - var (job, engineParameters, engineFactory) = RunnableReuse.PrepareForRun(instance, host, parameters); - - if (job == null) - return; - - var results = engineFactory.Create(engineParameters).Run(); - host.ReportResults(results); // printing costs memory, do this after runs - - instance.__TrickTheJIT__(); // compile the method for disassembler, but without actual run of the benchmark ;) - engineParameters.InProcessDiagnoserHandler.Handle(BenchmarkSignal.AfterEngine); - } - - public void __TrickTheJIT__() - { - throw new System.NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs index 7c04a611d0..ce8e7634e1 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs @@ -194,6 +194,13 @@ public ValueTask InvokeOnceValueTaskOfT() return new ValueTask(DecimalResult); } + [Benchmark] + public CustomAwaitable InvokeOnceCustomAwaitable() + { + Interlocked.Increment(ref Counter); + return new(); + } + [Benchmark] public static void InvokeOnceStaticVoid() { @@ -242,6 +249,13 @@ public static ValueTask InvokeOnceStaticValueTaskOfT() Interlocked.Increment(ref Counter); return new ValueTask(DecimalResult); } + + [Benchmark] + public static CustomAwaitable InvokeOnceStaticCustomAwaitable() + { + Interlocked.Increment(ref Counter); + return new(); + } } [Theory] @@ -249,6 +263,7 @@ public static ValueTask InvokeOnceStaticValueTaskOfT() [InlineData(typeof(GlobalSetupCleanupTask))] [InlineData(typeof(GlobalSetupCleanupValueTask))] [InlineData(typeof(GlobalSetupCleanupValueTaskSource))] + [InlineData(typeof(GlobalSetupCleanupCustomAwaitable))] public void InProcessEmitToolchainSupportsSetupAndCleanup(Type benchmarkType) { var logger = new OutputLogger(Output); @@ -365,6 +380,29 @@ public void InvokeOnceVoid() } } + public class GlobalSetupCleanupCustomAwaitable + { + [GlobalSetup] + public static CustomAwaitable GlobalSetup() + { + Interlocked.Increment(ref Counters.SetupCounter); + return new(); + } + + [GlobalCleanup] + public CustomAwaitable GlobalCleanup() + { + Interlocked.Increment(ref Counters.CleanupCounter); + return new(); + } + + [Benchmark] + public void InvokeOnceVoid() + { + Interlocked.Increment(ref Counters.BenchmarkCounter); + } + } + #if NET8_0_OR_GREATER [Fact] diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs index a4ec290c15..57777f8afe 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs @@ -14,8 +14,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; @@ -34,41 +36,43 @@ public InProcessTest(ITestOutputHelper output) : base(output) private const int UnrollFactor = 16; [Fact] - public void BenchmarkActionGlobalSetupSupported() => TestInvoke(x => BenchmarkAllCases.GlobalSetup(), UnrollFactor); + public async Task BenchmarkActionGlobalSetupSupported() => await TestInvoke(x => BenchmarkAllCases.GlobalSetup(), UnrollFactor); [Fact] - public void BenchmarkActionGlobalCleanupSupported() => TestInvoke(x => x.GlobalCleanup(), UnrollFactor); + public async Task BenchmarkActionGlobalCleanupSupported() => await TestInvoke(x => x.GlobalCleanup(), UnrollFactor); [Fact] - public void BenchmarkActionVoidSupported() => TestInvoke(x => x.InvokeOnceVoid(), UnrollFactor); + public async Task BenchmarkActionVoidSupported() => await TestInvoke(x => x.InvokeOnceVoid(), UnrollFactor); [Fact] - public void BenchmarkActionTaskSupported() => TestInvoke(x => x.InvokeOnceTaskAsync(), UnrollFactor); + public async Task BenchmarkActionTaskSupported() => await TestInvoke(x => x.InvokeOnceTaskAsync(), UnrollFactor); [Fact] - public void BenchmarkActionValueTaskSupported() => TestInvoke(x => x.InvokeOnceValueTaskAsync(), UnrollFactor); + public async Task BenchmarkActionValueTaskSupported() => await TestInvoke(x => x.InvokeOnceValueTaskAsync(), UnrollFactor); [Fact] - public void BenchmarkActionRefTypeSupported() => TestInvoke(x => x.InvokeOnceRefType(), UnrollFactor); + public async Task BenchmarkActionRefTypeSupported() => await TestInvoke(x => x.InvokeOnceRefType(), UnrollFactor); [Fact] - public void BenchmarkActionValueTypeSupported() => TestInvoke(x => x.InvokeOnceValueType(), UnrollFactor); + public async Task BenchmarkActionValueTypeSupported() => await TestInvoke(x => x.InvokeOnceValueType(), UnrollFactor); [Fact] - public void BenchmarkActionTaskOfTSupported() => TestInvoke(x => x.InvokeOnceTaskOfTAsync(), UnrollFactor); + public async Task BenchmarkActionTaskOfTSupported() => await TestInvoke(x => x.InvokeOnceTaskOfTAsync(), UnrollFactor); [Fact] - public void BenchmarkActionValueTaskOfTSupported() => TestInvoke(x => x.InvokeOnceValueTaskOfT(), UnrollFactor); + public async Task BenchmarkActionValueTaskOfTSupported() => await TestInvoke(x => x.InvokeOnceValueTaskOfT(), UnrollFactor); +#pragma warning disable xUnit1031 // Do not use blocking task operations in test method [Fact] - public unsafe void BenchmarkActionVoidPointerSupported() => TestInvoke(x => x.InvokeOnceVoidPointerType(), UnrollFactor); + public unsafe void BenchmarkActionVoidPointerSupported() => TestInvoke(x => x.InvokeOnceVoidPointerType(), UnrollFactor).GetAwaiter().GetResult(); +#pragma warning restore xUnit1031 // Do not use blocking task operations in test method // Can't use ref returns in expression, so pass the MethodInfo directly instead. [Fact] - public void BenchmarkActionByRefTypeSupported() => TestInvoke(typeof(BenchmarkAllCases).GetMethod(nameof(BenchmarkAllCases.InvokeOnceByRefType)), UnrollFactor); + public async Task BenchmarkActionByRefTypeSupported() => await TestInvoke(typeof(BenchmarkAllCases).GetMethod(nameof(BenchmarkAllCases.InvokeOnceByRefType)), UnrollFactor); [Fact] - public void BenchmarkActionByRefReadonlyValueTypeSupported() => TestInvoke(typeof(BenchmarkAllCases).GetMethod(nameof(BenchmarkAllCases.InvokeOnceByRefReadonlyType)), UnrollFactor); + public async Task BenchmarkActionByRefReadonlyValueTypeSupported() => await TestInvoke(typeof(BenchmarkAllCases).GetMethod(nameof(BenchmarkAllCases.InvokeOnceByRefReadonlyType)), UnrollFactor); [Fact] public void BenchmarkDifferentPlatformReturnsValidationError() @@ -89,77 +93,79 @@ public void BenchmarkDifferentPlatformReturnsValidationError() } [AssertionMethod] - private void TestInvoke(Expression> methodCall, int unrollFactor) + private async Task TestInvoke(Expression> methodCall, int unrollFactor) { var targetMethod = ((MethodCallExpression)methodCall.Body).Method; var descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod, targetMethod, targetMethod); // Run mode var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor); - TestInvoke(action, unrollFactor, false); + await TestInvoke(action, unrollFactor, false); // Idle mode action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor); - TestInvoke(action, unrollFactor, true); + await TestInvoke(action, unrollFactor, true); // GlobalSetup/GlobalCleanup action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, 1, false); + await TestInvoke(action, 1, false); action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, 1, false); + await TestInvoke(action, 1, false); // GlobalSetup/GlobalCleanup (empty) descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod); action = BenchmarkActionFactory.CreateGlobalSetup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, unrollFactor, true); + await TestInvoke(action, unrollFactor, true); action = BenchmarkActionFactory.CreateGlobalCleanup(descriptor, new BenchmarkAllCases()); - TestInvoke(action, unrollFactor, true); + await TestInvoke(action, unrollFactor, true); } [AssertionMethod] - private void TestInvoke(Expression> methodCall, int unrollFactor) + private async Task TestInvoke(Expression> methodCall, int unrollFactor) { var targetMethod = ((MethodCallExpression)methodCall.Body).Method; - TestInvoke(targetMethod, unrollFactor); + await TestInvoke(targetMethod, unrollFactor); } [AssertionMethod] - private void TestInvoke(MethodInfo targetMethod, int unrollFactor) + private async Task TestInvoke(MethodInfo targetMethod, int unrollFactor) { var descriptor = new Descriptor(typeof(BenchmarkAllCases), targetMethod); // Run mode var action = BenchmarkActionFactory.CreateWorkload(descriptor, new BenchmarkAllCases(), unrollFactor); - TestInvoke(action, unrollFactor, false); + await TestInvoke(action, unrollFactor, false); // Idle mode action = BenchmarkActionFactory.CreateOverhead(descriptor, new BenchmarkAllCases(), unrollFactor); - TestInvoke(action, unrollFactor, true); + await TestInvoke(action, unrollFactor, true); } [AssertionMethod] - private void TestInvoke(BenchmarkAction benchmarkAction, int unrollFactor, bool isIdle) + private async Task TestInvoke(BenchmarkAction benchmarkAction, int unrollFactor, bool isIdle) { try { BenchmarkAllCases.Counter = 0; + IClock clock = new MockClock(Frequency.MHz); + if (isIdle) { - benchmarkAction.InvokeSingle(); + await benchmarkAction.InvokeSingle(); Assert.Equal(0, BenchmarkAllCases.Counter); - benchmarkAction.InvokeUnroll(0); + await benchmarkAction.InvokeUnroll(0, clock); Assert.Equal(0, BenchmarkAllCases.Counter); - benchmarkAction.InvokeUnroll(11); + await benchmarkAction.InvokeUnroll(11, clock); Assert.Equal(0, BenchmarkAllCases.Counter); } else { - benchmarkAction.InvokeSingle(); + await benchmarkAction.InvokeSingle(); Assert.Equal(1, BenchmarkAllCases.Counter); - benchmarkAction.InvokeUnroll(0); + await benchmarkAction.InvokeUnroll(0, clock); Assert.Equal(1, BenchmarkAllCases.Counter); - benchmarkAction.InvokeUnroll(11); + await benchmarkAction.InvokeUnroll(11, clock); Assert.Equal(BenchmarkAllCases.Counter, 1 + unrollFactor * 11); } } diff --git a/tests/BenchmarkDotNet.IntegrationTests/JitOptimizationsTests.cs b/tests/BenchmarkDotNet.IntegrationTests/JitOptimizationsTests.cs index 20c452dbfa..ccb3a9dc9e 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/JitOptimizationsTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/JitOptimizationsTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; using Xunit; @@ -17,12 +18,12 @@ public JitOptimizationsTests(ITestOutputHelper output) } [Fact] - public void UserGetsWarningWhenNonOptimizedDllIsReferenced() + public async Task UserGetsWarningWhenNonOptimizedDllIsReferenced() { var benchmarksWithNonOptimizedDll = CreateBenchmarks(typeof(DisabledOptimizations.OptimizationsDisabledInCsproj)); - var warnings = JitOptimizationsValidator.DontFailOnError.Validate(benchmarksWithNonOptimizedDll).ToArray(); - var criticalErrors = JitOptimizationsValidator.FailOnError.Validate(benchmarksWithNonOptimizedDll).ToArray(); + var warnings = await JitOptimizationsValidator.DontFailOnError.ValidateAsync(benchmarksWithNonOptimizedDll).ToArrayAsync(); + var criticalErrors = await JitOptimizationsValidator.FailOnError.ValidateAsync(benchmarksWithNonOptimizedDll).ToArrayAsync(); Assert.NotEmpty(warnings); Assert.True(warnings.All(error => error.IsCritical == false)); @@ -31,11 +32,11 @@ public void UserGetsWarningWhenNonOptimizedDllIsReferenced() } [Fact] - public void UserGetsNoWarningWhenOnlyOptimizedDllAreReferenced() + public async Task UserGetsNoWarningWhenOnlyOptimizedDllAreReferenced() { var benchmarksWithOptimizedDll = CreateBenchmarks(typeof(EnabledOptimizations.OptimizationsEnabledInCsproj)); - var warnings = JitOptimizationsValidator.DontFailOnError.Validate(benchmarksWithOptimizedDll).ToArray(); + var warnings = await JitOptimizationsValidator.DontFailOnError.ValidateAsync(benchmarksWithOptimizedDll).ToArrayAsync(); if (warnings.Any()) { diff --git a/tests/BenchmarkDotNet.IntegrationTests/RunAsyncTests.cs b/tests/BenchmarkDotNet.IntegrationTests/RunAsyncTests.cs new file mode 100644 index 0000000000..2d7729d998 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/RunAsyncTests.cs @@ -0,0 +1,79 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Tests.XUnit; +using BenchmarkDotNet.Toolchains; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using BenchmarkDotNet.Toolchains.InProcess.NoEmit; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace BenchmarkDotNet.IntegrationTests; + +public class RunAsyncTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) +{ + public static TheoryData GetInProcessToolchains() => new( + [ + new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = false }), + new InProcessNoEmitToolchain(new() { ExecuteOnSeparateThread = false }), + ]); + + private void ExecuteAndAssert(IToolchain toolchain, bool expectsAsync) + { + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + var config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + var valueTask = CanExecuteAsync(config); + Assert.NotEqual(expectsAsync, valueTask.IsCompleted); + context.ExecuteUntilComplete(valueTask); + Assert.True(valueTask.IsCompletedSuccessfully); + } + + [Fact] + public void OutOfProcessBenchmarks() + => ExecuteAndAssert(Job.Default.GetToolchain(), true); + + [Theory] + [MemberData(nameof(GetInProcessToolchains), DisableDiscoveryEnumeration = true)] + public void InProcessSyncBenchmarksRunSync(IToolchain toolchain) + => ExecuteAndAssert(toolchain, false); + + [Theory] + [MemberData(nameof(GetInProcessToolchains), DisableDiscoveryEnumeration = true)] + public void InProcessAsyncBenchmarksRunAsync(IToolchain toolchain) + => ExecuteAndAssert(toolchain, true); + + public class BenchmarkSync + { + [GlobalSetup] public void GlobalSetup() { } + [GlobalCleanup] public void GlobalCleanup() { } + [IterationSetup] public void IterationSetup() { } + [IterationCleanup] public void IterationCleanup() { } + + [Benchmark] public void ReturnVoid() { } + [Benchmark] public object ReturnObject() => new(); + } + + public class BenchmarkAsync + { + [GlobalSetup] public async Task GlobalSetup() => await Task.Yield(); + + [GlobalCleanup] public async Task GlobalCleanup() => await Task.Yield(); + [IterationSetup] public async Task IterationSetup() => await Task.Yield(); + [IterationCleanup] public async Task IterationCleanup() => await Task.Yield(); + + [Benchmark] public async Task AsyncTask() => await Task.Yield(); + + [Benchmark] + public async ValueTask AsyncValueTaskObject() + { + await Task.Yield(); + return new(); + } + } +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/ToolchainTest.cs b/tests/BenchmarkDotNet.IntegrationTests/ToolchainTest.cs index f8cb9686f3..0dfb4f8349 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/ToolchainTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/ToolchainTest.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Jobs; @@ -25,7 +26,7 @@ public GenerateResult GenerateProject(BuildPartition buildPartition, ILogger log { logger.WriteLine("Generating"); Done = true; - return new GenerateResult(null, true, null, Array.Empty()); + return new GenerateResult(null, true, null, []); } } @@ -45,11 +46,11 @@ private class MyExecutor : IExecutor { public bool Done { get; private set; } - public ExecuteResult Execute(ExecuteParameters executeParameters) + public ValueTask ExecuteAsync(ExecuteParameters executeParameters) { executeParameters.Logger.WriteLine("Executing"); Done = true; - return new ExecuteResult(true, 0, default, Array.Empty(), Array.Empty(), Array.Empty(), executeParameters.LaunchIndex); + return new(new ExecuteResult(true, 0, default, [], [], [], executeParameters.LaunchIndex)); } } diff --git a/tests/BenchmarkDotNet.IntegrationTests/ValidatorsTest.cs b/tests/BenchmarkDotNet.IntegrationTests/ValidatorsTest.cs index 061d594e3a..4baa408196 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/ValidatorsTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/ValidatorsTest.cs @@ -58,7 +58,7 @@ private class FailingValidator : IValidator { public bool TreatsWarningsAsErrors => true; - public IEnumerable Validate(ValidationParameters input) + public async IAsyncEnumerable ValidateAsync(ValidationParameters input) { yield return new ValidationError(true, "It just fails"); } diff --git a/tests/BenchmarkDotNet.Tests/Attributes/ParamsAllValuesVerifyTests.cs b/tests/BenchmarkDotNet.Tests/Attributes/ParamsAllValuesVerifyTests.cs index f5d4081a77..4fe922444c 100644 --- a/tests/BenchmarkDotNet.Tests/Attributes/ParamsAllValuesVerifyTests.cs +++ b/tests/BenchmarkDotNet.Tests/Attributes/ParamsAllValuesVerifyTests.cs @@ -35,7 +35,7 @@ public static TheoryData GetBenchmarkTypes() [Theory] [MemberData(nameof(GetBenchmarkTypes))] - public Task BenchmarkShouldProduceSummary(Type benchmarkType) + public async Task BenchmarkShouldProduceSummary(Type benchmarkType) { Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; @@ -47,15 +47,15 @@ public Task BenchmarkShouldProduceSummary(Type benchmarkType) exporter.ExportToLog(summary, logger); var validator = ParamsAllValuesValidator.FailOnError; - var errors = validator.Validate(new ValidationParameters(summary.BenchmarksCases, summary.BenchmarksCases.First().Config)).ToList(); + var errors = await validator.ValidateAsync(new ValidationParameters(summary.BenchmarksCases, summary.BenchmarksCases.First().Config)).ToArrayAsync(); logger.WriteLine(); - logger.WriteLine("Errors: " + errors.Count); + logger.WriteLine("Errors: " + errors.Length); foreach (var error in errors) logger.WriteLineError("* " + error.Message); var settings = VerifyHelper.Create(); settings.UseTextForParameters(benchmarkType.Name); - return Verifier.Verify(logger.GetLog(), settings); + await Verifier.Verify(logger.GetLog(), settings); } public void Dispose() => Thread.CurrentThread.CurrentCulture = initCulture; diff --git a/tests/BenchmarkDotNet.Tests/CodeGeneratorTests.cs b/tests/BenchmarkDotNet.Tests/CodeGeneratorTests.cs index e5f9cbd0d5..bfa00edd24 100644 --- a/tests/BenchmarkDotNet.Tests/CodeGeneratorTests.cs +++ b/tests/BenchmarkDotNet.Tests/CodeGeneratorTests.cs @@ -32,6 +32,7 @@ public void AsyncVoidIsNotSupported(CodeGenBenchmarkRunCallType benchmarkRunCall Assert.Throws(() => CodeGenerator.Generate(new BuildPartition( [new BenchmarkBuildInfo(benchmark, ManualConfig.CreateEmpty().CreateImmutableConfig(), 0, new([]))], BenchmarkRunnerClean.DefaultResolver), + CodeGenEntryPointType.Synchronous, benchmarkRunCallType )); } @@ -52,6 +53,7 @@ public void UsingStatementsInTheAutoGeneratedCodeAreProhibited(CodeGenBenchmarkR var generatedSourceFile = CodeGenerator.Generate(new BuildPartition( [new BenchmarkBuildInfo(benchmark, ManualConfig.CreateEmpty().CreateImmutableConfig(), 0, new([]))], BenchmarkRunnerClean.DefaultResolver), + CodeGenEntryPointType.Synchronous, benchmarkRunCallType ); diff --git a/tests/BenchmarkDotNet.Tests/Engine/EngineMethodImplTests.cs b/tests/BenchmarkDotNet.Tests/Engine/EngineMethodImplTests.cs new file mode 100644 index 0000000000..8c9be9c1c7 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Engine/EngineMethodImplTests.cs @@ -0,0 +1,30 @@ +using System; +using System.Reflection; +using Xunit; + +namespace BenchmarkDotNet.Tests.Engine; + +public class EngineMethodImplTests +{ + [Fact] + public void AllEngineMethodsAreAggressivelyOptimized() + { + AssertMethodsAggressivelyOptimized(typeof(Engines.Engine)); + + static void AssertMethodsAggressivelyOptimized(Type type) + { + foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) + { + Assert.True( + method.MethodImplementationFlags.HasFlag(BenchmarkDotNet.Portability.CodeGenHelper.AggressiveOptimizationOptionForEmit), + $"Method is not aggressively optimized: {method}" + ); + } + + foreach (var nestedType in type.GetNestedTypes()) + { + AssertMethodsAggressivelyOptimized(nestedType); + } + } + } +} diff --git a/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs b/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs index 31d739abe4..db7f411337 100644 --- a/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs +++ b/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Reports; @@ -172,17 +173,18 @@ public void MediumTimeConsumingBenchmarksStartPilotFrom2AndIncrementItWithEveryS private EngineParameters CreateEngineParameters(Job job) { var host = new NoAcknowledgementConsoleHost(); + Func> emptyAction = (_, _) => new(default(ClockSpan)); return new() { - GlobalSetupAction = () => { }, - GlobalCleanupAction = () => { }, + GlobalSetupAction = () => new(), + GlobalCleanupAction = () => new(), Host = host, - OverheadActionUnroll = _ => { }, - OverheadActionNoUnroll = _ => { }, - IterationCleanupAction = () => { }, - IterationSetupAction = () => { }, - WorkloadActionUnroll = _ => { }, - WorkloadActionNoUnroll = _ => { }, + OverheadActionUnroll = emptyAction, + OverheadActionNoUnroll = emptyAction, + IterationCleanupAction = () => new(), + IterationSetupAction = () => new(), + WorkloadActionUnroll = emptyAction, + WorkloadActionNoUnroll = emptyAction, TargetJob = job, BenchmarkName = "", InProcessDiagnoserHandler = new([], host, Diagnosers.RunMode.None, null) diff --git a/tests/BenchmarkDotNet.Tests/Exporters/MarkdownExporterVerifyTests.cs b/tests/BenchmarkDotNet.Tests/Exporters/MarkdownExporterVerifyTests.cs index 9912fcfb1f..7198fab335 100644 --- a/tests/BenchmarkDotNet.Tests/Exporters/MarkdownExporterVerifyTests.cs +++ b/tests/BenchmarkDotNet.Tests/Exporters/MarkdownExporterVerifyTests.cs @@ -33,7 +33,7 @@ public static TheoryData GetGroupBenchmarkTypes() [Theory] [MemberData(nameof(GetGroupBenchmarkTypes))] - public Task GroupExporterTest(Type benchmarkType) + public async Task GroupExporterTest(Type benchmarkType) { Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; @@ -45,15 +45,15 @@ public Task GroupExporterTest(Type benchmarkType) exporter.ExportToLog(summary, logger); var validator = BaselineValidator.FailOnError; - var errors = validator.Validate(new ValidationParameters(summary.BenchmarksCases, summary.BenchmarksCases.First().Config)).ToList(); + var errors = await validator.ValidateAsync(new ValidationParameters(summary.BenchmarksCases, summary.BenchmarksCases.First().Config)).ToArrayAsync(); logger.WriteLine(); - logger.WriteLine("Errors: " + errors.Count); + logger.WriteLine("Errors: " + errors.Length); foreach (var error in errors) logger.WriteLineError("* " + error.Message); var settings = VerifyHelper.Create(); settings.UseTextForParameters(benchmarkType.Name); - return Verifier.Verify(logger.GetLog(), settings); + await Verifier.Verify(logger.GetLog(), settings); } public void Dispose() => Thread.CurrentThread.CurrentCulture = initCulture; diff --git a/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs b/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs index 9893c5ac4d..db4442444b 100644 --- a/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs +++ b/tests/BenchmarkDotNet.Tests/Mocks/MockEngine.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; -using JetBrains.Annotations; using Perfolizer.Horology; using Xunit.Abstractions; @@ -22,17 +22,18 @@ internal MockEngine(ITestOutputHelper output, Job job, Func> emptyAction = (_, _) => new(default(ClockSpan)); Parameters = new EngineParameters { TargetJob = job, - WorkloadActionUnroll = _ => { }, - WorkloadActionNoUnroll = _ => { }, - OverheadActionUnroll = _ => { }, - OverheadActionNoUnroll = _ => { }, - GlobalSetupAction = () => { }, - GlobalCleanupAction = () => { }, - IterationSetupAction = () => { }, - IterationCleanupAction = () => { }, + WorkloadActionUnroll = emptyAction, + WorkloadActionNoUnroll = emptyAction, + OverheadActionUnroll = emptyAction, + OverheadActionNoUnroll = emptyAction, + GlobalSetupAction = () => new(), + GlobalCleanupAction = () => new(), + IterationSetupAction = () => new(), + IterationCleanupAction = () => new(), BenchmarkName = "", Host = default!, InProcessDiagnoserHandler = default! @@ -49,7 +50,7 @@ private Measurement RunIteration(IterationData data) return measurement; } - public RunResults Run() => default; + public ValueTask RunAsync() => new(default(RunResults)); internal List Run(EngineStage stage) { diff --git a/tests/BenchmarkDotNet.Tests/Mocks/Toolchain/MockToolchain.cs b/tests/BenchmarkDotNet.Tests/Mocks/Toolchain/MockToolchain.cs index 365a55cb88..af327f401a 100644 --- a/tests/BenchmarkDotNet.Tests/Mocks/Toolchain/MockToolchain.cs +++ b/tests/BenchmarkDotNet.Tests/Mocks/Toolchain/MockToolchain.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Reports; @@ -12,24 +13,21 @@ namespace BenchmarkDotNet.Tests.Mocks.Toolchain { - public class MockToolchain : IToolchain + public class MockToolchain(Func> measurer) : IToolchain { - public MockToolchain(Func> measurer) - => Executor = new MockExecutor(measurer); - public string Name => nameof(MockToolchain); public IGenerator Generator => new MockGenerator(); public IBuilder Builder => new MockBuilder(); - public IExecutor Executor { get; private set; } + public IExecutor Executor { get; private set; } = new MockExecutor(measurer); public bool IsInProcess => false; - public IEnumerable Validate(BenchmarkCase benchmarkCase, IResolver resolver) => ImmutableArray.Empty; + public IAsyncEnumerable ValidateAsync(BenchmarkCase benchmarkCase, IResolver resolver) => AsyncEnumerable.Empty(); public override string ToString() => GetType().Name; private class MockGenerator : IGenerator { public GenerateResult GenerateProject(BuildPartition buildPartition, ILogger logger, string rootArtifactsFolderPath) - => GenerateResult.Success(ArtifactsPaths.Empty, ImmutableArray.Empty); + => GenerateResult.Success(ArtifactsPaths.Empty, []); } private class MockBuilder : IBuilder @@ -37,13 +35,9 @@ private class MockBuilder : IBuilder public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) => BuildResult.Success(generateResult); } - private class MockExecutor : IExecutor + private class MockExecutor(Func> measurer) : IExecutor { - private readonly Func> measurer; - - public MockExecutor(Func> measurer) => this.measurer = measurer; - - public ExecuteResult Execute(ExecuteParameters executeParameters) => new(measurer(executeParameters.BenchmarkCase)); + public ValueTask ExecuteAsync(ExecuteParameters executeParameters) => new(new ExecuteResult(measurer(executeParameters.BenchmarkCase))); } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Validators/CompilationValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/CompilationValidatorTests.cs index 683e258bbe..1a78b96fdc 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/CompilationValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/CompilationValidatorTests.cs @@ -8,13 +8,14 @@ using BenchmarkDotNet.Validators; using Xunit; using System.Reflection; +using System.Threading.Tasks; namespace BenchmarkDotNet.Tests.Validators { public class CompilationValidatorTests { [Fact] - public void BenchmarkedMethodNameMustNotContainWhitespaces() + public async Task BenchmarkedMethodNameMustNotContainWhitespaces() { Delegate method = BuildDummyMethod("Has Some Whitespaces"); @@ -30,7 +31,7 @@ public void BenchmarkedMethodNameMustNotContainWhitespaces() ) }, config); - var errors = CompilationValidator.FailOnError.Validate(parameters).Select(e => e.Message); + var errors = await CompilationValidator.FailOnError.ValidateAsync(parameters).Select(e => e.Message).ToArrayAsync(); Assert.Contains(errors, s => s.Equals( @@ -38,7 +39,7 @@ public void BenchmarkedMethodNameMustNotContainWhitespaces() } [Fact] - public void BenchmarkedMethodNameMustNotUseCsharpKeywords() + public async Task BenchmarkedMethodNameMustNotUseCsharpKeywords() { Delegate method = BuildDummyMethod("typeof"); @@ -53,7 +54,7 @@ public void BenchmarkedMethodNameMustNotUseCsharpKeywords() config) }, config); - var errors = CompilationValidator.FailOnError.Validate(parameters).Select(e => e.Message); + var errors = await CompilationValidator.FailOnError.ValidateAsync(parameters).Select(e => e.Message).ToArrayAsync(); Assert.Contains(errors, s => s.Equals( @@ -76,9 +77,9 @@ public void BenchmarkedMethodNameMustNotUseCsharpKeywords() [InlineData(typeof(OuterClass.InternalNestedClass), true)] [InlineData(typeof(BenchMarkPublicClass.InternalNestedClass), true)] /* Generics Remaining */ - public void Benchmark_Class_Modifers_Must_Be_Public(Type type, bool hasErrors) + public async Task Benchmark_Class_Modifers_Must_Be_Public(Type type, bool hasErrors) { - var validationErrors = CompilationValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(type)); + var validationErrors = await CompilationValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(type)).ToArrayAsync(); Assert.Equal(hasErrors, validationErrors.Any()); } @@ -86,9 +87,9 @@ public void Benchmark_Class_Modifers_Must_Be_Public(Type type, bool hasErrors) [Theory] [InlineData(typeof(BenchmarkClassWithStaticMethod), true)] [InlineData(typeof(BenchmarkClass), false)] - public void Benchmark_Class_Methods_Must_Be_Non_Static(Type type, bool hasErrors) + public async Task Benchmark_Class_Methods_Must_Be_Non_Static(Type type, bool hasErrors) { - var validationErrors = CompilationValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(type)); + var validationErrors = await CompilationValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(type)).ToArrayAsync(); Assert.Equal(hasErrors, validationErrors.Any()); } @@ -104,14 +105,13 @@ public void Benchmark_Class_Methods_Must_Be_Non_Static(Type type, bool hasErrors [InlineData(typeof(PrivateProtectedNestedClass), true)] [InlineData(typeof(ProtectedInternalClass), true)] [InlineData(typeof(ProtectedInternalClass.ProtectedInternalNestedClass), true)] - public void Benchmark_Class_Generic_Argument_Must_Be_Public(Type type, bool hasErrors) + public async Task Benchmark_Class_Generic_Argument_Must_Be_Public(Type type, bool hasErrors) { // Arrange var constructed = typeof(BenchmarkClass<>).MakeGenericType(type); // Act - var validationErrors = CompilationValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(constructed)) - .ToList(); + var validationErrors = await CompilationValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(constructed)).ToArrayAsync(); // Assert Assert.Equal(hasErrors, validationErrors.Any()); diff --git a/tests/BenchmarkDotNet.Tests/Validators/DeferredExecutionValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/DeferredExecutionValidatorTests.cs index 36a33dc85e..b80b8ab72b 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/DeferredExecutionValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/DeferredExecutionValidatorTests.cs @@ -73,11 +73,11 @@ public class ReturningLazyOfInt [InlineData(typeof(ReturningIQueryable))] [InlineData(typeof(ReturningIQueryableOfInt))] [InlineData(typeof(ReturningLazyOfInt))] - public void DeferredExecutionMeansError(Type returningDeferredExecutionResult) + public async Task DeferredExecutionMeansError(Type returningDeferredExecutionResult) { var benchmarks = BenchmarkConverter.TypeToBenchmarks(returningDeferredExecutionResult); - var validationErrors = DeferredExecutionValidator.FailOnError.Validate(benchmarks).ToArray(); + var validationErrors = await DeferredExecutionValidator.FailOnError.ValidateAsync(benchmarks).ToArrayAsync(); Assert.Equal(5, validationErrors.Count(error => error.IsCritical)); } @@ -107,11 +107,11 @@ public class ReturningDictionary [Theory] [InlineData(typeof(ReturningArray))] [InlineData(typeof(ReturningDictionary))] - public void MaterializedCollectionsAreOk(Type returningMaterializedResult) + public async Task MaterializedCollectionsAreOk(Type returningMaterializedResult) { var benchmarks = BenchmarkConverter.TypeToBenchmarks(returningMaterializedResult); - var validationErrors = DeferredExecutionValidator.FailOnError.Validate(benchmarks).ToArray(); + var validationErrors = await DeferredExecutionValidator.FailOnError.ValidateAsync(benchmarks).ToArrayAsync(); Assert.Empty(validationErrors); } diff --git a/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs index 9b7d8526ea..9408623023 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs @@ -13,9 +13,9 @@ namespace BenchmarkDotNet.Tests.Validators public class ExecutionValidatorTests { [Fact] - public void FailingConstructorsAreDiscovered() + public async Task FailingConstructorsAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(FailingConstructor))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(FailingConstructor))).ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.StartsWith("Unable to create instance of FailingConstructor", validationErrors.Single().Message); @@ -31,9 +31,9 @@ public void NonThrowing() { } } [Fact] - public void FailingGlobalSetupsAreDiscovered() + public async Task FailingGlobalSetupsAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(FailingGlobalSetup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(FailingGlobalSetup))).ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.StartsWith("Failed to execute [GlobalSetup]", validationErrors.Single().Message); @@ -50,9 +50,9 @@ public void NonThrowing() { } } [Fact] - public void FailingGlobalCleanupsAreDiscovered() + public async Task FailingGlobalCleanupsAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(FailingGlobalCleanup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(FailingGlobalCleanup))).ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.StartsWith("Failed to execute [GlobalCleanup]", validationErrors.Single().Message); @@ -69,9 +69,9 @@ public void NonThrowing() { } } [Fact] - public void MultipleGlobalSetupsAreDiscovered() + public async Task MultipleGlobalSetupsAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(MultipleGlobalSetups))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(MultipleGlobalSetups))).ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.StartsWith("Only single [GlobalSetup] method is allowed per type", validationErrors.Single().Message); @@ -90,9 +90,9 @@ public void NonThrowing() { } } [Fact] - public void MultipleGlobalCleanupsAreDiscovered() + public async Task MultipleGlobalCleanupsAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(MultipleGlobalCleanups))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(MultipleGlobalCleanups))).ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.StartsWith("Only single [GlobalCleanup] method is allowed per type", validationErrors.Single().Message); @@ -111,10 +111,10 @@ public void NonThrowing() { } } [Fact] - public void VirtualGlobalSetupsAreSupported() + public async Task VirtualGlobalSetupsAreSupported() { Assert.False(OverridesGlobalSetup.WasCalled); - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(OverridesGlobalSetup))); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(OverridesGlobalSetup))).ToArrayAsync(); Assert.True(OverridesGlobalSetup.WasCalled); Assert.Empty(validationErrors); @@ -138,10 +138,10 @@ public class OverridesGlobalSetup : BaseClassWithThrowingGlobalSetup } [Fact] - public void VirtualGlobalCleanupsAreSupported() + public async Task VirtualGlobalCleanupsAreSupported() { Assert.False(OverridesGlobalCleanup.WasCalled); - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(OverridesGlobalCleanup))); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(OverridesGlobalCleanup))).ToArrayAsync(); Assert.True(OverridesGlobalCleanup.WasCalled); Assert.Empty(validationErrors); @@ -165,9 +165,9 @@ public class OverridesGlobalCleanup : BaseClassWithThrowingGlobalCleanup } [Fact] - public void NonFailingGlobalSetupsAreOmitted() + public async Task NonFailingGlobalSetupsAreOmitted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(GlobalSetupThatRequiresParamsToBeSetFirst))); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(GlobalSetupThatRequiresParamsToBeSetFirst))).ToArrayAsync(); Assert.Empty(validationErrors); } @@ -190,9 +190,9 @@ public void NonThrowing() { } } [Fact] - public void NonFailingGlobalCleanupsAreOmitted() + public async Task NonFailingGlobalCleanupsAreOmitted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(GlobalCleanupThatRequiresParamsToBeSetFirst))); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(GlobalCleanupThatRequiresParamsToBeSetFirst))).ToArrayAsync(); Assert.Empty(validationErrors); } @@ -215,11 +215,11 @@ public void NonThrowing() { } } [Fact] - public void MissingParamsAttributeThatMakesGlobalSetupsFailAreDiscovered() + public async Task MissingParamsAttributeThatMakesGlobalSetupsFailAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError - .Validate(BenchmarkConverter.TypeToBenchmarks(typeof(FailingGlobalSetupWhichShouldHaveHadParamsForField))) - .ToList(); + var validationErrors = await ExecutionValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(FailingGlobalSetupWhichShouldHaveHadParamsForField))) + .ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.StartsWith("Failed to execute [GlobalSetup]", validationErrors.Single().Message); @@ -242,11 +242,11 @@ public void NonThrowing() { } } [Fact] - public void MissingParamsAttributeThatMakesGlobalCleanupsFailAreDiscovered() + public async Task MissingParamsAttributeThatMakesGlobalCleanupsFailAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError - .Validate(BenchmarkConverter.TypeToBenchmarks(typeof(FailingGlobalCleanupWhichShouldHaveHadParamsForField))) - .ToList(); + var validationErrors = await ExecutionValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(FailingGlobalCleanupWhichShouldHaveHadParamsForField))) + .ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.StartsWith("Failed to execute [GlobalCleanup]", validationErrors.Single().Message); @@ -269,11 +269,11 @@ public void NonThrowing() { } } [Fact] - public void NonPublicFieldsWithParamsAreDiscovered() + public async Task NonPublicFieldsWithParamsAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError - .Validate(BenchmarkConverter.TypeToBenchmarks(typeof(NonPublicFieldWithParams))) - .ToList(); + var validationErrors = await ExecutionValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(NonPublicFieldWithParams))) + .ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.StartsWith("Fields marked with [Params] must be public", validationErrors.Single().Message); @@ -310,9 +310,9 @@ public void NonThrowing() { } } [Fact] - public void NonFailingBenchmarksAreOmitted() + public async Task NonFailingBenchmarksAreOmitted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(NonFailingBenchmark))); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(NonFailingBenchmark))).ToArrayAsync(); Assert.Empty(validationErrors); } @@ -324,9 +324,9 @@ public void NonThrowing() { } } [Fact] - public void FailingBenchmarksAreDiscovered() + public async Task FailingBenchmarksAreDiscovered() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(FailingBenchmark))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(FailingBenchmark))).ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.Contains(validationErrors, error => error.Message.Contains("This benchmark throws")); @@ -339,9 +339,9 @@ public class FailingBenchmark } [Fact] - public void MultipleParamsDoNotMultiplyGlobalSetup() + public async Task MultipleParamsDoNotMultiplyGlobalSetup() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(MultipleParamsAndSingleGlobalSetup))); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(MultipleParamsAndSingleGlobalSetup))).ToArrayAsync(); Assert.Empty(validationErrors); } @@ -360,9 +360,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncTaskGlobalSetupIsExecuted() + public async Task AsyncTaskGlobalSetupIsExecuted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncTaskGlobalSetup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncTaskGlobalSetup))).ToArrayAsync(); Assert.True(AsyncTaskGlobalSetup.WasCalled); Assert.Empty(validationErrors); @@ -385,9 +385,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncTaskGlobalCleanupIsExecuted() + public async Task AsyncTaskGlobalCleanupIsExecuted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncTaskGlobalCleanup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncTaskGlobalCleanup))).ToArrayAsync(); Assert.True(AsyncTaskGlobalCleanup.WasCalled); Assert.Empty(validationErrors); @@ -410,9 +410,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncGenericTaskGlobalSetupIsExecuted() + public async Task AsyncGenericTaskGlobalSetupIsExecuted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericTaskGlobalSetup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericTaskGlobalSetup))).ToArrayAsync(); Assert.True(AsyncGenericTaskGlobalSetup.WasCalled); Assert.Empty(validationErrors); @@ -437,9 +437,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncGenericTaskGlobalCleanupIsExecuted() + public async Task AsyncGenericTaskGlobalCleanupIsExecuted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericTaskGlobalCleanup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericTaskGlobalCleanup))).ToArrayAsync(); Assert.True(AsyncGenericTaskGlobalCleanup.WasCalled); Assert.Empty(validationErrors); @@ -464,9 +464,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncValueTaskGlobalSetupIsExecuted() + public async Task AsyncValueTaskGlobalSetupIsExecuted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncValueTaskGlobalSetup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncValueTaskGlobalSetup))).ToArrayAsync(); Assert.True(AsyncValueTaskGlobalSetup.WasCalled); Assert.Empty(validationErrors); @@ -489,9 +489,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncValueTaskGlobalCleanupIsExecuted() + public async Task AsyncValueTaskGlobalCleanupIsExecuted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncValueTaskGlobalCleanup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncValueTaskGlobalCleanup))).ToArrayAsync(); Assert.True(AsyncValueTaskGlobalCleanup.WasCalled); Assert.Empty(validationErrors); @@ -514,9 +514,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncGenericValueTaskGlobalSetupIsExecuted() + public async Task AsyncGenericValueTaskGlobalSetupIsExecuted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericValueTaskGlobalSetup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericValueTaskGlobalSetup))).ToArrayAsync(); Assert.True(AsyncGenericValueTaskGlobalSetup.WasCalled); Assert.Empty(validationErrors); @@ -541,9 +541,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncGenericValueTaskGlobalCleanupIsExecuted() + public async Task AsyncGenericValueTaskGlobalCleanupIsExecuted() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericValueTaskGlobalCleanup))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericValueTaskGlobalCleanup))).ToArrayAsync(); Assert.True(AsyncGenericValueTaskGlobalCleanup.WasCalled); Assert.Empty(validationErrors); @@ -583,9 +583,9 @@ private class ValueTaskSource : IValueTaskSource, IValueTaskSource } [Fact] - public void AsyncValueTaskBackedByIValueTaskSourceIsAwaitedProperly() + public async Task AsyncValueTaskBackedByIValueTaskSourceIsAwaitedProperly() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncValueTaskSource))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncValueTaskSource))).ToArrayAsync(); Assert.True(AsyncValueTaskSource.WasCalled); Assert.Empty(validationErrors); @@ -614,9 +614,9 @@ public void NonThrowing() { } } [Fact] - public void AsyncGenericValueTaskBackedByIValueTaskSourceIsAwaitedProperly() + public async Task AsyncGenericValueTaskBackedByIValueTaskSourceIsAwaitedProperly() { - var validationErrors = ExecutionValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericValueTaskSource))).ToList(); + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(AsyncGenericValueTaskSource))).ToArrayAsync(); Assert.True(AsyncGenericValueTaskSource.WasCalled); Assert.Empty(validationErrors); diff --git a/tests/BenchmarkDotNet.Tests/Validators/ParamsValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/ParamsValidatorTests.cs index c4b4987176..8960cc2a57 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/ParamsValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/ParamsValidatorTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; @@ -22,13 +23,13 @@ public ParamsValidatorTests(ITestOutputHelper output) this.output = output; } - private void Check(params string[] messageParts) + private async ValueTask Check(params string[] messageParts) { var typeToBenchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(T)); Assert.NotEmpty(typeToBenchmarks.BenchmarksCases); - var validationErrors = ParamsValidator.FailOnError.Validate(typeToBenchmarks).ToList(); - output.WriteLine("Number of validation errors: " + validationErrors.Count); + var validationErrors = await ParamsValidator.FailOnError.ValidateAsync(typeToBenchmarks).ToArrayAsync(); + output.WriteLine("Number of validation errors: " + validationErrors.Length); foreach (var error in validationErrors) output.WriteLine("* " + error.Message); @@ -41,35 +42,35 @@ private void Check(params string[] messageParts) private const string Pa = "[ParamsAllValues]"; private const string Ps = "[ParamsSource]"; - [Fact] public void Const1Test() => Check(nameof(Const1.Input), "constant", P); - [Fact] public void Const2Test() => Check(nameof(Const2.Input), "constant", Pa); - [Fact] public void Const3Test() => Check(nameof(Const3.Input), "constant", Ps); - [Fact] public void StaticReadonly1Test() => Check(nameof(StaticReadonly1.Input), "readonly", P); - [Fact] public void StaticReadonly2Test() => Check(nameof(StaticReadonly2.Input), "readonly", Pa); - [Fact] public void StaticReadonly3Test() => Check(nameof(StaticReadonly3.Input), "readonly", Ps); - [Fact] public void NonStaticReadonly1Test() => Check(nameof(NonStaticReadonly1.Input), "readonly", P); - [Fact] public void NonStaticReadonly2Test() => Check(nameof(NonStaticReadonly2.Input), "readonly", Pa); - [Fact] public void NonStaticReadonly3Test() => Check(nameof(NonStaticReadonly3.Input), "readonly", Ps); - [Fact] public void FieldMultiple1Test() => Check(nameof(FieldMultiple1.Input), "single attribute", P, Pa); - [Fact] public void FieldMultiple2Test() => Check(nameof(FieldMultiple2.Input), "single attribute", P, Ps); - [Fact] public void FieldMultiple3Test() => Check(nameof(FieldMultiple3.Input), "single attribute", Pa, Ps); - [Fact] public void FieldMultiple4Test() => Check(nameof(FieldMultiple4.Input), "single attribute", P, Pa, Ps); - [Fact] public void PropMultiple1Test() => Check(nameof(PropMultiple1.Input), "single attribute", P, Pa); - [Fact] public void PropMultiple2Test() => Check(nameof(PropMultiple2.Input), "single attribute", P, Ps); - [Fact] public void PropMultiple3Test() => Check(nameof(PropMultiple3.Input), "single attribute", Pa, Ps); - [Fact] public void PropMultiple4Test() => Check(nameof(PropMultiple4.Input), "single attribute", P, Pa, Ps); - [Fact] public void PrivateSetter1Test() => Check(nameof(PrivateSetter1.Input), "setter is not public", P); - [Fact] public void PrivateSetter2Test() => Check(nameof(PrivateSetter2.Input), "setter is not public", Pa); - [Fact] public void PrivateSetter3Test() => Check(nameof(PrivateSetter3.Input), "setter is not public", Ps); - [Fact] public void NoSetter1Test() => Check(nameof(NoSetter1.Input), "no setter", P); - [Fact] public void NoSetter2Test() => Check(nameof(NoSetter2.Input), "no setter", Pa); - [Fact] public void NoSetter3Test() => Check(nameof(NoSetter3.Input), "no setter", Ps); - [Fact] public void InternalField1Test() => Check(nameof(InternalField1.Input), "it's not public", P); - [Fact] public void InternalField2Test() => Check(nameof(InternalField2.Input), "it's not public", Pa); - [Fact] public void InternalField3Test() => Check(nameof(InternalField3.Input), "it's not public", Ps); - [Fact] public void InternalProp1Test() => Check(nameof(InternalProp1.Input), "setter is not public", P); - [Fact] public void InternalProp2Test() => Check(nameof(InternalProp2.Input), "setter is not public", Pa); - [Fact] public void InternalProp3Test() => Check(nameof(InternalProp3.Input), "setter is not public", Ps); + [Fact] public async Task Const1Test() => await Check(nameof(Const1.Input), "constant", P); + [Fact] public async Task Const2Test() => await Check(nameof(Const2.Input), "constant", Pa); + [Fact] public async Task Const3Test() => await Check(nameof(Const3.Input), "constant", Ps); + [Fact] public async Task StaticReadonly1Test() => await Check(nameof(StaticReadonly1.Input), "readonly", P); + [Fact] public async Task StaticReadonly2Test() => await Check(nameof(StaticReadonly2.Input), "readonly", Pa); + [Fact] public async Task StaticReadonly3Test() => await Check(nameof(StaticReadonly3.Input), "readonly", Ps); + [Fact] public async Task NonStaticReadonly1Test() => await Check(nameof(NonStaticReadonly1.Input), "readonly", P); + [Fact] public async Task NonStaticReadonly2Test() => await Check(nameof(NonStaticReadonly2.Input), "readonly", Pa); + [Fact] public async Task NonStaticReadonly3Test() => await Check(nameof(NonStaticReadonly3.Input), "readonly", Ps); + [Fact] public async Task FieldMultiple1Test() => await Check(nameof(FieldMultiple1.Input), "single attribute", P, Pa); + [Fact] public async Task FieldMultiple2Test() => await Check(nameof(FieldMultiple2.Input), "single attribute", P, Ps); + [Fact] public async Task FieldMultiple3Test() => await Check(nameof(FieldMultiple3.Input), "single attribute", Pa, Ps); + [Fact] public async Task FieldMultiple4Test() => await Check(nameof(FieldMultiple4.Input), "single attribute", P, Pa, Ps); + [Fact] public async Task PropMultiple1Test() => await Check(nameof(PropMultiple1.Input), "single attribute", P, Pa); + [Fact] public async Task PropMultiple2Test() => await Check(nameof(PropMultiple2.Input), "single attribute", P, Ps); + [Fact] public async Task PropMultiple3Test() => await Check(nameof(PropMultiple3.Input), "single attribute", Pa, Ps); + [Fact] public async Task PropMultiple4Test() => await Check(nameof(PropMultiple4.Input), "single attribute", P, Pa, Ps); + [Fact] public async Task PrivateSetter1Test() => await Check(nameof(PrivateSetter1.Input), "setter is not public", P); + [Fact] public async Task PrivateSetter2Test() => await Check(nameof(PrivateSetter2.Input), "setter is not public", Pa); + [Fact] public async Task PrivateSetter3Test() => await Check(nameof(PrivateSetter3.Input), "setter is not public", Ps); + [Fact] public async Task NoSetter1Test() => await Check(nameof(NoSetter1.Input), "no setter", P); + [Fact] public async Task NoSetter2Test() => await Check(nameof(NoSetter2.Input), "no setter", Pa); + [Fact] public async Task NoSetter3Test() => await Check(nameof(NoSetter3.Input), "no setter", Ps); + [Fact] public async Task InternalField1Test() => await Check(nameof(InternalField1.Input), "it's not public", P); + [Fact] public async Task InternalField2Test() => await Check(nameof(InternalField2.Input), "it's not public", Pa); + [Fact] public async Task InternalField3Test() => await Check(nameof(InternalField3.Input), "it's not public", Ps); + [Fact] public async Task InternalProp1Test() => await Check(nameof(InternalProp1.Input), "setter is not public", P); + [Fact] public async Task InternalProp2Test() => await Check(nameof(InternalProp2.Input), "setter is not public", Pa); + [Fact] public async Task InternalProp3Test() => await Check(nameof(InternalProp3.Input), "setter is not public", Ps); public class Base { @@ -323,9 +324,9 @@ public class PropMultiple4 : Base #if NET5_0_OR_GREATER - [Fact] public void InitOnly1Test() => Check(nameof(InitOnly1.Input), "init-only", P); - [Fact] public void InitOnly2Test() => Check(nameof(InitOnly2.Input), "init-only", Pa); - [Fact] public void InitOnly3Test() => Check(nameof(InitOnly3.Input), "init-only", Ps); + [Fact] public async Task InitOnly1Test() => await Check(nameof(InitOnly1.Input), "init-only", P); + [Fact] public async Task InitOnly2Test() => await Check(nameof(InitOnly2.Input), "init-only", Pa); + [Fact] public async Task InitOnly3Test() => await Check(nameof(InitOnly3.Input), "init-only", Ps); #pragma warning disable BDN1206 public class InitOnly1 : Base diff --git a/tests/BenchmarkDotNet.Tests/Validators/ReturnValueValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/ReturnValueValidatorTests.cs index 40acc04fdc..72f85d4bd4 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/ReturnValueValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/ReturnValueValidatorTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; @@ -14,9 +15,9 @@ public class ReturnValueValidatorTests private const string ErrorMessagePrefix = "Inconsistent benchmark return values"; [Fact] - public void ThrowingBenchmarksAreDiscovered() + public async Task ThrowingBenchmarksAreDiscovered() { - var validationErrors = ReturnValueValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(ThrowingBenchmark))).ToList(); + var validationErrors = await ReturnValueValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(ThrowingBenchmark))).ToArrayAsync(); Assert.Single(validationErrors); Assert.Contains("Oops, sorry", validationErrors.Single().Message); @@ -29,9 +30,9 @@ public class ThrowingBenchmark } [Fact] - public void InconsistentReturnValuesAreDiscovered() + public async Task InconsistentReturnValuesAreDiscovered() { - var validationErrors = AssertInconsistent(); + var validationErrors = await AssertInconsistent(); Assert.Single(validationErrors); } @@ -45,9 +46,9 @@ public class InconsistentResults } [Fact] - public void NoDuplicateResultsArePrinted() + public async Task NoDuplicateResultsArePrinted() { - var validationErrors = AssertInconsistent(); + var validationErrors = await AssertInconsistent(); Assert.Single(validationErrors); var allInstancesOfFoo = Regex.Matches(validationErrors.Single().Message, @"\bFoo\b"); @@ -65,8 +66,8 @@ public class InconsistentResultsWithMultipleJobs } [Fact] - public void ConsistentReturnValuesAreOmitted() - => AssertConsistent(); + public async Task ConsistentReturnValuesAreOmitted() + => await AssertConsistent(); public class ConsistentResults { @@ -78,8 +79,8 @@ public class ConsistentResults } [Fact] - public void BenchmarksWithOnlyVoidMethodsAreOmitted() - => AssertConsistent(); + public async Task BenchmarksWithOnlyVoidMethodsAreOmitted() + => await AssertConsistent(); public class VoidMethods { @@ -91,8 +92,8 @@ public void Bar() { } } [Fact] - public void VoidMethodsAreIgnored() - => AssertConsistent(); + public async Task VoidMethodsAreIgnored() + => await AssertConsistent(); public class ConsistentResultsWithVoidMethod { @@ -107,8 +108,8 @@ public void Baz() { } } [Fact] - public void ConsistentReturnValuesInParameterGroupAreOmitted() - => AssertConsistent(); + public async Task ConsistentReturnValuesInParameterGroupAreOmitted() + => await AssertConsistent(); public class ConsistentResultsPerParameterGroup { @@ -123,10 +124,10 @@ public class ConsistentResultsPerParameterGroup } [Fact] - public void InconsistentReturnValuesInParameterGroupAreDetected() + public async Task InconsistentReturnValuesInParameterGroupAreDetected() { - var validationErrors = AssertInconsistent(); - Assert.Equal(2, validationErrors.Count); + var validationErrors = await AssertInconsistent(); + Assert.Equal(2, validationErrors.Length); } public class InconsistentResultsPerParameterGroup @@ -142,8 +143,8 @@ public class InconsistentResultsPerParameterGroup } [Fact] - public void ConsistentCollectionsAreOmitted() - => AssertConsistent(); + public async Task ConsistentCollectionsAreOmitted() + => await AssertConsistent(); public class ConsistentCollectionReturnType { @@ -155,8 +156,8 @@ public class ConsistentCollectionReturnType } [Fact] - public void InconsistentCollectionsAreDetected() - => AssertInconsistent(); + public async Task InconsistentCollectionsAreDetected() + => await AssertInconsistent(); public class InconsistentCollectionReturnType { @@ -168,8 +169,8 @@ public class InconsistentCollectionReturnType } [Fact] - public void ConsistentDictionariesAreOmitted() - => AssertConsistent(); + public async Task ConsistentDictionariesAreOmitted() + => await AssertConsistent(); public class ConsistentDictionaryReturnType { @@ -181,8 +182,8 @@ public class ConsistentDictionaryReturnType } [Fact] - public void InconsistentDictionariesAreDetected() - => AssertInconsistent(); + public async Task InconsistentDictionariesAreDetected() + => await AssertInconsistent(); public class InconsistentDictionaryReturnType { @@ -194,8 +195,8 @@ public class InconsistentDictionaryReturnType } [Fact] - public void ConsistentCustomEquatableImplementationIsOmitted() - => AssertConsistent(); + public async Task ConsistentCustomEquatableImplementationIsOmitted() + => await AssertConsistent(); public class ConsistentCustomEquatableReturnType { @@ -207,8 +208,8 @@ public class ConsistentCustomEquatableReturnType } [Fact] - public void InconsistentCustomEquatableImplementationIsDetected() - => AssertInconsistent(); + public async Task InconsistentCustomEquatableImplementationIsDetected() + => await AssertInconsistent(); public class InconsistentCustomEquatableReturnType { @@ -238,8 +239,8 @@ public class CustomEquatableB : IEquatable } [Fact] - public void ConsistentBenchmarksAlteringParameterAreOmitted() - => AssertConsistent(); + public async Task ConsistentBenchmarksAlteringParameterAreOmitted() + => await AssertConsistent(); public class ConsistentAlterParam { @@ -253,16 +254,16 @@ public class ConsistentAlterParam public int Bar() => ++Value; } - private static void AssertConsistent() + private static async Task AssertConsistent() { - var validationErrors = ReturnValueValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(TBenchmark))).ToList(); + var validationErrors = await ReturnValueValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(TBenchmark))).ToArrayAsync(); Assert.Empty(validationErrors); } - private static List AssertInconsistent() + private static async Task AssertInconsistent() { - var validationErrors = ReturnValueValidator.FailOnError.Validate(BenchmarkConverter.TypeToBenchmarks(typeof(TBenchmark))).ToList(); + var validationErrors = await ReturnValueValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(TBenchmark))).ToArrayAsync(); Assert.NotEmpty(validationErrors); Assert.All(validationErrors, error => Assert.StartsWith(ErrorMessagePrefix, error.Message)); diff --git a/tests/BenchmarkDotNet.Tests/Validators/RuntimeValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/RuntimeValidatorTests.cs index a89333ea31..2e01098498 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/RuntimeValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/RuntimeValidatorTests.cs @@ -6,6 +6,7 @@ using BenchmarkDotNet.Toolchains.CsProj; using BenchmarkDotNet.Validators; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace BenchmarkDotNet.Tests.Validators; @@ -13,7 +14,7 @@ namespace BenchmarkDotNet.Tests.Validators; public class RuntimeValidatorTests { [Fact] - public void SameRuntime_Should_Success() + public async Task SameRuntime_Should_Success() { // Arrange var config = new TestConfig1().CreateImmutableConfig(); @@ -21,14 +22,14 @@ public void SameRuntime_Should_Success() var parameters = new ValidationParameters(runInfo.BenchmarksCases, config); // Act - var errors = RuntimeValidator.DontFailOnError.Validate(parameters).Select(e => e.Message).ToArray(); + var errors = await RuntimeValidator.DontFailOnError.ValidateAsync(parameters).Select(e => e.Message).ToArrayAsync(); // Assert Assert.Empty(errors); } [Fact] - public void NullRuntimeMixed_Should_Failed() + public async Task NullRuntimeMixed_Should_Failed() { // Arrange var config = new TestConfig2().CreateImmutableConfig(); @@ -36,7 +37,7 @@ public void NullRuntimeMixed_Should_Failed() var parameters = new ValidationParameters(runInfo.BenchmarksCases, config); // Act - var errors = RuntimeValidator.DontFailOnError.Validate(parameters).Select(e => e.Message).ToArray(); + var errors = await RuntimeValidator.DontFailOnError.ValidateAsync(parameters).Select(e => e.Message).ToArrayAsync(); // Assert { @@ -50,7 +51,7 @@ public void NullRuntimeMixed_Should_Failed() } [Fact] - public void NotNullRuntimeOnly_Should_Success() + public async Task NotNullRuntimeOnly_Should_Success() { // Arrange var config = new TestConfig3().CreateImmutableConfig(); @@ -58,7 +59,7 @@ public void NotNullRuntimeOnly_Should_Success() var parameters = new ValidationParameters(runInfo.BenchmarksCases, config); // Act - var errors = RuntimeValidator.DontFailOnError.Validate(parameters).Select(e => e.Message).ToArray(); + var errors = await RuntimeValidator.DontFailOnError.ValidateAsync(parameters).Select(e => e.Message).ToArrayAsync(); // Assert Assert.Empty(errors); diff --git a/tests/BenchmarkDotNet.Tests/Validators/SetupCleanupValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/SetupCleanupValidatorTests.cs index 43d6b51f5c..3dfc466f00 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/SetupCleanupValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/SetupCleanupValidatorTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using BenchmarkDotNet.Validators; @@ -16,10 +17,10 @@ [Benchmark] public void Benchmark() { } } [Fact] - public void InvalidGlobalSetupTooManyBlankTargets() + public async Task InvalidGlobalSetupTooManyBlankTargets() { - var validationErrors = SetupCleanupValidator.FailOnError.Validate( - BenchmarkConverter.TypeToBenchmarks(typeof(BlankTargetClass))).ToArray(); + var validationErrors = await SetupCleanupValidator.FailOnError.ValidateAsync( + BenchmarkConverter.TypeToBenchmarks(typeof(BlankTargetClass))).ToArrayAsync(); var count = validationErrors.Count(v => v.IsCritical && v.Message.Contains("[GlobalSetupAttribute]") && v.Message.Contains("Blank")); @@ -35,10 +36,10 @@ [Benchmark] public void Benchmark() { } } [Fact] - public void InvalidGlobalSetupTooManyExplicitTargets() + public async Task InvalidGlobalSetupTooManyExplicitTargets() { - var validationErrors = SetupCleanupValidator.FailOnError.Validate( - BenchmarkConverter.TypeToBenchmarks(typeof(ExplicitTargetClass))).ToArray(); + var validationErrors = await SetupCleanupValidator.FailOnError.ValidateAsync( + BenchmarkConverter.TypeToBenchmarks(typeof(ExplicitTargetClass))).ToArrayAsync(); var count = validationErrors.Count(v => v.IsCritical && v.Message.Contains("[GlobalSetupAttribute]") && v.Message.Contains("Target = Benchmark"));