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